diff --git a/.github/ISSUE_TEMPLATE/Bug_report.md b/.github/ISSUE_TEMPLATE/Bug_report.md index de564e58..2071195d 100644 --- a/.github/ISSUE_TEMPLATE/Bug_report.md +++ b/.github/ISSUE_TEMPLATE/Bug_report.md @@ -3,28 +3,63 @@ name: Bug report about: Report a bug for this project --- +<!-- + For community support and other discussions, you are welcome to visit us on our community channels listed at https://mailcow.github.io/mailcow-dockerized-docs/#community-support. For professional commercial support, please check out https://mailcow.github.io/mailcow-dockerized-docs/#commercial-support instead +--> -**README and remove me** -For community support and other discussion, you are welcome to visit and stay with us @ Freenode, #mailcow -Answering can take a few seconds up to many hours, please be patient. -Commercial support, including a ticket system, can be found @ https://www.servercow.de/mailcow#support - we are also available via Telegram. \o/ +**Prior to placing the issue, please check following:** *(fill out each checkbox with a `X` once done)* +- [ ] I understand that not following below instructions might result in immediate closing and deletion of my issue. +- [ ] I have understood that answers are voluntary and community-driven, and not commercial support. +- [ ] I have verified that my issue has not been already answered in the past. I also checked previous [issues](https://github.com/mailcow/mailcow-dockerized/issues). -**Describe the bug, try to make it reproducible** -A clear and concise description of what the bug is. How can it be reproduced? -If applicable, add screenshots to help explain your problem. Very useful for bugs in mailcow UI. +--- -**System information and quick debugging** -General logs: -- Please take a look at the [documentation](https://mailcow.github.io/mailcow-dockerized-docs/debug-logs/). +**Description of the bug**: What kind of issue have you *exactly* come across? +<!-- + This should be a clear and concise description of what the bug is. What EXACTLY does happen? + If applicable, add screenshots to help explain your problem. Very useful for bugs in mailcow UI. + Write your detailed description below. +--> + +My issue is... + +**Reproduction of said bug**: How *exactly* do you reproduce the bug? +<!-- + Here it is really helpful to know how exactly you are able to reproduce the reported issue. + Meaning: What are the exact steps - one by one - to get the above described behavior. + Screenshots can be added, if helpful. Add the text below. +--> + +1. I go to... +2. And then to... +3. But once I do... + +__I have tried or I do...__ *(fill out each checkbox with a `X` if applicable)* +- [ ] In case of WebUI issue, I have tried clearing the browser cache and the issue persists. +- [ ] I do run mailcow on a Synology, QNAP or any other sort of NAS. + +**System information** +<!-- + In this stage we would kindly ask you to attach logs or general system information about your setup. + Please carefully read the questions and instructions below. +--> Further information (where applicable): - - Your OS (is Apparmor or SELinux active?) - - Your virtualization technology (KVM/QEMU, Xen, VMware, VirtualBox etc.) - - Your server/VM specifications (Memory, CPU Cores) - - Don't try to run mailcow on a Synology or QNAP NAS, do you? - - Docker and Docker Compose versions - - Output of `git diff origin/master`, any other changes to the code? + +| Question | Answer | +| --- | --- | +| My operating system | I_DO_REPLY_HERE | +| Is Apparmor, SELinux or similar active? | I_DO_REPLY_HERE | +| Virtualization technlogy (KVM, VMware, Xen, etc) | I_DO_REPLY_HERE | +| Server/VM specifications (Memory, CPU Cores) | I_DO_REPLY_HERE | +| Docker Version (`docker version`) | I_DO_REPLY_HERE | +| Docker-Compose Version (`docker-compose version`) | I_DO_REPLY_HERE | +| Reverse proxy (custom solution) | I_DO_REPLY_HERE | + +Further notes: + - Output of `git diff origin/master`, any other changes to the code? If so, please post them. - All third-party firewalls and custom iptables rules are unsupported. Please check the Docker docs about how to use Docker with your own ruleset. Nevertheless, iptabels output can help _us_ to help _you_: `iptables -L -vn`, `ip6tables -L -vn`, `iptables -L -vn -t nat` and `ip6tables -L -vn -t nat ` - - Reverse proxy? If you think this problem is related to your reverse proxy, please post your configuration. - - Browser (if it's a Web UI issue) - please clean your browser cache and try again, problem persists? - Check `docker exec -it $(docker ps -qf name=acme-mailcow) dig +short stackoverflow.com @172.22.1.254` (set the IP accordingly, if you changed the internal mailcow network) and `docker exec -it $(docker ps -qf name=acme-mailcow) dig +short stackoverflow.com @1.1.1.1` - output? Timeout? + + General logs: +- Please take a look at the [official documentation](https://mailcow.github.io/mailcow-dockerized-docs/debug-logs/). diff --git a/.gitignore b/.gitignore index 624e1c06..1e2733f5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,12 @@ rebuild-images.sh data/conf/sogo/sieve.creds +data/conf/phpfpm/sogo-sso/sogo-sso.pass data/conf/dovecot/dovecot-master.passwd data/conf/dovecot/dovecot-master.userdb mailcow.conf mailcow.conf_backup data/conf/nginx/*.active +data/conf/postfix/extra.cf data/conf/postfix/sql data/conf/postfix/allow_mailcow_local.regexp data/conf/dovecot/sql @@ -24,11 +26,15 @@ data/conf/nginx/*.custom data/conf/nginx/*.bak data/conf/dovecot/acl_anyone data/conf/dovecot/mail_plugins* +data/conf/dovecot/sogo-sso.conf data/conf/dovecot/extra.conf +data/conf/dovecot/shared_namespace.conf data/conf/rspamd/custom/* data/conf/portainer/ data/gitea/ data/gogs/ data/conf/sogo/plist_ldap +update_diffs/ .github/ docker-compose.override.yml +refresh_images.sh diff --git a/README.md b/README.md index ec500d9c..356a2f8a 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ ## Want to support mailcow? -Donate via **PayPal** [](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=JWBSYHF4SMC68) or via **Liberapay** [](https://liberapay.com/mailcow) +Please [consider a support contract (around 30 € per month) with Servercow](https://www.servercow.de/mailcow#support) to support further development. _We_ support _you_ while _you_ support _us_. :) Or just spread the word: moo. diff --git a/data/Dockerfiles/acme/Dockerfile b/data/Dockerfiles/acme/Dockerfile index a17064fe..dd872dff 100644 --- a/data/Dockerfiles/acme/Dockerfile +++ b/data/Dockerfiles/acme/Dockerfile @@ -1,8 +1,9 @@ -FROM alpine:3.9 +FROM alpine:3.10 LABEL maintainer "Andre Peters <andre.peters@servercow.de>" -RUN apk add --update --no-cache \ +RUN apk upgrade --no-cache \ + && apk add --update --no-cache \ bash \ curl \ openssl \ @@ -12,9 +13,9 @@ RUN apk add --update --no-cache \ redis \ tini \ tzdata \ - py-pip \ - && pip install --upgrade pip \ - && pip install acme-tiny + python3 \ + && python3 -m pip install --upgrade pip \ + && python3 -m pip install acme-tiny COPY docker-entrypoint.sh /srv/docker-entrypoint.sh COPY expand6.sh /srv/expand6.sh diff --git a/data/Dockerfiles/acme/docker-entrypoint.sh b/data/Dockerfiles/acme/docker-entrypoint.sh index bb9a5a53..a06ce01c 100755 --- a/data/Dockerfiles/acme/docker-entrypoint.sh +++ b/data/Dockerfiles/acme/docker-entrypoint.sh @@ -5,6 +5,21 @@ exec 5>&1 # Thanks to https://github.com/cvmiller -> https://github.com/cvmiller/expand6 source /srv/expand6.sh +# Skipping IP check when we like to live dangerously +if [[ "${SKIP_IP_CHECK}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then + SKIP_IP_CHECK=y +fi + +# Skipping HTTP check when we like to live dangerously +if [[ "${SKIP_HTTP_VERIFICATION}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then + SKIP_HTTP_VERIFICATION=y +fi + +# Request certificate for MAILCOW_HOSTNAME ony +if [[ "${ONLY_MAILCOW_HOSTNAME}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then + ONLY_MAILCOW_HOSTNAME=y +fi + log_f() { if [[ ${2} == "no_nl" ]]; then echo -n "$(date) - ${1}" @@ -42,7 +57,6 @@ mkdir -p ${ACME_BASE}/acme [[ -f ${ACME_BASE}/acme/private/privkey.pem ]] && mv ${ACME_BASE}/acme/private/privkey.pem ${ACME_BASE}/acme/key.pem [[ -f ${ACME_BASE}/acme/private/account.key ]] && mv ${ACME_BASE}/acme/private/account.key ${ACME_BASE}/acme/account.pem - reload_configurations(){ # Reading container IDs # Wrapping as array to ensure trimmed content when calling $NGINX etc. @@ -118,21 +132,25 @@ get_ipv6(){ } verify_challenge_path(){ + if [[ ${SKIP_HTTP_VERIFICATION} == "y" ]]; then + echo '(skipping check, returning 0)' + return 0 + fi # verify_challenge_path URL 4|6 - RAND_FILE=${RANDOM}${RANDOM}${RANDOM} - touch /var/www/acme/${RAND_FILE} - if [[ "$(curl -${2} http://${1}/.well-known/acme-challenge/${RAND_FILE} --write-out %{http_code} --silent --output /dev/null)" =~ ^(2|3) ]]; then - rm /var/www/acme/${RAND_FILE} + RANDOM_N=${RANDOM}${RANDOM}${RANDOM} + echo ${RANDOM_N} > /var/www/acme/${RANDOM_N} + if [[ "$(curl --insecure -${2} -L http://${1}/.well-known/acme-challenge/${RANDOM_N} --silent)" == "${RANDOM_N}" ]]; then + rm /var/www/acme/${RANDOM_N} return 0 else - rm /var/www/acme/${RAND_FILE} + rm /var/www/acme/${RANDOM_N} return 1 fi } [[ ! -f ${ACME_BASE}/dhparams.pem ]] && cp ${SSL_EXAMPLE}/dhparams.pem ${ACME_BASE}/dhparams.pem -if [[ -f ${ACME_BASE}/cert.pem ]] && [[ -f ${ACME_BASE}/key.pem ]]; then +if [[ -f ${ACME_BASE}/cert.pem ]] && [[ -f ${ACME_BASE}/key.pem ]] && [[ $(stat -c%s ${ACME_BASE}/cert.pem) != 0 ]]; then ISSUER=$(openssl x509 -in ${ACME_BASE}/cert.pem -noout -issuer) if [[ ${ISSUER} != *"Let's Encrypt"* && ${ISSUER} != *"mailcow"* && ${ISSUER} != *"Fake LE Intermediate"* ]]; then log_f "Found certificate with issuer other than mailcow snake-oil CA and Let's Encrypt, skipping ACME client..." @@ -156,6 +174,7 @@ else exec env TRIGGER_RESTART=1 $(readlink -f "$0") fi fi +chmod 600 ${ACME_BASE}/key.pem log_f "Waiting for database... " no_nl while ! mysqladmin status --socket=/var/run/mysqld/mysqld.sock -u${DBUSER} -p${DBPASS} --silent; do @@ -196,10 +215,8 @@ while true; do log_f "Using existing Lets Encrypt account key ${ACME_BASE}/acme/account.pem" fi - # Skipping IP check when we like to live dangerously - if [[ "${SKIP_IP_CHECK}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then - SKIP_IP_CHECK=y - fi + chmod 600 ${ACME_BASE}/acme/key.pem + chmod 600 ${ACME_BASE}/acme/account.pem # Cleaning up and init validation arrays unset SQL_DOMAIN_ARR @@ -228,7 +245,7 @@ while true; do ADDITIONAL_SAN_ARR+=($i) fi done - ADDITIONAL_WC_ARR+=('autodiscover') + ADDITIONAL_WC_ARR+=('autodiscover' 'autoconfig') # Start IP detection log_f "Detecting IP addresses... " no_nl @@ -255,9 +272,10 @@ while true; do SQL_DOMAIN_ARR+=("${domains}") done < <(mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT domain FROM domain WHERE backupmx=0" -Bs) + if [[ ${ONLY_MAILCOW_HOSTNAME} != "y" ]]; then for SQL_DOMAIN in "${SQL_DOMAIN_ARR[@]}"; do for SUBDOMAIN in "${ADDITIONAL_WC_ARR[@]}"; do - if [[ "${SUBDOMAIN}.${SQL_DOMAIN}" != "${MAILCOW_HOSTNAME}" ]]; then + if [[ "${SUBDOMAIN}.${SQL_DOMAIN}" != "${MAILCOW_HOSTNAME}" ]]; then A_SUBDOMAIN=$(dig A ${SUBDOMAIN}.${SQL_DOMAIN} +short | tail -n 1) AAAA_SUBDOMAIN=$(dig AAAA ${SUBDOMAIN}.${SQL_DOMAIN} +short | tail -n 1) # Check if CNAME without v6 enabled target @@ -268,10 +286,10 @@ while true; do log_f "Found AAAA record for ${SUBDOMAIN}.${SQL_DOMAIN}: ${AAAA_SUBDOMAIN} - skipping A record check" if [[ $(expand ${IPV6:-"0000:0000:0000:0000:0000:0000:0000:0000"}) == $(expand ${AAAA_SUBDOMAIN}) ]] || [[ ${SKIP_IP_CHECK} == "y" ]]; then if verify_challenge_path "${SUBDOMAIN}.${SQL_DOMAIN}" 6; then - log_f "Confirmed AAAA record ${AAAA_SUBDOMAIN}" + log_f "Confirmed AAAA record with IP ${AAAA_SUBDOMAIN}, adding SAN" VALIDATED_CONFIG_DOMAINS+=("${SUBDOMAIN}.${SQL_DOMAIN}") else - log_f "Confirmed AAAA record ${AAAA_SUBDOMAIN}, but HTTP validation failed" + log_f "Confirmed AAAA record with IP ${AAAA_SUBDOMAIN}, but HTTP validation failed" fi else log_f "Cannot match your IP ${IPV6:-NO_IPV6_LINK} against hostname ${SUBDOMAIN}.${SQL_DOMAIN} ($(expand ${AAAA_SUBDOMAIN}))" @@ -280,10 +298,10 @@ while true; do log_f "Found A record for ${SUBDOMAIN}.${SQL_DOMAIN}: ${A_SUBDOMAIN}" if [[ ${IPV4:-ERR} == ${A_SUBDOMAIN} ]] || [[ ${SKIP_IP_CHECK} == "y" ]]; then if verify_challenge_path "${SUBDOMAIN}.${SQL_DOMAIN}" 4; then - log_f "Confirmed A record ${A_SUBDOMAIN}" + log_f "Confirmed A record ${A_SUBDOMAIN}, adding SAN" VALIDATED_CONFIG_DOMAINS+=("${SUBDOMAIN}.${SQL_DOMAIN}") else - log_f "Confirmed AAAA record ${A_SUBDOMAIN}, but HTTP validation failed" + log_f "Confirmed A record with IP ${A_SUBDOMAIN}, but HTTP validation failed" fi else log_f "Cannot match your IP ${IPV4} against hostname ${SUBDOMAIN}.${SQL_DOMAIN} (${A_SUBDOMAIN})" @@ -294,6 +312,7 @@ while true; do fi done done + fi A_MAILCOW_HOSTNAME=$(dig A ${MAILCOW_HOSTNAME} +short | tail -n 1) AAAA_MAILCOW_HOSTNAME=$(dig AAAA ${MAILCOW_HOSTNAME} +short | tail -n 1) @@ -308,10 +327,10 @@ while true; do log_f "Confirmed AAAA record ${AAAA_MAILCOW_HOSTNAME}" VALIDATED_MAILCOW_HOSTNAME=${MAILCOW_HOSTNAME} else - log_f "Confirmed AAAA record ${A_MAILCOW_HOSTNAME}, but HTTP validation failed" + log_f "Confirmed AAAA record with IP ${AAAA_MAILCOW_HOSTNAME}, but HTTP validation failed" fi else - log_f "Cannot match your IP ${IPV6:-NO_IPV6_LINK} against hostname ${MAILCOW_HOSTNAME} ($(expand ${AAAA_MAILCOW_HOSTNAME}))" + log_f "Cannot match your IP ${IPV6:-NO_IPV6_LINK} against hostname ${MAILCOW_HOSTNAME} (DNS returned $(expand ${AAAA_MAILCOW_HOSTNAME}))" fi elif [[ ! -z ${A_MAILCOW_HOSTNAME} ]]; then log_f "Found A record for ${MAILCOW_HOSTNAME}: ${A_MAILCOW_HOSTNAME}" @@ -320,15 +339,16 @@ while true; do log_f "Confirmed A record ${A_MAILCOW_HOSTNAME}" VALIDATED_MAILCOW_HOSTNAME=${MAILCOW_HOSTNAME} else - log_f "Confirmed A record ${A_MAILCOW_HOSTNAME}, but HTTP validation failed" + log_f "Confirmed A record with IP ${A_MAILCOW_HOSTNAME}, but HTTP validation failed" fi else - log_f "Cannot match your IP ${IPV4} against hostname ${MAILCOW_HOSTNAME} (${A_MAILCOW_HOSTNAME})" + log_f "Cannot match your IP ${IPV4} against hostname ${MAILCOW_HOSTNAME} (DNS returned ${A_MAILCOW_HOSTNAME})" fi else log_f "No A or AAAA record found for hostname ${MAILCOW_HOSTNAME}" fi + if [[ ${ONLY_MAILCOW_HOSTNAME} != "y" ]]; then for SAN in "${ADDITIONAL_SAN_ARR[@]}"; do # Skip on CAA errors for SAN SAN_PARENT_DOMAIN=$(echo ${SAN} | cut -d. -f2-) @@ -354,13 +374,13 @@ while true; do log_f "Found AAAA record for ${SAN}: ${AAAA_SAN} - skipping A record check" if [[ $(expand ${IPV6:-"0000:0000:0000:0000:0000:0000:0000:0000"}) == $(expand ${AAAA_SAN}) ]] || [[ ${SKIP_IP_CHECK} == "y" ]]; then if verify_challenge_path "${SAN}" 6; then - log_f "Confirmed AAAA record ${AAAA_SAN}" + log_f "Confirmed AAAA record with IP ${AAAA_SAN}" ADDITIONAL_VALIDATED_SAN+=("${SAN}") else - log_f "Confirmed AAAA record ${AAAA_SAN}, but HTTP validation failed" + log_f "Confirmed AAAA record with IP ${AAAA_SAN}, but HTTP validation failed" fi else - log_f "Cannot match your IP ${IPV6:-NO_IPV6_LINK} against hostname ${SAN} ($(expand ${AAAA_SAN}))" + log_f "Cannot match your IP ${IPV6:-NO_IPV6_LINK} against hostname ${SAN} (DNS returned $(expand ${AAAA_SAN}))" fi elif [[ ! -z ${A_SAN} ]]; then log_f "Found A record for ${SAN}: ${A_SAN}" @@ -369,21 +389,23 @@ while true; do log_f "Confirmed A record ${A_SAN}" ADDITIONAL_VALIDATED_SAN+=("${SAN}") else - log_f "Confirmed A record ${A_SAN}, but HTTP validation failed" + log_f "Confirmed A record with IP ${A_SAN}, but HTTP validation failed" fi else - log_f "Cannot match your IP ${IPV4} against hostname ${SAN} (${A_SAN})" + log_f "Cannot match your IP ${IPV4} against hostname ${SAN} (DNS returned ${A_SAN})" fi else log_f "No A or AAAA record found for hostname ${SAN}" fi done + fi # Unique elements ALL_VALIDATED=(${VALIDATED_MAILCOW_HOSTNAME} $(echo ${VALIDATED_CONFIG_DOMAINS[*]} ${ADDITIONAL_VALIDATED_SAN[*]} | xargs -n1 | sort -u | xargs)) if [[ -z ${ALL_VALIDATED[*]} ]]; then log_f "Cannot validate hostnames, skipping Let's Encrypt for 1 hour." log_f "Use SKIP_LETS_ENCRYPT=y in mailcow.conf to skip it permanently." + redis-cli -h redis SET ACME_FAIL_TIME "$(date +%s)" sleep 1h exec $(readlink -f "$0") fi @@ -397,19 +419,19 @@ while true; do # Finding difference in SAN array now vs. SAN array by current configuration array_diff ORPHANED_SAN SAN_ARRAY_NOW ALL_VALIDATED if [[ ! -z ${ORPHANED_SAN[*]} ]]; then - log_f "Found orphaned SANs ${ORPHANED_SAN[*]}" + log_f "Found orphaned SAN ${ORPHANED_SAN[*]}" SAN_CHANGE=1 fi array_diff ADDED_SAN ALL_VALIDATED SAN_ARRAY_NOW if [[ ! -z ${ADDED_SAN[*]} ]]; then - log_f "Found new SANs ${ADDED_SAN[*]}" + log_f "Found new SAN ${ADDED_SAN[*]}" SAN_CHANGE=1 fi if [[ ${SAN_CHANGE} == 0 ]]; then # Certificate did not change but could be due for renewal (4 weeks) - if ! openssl x509 -checkend 1209600 -noout -in ${ACME_BASE}/cert.pem; then - log_f "Certificate is due for renewal (< 2 weeks)" + if ! openssl x509 -checkend 2592000 -noout -in ${ACME_BASE}/cert.pem; then + log_f "Certificate is due for renewal (< 30 days)" else log_f "Certificate validation done, neither changed nor due for renewal, sleeping for another day." sleep 1d @@ -462,7 +484,7 @@ while true; do cp ${ACME_BASE}/acme/cert.pem ${ACME_BASE}/cert.pem cp ${ACME_BASE}/acme/key.pem ${ACME_BASE}/key.pem reload_configurations - rm /var/www/acme/* + rm /var/www/acme/* 2> /dev/null log_f "Certificate successfully deployed, removing backup, sleeping 1d" sleep 1d else @@ -476,6 +498,7 @@ while true; do ACME_RESPONSE_B64=$(echo "${ACME_RESPONSE}" | openssl enc -e -A -base64) log_f "${ACME_RESPONSE_B64}" redis_only b64 log_f "Retrying in 30 minutes..." + redis-cli -h redis SET ACME_FAIL_TIME "$(date +%s)" sleep 30m exec $(readlink -f "$0") ;; diff --git a/data/Dockerfiles/clamd/Dockerfile b/data/Dockerfiles/clamd/Dockerfile index f5a3903b..f61f7c21 100644 --- a/data/Dockerfiles/clamd/Dockerfile +++ b/data/Dockerfiles/clamd/Dockerfile @@ -3,7 +3,7 @@ FROM debian:stretch-slim LABEL maintainer "André Peters <andre.peters@servercow.de>" # Installation -ENV CLAMAV 0.101.1 +ENV CLAMAV 0.101.4 RUN apt-get update && apt-get install -y --no-install-recommends \ ca-certificates \ diff --git a/data/Dockerfiles/clamd/bootstrap.sh b/data/Dockerfiles/clamd/bootstrap.sh index 1d49cd20..4795ee13 100755 --- a/data/Dockerfiles/clamd/bootstrap.sh +++ b/data/Dockerfiles/clamd/bootstrap.sh @@ -48,6 +48,7 @@ while true; do sleep 2m SANE_MIRRORS="$(dig +ignore +short rsync.sanesecurity.net)" for sane_mirror in ${SANE_MIRRORS}; do + CE= rsync -avp --chown=clamav:clamav --chmod=Du=rwx,Dgo=rx,Fu=rw,Fog=r --timeout=5 rsync://${sane_mirror}/sanesecurity/ \ --include 'blurl.ndb' \ --include 'junk.ndb' \ @@ -61,7 +62,9 @@ while true; do --include 'sanesecurity.ftm' \ --include 'sigwhitelist.ign2' \ --exclude='*' /var/lib/clamav/ - if [ $? -eq 0 ]; then + CE=$? + chmod 755 /var/lib/clamav/ + if [ ${CE} -eq 0 ]; then echo RELOAD | nc localhost 3310 break fi diff --git a/data/Dockerfiles/dockerapi/Dockerfile b/data/Dockerfiles/dockerapi/Dockerfile index 67bb6b07..37a2db5a 100644 --- a/data/Dockerfiles/dockerapi/Dockerfile +++ b/data/Dockerfiles/dockerapi/Dockerfile @@ -1,11 +1,12 @@ -FROM alpine:3.9 +FROM alpine:3.10 LABEL maintainer "Andre Peters <andre.peters@servercow.de>" -RUN apk add -U --no-cache python2 python-dev py-pip gcc musl-dev tzdata openssl-dev libffi-dev \ - && pip2 install --upgrade pip \ - && pip2 install --upgrade docker==3.0.1 flask flask-restful pyOpenSSL \ - && apk del python-dev py2-pip gcc +WORKDIR /app -COPY server.py / +RUN apk add --update --no-cache python3 openssl tzdata \ + && pip3 install --upgrade pip \ + && pip3 install --upgrade docker flask flask-restful -CMD ["python2", "-u", "/server.py"] +COPY server.py /app/ + +CMD ["python3", "-u", "/app/server.py"] diff --git a/data/Dockerfiles/dockerapi/server.py b/data/Dockerfiles/dockerapi/server.py index d38775db..8d6a1c66 100644 --- a/data/Dockerfiles/dockerapi/server.py +++ b/data/Dockerfiles/dockerapi/server.py @@ -1,10 +1,11 @@ +#!/usr/bin/env python3 + from flask import Flask from flask_restful import Resource, Api from flask import jsonify from flask import Response from flask import request from threading import Thread -from OpenSSL import crypto import docker import uuid import signal @@ -14,6 +15,8 @@ import re import sys import ssl import socket +import subprocess +import traceback docker_client = docker.DockerClient(base_url='unix://var/run/docker.sock', version='auto') app = Flask(__name__) @@ -43,261 +46,316 @@ class container_get(Resource): class container_post(Resource): def post(self, container_id, post_action): if container_id and container_id.isalnum() and post_action: - if post_action == 'stop': - try: - for container in docker_client.containers.list(all=True, filters={"id": container_id}): - container.stop() - return jsonify(type='success', msg='command completed successfully') - except Exception as e: - return jsonify(type='danger', msg=str(e)) - - elif post_action == 'start': - try: - for container in docker_client.containers.list(all=True, filters={"id": container_id}): - container.start() - return jsonify(type='success', msg='command completed successfully') - except Exception as e: - return jsonify(type='danger', msg=str(e)) - - elif post_action == 'restart': - try: - for container in docker_client.containers.list(all=True, filters={"id": container_id}): - container.restart() - return jsonify(type='success', msg='command completed successfully') - except Exception as e: - return jsonify(type='danger', msg=str(e)) - - elif post_action == 'top': - try: - for container in docker_client.containers.list(all=True, filters={"id": container_id}): - return jsonify(type='success', msg=container.top()) - except Exception as e: - return jsonify(type='danger', msg=str(e)) - - elif post_action == 'stats': - try: - for container in docker_client.containers.list(all=True, filters={"id": container_id}): - return jsonify(type='success', msg=container.stats(decode=True, stream=False)) - except Exception as e: - return jsonify(type='danger', msg=str(e)) - - elif post_action == 'exec': - - if not request.json or not 'cmd' in request.json: - return jsonify(type='danger', msg='cmd is missing') - - if request.json['cmd'] == 'mailq': - if 'items' in request.json: - r = re.compile("^[0-9a-fA-F]+$") - filtered_qids = filter(r.match, request.json['items']) - if filtered_qids: - if request.json['task'] == 'delete': - flagged_qids = ['-d %s' % i for i in filtered_qids] - sanitized_string = str(' '.join(flagged_qids)); - try: - for container in docker_client.containers.list(filters={"id": container_id}): - postsuper_r = container.exec_run(["/bin/bash", "-c", "/usr/sbin/postsuper " + sanitized_string]) - return exec_run_handler('generic', postsuper_r) - except Exception as e: - return jsonify(type='danger', msg=str(e)) - if request.json['task'] == 'hold': - flagged_qids = ['-h %s' % i for i in filtered_qids] - sanitized_string = str(' '.join(flagged_qids)); - try: - for container in docker_client.containers.list(filters={"id": container_id}): - postsuper_r = container.exec_run(["/bin/bash", "-c", "/usr/sbin/postsuper " + sanitized_string]) - return exec_run_handler('generic', postsuper_r) - except Exception as e: - return jsonify(type='danger', msg=str(e)) - if request.json['task'] == 'unhold': - flagged_qids = ['-H %s' % i for i in filtered_qids] - sanitized_string = str(' '.join(flagged_qids)); - try: - for container in docker_client.containers.list(filters={"id": container_id}): - postsuper_r = container.exec_run(["/bin/bash", "-c", "/usr/sbin/postsuper " + sanitized_string]) - return exec_run_handler('generic', postsuper_r) - except Exception as e: - return jsonify(type='danger', msg=str(e)) - if request.json['task'] == 'deliver': - flagged_qids = ['-i %s' % i for i in filtered_qids] - try: - for container in docker_client.containers.list(filters={"id": container_id}): - for i in flagged_qids: - postqueue_r = container.exec_run(["/bin/bash", "-c", "/usr/sbin/postqueue " + i], user='postfix') - # todo: check each exit code - return jsonify(type='success', msg=str("Scheduled immediate delivery")) - except Exception as e: - return jsonify(type='danger', msg=str(e)) - elif request.json['task'] == 'list': - try: - for container in docker_client.containers.list(filters={"id": container_id}): - mailq_return = container.exec_run(["/usr/sbin/postqueue", "-j"], user='postfix') - return exec_run_handler('utf8_text_only', mailq_return) - except Exception as e: - return jsonify(type='danger', msg=str(e)) - elif request.json['task'] == 'flush': - try: - for container in docker_client.containers.list(filters={"id": container_id}): - postqueue_r = container.exec_run(["/usr/sbin/postqueue", "-f"], user='postfix') - return exec_run_handler('generic', postqueue_r) - except Exception as e: - return jsonify(type='danger', msg=str(e)) - elif request.json['task'] == 'super_delete': - try: - for container in docker_client.containers.list(filters={"id": container_id}): - postsuper_r = container.exec_run(["/usr/sbin/postsuper", "-d", "ALL"]) - return exec_run_handler('generic', postsuper_r) - except Exception as e: - return jsonify(type='danger', msg=str(e)) - - elif request.json['cmd'] == 'system': - if request.json['task'] == 'fts_rescan': - if 'username' in request.json: - try: - for container in docker_client.containers.list(filters={"id": container_id}): - rescan_return = container.exec_run(["/bin/bash", "-c", "/usr/local/bin/doveadm fts rescan -u '" + request.json['username'].replace("'", "'\\''") + "'"], user='vmail') - if rescan_return.exit_code == 0: - return jsonify(type='success', msg='fts_rescan: rescan triggered') - else: - return jsonify(type='warning', msg='fts_rescan error') - except Exception as e: - return jsonify(type='danger', msg=str(e)) - if 'all' in request.json: - try: - for container in docker_client.containers.list(filters={"id": container_id}): - rescan_return = container.exec_run(["/bin/bash", "-c", "/usr/local/bin/doveadm fts rescan -A"], user='vmail') - if rescan_return.exit_code == 0: - return jsonify(type='success', msg='fts_rescan: rescan triggered') - else: - return jsonify(type='warning', msg='fts_rescan error') - except Exception as e: - return jsonify(type='danger', msg=str(e)) - elif request.json['task'] == 'df': - if 'dir' in request.json: - try: - for container in docker_client.containers.list(filters={"id": container_id}): - df_return = container.exec_run(["/bin/bash", "-c", "/bin/df -H '" + request.json['dir'].replace("'", "'\\''") + "' | /usr/bin/tail -n1 | /usr/bin/tr -s [:blank:] | /usr/bin/tr ' ' ','"], user='nobody') - if df_return.exit_code == 0: - return df_return.output.rstrip() - else: - return "0,0,0,0,0,0" - except Exception as e: - return jsonify(type='danger', msg=str(e)) - elif request.json['task'] == 'mysql_upgrade': - try: - for container in docker_client.containers.list(filters={"id": container_id}): - sql_shell = container.exec_run(["/bin/bash"], stdin=True, socket=True, user='mysql') - upgrade_cmd = "/usr/bin/mysql_upgrade -uroot -p'" + os.environ['DBROOT'].replace("'", "'\\''") + "'\n" - sql_socket = sql_shell.output; - try : - sql_socket.sendall(upgrade_cmd.encode('utf-8')) - sql_socket.shutdown(socket.SHUT_WR) - except socket.error: - return jsonify(type='danger', msg=str('socket error')) - worker_response = recv_socket_data(sql_socket) - matched = False - for line in worker_response.split("\n"): - if 'is already upgraded to' in line: - matched = True - if matched: - return jsonify(type='success', msg='mysql_upgrade: already upgraded') - else: - container.restart() - return jsonify(type='warning', msg='mysql_upgrade: upgrade was applied') - except Exception as e: - return jsonify(type='danger', msg=str(e)) - - elif request.json['cmd'] == 'reload': - if request.json['task'] == 'dovecot': - try: - for container in docker_client.containers.list(filters={"id": container_id}): - reload_return = container.exec_run(["/bin/bash", "-c", "/usr/local/sbin/dovecot reload"]) - return exec_run_handler('generic', reload_return) - except Exception as e: - return jsonify(type='danger', msg=str(e)) - if request.json['task'] == 'postfix': - try: - for container in docker_client.containers.list(filters={"id": container_id}): - reload_return = container.exec_run(["/bin/bash", "-c", "/usr/sbin/postfix reload"]) - return exec_run_handler('generic', reload_return) - except Exception as e: - return jsonify(type='danger', msg=str(e)) - if request.json['task'] == 'nginx': - try: - for container in docker_client.containers.list(filters={"id": container_id}): - reload_return = container.exec_run(["/bin/sh", "-c", "/usr/sbin/nginx -s reload"]) - return exec_run_handler('generic', reload_return) - except Exception as e: - return jsonify(type='danger', msg=str(e)) - - elif request.json['cmd'] == 'sieve': - if request.json['task'] == 'list': - if 'username' in request.json: - try: - for container in docker_client.containers.list(filters={"id": container_id}): - sieve_return = container.exec_run(["/bin/bash", "-c", "/usr/local/bin/doveadm sieve list -u '" + request.json['username'].replace("'", "'\\''") + "'"]) - return exec_run_handler('utf8_text_only', sieve_return) - except Exception as e: - return jsonify(type='danger', msg=str(e)) - elif request.json['task'] == 'print': - if 'username' in request.json and 'script_name' in request.json: - try: - for container in docker_client.containers.list(filters={"id": container_id}): - sieve_return = container.exec_run(["/bin/bash", "-c", "/usr/local/bin/doveadm sieve get -u '" + request.json['username'].replace("'", "'\\''") + "' '" + request.json['script_name'].replace("'", "'\\''") + "'"]) - return exec_run_handler('utf8_text_only', sieve_return) - except Exception as e: - return jsonify(type='danger', msg=str(e)) - - elif request.json['cmd'] == 'maildir': - if request.json['task'] == 'cleanup': - if 'maildir' in request.json: - try: - for container in docker_client.containers.list(filters={"id": container_id}): - sane_name = re.sub(r'\W+', '', request.json['maildir']) - maildir_cleanup = container.exec_run(["/bin/bash", "-c", "if [[ -d '/var/vmail/" + request.json['maildir'].replace("'", "'\\''") + "' ]]; then /bin/mv '/var/vmail/" + request.json['maildir'].replace("'", "'\\''") + "' '/var/vmail/_garbage/" + str(int(time.time())) + "_" + sane_name + "'; fi"], user='vmail') - return exec_run_handler('generic', maildir_cleanup) - except Exception as e: - return jsonify(type='danger', msg=str(e)) - - elif request.json['cmd'] == 'rspamd': - if request.json['task'] == 'worker_password': - if 'raw' in request.json: - try: - for container in docker_client.containers.list(filters={"id": container_id}): - worker_shell = container.exec_run(["/bin/bash"], stdin=True, socket=True, user='_rspamd') - worker_cmd = "/usr/bin/rspamadm pw -e -p '" + request.json['raw'].replace("'", "'\\''") + "' 2> /dev/null\n" - worker_socket = worker_shell.output; - try : - worker_socket.sendall(worker_cmd.encode('utf-8')) - worker_socket.shutdown(socket.SHUT_WR) - except socket.error: - return jsonify(type='danger', msg=str('socket error')) - worker_response = recv_socket_data(worker_socket) - matched = False - for line in worker_response.split("\n"): - if '$2$' in line: - matched = True - hash = line.strip() - hash_out = re.search('\$2\$.+$', hash).group(0) - f = open("/access.inc", "w") - f.write('enable_password = "' + re.sub('[^0-9a-zA-Z\$]+', '', hash_out.rstrip()) + '";\n') - f.close() - container.restart() - if matched: - return jsonify(type='success', msg='command completed successfully') - else: - return jsonify(type='danger', msg='command did not complete') - except Exception as e: - return jsonify(type='danger', msg=str(e)) + try: + """Dispatch container_post api call""" + if post_action == 'exec': + if not request.json or not 'cmd' in request.json: + return jsonify(type='danger', msg='cmd is missing') + if not request.json or not 'task' in request.json: + return jsonify(type='danger', msg='task is missing') + api_call_method_name = '__'.join(['container_post', str(post_action), str(request.json['cmd']), str(request.json['task']) ]) else: - return jsonify(type='danger', msg='Unknown command') + api_call_method_name = '__'.join(['container_post', str(post_action) ]) - else: - return jsonify(type='danger', msg='invalid action') + api_call_method = getattr(self, api_call_method_name, lambda container_id: jsonify(type='danger', msg='container_post - unknown api call')) + + + print("api call: %s, container_id: %s" % (api_call_method_name, container_id)) + return api_call_method(container_id) + except Exception as e: + print("error - container_post: %s" % str(e)) + return jsonify(type='danger', msg=str(e)) else: - return jsonify(type='danger', msg='invalid container id or missing action') + return jsonify(type='danger', msg='invalid container id or missing action') + + + # api call: container_post - post_action: stop + def container_post__stop(self, container_id): + for container in docker_client.containers.list(all=True, filters={"id": container_id}): + container.stop() + return jsonify(type='success', msg='command completed successfully') + + + # api call: container_post - post_action: start + def container_post__start(self, container_id): + for container in docker_client.containers.list(all=True, filters={"id": container_id}): + container.start() + return jsonify(type='success', msg='command completed successfully') + + + # api call: container_post - post_action: restart + def container_post__restart(self, container_id): + for container in docker_client.containers.list(all=True, filters={"id": container_id}): + container.restart() + return jsonify(type='success', msg='command completed successfully') + + + # api call: container_post - post_action: top + def container_post__top(self, container_id): + for container in docker_client.containers.list(all=True, filters={"id": container_id}): + return jsonify(type='success', msg=container.top()) + + + # api call: container_post - post_action: stats + def container_post__stats(self, container_id): + for container in docker_client.containers.list(all=True, filters={"id": container_id}): + for stat in container.stats(decode=True, stream=True): + return jsonify(type='success', msg=stat ) + + + # api call: container_post - post_action: exec - cmd: mailq - task: delete + def container_post__exec__mailq__delete(self, container_id): + if 'items' in request.json: + r = re.compile("^[0-9a-fA-F]+$") + filtered_qids = filter(r.match, request.json['items']) + if filtered_qids: + flagged_qids = ['-d %s' % i for i in filtered_qids] + sanitized_string = str(' '.join(flagged_qids)); + + for container in docker_client.containers.list(filters={"id": container_id}): + postsuper_r = container.exec_run(["/bin/bash", "-c", "/usr/sbin/postsuper " + sanitized_string]) + return exec_run_handler('generic', postsuper_r) + + + # api call: container_post - post_action: exec - cmd: mailq - task: hold + def container_post__exec__mailq__hold(self, container_id): + if 'items' in request.json: + r = re.compile("^[0-9a-fA-F]+$") + filtered_qids = filter(r.match, request.json['items']) + if filtered_qids: + flagged_qids = ['-h %s' % i for i in filtered_qids] + sanitized_string = str(' '.join(flagged_qids)); + + for container in docker_client.containers.list(filters={"id": container_id}): + postsuper_r = container.exec_run(["/bin/bash", "-c", "/usr/sbin/postsuper " + sanitized_string]) + return exec_run_handler('generic', postsuper_r) + + + # api call: container_post - post_action: exec - cmd: mailq - task: unhold + def container_post__exec__mailq__unhold(self, container_id): + if 'items' in request.json: + r = re.compile("^[0-9a-fA-F]+$") + filtered_qids = filter(r.match, request.json['items']) + if filtered_qids: + flagged_qids = ['-H %s' % i for i in filtered_qids] + sanitized_string = str(' '.join(flagged_qids)); + + for container in docker_client.containers.list(filters={"id": container_id}): + postsuper_r = container.exec_run(["/bin/bash", "-c", "/usr/sbin/postsuper " + sanitized_string]) + return exec_run_handler('generic', postsuper_r) + + + # api call: container_post - post_action: exec - cmd: mailq - task: deliver + def container_post__exec__mailq__deliver(self, container_id): + if 'items' in request.json: + r = re.compile("^[0-9a-fA-F]+$") + filtered_qids = filter(r.match, request.json['items']) + if filtered_qids: + flagged_qids = ['-i %s' % i for i in filtered_qids] + + for container in docker_client.containers.list(filters={"id": container_id}): + for i in flagged_qids: + postqueue_r = container.exec_run(["/bin/bash", "-c", "/usr/sbin/postqueue " + i], user='postfix') + # todo: check each exit code + return jsonify(type='success', msg=str("Scheduled immediate delivery")) + + + # api call: container_post - post_action: exec - cmd: mailq - task: list + def container_post__exec__mailq__list(self, container_id): + for container in docker_client.containers.list(filters={"id": container_id}): + mailq_return = container.exec_run(["/usr/sbin/postqueue", "-j"], user='postfix') + return exec_run_handler('utf8_text_only', mailq_return) + + + # api call: container_post - post_action: exec - cmd: mailq - task: flush + def container_post__exec__mailq__flush(self, container_id): + for container in docker_client.containers.list(filters={"id": container_id}): + postqueue_r = container.exec_run(["/usr/sbin/postqueue", "-f"], user='postfix') + return exec_run_handler('generic', postqueue_r) + + + # api call: container_post - post_action: exec - cmd: mailq - task: super_delete + def container_post__exec__mailq__super_delete(self, container_id): + for container in docker_client.containers.list(filters={"id": container_id}): + postsuper_r = container.exec_run(["/usr/sbin/postsuper", "-d", "ALL"]) + return exec_run_handler('generic', postsuper_r) + + + # api call: container_post - post_action: exec - cmd: system - task: fts_rescan + def container_post__exec__system__fts_rescan(self, container_id): + if 'username' in request.json: + for container in docker_client.containers.list(filters={"id": container_id}): + rescan_return = container.exec_run(["/bin/bash", "-c", "/usr/bin/doveadm fts rescan -u '" + request.json['username'].replace("'", "'\\''") + "'"], user='vmail') + if rescan_return.exit_code == 0: + return jsonify(type='success', msg='fts_rescan: rescan triggered') + else: + return jsonify(type='warning', msg='fts_rescan error') + + if 'all' in request.json: + for container in docker_client.containers.list(filters={"id": container_id}): + rescan_return = container.exec_run(["/bin/bash", "-c", "/usr/bin/doveadm fts rescan -A"], user='vmail') + if rescan_return.exit_code == 0: + return jsonify(type='success', msg='fts_rescan: rescan triggered') + else: + return jsonify(type='warning', msg='fts_rescan error') + + + # api call: container_post - post_action: exec - cmd: system - task: df + def container_post__exec__system__df(self, container_id): + if 'dir' in request.json: + for container in docker_client.containers.list(filters={"id": container_id}): + df_return = container.exec_run(["/bin/bash", "-c", "/bin/df -H '" + request.json['dir'].replace("'", "'\\''") + "' | /usr/bin/tail -n1 | /usr/bin/tr -s [:blank:] | /usr/bin/tr ' ' ','"], user='nobody') + if df_return.exit_code == 0: + return df_return.output.decode('utf-8').rstrip() + else: + return "0,0,0,0,0,0" + + + # api call: container_post - post_action: exec - cmd: system - task: mysql_upgrade + def container_post__exec__system__mysql_upgrade(self, container_id): + for container in docker_client.containers.list(filters={"id": container_id}): + cmd = "/usr/bin/mysql_upgrade -uroot -p'" + os.environ['DBROOT'].replace("'", "'\\''") + "'\n" + cmd_response = exec_cmd_container(container, cmd, user='mysql') + + matched = False + for line in cmd_response.split("\n"): + if 'is already upgraded to' in line: + matched = True + if matched: + return jsonify(type='success', msg='mysql_upgrade: already upgraded') + else: + container.restart() + return jsonify(type='warning', msg='mysql_upgrade: upgrade was applied') + + + # api call: container_post - post_action: exec - cmd: reload - task: dovecot + def container_post__exec__reload__dovecot(self, container_id): + for container in docker_client.containers.list(filters={"id": container_id}): + reload_return = container.exec_run(["/bin/bash", "-c", "/usr/sbin/dovecot reload"]) + return exec_run_handler('generic', reload_return) + + + # api call: container_post - post_action: exec - cmd: reload - task: postfix + def container_post__exec__reload__postfix(self, container_id): + for container in docker_client.containers.list(filters={"id": container_id}): + reload_return = container.exec_run(["/bin/bash", "-c", "/usr/sbin/postfix reload"]) + return exec_run_handler('generic', reload_return) + + + # api call: container_post - post_action: exec - cmd: reload - task: nginx + def container_post__exec__reload__nginx(self, container_id): + for container in docker_client.containers.list(filters={"id": container_id}): + reload_return = container.exec_run(["/bin/sh", "-c", "/usr/sbin/nginx -s reload"]) + return exec_run_handler('generic', reload_return) + + + # api call: container_post - post_action: exec - cmd: sieve - task: list + def container_post__exec__sieve__list(self, container_id): + if 'username' in request.json: + for container in docker_client.containers.list(filters={"id": container_id}): + sieve_return = container.exec_run(["/bin/bash", "-c", "/usr/bin/doveadm sieve list -u '" + request.json['username'].replace("'", "'\\''") + "'"]) + return exec_run_handler('utf8_text_only', sieve_return) + + + # api call: container_post - post_action: exec - cmd: sieve - task: print + def container_post__exec__sieve__print(self, container_id): + if 'username' in request.json and 'script_name' in request.json: + for container in docker_client.containers.list(filters={"id": container_id}): + cmd = ["/bin/bash", "-c", "/usr/bin/doveadm sieve get -u '" + request.json['username'].replace("'", "'\\''") + "' '" + request.json['script_name'].replace("'", "'\\''") + "'"] + sieve_return = container.exec_run(cmd) + return exec_run_handler('utf8_text_only', sieve_return) + + + # api call: container_post - post_action: exec - cmd: maildir - task: cleanup + def container_post__exec__maildir__cleanup(self, container_id): + if 'maildir' in request.json: + for container in docker_client.containers.list(filters={"id": container_id}): + sane_name = re.sub(r'\W+', '', request.json['maildir']) + cmd = ["/bin/bash", "-c", "if [[ -d '/var/vmail/" + request.json['maildir'].replace("'", "'\\''") + "' ]]; then /bin/mv '/var/vmail/" + request.json['maildir'].replace("'", "'\\''") + "' '/var/vmail/_garbage/" + str(int(time.time())) + "_" + sane_name + "'; fi"] + maildir_cleanup = container.exec_run(cmd, user='vmail') + return exec_run_handler('generic', maildir_cleanup) + + + + # api call: container_post - post_action: exec - cmd: rspamd - task: worker_password + def container_post__exec__rspamd__worker_password(self, container_id): + if 'raw' in request.json: + for container in docker_client.containers.list(filters={"id": container_id}): + cmd = "/usr/bin/rspamadm pw -e -p '" + request.json['raw'].replace("'", "'\\''") + "' 2> /dev/null" + cmd_response = exec_cmd_container(container, cmd, user="_rspamd") + + matched = False + for line in cmd_response.split("\n"): + if '$2$' in line: + hash = line.strip() + hash_out = re.search('\$2\$.+$', hash).group(0) + rspamd_passphrase_hash = re.sub('[^0-9a-zA-Z\$]+', '', hash_out.rstrip()) + + rspamd_password_filename = "/etc/rspamd/override.d/worker-controller-password.inc" + cmd = '''/bin/echo 'enable_password = "%s";' > %s && cat %s''' % (rspamd_passphrase_hash, rspamd_password_filename, rspamd_password_filename) + cmd_response = exec_cmd_container(container, cmd, user="_rspamd") + + if rspamd_passphrase_hash.startswith("$2$") and rspamd_passphrase_hash in cmd_response: + container.restart() + matched = True + + if matched: + return jsonify(type='success', msg='command completed successfully') + else: + return jsonify(type='danger', msg='command did not complete') + + +def exec_cmd_container(container, cmd, user, timeout=2, shell_cmd="/bin/bash"): + + def recv_socket_data(c_socket, timeout): + c_socket.setblocking(0) + total_data=[]; + data=''; + begin=time.time() + while True: + if total_data and time.time()-begin > timeout: + break + elif time.time()-begin > timeout*2: + break + try: + data = c_socket.recv(8192) + if data: + total_data.append(data.decode('utf-8')) + #change the beginning time for measurement + begin=time.time() + else: + #sleep for sometime to indicate a gap + time.sleep(0.1) + break + except: + pass + return ''.join(total_data) + + try : + socket = container.exec_run([shell_cmd], stdin=True, socket=True, user=user).output._sock + if not cmd.endswith("\n"): + cmd = cmd + "\n" + socket.send(cmd.encode('utf-8')) + data = recv_socket_data(socket, timeout) + socket.close() + return data + + except Exception as e: + print("error - exec_cmd_container: %s" % str(e)) + traceback.print_exc(file=sys.stdout) + +def exec_run_handler(type, output): + if type == 'generic': + if output.exit_code == 0: + return jsonify(type='success', msg='command completed successfully') + else: + return jsonify(type='danger', msg='command failed: ' + output.output.decode('utf-8')) + if type == 'utf8_text_only': + r = Response(response=output.output.decode('utf-8'), status=200, mimetype="text/plain") + r.headers["Content-Type"] = "text/plain; charset=utf-8" + return r class GracefulKiller: kill_now = False @@ -308,84 +366,26 @@ class GracefulKiller: def exit_gracefully(self, signum, frame): self.kill_now = True +def create_self_signed_cert(): + process = subprocess.Popen( + "openssl req -x509 -newkey rsa:4096 -sha256 -days 3650 -nodes -keyout /app/dockerapi_key.pem -out /app/dockerapi_cert.pem -subj /CN=dockerapi/O=mailcow -addext subjectAltName=DNS:dockerapi".split(), + stdout = subprocess.PIPE, stderr = subprocess.PIPE, shell=False + ) + process.wait() + def startFlaskAPI(): create_self_signed_cert() try: ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) ctx.check_hostname = False - ctx.load_cert_chain(certfile='/cert.pem', keyfile='/key.pem') + ctx.load_cert_chain(certfile='/app/dockerapi_cert.pem', keyfile='/app/dockerapi_key.pem') except: - print "Cannot initialize TLS, retrying in 5s..." + print ("Cannot initialize TLS, retrying in 5s...") time.sleep(5) app.run(debug=False, host='0.0.0.0', port=443, threaded=True, ssl_context=ctx) -def recv_socket_data(c_socket, timeout=10): - c_socket.setblocking(0) - total_data=[]; - data=''; - begin=time.time() - while True: - if total_data and time.time()-begin > timeout: - break - elif time.time()-begin > timeout*2: - break - try: - data = c_socket.recv(8192) - if data: - total_data.append(data) - #change the beginning time for measurement - begin=time.time() - else: - #sleep for sometime to indicate a gap - time.sleep(0.1) - break - except: - pass - return ''.join(total_data) - -def exec_run_handler(type, output): - if type == 'generic': - if output.exit_code == 0: - return jsonify(type='success', msg='command completed successfully') - else: - return jsonify(type='danger', msg='command failed: ' + output.output) - if type == 'utf8_text_only': - r = Response(response=output.output, status=200, mimetype="text/plain") - r.headers["Content-Type"] = "text/plain; charset=utf-8" - return r - -def create_self_signed_cert(): - success = False - while not success: - try: - pkey = crypto.PKey() - pkey.generate_key(crypto.TYPE_RSA, 2048) - cert = crypto.X509() - cert.get_subject().O = "mailcow" - cert.get_subject().CN = "dockerapi" - cert.set_serial_number(int(uuid.uuid4())) - cert.gmtime_adj_notBefore(0) - cert.gmtime_adj_notAfter(10*365*24*60*60) - cert.set_issuer(cert.get_subject()) - cert.set_pubkey(pkey) - cert.sign(pkey, 'sha512') - cert = crypto.dump_certificate(crypto.FILETYPE_PEM, cert) - pkey = crypto.dump_privatekey(crypto.FILETYPE_PEM, pkey) - with os.fdopen(os.open('/cert.pem', os.O_WRONLY | os.O_CREAT, 0o644), 'w') as handle: - handle.write(cert) - with os.fdopen(os.open('/key.pem', os.O_WRONLY | os.O_CREAT, 0o600), 'w') as handle: - handle.write(pkey) - success = True - except: - time.sleep(1) - try: - os.remove('/cert.pem') - os.remove('/key.pem') - except OSError: - pass - api.add_resource(containers_get, '/containers/json') -api.add_resource(container_get, '/containers/<string:container_id>/json') +api.add_resource(container_get, '/containers/<string:container_id>/json') api.add_resource(container_post, '/containers/<string:container_id>/<string:post_action>') if __name__ == '__main__': @@ -397,5 +397,4 @@ if __name__ == '__main__': time.sleep(1) if killer.kill_now: break - print "Stopping dockerapi-mailcow" - + print ("Stopping dockerapi-mailcow") diff --git a/data/Dockerfiles/dovecot/Dockerfile b/data/Dockerfiles/dovecot/Dockerfile index 1320df1f..8bc60d9f 100644 --- a/data/Dockerfiles/dovecot/Dockerfile +++ b/data/Dockerfiles/dovecot/Dockerfile @@ -3,117 +3,112 @@ LABEL maintainer "Andre Peters <andre.peters@servercow.de>" ARG DEBIAN_FRONTEND=noninteractive ENV LC_ALL C -ENV DOVECOT_VERSION 2.3.4 -ENV PIGEONHOLE_VERSION 0.5.4 -RUN apt-get update && apt-get -y --no-install-recommends install \ - automake \ - autotools-dev \ - build-essential \ - ca-certificates \ - cpanminus \ - curl \ - default-libmysqlclient-dev \ - dnsutils \ - gettext \ - jq \ - libjson-webtoken-perl \ - libcgi-pm-perl \ - libcrypt-openssl-rsa-perl \ - libdata-uniqid-perl \ - libhtml-parser-perl \ - libmail-imapclient-perl \ - libparse-recdescent-perl \ - libsys-meminfo-perl \ - libtest-mockobject-perl \ - libwww-perl \ - libauthen-ntlm-perl \ - libbz2-dev \ - libcrypt-ssleay-perl \ - libcurl4-openssl-dev \ - libdbd-mysql-perl \ - libdbi-perl \ - libdigest-hmac-perl \ - libexpat1-dev \ - libfile-copy-recursive-perl \ - libio-compress-perl \ - libio-socket-inet6-perl \ - libio-socket-ssl-perl \ - libio-tee-perl \ - libipc-run-perl \ - libldap2-dev \ - liblockfile-simple-perl \ - liblz-dev \ - liblz4-dev \ - liblzma-dev \ - libmodule-scandeps-perl \ - libnet-ssleay-perl \ - libpam-dev \ - libpar-packer-perl \ - libreadonly-perl \ - libssl-dev \ - libterm-readkey-perl \ - libtest-pod-perl \ - libtest-simple-perl \ - libtry-tiny-perl \ - libunicode-string-perl \ - libproc-processtable-perl \ - libtest-nowarnings-perl \ - libtest-deep-perl \ - libtest-warn-perl \ - libregexp-common-perl \ - liburi-perl \ - lzma-dev \ - python-html2text \ - python-jinja2 \ - python-mysql.connector \ - python-redis \ - make \ - mysql-client \ - procps \ - supervisor \ - cron \ - redis-server \ - syslog-ng \ - syslog-ng-core \ - syslog-ng-mod-redis \ - && rm -rf /var/lib/apt/lists/* \ - && curl https://www.dovecot.org/releases/2.3/dovecot-$DOVECOT_VERSION.tar.gz | tar xvz \ - && cd dovecot-$DOVECOT_VERSION \ - && ./configure --with-solr --with-mysql --with-ldap --with-lzma --with-lz4 --with-ssl=openssl --with-notify=inotify --with-storages=mdbox,sdbox,maildir,mbox,imapc,pop3c --with-bzlib --with-zlib --enable-hardening \ - && make -j3 \ - && make install \ - && make clean \ - && cd .. && rm -rf dovecot-$DOVECOT_VERSION \ - && curl https://pigeonhole.dovecot.org/releases/2.3/dovecot-2.3-pigeonhole-$PIGEONHOLE_VERSION.tar.gz | tar xvz \ - && cd dovecot-2.3-pigeonhole-$PIGEONHOLE_VERSION \ - && ./configure \ - && make -j3 \ - && make install \ - && make clean \ - && cd .. \ - && rm -rf dovecot-2.3-pigeonhole-$PIGEONHOLE_VERSION \ - && cpanm Data::Uniqid Mail::IMAPClient String::Util \ - && groupadd -g 5000 vmail \ +# Add groups and users before installing Dovecot to not break compatibility +RUN groupadd -g 5000 vmail \ && groupadd -g 401 dovecot \ && groupadd -g 402 dovenull \ && useradd -g vmail -u 5000 vmail -d /var/vmail \ && useradd -c "Dovecot unprivileged user" -d /dev/null -u 401 -g dovecot -s /bin/false dovecot \ && useradd -c "Dovecot login user" -d /dev/null -u 402 -g dovenull -s /bin/false dovenull \ && touch /etc/default/locale \ - && apt-get purge -y build-essential automake autotools-dev default-libmysqlclient-dev libbz2-dev libcurl4-openssl-dev libexpat1-dev liblz-dev liblz4-dev liblzma-dev libpam-dev libssl-dev lzma-dev \ + && apt-get update \ + && apt-get -y --no-install-recommends install \ + apt-transport-https \ + ca-certificates \ + cpanminus \ + cron \ + curl \ + dnsutils \ + dirmngr \ + gettext \ + gnupg2 \ + jq \ + libauthen-ntlm-perl \ + libcgi-pm-perl \ + libcrypt-openssl-rsa-perl \ + libcrypt-ssleay-perl \ + libdata-uniqid-perl \ + libdbd-mysql-perl \ + libdbi-perl \ + libdigest-hmac-perl \ + libdist-checkconflicts-perl \ + libfile-copy-recursive-perl \ + libfile-tail-perl \ + libhtml-parser-perl \ + libio-compress-perl \ + libio-socket-inet6-perl \ + libio-socket-ssl-perl \ + libio-tee-perl \ + libipc-run-perl \ + libjson-webtoken-perl \ + liblockfile-simple-perl \ + libmail-imapclient-perl \ + libmodule-implementation-perl \ + libmodule-scandeps-perl \ + libnet-ssleay-perl \ + libpackage-stash-perl \ + libpackage-stash-xs-perl \ + libpar-packer-perl \ + libparse-recdescent-perl \ + libproc-processtable-perl \ + libreadonly-perl \ + libregexp-common-perl \ + libsys-meminfo-perl \ + libterm-readkey-perl \ + libtest-deep-perl \ + libtest-fatal-perl \ + libtest-mock-guard-perl \ + libtest-mockobject-perl \ + libtest-nowarnings-perl \ + libtest-pod-perl \ + libtest-requires-perl \ + libtest-simple-perl \ + libtest-warn-perl \ + libtry-tiny-perl \ + libunicode-string-perl \ + liburi-perl \ + libwww-perl \ + mysql-client \ + procps \ + python-html2text \ + python-jinja2 \ + python-mysql.connector \ + python-redis \ + redis-server \ + supervisor \ + syslog-ng \ + syslog-ng-core \ + syslog-ng-mod-redis \ + && apt-key adv --fetch-keys https://repo.dovecot.org/DOVECOT-REPO-GPG \ + && echo 'deb https://repo.dovecot.org/ce-2.3-latest/debian/stretch stretch main' > /etc/apt/sources.list.d/dovecot.list \ + && apt-get update \ + && apt-get -y --no-install-recommends install \ + dovecot-lua \ + dovecot-managesieved \ + dovecot-sieve \ + dovecot-lmtpd \ + dovecot-ldap \ + dovecot-mysql \ + dovecot-core \ + dovecot-pop3d \ + dovecot-imapd \ + dovecot-solr \ && apt-get autoremove --purge -y \ - && rm -rf /tmp/* /var/tmp/* + && apt-get autoclean \ + && rm -rf /var/lib/apt/lists/* \ + && rm -rf /tmp/* /var/tmp/* /etc/cron.daily/* COPY trim_logs.sh /usr/local/bin/trim_logs.sh +COPY clean_q_aged.sh /usr/local/bin/clean_q_aged.sh COPY syslog-ng.conf /etc/syslog-ng/syslog-ng.conf 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 report-spam.sieve /usr/local/lib/dovecot/sieve/report-spam.sieve -COPY report-ham.sieve /usr/local/lib/dovecot/sieve/report-ham.sieve -COPY rspamd-pipe-ham /usr/local/lib/dovecot/sieve/rspamd-pipe-ham -COPY rspamd-pipe-spam /usr/local/lib/dovecot/sieve/rspamd-pipe-spam +COPY report-spam.sieve /usr/lib/dovecot/sieve/report-spam.sieve +COPY report-ham.sieve /usr/lib/dovecot/sieve/report-ham.sieve +COPY rspamd-pipe-ham /usr/lib/dovecot/sieve/rspamd-pipe-ham +COPY rspamd-pipe-spam /usr/lib/dovecot/sieve/rspamd-pipe-spam COPY sa-rules.sh /usr/local/bin/sa-rules.sh COPY maildir_gc.sh /usr/local/bin/maildir_gc.sh COPY docker-entrypoint.sh / diff --git a/data/Dockerfiles/dovecot/clean_q_aged.sh b/data/Dockerfiles/dovecot/clean_q_aged.sh new file mode 100755 index 00000000..37ed1ff2 --- /dev/null +++ b/data/Dockerfiles/dovecot/clean_q_aged.sh @@ -0,0 +1,18 @@ +#!/bin/bash + +MAX_AGE=$(redis-cli --raw -h redis-mailcow GET Q_MAX_AGE) + +if [[ -z ${MAX_AGE} ]]; then + echo "Max age for quarantine items not defined" + exit 1 +fi + +NUM_REGEXP='^[0-9]+$' +if ! [[ ${MAX_AGE} =~ ${NUM_REGEXP} ]] ; then + echo "Max age for quarantine items invalid" + exit 1 +fi + +TO_DELETE=$(mysql --socket=/var/run/mysqld/mysqld.sock -u __DBUSER__ -p__DBPASS__ __DBNAME__ -e "SELECT COUNT(id) FROM quarantine WHERE created < NOW() - INTERVAL ${MAX_AGE//[!0-9]/} DAY" -BN) +mysql --socket=/var/run/mysqld/mysqld.sock -u __DBUSER__ -p__DBPASS__ __DBNAME__ -e "DELETE FROM quarantine WHERE created < NOW() - INTERVAL ${MAX_AGE//[!0-9]/} DAY" +echo "Deleted ${TO_DELETE} items from quarantine table (max age is ${MAX_AGE//[!0-9]/} days)" diff --git a/data/Dockerfiles/dovecot/docker-entrypoint.sh b/data/Dockerfiles/dovecot/docker-entrypoint.sh index 0589579d..f016b02a 100755 --- a/data/Dockerfiles/dovecot/docker-entrypoint.sh +++ b/data/Dockerfiles/dovecot/docker-entrypoint.sh @@ -16,10 +16,14 @@ sed -i "s/__DBUSER__/${DBUSER}/g" /usr/local/bin/quarantine_notify.py sed -i "s/__DBPASS__/${DBPASS}/g" /usr/local/bin/quarantine_notify.py sed -i "s/__DBNAME__/${DBNAME}/g" /usr/local/bin/quarantine_notify.py +sed -i "s/__DBUSER__/${DBUSER}/g" /usr/local/bin/clean_q_aged.sh +sed -i "s/__DBPASS__/${DBPASS}/g" /usr/local/bin/clean_q_aged.sh +sed -i "s/__DBNAME__/${DBNAME}/g" /usr/local/bin/clean_q_aged.sh + sed -i "s/__LOG_LINES__/${LOG_LINES}/g" /usr/local/bin/trim_logs.sh # Create missing directories -[[ ! -d /usr/local/etc/dovecot/sql/ ]] && mkdir -p /usr/local/etc/dovecot/sql/ +[[ ! -d /etc/dovecot/sql/ ]] && mkdir -p /etc/dovecot/sql/ [[ ! -d /var/vmail/_garbage ]] && mkdir -p /var/vmail/_garbage [[ ! -d /var/vmail/sieve ]] && mkdir -p /var/vmail/sieve [[ ! -d /etc/sogo ]] && mkdir -p /etc/sogo @@ -29,7 +33,8 @@ sed -i "s/__LOG_LINES__/${LOG_LINES}/g" /usr/local/bin/trim_logs.sh DBPASS=$(echo ${DBPASS} | sed 's/"/\\"/g') # Create quota dict for Dovecot -cat <<EOF > /usr/local/etc/dovecot/sql/dovecot-dict-sql-quota.conf +cat <<EOF > /etc/dovecot/sql/dovecot-dict-sql-quota.conf +# Autogenerated by mailcow connect = "host=/var/run/mysqld/mysqld.sock dbname=${DBNAME} user=${DBUSER} password=${DBPASS}" map { pattern = priv/quota/storage @@ -46,7 +51,8 @@ map { EOF # Create dict used for sieve pre and postfilters -cat <<EOF > /usr/local/etc/dovecot/sql/dovecot-dict-sql-sieve_before.conf +cat <<EOF > /etc/dovecot/sql/dovecot-dict-sql-sieve_before.conf +# Autogenerated by mailcow connect = "host=/var/run/mysqld/mysqld.sock dbname=${DBNAME} user=${DBUSER} password=${DBPASS}" map { pattern = priv/sieve/name/\$script_name @@ -68,7 +74,8 @@ map { } EOF -cat <<EOF > /usr/local/etc/dovecot/sql/dovecot-dict-sql-sieve_after.conf +cat <<EOF > /etc/dovecot/sql/dovecot-dict-sql-sieve_after.conf +# Autogenerated by mailcow connect = "host=/var/run/mysqld/mysqld.sock dbname=${DBNAME} user=${DBUSER} password=${DBPASS}" map { pattern = priv/sieve/name/\$script_name @@ -90,36 +97,41 @@ map { } EOF -echo -n ${ACL_ANYONE} > /usr/local/etc/dovecot/acl_anyone +echo -n ${ACL_ANYONE} > /etc/dovecot/acl_anyone if [[ "${SKIP_SOLR}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then -echo -n 'quota acl zlib listescape mail_crypt mail_crypt_acl mail_log notify' > /usr/local/etc/dovecot/mail_plugins -echo -n 'quota imap_quota imap_acl acl zlib imap_zlib imap_sieve listescape mail_crypt mail_crypt_acl notify mail_log' > /usr/local/etc/dovecot/mail_plugins_imap -echo -n 'quota sieve acl zlib listescape mail_crypt mail_crypt_acl' > /usr/local/etc/dovecot/mail_plugins_lmtp +echo -n 'quota acl zlib listescape mail_crypt mail_crypt_acl mail_log notify' > /etc/dovecot/mail_plugins +echo -n 'quota imap_quota imap_acl acl zlib imap_zlib imap_sieve listescape mail_crypt mail_crypt_acl notify mail_log' > /etc/dovecot/mail_plugins_imap +echo -n 'quota sieve acl zlib listescape mail_crypt mail_crypt_acl' > /etc/dovecot/mail_plugins_lmtp else -echo -n 'quota acl zlib listescape mail_crypt mail_crypt_acl mail_log notify fts fts_solr' > /usr/local/etc/dovecot/mail_plugins -echo -n 'quota imap_quota imap_acl acl zlib imap_zlib imap_sieve listescape mail_crypt mail_crypt_acl notify mail_log fts fts_solr' > /usr/local/etc/dovecot/mail_plugins_imap -echo -n 'quota sieve acl zlib listescape mail_crypt mail_crypt_acl fts fts_solr' > /usr/local/etc/dovecot/mail_plugins_lmtp +echo -n 'quota acl zlib listescape mail_crypt mail_crypt_acl mail_log notify fts fts_solr' > /etc/dovecot/mail_plugins +echo -n 'quota imap_quota imap_acl acl zlib imap_zlib imap_sieve listescape mail_crypt mail_crypt_acl notify mail_log fts fts_solr' > /etc/dovecot/mail_plugins_imap +echo -n 'quota sieve acl zlib listescape mail_crypt mail_crypt_acl fts fts_solr' > /etc/dovecot/mail_plugins_lmtp fi -chmod 644 /usr/local/etc/dovecot/mail_plugins /usr/local/etc/dovecot/mail_plugins_imap /usr/local/etc/dovecot/mail_plugins_lmtp /templates/quarantine.tpl +chmod 644 /etc/dovecot/mail_plugins /etc/dovecot/mail_plugins_imap /etc/dovecot/mail_plugins_lmtp /templates/quarantine.tpl -cat <<EOF > /usr/local/etc/dovecot/sql/dovecot-dict-sql-userdb.conf +cat <<EOF > /etc/dovecot/sql/dovecot-dict-sql-userdb.conf +# Autogenerated by mailcow driver = mysql connect = "host=/var/run/mysqld/mysqld.sock dbname=${DBNAME} user=${DBUSER} password=${DBPASS}" -user_query = SELECT CONCAT(JSON_UNQUOTE(JSON_EXTRACT(attributes, '$.mailbox_format')), mailbox_path_prefix, '%d/%n/:VOLATILEDIR=/var/volatile/%u') AS mail, 5000 AS uid, 5000 AS gid, concat('*:bytes=', quota) AS quota_rule FROM mailbox WHERE username = '%u' AND active = '1' +user_query = SELECT CONCAT(JSON_UNQUOTE(JSON_EXTRACT(attributes, '$.mailbox_format')), mailbox_path_prefix, '%d/%n/${MAILDIR_SUB}:VOLATILEDIR=/var/volatile/%u') AS mail, 5000 AS uid, 5000 AS gid, concat('*:bytes=', quota) AS quota_rule FROM mailbox WHERE username = '%u' AND active = '1' iterate_query = SELECT username FROM mailbox WHERE active='1'; EOF # Create pass dict for Dovecot -cat <<EOF > /usr/local/etc/dovecot/sql/dovecot-dict-sql-passdb.conf +cat <<EOF > /etc/dovecot/sql/dovecot-dict-sql-passdb.conf +# Autogenerated by mailcow driver = mysql connect = "host=/var/run/mysqld/mysqld.sock dbname=${DBNAME} user=${DBUSER} password=${DBPASS}" default_pass_scheme = SSHA256 password_query = SELECT password FROM mailbox WHERE active = '1' AND username = '%u' AND domain IN (SELECT domain FROM domain WHERE domain='%d' AND active='1') AND JSON_EXTRACT(attributes, '$.force_pw_update') NOT LIKE '%%1%%' EOF -# Create global sieve_after script -cat /usr/local/etc/dovecot/sieve_after > /var/vmail/sieve/global.sieve +# Migrate old sieve_after file +[[ -f /etc/dovecot/sieve_after ]] && mv /etc/dovecot/sieve_after /etc/dovecot/global_sieve_after +# Create global sieve scripts +cat /etc/dovecot/global_sieve_after > /var/vmail/sieve/global_sieve_after.sieve +cat /etc/dovecot/global_sieve_before > /var/vmail/sieve/global_sieve_before.sieve # Check permissions of vmail/attachments directory. # Do not do this every start-up, it may take a very long time. So we use a stat check here. @@ -127,14 +139,51 @@ if [[ $(stat -c %U /var/vmail/) != "vmail" ]] ; then chown -R vmail:vmail /var/v if [[ $(stat -c %U /var/vmail/_garbage) != "vmail" ]] ; then chown -R vmail:vmail /var/vmail/_garbage ; fi if [[ $(stat -c %U /var/attachments) != "vmail" ]] ; then chown -R vmail:vmail /var/attachments ; fi +# Cleanup random user maildirs +rm -rf /var/vmail/mailcow.local/* + + # 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}@mailcow.local:{SHA1}$(echo -n ${RAND_PASS} | sha1sum | awk '{print $1}') > /usr/local/etc/dovecot/dovecot-master.passwd -echo ${RAND_USER}@mailcow.local::5000:5000:::: > /usr/local/etc/dovecot/dovecot-master.userdb +echo ${RAND_USER}@mailcow.local:{SHA1}$(echo -n ${RAND_PASS} | sha1sum | awk '{print $1}') > /etc/dovecot/dovecot-master.passwd +echo ${RAND_USER}@mailcow.local::5000:5000:::: > /etc/dovecot/dovecot-master.userdb echo ${RAND_USER}@mailcow.local:${RAND_PASS} > /etc/sogo/sieve.creds +if [[ -z ${MAILDIR_SUB} ]]; then + MAILDIR_SUB_SHARED= +else + MAILDIR_SUB_SHARED=/${MAILDIR_SUB} +fi +cat <<EOF > /etc/dovecot/shared_namespace.conf +# Autogenerated by mailcow +namespace { + type = shared + separator = / + prefix = Shared/%%u/ + location = maildir:%%h${MAILDIR_SUB_SHARED}:INDEX=~${MAILDIR_SUB_SHARED}/Shared/%%u;CONTROL=~${MAILDIR_SUB_SHARED}/Shared/%%u + subscriptions = no + list = children +} +EOF + +if [[ "${ALLOW_ADMIN_EMAIL_LOGIN}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then + # Create random master Password for SOGo 'login as user' via proxy auth + RAND_PASS=$(cat /dev/urandom | tr -dc 'a-z0-9' | fold -w 32 | head -n 1) + echo -n ${RAND_PASS} > /etc/phpfpm/sogo-sso.pass + cat <<EOF > /etc/dovecot/sogo-sso.conf +# Autogenerated by mailcow +passdb { + driver = static + args = allow_real_nets=${IPV4_NETWORK}.248/32 password={plain}${RAND_PASS} +} +EOF +else + rm -f /etc/dovecot/sogo-sso.pass + rm -f /etc/dovecot/sogo-sso.conf +fi + # 401 is user dovecot if [[ ! -s /mail_crypt/ecprivkey.pem || ! -s /mail_crypt/ecpubkey.pem ]]; then openssl ecparam -name prime256v1 -genkey | openssl pkey -out /mail_crypt/ecprivkey.pem @@ -145,43 +194,46 @@ else fi # Compile sieve scripts -sievec /var/vmail/sieve/global.sieve -sievec /usr/local/lib/dovecot/sieve/report-spam.sieve -sievec /usr/local/lib/dovecot/sieve/report-ham.sieve +sievec /var/vmail/sieve/global_sieve_before.sieve +sievec /var/vmail/sieve/global_sieve_after.sieve +sievec /usr/lib/dovecot/sieve/report-spam.sieve +sievec /usr/lib/dovecot/sieve/report-ham.sieve # Fix permissions -chown root:root /usr/local/etc/dovecot/sql/*.conf -chown root:dovecot /usr/local/etc/dovecot/sql/dovecot-dict-sql-sieve* /usr/local/etc/dovecot/sql/dovecot-dict-sql-quota* -chmod 640 /usr/local/etc/dovecot/sql/*.conf +chown root:root /etc/dovecot/sql/*.conf +chown root:dovecot /etc/dovecot/sql/dovecot-dict-sql-sieve* /etc/dovecot/sql/dovecot-dict-sql-quota* +chmod 640 /etc/dovecot/sql/*.conf chown -R vmail:vmail /var/vmail/sieve chown -R vmail:vmail /var/volatile adduser vmail tty chmod g+rw /dev/console -chmod +x /usr/local/lib/dovecot/sieve/rspamd-pipe-ham \ - /usr/local/lib/dovecot/sieve/rspamd-pipe-spam \ +chown root:tty /dev/console +chmod +x /usr/lib/dovecot/sieve/rspamd-pipe-ham \ + /usr/lib/dovecot/sieve/rspamd-pipe-spam \ /usr/local/bin/imapsync_cron.pl \ /usr/local/bin/postlogin.sh \ /usr/local/bin/imapsync \ /usr/local/bin/trim_logs.sh \ /usr/local/bin/sa-rules.sh \ + /usr/local/bin/clean_q_aged.sh \ /usr/local/bin/maildir_gc.sh \ /usr/local/sbin/stop-supervisor.sh \ /usr/local/bin/quota_notify.py # Setup cronjobs echo '* * * * * root /usr/local/bin/imapsync_cron.pl 2>&1 | /usr/bin/logger' > /etc/cron.d/imapsync -echo '30 3 * * * vmail /usr/local/bin/doveadm quota recalc -A' > /etc/cron.d/dovecot-sync +#echo '30 3 * * * vmail /usr/local/bin/doveadm quota recalc -A' > /etc/cron.d/dovecot-sync echo '* * * * * vmail /usr/local/bin/trim_logs.sh >> /dev/console 2>&1' > /etc/cron.d/trim_logs echo '25 * * * * vmail /usr/local/bin/maildir_gc.sh >> /dev/console 2>&1' > /etc/cron.d/maildir_gc echo '30 1 * * * root /usr/local/bin/sa-rules.sh >> /dev/console 2>&1' > /etc/cron.d/sa-rules -echo '0 2 * * * root /usr/bin/curl http://solr:8983/solr/dovecot/update?optimize=true >> /dev/console 2>&1' > /etc/cron.d/solr-optimize +echo '0 2 * * * root /usr/bin/curl http://solr:8983/solr/dovecot-fts/update?optimize=true >> /dev/console 2>&1' > /etc/cron.d/solr-optimize echo '*/20 * * * * vmail /usr/local/bin/quarantine_notify.py >> /dev/console 2>&1' > /etc/cron.d/quarantine_notify - +echo '15 4 * * * vmail /usr/local/bin/clean_q_aged.sh >> /dev/console 2>&1' > /etc/cron.d/clean_q_aged # Fix more than 1 hardlink issue touch /etc/crontab /etc/cron.*/* # Clean old PID if any -[[ -f /usr/local/var/run/dovecot/master.pid ]] && rm /usr/local/var/run/dovecot/master.pid +[[ -f /var/run/dovecot/master.pid ]] && rm /var/run/dovecot/master.pid # Clean stopped imapsync jobs rm -f /tmp/imapsync_busy.lock @@ -191,6 +243,20 @@ IMAPSYNC_TABLE=$(mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBP # Envsubst maildir_gc echo "$(envsubst < /usr/local/bin/maildir_gc.sh)" > /usr/local/bin/maildir_gc.sh +PUBKEY_MCRYPT=$(doveconf -P | grep -i mail_crypt_global_public_key | cut -d '<' -f2) +if [ -f ${PUBKEY_MCRYPT} ]; then + GUID=$(cat <(echo ${MAILCOW_HOSTNAME}) /mail_crypt/ecpubkey.pem | sha256sum | cut -d ' ' -f1 | tr -cd "[a-fA-F0-9.:/] ") + if [ ${#GUID} -eq 64 ]; then + mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} << EOF +REPLACE INTO versions (application, version) VALUES ("GUID", "${GUID}"); +EOF + else + mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} << EOF +REPLACE INTO versions (application, version) VALUES ("GUID", "INVALID"); +EOF + fi +fi + # Collect SA rules once now /usr/local/bin/sa-rules.sh diff --git a/data/Dockerfiles/dovecot/imapsync b/data/Dockerfiles/dovecot/imapsync index cfbd1ee8..a75795b0 100755 --- a/data/Dockerfiles/dovecot/imapsync +++ b/data/Dockerfiles/dovecot/imapsync @@ -1,6 +1,6 @@ -#!/usr/bin/perl +#!/usr/bin/env perl -# $Id: imapsync,v 1.882 2018/05/05 21:10:43 gilles Exp gilles $ +# $Id: imapsync,v 1.937 2019/05/01 22:14:00 gilles Exp gilles $ # structure # pod documentation # use pragmas @@ -10,7 +10,7 @@ # default values # folder loop # subroutines -# sub usage { +# sub usage # pod documentation @@ -19,13 +19,13 @@ =head1 NAME -imapsync - Email IMAP tool for syncing, copying and migrating -email mailboxes between two imap servers, one way, +imapsync - Email IMAP tool for syncing, copying and migrating +email mailboxes between two imap servers, one way, and without duplicates. =head1 VERSION -This documentation refers to Imapsync $Revision: 1.882 $ +This documentation refers to Imapsync $Revision: 1.937 $ =head1 USAGE @@ -47,23 +47,23 @@ one another. Imapsync command is a tool allowing incremental and recursive imap transfers from one mailbox to another. -By default all folders are transferred, recursively, meaning -the whole folder hierarchy is taken, all messages in them, -and all messages flags (\Seen \Answered \Flagged etc.) +By default all folders are transferred, recursively, meaning +the whole folder hierarchy is taken, all messages in them, +and all messages flags (\Seen \Answered \Flagged etc.) are synced too. -Imapsync reduces the amount of data transferred by not transferring -a given message if it resides already on both sides. +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. -By default, the identification headers are -"Message-Id:" and "Received:" lines +Same specific headers and the transfer is done only once. +By default, the identification headers are +"Message-Id:" and "Received:" lines but this choice can be changed with the --useheader option. -All flags are preserved, unread messages will stay unread, +All flags are preserved, unread messages will stay unread, read ones will stay read, deleted will stay deleted. -You can stop the transfer at any time and restart it later, +You can stop the transfer at any time and restart it later, imapsync works well with bad connections and interruptions, by design. @@ -75,7 +75,7 @@ In that case, use the --delete1 option. Option --delete1 implies also option --expunge1 so all messages marked deleted on host1 will be really deleted. -You can also decide to remove empty folders once all of their +You can also decide to remove empty folders once all of their messages have been transferred. Add --delete1emptyfolders to obtain this behavior. @@ -98,7 +98,7 @@ Michael R. Elkins) for a 2 ways synchronization. usage: imapsync [options] Mandatory options are the six values, three on each sides, -needed to log in into the IMAP servers, ie, +needed to log in into the IMAP servers, ie, a host, a username, and a password, two times. Conventions used: @@ -108,20 +108,20 @@ Conventions used: reg means regular expression cmd means command - --dry : Makes imapsync doing nothing for real, just print what - would be done without --dry. + --dry : Makes imapsync doing nothing for real, just print what + would be done without --dry. =head2 OPTIONS/credentials --host1 str : Source or "from" imap server. Mandatory. - --port1 int : Port to connect on host1. + --port1 int : Port to connect on host1. Optional since default port is 143 or 993 if --ssl1 --user1 str : User to login on host1. Mandatory. --password1 str : Password for the user1. --host2 str : "destination" imap server. Mandatory. - --port2 int : Port to connect on host2. + --port2 int : Port to connect on host2. Optional since default port is 143 or 993 if --ssl2 --user2 str : User to login on host2. Mandatory. --password2 str : Password for the user2. @@ -135,6 +135,9 @@ Conventions used: the password on the command line like --password1 does. --passfile2 str : Password file for the user2. Contains the password. +You can also pass the passwords in the environment variables +IMAPSYNC_PASSWORD1 and IMAPSYNC_PASSWORD2 + =head2 OPTIONS/encryption --nossl1 : Do not use a SSL connection on host1. @@ -219,10 +222,18 @@ Conventions used: --f1f2 str1=str2 : Force folder str1 to be synced to str2, --f1f2 overrides --automap and --regextrans2. - --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. + --subfolder2 str : Syncs the whole host1 folders hierarchy under the + host2 folder named str. + It does it internally by adding three + --regextrans2 options before all others. + Add --debug to see what's really going on. + + --subfolder1 str : Syncs the host1 folders hierarchy under str + to the root hierarchy of host2. + It's the couterpart of a sync done by --subfolder2 + in the reverse order. Use --subfolder2 str + for a backup under str and --subfolder1 str + for the restore from str. --subscribed : Transfers subscribed folders. --subscribe : Subscribe to the folders transferred on the @@ -252,7 +263,7 @@ Conventions used: --nofoldersizes : Do not calculate the size of each folder at the beginning of the sync. Default is to calculate them. - --nofoldersizesatend: Do not calculate the size of each folder at the + --nofoldersizesatend: Do not calculate the size of each folder at the end of the sync. Default is to calculate them. --justfoldersizes : Exit after having printed the initial folder sizes. @@ -306,7 +317,7 @@ Conventions used: --resyncflags : Resync flags for already transferred messages. On by default. --noresyncflags : Do not resync flags for already transferred messages. - May be useful when a user has already started to play + May be useful when a user has already started to play with its host2 account. =head2 OPTIONS/deletions @@ -377,7 +388,7 @@ Conventions used: command. Applied on both sides. For a complete of what can be search see https://imapsync.lamiral.info/FAQ.d/FAQ.Messages_Selection.txt - + --search1 str : Same as --search but for selecting host1 messages only. --search2 str : Same as --search but for selecting host2 messages only. --search CRIT equals --search1 CRIT --search2 CRIT @@ -403,7 +414,10 @@ Conventions used: --syncacls : Synchronizes acls (Access Control Lists). --nosyncacls : Does not synchronize acls. This is the default. Acls in IMAP are not standardized, be careful. - + --addheader : When a message has no headers to be identified, + --addheader adds a "Message-Id" header, + like "Message-Id: 12345@imapsync", where 12345 + is the imap UID of the message on the host1 folder. =head2 OPTIONS/debugging @@ -426,35 +440,35 @@ Conventions used: Useful to check the ipv6 connectivity. Needs internet. -=head2 OPTIONS/specific +=head2 OPTIONS/specific --gmail1 : sets --host1 to Gmail and options from FAQ.Gmail.txt --gmail2 : sets --host2 to Gmail and options from FAQ.Gmail.txt - + --office1 : sets --host1 to Office365 options from FAQ.Exchange.txt --office2 : sets --host2 to Office365 options from FAQ.Exchange.txt --exchange1 : sets options from FAQ.Exchange.txt, account1 part --exchange2 : sets options from FAQ.Exchange.txt, account2 part - + --domino1 : sets options from FAQ.Domino.txt, account1 part --domino2 : sets options from FAQ.Domino.txt, account2 part - - - -=head2 OPTIONS/behavior + + + +=head2 OPTIONS/behavior --maxmessagespersecond int : limits the number of messages transferred per second. - + --maxbytespersecond int : limits the average transfer rate per second. - --maxbytesafter int : starts --maxbytespersecond limitation only after + --maxbytesafter int : starts --maxbytespersecond limitation only after --maxbytesafter amount of data transferred. - + --maxsleep int : do not sleep more than int seconds. On by default, 2 seconds max, like --maxsleep 2 - --abort : terminates a previous call still running. + --abort : terminates a previous call still running. It uses the pidfile to know what process to abort. --exitwhenover int : Stop syncing when total bytes transferred reached. @@ -498,12 +512,12 @@ dangerous because of the 'ps auxwwwwe' command. So, saving the password in a well protected file (600 or rw-------) is the best solution. -Imapsync activates ssl or tls encryption by default, if possible. -What detailed behavior is under this "if possible"? -Imapsync activates ssl if the well known port imaps port (993) is open -on the imap servers. If the imaps port is closed then it open a -normal (clear) connection on port 143 but it looks for TLS support -in the CAPABILITY list of the servers. If TLS is supported +Imapsync activates ssl or tls encryption by default, if possible. +What detailed behavior is under this "if possible"? +Imapsync activates ssl if the well known port imaps port (993) is open +on the imap servers. If the imaps port is closed then it open a +normal (clear) connection on port 143 but it looks for TLS support +in the CAPABILITY list of the servers. If TLS is supported then imapsync goes to encryption. If the automatic ssl/tls detection fails then imapsync will @@ -519,7 +533,28 @@ or at https://imapsync.lamiral.info/FAQ.d/FAQ.Security.txt Imapsync will exit with a 0 status (return code) if everything went good. Otherwise, it exits with a non-zero status. +Here is the list of the exit code values (an integer between 0 and 255), +the names reflects their meaning: +=for comment +egrep '^Readonly my.*\$EX' imapsync | egrep -o 'EX.*' | sed 's_^_ _' + + + EX_OK => 0 ; #/* successful termination */ + EX_USAGE => 64 ; #/* command line usage error */ + EX_NOINPUT => 66 ; #/* cannot open input */ + EX_UNAVAILABLE => 69 ; #/* service unavailable */ + EX_SOFTWARE => 70 ; #/* internal software error */ + EXIT_CATCH_ALL => 1 ; # Any other error + EXIT_BY_SIGNAL => 6 ; # Should be 128+n where n is the sig_num + EXIT_PID_FILE_ERROR => 8 ; + EXIT_CONNECTION_FAILURE => 10 ; + EXIT_TLS_FAILURE => 12 ; + EXIT_AUTHENTICATION_FAILURE => 16 ; + EXIT_SUBFOLDER1_NO_EXISTS => 21 ; + EXIT_WITH_ERRORS => 111 ; + EXIT_WITH_ERRORS_MAX => 112 ; + EXIT_TESTS_FAILED => 254 ; # Like Test::More API =head1 LICENSE AND COPYRIGHT @@ -546,7 +581,7 @@ Feedback good or bad is very often welcome. Gilles LAMIRAL earns his living by writing, installing, configuring and teaching free, open and often gratis software. Imapsync used to be "always gratis" but now it is -only "often gratis" because imapsync is sold by its author, +only "often gratis" because imapsync is sold by its author, a good way to maintain and support free open public software over decades. @@ -609,13 +644,13 @@ https://imapsync.lamiral.info/examples/ =head1 INSTALL Imapsync works under any Unix with perl. - + Imapsync works under most Windows (2000, XP, Vista, Seven, Eight, Ten and all Server releases 2000, 2003, 2008 and R2, 2012 and R2) as a standalone binary software called imapsync.exe, - usually launched from a batch file in order to avoid always typing + usually launched from a batch file in order to avoid always typing the options. - + Imapsync works under OS X as a standalone binary software called imapsync_bin_Darwin @@ -652,38 +687,54 @@ Feel free to hack imapsync as the NOLIMIT license permits it. See also https://imapsync.lamiral.info/S/external.shtml for a better up to date list. - imap_tools : https://github.com/andrewnimmo/rick-sanders-imap-tools - offlineimap : https://github.com/nicolas33/offlineimap - Doveadm-Sync : http://wiki2.dovecot.org/Tools/Doveadm/Sync - ( Dovecot sync tool ) - 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://www.ardiehl.de/imapcopy/ - 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/ +Last updated and verified on Thu Apr 11, 2019. - -Feedback (good or bad) will often be welcome. + imapsync : https://github.com/imapsync/imapsync + (this is an imapsync copy, sometimes delayed, + with --noreleasecheck by default since release 1.592, 2014/05/22) + imap_tools : https://web.archive.org/web/20161228145952/http://www.athensfbc.com/imap_tools/ + The imap_tools code is now at + https://github.com/andrewnimmo/rick-sanders-imap-tools + imaputils : https://github.com/mtsatsenko/imaputils (very old imap_tools fork) + Doveadm-Sync : https://wiki2.dovecot.org/Tools/Doveadm/Sync ( Dovecot sync tool ) + davmail : http://davmail.sourceforge.net/ + offlineimap : http://offlineimap.org/ + mbsync : http://isync.sourceforge.net/ + mailsync : http://mailsync.sourceforge.net/ + mailutil : http://www.washington.edu/imap/ part of the UW IMAP tookit. + imaprepl : https://bl0rg.net/software/ http://freecode.com/projects/imap-repl/ + imapcopy (Pascal): http://www.ardiehl.de/imapcopy/ + imapcopy (Java) : https://code.google.com/archive/p/imapcopy/ + imapsize : http://www.broobles.com/imapsize/ + migrationtool : http://sourceforge.net/projects/migrationtool/ + imapmigrate : http://sourceforge.net/projects/cyrus-utils/ + larch : https://github.com/rgrove/larch (derived from wonko_imapsync, good at Gmail) + wonko_imapsync : http://wonko.com/article/554 (superseded by larch) + pop2imap : http://www.linux-france.org/prj/pop2imap/ (I wrote that too) + exchange-away : http://exchange-away.sourceforge.net/ + SyncBackPro : http://www.2brightsparks.com/syncback/sbpro.html + ImapSyncClient : https://github.com/ridaamirini/ImapSyncClient + MailStore : https://www.mailstore.com/en/products/mailstore-home/ + mnIMAPSync : https://github.com/manusa/mnIMAPSync + imap-upload : http://imap-upload.sourceforge.net/ + (a tool for uploading a local mbox file to IMAP4 server) =head1 HISTORY I wrote imapsync because an enterprise (basystemes) paid me to install a new imap server without losing huge old mailboxes located in a far -away remote imap server, accessible by a low-bandwidth often broken link. -The tool imapcp (written in python) could not help me because I had to verify -every mailbox was well transferred, and then delete it after a good -transfer. Imapsync started its life as a patch of the copy_folder.pl +away remote imap server, accessible by an often broken low-bandwidth ISDN link. + +I had to verify every mailbox was well transferred, all folders, all messages, +without wasting bandwidth or creating duplicates upon resyncs. The design was +made with the rsync command in mind. + +Imapsync started its life as a patch of the copy_folder.pl script. The script copy_folder.pl comes from the Mail-IMAPClient-2.1.3 perl -module tarball source (more precisely in the examples/ directory of the -Mail-IMAPClient tarball). So many happened since then that I wonder +module tarball source (more precisely in the examples/ directory of the +Mail-IMAPClient tarball). + +So many happened since then that I wonder if it remains any lines of the original copy_folder.pl in imapsync source code. @@ -728,6 +779,8 @@ use Cwd ; use Readonly ; use Sys::MemInfo ; use Regexp::Common ; +use Text::ParseWords; +use File::Tail ; local $OUTPUT_AUTOFLUSH = 1 ; @@ -735,6 +788,10 @@ local $OUTPUT_AUTOFLUSH = 1 ; # Let us do like sysexits.h # /usr/include/sysexits.h +# and https://www.tldp.org/LDP/abs/html/exitcodes.html + +# Should avoid 2 126 127 128..128+64=192 255 +# Should use 0 1 3..125 193..254 Readonly my $EX_OK => 0 ; #/* successful termination */ Readonly my $EX_USAGE => 64 ; #/* command line usage error */ @@ -754,21 +811,35 @@ Readonly my $EX_SOFTWARE => 70 ; #/* internal software error */ #Readonly my $EX_CONFIG => 78 ; #/* configuration error */ # Mine -Readonly my $EXIT_BY_SIGNAL => 6 ; +Readonly my $EXIT_CATCH_ALL => 1 ; # Any other error + +Readonly my $EXIT_BY_SIGNAL => 6 ; # Should be 128+n where n is the sig_num Readonly my $EXIT_PID_FILE_ERROR => 8 ; +Readonly my $EXIT_CONNECTION_FAILURE => 10 ; +Readonly my $EXIT_TLS_FAILURE => 12 ; +Readonly my $EXIT_AUTHENTICATION_FAILURE => 16 ; +Readonly my $EXIT_SUBFOLDER1_NO_EXISTS => 21 ; + Readonly my $EXIT_WITH_ERRORS => 111 ; Readonly my $EXIT_WITH_ERRORS_MAX => 112 ; -Readonly my $EXIT_UNKNOWN => 126 ; + Readonly my $EXIT_TESTS_FAILED => 254 ; # Like Test::More API + + + + + + Readonly my $DEFAULT_LOGDIR => 'LOG_imapsync' ; Readonly my $ERRORS_MAX => 50 ; # exit after 50 errors. Readonly my $ERRORS_MAX_CGI => 20 ; # exit after 20 errors in CGI context. + 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. @@ -804,6 +875,8 @@ Readonly my $NUMBER_42 => 42 ; Readonly my $NUMBER_100 => 100 ; Readonly my $NUMBER_200 => 200 ; Readonly my $NUMBER_300 => 300 ; +Readonly my $NUMBER_123456 => 123456 ; +Readonly my $NUMBER_654321 => 654321 ; Readonly my $NUMBER_20_000 => 20_000 ; @@ -828,6 +901,14 @@ Readonly my $UMASK_PARANO => '0077' ; Readonly my $STR_use_releasecheck => q{Check if a new imapsync release is available by adding --releasecheck} ; +Readonly my $GMAIL_MAXSIZE => 35_651_584 ; + + +# if ( 'MSWin32' eq $OSNAME ) +# if ( 'darwin' eq $OSNAME ) +# if ( 'linux' eq $OSNAME ) + + # global variables # Currently working to finish with only $sync @@ -835,49 +916,39 @@ Readonly my $STR_use_releasecheck => q{Check if a new imapsync release is availa my( $sync, - $debug, $debugimap, $debugimap1, $debugimap2, $debugcontent, $debugflags, + $debugimap, $debugimap1, $debugimap2, $debugcontent, $debugflags, $debuglist, $debugdev, $debugmaxlinelength, $debugcgi, $domain1, $domain2, - $passfile1, $passfile2, @include, @exclude, @folderrec, @folderfirst, @folderlast, $prefix1, $prefix2, - $subfolder2, - @regextrans2, @regexmess, @regexflag, @skipmess, @pipemess, $pipemesscheck, + @regexmess, @regexflag, @skipmess, @pipemess, $pipemesscheck, $flagscase, $filterflags, $syncflagsaftercopy, - $sep1, $sep2, $syncinternaldates, $idatefromheader, $syncacls, $fastio1, $fastio2, - $maxsize, $minsize, $maxage, $minage, - $exitwhenover, + $minsize, $maxage, $minage, $search, $search1, $search2, $skipheader, @useheader, $skipsize, $allowsizemismatch, $foldersizes, $foldersizesatend, $buffersize, - $delete1, $delete2, $delete2duplicates, - $expunge1, $expunge2, $uidexpunge2, - $justfoldersizes, + + $authmd5, $authmd51, $authmd52, $subscribed, $subscribe, $subscribeall, $help, - $justfolders, $justbanner, + $justbanner, $fast, - $total_bytes_skipped, - $total_bytes_error, - $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, @@ -897,10 +968,7 @@ my( $delete2folders, $delete2foldersonly, $delete2foldersbutnot, $usecache, $debugcache, $cacheaftercopy, $wholeheaderifneeded, %h1_msgs_copy_by_uid, $useuid, $h2_uidguess, - %h1, %h2, $checkmessageexists, - $expungeaftereach, - $fixslash2, $messageidnodomain, $fixInboxINBOX, $maxlinelength, $maxlinelengthcmd, @@ -912,18 +980,18 @@ my( $disarmreadreceipts, $mixfolders, $skipemptyfolders, $fetch_hash_set, -); +) ; # main program # global variables initialization -# Currently removing all global variables except $sync +# I'm currently removing all global variables except $sync # passing each of them under $sync->{variable_name} $sync->{timestart} = time ; # Is a float because of use Time::HiRres -$sync->{rcs} = q{$Id: imapsync,v 1.882 2018/05/05 21:10:43 gilles Exp gilles $} ; +$sync->{rcs} = q{$Id: imapsync,v 1.937 2019/05/01 22:14:00 gilles Exp gilles $} ; $sync->{ memory_consumption_at_start } = memory_consumption( ) || 0 ; @@ -932,26 +1000,27 @@ my @loadavg = loadavg( ) ; $sync->{cpu_number} = cpu_number( ) ; $sync->{loaddelay} = load_and_delay( $sync->{cpu_number}, @loadavg ) ; -$sync->{loadavg} = join( q{ }, $loadavg[ 0 ] ) +$sync->{loadavg} = join( q{ }, $loadavg[ 0 ] ) . " on $sync->{cpu_number} cores and " . ram_memory_info( ) ; -$sync->{total_bytes_transferred} = 0 ; -$total_bytes_skipped = 0; -$total_bytes_error = 0; -$sync->{nb_msg_transferred} = 0; -$nb_msg_skipped = $nb_msg_skipped_dry_mode = 0; -$h1_nb_msg_deleted = $h2_nb_msg_deleted = 0; +$sync->{ total_bytes_transferred } = 0 ; +$sync->{ total_bytes_skipped } = 0; +$sync->{ nb_msg_transferred } = 0; +$sync->{ nb_msg_skipped } = $nb_msg_skipped_dry_mode = 0; +$sync->{ 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; +$sync->{ h1_nb_msg_noheader } = 0 ; +$h2_nb_msg_noheader = 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 ; +$sync->{ h1_nb_msg_processed } = $h1_bytes_processed = 0 ; + +$sync->{ h2_nb_msg_crossdup } = 0 ; #$h1_nb_msg_end = $h1_bytes_end = 0 ; #$h2_nb_msg_end = $h2_bytes_end = 0 ; @@ -977,7 +1046,7 @@ my %month_abrev = ( my $cgidir ; -# Just create a CGI object if under cgi context only. +# Just create a CGI object if under cgi context only. # Needed for the get_options() call cgibegin( $sync ) ; @@ -990,8 +1059,7 @@ cgibuildheader( $sync ) ; myprint( output( $sync ) ) ; output_reset_with( $sync ) ; -# Can break here if load is too heavy -cgiload( $sync ) ; +# Old place for cgiload( $sync ) ; # don't go on if options are not all known. if ( ! defined $options_good ) { exit $EX_USAGE ; } @@ -1001,8 +1069,8 @@ if ( ! defined $options_good ) { exit $EX_USAGE ; } # the second line (ending with "1 ;") can then stay active or be commented, # the result will be the same: no releasecheck by default (because 0 is then the defined value). -$sync->{releasecheck} = defined $sync->{releasecheck} ? $sync->{releasecheck} : 0 ; -#$sync->{releasecheck} = defined $sync->{releasecheck} ? $sync->{releasecheck} : 1 ; +#$sync->{releasecheck} = defined $sync->{releasecheck} ? $sync->{releasecheck} : 0 ; +$sync->{releasecheck} = defined $sync->{releasecheck} ? $sync->{releasecheck} : 1 ; # just the version if ( $sync->{ version } ) { @@ -1020,8 +1088,12 @@ after_get_options( $sync, $options_good ) ; # Under CGI environment, fix caveat emptor potential issues cgisetcontext( $sync ) ; +# --gmail --gmail --exchange --office etc. easyany( $sync ) ; +$sync->{ sanitize } = defined $sync->{ sanitize } ? $sync->{ sanitize } : 1 ; +sanitize( $sync ) ; + $sync->{ tmpdir } ||= File::Spec->tmpdir( ) ; # Unit tests @@ -1032,41 +1104,54 @@ testslive( $sync ) if ( $sync->{testslive} ) ; testslive6( $sync ) if ( $sync->{testslive6} ) ; # + $sync->{pidfile} = defined $sync->{pidfile} ? $sync->{pidfile} : $sync->{ tmpdir } . '/imapsync.pid' ; $sync->{pidfilelocking} = defined $sync->{pidfilelocking} ? $sync->{pidfilelocking} : 0 ; # old abort place -@{ $sync->{ sigexit } } = ( defined( $sync->{ sigexit } ) ) ? @{ $sync->{ sigexit } } : ( 'QUIT', 'TERM' ) ; +# Unix signals +@{ $sync->{ sigexit } } = ( defined( $sync->{ sigexit } ) ) ? @{ $sync->{ sigexit } } : ( 'QUIT', 'TERM' ) ; @{ $sync->{ sigreconnect } } = ( defined( $sync->{ sigreconnect } ) ) ? @{ $sync->{ sigreconnect } } : ( 'INT' ) ; +@{ $sync->{ sigprint } } = ( defined( $sync->{ sigprint } ) ) ? @{ $sync->{ sigprint } } : ( 'HUP' ) ; +@{ $sync->{ sigignore } } = ( defined( $sync->{ sigignore } ) ) ? @{ $sync->{ sigignore } } : ( ) ; -sig_install( $sync, \&catch_exit, @{ $sync->{ sigexit } } ) ; +local %SIG = %SIG ; +sig_install( $sync, \&catch_exit, @{ $sync->{ sigexit } } ) ; sig_install( $sync, \&catch_reconnect, @{ $sync->{ sigreconnect } } ) ; -# --sigignore can override sigexit and sigreconnect (for the same signals only) +sig_install( $sync, \&catch_print, @{ $sync->{ sigprint } } ) ; +# --sigignore can override sigexit, sigreconnect and sigprint (for the same signals only) sig_install( $sync, \&catch_ignore, @{ $sync->{ sigignore } } ) ; -sig_install( $sync, \&toggle_sleep, 'USR1' ) ; +sig_install_toggle_sleep( $sync ) ; + $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 ; - +# log and output if ( $sync->{log} ) { setlogfile( $sync ) ; teelaunch( $sync ) ; # now $sync->{tee} is a filehandle to STDOUT and the logfile } # STDERR goes to the same place: LOG and STDOUT (if logging is on) -$sync->{tee} and local *STDERR = *${$sync->{tee}}{IO} ; - +$sync->{tee} and local *STDERR = *${$sync->{tee}}{IO} ; + + + $timestart_int = int( $sync->{timestart} ) ; $timebefore = $sync->{timestart} ; + my $timestart_str = localtime( $sync->{timestart} ) ; + +# The prints in the log starts here + myprint( localhost_info( $sync ), "\n" ) ; myprint( "Transfer started at $timestart_str\n" ) ; -myprint( "PID is $PROCESS_ID\n" ) ; +myprint( "PID is $PROCESS_ID my PPID is ", mygetppid( ), "\n" ) ; myprint( "Log file is $sync->{logfile} ( to change it, use --logfile path ; or use --nolog to turn off logging )\n" ) if ( $sync->{log} ) ; myprint( "Load is " . ( join( q{ }, loadavg( ) ) || 'unknown' ), " on $sync->{cpu_number} cores\n" ) ; #myprintf( "Memory consumption so far: %.1f MiB\n", memory_consumption( ) / $KIBI / $KIBI ) ; @@ -1076,7 +1161,6 @@ myprint( 'Effective user id is ' . getpwuid_any_os( $EFFECTIVE_USER_ID ). " (eui $modulesversion = defined $modulesversion ? $modulesversion : 1 ; - my $warn_release = ( $sync->{releasecheck} ) ? check_last_release( ) : $STR_use_releasecheck ; @@ -1089,7 +1173,7 @@ $cacheaftercopy = 1 if ( $usecache and ( ! defined $cacheaftercopy ) ) ; $sync->{ checkselectable } = defined $sync->{ checkselectable } ? $sync->{ checkselectable } : 1 ; $sync->{ checkfoldersexist } = defined $sync->{ checkfoldersexist } ? $sync->{ checkfoldersexist } : 1 ; $checkmessageexists = defined $checkmessageexists ? $checkmessageexists : 0 ; -$expungeaftereach = defined $expungeaftereach ? $expungeaftereach : 1 ; +$sync->{ expungeaftereach } = defined $sync->{ expungeaftereach } ? $sync->{ expungeaftereach } : 1 ; # abletosearch is on by default $sync->{abletosearch} = defined $sync->{abletosearch} ? $sync->{abletosearch} : 1 ; @@ -1099,41 +1183,65 @@ $checkmessageexists = 0 if ( not $sync->{abletosearch1} ) ; $sync->{showpasswords} = defined $sync->{showpasswords} ? $sync->{showpasswords} : 0 ; -$fixslash2 = defined $fixslash2 ? $fixslash2 : 1 ; +$sync->{ fixslash2 } = defined $sync->{ fixslash2 } ? $sync->{ 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 ) ) ; +$sync->{ delete2duplicates } = 1 if ( $sync->{ delete2 } and ( ! defined $sync->{ delete2duplicates } ) ) ; $sync->{maxmessagespersecond} = defined $sync->{maxmessagespersecond} ? $sync->{maxmessagespersecond} : 0 ; $sync->{maxbytespersecond} = defined $sync->{maxbytespersecond} ? $sync->{maxbytespersecond} : 0 ; $sync->{sslcheck} = defined $sync->{sslcheck} ? $sync->{sslcheck} : 1 ; -myprint( banner_imapsync( @ARGV ) ) ; +myprint( banner_imapsync( $sync, @ARGV ) ) ; myprint( "Temp directory is $sync->{ tmpdir } ( to change it use --tmpdir dirpath )\n" ) ; + myprint( output( $sync ) ) ; +output_reset_with( $sync ) ; do_valid_directory( $sync->{ tmpdir } ) || croak "Error creating tmpdir $sync->{ tmpdir } : $OS_ERROR" ; remove_pidfile_not_running( $sync->{pidfile} ) ; +# if another imapsync is running then tail -f its logfile and exit +# useful in cgi context +if ( $sync->{tail} and tail( $sync ) ) +{ + myprint( "Tail -f finished. Now finishing myself\n" ) ; + exit_clean( $sync, $EX_OK ) ; + exit $EX_OK ; +} if ( ! write_pidfile( $sync ) ) { + myprint( "Exiting with return value $EXIT_PID_FILE_ERROR\n" ) ; exit $EXIT_PID_FILE_ERROR ; } -# simulong is just a loop printing some lines for xx seconds with option "--simulong xx". -if ( $sync->{simulong} ) { simulong( $sync->{simulong} ) ; } -# New place to abort -if ( $sync->{abort} ) { - abort( $sync ) ; +# New place for abort +# abort before simulong in order to be able to abort a simulong sync +if ( $sync->{ abort } ) +{ + abort( $sync ) ; } +# simulong is just a loop printing some lines for xx seconds with option "--simulong xx". +if ( $sync->{ simulong } ) +{ + simulong( $sync->{ simulong } ) ; +} + + +# New place for cgiload 2019_03_03 +# because I want to log it +# Can break here if load is too heavy +cgiload( $sync ) ; + + $fixcolonbug = defined $fixcolonbug ? $fixcolonbug : 1 ; if ( $usecache and $fixcolonbug ) { tmpdir_fix_colon_bug( $sync ) } ; @@ -1141,7 +1249,7 @@ if ( $usecache and $fixcolonbug ) { tmpdir_fix_colon_bug( $sync ) } ; $modulesversion and myprint( "Modules version list:\n", modulesversion(), "( use --no-modulesversion to turn off printing this Perl modules list )\n" ) ; -check_lib_version( ) or +check_lib_version( $sync ) or croak "imapsync needs perl lib Mail::IMAPClient release 3.30 or superior.\n"; @@ -1167,21 +1275,20 @@ if ( $sync->{resyncflags} ) { } - sslcheck( $sync ) ; - +#print Data::Dumper->Dump( [ \$sync ] ) ; $split1 ||= $SPLIT ; $split2 ||= $SPLIT ; -$sync->{host1} || missing_option( '--host1' ) ; +$sync->{host1} || missing_option( $sync, '--host1' ) ; $sync->{port1} ||= ( $sync->{ssl1} ) ? $IMAP_SSL_PORT : $IMAP_PORT ; -$sync->{host2} || missing_option( '--host2' ) ; +$sync->{host2} || missing_option( $sync, '--host2' ) ; $sync->{port2} ||= ( $sync->{ssl2} ) ? $IMAP_SSL_PORT : $IMAP_PORT ; $debugimap1 = $debugimap2 = 1 if ( $debugimap ) ; -$debug = 1 if ( $debugimap1 or $debugimap2 ) ; +$sync->{ debug } = 1 if ( $debugimap1 or $debugimap2 ) ; # By default, don't take size to compare $skipsize = (defined $skipsize) ? $skipsize : 1; @@ -1200,8 +1307,6 @@ if ( defined $delete2foldersbutnot or defined $delete2foldersonly ) { } - - my $SSL_VERIFY_POLICY ; my %SSL_VERIFY_STR ; @@ -1210,8 +1315,8 @@ Readonly %SSL_VERIFY_STR => ( IO::Socket::SSL::SSL_VERIFY_NONE( ) => 'SSL_VERIFY_NONE, ie, do not check the certificate server.' , IO::Socket::SSL::SSL_VERIFY_PEER( ) => 'SSL_VERIFY_PEER, ie, check the certificate server' , ) ; -$IO::Socket::SSL::DEBUG = $sync->{debugssl} || 1 ; +$IO::Socket::SSL::DEBUG = defined( $sync->{debugssl} ) ? $sync->{debugssl} : 1 ; if ( $sync->{ssl1} or $sync->{ssl2} or $sync->{tls1} or $sync->{tls2}) { @@ -1229,15 +1334,15 @@ if ( $sync->{ssl2} ) { } - if ( $sync->{justconnect} ) { - justconnect( ) ; + justconnect( $sync ) ; myprint( debugmemory( $sync, " after justconnect() call" ) ) ; exit_clean( $sync, $EX_OK ) ; } -$sync->{user1} || missing_option( '--user1' ) ; -$sync->{user2} || missing_option( '--user2' ) ; + +$sync->{user1} || missing_option( $sync, '--user1' ) ; +$sync->{user2} || missing_option( $sync, '--user2' ) ; $syncinternaldates = defined $syncinternaldates ? $syncinternaldates : 1; @@ -1246,30 +1351,30 @@ $syncinternaldates = defined $syncinternaldates ? $syncinternaldates : 1; # Done because --delete1 --noexpunge1 is very dangerous on the second run: # the Deleted flag is then synced to all previously transferred messages. # So --delete1 implies --expunge1 is a better usability default behavior. -if ( $delete1 ) { - if ( ! defined $expunge1 ) { +if ( $sync->{ delete1 } ) { + if ( ! defined $sync->{ expunge1 } ) { myprint( "Info: turning on --expunge1 because --delete1 --noexpunge1 is very dangerous on the second run.\n" ) ; - $expunge1 = 1 ; + $sync->{ expunge1 } = 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" ) ; +if ( $sync->{ uidexpunge2 } and not Mail::IMAPClient->can( 'uidexpunge' ) ) { + myprint( "Failure: uidexpunge not supported (IMAPClient release < 3.17), use nothing or --expunge2 instead\n" ) ; exit_clean( $sync, $EX_SOFTWARE ) ; } -if ( ( $delete2 or $delete2duplicates ) and not defined $uidexpunge2 ) { +if ( ( $sync->{ delete2 } or $sync->{ delete2duplicates } ) and not defined $sync->{ uidexpunge2 } ) { if ( Mail::IMAPClient->can( 'uidexpunge' ) ) { myprint( "Info: will act as --uidexpunge2\n" ) ; - $uidexpunge2 = 1 ; - }elsif ( not defined $expunge2 ) { + $sync->{ uidexpunge2 } = 1 ; + }elsif ( not defined $sync->{ expunge2 } ) { myprint( "Info: will act as --expunge2 (no uidexpunge support)\n" ) ; - $expunge2 = 1 ; + $sync->{ expunge2 } = 1 ; } } -if ( $delete1 and $delete2 ) { +if ( $sync->{ delete1 } and $sync->{ delete2 } ) { myprint( "Warning: using --delete1 and --delete2 together is almost always a bad idea, exiting imapsync\n" ) ; exit_clean( $sync, $EX_USAGE ) ; } @@ -1310,11 +1415,11 @@ $authmech1 = uc $authmech1; $authmech2 = uc $authmech2; if (defined $proxyauth1 && !$authuser1) { - missing_option( 'With --proxyauth1, --authuser1' ) ; + missing_option( $sync, 'With --proxyauth1, --authuser1' ) ; } if (defined $proxyauth2 && !$authuser2) { - missing_option( 'With --proxyauth2, --authuser2' ) ; + missing_option( $sync, 'With --proxyauth2, --authuser2' ) ; } $authuser1 ||= $sync->{user1}; @@ -1333,7 +1438,7 @@ myprint( "Host2: imap connection 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 ; } +if ( $sync->{ justfolders } and not defined $foldersizes ) { $foldersizes = 0 ; } $foldersizes = ( defined $foldersizes ) ? $foldersizes : 1 ; $foldersizesatend = ( defined $foldersizesatend ) ? $foldersizesatend : $foldersizes ; @@ -1365,18 +1470,14 @@ get_password1( $sync ) ; get_password2( $sync ) ; - $sync->{dry_message} = q{} ; if( $sync->{dry} ) { $sync->{dry_message} = "\t(not really since --dry mode)" ; } - $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} ; } @@ -1390,17 +1491,17 @@ if ( @pipemess and $pipemesscheck ) { 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" ) ; + exit_clean( $sync, $EX_USAGE, "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" ) ; + 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" ) ; + exit_clean( $sync, $EX_USAGE, "Error: --maxlinelengthcmd command is bad, check it\n" ) ; } myprint( "Ok with --maxlinelengthcmd $maxlinelengthcmd\n" ) ; } @@ -1410,7 +1511,7 @@ if ( @regexmess ) { 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' ) ; + exit_clean( $sync, $EX_USAGE, 'Error: one of --regexmess option is bad, check it' ) ; } myprint( "Ok with each --regexmess\n" ) ; } @@ -1420,7 +1521,7 @@ if ( @skipmess ) { 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' ) ; + exit_clean( $sync, $EX_USAGE, 'Error: one of --skipmess option is bad, check it' ) ; } myprint( "Ok with each --skipmess\n" ) ; } @@ -1430,44 +1531,52 @@ if ( @regexflag ) { 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' ) ; + exit_clean( $sync, $EX_USAGE, 'Error: one of --regexflag option is bad, check it' ) ; } myprint( "Ok with each --regexflag\n" ) ; } -$sync->{imap1} = my $imap1 = login_imap( $sync->{host1}, $sync->{port1}, $sync->{user1}, $domain1, $sync->{password1}, +$sync->{imap1} = login_imap( $sync->{host1}, $sync->{port1}, $sync->{user1}, $domain1, $sync->{password1}, $debugimap1, $sync->{h1}->{timeout}, $fastio1, $sync->{ssl1}, $sync->{tls1}, $authmech1, $authuser1, $reconnectretry1, $proxyauth1, $uid1, $split1, 'Host1', $sync->{h1}, $sync ) ; -$sync->{imap2} = my $imap2 = login_imap( $sync->{host2}, $sync->{port2}, $sync->{user2}, $domain2, $sync->{password2}, +$sync->{imap2} = login_imap( $sync->{host2}, $sync->{port2}, $sync->{user2}, $domain2, $sync->{password2}, $debugimap2, $sync->{h2}->{timeout}, $fastio2, $sync->{ssl2}, $sync->{tls2}, $authmech2, $authuser2, $reconnectretry2, $proxyauth2, $uid2, $split2, 'Host2', $sync->{h2}, $sync ) ; -$debug and myprint( 'Host1 Buffer I/O: ', $imap1->Buffer(), "\n" ) ; -$debug and myprint( 'Host2 Buffer I/O: ', $imap2->Buffer(), "\n" ) ; +$sync->{ debug } and myprint( 'Host1 Buffer I/O: ', $sync->{imap1}->Buffer(), "\n" ) ; +$sync->{ debug } and myprint( 'Host2 Buffer I/O: ', $sync->{imap2}->Buffer(), "\n" ) ; -if ( ! $imap1->IsAuthenticated( ) ) { die_clean( 'Not authenticated on host1' ) ; } +if ( ! $sync->{imap1}->IsAuthenticated( ) ) { exit_clean( $sync, $EXIT_AUTHENTICATION_FAILURE, 'Not authenticated on host1' ) ; } myprint( "Host1: state Authenticated\n" ) ; -if ( ! $imap2->IsAuthenticated( ) ) { die_clean( 'Not authenticated on host2' ) ; } +if ( ! $sync->{imap2}->IsAuthenticated( ) ) { exit_clean( $sync, $EXIT_AUTHENTICATION_FAILURE, 'Not authenticated on host2' ) ; } myprint( "Host2: state Authenticated\n" ) ; -myprint( 'Host1 capability once authenticated: ', join(q{ }, @{ $imap1->capability() || [] }), "\n" ) ; -myprint( 'Host2 capability once authenticated: ', join(q{ }, @{ $imap2->capability() || [] }), "\n" ) ; +myprint( 'Host1 capability once authenticated: ', join(q{ }, @{ $sync->{imap1}->capability() || [] }), "\n" ) ; + +#myprint( Data::Dumper->Dump( [ $sync->{imap1} ] ) ) ; +#myprint( "imap4rev1: " . $sync->{imap1}->imap4rev1() . "\n" ) ; + +myprint( 'Host2 capability once authenticated: ', join(q{ }, @{ $sync->{imap2}->capability() || [] }), "\n" ) ; + # ID on by default since 1.832 $sync->{id} = defined $sync->{id} ? $sync->{id} : 1 ; imap_id_stuff( $sync ) ; -#quota( $imap1, 'h1', $sync ) ; # quota on host1 is useless and pollute host2 output. -quota( $imap2, 'h2', $sync ) ; +#quota( $sync, $sync->{imap1}, 'h1' ) ; # quota on host1 is useless and pollute host2 output. +quota( $sync, $sync->{imap2}, 'h2' ) ; -if ( $sync->{justlogin} ) { - $imap1->logout( ) ; - $imap2->logout( ) ; +maxsize_setting( $sync ) ; + +if ( $sync->{ justlogin } ) { + $sync->{imap1}->logout( ) ; + $sync->{imap2}->logout( ) ; + myprint( "Exiting because of --justlogin\n" ) ; exit_clean( $sync, $EX_OK ) ; } @@ -1490,14 +1599,14 @@ 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( ) ; +@h1_folders_all = sort $sync->{imap1}->folders( ) ; +@h2_folders_all = sort $sync->{imap2}->folders( ) ; myprint( 'Host1: found ', scalar @h1_folders_all , " folders.\n" ) ; myprint( 'Host2: found ', scalar @h2_folders_all , " folders.\n" ) ; -foreach my $f ( @h1_folders_all ) { - $h1_folders_all{ $f } = 1 +foreach my $f ( @h1_folders_all ) { + $h1_folders_all{ $f } = 1 } foreach my $f ( @h2_folders_all ) { $h2_folders_all{ $f } = 1 ; @@ -1508,30 +1617,39 @@ $sync->{h1_folders_all} = \%h1_folders_all ; $sync->{h2_folders_all} = \%h2_folders_all ; $sync->{h2_folders_all_UPPER} = \%h2_folders_all_UPPER ; +private_folders_separators_and_prefixes( ) ; + + # Make a hash of subscribed folders in both servers. -for ( $imap1->subscribed( ) ) { $h1_subscribed_folder{ $_ } = 1 } ; -for ( $imap2->subscribed( ) ) { $h2_subscribed_folder{ $_ } = 1 } ; +for ( $sync->{imap1}->subscribed( ) ) { $h1_subscribed_folder{ $_ } = 1 } ; +for ( $sync->{imap2}->subscribed( ) ) { $h2_subscribed_folder{ $_ } = 1 } ; -if ( defined $subfolder2 ) { - unshift @regextrans2, - q(s,^$sync->{h2_prefix}(.*),$sync->{h2_prefix}${subfolder2}${h2_sep}$1,), - q(s,^INBOX$,$sync->{h2_prefix}${subfolder2}${h2_sep}INBOX,) ; +if ( defined $sync->{ subfolder1 } ) { + subfolder1( $sync ) ; +} + + + +if ( defined $sync->{ subfolder2 } ) { + subfolder2( $sync ) ; } if ( $fixInboxINBOX and ( my $reg = fix_Inbox_INBOX_mapping( \%h1_folders_all, \%h2_folders_all ) ) ) { - push @regextrans2, $reg ; + push @{ $sync->{ regextrans2 } }, $reg ; } -if ( ( $sync->{folder} and scalar @{ $sync->{folder} } ) - or $subscribed - or scalar @folderrec ) { + +if ( ( $sync->{ folder } and scalar @{ $sync->{ folder } } ) + or $subscribed + or scalar @folderrec ) +{ # folders given by option --folder - if ( $sync->{folder} and scalar @{ $sync->{folder} } ) { - add_to_requested_folders( @{ $sync->{folder} } ); + if ( $sync->{ folder } and scalar @{ $sync->{ folder } } ) { + add_to_requested_folders( @{ $sync->{ folder } } ) ; } # option --subscribed @@ -1540,16 +1658,18 @@ if ( ( $sync->{folder} and scalar @{ $sync->{folder} } ) } # option --folderrec - if (scalar @folderrec) { - foreach my $folderrec (@folderrec) { - add_to_requested_folders($imap1->folders($folderrec)); + if ( scalar @folderrec ) { + foreach my $folderrec ( @folderrec ) { + add_to_requested_folders( $sync->{imap1}->folders( $folderrec ) ) ; } } -} else { +} +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); + add_to_requested_folders( @h1_folders_all ) ; } } @@ -1584,7 +1704,7 @@ if ( $sync->{ checkfoldersexist } ) { my @h1_folders_wanted_exist ; myprint( "Host1: Checking wanted folders exist. Use --nocheckfoldersexist to avoid this check (shared of public namespace targeted).\n" ) ; foreach my $folder ( @h1_folders_wanted ) { - ( $debug or $sync->{debugfolders} ) and myprint( "Checking $folder exists on host1\n" ) ; + ( $sync->{ 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 ; @@ -1602,16 +1722,16 @@ if ( $sync->{ checkselectable } ) { my @h1_folders_wanted_selectable ; myprint( "Host1: Checking 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" ) ; - # It does an imap command LIST "" $folder and then search for no \Noselect - if ( ! $imap1->selectable( $folder ) ) { + ( $sync->{ debug } or $sync->{debugfolders} ) and myprint( "Checking $folder is selectable on host1\n" ) ; + # It does an imap command LIST "" $folder and then search for no \Noselect + if ( ! $sync->{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->{ debug } or $sync->{debugfolders} ) and myprint( 'Host1: checking folders took ', timenext( ), " s\n" ) ; }else{ myprint( "Host1: Not checking that wanted folders are selectable. Remove --nocheckselectable to get this check.\n" ) ; } @@ -1619,20 +1739,9 @@ if ( $sync->{ checkselectable } ) { $sync->{h1_folders_wanted} = \@h1_folders_wanted ; -my( $h1_sep, $h2_sep ) ; -# what are the private folders separators for each server ? +# Old place of private_folders_separators_and_prefixes( ) call. +#private_folders_separators_and_prefixes( ) ; -( $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 ) ; - - -$sync->{ h1_prefix } = get_prefix( $imap1, $prefix1, '--prefix1', 'Host1', \@h1_folders_all ) ; -$sync->{ h2_prefix } = get_prefix( $imap2, $prefix2, '--prefix2', 'Host2', \@h2_folders_all ) ; - - -myprint( "Host1 separator and prefix: [$h1_sep][$sync->{ h1_prefix }]\n" ) ; -myprint( "Host2 separator and prefix: [$h2_sep][$sync->{ h2_prefix }]\n" ) ; # this hack is because LWP post does not pass well a hash in the $form parameter # but it does pass well an array @@ -1643,7 +1752,7 @@ automap( $sync ) ; foreach my $h1_fold ( @h1_folders_wanted ) { my $h2_fold ; - $h2_fold = imap2_folder_name( $h1_fold ) ; + $h2_fold = imap2_folder_name( $sync, $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 }++ ; @@ -1653,7 +1762,7 @@ foreach my $h1_fold ( @h1_folders_wanted ) { foreach my $h1_fold ( @h1_folders_all ) { my $h2_fold ; - $h2_fold = imap2_folder_name( $h1_fold ) ; + $h2_fold = imap2_folder_name( $sync, $h1_fold ) ; $h2_folders_from_1_all{ $h2_fold }++ ; } @@ -1666,32 +1775,32 @@ All foldernames are presented between brackets like [X] where X is the foldernam 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. +Y is the utf8 output just printed for convenience, to recognize it. END_LISTING myprint( - "Host1 folders list (first the raw imap format then the [X] = [Y]):\n", - $imap1->list( ), + "Host1: folders list (first the raw imap format then the [X] = [Y]):\n", + $sync->{imap1}->list( ), "\n", jux_utf8_list( @h1_folders_all ), "\n", - "Host2 folders list (first the raw imap format then the [X] = [Y]):\n", - $imap2->list( ), + "Host2: folders list (first the raw imap format then the [X] = [Y]):\n", + $sync->{imap2}->list( ), "\n", jux_utf8_list( @h2_folders_all ), - "\n", - q{} + "\n", + q{} ) ; if ( $subscribed ) { - myprint( + myprint( 'Host1 subscribed folders list: ', jux_utf8_list( sort keys %h1_subscribed_folder ), "\n", ) ; } - + my @h2_folders_not_in_1; @h2_folders_not_in_1 = list_folders_in_2_not_in_1( ) ; @@ -1731,15 +1840,20 @@ exit_clean( $sync, $EX_OK ) if ( $sync->{justautomap} ) ; debugsleep( $sync ) ; if ( $foldersizes ) { - foldersizes_on_h1h2( ) ; + foldersizes_on_h1h2( $sync ) ; } -exit_clean( $sync, $EX_OK ) if ( $justfoldersizes ) ; + +if ( $sync->{ justfoldersizes } ) +{ + myprint( "Exiting because of --justfoldersizes\n" ) ; + exit_clean( $sync, $EX_OK ) ; +} $sync->{stats} = 1 ; -if ( $sync->{'delete1emptyfolders'} ) { +if ( $sync->{ delete1emptyfolders } ) { delete1emptyfolders( $sync ) ; } @@ -1755,116 +1869,128 @@ $sync->{begin_transfer_time} = time ; my %uid_candidate_for_deletion ; my %uid_candidate_no_deletion ; -my %h2_folders_of_md5 = ( ) ; +$sync->{ h2_folders_of_md5 } = { } ; FOLDER: foreach my $h1_fold ( @h1_folders_wanted ) { - if ( ! reconnect_12_if_needed( $sync ) ) { last FOLDER ; } + if ( ! reconnect_12_if_needed( $sync ) ) { last FOLDER ; } - my $h2_fold = imap2_folder_name( $h1_fold ) ; + my $h2_fold = imap2_folder_name( $sync, $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 ) ) ; myprint( debugmemory( $sync, " at folder loop" ) ) ; - + # host1 can not be fetched read only, select is needed because of expunge. - select_folder( $imap1, $h1_fold, 'Host1' ) or next FOLDER ; + select_folder( $sync, $sync->{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" ) ; + my $h1_fold_nb_messages = count_from_select( $sync->{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" ) ; + myprint( "Host1: skipping empty host1 folder [$h1_fold]\n" ) ; next FOLDER ; } + # Code added from https://github.com/imapsync/imapsync/issues/95 + # Thanks jh1995 + if ( $skipemptyfolders ) { + my $h1_msgs_all_hash_ref_tmp = { } ; + my @h1_msgs_tmp = select_msgs( $sync->{imap1}, $h1_msgs_all_hash_ref_tmp, $search1, $h1_fold ) ; + my $h1_fold_nb_messages_tmp = scalar( @h1_msgs_tmp ) ; + if ( 0 == $h1_fold_nb_messages_tmp ) { + myprint( "Host1: skipping empty host1 folder [$h1_fold] (0 message found by SEARCH)\n" ) ; + next FOLDER ; + } + } + if ( ! exists $h2_folders_all{ $h2_fold } ) { - create_folder( $imap2, $h2_fold, $h1_fold ) or next FOLDER ; + create_folder( $sync, $sync->{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' ) ) + select_folder( $sync, $sync->{imap2}, $h2_fold, 'Host2' ) + or ( create_folder( $sync, $sync->{imap2}, $h2_fold, $h1_fold ) + and select_folder( $sync, $sync->{imap2}, $h2_fold, 'Host2' ) ) or next FOLDER ; - my @select_results = $imap2->Results( ) ; + my @select_results = $sync->{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" ) ; + myprint( "Host2: folder [$h2_fold] has $h2_fold_nb_messages messages in total (mentioned by SELECT)\n" ) ; my $permanentflags2 = permanentflags( @select_results ) ; - myprint( "Host2 folder [$h2_fold] permanentflags: $permanentflags2\n" ) ; + myprint( "Host2: folder [$h2_fold] permanentflags: $permanentflags2\n" ) ; - if ( $expunge1 ){ + if ( $sync->{ expunge1 } ) + { myprint( "Host1: Expunging $h1_fold $sync->{dry_message}\n" ) ; - if ( ! $sync->{dry} ) { $imap1->expunge( ) } ; + if ( ! $sync->{dry} ) { $sync->{imap1}->expunge( ) } ; } if ( ( ( $subscribe and exists $h1_subscribed_folder{ $h1_fold } ) or $subscribeall ) - and not exists $h2_subscribed_folder{ $h2_fold } ) { + and not exists $h2_subscribed_folder{ $h2_fold } ) + { myprint( "Host2: Subscribing to folder $h2_fold\n" ) ; - if ( ! $sync->{dry} ) { $imap2->subscribe( $h2_fold ) } ; + if ( ! $sync->{dry} ) { $sync->{imap2}->subscribe( $h2_fold ) } ; } - next FOLDER if ( $justfolders ) ; + next FOLDER if ( $sync->{ justfolders } ) ; - if ( ! reconnect_12_if_needed( $sync ) ) { last FOLDER ; } + if ( ! reconnect_12_if_needed( $sync ) ) { last FOLDER ; } my $h1_msgs_all_hash_ref = { } ; - my @h1_msgs = select_msgs( $imap1, $h1_msgs_all_hash_ref, $search1, $sync->{abletosearch1}, $h1_fold ); + my @h1_msgs = select_msgs( $sync->{imap1}, $h1_msgs_all_hash_ref, $search1, $sync->{abletosearch1}, $h1_fold ); - if ( ! reconnect_12_if_needed( $sync ) ) { last FOLDER ; } + if ( ! reconnect_12_if_needed( $sync ) ) { last FOLDER ; } 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" ) ; + myprint( "Host1: folder [$h1_fold] considering $h1_msgs_nb messages\n" ) ; + ( $sync->{ debug } or $debuglist ) and myprint( "Host1: folder [$h1_fold] considering $h1_msgs_nb messages, LIST gives: @h1_msgs\n" ) ; + $sync->{ 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, $sync->{abletosearch2}, $h2_fold ) ; + my @h2_msgs = select_msgs( $sync->{imap2}, $h2_msgs_all_hash_ref, $search2, $sync->{abletosearch2}, $h2_fold ) ; - if ( ! reconnect_12_if_needed( $sync ) ) { last FOLDER ; } + if ( ! reconnect_12_if_needed( $sync ) ) { last FOLDER ; } 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" ) ; + myprint( "Host2: folder [$h2_fold] considering $h2_msgs_nb messages\n" ) ; + ( $sync->{ debug } or $debuglist ) and myprint( "Host2: folder [$h2_fold] considering $h2_msgs_nb messages, LIST gives: @h2_msgs\n" ) ; + $sync->{ debug } and myprint( "Host2: selecting messages of folder [$h2_fold] took ", timenext(), " s\n" ) ; my $cache_base = "$sync->{ tmpdir }/imapsync_cache/" ; my $cache_dir = cache_folder( $cache_base, "$sync->{host1}/$sync->{user1}/$sync->{host2}/$sync->{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{} ; + my $h1_uidvalidity = $sync->{imap1}->uidvalidity( ) || q{} ; + my $h2_uidvalidity = $sync->{imap2}->uidvalidity( ) || q{} ; - if ( ! reconnect_12_if_needed( $sync ) ) { last FOLDER ; } + if ( ! reconnect_12_if_needed( $sync ) ) { last FOLDER ; } if ( $usecache ) { - myprint( "cache directory: $cache_dir\n" ) ; + myprint( "Local cache directory: $cache_dir ( " . length( $cache_dir ) . " characters long )\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( '[', + $sync->{ debug } and myprint( '[', map ( { "$_->$cache_1_2_ref->{$_} " } keys %{ $cache_1_2_ref } ), " ]\n" ) ; } - my %h1_hash = () ; - my %h2_hash = () ; + my %h1_hash = ( ) ; + my %h2_hash = ( ) ; my ( %h1_msgs, %h2_msgs ) ; - @h1_msgs{ @h1_msgs } = (); - @h2_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 } ; @@ -1892,112 +2018,128 @@ FOLDER: foreach my $h1_fold ( @h1_folders_wanted ) { #myprint( "delete2: @h2_msgs_delete2_not_in_cache\n" ) ; } - $debug and myprint( "Host1 parsing headers of folder [$h1_fold]\n" ) ; + $sync->{ 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_heads_ref = $sync->{imap1}->parse_headers([@h1_msgs_not_in_cache], @useheader) if (@h1_msgs_not_in_cache); + $sync->{ 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 ( $sync->{abletosearch1} ) { - $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 ) + $sync->{ debug } and myprint( "Host1: getting flags idate and sizes of folder [$h1_fold]\n" ) ; + + my @h1_common_fetch_param = ( 'FLAGS', 'INTERNALDATE', 'RFC822.SIZE' ) ; + if ( $sync->{ synclabels } or $sync->{ resynclabels } ) { push @h1_common_fetch_param, 'X-GM-LABELS' ; } + + if ( $sync->{abletosearch1} ) + { + $h1_fir_ref = $sync->{imap1}->fetch_hash( \@h1_msgs, @h1_common_fetch_param, $h1_fir_ref ) if ( @h1_msgs ) ; } - $debug and myprint( "Host1 getting flags idate and sizes of folder [$h1_fold] took ", timenext(), " s\n" ) ; - if ( ! $h1_fir_ref ) { - my $error = join( q{}, "Host1 folder $h1_fold: Could not fetch_hash ", - scalar @h1_msgs, ' msgs: ', $imap1->LastError || q{}, "\n" ) ; + else + { + my $uidnext = $sync->{imap1}->uidnext( $h1_fold ) || $uidnext_default ; + my $fetch_hash_uids = $fetch_hash_set || "1:$uidnext" ; + $h1_fir_ref = $sync->{imap1}->fetch_hash( $fetch_hash_uids, @h1_common_fetch_param, $h1_fir_ref ) + if ( @h1_msgs ) ; + } + + $sync->{ debug } and myprint( "Host1: getting flags idate and sizes of folder [$h1_fold] took ", timenext(), " s\n" ) ; + if ( ! $h1_fir_ref ) + { + my $error = join( q{}, "Host1: folder $h1_fold : Could not fetch_hash ", + scalar @h1_msgs, ' msgs: ', $sync->{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( $sync, $imap1, $m, $h1_heads_ref, $h1_fir_ref, 'Host1', \%h1_hash ) ; - if ( ! defined $rc ) { + foreach my $m ( @h1_msgs_not_in_cache ) + { + my $rc = parse_header_msg( $sync, $sync->{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) { + myprint( "Host1: $h1_fold/$m size $h1_size ignored (no wanted headers so we ignore this message. To solve this: use --addheader)\n" ) ; + $sync->{ total_bytes_skipped } += $h1_size ; + $sync->{ nb_msg_skipped } += 1 ; + $sync->{ h1_nb_msg_noheader } +=1 ; + $sync->{ 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; + $sync->{ nb_msg_skipped } += 1; $h1_nb_msg_duplicate += 1; - $h1_nb_msg_processed +=1 ; + $sync->{ 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" ) ; - # Getting headers and metada can be so long that host2 might be disconnected here + myprint( "Host1: folder [$h1_fold] selected $h1_msgs_nb messages, duplicates $h1_msgs_duplicate_nb\n" ) ; + + $sync->{ debug } and myprint( 'Host1: whole time parsing headers took ', timenext(), " s\n" ) ; + # Getting headers and metada can be so long that host2 might be disconnected here if ( ! reconnect_12_if_needed( $sync ) ) { last FOLDER ; } - $debug and myprint( "Host2 parsing headers of folder [$h2_fold]\n" ) ; + $sync->{ 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" ) ; + $h2_heads_ref = $sync->{imap2}->parse_headers([@h2_msgs_not_in_cache], @useheader) if (@h2_msgs_not_in_cache); + $sync->{ 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" ) ; + $sync->{ 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 + my @h2_common_fetch_param = ( 'FLAGS', 'INTERNALDATE', 'RFC822.SIZE' ) ; + if ( $sync->{ synclabels } or $sync->{ resynclabels } ) { push @h2_common_fetch_param, 'X-GM-LABELS' ; } + if ( $sync->{abletosearch2} and scalar( @h2_msgs ) ) { - $h2_fir_ref = $imap2->fetch_hash( \@h2_msgs, 'FLAGS', 'INTERNALDATE', 'RFC822.SIZE', $h2_fir_ref) ; + $h2_fir_ref = $sync->{imap2}->fetch_hash( \@h2_msgs, @h2_common_fetch_param, $h2_fir_ref) ; }else{ - my $uidnext = $imap2->uidnext( $h2_fold ) || $uidnext_default ; + my $uidnext = $sync->{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 ) + $h2_fir_ref = $sync->{imap2}->fetch_hash( $fetch_hash_uids, @h2_common_fetch_param, $h2_fir_ref ) if ( @h2_msgs ) ; } - $debug and myprint( "Host2 getting flags idate and sizes of folder [$h2_fold] took ", timenext(), " s\n" ) ; + $sync->{ 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( $sync, $imap2, $m, $h2_heads_ref, $h2_fir_ref, 'Host2', \%h2_hash ) ; + my $rc = parse_header_msg( $sync, $sync->{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" ) ; + 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 } ++ ; + $sync->{ h2_folders_of_md5 }->{ $md5 }->{ $h2_fold } ++ ; + } + # %h1_folders_of_md5 + foreach my $md5 ( keys %h1_hash ) { + $sync->{ h1_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" ) ; + myprint( "Host2: folder [$h2_fold] selected $h2_msgs_nb messages, duplicates $h2_msgs_duplicate_nb\n" ) ; - $debug and myprint( "++++ Verifying [$h1_fold] -> [$h2_fold]\n" ) ; + $sync->{ debug } and myprint( 'Host2 whole time parsing headers took ', timenext( ), " s\n" ) ; + + $sync->{ debug } and myprint( "++++ Verifying [$h1_fold] -> [$h2_fold]\n" ) ; # messages in host1 that are not in host2 my @h1_hash_keys_sorted_by_uid @@ -2008,30 +2150,31 @@ FOLDER: foreach my $h1_fold ( @h1_folders_wanted ) { my @h2_hash_keys_sorted_by_uid = sort {$h2_hash{$a}{'m'} <=> $h2_hash{$b}{'m'}} keys %h2_hash; + # Deletions on account2. - if( $delete2duplicates and not exists $h2_folders_from_1_several{ $h2_fold } ) { + if( $sync->{ 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 $sync->{dry_message}\n" ) ; - push @h2_expunge, $h2_msg if $uidexpunge2 ; + myprint( "Host2: msg $h2_fold/$h2_msg marked \\Deleted [duplicate] on host2 $sync->{dry_message}\n" ) ; + push @h2_expunge, $h2_msg if $sync->{ uidexpunge2 } ; if ( ! $sync->{dry} ) { - $imap2->delete_message( $h2_msg ) ; + $sync->{imap2}->delete_message( $h2_msg ) ; $h2_nb_msg_deleted += 1 ; } } my $cnt = scalar @h2_expunge ; - if( @h2_expunge and not $expunge2 ) { + if( @h2_expunge and not $sync->{ expunge2 } ) { myprint( "Host2: UidExpunging $cnt message(s) in folder $h2_fold $sync->{dry_message}\n" ) ; - $imap2->uidexpunge( \@h2_expunge ) if ! $sync->{dry} ; + $sync->{imap2}->uidexpunge( \@h2_expunge ) if ! $sync->{dry} ; } - if ( $expunge2 ){ + if ( $sync->{ expunge2 } ){ myprint( "Host2: Expunging folder $h2_fold $sync->{dry_message}\n" ) ; - $imap2->expunge( ) if ! $sync->{dry} ; + $sync->{imap2}->expunge( ) if ! $sync->{dry} ; } } - if( $delete2 and not exists $h2_folders_from_1_several{ $h2_fold } ) { + if( $sync->{ 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) { @@ -2042,35 +2185,35 @@ FOLDER: foreach my $h1_fold ( @h1_folders_wanted ) { my $isdel = $h2_flags =~ /\B\\Deleted\b/x ? 1 : 0; myprint( "Host2: msg $h2_fold/$h2_msg marked \\Deleted on host2 [$m_id] $sync->{dry_message}\n" ) if ! $isdel; - push @h2_expunge, $h2_msg if $uidexpunge2; + push @h2_expunge, $h2_msg if $sync->{ uidexpunge2 }; if ( ! ( $sync->{dry} or $isdel ) ) { - $imap2->delete_message($h2_msg); + $sync->{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 $sync->{dry_message}\n" ) ; - push @h2_expunge, $h2_msg if $uidexpunge2; + push @h2_expunge, $h2_msg if $sync->{ uidexpunge2 }; if ( ! $sync->{dry} ) { - $imap2->delete_message($h2_msg); + $sync->{imap2}->delete_message($h2_msg); $h2_nb_msg_deleted += 1; } } my $cnt = scalar @h2_expunge ; - if( @h2_expunge and not $expunge2 ) { + if( @h2_expunge and not $sync->{ expunge2 } ) { myprint( "Host2: UidExpunging $cnt message(s) in folder $h2_fold $sync->{dry_message}\n" ) ; - $imap2->uidexpunge( \@h2_expunge ) if ! $sync->{dry} ; + $sync->{imap2}->uidexpunge( \@h2_expunge ) if ! $sync->{dry} ; } - if ( $expunge2 ) { + if ( $sync->{ expunge2 } ) { myprint( "Host2: Expunging folder $h2_fold $sync->{dry_message}\n" ) ; - $imap2->expunge( ) if ! $sync->{dry} ; + $sync->{imap2}->expunge( ) if ! $sync->{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" ) ; + if( $sync->{ 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' } ; @@ -2078,11 +2221,11 @@ FOLDER: foreach my $h1_fold ( @h1_folders_wanted ) { my $h2_flags = $h2_hash{ $m_id }{ 'F' } || q{} ; my $isdel = $h2_flags =~ /\B\\Deleted\b/x ? 1 : 0 ; if ( ! $isdel ) { - $debug and myprint( "Host2: msg $h2_fold/$h2_msg candidate for deletion [$m_id]\n" ) ; + $sync->{ 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" ) ; + $sync->{ debug } and myprint( "Host2: msg $h2_fold/$h2_msg will cancel deletion [$m_id]\n" ) ; $uid_candidate_no_deletion{ $h2_fold }{ $h2_msg }++ ; } } @@ -2101,14 +2244,14 @@ FOLDER: foreach my $h1_fold ( @h1_folders_wanted ) { # 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" ) ; + $sync->{ 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" ) ; + $sync->{ debug } and myprint( "Host2: msg $h2_fold/$h2_msg canceled deletion\n" ) ; }else{ myprint( "Host2: msg $h2_fold/$h2_msg marked \\Deleted $sync->{dry_message}\n" ) ; - push @h2_expunge, $h2_msg if $uidexpunge2 ; + push @h2_expunge, $h2_msg if $sync->{ uidexpunge2 } ; if ( ! $sync->{dry} ) { - $imap2->delete_message( $h2_msg ) ; + $sync->{imap2}->delete_message( $h2_msg ) ; $h2_nb_msg_deleted += 1 ; } } @@ -2116,133 +2259,177 @@ FOLDER: foreach my $h1_fold ( @h1_folders_wanted ) { } my $cnt = scalar @h2_expunge ; - if( @h2_expunge and not $expunge2 ) { + if( @h2_expunge and not $sync->{ expunge2 } ) { myprint( "Host2: UidExpunging $cnt message(s) in folder $h2_fold $sync->{dry_message}\n" ) ; - $imap2->uidexpunge( \@h2_expunge ) if ! $sync->{dry} ; + $sync->{imap2}->uidexpunge( \@h2_expunge ) if ! $sync->{dry} ; } - if ( $expunge2 ) { + if ( $sync->{ expunge2 } ) { myprint( "Host2: Expunging host2 folder $h2_fold $sync->{dry_message}\n" ) ; - $imap2->expunge( ) if ! $sync->{dry} ; + $sync->{imap2}->expunge( ) if ! $sync->{dry} ; } $h2_folders_from_1_several{ $h2_fold }-- ; } - my $h2_uidnext = $imap2->uidnext( $h2_fold ) ; - $debug and myprint( "Host2 uidnext: $h2_uidnext\n" ) ; + my $h2_uidnext = $sync->{imap2}->uidnext( $h2_fold ) ; + $sync->{ debug } and myprint( "Host2: uidnext is $h2_uidnext\n" ) ; $h2_uidguess = $h2_uidnext ; # Getting host2 headers, metada and delete2 stuff can be so long that host1 might be disconnected here if ( ! reconnect_12_if_needed( $sync ) ) { last FOLDER ; } + my @h1_msgs_to_delete ; MESS: foreach my $m_id (@h1_hash_keys_sorted_by_uid) { if ( ! reconnect_12_if_needed( $sync ) ) { last FOLDER ; } - #myprint( "h1_nb_msg_processed: $h1_nb_msg_processed\n" ) ; + #myprint( "h1_nb_msg_processed: $sync->{ 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'}; + #my $labels = labels( $sync->{imap1}, $h1_msg ) ; + #print "LABELS: $labels\n" ; + if ( ( not exists $h2_hash{ $m_id } ) - and ( not ( exists $h2_folders_of_md5{ $m_id } ) - or not $skipcrossduplicates ) ) { + and ( not ( exists $sync->{ 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 ) { + if ( $h2_msg and $sync->{ delete1 } and not $sync->{ expungeaftereach } ) { + # not expunged + push @h1_msgs_to_delete, $h1_msg ; + } + + # A bug here with imapsync 1.920, fixed in 1.921 + # Added $h2_msg in the condition. Errors of APPEND were not counted as missing messages on host2! + if ( $h2_msg and not $sync->{ dry } ) + { + $sync->{ h2_folders_of_md5 }->{ $m_id }->{ $h2_fold } ++ ; + } + + # + if( $sync->{ 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 ( total_bytes_max_reached( $sync ) ) { + # a bug when using --delete1 --noexpungeaftereach + # same thing below on all total_bytes_max_reached! + last FOLDER ; + } next MESS; } - else{ + else + { # already on host2 - if ( exists $h2_hash{ $m_id } ) { + 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 ) { + $sync->{ debug } and myprint( "Host1: found that 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 ; + elsif( exists $sync->{ h2_folders_of_md5 }->{ $m_id } ) + { + my @folders_dup = keys %{ $sync->{ h2_folders_of_md5 }->{ $m_id } } ; + ( $sync->{ debug } or $debugcrossduplicates ) and myprint( "Host1: found that msg $h1_fold/$h1_msg is also in Host2 folders @folders_dup\n" ) ; + $sync->{ h2_nb_msg_crossdup } +=1 ; + } + $sync->{ total_bytes_skipped } += $h1_size ; + $sync->{ nb_msg_skipped } += 1 ; + $sync->{ 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'}; if ( $sync->{resyncflags} ) { - sync_flags_fir( $h1_fold, $h1_msg, $h2_fold, $h2_msg, $permanentflags2, $h1_fir_ref, $h2_fir_ref ) ; + sync_flags_fir( $sync, $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" ) ; + $sync->{ debug } and myprint( + "Host1: size msg $h1_fold/$h1_msg = $h1_size <> $h2_size = Host2 $h2_fold/$h2_msg\n" ) ; + + if ( $sync->{ resynclabels } ) + { + resynclabels( $sync, $h1_msg, $h2_msg, $h1_fir_ref, $h2_fir_ref, $h1_fold ) + } } if ( ! reconnect_12_if_needed( $sync ) ) { last FOLDER ; } - - if ( $delete1 ) { - delete_message_on_host1( $h1_msg, $h1_fold ) ; + if ( $sync->{ delete1 } ) { + push @h1_msgs_to_delete, $h1_msg ; } } # END MESS: loop - if ( ! reconnect_12_if_needed( $sync ) ) { last FOLDER ; } - 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" ) ; - if ( $sync->{resyncflags} ) { - 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 ; - if ( ! reconnect_12_if_needed( $sync ) ) { last FOLDER ; } + delete_message_on_host1( $sync, $h1_fold, $sync->{ expunge1 }, @h1_msgs_to_delete, @h1_msgs_in_cache ) ; + + if ( ! reconnect_12_if_needed( $sync ) ) { last FOLDER ; } + + # MESS_IN_CACHE: + if ( ! $sync->{ delete1 } ) + { + 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" ) ; + if ( $sync->{resyncflags} ) + { + sync_flags_fir( $sync, $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 ; + $sync->{ total_bytes_skipped } += $h1_size; + $sync->{ nb_msg_skipped } += 1; + $sync->{ h1_nb_msg_processed } +=1 ; + } } + if ( ! reconnect_12_if_needed( $sync ) ) { last FOLDER ; } + + @h1_msgs_to_delete = ( ) ; #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" ) ; + # MESS_BY_UID: + foreach my $h1_msg ( sort { $a <=> $b } keys %h1_msgs_copy_by_uid ) + { + $sync->{ debug } and myprint( "Copy by uid $h1_fold/$h1_msg\n" ) ; if ( ! reconnect_12_if_needed( $sync ) ) { last FOLDER ; } 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 ) { + if( $sync->{ 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( ) ; + last FOLDER if total_bytes_max_reached( $sync ) ; } - if ( $expunge1 ){ + if ( $sync->{ expunge1 } ){ myprint( "Host1: Expunging folder $h1_fold $sync->{dry_message}\n" ) ; - if ( ! $sync->{dry} ) { $imap1->expunge( ) } ; + if ( ! $sync->{dry} ) { $sync->{imap1}->expunge( ) } ; } - if ( $expunge2 ){ + if ( $sync->{ expunge2 } ){ myprint( "Host2: Expunging folder $h2_fold $sync->{dry_message}\n" ) ; - if ( ! $sync->{dry} ) { $imap2->expunge( ) } ; + if ( ! $sync->{dry} ) { $sync->{imap2}->expunge( ) } ; } - $debug and myprint( 'Time: ', timenext( ), " s\n" ) ; + $sync->{ debug } and myprint( 'Time: ', timenext( ), " s\n" ) ; } myprint( "++++ End looping on each folder\n" ) ; -if ( $delete1 and $sync->{'delete1emptyfolders'} ) { +if ( $sync->{ delete1 } and $sync->{ delete1emptyfolders } ) { delete1emptyfolders( $sync ) ; } -( $debug or $sync->{debugfolders} ) and myprint( 'Time: ', timenext( ), " s\n" ) ; +( $sync->{ debug } or $sync->{debugfolders} ) and myprint( 'Time: ', timenext( ), " s\n" ) ; if ( $foldersizesatend ) { @@ -2252,11 +2439,11 @@ Folders sizes after the synchronization. You can remove this foldersizes listing by using "--nofoldersizesatend" END_SIZE - foldersizesatend( ) ; + foldersizesatend( $sync ) ; } -if ( ! lost_connection( $imap1, "for host1 [$sync->{host1}]" ) ) { $imap1->logout( ) ; } -if ( ! lost_connection( $imap2, "for host2 [$sync->{host2}]" ) ) { $imap2->logout( ) ; } +if ( ! lost_connection( $sync, $sync->{imap1}, "for host1 [$sync->{host1}]" ) ) { $sync->{imap1}->logout( ) ; } +if ( ! lost_connection( $sync, $sync->{imap2}, "for host2 [$sync->{host2}]" ) ) { $sync->{imap2}->logout( ) ; } stats( $sync ) ; myprint( errorsdump( $sync->{nb_errors}, errors_log( $sync ) ) ) if ( $sync->{errorsdump} ) ; @@ -2270,23 +2457,27 @@ exit_clean( $sync, $EX_OK ) ; # subroutines -sub myprint { +sub myprint +{ #print @ARG ; print { $sync->{ tee } || \*STDOUT } @ARG ; return ; } -sub myprintf { +sub myprintf +{ printf { $sync->{ tee } || \*STDOUT } @ARG ; return ; } -sub mysprintf { +sub mysprintf +{ my( $format, @list ) = @ARG ; return sprintf $format, @list ; } -sub output_start { +sub output_start +{ my $mysync = shift @ARG ; if ( not $mysync ) { return ; } @@ -2297,7 +2488,8 @@ sub output_start { } -sub tests_output_start { +sub tests_output_start +{ note( 'Entering tests_output_start()' ) ; my $mysync = { } ; @@ -2313,7 +2505,8 @@ sub tests_output_start { return ; } -sub tests_output { +sub tests_output +{ note( 'Entering tests_output()' ) ; my $mysync = { } ; @@ -2329,7 +2522,8 @@ sub tests_output { return ; } -sub output { +sub output +{ my $mysync = shift @ARG ; if ( not $mysync ) { return ; } @@ -2341,7 +2535,8 @@ sub output { -sub tests_output_reset_with { +sub tests_output_reset_with +{ note( 'Entering tests_output_reset_with()' ) ; my $mysync = { } ; @@ -2356,7 +2551,8 @@ sub tests_output_reset_with { return ; } -sub output_reset_with { +sub output_reset_with +{ my $mysync = shift @ARG ; if ( not $mysync ) { return ; } @@ -2368,13 +2564,14 @@ sub output_reset_with { -sub abort { +sub abort +{ my $mysync = shift @ARG ; - if ( ! -r $sync->{pidfile} ) { - myprint( "Can not read pidfile $sync->{pidfile}. Exiting.\n" ) ; + if ( ! -r $mysync->{pidfile} ) { + myprint( "Can not read pidfile $mysync->{pidfile}. Exiting.\n" ) ; exit $EX_OK ; } - my $pidtokill = firstline( $sync->{pidfile} ) ; + my $pidtokill = firstline( $mysync->{pidfile} ) ; if ( ! $pidtokill ) { myprint( "No process to abort. Exiting.\n" ) ; exit $EX_OK ; @@ -2401,14 +2598,19 @@ sub abort { if ( kill 'ZERO', $pidtokill ) { myprint( "Process PID $pidtokill still there. Can not do much. Exiting.\n" ) ; exit $EX_OK ; + }else{ + myprint( "Process PID $pidtokill ended. Exiting.\n" ) ; + exit $EX_OK ; } + # well abort job done anyway - return ; + exit $EX_OK ; } -sub docker_context { +sub docker_context +{ my $mysync = shift ; -e '/.dockerenv' || return ; myprint( "Docker context detected with /.dockerenv\n" ) ; @@ -2419,11 +2621,12 @@ sub docker_context { # In case myprint( "Changing current directory to /var/tmp/\n" ) ; chdir '/var/tmp/' ; - + return ; } -sub cgibegin { +sub cgibegin +{ my $mysync = shift ; if ( ! under_cgi_context( $mysync ) ) { return ; } require CGI ; @@ -2434,8 +2637,10 @@ sub cgibegin { return ; } -sub tests_under_cgi_context { +sub tests_under_cgi_context +{ note( 'Entering tests_under_cgi_context()' ) ; + # $ENV{SERVER_SOFTWARE} = 'under imapsync' ; do { # Not in cgi context @@ -2462,7 +2667,8 @@ sub tests_under_cgi_context { } -sub under_cgi_context { +sub under_cgi_context +{ my $mysync = shift ; # Under cgi context if ( $ENV{SERVER_SOFTWARE} ) { @@ -2472,7 +2678,8 @@ sub under_cgi_context { return ; } -sub cgibuildheader { +sub cgibuildheader +{ my $mysync = shift ; if ( ! under_cgi_context( $mysync ) ) { return ; } @@ -2509,19 +2716,24 @@ sub cgibuildheader { return ; } -sub cgiload { - my $mysync = shift ; - if ( ! under_cgi_context( $mysync ) ) { return ; } - if ( $mysync->{ abort } ) { return ; } # keep going to abort - if ( $mysync->{ loaddelay } ) { - myprint( "Server is on heavy load. Be back in $mysync->{ loaddelay } min. Load is $mysync->{ loadavg }\n") ; - exit_clean( $mysync, $EX_UNAVAILABLE ) ; - } - return ; +sub cgiload +{ + # Exit on heavy load in CGI context + my $mysync = shift ; + if ( ! under_cgi_context( $mysync ) ) { return ; } + if ( $mysync->{ abort } ) { return ; } # keep going to abort since some ressources will be free soon + if ( $mysync->{ loaddelay } ) + { + myprint( "Server is on heavy load. Be back in $mysync->{ loaddelay } min. Load is $mysync->{ loadavg }\n") ; + exit_clean( $mysync, $EX_UNAVAILABLE ) ; + } + return ; } -sub tests_set_umask { +sub tests_set_umask +{ note( 'Entering tests_set_umask()' ) ; + my $save_umask = umask ; my $mysync = {} ; @@ -2536,7 +2748,8 @@ sub tests_set_umask { return ; } -sub set_umask { +sub set_umask +{ my $mysync = shift ; my $previous_umask = umask_str( ) ; my $new_umask = umask_str( $UMASK_PARANO ) ; @@ -2547,8 +2760,10 @@ sub set_umask { return ; } -sub tests_umask_str { +sub tests_umask_str +{ note( 'Entering tests_umask_str()' ) ; + my $save_umask = umask ; is( umask_str( ), umask_str( ), 'umask_str: no parameters => idopotent' ) ; @@ -2580,7 +2795,8 @@ sub tests_umask_str { return ; } -sub umask_str { +sub umask_str +{ my $value = shift ; if ( defined $value ) { @@ -2591,8 +2807,10 @@ sub umask_str { return( sprintf( '%#04o', $current ) ) ; } -sub tests_umask { +sub tests_umask +{ note( 'Entering tests_umask()' ) ; + my $save_umask ; is( umask, umask, 'umask: umask is umask' ) ; is( $save_umask = umask, umask, "umask: umask is umask again + save it: $save_umask" ) ; @@ -2613,7 +2831,8 @@ sub tests_umask { return ; } -sub cgisetcontext { +sub cgisetcontext +{ my $mysync = shift ; if ( ! under_cgi_context( $mysync ) ) { return ; } @@ -2621,7 +2840,7 @@ sub cgisetcontext { set_umask( $mysync ) ; # Remove all content in unsafe evaled options - @regextrans2 = ( ) ; + @{ $mysync->{ regextrans2 } } = ( ) ; @regexflag = ( ) ; @regexmess = ( ) ; @skipmess = ( ) ; @@ -2651,15 +2870,26 @@ sub cgisetcontext { chdir $cgidir or die "Can not cd to $cgidir: $OS_ERROR\n" ; $mysync->{ tmpdir } = $cgidir ; cgioutputenvcontext( $mysync ) ; - $debug and output( $mysync, 'Current directory is ' . getcwd( ) . "\n" ) ; - $debug and output( $mysync, 'Real user id is ' . getpwuid_any_os( $REAL_USER_ID ) . " (uid $REAL_USER_ID)\n" ) ; - $debug and output( $mysync, 'Effective user id is ' . getpwuid_any_os( $EFFECTIVE_USER_ID ). " (euid $EFFECTIVE_USER_ID)\n" ) ; - # @{ $mysync->{ sigexit } } = ( 'QUIT' ) ; - # output( $mysync, "Setting the QUIT signal to exit properly\n" ) ; + $mysync->{ debug } and output( $mysync, 'Current directory is ' . getcwd( ) . "\n" ) ; + $mysync->{ debug } and output( $mysync, 'Real user id is ' . getpwuid_any_os( $REAL_USER_ID ) . " (uid $REAL_USER_ID)\n" ) ; + $mysync->{ debug } and output( $mysync, 'Effective user id is ' . getpwuid_any_os( $EFFECTIVE_USER_ID ). " (euid $EFFECTIVE_USER_ID)\n" ) ; + + $skipemptyfolders = defined $skipemptyfolders ? $skipemptyfolders : 1 ; + + # Out of memory with messages over 1 GB ? + $mysync->{ maxsize } = defined $mysync->{ maxsize } ? $mysync->{ maxsize } : 1_000_000_000 ; + + # tail -f behaviour on by default + $mysync->{ tail } = defined $mysync->{ tail } ? $mysync->{ tail } : 1 ; + + # not sure it's for good + @useheader = qw( Message-Id ) ; + return ; } -sub cgioutputenvcontext { +sub cgioutputenvcontext +{ my $mysync = shift @ARG ; for my $envvar ( qw( REMOTE_ADDR REMOTE_HOST HTTP_REFERER HTTP_USER_AGENT SERVER_SOFTWARE SERVER_PORT HTTP_COOKIE ) ) { @@ -2675,7 +2905,8 @@ sub cgioutputenvcontext { -sub debugsleep { +sub debugsleep +{ my $mysync = shift @ARG ; if ( defined $mysync->{debugsleep} ) { myprint( "Info: sleeping $mysync->{debugsleep}s\n" ) ; @@ -2684,48 +2915,426 @@ sub debugsleep { return ; } -sub foldersizes_on_h1h2 { +sub foldersizes_on_h1h2 +{ + my $mysync = shift ; + 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. +You can remove foldersizes listings by using "--nofoldersizes" and "--nofoldersizesatend" +but then you will also lose the ETA (Estimation Time of Arrival) given after each message copy. END_SIZE - ( $h1_nb_msg_start, $h1_bytes_start ) = foldersizes( 'Host1', $imap1, $search1, $sync->{abletosearch1}, @h1_folders_wanted ) ; - ( $h2_nb_msg_start, $h2_bytes_start ) = foldersizes( 'Host2', $imap2, $search2, $sync->{abletosearch2}, @h2_folders_from_1_wanted ) ; + ( $h1_nb_msg_start, $h1_bytes_start ) = foldersizes( 'Host1', $mysync->{imap1}, $search1, $mysync->{abletosearch1}, @h1_folders_wanted ) ; + ( $h2_nb_msg_start, $h2_bytes_start ) = foldersizes( 'Host2', $mysync->{imap2}, $search2, $mysync->{abletosearch2}, @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 ) ; + errors_incr( $mysync, $error ) ; $foldersizes = 0 ; $foldersizesatend = 0 ; return ; } - my $h2_bytes_limit = $sync->{h2}->{quota_limit_bytes} || 0 ; + my $h2_bytes_limit = $mysync->{h2}->{quota_limit_bytes} || 0 ; if ( $h2_bytes_limit and ( $h2_bytes_limit < $h1_bytes_start ) ) { my $quota_percent = mysprintf( '%.0f', $NUMBER_100 * $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 ) ; + errors_incr( $mysync, $error ) ; } return ; } -sub total_bytes_max_reached { +sub total_bytes_max_reached +{ + my $mysync = shift ; - return( 0 ) if not $exitwhenover ; - if ( $sync->{total_bytes_transferred} >= $exitwhenover ) { - myprint( "Maximum bytes transferred reached, $sync->{total_bytes_transferred} >= $exitwhenover, ending sync\n" ) ; + if ( ! $mysync->{ exitwhenover } ) { + return( 0 ) ; + } + if ( $mysync->{ total_bytes_transferred } >= $mysync->{ exitwhenover } ) { + myprint( "Maximum bytes transferred reached, $mysync->{total_bytes_transferred} >= $mysync->{ exitwhenover }, ending sync\n" ) ; return( 1 ) ; } + return ; +} + +sub tests_mock_capability +{ + note( 'Entering tests_mock_capability()' ) ; + + my $myimap ; + ok( $myimap = mock_capability( ), + 'mock_capability: (1) no args => a Test::MockObject' + ) ; + ok( $myimap->isa( 'Test::MockObject' ), + 'mock_capability: (2) no args => a Test::MockObject' + ) ; + + is( undef, $myimap->capability( ), + 'mock_capability: (3) no args => capability undef' + ) ; + + ok( mock_capability( $myimap ), + 'mock_capability: (1) one arg => MockObject' + ) ; + + is( undef, $myimap->capability( ), + 'mock_capability: (2) one arg OO style => capability undef' + ) ; + + ok( mock_capability( $myimap, $NUMBER_123456 ), + 'mock_capability: (1) two args 123456 => capability 123456' + ) ; + + is( $NUMBER_123456, $myimap->capability( ), + 'mock_capability: (2) two args 123456 => capability 123456' + ) ; + + ok( mock_capability( $myimap, 'ABCD' ), + 'mock_capability: (1) two args ABCD => capability ABCD' + ) ; + is( 'ABCD', $myimap->capability( ), + 'mock_capability: (2) two args ABCD => capability ABCD' + ) ; + + ok( mock_capability( $myimap, [ 'ABCD' ] ), + 'mock_capability: (1) two args [ ABCD ] => capability [ ABCD ]' + ) ; + is_deeply( [ 'ABCD' ], $myimap->capability( ), + 'mock_capability: (2) two args [ ABCD ] => capability [ ABCD ]' + ) ; + + ok( mock_capability( $myimap, [ 'ABC', 'DEF' ] ), + 'mock_capability: (1) two args [ ABC, DEF ] => capability [ ABC, DEF ]' + ) ; + is_deeply( [ 'ABC', 'DEF' ], $myimap->capability( ), + 'mock_capability: (2) two args [ ABC, DEF ] => capability capability [ ABC, DEF ]' + ) ; + + ok( mock_capability( $myimap, 'ABC', 'DEF' ), + 'mock_capability: (1) two args ABC, DEF => capability [ ABC, DEF ]' + ) ; + is_deeply( [ 'ABC', 'DEF' ], [ $myimap->capability( ) ], + 'mock_capability: (2) two args ABC, DEF => capability capability [ ABC, DEF ]' + ) ; + + ok( mock_capability( $myimap, 'IMAP4rev1', 'APPENDLIMIT=123456' ), + 'mock_capability: (1) two args IMAP4rev1, APPENDLIMIT=123456 => capability [ IMAP4rev1, APPENDLIMIT=123456 ]' + ) ; + is_deeply( [ 'IMAP4rev1', 'APPENDLIMIT=123456' ], [ $myimap->capability( ) ], + 'mock_capability: (2) two args IMAP4rev1, APPENDLIMIT=123456 => capability capability [ IMAP4rev1, APPENDLIMIT=123456 ]' + ) ; + + note( 'Leaving tests_mock_capability()' ) ; + return ; +} + +sub sig_install_toggle_sleep +{ + my $mysync = shift ; + if ( ! 'MSWin32' eq $OSNAME ) { + sig_install( $mysync, \&toggle_sleep, 'USR1' ) + } + return ; +} + + +sub mock_capability +{ + my $myimap = shift ; + my @has_capability_value = @ARG ; + my ( $has_capability_value ) = @has_capability_value ; + + if ( ! $myimap ) + { + require_ok( "Test::MockObject" ) ; + $myimap = Test::MockObject->new( ) ; + } + + $myimap->mock( + 'capability', + sub { return wantarray ? + @has_capability_value + : $has_capability_value ; + } + ) ; + + return $myimap ; +} + + +sub tests_capability_of +{ + note( 'Entering tests_capability_of()' ) ; + + is( undef, capability_of( ), + 'capability_of: no args => undef' ) ; + + my $myimap ; + is( undef, capability_of( $myimap ), + 'capability_of: undef => undef' ) ; + + + $myimap = mock_capability( $myimap, 'IMAP4rev1', 'APPENDLIMIT=123456' ) ; + + is( undef, capability_of( $myimap, 'CACA' ), + 'capability_of: two args unknown capability => undef' ) ; + + + is( $NUMBER_123456, capability_of( $myimap, 'APPENDLIMIT' ), + 'capability_of: two args APPENDLIMIT 123456 => 123456 yeah!' ) ; + + note( 'Leaving tests_capability_of()' ) ; + return ; +} + + +sub capability_of +{ + my $imap = shift || return ; + my $capability_keyword = shift || return ; + + my @capability = $imap->capability ; + + if ( ! @capability ) { return ; } + my $capability_value = search_in_array( $capability_keyword, @capability ) ; + + return $capability_value ; +} + + +sub tests_search_in_array +{ + note( 'Entering tests_search_in_array()' ) ; + + is( undef, search_in_array( 'KA' ), + 'search_in_array: no array => undef ' ) ; + + is( 'VA', search_in_array( 'KA', ( 'KA=VA' ) ), + 'search_in_array: KA KA=VA => VA ' ) ; + + is( 'VA', search_in_array( 'KA', ( 'KA=VA', 'KB=VB' ) ), + 'search_in_array: KA KA=VA KB=VB => VA ' ) ; + + is( 'VB', search_in_array( 'KB', ( 'KA=VA', 'KB=VB' ) ), + 'search_in_array: KA=VA KB=VB => VB ' ) ; + + note( 'Leaving tests_search_in_array()' ) ; + return ; +} + +sub search_in_array +{ + my ( $key, @array ) = @ARG ; + + foreach my $item ( @array ) + { + + if ( $item =~ /([^=]+)=(.*)/ ) + { + if ( $1 eq $key ) + { + return $2 ; + } + } + } + + return ; } -sub all_defined { + +sub tests_appendlimit_from_capability +{ + note( 'Entering tests_appendlimit_from_capability()' ) ; + + is( undef, appendlimit_from_capability( ), + 'appendlimit_from_capability: no args => undef' + ) ; + + my $myimap ; + is( undef, appendlimit_from_capability( $myimap ), + 'appendlimit_from_capability: undef arg => undef' + ) ; + + + $myimap = mock_capability( $myimap, 'IMAP4rev1', 'APPENDLIMIT=123456' ) ; + + # Normal behavior + is( $NUMBER_123456, appendlimit_from_capability( $myimap ), + 'appendlimit_from_capability: APPENDLIMIT=123456 => 123456' + ) ; + + # Not a number + $myimap = mock_capability( $myimap, 'IMAP4rev1', 'APPENDLIMIT=ABC' ) ; + + is( undef, appendlimit_from_capability( $myimap ), + 'appendlimit_from_capability: not a number => undef' + ) ; + + note( 'Leaving tests_appendlimit_from_capability()' ) ; + return ; +} + + +sub appendlimit_from_capability +{ + my $myimap = shift ; + if ( ! $myimap ) + { + myprint( "Warn: no imap with call to appendlimit_from_capability\n" ) ; + return ; + } + + #myprint( Data::Dumper->Dump( [ \$myimap ] ) ) ; + my $appendlimit = capability_of( $myimap, 'APPENDLIMIT' ) ; + #myprint( "has_capability APPENDLIMIT $appendlimit\n" ) ; + if ( is_an_integer( $appendlimit ) ) + { + return $appendlimit ; + } + return ; +} + + +sub tests_appendlimit +{ + note( 'Entering tests_appendlimit()' ) ; + + is( undef, appendlimit( ), + 'appendlimit: no args => undef' + ) ; + + my $mysync = { } ; + + is( undef, appendlimit( $mysync ), + 'appendlimit: no imap2 => undef' + ) ; + + my $myimap ; + $myimap = mock_capability( $myimap, 'IMAP4rev1', 'APPENDLIMIT=123456' ) ; + + $mysync->{ imap2 } = $myimap ; + + is( 123456, appendlimit( $mysync ), + 'appendlimit: imap2 with APPENDLIMIT=123456 => 123456' + ) ; + + note( 'Leaving tests_appendlimit()' ) ; + return ; +} + +sub appendlimit +{ + my $mysync = shift || return ; + my $myimap = $mysync->{ imap2 } ; + + my $appendlimit = appendlimit_from_capability( $myimap ) ; + if ( defined $appendlimit ) + { + myprint( "Host2: found APPENDLIMIT=$appendlimit in CAPABILITY\n" ) ; + return $appendlimit ; + } + return ; + +} + + +sub tests_maxsize_setting +{ + note( 'Entering tests_maxsize_setting()' ) ; + + is( undef, maxsize_setting( ), + 'maxsize_setting: no args => undef' + ) ; + + my $mysync ; + + is( undef, maxsize_setting( $mysync ), + 'maxsize_setting: undef arg => undef' + ) ; + + $mysync = { } ; + $mysync->{ maxsize } = $NUMBER_123456 ; + + is( $NUMBER_123456, maxsize_setting( $mysync ), + 'maxsize_setting: --maxsize 123456 alone => 123456' + ) ; + + + $mysync = { } ; + my $myimap ; + + $myimap = mock_capability( $myimap, 'IMAP4rev1', 'APPENDLIMIT=654321' ) ; + $mysync->{ imap2 } = $myimap ; + + # APPENDLIMIT alone + is( $NUMBER_654321, maxsize_setting( $mysync ), + 'maxsize_setting: APPENDLIMIT 654321 alone => 654321' + ) ; + + is( $NUMBER_654321, $mysync->{ maxsize }, + 'maxsize_setting: APPENDLIMIT 654321 alone => maxsize 654321' + ) ; + + + + # Case: "APPENDLIMIT >= --maxsize" => maxsize. + $mysync->{ maxsize } = $NUMBER_123456 ; + + is( $NUMBER_123456, maxsize_setting( $mysync ), + 'maxsize_setting: APPENDLIMIT 654321 --maxsize 123456 => 123456' + ) ; + + # Case: "APPENDLIMIT < --maxsize" => APPENDLIMIT. + + $myimap = mock_capability( $myimap, 'IMAP4rev1', 'APPENDLIMIT=123456' ) ; + $mysync->{ maxsize } = $NUMBER_654321 ; + + is( $NUMBER_123456, maxsize_setting( $mysync ), + 'maxsize_setting: APPENDLIMIT 123456 --maxsize 654321 => 123456 ' + ) ; + + note( 'Leaving tests_maxsize_setting()' ) ; + + return ; +} + +sub maxsize_setting +{ + my $mysync = shift || return ; + + $mysync->{ appendlimit } = appendlimit( $mysync ) ; + + my $maxsize ; + + if ( all_defined( $mysync->{ appendlimit }, $mysync->{ maxsize } ) ) + { + return min( $mysync->{ maxsize }, $mysync->{ appendlimit } ) ; + } + elsif ( defined $mysync->{ appendlimit } ) + { + $mysync->{ maxsize } = $mysync->{ appendlimit } ; + return $mysync->{ maxsize } ; + }elsif ( defined $mysync->{ maxsize } ) + { + return $mysync->{ maxsize } ; + }else + { + return ; + } +} + + + + +sub all_defined +{ if ( not @ARG ) { return 0 ; } @@ -2737,7 +3346,8 @@ sub all_defined { return 1 ; } -sub tests_all_defined { +sub tests_all_defined +{ note( 'Entering tests_all_defined()' ) ; is( 0, all_defined( ), 'all_defined: no param => 0' ) ; @@ -2754,7 +3364,8 @@ sub tests_all_defined { } -sub tests_hashsynclocal { +sub tests_hashsynclocal +{ note( 'Entering tests_hashsynclocal()' ) ; my $mysync = { @@ -2792,7 +3403,8 @@ sub tests_hashsynclocal { return ; } -sub hashsynclocal { +sub hashsynclocal +{ my $mysync = shift ; my $hashkey = shift ; # Optional, only there for tests my $hashfile = $mysync->{ hashfile } ; @@ -2810,7 +3422,8 @@ sub hashsynclocal { } -sub tests_hashsync { +sub tests_hashsync +{ note( 'Entering tests_hashsync()' ) ; @@ -2827,7 +3440,8 @@ sub tests_hashsync { return ; } -sub hashsync { +sub hashsync +{ my $mysync = shift ; my $hashkey = shift ; @@ -2845,7 +3459,8 @@ sub hashsync { } -sub tests_createhashfileifneeded { +sub tests_createhashfileifneeded +{ note( 'Entering tests_createhashfileifneeded()' ) ; is( undef, createhashfileifneeded( ), 'createhashfileifneeded: no parameters => undef' ) ; @@ -2854,7 +3469,8 @@ sub tests_createhashfileifneeded { return ; } -sub createhashfileifneeded { +sub createhashfileifneeded +{ my $hashfile = shift ; my $hashkey = shift || rand32( ) ; @@ -2887,7 +3503,8 @@ sub createhashfileifneeded { return ; } -sub tests_rand32 { +sub tests_rand32 +{ note( 'Entering tests_rand32()' ) ; my $string = rand32( ) ; @@ -2899,14 +3516,16 @@ sub tests_rand32 { return ; } -sub rand32 { +sub rand32 +{ my @chars = ( "a".."z" ) ; my $string; $string .= $chars[rand @chars] for 1..32 ; return $string ; } -sub imap_id_stuff { +sub imap_id_stuff +{ my $mysync = shift ; if ( not $mysync->{id} ) { return ; } ; @@ -2919,7 +3538,8 @@ sub imap_id_stuff { return ; } -sub imap_id { +sub imap_id +{ my ( $mysync, $imap, $Side ) = @_ ; $Side ||= q{} ; @@ -2943,7 +3563,8 @@ sub imap_id { return( $imap_id_response ) ; } -sub imapsync_id { +sub imapsync_id +{ my $mysync = shift ; my $overhashref = shift ; # See http://tools.ietf.org/html/rfc2971.html @@ -2957,7 +3578,7 @@ sub imapsync_id { vendor => 'Gilles LAMIRAL', 'support-url' => 'https://imapsync.lamiral.info/', # Example of date-time: 19-Sep-2015 08:56:07 - date => date_from_rcs( q{$Date: 2018/05/05 21:10:43 $ } ), + date => date_from_rcs( q{$Date: 2019/05/01 22:14:00 $ } ), } ; my $imapsync_id_github = { @@ -2966,7 +3587,7 @@ sub imapsync_id { os => $OSNAME, vendor => 'github', 'support-url' => 'https://github.com/imapsync/imapsync', - date => date_from_rcs( q{$Date: 2018/05/05 21:10:43 $ } ), + date => date_from_rcs( q{$Date: 2019/05/01 22:14:00 $ } ), } ; $imapsync_id = $imapsync_id_lamiral ; @@ -2977,7 +3598,8 @@ sub imapsync_id { return( $imapsync_id_str ) ; } -sub tests_imapsync_id { +sub tests_imapsync_id +{ note( 'Entering tests_imapsync_id()' ) ; my $mysync ; @@ -2987,17 +3609,18 @@ sub tests_imapsync_id { version => 111, os => 'beurk', date => '22-12-1968', - side => 'host1' + side => 'host1' } ), - 'tests_imapsync_id override' + 'tests_imapsync_id override' ) ; note( 'Leaving tests_imapsync_id()' ) ; return ; } -sub format_for_imap_arg { +sub format_for_imap_arg +{ my $ref = shift ; my $string = q{} ; @@ -3018,7 +3641,8 @@ sub format_for_imap_arg { -sub tests_format_for_imap_arg { +sub tests_format_for_imap_arg +{ note( 'Entering tests_format_for_imap_arg()' ) ; ok( 'NIL' eq format_for_imap_arg( { } ), 'format_for_imap_arg empty hash ref' ) ; @@ -3029,8 +3653,9 @@ sub tests_format_for_imap_arg { return ; } -sub quota { - my ( $imap, $side, $mysync ) = @_ ; +sub quota +{ + my ( $mysync, $imap, $side ) = @_ ; my %side = ( h1 => 'Host1', @@ -3050,8 +3675,8 @@ sub quota { #$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 ) ; + my $quota_limit_bytes = quota_extract_storage_limit_in_bytes( $mysync, $getquotaroot ) ; + my $quota_current_bytes = quota_extract_storage_current_in_bytes( $mysync, $getquotaroot ) ; $mysync->{$side}->{quota_limit_bytes} = $quota_limit_bytes ; $mysync->{$side}->{quota_current_bytes} = $quota_current_bytes ; my $quota_percent ; @@ -3068,61 +3693,69 @@ sub quota { return ; } -sub tests_quota_extract_storage_limit_in_bytes { - note( 'Entering tests_quota_extract_storage_limit_in_bytes()' ) ; +sub tests_quota_extract_storage_limit_in_bytes +{ + note( 'Entering tests_quota_extract_storage_limit_in_bytes()' ) ; + my $mysync = {} ; 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_104_857_600 * $KIBI == quota_extract_storage_limit_in_bytes( $imap_output ), 'quota_extract_storage_limit_in_bytes ') ; + ok( $NUMBER_104_857_600 * $KIBI == quota_extract_storage_limit_in_bytes( $mysync, $imap_output ), 'quota_extract_storage_limit_in_bytes ') ; - note( 'Leaving tests_quota_extract_storage_limit_in_bytes()' ) ; + note( 'Leaving tests_quota_extract_storage_limit_in_bytes()' ) ; return ; } -sub quota_extract_storage_limit_in_bytes { +sub quota_extract_storage_limit_in_bytes +{ + my $mysync = shift ; my $imap_output = shift ; my $limit_kb ; $limit_kb = ( map { /.*\(\s*STORAGE\s+\d+\s+(\d+)\s*\)/x ? $1 : () } @{ $imap_output } )[0] ; $limit_kb ||= 0 ; - $debug and myprint( "storage_limit_kb = $limit_kb\n" ) ; + $mysync->{ debug } and myprint( "storage_limit_kb = $limit_kb\n" ) ; return( $KIBI * $limit_kb ) ; } -sub tests_quota_extract_storage_current_in_bytes { +sub tests_quota_extract_storage_current_in_bytes +{ note( 'Entering tests_quota_extract_storage_current_in_bytes()' ) ; - + my $mysync = {} ; 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 ') ; + ok( 1*$KIBI == quota_extract_storage_current_in_bytes( $mysync, $imap_output ), 'quota_extract_storage_current_in_bytes: 1 => 1024 ') ; note( 'Leaving tests_quota_extract_storage_current_in_bytes()' ) ; return ; } -sub quota_extract_storage_current_in_bytes { +sub quota_extract_storage_current_in_bytes +{ + my $mysync = shift ; my $imap_output = shift ; my $current_kb ; $current_kb = ( map { /.*\(\s*STORAGE\s+(\d+)\s+\d+\s*\)/x ? $1 : () } @{ $imap_output } )[0] ; $current_kb ||= 0 ; - $debug and myprint( "storage_current_kb = $current_kb\n" ) ; + $mysync->{ debug } and myprint( "storage_current_kb = $current_kb\n" ) ; return( $KIBI * $current_kb ) ; } -sub automap { +sub automap +{ my ( $mysync ) = @_ ; if ( $mysync->{automap} ) { @@ -3132,8 +3765,8 @@ sub automap { return ; } - $mysync->{h1_special} = special_from_folders_hash( $mysync->{imap1}, 'Host1' ) ; - $mysync->{h2_special} = special_from_folders_hash( $mysync->{imap2}, 'Host2' ) ; + $mysync->{h1_special} = special_from_folders_hash( $mysync, $mysync->{imap1}, 'Host1' ) ; + $mysync->{h2_special} = special_from_folders_hash( $mysync, $mysync->{imap2}, 'Host2' ) ; build_possible_special( $mysync ) ; build_guess_special( $mysync ) ; @@ -3145,7 +3778,8 @@ sub automap { -sub build_guess_special { +sub build_guess_special +{ my ( $mysync ) = shift ; foreach my $h1_fold ( sort keys %{ $mysync->{h1_folders_all} } ) { @@ -3175,7 +3809,8 @@ sub build_guess_special { return ; } -sub guess_special { +sub guess_special +{ my( $folder, $possible_special_ref, $prefix ) = @_ ; my $folder_no_prefix = $folder ; @@ -3189,7 +3824,8 @@ sub guess_special { return( $guess_special ) ; } -sub tests_guess_special { +sub tests_guess_special +{ note( 'Entering tests_guess_special()' ) ; my $possible_special_ref = build_possible_special( my $mysync ) ; @@ -3202,9 +3838,10 @@ sub tests_guess_special { return ; } -sub build_automap { +sub build_automap +{ my $mysync = shift ; - $debug and myprint( "Entering build_automap\n" ) ; + $mysync->{ debug } and myprint( "Entering build_automap\n" ) ; foreach my $h1_fold ( @{ $mysync->{h1_folders_wanted} } ) { my $h2_fold ; my $h1_special = $mysync->{h1_special}{$h1_fold} ; @@ -3253,7 +3890,8 @@ sub build_automap { # I will not add what there is at: # http://stackoverflow.com/questions/2185391/localized-gmail-imap-folders/2185548#2185548 # because it works well without -sub build_possible_special { +sub build_possible_special +{ my $mysync = shift ; my $possible_special = { } ; # All|Archive|Drafts|Flagged|Junk|Sent|Trash @@ -3272,19 +3910,41 @@ sub build_possible_special { 'Elementy wys&AUI-ane'] ; $possible_special->{'\Trash'} = [ 'Trash', 'TRASH', '&BCMENAQwBDsENQQ9BD0ESwQ1-', '&BBoEPgRABDcEOAQ9BDA-', 'Kosz', 'Deleted Items' ] ; - + foreach my $special ( qw( \All \Archive \Drafts \Flagged \Junk \Sent \Trash ) ){ foreach my $possible_folder ( @{ $possible_special->{$special} } ) { $possible_special->{ $possible_folder } = $special ; } ; } $mysync->{possible_special} = $possible_special ; - $debug and myprint( Data::Dumper->Dump( [ $possible_special ], [ 'possible_special' ] ) ) ; + $mysync->{ debug } and myprint( Data::Dumper->Dump( [ $possible_special ], [ 'possible_special' ] ) ) ; return( $possible_special ) ; } -sub special_from_folders_hash { - my ( $imap, $side ) = @_ ; +sub tests_special_from_folders_hash +{ + note( 'Entering tests_special_from_folders_hash()' ) ; + + my $mysync = {} ; + require_ok( "Test::MockObject" ) ; + my $imapT = Test::MockObject->new( ) ; + + is( undef, special_from_folders_hash( ), 'special_from_folders_hash: no args' ) ; + is( undef, special_from_folders_hash( $mysync ), 'special_from_folders_hash: undef args' ) ; + is_deeply( {}, special_from_folders_hash( $mysync, $imapT ), 'special_from_folders_hash: $imap void' ) ; + + $imapT->mock( 'folders_hash', sub { return( [ { name => 'Sent', attrs => [ '\Sent' ] } ] ) } ) ; + + is_deeply( { Sent => '\Sent', '\Sent' => 'Sent' }, + special_from_folders_hash( $mysync, $imapT ), 'special_from_folders_hash: $imap \Sent' ) ; + + note( 'Leaving tests_special_from_folders_hash()' ) ; + return( ) ; +} + +sub special_from_folders_hash +{ + my ( $mysync, $imap, $side ) = @_ ; my %special = ( ) ; if ( ! defined $imap ) { return ; } @@ -3292,7 +3952,7 @@ sub special_from_folders_hash { if ( ! $imap->can( 'folders_hash' ) ) { my $error = "$side: To have automagic rfc6154 folder mapping, upgrade Mail::IMAPClient >= 3.34\n" ; - errors_incr( $sync, $error ) ; + errors_incr( $mysync, $error ) ; return( \%special ) ; # empty hash ref } my $folders_hash = $imap->folders_hash( ) ; @@ -3315,26 +3975,10 @@ sub special_from_folders_hash { return( \%special ) ; } -sub tests_special_from_folders_hash { - note( 'Entering tests_special_from_folders_hash()' ) ; - - - require Test::MockObject ; - my $imapT = Test::MockObject->new( ) ; - - is( undef, special_from_folders_hash( ), 'special_from_folders_hash: no args' ) ; - is_deeply( {}, special_from_folders_hash( $imapT ), 'special_from_folders_hash: $imap void' ) ; - - $imapT->mock( 'folders_hash', sub { return( [ { name => 'Sent', attrs => [ '\Sent' ] } ] ) } ) ; - is_deeply( { Sent => '\Sent', '\Sent' => 'Sent' }, special_from_folders_hash( $imapT ), 'special_from_folders_hash: $imap \Sent' ) ; - - note( 'Leaving tests_special_from_folders_hash()' ) ; - return( ) ; -} - -sub errors_incr { +sub errors_incr +{ my ( $mysync, @error ) = @ARG ; - $sync->{nb_errors}++ ; + $mysync->{nb_errors}++ ; if ( @error ) { errors_log( $mysync, @error ) ; @@ -3342,10 +3986,10 @@ sub errors_incr { } $mysync->{errorsmax} ||= $ERRORS_MAX ; - if ( $sync->{nb_errors} >= $mysync->{errorsmax} ) { - myprint( "Maximum number of errors $mysync->{errorsmax} reached ( you can change $mysync->{errorsmax} to any value, for example 100 with --errorsmax 100 ). Exiting.\n" ) ; + if ( $mysync->{nb_errors} >= $mysync->{errorsmax} ) { + myprint( "Maximum number of errors $mysync->{errorsmax} reached ( you can change $mysync->{errorsmax} to any value, for example 100 with --errorsmax 100 ). Exiting.\n" ) ; if ( $mysync->{errorsdump} ) { - myprint( errorsdump( $sync->{nb_errors}, errors_log( $mysync ) ) ) ; + myprint( errorsdump( $mysync->{nb_errors}, errors_log( $mysync ) ) ) ; # again since errorsdump( ) can be very verbose and masquerade previous warning myprint( "Maximum number of errors $mysync->{errorsmax} reached ( you can change $mysync->{errorsmax} to any value, for example 100 with --errorsmax 100 ). Exiting.\n" ) ; } @@ -3354,7 +3998,23 @@ sub errors_incr { return ; } -sub errors_log { +sub tests_errors_log +{ + note( 'Entering tests_errors_log()' ) ; + is( undef, errors_log( ), 'errors_log: no args => undef' ) ; + my $mysync = {} ; + is( undef, errors_log( $mysync ), 'errors_log: empty => undef' ) ; + is_deeply( [ 'aieaie' ], [ errors_log( $mysync, 'aieaie' ) ], 'errors_log: aieaie => aieaie' ) ; + # cumulative + is_deeply( [ 'aieaie' ], [ errors_log( $mysync ) ], 'errors_log: nothing more => aieaie' ) ; + is_deeply( [ 'aieaie', 'ouille' ], [ errors_log( $mysync, 'ouille' ) ], 'errors_log: ouille => aieaie ouille' ) ; + is_deeply( [ 'aieaie', 'ouille' ], [ errors_log( $mysync ) ], 'errors_log: nothing more => aieaie ouille' ) ; + note( 'Leaving tests_errors_log()' ) ; + return ; +} + +sub errors_log +{ my ( $mysync, @error ) = @ARG ; if ( ! $mysync->{errors_log} ) { @@ -3372,16 +4032,9 @@ sub errors_log { } } -sub tests_errors_log { - note( 'Entering tests_errors_log()' ) ; - - note( 'Leaving tests_errors_log()' ) ; - return ; -} - - -sub errorsdump { +sub errorsdump +{ my( $nb_errors, @errors_log ) = @ARG ; my $error_num = 0 ; my $errors_list = q{} ; @@ -3396,7 +4049,8 @@ sub errorsdump { } -sub tests_live_result { +sub tests_live_result +{ note( 'Entering tests_live_result()' ) ; my $nb_errors = shift ; @@ -3409,77 +4063,94 @@ sub tests_live_result { return ; } -sub foldersizesatend { +sub foldersizesatend +{ + my $mysync = shift ; timenext( ) ; - return if ( $imap1->IsUnconnected( ) ) ; - return if ( $imap2->IsUnconnected( ) ) ; + return if ( $mysync->{imap1}->IsUnconnected( ) ) ; + return if ( $mysync->{imap2}->IsUnconnected( ) ) ; # Get all folders on host2 again since new were created - @h2_folders_all = sort $imap2->folders(); + @h2_folders_all = sort $mysync->{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, $sync->{abletosearch1}, @h1_folders_wanted ) ; - ( $h2_nb_msg_end, $h2_bytes_end ) = foldersizes( 'Host2', $imap2, $search2, $sync->{abletosearch2}, @h2_folders_from_1_wanted ) ; + ( $h1_nb_msg_end, $h1_bytes_end ) = foldersizes( 'Host1', $mysync->{imap1}, $search1, $mysync->{abletosearch1}, @h1_folders_wanted ) ; + ( $h2_nb_msg_end, $h2_bytes_end ) = foldersizes( 'Host2', $mysync->{imap2}, $search2, $mysync->{abletosearch2}, @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 ) ; + errors_incr( $mysync, $error ) ; } return ; } -sub size_filtered_flag { +sub size_filtered_flag +{ + my $mysync = shift ; my $h1_size = shift ; - if (defined $maxsize and $h1_size >= $maxsize) { + if ( defined $mysync->{ maxsize } and $h1_size >= $mysync->{ maxsize } ) { return( 1 ) ; } - if (defined $minsize and $h1_size <= $minsize) { + 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 ) = @_ ; +sub sync_flags_fir +{ + my ( $mysync, $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 ) ; + return if size_filtered_flag( $mysync, $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 ) ; + sync_flags( $mysync, $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 ) = @_ ; +sub sync_flags_after_copy +{ + # Activated with option --syncflagsaftercopy + my( $mysync, $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 ) ; + if ( my @h2_flags = $mysync->{imap2}->flags( $h2_msg ) ) { + my $h2_flags = "@h2_flags" ; + ( $mysync->{ debug } or $debugflags ) and myprint( "Host2: msg $h2_fold/$h2_msg flags before sync flags after copy ( $h2_flags )\n" ) ; + sync_flags( $mysync, $h1_fold, $h1_msg, $h1_flags, $h2_fold, $h2_msg, $h2_flags, $permanentflags2 ) ; + }else{ + myprint( "Host2: msg $h2_fold/$h2_msg could not get its flags for sync flags after copy\n" ) ; + } return ; } -sub sync_flags { - my( $h1_fold, $h1_msg, $h1_flags, $h2_fold, $h2_msg, $h2_flags, $permanentflags2 ) = @_ ; +# Globals +# $debug +# $debugflags +# $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" ) ; + +sub sync_flags +{ + my( $mysync, $h1_fold, $h1_msg, $h1_flags, $h2_fold, $h2_msg, $h2_flags, $permanentflags2 ) = @_ ; + + ( $mysync->{ debug } or $debugflags ) and + myprint( "Host1: flags init msg $h1_fold/$h1_msg flags( $h1_flags ) Host2 msg $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" ) ; + ( $mysync->{ debug } or $debugflags ) and + myprint( "Host1: flags filt msg $h1_fold/$h1_msg flags( $h1_flags ) Host2 msg $h2_fold/$h2_msg flags( $h2_flags )\n" ) ; # compare flags - set flags if there a difference @@ -3487,17 +4158,18 @@ sub sync_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. + $diff and ( $mysync->{ debug } or $debugflags ) + and myprint( "Host2: flags msg $h2_fold/$h2_msg replacing h2 flags( $h2_flags ) with h1 flags( $h1_flags )\n" ) ; - if ( not $sync->{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 ) ; + # This sets flags exactly. So flags can be removed with this. + # When you remove a \Seen flag on host1 you want it + # to be removed on host2. Just add flags is not what + # we need most of the time, so no + like in "+FLAGS.SILENT". + + if ( not $mysync->{dry} and $diff and not $mysync->{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]: ", + $mysync->{imap2}->LastError || q{}, "\n" ; + errors_incr( $mysync, $error_msg ) ; } return ; @@ -3505,11 +4177,13 @@ sub sync_flags { -sub _filter { +sub _filter +{ + my $mysync = shift ; my $str = shift or return q{} ; my $sz = $SIZE_MAX_STR ; my $len = length $str ; - if ( not $debug and $len > $sz*2 ) { + if ( not $mysync->{ debug } and $len > $sz*2 ) { my $beg = substr $str, 0, $sz ; my $end = substr $str, -$sz, $sz ; $str = $beg . '...' . $end ; @@ -3520,17 +4194,18 @@ sub _filter { -sub lost_connection { - my( $imap, $error_message ) = @_; +sub lost_connection +{ + my( $mysync, $imap, $error_message ) = @_; if ( $imap->IsUnconnected( ) ) { - $sync->{nb_errors}++ ; + $mysync->{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) ; + $lcomm = _filter( $mysync, $lcomm ) ; + $einfo = _filter( $mysync, $einfo ) ; + myprint( "Failure: last command: $lcomm\n") if ( $mysync->{ debug } && $lcomm) ; myprint( "Failure: lost connection $error_message: ", $einfo, "\n") ; return( 1 ) ; } @@ -3539,8 +4214,10 @@ sub lost_connection { } } -sub tests_max { +sub tests_max +{ note( 'Entering tests_max()' ) ; + is( 0, max( 0 ), 'max 0 => 0' ) ; is( 1, max( 1 ), 'max 1 => 1' ) ; is( $MINUS_ONE, max( $MINUS_ONE ), 'max -1 => -1') ; @@ -3566,10 +4243,11 @@ sub tests_max { return ; } -sub max { +sub max +{ my @list = @_ ; return( undef ) if ( 0 == scalar @list ) ; - + my( @numbers, @notnumbers ) ; foreach my $item ( @list ) { if ( is_number( $item ) ) { @@ -3578,7 +4256,7 @@ sub max { push @notnumbers, $item ; } } - + my @sorted ; if ( @numbers ) { @sorted = sort { $a <=> $b } @numbers ; @@ -3587,11 +4265,14 @@ sub max { }else{ return ; } - + return( pop @sorted ) ; } -sub tests_is_number { +sub tests_is_number +{ + note( 'Entering tests_is_number()' ) ; + ok( ! is_number( ), 'is_number: no args => undef ' ) ; ok( is_number( 1 ), 'is_number: 1 => 1' ) ; ok( is_number( 1.1 ), 'is_number: 1.1 => 1' ) ; @@ -3603,23 +4284,27 @@ sub tests_is_number { ok( ! is_number( '0haha' ), 'is_number: 0haha => no' ) ; ok( ! is_number( '2haha' ), 'is_number: 2haha => no' ) ; ok( ! is_number( 'haha2' ), 'is_number: haha2 => no' ) ; + + note( 'Leaving tests_is_number()' ) ; return ; } -sub is_number { +sub is_number +{ my $item = shift ; - + if ( ! defined $item ) { return ; } - + if ( $item =~ /\A$RE{num}{real}\Z/ ) { return 1 ; } return ; } -sub tests_min { +sub tests_min +{ note( 'Entering tests_min()' ) ; is( 0, min( 0 ), 'min 0 => 0' ) ; @@ -3633,7 +4318,7 @@ sub tests_min { is( 1, min( '100', '42', 1 ), 'min 100 42 1 => 1' ) ; is( 1, min( $NUMBER_100, 'haha', 1 ), 'min 100 haha 1 => 1') ; is( $MINUS_ONE, min( $MINUS_ONE, 1 ), 'min -1 1 => -1') ; - + is( 1, min( undef, 1 ), 'min undef 1 => 1' ) ; is( 0, min( undef, 0 ), 'min undef 0 => 0' ) ; is( 1, min( undef, 1 ), 'min undef 1 => 1' ) ; @@ -3649,7 +4334,8 @@ sub tests_min { } -sub min { +sub min +{ my @list = @_ ; return( undef ) if ( 0 == scalar @list ) ; @@ -3661,7 +4347,7 @@ sub min { push @notnumbers, $item ; } } - + my @sorted ; if ( @numbers ) { @sorted = sort { $a <=> $b } @numbers ; @@ -3675,8 +4361,10 @@ sub min { } -sub check_lib_version { - $debug and myprint( "IMAPClient $Mail::IMAPClient::VERSION\n" ) ; +sub check_lib_version +{ + my $mysync = shift ; + $mysync->{ 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 ; @@ -3689,18 +4377,21 @@ sub check_lib_version { return ; } -sub module_version_str { +sub module_version_str +{ my( $module_name, $module_version ) = @_ ; my $str = mysprintf( "%-20s %s\n", $module_name, $module_version ) ; return( $str ) ; } -sub modulesversion { +sub modulesversion +{ my @list_version; my %modulesversion = ( 'Authen::NTLM' => sub { $Authen::NTLM::VERSION }, + 'CGI' => sub { $CGI::VERSION }, 'Compress::Zlib' => sub { $Compress::Zlib::VERSION }, 'Crypt::OpenSSL::RSA' => sub { $Crypt::OpenSSL::RSA::VERSION }, 'Data::Uniqid' => sub { $Data::Uniqid::VERSION }, @@ -3711,10 +4402,11 @@ sub modulesversion { 'File::Spec' => sub { $File::Spec::VERSION }, 'Getopt::Long' => sub { $Getopt::Long::VERSION }, 'HTML::Entities' => sub { $HTML::Entities::VERSION }, - 'IO::Socket::INET6' => sub { $IO::Socket::INET6::VERSION }, - 'IO::Socket::INET' => sub { $IO::Socket::INET::VERSION }, - 'IO::Socket::SSL' => sub { $IO::Socket::SSL::VERSION }, 'IO::Socket' => sub { $IO::Socket::VERSION }, + 'IO::Socket::INET' => sub { $IO::Socket::INET::VERSION }, + 'IO::Socket::INET6' => sub { $IO::Socket::INET6::VERSION }, + 'IO::Socket::IP' => sub { $IO::Socket::IP::VERSION }, + 'IO::Socket::SSL' => sub { $IO::Socket::SSL::VERSION }, 'IO::Tee' => sub { $IO::Tee::VERSION }, 'JSON' => sub { $JSON::VERSION }, 'JSON::WebToken' => sub { $JSON::WebToken::VERSION }, @@ -3742,17 +4434,58 @@ sub modulesversion { push @list_version, module_version_str( $module_name, $v ) ; } - return( @list_version ) ; } +sub tests_command_line_nopassword +{ + note( 'Entering tests_command_line_nopassword()' ) ; + + ok( q{} eq command_line_nopassword(), 'command_line_nopassword void' ); + my $mysync = {} ; + ok( '--blabla' eq command_line_nopassword( $mysync, '--blabla' ), 'command_line_nopassword --blabla' ); + #myprint( command_line_nopassword((qw{ --password1 secret1 })), "\n" ) ; + ok( '--password1 MASKED' eq command_line_nopassword( $mysync, qw{ --password1 secret1}), 'command_line_nopassword --password1' ); + ok( '--blabla --password1 MASKED --blibli' + eq command_line_nopassword( $mysync, qw{ --blabla --password1 secret1 --blibli } ), 'command_line_nopassword --password1 --blibli' ); + $mysync->{showpasswords} = 1 ; + ok( q{} eq command_line_nopassword(), 'command_line_nopassword void' ); + ok( '--blabla' eq command_line_nopassword( $mysync, '--blabla'), 'command_line_nopassword --blabla' ); + #myprint( command_line_nopassword((qw{ --password1 secret1 })), "\n" ) ; + ok( '--password1 secret1' eq command_line_nopassword( $mysync, qw{ --password1 secret1} ), 'command_line_nopassword --password1' ); + ok( '--blabla --password1 secret1 --blibli' + eq command_line_nopassword( $mysync, qw{ --blabla --password1 secret1 --blibli } ), 'command_line_nopassword --password1 --blibli' ); + + note( 'Leaving tests_command_line_nopassword()' ) ; + return ; +} + # Construct a command line copy with passwords replaced by MASKED. -sub command_line_nopassword { - my @argv = @_ ; +sub command_line_nopassword +{ + my $mysync = shift @ARG ; + my @argv = @ARG ; my @argv_nopassword ; - return( "@argv" ) if $sync->{showpasswords} ; + if ( $mysync->{ cmdcgi } ) { + @argv_nopassword = mask_password_value( @{ $mysync->{ cmdcgi } } ) ; + return( "@argv_nopassword" ) ; + } + + if ( $mysync->{showpasswords} ) + { + return( "@argv" ) ; + } + + @argv_nopassword = mask_password_value( @argv ) ; + return("@argv_nopassword") ; +} + +sub mask_password_value +{ + my @argv = @ARG ; + my @argv_nopassword ; while ( @argv ) { my $arg = shift @argv ; # option name or value if ( $arg =~ m/-password[12]/x ) { @@ -3762,34 +4495,54 @@ sub command_line_nopassword { push @argv_nopassword, $arg ; # same option or value } } - return("@argv_nopassword") ; + return @argv_nopassword ; } -sub tests_command_line_nopassword { - note( 'Entering 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'); - $sync->{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'); +sub tests_get_stdin_masked +{ + note( 'Entering tests_get_stdin_masked()' ) ; - note( 'Leaving tests_command_line_nopassword()' ) ; + is( q{}, get_stdin_masked( ), 'get_stdin_masked: no args' ) ; + is( q{}, get_stdin_masked( 'Please ENTER: ' ), 'get_stdin_masked: ENTER' ) ; + + note( 'Leaving tests_get_stdin_masked()' ) ; return ; } -sub ask_for_password { - my ( $user, $host ) = @ARG ; - myprint( "What's the password for $user" . '@' . "$host? (not visible while you type, then enter RETURN) " ) ; +####################################################### +# The issue is that prompt() does not prompt the prompt +# when the program is used like +# { sleep 2 ; echo blablabla ; } | ./imapsync ...--host1 lo --user1 tata --host2 lo --user2 titi + +# use IO::Prompter ; +sub get_stdin_masked +{ + my $prompt = shift || 'Say something: ' ; + local @ARGV = () ; + my $input = prompt( + -prompt => $prompt, + -echo => '*', + ) ; + #myprint( "You said: $input\n" ) ; + return $input ; +} + +sub ask_for_password_new +{ + my $prompt = shift ; + my $password = get_stdin_masked( $prompt ) ; + return $password ; +} +######################################################### + + +sub ask_for_password +{ + my $prompt = shift ; + myprint( $prompt ) ; Term::ReadKey::ReadMode( 2 ) ; + ## no critic (InputOutput::ProhibitExplicitStdin) my $password = <STDIN> ; chomp $password ; myprint( "\nGot it\n" ) ; @@ -3799,34 +4552,38 @@ sub ask_for_password { # Have to refactor get_password1() get_password2() # to have only get_password() and two calls -sub get_password1 { +sub get_password1 +{ my $mysync = shift ; $mysync->{password1} - || $passfile1 + || $mysync->{ passfile1 } || 'PREAUTH' eq $authmech1 || 'EXTERNAL' eq $authmech1 || $ENV{IMAPSYNC_PASSWORD1} - || do { - myprint( << 'FIN_PASSFILE' ) ; + || 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". An other solution is to set the environment variable IMAPSYNC_PASSWORD1 FIN_PASSFILE + my $user = $authuser1 || $mysync->{user1} ; + my $host = $mysync->{host1} ; + my $prompt = "What's the password for $user" . ' at ' . "$host? (not visible while you type, then enter RETURN) " ; + $mysync->{password1} = ask_for_password( $prompt ) ; + } ; - $mysync->{password1} = ask_for_password( $authuser1 || $mysync->{user1}, $mysync->{host1} ) ; - } ; - - if ( defined $passfile1 ) { - if ( ! -e -r $passfile1 ) { - myprint( "Failure: file from parameter --passfile1 $passfile1 does not exist or is not readable\n" ) ; + if ( defined $mysync->{ passfile1 } ) { + if ( ! -e -r $mysync->{ passfile1 } ) { + myprint( "Failure: file from parameter --passfile1 $mysync->{ passfile1 } does not exist or is not readable\n" ) ; exit_clean( $mysync, $EX_NOINPUT ) ; } # passfile1 readable - $mysync->{password1} = firstline ( $passfile1 ) ; + $mysync->{password1} = firstline ( $mysync->{ passfile1 } ) ; return ; } if ( $ENV{IMAPSYNC_PASSWORD1} ) { @@ -3836,35 +4593,39 @@ FIN_PASSFILE return ; } -sub get_password2 { +sub get_password2 +{ my $mysync = shift ; $mysync->{password2} - || $passfile2 + || $mysync->{ passfile2 } || 'PREAUTH' eq $authmech2 || 'EXTERNAL' eq $authmech2 || $ENV{IMAPSYNC_PASSWORD2} - || do { - myprint( << 'FIN_PASSFILE' ) ; + || 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". An other solution is to set the environment variable IMAPSYNC_PASSWORD2 FIN_PASSFILE - - $mysync->{password2} = ask_for_password( $authuser2 || $mysync->{user2}, $mysync->{host2} ) ; - } ; + my $user = $authuser2 || $mysync->{user2} ; + my $host = $mysync->{host2} ; + my $prompt = "What's the password for $user" . ' at ' . "$host? (not visible while you type, then enter RETURN) " ; + $mysync->{password2} = ask_for_password( $prompt ) ; + } ; - if ( defined $passfile2 ) { - if ( ! -e -r $passfile2 ) { - myprint( "Failure: file from parameter --passfile2 $passfile2 does not exist or is not readable\n" ) ; + if ( defined $mysync->{ passfile2 } ) { + if ( ! -e -r $mysync->{ passfile2 } ) { + myprint( "Failure: file from parameter --passfile2 $mysync->{ passfile2 } does not exist or is not readable\n" ) ; exit_clean( $mysync, $EX_NOINPUT ) ; } # passfile2 readable - $mysync->{password2} = firstline ( $passfile2 ) ; + $mysync->{password2} = firstline ( $mysync->{ passfile2 } ) ; return ; } if ( $ENV{IMAPSYNC_PASSWORD2} ) { @@ -3876,63 +4637,169 @@ FIN_PASSFILE -sub catch_ignore { + +sub remove_tmp_files +{ + my $mysync = shift or return ; + $mysync->{pidfile} or return ; + if ( -e $mysync->{pidfile} ) { + unlink $mysync->{pidfile} ; + } + return ; +} + +sub cleanup_before_exit +{ + my $mysync = shift ; + remove_tmp_files( $mysync ) ; + if ( $mysync->{imap1} and $mysync->{imap1}->IsConnected() ) + { + myprint( "Disconnecting from host1 $mysync->{ host1 } user1 $mysync->{ user1 }\n" ) ; + $mysync->{imap1}->logout( ) ; + } + if ( $mysync->{imap2} and $mysync->{imap2}->IsConnected() ) + { + myprint( "Disconnecting from host2 $mysync->{ host2 } user2 $mysync->{ user2 }\n" ) ; + $mysync->{imap2}->logout( ) ; + } + if ( $mysync->{log} ) { + myprint( "Log file is $mysync->{logfile} ( to change it, use --logfile filepath ; or use --nolog to turn off logging )\n" ) ; + } + if ( $mysync->{log} and $mysync->{logfile_handle} ) { + #myprint( "Closing $mysync->{ logfile }\n" ) ; + close $mysync->{logfile_handle} ; + } + return ; +} + + + +sub exit_clean +{ + my $mysync = shift @ARG ; + my $status = shift @ARG ; + my @messages = @ARG ; + if ( @messages ) + { + myprint( @messages ) ; + } + myprint( "Exiting with return value $status\n" ) ; + cleanup_before_exit( $mysync ) ; + + exit $status ; +} + +sub missing_option +{ + my $mysync = shift ; + my $option = shift ; + exit_clean( $mysync, $EX_USAGE, "$option option is mandatory, for help run $PROGRAM_NAME --help\n" ) ; + return ; +} + + +sub catch_ignore +{ my $mysync = shift ; my $signame = shift ; - + my $sigcounter = ++$mysync->{ sigcounter }{ $signame } ; - myprint( "\nGot a signal $signame (my PID is $PROCESS_ID). Received $sigcounter $signame signals so far. Thanks!\n" ) ; + myprint( "\nGot a signal $signame (my PID is $PROCESS_ID my PPID is ", getppid( ), + "). Received $sigcounter $signame signals so far. Thanks!\n" ) ; stats( $mysync ) ; return ; } -sub catch_exit { +sub catch_exit +{ my $mysync = shift ; my $signame = shift || q{} ; if ( $signame ) { - myprint( "\nGot a signal $signame (my PID is $PROCESS_ID). Asked to terminate\n" ) ; + myprint( "\nGot a signal $signame (my PID is $PROCESS_ID my PPID is ", getppid( ), + "). Asked to terminate\n" ) ; + if ( $mysync->{stats} ) { + myprint( "Here are the final stats of this sync not completely finished so far\n" ) ; + stats( $mysync ) ; + myprint( "Ended by a signal $signame (my PID is $PROCESS_ID my PPID is ", + getppid( ), "). I am asked to terminate immediately.\n" ) ; + myprint( "You should resynchronize those accounts by running a sync again,\n", + "since some messages and entire folders might still be missing on host2.\n" ) ; + } + ## no critic (RequireLocalizedPunctuationVars) + $SIG{ $signame } = 'DEFAULT'; # restore default action + # kill myself with $signame + # https://www.cons.org/cracauer/sigint.html + myprint( "Killing myself with signal $signame\n" ) ; + cleanup_before_exit( $mysync ) ; + kill( $signame, $PROCESS_ID ) ; + } + else + { + exit_clean( $mysync, $EXIT_BY_SIGNAL ) ; } - myprint( "Here are the final stats of this sync not completely finished so far\n" ) ; - stats( $mysync ) ; - myprint( "Ended by a signal $signame (my PID is $PROCESS_ID). I am asked to terminate immediately.\n" ) ; - myprint( "You should resynchronize those accounts by running a sync again,\n", - "since some messages and entire folders might still be missing on host2.\n" ) ; - exit_clean( $mysync, $EXIT_BY_SIGNAL ) ; return ; } -sub catch_reconnect { + +sub catch_print +{ my $mysync = shift ; my $signame = shift ; - myprint( "\nGot a signal $signame (my PID is $PROCESS_ID)\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}->{IMAPSYNC_RECONNECT_COUNT} += 1 ; - $mysync->{imap1}->reconnect( ) ; - myprint( "Info: reconnecting to host2 imap server\n" ) ; - $mysync->{imap2}->State( Mail::IMAPClient::Unconnected ) ; - $mysync->{imap2}->{IMAPSYNC_RECONNECT_COUNT} += 1 ; - $mysync->{imap2}->reconnect( ) ; - myprint( "Info: reconnected to both imap servers\n" ) ; + my $sigcounter = ++$mysync->{ sigcounter }{ $signame } ; + myprint( "\nGot a signal $signame (my PID is $PROCESS_ID my PPID is ", getppid( ), + "). Received $sigcounter $signame signals so far. Thanks!\n" ) ; return ; } -sub tests_reconnect_12_if_needed { + +sub catch_reconnect +{ + my $mysync = shift ; + my $signame = shift ; + if ( here_twice( $mysync ) ) { + myprint( "Got two signals $signame within $INTERVAL_TO_EXIT seconds. Exiting...\n" ) ; + catch_exit( $mysync, $signame ) ; + }else{ + myprint( "\nGot a signal $signame (my PID is $PROCESS_ID my PPID is ", getppid( ), ")\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", + ) ; + 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 $mysync->{host1}\n" ) ; + $mysync->{imap1}->State( Mail::IMAPClient::Unconnected ) ; + $mysync->{imap1}->{IMAPSYNC_RECONNECT_COUNT} += 1 ; + if ( $mysync->{imap1}->reconnect( ) ) + { + myprint( "Info: reconnected to host1 imap server $mysync->{host1}\n" ) ; + } + else + { + exit_clean( $mysync, $EXIT_CONNECTION_FAILURE ) ; + } + myprint( "Info: reconnecting to host2 imap server\n" ) ; + $mysync->{imap2}->State( Mail::IMAPClient::Unconnected ) ; + $mysync->{imap2}->{IMAPSYNC_RECONNECT_COUNT} += 1 ; + if ( $mysync->{imap2}->reconnect( ) ) + { + myprint( "Info: reconnected to host2 imap server $mysync->{host2}\n" ) ; + } + else + { + exit_clean( $mysync, $EXIT_CONNECTION_FAILURE ) ; + } + myprint( "Info: reconnected to both imap servers\n" ) ; + } + return ; +} + +sub tests_reconnect_12_if_needed +{ note( 'Entering tests_reconnect_12_if_needed()' ) ; my $mysync ; @@ -3949,7 +4816,8 @@ sub tests_reconnect_12_if_needed { return ; } -sub reconnect_12_if_needed { +sub reconnect_12_if_needed +{ my $mysync = shift ; #return 2 ; if ( ! reconnect_if_needed( $mysync->{imap1} ) ) { @@ -3963,7 +4831,8 @@ sub reconnect_12_if_needed { } -sub tests_reconnect_if_needed { +sub tests_reconnect_if_needed +{ note( 'Entering tests_reconnect_if_needed()' ) ; @@ -3983,7 +4852,8 @@ sub tests_reconnect_if_needed { return ; } -sub reconnect_if_needed { +sub reconnect_if_needed +{ # return undef upon failure. # return 1 upon connection success, with or without reconnection. @@ -4015,7 +4885,8 @@ sub reconnect_if_needed { -sub here_twice { +sub here_twice +{ my $mysync = shift ; my $now = time ; my $previous = $mysync->{lastcatch} || 0 ; @@ -4029,35 +4900,42 @@ sub here_twice { } -sub justconnect { - - $imap1 = connect_imap( $sync->{host1}, $sync->{port1}, $debugimap1, $sync->{ssl1}, $sync->{tls1}, 'Host1', $sync->{h1}->{timeout}, $sync->{h1} ) ; - $imap2 = connect_imap( $sync->{host2}, $sync->{port2}, $debugimap2, $sync->{ssl2}, $sync->{tls2}, 'Host2', $sync->{h2}->{timeout}, $sync->{h2} ) ; - $imap1->logout( ) ; - $imap2->logout( ) ; +sub justconnect +{ + my $mysync = shift ; + $mysync->{imap1} = connect_imap( $mysync->{host1}, $mysync->{port1}, $debugimap1, + $mysync->{ssl1}, $mysync->{tls1}, 'Host1', $mysync->{h1}->{timeout}, $mysync->{h1} ) ; + $mysync->{imap2} = connect_imap( $mysync->{host2}, $mysync->{port2}, $debugimap2, + $mysync->{ssl2}, $mysync->{tls2}, 'Host2', $mysync->{h2}->{timeout}, $mysync->{h2} ) ; + $mysync->{imap1}->logout( ) ; + $mysync->{imap2}->logout( ) ; return ; } +sub skip_macosx +{ + return ; + return( 'macosx.polarhome.com' eq hostname() ) ; +} -sub tests_mailimapclient_connect { +sub tests_mailimapclient_connect +{ note( 'Entering tests_mailimapclient_connect()' ) ; + my $imap ; - # ipv4 + # ipv4 ok( $imap = Mail::IMAPClient->new( ), 'mailimapclient_connect ipv4: new' ) ; is( 'Mail::IMAPClient', ref( $imap ), 'mailimapclient_connect ipv4: ref is Mail::IMAPClient' ) ; - SKIP: { - if ( 'macosx' eq hostname() - or 'macosx.polarhome.com' eq hostname() - ) { skip( 'Tests avoided on macosx get stuck', 1 ) ; } - is( undef, $imap->connect( ), 'mailimapclient_connect ipv4: connect with no server => failure' ) ; - } + # Mail::IMAPClient 3.40 die on this... So we skip it, thanks to "mature" IO::Socket::IP + # is( undef, $imap->connect( ), 'mailimapclient_connect ipv4: connect with no server => failure' ) ; + is( 'test.lamiral.info', $imap->Server( 'test.lamiral.info' ), 'mailimapclient_connect ipv4: setting Server(test.lamiral.info)' ) ; is( 1, $imap->Debug( 1 ), 'mailimapclient_connect ipv4: setting Debug( 1 )' ) ; is( 143, $imap->Port( 143 ), 'mailimapclient_connect ipv4: setting Port( 143 )' ) ; is( 3, $imap->Timeout( 3 ), 'mailimapclient_connect ipv4: setting Timout( 30 )' ) ; - like( ref( $imap->connect( ) ), qr/IO::Socket::INET/, 'mailimapclient_connect ipv4: connect to test.lamiral.info' ) ; + like( ref( $imap->connect( ) ), qr/IO::Socket::INET|IO::Socket::IP/, 'mailimapclient_connect ipv4: connect to test.lamiral.info' ) ; like( $imap->logout( ), qr/Mail::IMAPClient/, 'mailimapclient_connect ipv4: logout' ) ; is( undef, undef $imap, 'mailimapclient_connect ipv4: free variable' ) ; @@ -4070,14 +4948,23 @@ sub tests_mailimapclient_connect { like( ref( $imap->connect( ) ), qr/IO::Socket::SSL/, 'mailimapclient_connect ipv4 + ssl: connect to test.lamiral.info' ) ; is( $imap->logout( ), undef, 'mailimapclient_connect ipv4 + ssl: logout in ssl causes failure' ) ; is( undef, undef $imap, 'mailimapclient_connect ipv4 + ssl: free variable' ) ; - + # ipv6 + ssl ok( $imap = Mail::IMAPClient->new( ), 'mailimapclient_connect ipv6 + ssl: new' ) ; is( 'ks2ipv6.lamiral.info', $imap->Server( 'ks2ipv6.lamiral.info' ), 'mailimapclient_connect ipv6 + ssl: setting Server(ks2ipv6.lamiral.info)' ) ; ok( $imap->Ssl( [ SSL_verify_mode => SSL_VERIFY_NONE ] ), 'mailimapclient_connect ipv6 + ssl: setting Ssl( SSL_VERIFY_NONE )' ) ; is( 993, $imap->Port( 993 ), 'mailimapclient_connect ipv6 + ssl: setting Port( 993 )' ) ; SKIP: { - if ( 'CUILLERE' eq hostname() ) { skip( 'Tests avoided on CUILLERE can not do ipv6', 2 ) ; } + if ( + 'CUILLERE' eq hostname() + or + skip_macosx() + or + -e '/.dockerenv' + ) + { + skip( 'Tests avoided on CUILLERE can not do ipv6', 2 ) ; + } like( ref( $imap->connect( ) ), qr/IO::Socket::SSL/, 'mailimapclient_connect ipv6 + ssl: connect to ks2ipv6.lamiral.info' ) ; is( $imap->logout( ), undef, 'mailimapclient_connect ipv6 + ssl: logout in ssl causes failure' ) ; } @@ -4088,87 +4975,103 @@ sub tests_mailimapclient_connect { return ; } -sub tests_mailimapclient_connect_bug { + +sub tests_mailimapclient_connect_bug +{ note( 'Entering tests_mailimapclient_connect_bug()' ) ; + my $imap ; # ipv6 - ok( $imap = Mail::IMAPClient->new( ), 'mailimapclient_connect ipv6: new' ) ; - is( 'ks2ipv6.lamiral.info', $imap->Server( 'ks2ipv6.lamiral.info' ), 'mailimapclient_connect ipv6: setting Server(ks2ipv6.lamiral.info)' ) ; - is( 143, $imap->Port( 143 ), 'mailimapclient_connect ipv6: setting Port( 993 )' ) ; - + ok( $imap = Mail::IMAPClient->new( ), 'mailimapclient_connect_bug ipv6: new' ) ; + is( 'ks2ipv6.lamiral.info', $imap->Server( 'ks2ipv6.lamiral.info' ), 'mailimapclient_connect_bug ipv6: setting Server(ks2ipv6.lamiral.info)' ) ; + is( 143, $imap->Port( 143 ), 'mailimapclient_connect_bug ipv6: setting Port( 993 )' ) ; + SKIP: { - if ( 'CUILLERE' eq hostname() ) { skip( 'Tests avoided on CUILLERE can not do ipv6', 1 ) ; } - like( ref( $imap->connect( ) ), qr/IO::Socket::INET/, 'mailimapclient_connect ipv6: connect to ks2ipv6.lamiral.info' ) - or diag( 'mailimapclient_connect ipv6: ', $imap->LastError( ), $!, ) ; + if ( + 'CUILLERE' eq hostname() + or + skip_macosx() + or + -e '/.dockerenv' + ) + { + skip( 'Tests avoided on CUILLERE can not do ipv6', 1 ) ; + } + like( ref( $imap->connect( ) ), qr/IO::Socket::INET/, 'mailimapclient_connect_bug ipv6: connect to ks2ipv6.lamiral.info' ) + or diag( 'mailimapclient_connect_bug ipv6: ', $imap->LastError( ), $!, ) ; } - #is( $imap->logout( ), undef, 'mailimapclient_connect ipv6: logout in ssl causes failure' ) ; - is( undef, undef $imap, 'mailimapclient_connect ipv6: free variable' ) ; + #is( $imap->logout( ), undef, 'mailimapclient_connect_bug ipv6: logout in ssl causes failure' ) ; + is( undef, undef $imap, 'mailimapclient_connect_bug ipv6: free variable' ) ; note( 'Leaving tests_mailimapclient_connect_bug()' ) ; return ; } -sub mailimapclient_connect { - return ; -} - - - -sub tests_connect_socket { +sub tests_connect_socket +{ note( 'Entering tests_connect_socket()' ) ; - + is( undef, connect_socket( ), 'connect_socket: no args' ) ; my $socket ; my $imap ; SKIP: { - if ( 'CUILLERE' eq hostname() ) { skip( 'Tests avoided on CUILLERE cannot do ipv6', 2 ) ; } - - $socket = IO::Socket::INET6->new( - PeerAddr => 'ks2ipv6.lamiral.info', - PeerPort => 143, - ) ; + if ( + 'CUILLERE' eq hostname() + or + skip_macosx() + or + -e '/.dockerenv' + ) + { + skip( 'Tests avoided on CUILLERE/macosx.polarhome.com/docker cannot do ipv6', 2 ) ; + } - - ok( $imap = connect_socket( $socket ), 'connect_socket: ks2ipv6.lamiral.info port 143 IO::Socket::INET6' ) ; - #$imap->Debug( 1 ) ; - # myprint( $imap->capability( ) ) ; - if ( $imap ) { - $imap->logout( ) ; - } - - #$IO::Socket::SSL::DEBUG = 4 ; - $socket = IO::Socket::SSL->new( - PeerHost => 'ks2ipv6.lamiral.info', - PeerPort => 993, - SSL_verify_mode => SSL_VERIFY_NONE, - ) ; - # myprint $socket ; - ok( $imap = connect_socket( $socket ), 'connect_socket: ks2ipv6.lamiral.info port 993 IO::Socket::SSL' ) ; - #$imap->Debug( 1 ) ; - # myprint $imap->capability( ) ; - $socket->close( ) ; - if ( $imap ) { - $socket->close( ) ; - } - #$socket->close(SSL_no_shutdown => 1) ; - #$imap->logout( ) ; - #myprint "\n" ; - #$imap->logout( ) ; - } + $socket = IO::Socket::INET6->new( + PeerAddr => 'ks2ipv6.lamiral.info', + PeerPort => 143, + ) ; + + ok( $imap = connect_socket( $socket ), 'connect_socket: ks2ipv6.lamiral.info port 143 IO::Socket::INET6' ) ; + #$imap->Debug( 1 ) ; + # myprint( $imap->capability( ) ) ; + if ( $imap ) { + $imap->logout( ) ; + } + + #$IO::Socket::SSL::DEBUG = 4 ; + $socket = IO::Socket::SSL->new( + PeerHost => 'ks2ipv6.lamiral.info', + PeerPort => 993, + SSL_verify_mode => SSL_VERIFY_NONE, + ) ; + # myprint( $socket ) ; + ok( $imap = connect_socket( $socket ), 'connect_socket: ks2ipv6.lamiral.info port 993 IO::Socket::SSL' ) ; + #$imap->Debug( 1 ) ; + # myprint( $imap->capability( ) ) ; + # $socket->close( ) ; + if ( $imap ) { + $socket->close( ) ; + } + #$socket->close(SSL_no_shutdown => 1) ; + #$imap->logout( ) ; + #myprint( "\n" ) ; + #$imap->logout( ) ; + } note( 'Leaving tests_connect_socket()' ) ; return ; } -sub connect_socket { +sub connect_socket +{ my( $socket ) = @ARG ; if ( ! defined $socket ) { return ; } - + my $host = $socket->peerhost( ) ; my $port = $socket->peerport( ) ; #print "socket->peerhost: ", $socket->peerhost( ), "\n" ; @@ -4181,37 +5084,48 @@ sub connect_socket { } -sub tests_probe_imapssl { +sub tests_probe_imapssl +{ note( 'Entering tests_probe_imapssl()' ) ; is( undef, probe_imapssl( ), 'probe_imapssl: no args => undef' ) ; is( undef, probe_imapssl( 'unknown' ), 'probe_imapssl: unknown => undef' ) ; SKIP: { - if ( 'CUILLERE' eq hostname() ) { skip( 'Tests avoided on CUILLERE cannot do ipv6', 1 ) ; } + if ( + 'CUILLERE' eq hostname() + or + skip_macosx() + or + -e '/.dockerenv' + ) + { + skip( 'Tests avoided on CUILLERE/macosx.polarhome.com/docker cannot do ipv6', 2 ) ; + } like( probe_imapssl( 'ks2ipv6.lamiral.info' ), qr/^\* OK/, 'probe_imapssl: ks2ipv6.lamiral.info matches "* OK"' ) ; + like( probe_imapssl( 'imap.gmail.com' ), qr/^\* OK/, 'probe_imapssl: imap.gmail.com matches "* OK"' ) ; } ; like( probe_imapssl( 'test1.lamiral.info' ), qr/^\* OK/, 'probe_imapssl: test1.lamiral.info matches "* OK"' ) ; - like( probe_imapssl( 'imap.gmail.com' ), qr/^\* OK/, 'probe_imapssl: imap.gmail.com matches "* OK"' ) ; note( 'Leaving tests_probe_imapssl()' ) ; return ; } -sub probe_imapssl { +sub probe_imapssl +{ my $host = shift ; - + if ( ! $host ) { return ; } - - my $socket = IO::Socket::SSL->new( + + my $socket = IO::Socket::SSL->new( PeerHost => $host, PeerPort => $IMAP_SSL_PORT, SSL_verify_mode => SSL_VERIFY_NONE, ) ; #print "$socket\n" ; if ( ! $socket ) { return ; } - + my $banner ; $socket->sysread( $banner, 65_536 ) ; #print "$banner" ; @@ -4220,7 +5134,8 @@ sub probe_imapssl { } -sub connect_imap { +sub connect_imap +{ my( $host, $port, $mydebugimap, $ssl, $tls, $Side, $mytimeout, $h ) = @_ ; my $imap = Mail::IMAPClient->new( ) ; if ( $ssl ) { set_ssl( $imap, $h ) } @@ -4233,7 +5148,7 @@ sub connect_imap { myprint( "$Side: connecting on $side [$host] port [$port]\n" ) ; $imap->connect( ) - or die_clean( "$Side: Can not open imap connection on [$host]: " . $imap->LastError . " $OS_ERROR\n" ) ; + or exit_clean( $sync, $EXIT_CONNECTION_FAILURE, "$Side: Can not open imap connection on [$host]: " . $imap->LastError . " $OS_ERROR\n" ) ; myprint( "$Side IP address: ", $imap->Socket->peerhost(), "\n" ) ; my $banner = $imap->Results()->[0] ; @@ -4243,14 +5158,15 @@ sub connect_imap { if ( $tls ) { set_tls( $imap, $h ) ; $imap->starttls( ) - or die_clean("$Side: Can not go to tls encryption on $side [$host]:", $imap->LastError, "\n" ) ; + or exit_clean( $sync, $EXIT_TLS_FAILURE, "$Side: Can not go to tls encryption on $side [$host]:", $imap->LastError, "\n" ) ; myprint( "$Side: Socket successfuly converted to SSL\n" ) ; } return( $imap ) ; } -sub login_imap { +sub login_imap +{ my @allargs = @_ ; my( @@ -4265,7 +5181,7 @@ sub login_imap { my $imap = init_imap( @allargs ) ; $imap->connect() - or die_clean("$Side failure: can not open imap connection on $side [$host] with user [$user]: " . $imap->LastError . " $OS_ERROR\n" ) ; + or exit_clean( $mysync, $EXIT_CONNECTION_FAILURE, "$Side failure: can not open imap connection on $side [$host] with user [$user]: " . $imap->LastError . " $OS_ERROR\n" ) ; myprint( "$Side IP address: ", $imap->Socket->peerhost(), "\n" ) ; my $banner = $imap->Results()->[0] ; @@ -4282,14 +5198,14 @@ sub login_imap { $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]" ) ; + exit_clean( $mysync, $EXIT_AUTHENTICATION_FAILURE, "$Side failure: error login on $side [$host] with user [$user] auth [PREAUTH]" ) ; } } if ( $tls ) { set_tls( $imap, $h ) ; $imap->starttls( ) - or die_clean("$Side failure: Can not go to tls encryption on $side [$host]:", $imap->LastError, "\n" ) ; + or exit_clean( $mysync, $EXIT_TLS_FAILURE, "$Side failure: Can not go to tls encryption on $side [$host]:", $imap->LastError, "\n" ) ; myprint( "$Side: Socket successfuly converted to SSL\n" ) ; } @@ -4300,46 +5216,52 @@ sub login_imap { } -sub authenticate_imap { - - my($imap, +sub authenticate_imap +{ + my( $imap, $host, $port, $user, $domain, $password, $mydebugimap, $mytimeout, $fastio, $ssl, $tls, $authmech, $authuser, $reconnectretry, $proxyauth, $uid, $split, $Side, $h, $mysync ) = @_ ; check_capability( $imap, $authmech, $Side ) ; + $imap->User( $user ) ; + $imap->Domain( $domain ) if ( defined $domain ) ; + $imap->Authuser( $authuser ) ; + $imap->Password( $password ) ; + + if ( 'X-MASTERAUTH' eq $authmech ) + { + xmasterauth( $imap ) ; + return ; + } if ( $proxyauth ) { $imap->Authmechanism(q{}) ; - $imap->User($authuser) ; + $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( ) ) { + unless ( $authmech eq 'PREAUTH' or $authmech eq 'X-MASTERAUTH' 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 ) ; + exit_clean( $mysync, $EXIT_AUTHENTICATION_FAILURE, $error ) ; }else{ - myprint( $error ) ; + 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") ; + exit_clean( $mysync, $EXIT_AUTHENTICATION_FAILURE, "$info [LOGIN]: ", $imap->LastError, "\n") ; } if ( $proxyauth ) { @@ -4347,43 +5269,48 @@ sub authenticate_imap { 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" ) ; + exit_clean( $mysync, $EXIT_AUTHENTICATION_FAILURE, "$info: $einfo\n" ) ; } } return ; } -sub check_capability { +sub check_capability +{ my( $imap, $authmech, $Side ) = @_ ; - if ($imap->has_capability( "AUTH=$authmech" ) - or $imap->has_capability( $authmech ) ) { + 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) ; - return ; + $Side, $imap->Server, $authmech) ; + return ; } - if ( $authmech eq 'LOGIN' ) { - # Well, the warning is so common and useless that I prefer to remove it - # No more "... says it has NO CAPABILITY for AUTHENTICATE LOGIN" - return ; - } + if ( $authmech eq 'LOGIN' ) + { + # Well, the warning is so common and useless that I prefer to remove it + # No more "... says it has NO CAPABILITY for AUTHENTICATE LOGIN" + return ; + } - myprintf( "%s: %s says it has NO CAPABILITY for AUTHENTICATE %s\n", - $Side, $imap->Server, $authmech ) ; + 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" ) ; + if ( $authmech eq 'PLAIN' ) + { + myprint( "$Side: frequently PLAIN is only supported with SSL, try --ssl or --tls options\n" ) ; } return ; } -sub set_ssl { +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) @@ -4412,7 +5339,8 @@ sub set_ssl { return ; } -sub set_tls { +sub set_tls +{ my ( $imap, $h ) = @_ ; my $sslargs_hash = $h->{sslargs} ; @@ -4440,7 +5368,8 @@ sub set_tls { -sub init_imap { +sub init_imap +{ my( $host, $port, $user, $domain, $password, $mydebugimap, $mytimeout, $fastio, @@ -4478,7 +5407,8 @@ sub init_imap { } -sub plainauth { +sub plainauth +{ my $code = shift; my $imap = shift; @@ -4514,7 +5444,8 @@ sub plainauth { # # 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 { +sub xoauth2 +{ require JSON::WebToken ; require LWP::UserAgent ; require HTML::Entities ; @@ -4532,15 +5463,15 @@ sub xoauth2 { if( $imap->Password =~ /^(.*\.json)$/x ) { my $json = JSON->new( ) ; my $filename = $1; - $debug and myprint( "XOAUTH2 json file: $filename\n" ) ; - open( my $FILE, '<', $filename ) or die_clean( "error [$filename]: $OS_ERROR " ) ; + $sync->{ debug } and myprint( "XOAUTH2 json file: $filename\n" ) ; + open( my $FILE, '<', $filename ) or exit_clean( $sync, $EXIT_AUTHENTICATION_FAILURE, "error [$filename]: $OS_ERROR " ) ; 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"); + $sync->{ debug } and myprint( "Service account: $iss\n"); + $sync->{ debug } and myprint( "Private key:\n$key\n"); } else { # Get iss (service account address), keyfile name, and keypassword if necessary @@ -4549,12 +5480,12 @@ sub xoauth2 { # 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"); + $sync->{ 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"); + $sync->{ debug } and myprint( "Private key:\n$key\n"); } # Create jwt of oauth2 request @@ -4578,9 +5509,9 @@ sub xoauth2 { assertion => $jwt } ) ; unless( $response->is_success( ) ) { - die_clean( $response->code, "\n", $response->content, "\n" ) ; + exit_clean( $sync, $EXIT_AUTHENTICATION_FAILURE, $response->code, "\n", $response->content, "\n" ) ; }else{ - $debug and myprint( $response->content ) ; + $sync->{ debug } and myprint( $response->content ) ; } # access_token in response is what we need @@ -4589,7 +5520,7 @@ sub xoauth2 { # 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"); + $sync->{ debug } and myprint( "XOAUTH2 String: $xoauth2_string\n"); return($xoauth2_string); } @@ -4597,7 +5528,8 @@ sub xoauth2 { # xoauth() thanks to Eduardo Bortoluzzi Junior -sub xoauth { +sub xoauth +{ require URI::Escape ; require Data::Uniqid ; @@ -4612,7 +5544,7 @@ sub xoauth { # 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" ) ; + $sync->{ debug } and myprint( "XOAUTH: consumer key: $user_parts[1]\n" ) ; # All the parameters needed to be signed on the XOAUTH my %hash = (); @@ -4637,7 +5569,7 @@ sub xoauth { } $base .= URI::Escape::uri_escape($baseparms); - $debug and myprint( "XOAUTH: base request to sign: $base\n" ) ; + $sync->{ 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{&} ) ; @@ -4662,31 +5594,91 @@ sub xoauth { $string .= $baseparms; - $debug and myprint( "XOAUTH: authentication string: $string\n" ) ; + $sync->{ debug } and myprint( "XOAUTH: authentication string: $string\n" ) ; # It must be base64 encoded return encode_base64("$string", q{}); } +sub xmasterauth +{ + # This is Kerio auth admin + # This code comes from + # https://github.com/imapsync/imapsync/pull/53/files -sub banner_imapsync { + my $imap = shift ; - my @argv = @_ ; + my $user = $imap->User( ) ; + my $password = $imap->Password( ) ; + my $authmech = 'X-MASTERAUTH' ; + + my @challenge = $imap->tag_and_run( $authmech, "+" ) ; + if ( not defined $challenge[0] ) + { + exit_clean( $sync, $EXIT_AUTHENTICATION_FAILURE, "Failure authenticate with $authmech: ", $imap->LastError, "\n") ; + return ; # hahaha! + } + $sync->{ debug } and myprint( "X-MASTERAUTH challenge: [@challenge]\n" ) ; + + $challenge[1] =~ s/^\+ |^\s+|\s+$//g ; + $imap->_imap_command( { addcrlf => 1, addtag => 0, tag => $imap->Count }, md5_hex( $challenge[1] . $password ) ) + or exit_clean( $sync, $EXIT_AUTHENTICATION_FAILURE, "Failure authenticate with $authmech: ", $imap->LastError, "\n") ; + + $imap->tag_and_run( 'X-SETUSER ' . $user ) + or exit_clean( $sync, $EXIT_AUTHENTICATION_FAILURE, "Failure authenticate with $authmech: ", "X-SETUSER ", $imap->LastError, "\n") ; + + $imap->State( Mail::IMAPClient::Authenticated ) ; + # I comment this state because "Selected" state is usually done by SELECT or EXAMINE imap commands + # $imap->State( Mail::IMAPClient::Selected ) ; + + return ; +} + + +sub tests_do_valid_directory +{ + note( 'Entering tests_do_valid_directory()' ) ; + + Readonly my $NB_UNIX_tests_do_valid_directory => 2 ; + SKIP: { + skip( 'Tests only for Unix', $NB_UNIX_tests_do_valid_directory ) if ( 'MSWin32' eq $OSNAME ) ; + ok( 1 == do_valid_directory( '.'), 'do_valid_directory: . good' ) ; + ok( 1 == do_valid_directory( './W/tmp/tests/valid/sub'), 'do_valid_directory: ./W/tmp/tests/valid/sub good' ) ; + } + Readonly my $NB_UNIX_tests_do_valid_directory_non_root => 2 ; + SKIP: { + skip( 'Tests only for Unix', $NB_UNIX_tests_do_valid_directory_non_root ) if ( 'MSWin32' eq $OSNAME or '0' eq $EFFECTIVE_USER_ID ) ; + diag( 'Error / not writable is on purpose' ) ; + ok( 0 == do_valid_directory( '/'), 'do_valid_directory: / bad' ) ; + diag( 'Error permission denied on /noway is on purpose' ) ; + ok( 0 == do_valid_directory( '/noway'), 'do_valid_directory: /noway bad' ) ; + } + + + note( 'Leaving tests_do_valid_directory()' ) ; + return ; +} + +sub banner_imapsync +{ + my $mysync = shift @ARG ; + my @argv = @ARG ; my $banner_imapsync = join q{}, q{$RCSfile: imapsync,v $ }, - q{$Revision: 1.882 $ }, - q{$Date: 2018/05/05 21:10:43 $ }, - "\n", - "Command line used:\n", - "$PROGRAM_NAME ", command_line_nopassword( @argv ), "\n" ; + q{$Revision: 1.937 $ }, + q{$Date: 2019/05/01 22:14:00 $ }, + "\n", + "Command line used, run by $EXECUTABLE_NAME:\n", + "$PROGRAM_NAME ", command_line_nopassword( $mysync, @argv ), "\n" ; return( $banner_imapsync ) ; } -sub do_valid_directory { - my $dir = shift; +sub do_valid_directory +{ + my $dir = shift @ARG ; # all good => return ok. return( 1 ) if ( -d $dir and -r _ and -w _ ) ; @@ -4711,31 +5703,11 @@ sub do_valid_directory { return( 0 ) ; } -sub tests_do_valid_directory { - note( 'Entering tests_do_valid_directory()' ) ; - Readonly my $NB_UNIX_tests_do_valid_directory => 2 ; - SKIP: { - skip( 'Tests only for Unix', $NB_UNIX_tests_do_valid_directory ) if ( 'MSWin32' eq $OSNAME ) ; - ok( 1 == do_valid_directory( '.'), 'do_valid_directory: . good' ) ; - ok( 1 == do_valid_directory( './W/tmp/tests/valid/sub'), 'do_valid_directory: ./W/tmp/tests/valid/sub good' ) ; - } - Readonly my $NB_UNIX_tests_do_valid_directory_non_root => 2 ; - SKIP: { - skip( 'Tests only for Unix', $NB_UNIX_tests_do_valid_directory_non_root ) if ( 'MSWin32' eq $OSNAME or '0' eq $EFFECTIVE_USER_ID ) ; - diag( 'Error / not writable is on purpose' ) ; - ok( 0 == do_valid_directory( '/'), 'do_valid_directory: / bad' ) ; - diag( 'Error permission denied on /noway is on purpose' ) ; - ok( 0 == do_valid_directory( '/noway'), 'do_valid_directory: /noway bad' ) ; - } - - - note( 'Leaving tests_do_valid_directory()' ) ; - return ; -} +sub tests_match_a_pid_number +{ + note( 'Entering tests_match_a_pid_number()' ) ; - -sub tests_match_a_pid_number { is( undef, match_a_pid_number( ), 'match_a_pid_number: no args => undef' ) ; is( undef, match_a_pid_number( '' ), 'match_a_pid_number: "" => undef' ) ; is( undef, match_a_pid_number( 'lalala' ), 'match_a_pid_number: lalala => undef' ) ; @@ -4743,23 +5715,31 @@ sub tests_match_a_pid_number { is( 1, match_a_pid_number( 123 ), 'match_a_pid_number: 123 => 1' ) ; is( 1, match_a_pid_number( '123' ), 'match_a_pid_number: "123" => 1' ) ; is( undef, match_a_pid_number( 'a123' ), 'match_a_pid_number: a123 => undef' ) ; - is( 1, match_a_pid_number( 65535 ), 'match_a_pid_number: 65535 => 1' ) ; + is( 1, match_a_pid_number( 99999 ), 'match_a_pid_number: 99999 => 1' ) ; is( undef, match_a_pid_number( 0 ), 'match_a_pid_number: 0 => undef' ) ; - is( undef, match_a_pid_number( 65536 ), 'match_a_pid_number: 65536 => undef' ) ; - is( undef, match_a_pid_number( 99999 ), 'match_a_pid_number: 99999 => undef' ) ; + is( undef, match_a_pid_number( 100000 ), 'match_a_pid_number: 100000 => undef' ) ; + is( undef, match_a_pid_number( 123456 ), 'match_a_pid_number: 123456 => undef' ) ; + + note( 'Leaving tests_match_a_pid_number()' ) ; return ; } -sub match_a_pid_number { - my $pid = shift ; +sub match_a_pid_number +{ + my $pid = shift @ARG ; if ( ! $pid ) { return ; } if ( ! match( $pid, '^\d+$' ) ) { return ; } if ( 0 > $pid ) { return ; } - if ( 65535 < $pid ) { return ; } + #if ( 65535 < $pid ) { return ; } + if ( 99999 < $pid ) { return ; } return 1 ; } -sub tests_remove_pidfile_not_running { +sub tests_remove_pidfile_not_running +{ + note( 'Entering tests_remove_pidfile_not_running()' ) ; + + ok( (-d 'W/tmp/tests/' or mkpath( 'W/tmp/tests/' ) ), 'remove_pidfile_not_running: mkpath W/tmp/tests/' ) ; is( undef, remove_pidfile_not_running( ), 'remove_pidfile_not_running: no args => undef' ) ; is( undef, remove_pidfile_not_running( './W' ), 'remove_pidfile_not_running: a dir => undef' ) ; is( undef, remove_pidfile_not_running( 'noexists' ), 'remove_pidfile_not_running: noexists => undef' ) ; @@ -4771,24 +5751,26 @@ sub tests_remove_pidfile_not_running { is( 1, remove_pidfile_not_running( 'W/tmp/tests/notrunning.pid' ), 'remove_pidfile_not_running: W/tmp/tests/notrunning.pid => 1' ) ; is( $PROCESS_ID, string_to_file( $PROCESS_ID, 'W/tmp/tests/running.pid' ), 'remove_pidfile_not_running: prepa W/tmp/tests/running.pid' ) ; is( undef, remove_pidfile_not_running( 'W/tmp/tests/running.pid' ), 'remove_pidfile_not_running: W/tmp/tests/running.pid => undef' ) ; - + + note( 'Leaving tests_remove_pidfile_not_running()' ) ; return ; } -sub remove_pidfile_not_running { - # - my $pid_filename = shift ; - - if ( ! $pid_filename ) { return } ; - if ( ! -e $pid_filename ) { return } ; - if ( ! -f $pid_filename ) { return } ; - +sub remove_pidfile_not_running +{ + # + my $pid_filename = shift @ARG ; + + if ( ! $pid_filename ) { myprint( "No variable pid_filename\n" ) ; return } ; + if ( ! -e $pid_filename ) { myprint( "File $pid_filename does not exist\n" ) ; return } ; + if ( ! -f $pid_filename ) { myprint( "File $pid_filename is not a file\n" ) ; return } ; + my $pid = firstline( $pid_filename ) ; - if ( ! match_a_pid_number( $pid ) ) { return } ; + if ( ! match_a_pid_number( $pid ) ) { myprint( "pid $pid in $pid_filename is not a number\n" ) ; return } ; # can't kill myself => do nothing - if ( ! kill 'ZERO', $PROCESS_ID ) { return } ; - - # can't kill the pid => it is gone or own by another user => remove pidfile + if ( ! kill 'ZERO', $PROCESS_ID ) { myprint( "Can not kill ZERO myself $PROCESS_ID\n" ) ; return } ; + + # can't kill ZERO the pid => it is gone or own by another user => remove pidfile if ( ! kill 'ZERO', $pid ) { myprint( "Removing old $pid_filename since its PID $pid is not running anymore (oo-killed?)\n" ) ; if ( unlink $pid_filename ) { @@ -4803,110 +5785,234 @@ sub remove_pidfile_not_running { return ; } -sub tests_write_pidfile { + +sub tests_tail +{ + note( 'Entering tests_tail()' ) ; + + ok( (-d 'W/tmp/tests/' or mkpath( 'W/tmp/tests/' ) ), 'tail: mkpath W/tmp/tests/' ) ; + ok( ( ! -e 'W/tmp/tests/tail.pid' || unlink 'W/tmp/tests/tail.pid' ), 'tail: unlink W/tmp/tests/tail.pid' ) ; + ok( ( ! -e 'W/tmp/tests/tail.txt' || unlink 'W/tmp/tests/tail.txt' ), 'tail: unlink W/tmp/tests/tail.txt' ) ; + + is( undef, tail( ), 'tail: no args => undef' ) ; my $mysync ; - - is( 1, write_pidfile( ), 'write_pidfile: no args => 1' ) ; - - $mysync->{pidfile} = '/no/no/no.pid' ; - is( 1, write_pidfile( $mysync ), 'write_pidfile: no permission for /no/no/no.pid, no lock => 1' ) ; - $mysync->{pidfilelocking} = 1 ; - is( undef, write_pidfile( $mysync ), 'write_pidfile: no permission for /no/no/no.pid + lock => undef' ) ; - - $mysync->{pidfile} = 'W/tmp/tests/test.pid' ; - ok( (-d 'W/tmp/tests/' or mkpath( 'W/tmp/tests/' ) ), 'write_pidfile: mkpath W/tmp/tests/' ) ; - is( 1, touch( $mysync->{pidfile} ), 'write_pidfile: lock prepa' ) ; - - $mysync->{pidfilelocking} = 0 ; - is( 1, write_pidfile( $mysync ), 'write_pidfile: W/tmp/tests/test.pid => 1' ) ; - is( $PROCESS_ID, firstline( 'W/tmp/tests/test.pid' ), "write_pidfile: W/tmp/tests/test.pid contains $PROCESS_ID" ) ; + is( undef, tail( $mysync ), 'tail: no pidfile => undef' ) ; + + $mysync->{pidfile} = 'W/tmp/tests/tail.pid' ; + is( undef, tail( $mysync ), 'tail: no pidfilelocking => undef' ) ; $mysync->{pidfilelocking} = 1 ; - is( undef, write_pidfile( $mysync ), 'write_pidfile: W/tmp/tests/test.pid + lock => undef' ) ; - + is( undef, tail( $mysync ), 'tail: pidfile no exists => undef' ) ; + + + my $pidandlog = "33333\nW/tmp/tests/tail.txt\n" ; + is( $pidandlog, string_to_file( $pidandlog, $mysync->{pidfile} ), 'tail: put pid 33333 and tail.txt in pidfile' ) ; + is( undef, tail( $mysync ), 'tail: logfile to tail no exists => undef' ) ; + + my $tailcontent = "L1\nL2\nL3\nL4\nL5\n" ; + is( $tailcontent, string_to_file( $tailcontent, 'W/tmp/tests/tail.txt' ), + 'tail: put L1\nL2\nL3\nL4\nL5\n in W/tmp/tests/tail.txt' ) ; + + is( undef, tail( $mysync ), 'tail: fake pid in pidfile + tail off => 1' ) ; + + $mysync->{ tail } = 1 ; + is( 1, tail( $mysync ), 'tail: fake pid in pidfile + tail on=> 1' ) ; + + # put my own pid, won't do tail + $pidandlog = "$PROCESS_ID\nW/tmp/tests/tail.txt\n" ; + is( $pidandlog, string_to_file( $pidandlog, $mysync->{pidfile} ), 'tail: put my own PID in pidfile' ) ; + is( undef, tail( $mysync ), 'tail: my own pid in pidfile => undef' ) ; + + note( 'Leaving tests_tail()' ) ; return ; } -sub write_pidfile { + + +sub tail +{ + # return undef on failures + # return 1 on success + + my $mysync = shift ; + + # no tail when aborting! + if ( $mysync->{ abort } ) { return ; } + + my $pidfile = $mysync->{pidfile} ; + my $lock = $mysync->{pidfilelocking} ; + my $tail = $mysync->{tail} ; + + if ( ! $pidfile ) { return ; } + if ( ! $lock ) { return ; } + if ( ! $tail ) { return ; } + + my $pidtotail = firstline( $pidfile ) ; + if ( ! $pidtotail ) { return ; } + + + + # It should not happen but who knows... + if ( $pidtotail eq $PROCESS_ID ) { return ; } + + + my $filetotail = secondline( $pidfile ) ; + if ( ! $filetotail ) { return ; } + + if ( ! -r $filetotail ) + { + #myprint( "Error: can not read $filetotail\n" ) ; + return ; + } + + myprint( "Doing a tail -f on $filetotail for processus pid $pidtotail until it is finished.\n" ) ; + my $file = File::Tail->new( + name => $filetotail, + nowait => 1, + interval => 1, + tail => 1, + adjustafter => 2 + ); + + my $moretimes = 200 ; + # print one line at least + my $line = $file->read ; + myprint( $line ) ; + while ( isrunning( $pidtotail, \$moretimes ) and defined( $line = $file->read ) ) + { + myprint( $line ); + sleep( 0.02 ) ; + } + + return 1 ; +} + +sub isrunning +{ + my $pidtocheck = shift ; + my $moretimes_ref = shift ; + + if ( kill 'ZERO', $pidtocheck ) + { + #myprint( "$pidtocheck running\n" ) ; + return 1 ; + } + elsif ( $$moretimes_ref >= 0 ) + { + # continue to consider it running + $$moretimes_ref-- ; + return 1 ; + } + else + { + myprint( "Tailed processus $pidtocheck ended\n" ) ; + return ; + } +} + +sub tests_write_pidfile +{ + note( 'Entering tests_write_pidfile()' ) ; + + my $mysync ; + + is( 1, write_pidfile( ), 'write_pidfile: no args => 1' ) ; + + # no pidfile => ok + $mysync->{pidfile} = q{} ; + is( 1, write_pidfile( $mysync ), 'write_pidfile: no pidfile => undef' ) ; + + # The pidfile path is bad => failure + $mysync->{pidfile} = '/no/no/no.pid' ; + is( undef, write_pidfile( $mysync ), 'write_pidfile: no permission for /no/no/no.pid, no lock => undef' ) ; + + $mysync->{pidfilelocking} = 1 ; + is( undef, write_pidfile( $mysync ), 'write_pidfile: no permission for /no/no/no.pid + lock => undef' ) ; + + $mysync->{pidfile} = 'W/tmp/tests/test.pid' ; + ok( (-d 'W/tmp/tests/' or mkpath( 'W/tmp/tests/' ) ), 'write_pidfile: mkpath W/tmp/tests/' ) ; + is( 1, touch( $mysync->{pidfile} ), 'write_pidfile: lock prepa' ) ; + + $mysync->{pidfilelocking} = 0 ; + is( 1, write_pidfile( $mysync ), 'write_pidfile: W/tmp/tests/test.pid + no lock => 1' ) ; + is( $PROCESS_ID, firstline( 'W/tmp/tests/test.pid' ), "write_pidfile: W/tmp/tests/test.pid contains $PROCESS_ID" ) ; + is( q{}, secondline( 'W/tmp/tests/test.pid' ), "write_pidfile: W/tmp/tests/test.pid contains no second line" ) ; + + $mysync->{pidfilelocking} = 1 ; + is( undef, write_pidfile( $mysync ), 'write_pidfile: W/tmp/tests/test.pid + lock => undef' ) ; + + + $mysync->{pidfilelocking} = 0 ; + $mysync->{ logfile } = 'rrrr.txt' ; + is( 1, write_pidfile( $mysync ), 'write_pidfile: W/tmp/tests/test.pid + no lock + logfile => 1' ) ; + is( $PROCESS_ID, firstline( 'W/tmp/tests/test.pid' ), "write_pidfile: + no lock + logfile W/tmp/tests/test.pid contains $PROCESS_ID" ) ; + is( q{rrrr.txt}, secondline( 'W/tmp/tests/test.pid' ), "write_pidfile: + no lock + logfile W/tmp/tests/test.pid contains rrrr.txt" ) ; + + + note( 'Leaving tests_write_pidfile()' ) ; + return ; +} + + + +sub write_pidfile +{ # returns undef if something is considered fatal # returns 1 otherwise - - if ( ! @ARG ) { return 1 ; } - - my $mysync = shift ; - - # Do not write the pid file if this process goal is to abort the process designed by the pid file - if ( $mysync->{abort} ) { return 1 ; } - - # - my $pid_filename = $mysync->{pidfile} ; - my $lock = $mysync->{pidfilelocking} ; - myprint( "PID file is $pid_filename ( to change it use --pidfile filepath ; to avoid it use --pidfile \"\" )\n" ) ; + if ( ! @ARG ) { return 1 ; } + + my $mysync = shift @ARG ; + + # Do not write the pid file if this process goal is to abort the process designed by the pid file + if ( $mysync->{abort} ) { return 1 ; } + + # + my $pid_filename = $mysync->{ pidfile } ; + my $lock = $mysync->{ pidfilelocking } ; + + if ( ! $pid_filename ) + { + myprint( "PID file is unset ( to set it, use --pidfile filepath ; to avoid it use --pidfile \"\" )\n" ) ; + return( 1 ) ; + } + + 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" ) ; return ; - + } + if ( -e $pid_filename ) { myprint( "$pid_filename already exists, overwriting it ( use --pidfilelocking to avoid concurrent runs )\n" ) ; } + my $pid_string = "$PROCESS_ID\n" ; + my $pid_message = "Writing my PID $PROCESS_ID in $pid_filename\n" ; + + if ( $mysync->{ logfile } ) + { + $pid_string .= "$mysync->{ logfile }\n" ; + $pid_message .= "Writing also my logfile name in $pid_filename : $mysync->{ logfile }\n" ; + } + if ( open my $FILE_HANDLE, '>', $pid_filename ) { - myprint( "Writing my PID $PROCESS_ID in $pid_filename\n" ) ; - print $FILE_HANDLE $PROCESS_ID ; + myprint( $pid_message ) ; + print $FILE_HANDLE $pid_string ; close $FILE_HANDLE ; return( 1 ) ; - } else { - myprint( "Could not open $pid_filename for writing. Check permissions or disk space.\n" ) ; - if ( $lock ) { - return ; - }else{ - return( 1 ) ; - } + } + else + { + myprint( "Could not open $pid_filename for writing. Check permissions or disk space: $OS_ERROR\n" ) ; + return ; } } - -sub remove_tmp_files { - my $mysync = shift or return ; - $mysync->{pidfile} or return ; - if ( -e $mysync->{pidfile} ) { - 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 ) ; - myprint( @messages ) ; - exit 255 ; -} - -sub missing_option { - my ( $option ) = @_ ; - die_clean( "$option option is mandatory, for help run $PROGRAM_NAME --help\n" ) ; - return ; -} - - -sub fix_Inbox_INBOX_mapping { +sub fix_Inbox_INBOX_mapping +{ my( $h1_all, $h2_all ) = @_ ; my $regex = q{} ; @@ -4919,7 +6025,8 @@ sub fix_Inbox_INBOX_mapping { return( $regex ) ; } -sub tests_fix_Inbox_INBOX_mapping { +sub tests_fix_Inbox_INBOX_mapping +{ note( 'Entering tests_fix_Inbox_INBOX_mapping()' ) ; @@ -4954,7 +6061,8 @@ sub tests_fix_Inbox_INBOX_mapping { } -sub jux_utf8_list { +sub jux_utf8_list +{ my @s_inp = @_ ; my $s_out = q{} ; foreach my $s ( @s_inp ) { @@ -4963,19 +6071,21 @@ sub jux_utf8_list { return( $s_out ) ; } -sub tests_jux_utf8_list { +sub tests_jux_utf8_list +{ note( 'Entering 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-' ) ; + ok( "[&ANY-] = [Ö]\n" eq jux_utf8_list( '&ANY-' ), 'jux_utf8_list: &ANY-' ) ; note( 'Leaving tests_jux_utf8_list()' ) ; return( 0 ) ; } -sub jux_utf8 { +sub jux_utf8 +{ # juxtapose utf8 at the right if different my ( $s_utf7 ) = shift ; my ( $s_utf8 ) = imap_utf7_decode( $s_utf7 ) ; @@ -4990,15 +6100,16 @@ sub jux_utf8 { } # editing utf8 can be tricky without an utf8 editor -sub tests_jux_utf8 { +sub tests_jux_utf8 +{ note( 'Entering 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( '[&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-] = [Проект]' ) ; + ok( '[+BD8EQAQ1BDQEOwQ+BDM-] = [предлог]' eq jux_utf8( '+BD8EQAQ1BDQEOwQ+BDM-' ), 'jux_utf8: => [+BD8EQAQ1BDQEOwQ+BDM-] = [предлог]' ) ; + ok( '[&BB8EQAQ+BDUEOgRC-] = [Проект]' eq jux_utf8( '&BB8EQAQ+BDUEOgRC-' ), 'jux_utf8: => [&BB8EQAQ+BDUEOgRC-] = [Проект]' ) ; note( 'Leaving tests_jux_utf8()' ) ; return ; @@ -5007,7 +6118,8 @@ sub tests_jux_utf8 { # 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 { +sub imap_utf7_decode +{ my ( $s ) = shift ; # Algorithm @@ -5020,7 +6132,8 @@ sub imap_utf7_decode { return( Unicode::String::utf7( $s )->utf8 ) ; } -sub imap_utf7_encode { +sub imap_utf7_encode +{ my ( $s ) = @_ ; $s = Unicode::String::utf8( $s )->utf7 ; @@ -5034,13 +6147,14 @@ sub imap_utf7_encode { -sub select_folder { - my ( $imap, $folder, $hostside ) = @_ ; +sub select_folder +{ + my ( $mysync, $imap, $folder, $hostside ) = @_ ; if ( ! $imap->select( $folder ) ) { my $error = join q{}, "$hostside folder $folder: Could not select: ", $imap->LastError, "\n" ; - errors_incr( $sync, $error ) ; + errors_incr( $mysync, $error ) ; return( 0 ) ; }else{ # ok select succeeded @@ -5048,13 +6162,14 @@ sub select_folder { } } -sub examine_folder { - my ( $imap, $folder, $hostside ) = @_ ; +sub examine_folder +{ + my ( $mysync, $imap, $folder, $hostside ) = @_ ; if ( ! $imap->examine( $folder ) ) { my $error = join q{}, "$hostside folder $folder: Could not examine: ", $imap->LastError, "\n" ; - errors_incr( $sync, $error ) ; + errors_incr( $mysync, $error ) ; return( 0 ) ; }else{ # ok select succeeded @@ -5063,10 +6178,9 @@ sub examine_folder { } - - -sub count_from_select { - my @lines = @_ ; +sub count_from_select +{ + my @lines = @ARG ; my $count ; foreach my $line ( @lines ) { #myprint( "line = [$line]\n" ) ; @@ -5080,23 +6194,10 @@ sub count_from_select { - - - - - - - - - - - - - - - -sub create_folder_old { - my( $imap, $h2_fold, $h1_fold ) = @_ ; +sub create_folder_old +{ + my $mysync = shift @ARG ; + my( $imap, $h2_fold, $h1_fold ) = @ARG ; myprint( "Creating (old way) folder [$h2_fold] on host2\n" ) ; if ( ( 'INBOX' eq uc $h2_fold ) @@ -5104,12 +6205,12 @@ sub create_folder_old { myprint( "Folder [$h2_fold] already exists\n" ) ; return( 1 ) ; } - if ( ! $sync->{dry} ){ + if ( ! $mysync->{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 ) ; + errors_incr( $mysync, $error ) ; # success if folder exists ("already exists" error) return( 1 ) if $imap->exists( $h2_fold ) ; # failure since create failed @@ -5121,58 +6222,60 @@ sub create_folder_old { } }else{ # dry mode, no folder so many imap will fail, assuming failure - myprint( "Created ( the old way ) folder [$h2_fold] on host2 $sync->{dry_message}\n" ) ; + myprint( "Created ( the old way ) folder [$h2_fold] on host2 $mysync->{dry_message}\n" ) ; return( 0 ) ; } } -sub create_folder { - my( $imap2 , $h2_fold , $h1_fold ) = @_ ; +sub create_folder +{ + my $mysync = shift @ARG ; + my( $myimap2 , $h2_fold , $h1_fold ) = @ARG ; my( @parts , $parent ) ; - if ( $imap2->IsUnconnected( ) ) { + if ( $myimap2->IsUnconnected( ) ) { myprint( "Host2: Unconnected state\n" ) ; return( 0 ) ; } if ( $create_folder_old ) { - return( create_folder_old( $imap2 , $h2_fold , $h1_fold ) ) ; + return( create_folder_old( $mysync, $myimap2 , $h2_fold , $h1_fold ) ) ; } myprint( "Creating folder [$h2_fold] on host2\n" ) ; if ( ( 'INBOX' eq uc $h2_fold ) - and ( $imap2->exists( $h2_fold ) ) ) { + and ( $myimap2->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" ) ; + if ( $mixfolders and $myimap2->exists( $h2_fold ) ) { + myprint( "Folder [$h2_fold] already exists (--nomixfolders is not set)\n" ) ; return( 1 ) ; } - if ( ( not $mixfolders ) and ( $imap2->exists( $h2_fold ) ) ) { + if ( ( not $mixfolders ) and ( $myimap2->exists( $h2_fold ) ) ) { myprint( "Folder [$h2_fold] already exists and --nomixfolders is set\n" ) ; return( 0 ) ; } - @parts = split /\Q$h2_sep\E/x, $h2_fold ; + @parts = split /\Q$mysync->{ h2_sep }\E/x, $h2_fold ; pop @parts ; - $parent = join $h2_sep, @parts ; + $parent = join $mysync->{ h2_sep }, @parts ; $parent =~ s/^\s+|\s+$//xg ; - if ( ( $parent ne q{} ) and ( ! $imap2->exists( $parent ) ) ) { - create_folder( $imap2 , $parent , $h1_fold ) ; + if ( ( $parent ne q{} ) and ( ! $myimap2->exists( $parent ) ) ) { + create_folder( $mysync, $myimap2 , $parent , $h1_fold ) ; } - if ( ! $sync->{dry} ) { - if ( ! $imap2->create( $h2_fold ) ) { + if ( ! $mysync->{dry} ) { + if ( ! $myimap2->create( $h2_fold ) ) { my $error = join q{}, "Could not create folder [$h2_fold] from [$h1_fold]: " , - $imap2->LastError( ), "\n" ; - errors_incr( $sync, $error ) ; + $myimap2->LastError( ), "\n" ; + errors_incr( $mysync, $error ) ; # success if folder exists ("already exists" error) - return( 1 ) if $imap2->exists( $h2_fold ) ; + return( 1 ) if $myimap2->exists( $h2_fold ) ; # failure since create failed return( 0 ) ; }else{ @@ -5182,8 +6285,8 @@ sub create_folder { } }else{ # dry mode, no folder so many imap will fail, assuming failure - myprint( "Created folder [$h2_fold] on host2 $sync->{dry_message}\n" ) ; - if ( ! $justfolders ) { + myprint( "Created folder [$h2_fold] on host2 $mysync->{dry_message}\n" ) ; + if ( ! $mysync->{ 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" ) ; } @@ -5193,14 +6296,16 @@ sub create_folder { -sub tests_folder_routines { +sub tests_folder_routines +{ note( 'Entering 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' ); + + is_deeply( [ 'folder_foo' ], [ remove_from_requested_folders( 'folder_foo' ) ], 'removed folder_foo => 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" ); @@ -5208,53 +6313,69 @@ sub tests_folder_routines { 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' ) ; + + is_deeply( [ 'folder_bar' ], [ 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' ) ; + ok( add_to_requested_folders( 'A_99', 'M_55', 'Z_11' ), 'add_to_requested_folders M_55 Z_11' ); + ok( 0 == compare_lists( [ sort_requested_folders( ) ], [ 'A_99', 'M_55', 'Z_11' ] ), 'sort_requested_folders: middle' ) ; + + @folderfirst = ( 'Z_11' ) ; - ok( 0 == compare_lists( [ sort_requested_folders( ) ], [ 'Z_11', 'M_55' ] ), 'sort_requested_folders: first+middle' ) ; + + ok( 0 == compare_lists( [ sort_requested_folders( ) ], [ 'Z_11', 'A_99', 'M_55' ] ), 'sort_requested_folders: first+middle' ) ; + + is_deeply( [ 'Z_11', 'A_99', 'M_55' ], [ sort_requested_folders( ) ], 'sort_requested_folders: first+middle is_deeply' ) ; + @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' ) ; + 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' ) ; + + + ok( add_to_requested_folders('A_88', 'Z_22',), 'add_to_requested_folders A_88 Z_22' ) ; @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' ) ; + undef @folderfirst ; + undef @folderlast ; note( 'Leaving tests_folder_routines()' ) ; return ; } -sub sort_requested_folders { +sub sort_requested_folders +{ my @requested_folders_sorted = () ; - foreach my $folder ( @folderfirst ) { - remove_from_requested_folders( $folder ) ; - } + #myprint "folderfirst: @folderfirst\n" ; + my @folderfirst_requested = remove_from_requested_folders( @folderfirst ) ; + #myprint "folderfirst_requested: @folderfirst_requested\n" ; - foreach my $folder ( @folderlast ) { - remove_from_requested_folders( $folder ) ; - } + my @folderlast_requested = remove_from_requested_folders( @folderlast ) ; my @middle = sort keys %requested_folder ; - @requested_folders_sorted = ( @folderfirst, @middle, @folderlast ) ; + @requested_folders_sorted = ( @folderfirst_requested, @middle, @folderlast_requested ) ; + #myprint "requested_folders_sorted: @requested_folders_sorted\n" ; + add_to_requested_folders( @requested_folders_sorted ) ; return( @requested_folders_sorted ) ; } -sub is_requested_folder { +sub is_requested_folder +{ my ( $folder ) = @_; - return( defined $requested_folder{ $folder } ) ; + return( defined $requested_folder{ $folder } ) ; } -sub add_to_requested_folders { +sub add_to_requested_folders +{ my @wanted_folders = @_ ; foreach my $folder ( @wanted_folders ) { @@ -5263,16 +6384,65 @@ sub add_to_requested_folders { return( keys %requested_folder ) ; } -sub remove_from_requested_folders { - my @wanted_folders = @_ ; +sub tests_remove_from_requested_folders +{ + note( 'Entering tests_remove_from_requested_folders()' ) ; - foreach my $folder ( @wanted_folders ) { - delete $requested_folder{ $folder } ; - } - return( keys %requested_folder ) ; + is( undef, undef, 'remove_from_requested_folders: undef is undef' ) ; + is_deeply( [], [ remove_from_requested_folders( ) ], 'remove_from_requested_folders: no args' ) ; + %requested_folder = ( + 'F1' => 1, + ) ; + is_deeply( [], [ remove_from_requested_folders( ) ], 'remove_from_requested_folders: remove nothing among F1 => nothing' ) ; + is_deeply( [], [ remove_from_requested_folders( 'Fno' ) ], 'remove_from_requested_folders: remove Fno among F1 => nothing' ) ; + is_deeply( [ 'F1' ], [ remove_from_requested_folders( 'F1' ) ], 'remove_from_requested_folders: remove F1 among F1 => F1' ) ; + is_deeply( { }, { %requested_folder }, 'remove_from_requested_folders: remove F1 among F1 => %requested_folder emptied' ) ; + + %requested_folder = ( + 'F1' => 1, + 'F2' => 1, + ) ; + is_deeply( [], [ remove_from_requested_folders( ) ], 'remove_from_requested_folders: remove nothing among F1 F2 => nothing' ) ; + is_deeply( [], [ remove_from_requested_folders( 'Fno' ) ], 'remove_from_requested_folders: remove Fno among F1 F2 => nothing' ) ; + is_deeply( [ 'F1' ], [ remove_from_requested_folders( 'F1' ) ], 'remove_from_requested_folders: remove F1 among F1 F2 => F1' ) ; + is_deeply( { 'F2' => 1 }, { %requested_folder }, 'remove_from_requested_folders: remove F1 among F1 F2 => %requested_folder F2' ) ; + + is_deeply( [], [ remove_from_requested_folders( 'F1' ) ], 'remove_from_requested_folders: remove F1 among F2 => nothing' ) ; + is_deeply( [ 'F2' ], [ remove_from_requested_folders( 'F1', 'F2' ) ], 'remove_from_requested_folders: remove F1 F2 among F2 => F2' ) ; + is_deeply( {}, { %requested_folder }, 'remove_from_requested_folders: remove F1 among F1 F2 => %requested_folder F2' ) ; + + %requested_folder = ( + 'F1' => 1, + 'F2' => 1, + 'F3' => 1, + ) ; + is_deeply( [ 'F1', 'F2' ], [ remove_from_requested_folders( 'F1', 'F2' ) ], 'remove_from_requested_folders: remove F1 F2 among F1 F2 F3 => F1 F2' ) ; + is_deeply( { 'F3' => 1 }, { %requested_folder }, 'remove_from_requested_folders: remove F1 F2 among F1 F2 F3 => %requested_folder F3' ) ; + + + + note( 'Leaving tests_remove_from_requested_folders()' ) ; + return ; } -sub compare_lists { + +sub remove_from_requested_folders +{ + my @unwanted_folders = @_ ; + + my @removed_folders = () ; + foreach my $folder ( @unwanted_folders ) { + if ( exists $requested_folder{ $folder } ) + { + delete $requested_folder{ $folder } ; + push @removed_folders, $folder ; + } + } + return( @removed_folders ) ; +} + +sub compare_lists +{ my ($list_1_ref, $list_2_ref) = @_; return($MINUS_ONE) if ((not defined $list_1_ref) and defined $list_2_ref); @@ -5306,7 +6476,8 @@ sub compare_lists { return 0 ; } -sub tests_compare_lists { +sub tests_compare_lists +{ note( 'Entering tests_compare_lists()' ) ; my $empty_list_ref = []; @@ -5359,7 +6530,8 @@ sub tests_compare_lists { } -sub guess_prefix { +sub guess_prefix +{ my @foldernames = @_ ; my $prefix_guessed = q{} ; @@ -5376,7 +6548,8 @@ sub guess_prefix { return( $prefix_guessed ) ; } -sub tests_guess_prefix { +sub tests_guess_prefix +{ note( 'Entering tests_guess_prefix()' ) ; is( guess_prefix( ), q{}, 'guess_prefix: no args => empty string' ) ; @@ -5396,14 +6569,15 @@ sub tests_guess_prefix { return ; } -sub get_prefix { +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" ) ; + ( $sync->{ 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" ) ; + ( $sync->{ 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] ; @@ -5433,7 +6607,8 @@ sub get_prefix { } -sub guess_separator { +sub guess_separator +{ my @foldernames = @_ ; #return( undef ) unless ( @foldernames ) ; @@ -5447,12 +6622,13 @@ sub guess_separator { $counter{'\\'}++ while ( $folder =~ m{[^\\](\\){1}(?=[^\\])}xg ) ; # count \ } my @race_sorted = sort { $counter{ $b } <=> $counter{ $a } } keys %counter ; - $debug and myprint( "@foldernames\n@race_sorted\n", %counter, "\n" ) ; + $sync->{ debug } and myprint( "@foldernames\n@race_sorted\n", %counter, "\n" ) ; $sep_guessed = shift @race_sorted || $LAST_RESSORT_SEPARATOR ; # / when nothing found. return( $sep_guessed ) ; } -sub tests_guess_separator { +sub tests_guess_separator +{ note( 'Entering tests_guess_separator()' ) ; ok( '/' eq guess_separator( ), 'guess_separator: no args' ) ; @@ -5468,16 +6644,18 @@ sub tests_guess_separator { return ; } -sub get_separator { +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" ) ; + ( $sync->{ 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' ) ) { + ( $sync->{ 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" ) ; @@ -5501,7 +6679,8 @@ sub get_separator { } } } - else{ + else + { if ( defined $sep_in ) { myprint( "$Side: No NAMESPACE capability but using [$sep_in] given by $sep_opt\n" ) ; $sep_out = $sep_in ; @@ -5516,7 +6695,8 @@ sub get_separator { return ; } -sub help_to_guess_sep { +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" @@ -5526,7 +6706,8 @@ sub help_to_guess_sep { return( $help_to_guess_sep ) ; } -sub help_to_guess_prefix { +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" @@ -5537,7 +6718,8 @@ sub help_to_guess_prefix { } -sub folders_list_to_help { +sub folders_list_to_help +{ my( $imap ) = shift ; my @folders = $imap->folders ; @@ -5545,122 +6727,462 @@ sub folders_list_to_help { return( $listing ) ; } +sub private_folders_separators_and_prefixes +{ +# what are the private folders separators and prefixes for each server ? + + ( $sync->{ debug } or $sync->{debugfolders} ) and myprint( "Getting separators\n" ) ; + $sync->{ h1_sep } = get_separator( $sync->{imap1}, $sync->{ sep1 }, '--sep1', 'Host1', \@h1_folders_all ) ; + $sync->{ h2_sep } = get_separator( $sync->{imap2}, $sync->{ sep2 }, '--sep2', 'Host2', \@h2_folders_all ) ; -sub tests_imap2_folder_name { - note( 'Entering tests_imap2_folder_name()' ) ; + $sync->{ h1_prefix } = get_prefix( $sync->{imap1}, $prefix1, '--prefix1', 'Host1', \@h1_folders_all ) ; + $sync->{ h2_prefix } = get_prefix( $sync->{imap2}, $prefix2, '--prefix2', 'Host2', \@h2_folders_all ) ; -$sync->{ h1_prefix } = $sync->{ h2_prefix } = q{} ; -$h1_sep = '/'; -$h2_sep = '.'; - -$debug and myprint( <<"EOS" -prefix1: [$sync->{ h1_prefix }] -prefix2: [$sync->{ 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->{f1f2h}{ 'auto' } = 'moto' ; -ok( 'moto' eq imap2_folder_name( 'auto' ), 'imap2_folder_name: auto' ) ; -$sync->{f1f2h}{ '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'); + myprint( "Host1: separator and prefix: [$sync->{ h1_sep }][$sync->{ h1_prefix }]\n" ) ; + myprint( "Host2: separator and prefix: [$sync->{ h2_sep }][$sync->{ h2_prefix }]\n" ) ; + return ; +} +sub subfolder1 +{ + my $mysync = shift ; + my $subfolder1 = sanitize_subfolder( $mysync->{ subfolder1 } ) ; -$fixslash2 = 0 ; -$sync->{ h1_prefix } = q{ }; + if ( $subfolder1 ) + { + # turns off automap + myprint( "Turning off automapping folders because of --subfolder1\n" ) ; + $mysync->{ automap } = undef ; + myprint( "Sanitizing subfolder1: [$mysync->{ subfolder1 }] => [$subfolder1]\n" ) ; + $mysync->{ subfolder1 } = $subfolder1 ; + add_subfolder1_to_folderrec( $mysync ) || exit_clean( $mysync, $EXIT_SUBFOLDER1_NO_EXISTS ) ; + } + else + { + $mysync->{ subfolder1 } = undef ; + } +} -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'); +sub subfolder2 +{ + my $mysync = shift ; + my $subfolder2 = sanitize_subfolder( $mysync->{ subfolder2 } ) ; + if ( $subfolder2 ) + { + # turns off automap + myprint( "Turning off automapping folders because of --subfolder2\n" ) ; + $mysync->{ automap } = undef ; + myprint( "Sanitizing subfolder2: [$mysync->{ subfolder2 }] => [$subfolder2]\n" ) ; + $mysync->{ subfolder2 } = $subfolder2 ; + set_regextrans2_for_subfolder2( $mysync ) ; + } + else + { + $mysync->{ subfolder2 } = undef ; + } -$h1_sep = '.' ; -$h2_sep = '/' ; -$sync->{ h1_prefix } = 'INBOX.' ; -$sync->{ 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' ) ; +} + +sub tests_sanitize_subfolder +{ + note( 'Entering tests_sanitize_subfolder()' ) ; + + is( undef, sanitize_subfolder( ), 'sanitize_subfolder: no args => undef' ) ; + is( undef, sanitize_subfolder( '' ), 'sanitize_subfolder: empty => undef' ) ; + is( undef, sanitize_subfolder( ' ' ), 'sanitize_subfolder: blank => undef' ) ; + is( undef, sanitize_subfolder( ' ' ), 'sanitize_subfolder: blanks => undef' ) ; + is( 'abcd', sanitize_subfolder( 'abcd' ), 'sanitize_subfolder: abcd => abcd' ) ; + is( 'ab cd', sanitize_subfolder( ' ab cd ' ), 'sanitize_subfolder: " ab cd " => "ab cd"' ) ; + is( 'abcd', sanitize_subfolder( q{a&~b#\\c[]=d;} ), 'sanitize_subfolder: "a&~b#\\c[]=d;" => "abcd"' ) ; + is( 'aA.b-_ 8c/dD', sanitize_subfolder( 'aA.b-_ 8c/dD' ), 'sanitize_subfolder: aA.b-_ 8c/dD => aA.b-_ 8c/dD' ) ; + note( 'Leaving tests_sanitize_subfolder()' ) ; + return ; +} - note( 'Leaving tests_imap2_folder_name()' ) ; - return ; +sub sanitize_subfolder +{ + my $subfolder = shift ; + if ( ! $subfolder ) + { + return ; + } + # Remove edging blanks + $subfolder =~ s,^ +| +$,,g ; + # Keep only abcd...ABCD...0123... and -_./ + $subfolder =~ tr,-_a-zA-Z0-9./ ,,cd ; + + # A blank subfolder is not a subfolder + if ( ! $subfolder ) + { + return ; + } + else + { + return $subfolder ; + } } -# Global variables to remove: -# $debug -# $sync -sub imap2_folder_name { - my $mysync = $sync ; # will be soon next line - #my $mysync = shift ; + +sub tests_add_subfolder1_to_folderrec +{ + note( 'Entering tests_add_subfolder1_to_folderrec()' ) ; + + is( undef, add_subfolder1_to_folderrec( ), 'add_subfolder1_to_folderrec: undef => undef' ) ; + is_deeply( [], [ add_subfolder1_to_folderrec( ) ], 'add_subfolder1_to_folderrec: no args => empty array' ) ; + @folderrec = () ; + my $mysync = {} ; + is_deeply( [ ], [ add_subfolder1_to_folderrec( $mysync ) ], 'add_subfolder1_to_folderrec: empty => empty array' ) ; + is_deeply( [ ], [ @folderrec ], 'add_subfolder1_to_folderrec: empty => empty folderrec' ) ; + $mysync->{ subfolder1 } = 'SUBI' ; + $h1_folders_all{ 'SUBI' } = 1 ; + $mysync->{ h1_prefix } = 'INBOX/' ; + is_deeply( [ 'SUBI' ], [ add_subfolder1_to_folderrec( $mysync ) ], 'add_subfolder1_to_folderrec: SUBI => SUBI' ) ; + is_deeply( [ 'SUBI' ], [ @folderrec ], 'add_subfolder1_to_folderrec: SUBI => folderrec SUBI ' ) ; + + @folderrec = () ; + $mysync->{ subfolder1 } = 'SUBO' ; + is_deeply( [ ], [ add_subfolder1_to_folderrec( $mysync ) ], 'add_subfolder1_to_folderrec: SUBO no exists => empty array' ) ; + is_deeply( [ ], [ @folderrec ], 'add_subfolder1_to_folderrec: SUBO no exists => empty folderrec' ) ; + $h1_folders_all{ 'INBOX/SUBO' } = 1 ; + is_deeply( [ 'INBOX/SUBO' ], [ add_subfolder1_to_folderrec( $mysync ) ], 'add_subfolder1_to_folderrec: SUBO + INBOX/SUBO exists => INBOX/SUBO' ) ; + is_deeply( [ 'INBOX/SUBO' ], [ @folderrec ], 'add_subfolder1_to_folderrec: SUBO + INBOX/SUBO exists => INBOX/SUBO folderrec' ) ; + + note( 'Leaving tests_add_subfolder1_to_folderrec()' ) ; + return ; +} + + +sub add_subfolder1_to_folderrec +{ + my $mysync = shift ; + if ( ! $mysync || ! $mysync->{ subfolder1 } ) + { + return ; + } + + my $subfolder1 = $mysync->{ subfolder1 } ; + my $subfolder1_extended = $mysync->{ h1_prefix } . $subfolder1 ; + + if ( exists $h1_folders_all{ $subfolder1 } ) + { + myprint( qq{Acting like --folderrec "$subfolder1"\n} ) ; + push @folderrec, $subfolder1 ; + } + elsif ( exists $h1_folders_all{ $subfolder1_extended } ) + { + myprint( qq{Acting like --folderrec "$subfolder1_extended"\n} ) ; + push @folderrec, $subfolder1_extended ; + } + else + { + myprint( qq{Nor folder "$subfolder1" nor "$subfolder1_extended" exists on host1\n} ) ; + } + return @folderrec ; +} + +sub set_regextrans2_for_subfolder2 +{ + my $mysync = shift ; + + + unshift @{ $mysync->{ regextrans2 } }, + q(s,^$mysync->{ h2_prefix }(.*),$mysync->{ h2_prefix }$mysync->{ subfolder2 }$mysync->{ h2_sep }$1,), + q(s,^INBOX$,$mysync->{ h2_prefix }$mysync->{ subfolder2 }$mysync->{ h2_sep }INBOX,), + q(s,^($mysync->{ h2_prefix }){2},$mysync->{ h2_prefix },); + + #myprint( "@{ $mysync->{ regextrans2 } }\n" ) ; + return ; +} + + + +# Looks like no globals here + +sub tests_imap2_folder_name +{ + note( 'Entering tests_imap2_folder_name()' ) ; + + my $mysync = {} ; + $mysync->{ h1_prefix } = q{} ; + $mysync->{ h2_prefix } = q{} ; + $mysync->{ h1_sep } = '/'; + $mysync->{ h2_sep } = '.'; + + $mysync->{ debug } and myprint( <<"EOS" +prefix1: [$mysync->{ h1_prefix }] +prefix2: [$mysync->{ h2_prefix }] +sep1: [$sync->{ h1_sep }] +sep2: [$sync->{ h2_sep }] +EOS +) ; + + $mysync->{ fixslash2 } = 0 ; + is( q{INBOX}, imap2_folder_name( $mysync, q{} ), 'imap2_folder_name: empty string' ) ; + is( 'blabla', imap2_folder_name( $mysync, 'blabla' ), 'imap2_folder_name: blabla' ) ; + is('spam.spam', imap2_folder_name( $mysync, 'spam/spam' ), 'imap2_folder_name: spam/spam' ) ; + + is( 'spam/spam', imap2_folder_name( $mysync, 'spam.spam' ), 'imap2_folder_name: spam.spam') ; + is( 'spam.spam/spam', imap2_folder_name( $mysync, 'spam/spam.spam' ), 'imap2_folder_name: spam/spam.spam' ) ; + is( 's pam.spam/sp am', imap2_folder_name( $mysync, 's pam/spam.sp am' ), 'imap2_folder_name: s pam/spam.sp am' ) ; + + $mysync->{f1f2h}{ 'auto' } = 'moto' ; + is( 'moto', imap2_folder_name( $mysync, 'auto' ), 'imap2_folder_name: auto' ) ; + $mysync->{f1f2h}{ 'auto/auto' } = 'moto x 2' ; + is( 'moto x 2', imap2_folder_name( $mysync, 'auto/auto' ), 'imap2_folder_name: auto/auto' ) ; + + @{ $mysync->{ regextrans2 } } = ( 's,/,X,g' ) ; + is( q{INBOX}, imap2_folder_name( $mysync, q{} ), 'imap2_folder_name: empty string [s,/,X,g]' ) ; + is( 'blabla', imap2_folder_name( $mysync, 'blabla' ), 'imap2_folder_name: blabla [s,/,X,g]' ) ; + is('spam.spam', imap2_folder_name( $mysync, 'spam/spam'), 'imap2_folder_name: spam/spam [s,/,X,g]'); + is('spamXspam', imap2_folder_name( $mysync, 'spam.spam'), 'imap2_folder_name: spam.spam [s,/,X,g]'); + is('spam.spamXspam', imap2_folder_name( $mysync, 'spam/spam.spam'), 'imap2_folder_name: spam/spam.spam [s,/,X,g]'); + + @{ $mysync->{ regextrans2 } } = ( 's, ,_,g' ) ; + is('blabla', imap2_folder_name( $mysync, 'blabla'), 'imap2_folder_name: blabla [s, ,_,g]'); + is('bla_bla', imap2_folder_name( $mysync, 'bla bla'), 'imap2_folder_name: blabla [s, ,_,g]'); + + @{ $mysync->{ regextrans2 } } = ( q{s,(.*),\U$1,} ) ; + is( 'BLABLA', imap2_folder_name( $mysync, 'blabla' ), q{imap2_folder_name: blabla [s,\U(.*)\E,$1,]} ) ; + + $mysync->{ fixslash2 } = 1 ; + @{ $mysync->{ regextrans2 } } = ( ) ; + is(q{INBOX}, imap2_folder_name( $mysync, q{}), 'imap2_folder_name: empty string'); + is('blabla', imap2_folder_name( $mysync, 'blabla'), 'imap2_folder_name: blabla'); + is('spam.spam', imap2_folder_name( $mysync, 'spam/spam'), 'imap2_folder_name: spam/spam -> spam.spam'); + is('spam_spam', imap2_folder_name( $mysync, 'spam.spam'), 'imap2_folder_name: spam.spam -> spam_spam'); + is('spam.spam_spam', imap2_folder_name( $mysync, 'spam/spam.spam'), 'imap2_folder_name: spam/spam.spam -> spam.spam_spam'); + is('s pam.spam_spa m', imap2_folder_name( $mysync, 's pam/spam.spa m'), 'imap2_folder_name: s pam/spam.spa m -> s pam.spam_spa m'); + + $mysync->{ h1_sep } = '.'; + $mysync->{ h2_sep } = '/'; + is( q{INBOX}, imap2_folder_name( $mysync, q{}), 'imap2_folder_name: empty string'); + is('blabla', imap2_folder_name( $mysync, 'blabla'), 'imap2_folder_name: blabla'); + is('spam.spam', imap2_folder_name( $mysync, 'spam/spam'), 'imap2_folder_name: spam/spam -> spam.spam'); + is('spam/spam', imap2_folder_name( $mysync, 'spam.spam'), 'imap2_folder_name: spam.spam -> spam/spam'); + is('spam.spam/spam', imap2_folder_name( $mysync, 'spam/spam.spam'), 'imap2_folder_name: spam/spam.spam -> spam.spam/spam'); + + + + $mysync->{ fixslash2 } = 0 ; + $mysync->{ h1_prefix } = q{ }; + + is( 'spam.spam/spam', imap2_folder_name( $mysync, 'spam/spam.spam' ), 'imap2_folder_name: spam/spam.spam -> spam.spam/spam' ) ; + is( 'spam.spam/spam', imap2_folder_name( $mysync, ' spam/spam.spam' ), 'imap2_folder_name: spam/spam.spam -> spam.spam/spam' ) ; + + $mysync->{ h1_sep } = '.' ; + $mysync->{ h2_sep } = '/' ; + $mysync->{ h1_prefix } = 'INBOX.' ; + $mysync->{ h2_prefix } = q{} ; + @{ $mysync->{ regextrans2 } } = ( q{s,(.*),\U$1,} ) ; + is( 'BLABLA', imap2_folder_name( $mysync, 'blabla' ), 'imap2_folder_name: blabla' ) ; + is( 'TEST/TEST/TEST/TEST', imap2_folder_name( $mysync, 'INBOX.TEST.test.Test.tesT' ), 'imap2_folder_name: INBOX.TEST.test.Test.tesT' ) ; + @{ $mysync->{ regextrans2 } } = ( q{s,(.*),\L$1,} ) ; + is( 'test/test/test/test', imap2_folder_name( $mysync, 'INBOX.TEST.test.Test.tesT' ), 'imap2_folder_name: INBOX.TEST.test.Test.tesT' ) ; + + # INBOX + $mysync = {} ; + $mysync->{ h1_prefix } = q{Pf1.} ; + $mysync->{ h2_prefix } = q{Pf2/} ; + $mysync->{ h1_sep } = '.'; + $mysync->{ h2_sep } = '/'; + + # + #$mysync->{ debug } = 1 ; + is( 'Pf2/F1/F2/F3', imap2_folder_name( $mysync, 'F1.F2.F3' ), 'imap2_folder_name: F1.F2.F3 -> Pf2/F1/F2/F3' ) ; + is( 'Pf2/F1/INBOX', imap2_folder_name( $mysync, 'F1.INBOX' ), 'imap2_folder_name: F1.INBOX -> Pf2/F1/INBOX' ) ; + is( 'INBOX', imap2_folder_name( $mysync, 'INBOX' ), 'imap2_folder_name: INBOX -> INBOX' ) ; + + is( 'Pf2/F1/F2/F3', imap2_folder_name( $mysync, 'Pf1.F1.F2.F3' ), 'imap2_folder_name: Pf1.F1.F2.F3 -> Pf2/F1/F2/F3' ) ; + is( 'Pf2/F1/INBOX', imap2_folder_name( $mysync, 'Pf1.F1.INBOX' ), 'imap2_folder_name: Pf1.F1.INBOX -> Pf2/F1/INBOX' ) ; + is( 'INBOX', imap2_folder_name( $mysync, 'Pf1.INBOX' ), 'imap2_folder_name: Pf1.INBOX -> INBOX' ) ; # not Pf2/INBOX: Yes I can! + + + + # subfolder2 + $mysync = {} ; + $mysync->{ h1_prefix } = q{} ; + $mysync->{ h2_prefix } = q{} ; + $mysync->{ h1_sep } = '/'; + $mysync->{ h2_sep } = '.'; + + + set_regextrans2_for_subfolder2( $mysync ) ; + $mysync->{ subfolder2 } = 'S1.S2' ; + is( 'S1.S2.F1.F2.F3', imap2_folder_name( $mysync, 'F1/F2/F3' ), 'imap2_folder_name: F1/F2/F3 -> S1.S2.F1.F2.F3' ) ; + is( 'S1.S2.INBOX', imap2_folder_name( $mysync, 'INBOX' ), 'imap2_folder_name: F1/F2/F3 -> S1.S2.INBOX' ) ; + + $mysync = {} ; + $mysync->{ h1_prefix } = q{Pf1/} ; + $mysync->{ h2_prefix } = q{Pf2.} ; + $mysync->{ h1_sep } = '/'; + $mysync->{ h2_sep } = '.'; + #$mysync->{ debug } = 1 ; + + set_regextrans2_for_subfolder2( $mysync ) ; + $mysync->{ subfolder2 } = 'Pf2.S1.S2' ; + is( 'Pf2.S1.S2.F1.F2.F3', imap2_folder_name( $mysync, 'F1/F2/F3' ), 'imap2_folder_name: F1/F2/F3 -> Pf2.S1.S2.F1.F2.F3' ) ; + is( 'Pf2.S1.S2.INBOX', imap2_folder_name( $mysync, 'INBOX' ), 'imap2_folder_name: INBOX -> Pf2.S1.S2.INBOX' ) ; + is( 'Pf2.S1.S2.F1.F2.F3', imap2_folder_name( $mysync, 'Pf1/F1/F2/F3' ), 'imap2_folder_name: F1/F2/F3 -> Pf2.S1.S2.F1.F2.F3' ) ; + is( 'Pf2.S1.S2.INBOX', imap2_folder_name( $mysync, 'Pf1/INBOX' ), 'imap2_folder_name: INBOX -> Pf2.S1.S2.INBOX' ) ; + + # subfolder1 + # scenario as the reverse of the previous tests, separators point of vue + $mysync = {} ; + $mysync->{ h1_prefix } = q{Pf1.} ; + $mysync->{ h2_prefix } = q{Pf2/} ; + $mysync->{ h1_sep } = '.'; + $mysync->{ h2_sep } = '/'; + #$mysync->{ debug } = 1 ; + + $mysync->{ subfolder1 } = 'S1.S2' ; + is( 'Pf2/F1/F2/F3', imap2_folder_name( $mysync, 'S1.S2.F1.F2.F3' ), 'imap2_folder_name: S1.S2.F1.F2.F3 -> Pf2/F1/F2/F3' ) ; + is( 'Pf2/F1/F2/F3', imap2_folder_name( $mysync, 'Pf1.S1.S2.F1.F2.F3' ), 'imap2_folder_name: Pf1.S1.S2.F1.F2.F3 -> Pf2/F1/F2/F3' ) ; + + is( 'INBOX', imap2_folder_name( $mysync, 'S1.S2.INBOX' ), 'imap2_folder_name: S1.S2.INBOX -> INBOX' ) ; + is( 'INBOX', imap2_folder_name( $mysync, 'S1.S2' ), 'imap2_folder_name: S1.S2 -> INBOX' ) ; + is( 'INBOX', imap2_folder_name( $mysync, 'S1.S2.' ), 'imap2_folder_name: S1.S2. -> INBOX' ) ; + + is( 'INBOX', imap2_folder_name( $mysync, 'Pf1.S1.S2.INBOX' ), 'imap2_folder_name: Pf1.S1.S2.INBOX -> INBOX' ) ; + is( 'INBOX', imap2_folder_name( $mysync, 'Pf1.S1.S2' ), 'imap2_folder_name: Pf1.S1.S2 -> INBOX' ) ; + is( 'INBOX', imap2_folder_name( $mysync, 'Pf1.S1.S2.' ), 'imap2_folder_name: Pf1.S1.S2. -> INBOX' ) ; + + + $mysync->{ subfolder1 } = 'S1.S2.' ; + is( 'Pf2/F1/F2/F3', imap2_folder_name( $mysync, 'S1.S2.F1.F2.F3' ), 'imap2_folder_name: S1.S2.F1.F2.F3 -> Pf2/F1/F2/F3' ) ; + is( 'Pf2/F1/F2/F3', imap2_folder_name( $mysync, 'Pf1.S1.S2.F1.F2.F3' ), 'imap2_folder_name: Pf1.S1.S2.F1.F2.F3 -> Pf2/F1/F2/F3' ) ; + + is( 'INBOX', imap2_folder_name( $mysync, 'S1.S2.INBOX' ), 'imap2_folder_name: S1.S2.INBOX -> INBOX' ) ; + is( 'INBOX', imap2_folder_name( $mysync, 'S1.S2' ), 'imap2_folder_name: S1.S2 -> INBOX' ) ; + is( 'INBOX', imap2_folder_name( $mysync, 'S1.S2.' ), 'imap2_folder_name: S1.S2. -> INBOX' ) ; + + is( 'INBOX', imap2_folder_name( $mysync, 'Pf1.S1.S2.INBOX' ), 'imap2_folder_name: Pf1.S1.S2.INBOX -> INBOX' ) ; + is( 'INBOX', imap2_folder_name( $mysync, 'Pf1.S1.S2' ), 'imap2_folder_name: Pf1.S1.S2 -> INBOX' ) ; + is( 'INBOX', imap2_folder_name( $mysync, 'Pf1.S1.S2.' ), 'imap2_folder_name: Pf1.S1.S2. -> INBOX' ) ; + + + # subfolder1 + # scenario as Gmail + $mysync = {} ; + $mysync->{ h1_prefix } = q{} ; + $mysync->{ h2_prefix } = q{} ; + $mysync->{ h1_sep } = '/'; + $mysync->{ h2_sep } = '/'; + #$mysync->{ debug } = 1 ; + + $mysync->{ subfolder1 } = 'S1/S2' ; + is( 'F1/F2/F3', imap2_folder_name( $mysync, 'S1/S2/F1/F2/F3' ), 'imap2_folder_name: S1/S2/F1/F2/F3 -> F1/F2/F3' ) ; + is( 'INBOX', imap2_folder_name( $mysync, 'S1/S2/INBOX' ), 'imap2_folder_name: S1/S2/INBOX -> INBOX' ) ; + is( 'INBOX', imap2_folder_name( $mysync, 'S1/S2' ), 'imap2_folder_name: S1/S2 -> INBOX' ) ; + is( 'INBOX', imap2_folder_name( $mysync, 'S1/S2/' ), 'imap2_folder_name: S1/S2/ -> INBOX' ) ; + + $mysync->{ subfolder1 } = 'S1/S2/' ; + is( 'F1/F2/F3', imap2_folder_name( $mysync, 'S1/S2/F1/F2/F3' ), 'imap2_folder_name: S1/S2/F1/F2/F3 -> F1/F2/F3' ) ; + is( 'INBOX', imap2_folder_name( $mysync, 'S1/S2/INBOX' ), 'imap2_folder_name: S1/S2/INBOX -> INBOX' ) ; + is( 'INBOX', imap2_folder_name( $mysync, 'S1/S2' ), 'imap2_folder_name: S1/S2 -> INBOX' ) ; + is( 'INBOX', imap2_folder_name( $mysync, 'S1/S2/' ), 'imap2_folder_name: S1/S2/ -> INBOX' ) ; + + + note( 'Leaving tests_imap2_folder_name()' ) ; + return ; +} + + +# Global variables to remove: +# + + +sub imap2_folder_name +{ + my $mysync = shift ; my ( $h1_fold ) = shift ; my ( $h2_fold ) ; if ( $mysync->{f1f2h}{ $h1_fold } ) { $h2_fold = $mysync->{f1f2h}{ $h1_fold } ; - ( $debug or $mysync->{debugfolders} ) and myprint( "f1f2 [$h1_fold] -> [$h2_fold]\n" ) ; + ( $mysync->{ debug } or $mysync->{debugfolders} ) and myprint( "f1f2 [$h1_fold] -> [$h2_fold]\n" ) ; return( $h2_fold ) ; } if ( $mysync->{f1f2auto}{ $h1_fold } ) { $h2_fold = $mysync->{f1f2auto}{ $h1_fold } ; - ( $debug or $mysync->{debugfolders} ) and myprint( "automap [$h1_fold] -> [$h2_fold]\n" ) ; + ( $mysync->{ debug } or $mysync->{debugfolders} ) and myprint( "automap [$h1_fold] -> [$h2_fold]\n" ) ; return( $h2_fold ) ; } + if ( $mysync->{ subfolder1 } ) + { + my $esc_h1_sep = "\\" . $mysync->{ h1_sep } ; + # case where subfolder1 has the sep1 at the end, then remove it + my $part_to_removed = remove_last_char_if_is( $mysync->{ subfolder1 }, $mysync->{ h1_sep } ) ; + # remove the subfolder1 part and the sep1 if present after + $h1_fold =~ s{$part_to_removed($esc_h1_sep)?}{} ; + #myprint( "h1_fold=$h1_fold\n" ) ; + } + + if ( ( '' eq $h1_fold ) or ( $mysync->{ h1_prefix } eq $h1_fold ) ) + { + $h1_fold = 'INBOX' ; + } + $h2_fold = prefix_seperator_invertion( $mysync, $h1_fold ) ; - $h2_fold = regextrans2( $h2_fold ) ; + $h2_fold = regextrans2( $mysync, $h2_fold ) ; return( $h2_fold ) ; } -sub tests_prefix_seperator_invertion { - undef $h1_sep; - undef $h2_sep ; - + +sub tests_remove_last_char_if_is +{ + note( 'Entering tests_remove_last_char_if_is()' ) ; + + is( undef, remove_last_char_if_is( ), 'remove_last_char_if_is: no args => undef' ) ; + is( '', remove_last_char_if_is( '' ), 'remove_last_char_if_is: empty => empty' ) ; + is( '', remove_last_char_if_is( '', 'Z' ), 'remove_last_char_if_is: empty Z => empty' ) ; + is( '', remove_last_char_if_is( 'Z', 'Z' ), 'remove_last_char_if_is: Z Z => empty' ) ; + is( 'abc', remove_last_char_if_is( 'abcZ', 'Z' ), 'remove_last_char_if_is: abcZ Z => abc' ) ; + is( 'abcY', remove_last_char_if_is( 'abcY', 'Z' ), 'remove_last_char_if_is: abcY Z => abcY' ) ; + note( 'Leaving tests_remove_last_char_if_is()' ) ; + return ; +} + + + + +sub remove_last_char_if_is +{ + my $string = shift ; + my $char = shift ; + + if ( ! defined $string ) + { + return ; + } + + if ( ! defined $char ) + { + return $string ; + } + + my $last_char = substr $string, -1 ; + if ( $char eq $last_char ) + { + chop $string ; + return $string ; + } + else + { + return $string ; + } +} + +sub tests_prefix_seperator_invertion +{ + note( 'Entering tests_prefix_seperator_invertion()' ) ; + is( undef, prefix_seperator_invertion( ), 'prefix_seperator_invertion: no args => undef' ) ; is( q{}, prefix_seperator_invertion( undef, q{} ), 'prefix_seperator_invertion: empty string => empty string' ) ; is( 'lalala', prefix_seperator_invertion( undef, 'lalala' ), 'prefix_seperator_invertion: lalala => lalala' ) ; @@ -5668,103 +7190,111 @@ sub tests_prefix_seperator_invertion { is( 'lal.ala', prefix_seperator_invertion( undef, 'lal.ala' ), 'prefix_seperator_invertion: lal.ala => lal.ala' ) ; is( '////', prefix_seperator_invertion( undef, '////' ), 'prefix_seperator_invertion: //// => ////' ) ; is( '.....', prefix_seperator_invertion( undef, '.....' ), 'prefix_seperator_invertion: ..... => .....' ) ; - + my $mysync = { h1_prefix => '', h2_prefix => '', h1_sep => '/', h2_sep => '/', } ; - + is( q{}, prefix_seperator_invertion( $mysync, q{} ), 'prefix_seperator_invertion: $mysync empty string => empty string' ) ; is( 'lalala', prefix_seperator_invertion( $mysync, 'lalala' ), 'prefix_seperator_invertion: $mysync lalala => lalala' ) ; is( 'lal/ala', prefix_seperator_invertion( $mysync, 'lal/ala' ), 'prefix_seperator_invertion: $mysync lal/ala => lal/ala' ) ; is( 'lal.ala', prefix_seperator_invertion( $mysync, 'lal.ala' ), 'prefix_seperator_invertion: $mysync lal.ala => lal.ala' ) ; is( '////', prefix_seperator_invertion( $mysync, '////' ), 'prefix_seperator_invertion: $mysync //// => ////' ) ; is( '.....', prefix_seperator_invertion( $mysync, '.....' ), 'prefix_seperator_invertion: $mysync ..... => .....' ) ; - + $mysync = { h1_prefix => 'PPP', h2_prefix => 'QQQ', h1_sep => 's', h2_sep => 't', } ; - + is( q{QQQ}, prefix_seperator_invertion( $mysync, q{} ), 'prefix_seperator_invertion: PPPQQQst empty string => QQQ' ) ; is( 'QQQlalala', prefix_seperator_invertion( $mysync, 'lalala' ), 'prefix_seperator_invertion: PPPQQQst lalala => QQQlalala' ) ; is( 'QQQlal/ala', prefix_seperator_invertion( $mysync, 'lal/ala' ), 'prefix_seperator_invertion: PPPQQQst lal/ala => QQQlal/ala' ) ; is( 'QQQlal.ala', prefix_seperator_invertion( $mysync, 'lal.ala' ), 'prefix_seperator_invertion: PPPQQQst lal.ala => QQQlal.ala' ) ; is( 'QQQ////', prefix_seperator_invertion( $mysync, '////' ), 'prefix_seperator_invertion: PPPQQQst //// => QQQ////' ) ; is( 'QQQ.....', prefix_seperator_invertion( $mysync, '.....' ), 'prefix_seperator_invertion: PPPQQQst ..... => QQQ.....' ) ; - + is( 'QQQPlalala', prefix_seperator_invertion( $mysync, 'PPPPlalala' ), 'prefix_seperator_invertion: PPPQQQst PPPPlalala => QQQPlalala' ) ; is( 'QQQ', prefix_seperator_invertion( $mysync, 'PPP' ), 'prefix_seperator_invertion: PPPQQQst PPP => QQQ' ) ; is( 'QQQttt', prefix_seperator_invertion( $mysync, 'sss' ), 'prefix_seperator_invertion: PPPQQQst sss => QQQttt' ) ; is( 'QQQt', prefix_seperator_invertion( $mysync, 's' ), 'prefix_seperator_invertion: PPPQQQst s => QQQt' ) ; is( 'QQQtAAAtBBB', prefix_seperator_invertion( $mysync, 'PPPsAAAsBBB' ), 'prefix_seperator_invertion: PPPQQQst PPPsAAAsBBB => QQQtAAAtBBB' ) ; - + + note( 'Leaving tests_prefix_seperator_invertion()' ) ; return ; } -# Global variables to remove: -# $h1_sep -# $h2_sep -# $debug +# Global variables to remove: -sub prefix_seperator_invertion { + +sub prefix_seperator_invertion +{ my $mysync = shift ; my $h1_fold = shift ; my $h2_fold ; if ( not defined $h1_fold ) { return ; } - + my $my_h1_prefix = $mysync->{ h1_prefix } || q{} ; my $my_h2_prefix = $mysync->{ h2_prefix } || q{} ; - my $my_h1_sep = $h1_sep || $mysync->{ h1_sep } || '/' ; - my $my_h2_sep = $h2_sep || $mysync->{ h2_sep } || '/' ; - + my $my_h1_sep = $mysync->{ h1_sep } || '/' ; + my $my_h2_sep = $mysync->{ h2_sep } || '/' ; + # first we remove the prefix $h1_fold =~ s/^\Q$my_h1_prefix\E//x ; - ( $debug or $mysync->{debugfolders} ) and myprint( "removed host1 prefix: [$h1_fold]\n" ) ; - $h2_fold = separator_invert( $h1_fold, $my_h1_sep, $my_h2_sep ) ; - ( $debug or $mysync->{debugfolders} ) and myprint( "inverted separators: [$h2_fold]\n" ) ; + ( $mysync->{ debug } or $mysync->{debugfolders} ) and myprint( "removed host1 prefix: [$h1_fold]\n" ) ; + $h2_fold = separator_invert( $mysync, $h1_fold, $my_h1_sep, $my_h2_sep ) ; + ( $mysync->{ debug } or $mysync->{debugfolders} ) and myprint( "inverted separators: [$h2_fold]\n" ) ; + # Adding the prefix supplied by namespace or the --prefix2 option - $h2_fold = $my_h2_prefix . $h2_fold - unless( ( $my_h2_prefix eq 'INBOX' . $my_h2_sep ) and ( $h2_fold =~ m/^INBOX$/xi ) ) ; - ( $debug or $mysync->{debugfolders} ) and myprint( "added host2 prefix: [$h2_fold]\n" ) ; + # except for INBOX or Inbox + if ( $h2_fold !~ m/^INBOX$/xi ) + { + $h2_fold = $my_h2_prefix . $h2_fold ; + } + + ( $mysync->{ debug } or $mysync->{debugfolders} ) and myprint( "added host2 prefix: [$h2_fold]\n" ) ; return( $h2_fold ) ; } -sub tests_separator_invert { +sub tests_separator_invert +{ note( 'Entering tests_separator_invert()' ) ; - $fixslash2 = 0 ; + my $mysync = {} ; + $mysync->{ 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( q{} eq separator_invert( $mysync, q{}, q{}, q{} ), 'separator_invert: 3 empty strings' ) ; + ok( 'lalala' eq separator_invert( $mysync, 'lalala', q{}, q{} ), 'separator_invert: empty separator' ) ; + ok( 'lalala' eq separator_invert( $mysync, 'lalala', '/', '/' ), 'separator_invert: same separator /' ) ; + ok( 'lal/ala' eq separator_invert( $mysync, 'lal/ala', '/', '/' ), 'separator_invert: same separator / 2' ) ; + ok( 'lal.ala' eq separator_invert( $mysync, 'lal/ala', '/', '.' ), 'separator_invert: separators /.' ) ; + ok( 'lal/ala' eq separator_invert( $mysync, 'lal.ala', '.', '/' ), 'separator_invert: separators ./' ) ; + ok( 'la.l/ala' eq separator_invert( $mysync, '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 /.' ) ; + ok( 'l/al.ala' eq separator_invert( $mysync, 'l.al/ala', '/', '.' ), 'separator_invert: separators /.' ) ; + $mysync->{ fixslash2 } = 1 ; + ok( 'l_al.ala' eq separator_invert( $mysync, 'l.al/ala', '/', '.' ), 'separator_invert: separators /.' ) ; note( 'Leaving tests_separator_invert()' ) ; return ; } -# Global variables to remove: -# $fixslash2 -sub separator_invert { - my( $h1_fold, $h1_separator, $h2_separator ) = @_ ; +# Global variables to remove: +# +sub separator_invert +{ + my( $mysync, $h1_fold, $h1_separator, $h2_separator ) = @_ ; - return( undef ) if ( not defined $h1_fold or not defined $h1_separator or not defined $h2_separator ) ; + return( undef ) if ( not all_defined( $mysync, $h1_fold, $h1_separator, $h2_separator ) ) ; # The separator we hope we'll never encounter: 00000000 == 0x00 my $o_sep = "\000" ; @@ -5772,27 +7302,29 @@ sub separator_invert { $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 ) ; + $h2_fold =~ s,/,_,xg if( $mysync->{ fixslash2 } and '/' ne $h2_separator and '/' eq $h1_separator ) ; return( $h2_fold ) ; } -sub regextrans2 { - my( $h2_fold ) = @_ ; +sub regextrans2 +{ + my( $mysync, $h2_fold ) = @_ ; # Transforming the folder name by the --regextrans2 option(s) - foreach my $regextrans2 ( @regextrans2 ) { + foreach my $regextrans2 ( @{ $mysync->{ 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" ) ; + ( $mysync->{ debug } or $mysync->{debugfolders} ) and myprint( "[$h2_fold_before] -> [$h2_fold] using regextrans2 [$regextrans2]\n" ) ; if ( not ( defined $ret ) or $EVAL_ERROR ) { - die_clean( "error: eval regextrans2 '$regextrans2': $EVAL_ERROR\n" ) ; + exit_clean( $mysync, $EX_USAGE, "error: eval regextrans2 '$regextrans2': $EVAL_ERROR\n" ) ; } } return( $h2_fold ) ; } -sub tests_decompose_regex { +sub tests_decompose_regex +{ note( 'Entering tests_decompose_regex()' ) ; ok( 1, 'decompose_regex 1' ) ; @@ -5803,7 +7335,8 @@ sub tests_decompose_regex { return ; } -sub decompose_regex { +sub decompose_regex +{ my $regex = shift ; my( $left_part, $right_part ) ; @@ -5813,7 +7346,8 @@ sub decompose_regex { } -sub foldersizes { +sub foldersizes +{ my ( $side, $imap, $search_cmd, $abletosearch, @folders ) = @_ ; my $total_size = 0 ; @@ -5859,7 +7393,7 @@ sub foldersizes { if ( $nb_msgs > 0 and @msgs ) { if ( $abletosearch ) { if ( ! $imap->fetch_hash( \@msgs, 'RFC822.SIZE', $hash_ref) ) { - my $error = "$side failure with fetch_hash: $EVAL_ERROR" ; + my $error = "$side failure with fetch_hash: $EVAL_ERROR\n" ; errors_incr( $sync, $error ) ; return ; } @@ -5867,7 +7401,7 @@ sub foldersizes { 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: $EVAL_ERROR" ; + my $error = "$side failure with fetch_hash: $EVAL_ERROR\n" ; errors_incr( $sync, $error ) ; return ; } @@ -5894,7 +7428,8 @@ sub foldersizes { return( $total_nb, $total_size ) ; } -sub timenext { +sub timenext +{ my ( $timenow, $timediff ) ; # $timebefore is global, beurk ! $timenow = time ; @@ -5903,7 +7438,8 @@ sub timenext { return( $timediff ) ; } -sub timesince { +sub timesince +{ my $timeinit = shift || 0 ; my ( $timenow, $timediff ) ; $timenow = time ; @@ -5915,7 +7451,8 @@ sub timesince { -sub tests_flags_regex { +sub tests_flags_regex +{ note( 'Entering tests_flags_regex()' ) ; ok( q{} eq flags_regex(q{} ), 'flags_regex, null string q{}' ) ; @@ -5926,7 +7463,7 @@ sub tests_flags_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' ) ; + @regexflag = ( q{s/\$Spam//g} ) ; ok( q{\Seen NonJunk } eq flags_regex( q{\Seen NonJunk $Spam} ), q{flags_regex, remove $Spam: 's/\$Spam//g'} ) ; @regexflag = ( 's/\\\\Seen//g' ) ; @@ -5964,41 +7501,9 @@ sub tests_flags_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/'} ) ; + ok('REM REM REM REM REM jrdH8u' eq flags_regex('REM REM REM REM REM'), q{Add jrdH8u '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' ) ; - + ok('REM REM REM REM REM ' eq flags_regex('REM REM REM REM REM jrdH8u'), q{Remove jrdH8u s/jrdH8u *//} ) ; @regexflag = ( 's/.*?(?:(\\\\(?:Answered|Flagged|Deleted|Seen|Draft)\s?)|$)/defined($1)?$1:q()/eg' @@ -6018,11 +7523,33 @@ sub tests_flags_regex { eq flags_regex('\\Deleted \\Answered \\Draft \\Flagged '), 'Keep only regex: Exchange case (Phil)' ) ; + @regexflag = ( 's/\\\\Flagged//g' ) ; + + is('\Deleted \Answered \Draft ', + flags_regex('\\Deleted \\Answered \\Draft \\Flagged '), + 'flags_regex: remove \Flagged 1' ) ; + is('\\Deleted \\Answered \\Draft', + flags_regex('\\Deleted \\Flagged \\Answered \\Draft'), + 'flags_regex: remove \Flagged 2' ) ; + + # I didn't understand why it gives \F + # https://perldoc.perl.org/perlrebackslash.html + # \F Foldcase till \E. Not in []. + # https://perldoc.perl.org/functions/fc.html + + # \F Not available in old Perl so I comment the test + + # @regexflag = ( 's/\\Flagged/X/g' ) ; + #is('\Deleted FX \Answered \FX \Draft \FX', + #flags_regex( '\Deleted Flagged \Answered \Flagged \Draft \Flagged' ), + # 'flags_regex: remove \Flagged 3 mistery...' ) ; + note( 'Leaving tests_flags_regex()' ) ; return ; } -sub flags_regex { +sub flags_regex +{ my ( $h1_flags ) = @_ ; foreach my $regexflag ( @regexflag ) { my $h1_flags_orig = $h1_flags ; @@ -6037,12 +7564,13 @@ sub flags_regex { return( $h1_flags ) ; } -sub acls_sync { +sub acls_sync +{ my($h1_fold, $h2_fold) = @_ ; if ( $syncacls ) { - my $h1_hash = $imap1->getacl($h1_fold) + my $h1_hash = $sync->{imap1}->getacl($h1_fold) or myprint( "Could not getacl for $h1_fold: $EVAL_ERROR\n" ) ; - my $h2_hash = $imap2->getacl($h2_fold) + my $h2_hash = $sync->{imap2}->getacl($h2_fold) or myprint( "Could not getacl for $h2_fold: $EVAL_ERROR\n" ) ; my %users = map { ($_, 1) } ( keys %{ $h1_hash} , keys %{ $h2_hash } ) ; foreach my $user (sort keys %users ) { @@ -6052,7 +7580,7 @@ sub acls_sync { $h1_hash->{$user} eq $h2_hash->{$user}); unless ($sync->{dry}) { myprint( "setting acl $h2_fold $user $acl\n" ) ; - $imap2->setacl($h2_fold, $user, $acl) + $sync->{imap2}->setacl($h2_fold, $user, $acl) or myprint( "Could not set acl: $EVAL_ERROR\n" ) ; } } @@ -6061,7 +7589,8 @@ sub acls_sync { } -sub tests_permanentflags { +sub tests_permanentflags +{ note( 'Entering tests_permanentflags()' ) ; my $string; @@ -6081,12 +7610,13 @@ sub tests_permanentflags { return ; } -sub permanentflags { +sub permanentflags +{ my @lines = @_ ; foreach my $line (@lines) { if ( $line =~ m{\[PERMANENTFLAGS\s\(([^)]+?)\)\]}x ) { - ( $debugflags or $debug ) and myprint( "permanentflags: $line" ) ; + ( $debugflags or $sync->{ debug } ) and myprint( "permanentflags: $line" ) ; my $permanentflags = $1 ; if ( $permanentflags =~ m{\\\*}x ) { $permanentflags = q{} ; @@ -6097,7 +7627,8 @@ sub permanentflags { return( q{} ) ; } -sub tests_flags_filter { +sub tests_flags_filter +{ note( 'Entering tests_flags_filter()' ) ; ok( '\Seen' eq flags_filter('\Seen', '\Draft \Seen \Answered'), 'flags_filter ' ); @@ -6113,7 +7644,8 @@ sub tests_flags_filter { return ; } -sub flags_filter { +sub flags_filter +{ my( $flags, $allowed_flags ) = @_ ; my @flags = split /\s+/x, $flags ; @@ -6125,7 +7657,8 @@ sub flags_filter { return( $flags_out ) ; } -sub flagscase { +sub flagscase +{ my $flags = shift ; my @flags = split /\s+/x, $flags ; @@ -6137,7 +7670,8 @@ sub flagscase { return( $flags_out ) ; } -sub tests_flagscase { +sub tests_flagscase +{ note( 'Entering tests_flagscase()' ) ; ok( '\Seen' eq flagscase( '\Seen' ), 'flagscase: \Seen -> \Seen' ) ; @@ -6155,7 +7689,8 @@ sub tests_flagscase { -sub ucsecond { +sub ucsecond +{ my $string = shift ; my $output ; @@ -6167,8 +7702,10 @@ sub ucsecond { } -sub tests_ucsecond { +sub tests_ucsecond +{ note( 'Entering 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' ) ; @@ -6183,7 +7720,8 @@ sub tests_ucsecond { } -sub select_msgs { +sub select_msgs +{ my ( $imap, $msgs_all_hash_ref, $search_cmd, $abletosearch, $folder ) = @_ ; my ( @msgs ) ; @@ -6196,7 +7734,8 @@ sub select_msgs { } -sub select_msgs_by_search { +sub select_msgs_by_search +{ my ( $imap, $msgs_all_hash_ref, $search_cmd, $folder ) = @_ ; my ( @msgs, @msgs_all ) ; @@ -6234,7 +7773,8 @@ sub select_msgs_by_search { } -sub select_msgs_by_fetch { +sub select_msgs_by_fetch +{ my ( $imap, $msgs_all_hash_ref, $search_cmd, $folder ) = @_ ; my ( @msgs, @msgs_all, %fetch ) ; @@ -6251,7 +7791,7 @@ sub select_msgs_by_fetch { @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 ) { @@ -6286,7 +7826,8 @@ sub select_msgs_by_fetch { return( @msgs ) ; } -sub select_msgs_by_age { +sub select_msgs_by_age +{ my( $imap ) = @_ ; my( @max, @min, @msgs, @inter, @union ) ; @@ -6302,7 +7843,8 @@ sub select_msgs_by_age { return( @msgs ) ; } -sub msgs_from_maxmin { +sub msgs_from_maxmin +{ my( $max_ref, $min_ref ) = @_ ; my( @max, @min, @msgs, @inter, @union ) ; @@ -6325,7 +7867,8 @@ sub msgs_from_maxmin { return( @msgs ) ; } -sub tests_msgs_from_maxmin { +sub tests_msgs_from_maxmin +{ note( 'Entering tests_msgs_from_maxmin()' ) ; my @msgs ; @@ -6346,14 +7889,17 @@ sub tests_msgs_from_maxmin { return ; } -sub tests_info_date_from_uid { - +sub tests_info_date_from_uid +{ + note( 'Entering tests_info_date_from_uid()' ) ; + note( 'Leaving tests_info_date_from_uid()' ) ; return ; } -sub info_date_from_uid { - +sub info_date_from_uid +{ + #my $first_uid = $msgs_all[ 0 ] ; #my $first_idate = $fetch{ $first_uid }->{'INTERNALDATE'} ; #my $first_epoch = epoch( $first_idate ) ; @@ -6362,7 +7908,8 @@ sub info_date_from_uid { } -sub lastuid { +sub lastuid +{ my $imap = shift ; my $folder = shift ; my $lastuid_guess = shift ; @@ -6390,26 +7937,28 @@ sub lastuid { return( $lastuid ) ; } -sub size_filtered { +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; + if ( defined $sync->{ maxsize } and $h1_size > $sync->{ maxsize } ) { + myprint( "msg $h1_fold/$h1_msg skipped ($h1_size exceeds maxsize limit $sync->{ maxsize } bytes)\n" ) ; + $sync->{ total_bytes_skipped } += $h1_size; + $sync->{ nb_msg_skipped } += 1; return( 1 ) ; } - if (defined $minsize and $h1_size <= $minsize) { + 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; + $sync->{ total_bytes_skipped } += $h1_size; + $sync->{ nb_msg_skipped } += 1; return( 1 ) ; } return( 0 ) ; } -sub message_exists { +sub message_exists +{ my( $imap, $msg ) = @_ ; return( 1 ) if not $imap->Uid( ) ; @@ -6420,11 +7969,28 @@ sub message_exists { return( 0 ) ; } -sub copy_message { + +# Globals +# $sync->{ total_bytes_skipped } +# $sync->{ nb_msg_skipped } +# $mysync->{ h1_nb_msg_processed } +sub stats_update_skip_message +{ + my $mysync = shift ; # to be used + my $h1_size = shift ; + + $mysync->{ total_bytes_skipped } += $h1_size ; + $mysync->{ nb_msg_skipped } += 1 ; + $mysync->{ h1_nb_msg_processed } +=1 ; + return ; +} + +sub copy_message +{ # copy my ( $mysync, $h1_msg, $h1_fold, $h2_fold, $h1_fir_ref, $permanentflags2, $cache_dir ) = @_ ; - ( $debug or $mysync->{dry}) and myprint( "msg $h1_fold/$h1_msg copying to $h2_fold $mysync->{dry_message}\n" ) ; + ( $mysync->{ debug } or $mysync->{dry}) and myprint( "msg $h1_fold/$h1_msg copying to $h2_fold $mysync->{dry_message}\n" ) ; my $h1_size = $h1_fir_ref->{$h1_msg}->{'RFC822.SIZE'} || 0 ; my $h1_flags = $h1_fir_ref->{$h1_msg}->{'FLAGS'} || q{} ; @@ -6432,33 +7998,28 @@ sub copy_message { if ( size_filtered( $h1_size, $h1_msg, $h1_fold, $h2_fold ) ) { - $h1_nb_msg_processed +=1 ; + $mysync->{ h1_nb_msg_processed } +=1 ; return ; } debugsleep( $mysync ) ; 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 ; + if ( $checkmessageexists and not message_exists( $mysync->{imap1}, $h1_msg ) ) { + stats_update_skip_message( $mysync, $h1_size ) ; return ; } - myprint( debugmemory( $sync, " at C1" ) ) ; + myprint( debugmemory( $mysync, " at C1" ) ) ; my ( $string, $string_len ) ; ( $string_len ) = message_for_host2( $mysync, $h1_msg, $h1_fold, $h1_size, $h1_flags, $h1_idate, $h1_fir_ref, \$string ) ; - myprint( debugmemory( $sync, " at C2" ) ) ; + myprint( debugmemory( $mysync, " at C2" ) ) ; # not defined or empty $string if ( ( not $string ) or ( 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 ; + stats_update_skip_message( $mysync, $h1_size ) ; return ; } @@ -6466,39 +8027,40 @@ sub copy_message { 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 ; + stats_update_skip_message( $mysync, $h1_size ) ; 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" ) ; + ( $mysync->{ 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" ) ; + ( $mysync->{ 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( $mysync, \$string, $h1_fold, $h1_msg, $string_len, $h2_fold, $h1_size, $h1_flags, $h1_date, $cache_dir ) ; - $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 ) ; + sync_flags_after_copy( $mysync, $h1_fold, $h1_msg, $h1_flags, $h2_fold, $new_id, $permanentflags2 ) ; } - myprint( debugmemory( $sync, " at C3" ) ) ; + myprint( debugmemory( $mysync, " at C3" ) ) ; return $new_id ; } -sub linelengthstuff { +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" ) ; @@ -6531,7 +8093,8 @@ sub linelengthstuff { } -sub message_for_host2 { +sub message_for_host2 +{ # global variable list: # @skipmess @@ -6555,16 +8118,19 @@ sub message_for_host2 { my ( $mysync, $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 (!defined $h1_idate) or (!$h1_fir_ref) or (!$string_ref) ) { + if ( ( ! $mysync ) or ( ! $h1_msg ) or ( ! $h1_fold ) or ( ! defined $h1_size ) + or ( ! defined $h1_flags) or ( ! defined $h1_idate ) + or ( ! $h1_fir_ref) or ( ! $string_ref ) ) + { return ; } - myprint( debugmemory( $sync, " at M1" ) ) ; + myprint( debugmemory( $mysync, " at M1" ) ) ; - my $imap1 = $mysync->{imap1} ; - my $string_ok = $imap1->message_to_file( $string_ref, $h1_msg ) ; - myprint( debugmemory( $sync, " at M2" ) ) ; + my $string_ok = $mysync->{imap1}->message_to_file( $string_ref, $h1_msg ) ; + + myprint( debugmemory( $mysync, " at M2" ) ) ; my $string_len = length_ref( $string_ref ) ; @@ -6573,10 +8139,9 @@ sub message_for_host2 { # 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" ; + $mysync->{imap1}->LastError || q{}, "\n" ; errors_incr( $mysync, $error ) ; - $total_bytes_error += $h1_size if ( $h1_size ) ; - $h1_nb_msg_processed +=1 ; + $mysync->{ h1_nb_msg_processed } +=1 ; return ; } @@ -6621,7 +8186,7 @@ sub message_for_host2 { if ( $mysync->{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" ) ; + $mysync->{ debug } and myprint( "msg $h1_fold/$h1_msg adding custom header [$header]\n" ) ; ${ $string_ref } = $header . "\r\n" . ${ $string_ref } ; } @@ -6633,12 +8198,15 @@ sub message_for_host2 { ${ $string_ref }, "F message content ended on previous line\n", q{=} x $STD_CHAR_PER_LINE, "\n" ) ; - myprint( debugmemory( $sync, " at M3" ) ) ; + myprint( debugmemory( $mysync, " at M3" ) ) ; return $string_len ; } -sub tests_message_for_host2 { + + +sub tests_message_for_host2 +{ note( 'Entering tests_message_for_host2()' ) ; @@ -6647,7 +8215,7 @@ sub tests_message_for_host2 { is( undef, message_for_host2( ), q{message_for_host2: no args} ) ; is( undef, message_for_host2( $mysync, $h1_msg, $h1_fold, $h1_size, $h1_flags, $h1_idate, $h1_fir_ref, $string_ref ), q{message_for_host2: undef args} ) ; - require Test::MockObject ; + require_ok( "Test::MockObject" ) ; my $imapT = Test::MockObject->new( ) ; $mysync->{imap1} = $imapT ; my $string ; @@ -6711,13 +8279,496 @@ sub tests_message_for_host2 { return ; } -sub length_ref { +sub tests_labels_remove_subfolder1 +{ + note( 'Entering tests_labels_remove_subfolder1()' ) ; + is( undef, labels_remove_subfolder1( ), 'labels_remove_subfolder1: no parameters => undef' ) ; + is( 'Blabla', labels_remove_subfolder1( 'Blabla' ), 'labels_remove_subfolder1: one parameter Blabla => Blabla' ) ; + is( 'Blan blue', labels_remove_subfolder1( 'Blan blue' ), 'labels_remove_subfolder1: one parameter Blan blue => Blan blue' ) ; + is( '\Bla "Blan blan" Blabla', labels_remove_subfolder1( '\Bla "Blan blan" Blabla' ), + 'labels_remove_subfolder1: one parameter \Bla "Blan blan" Blabla => \Bla "Blan blan" Blabla' ) ; + + is( 'Bla', labels_remove_subfolder1( 'Subf/Bla', 'Subf' ), 'labels_remove_subfolder1: Subf/Bla Subf => "Bla"' ) ; + + + is( '"\\\\Bla"', labels_remove_subfolder1( '"\\\\Bla"', 'Subf' ), 'labels_remove_subfolder1: "\\\\Bla" Subf => "\\\\Bla"' ) ; + + is( 'Bla Kii', labels_remove_subfolder1( 'Subf/Bla Subf/Kii', 'Subf' ), + 'labels_remove_subfolder1: Subf/Bla Subf/Kii, Subf => "Bla" "Kii"' ) ; + + is( '"\\\\Bla" Kii', labels_remove_subfolder1( '"\\\\Bla" Subf/Kii', 'Subf' ), + 'labels_remove_subfolder1: "\\\\Bla" Subf/Kii Subf => "\\\\Bla" Kii' ) ; + + is( '"Blan blan"', labels_remove_subfolder1( '"Subf/Blan blan"', 'Subf' ), + 'labels_remove_subfolder1: "Subf/Blan blan" Subf => "Blan blan"' ) ; + + is( '"\\\\Loo" "Blan blan" Kii', labels_remove_subfolder1( '"\\\\Loo" "Subf/Blan blan" Subf/Kii', 'Subf' ), + 'labels_remove_subfolder1: "\\\\Loo" "Subf/Blan blan" Subf/Kii + Subf => "\\\\Loo" "Blan blan" Kii' ) ; + + is( '"\\\\Inbox"', labels_remove_subfolder1( 'Subf/INBOX', 'Subf' ), + 'labels_remove_subfolder1: Subf/INBOX + Subf => "\\\\Inbox"' ) ; + + is( '"\\\\Loo" "Blan blan" Kii "\\\\Inbox"', labels_remove_subfolder1( '"\\\\Loo" "Subf/Blan blan" Subf/Kii Subf/INBOX', 'Subf' ), + 'labels_remove_subfolder1: "\\\\Loo" "Subf/Blan blan" Subf/Kii Subf/INBOX + Subf => "\\\\Loo" "Blan blan" Kii "\\\\Inbox"' ) ; + + + note( 'Leaving tests_labels_remove_subfolder1()' ) ; + return ; +} + + + +sub labels_remove_subfolder1 +{ + my $labels = shift ; + my $subfolder1 = shift ; + + if ( not defined $labels ) { return ; } + if ( not defined $subfolder1 ) { return $labels ; } + + my @labels = quotewords('\s+', 1, $labels ) ; + #myprint( "@labels\n" ) ; + my @labels_subfolder2 ; + + foreach my $label ( @labels ) + { + if ( $label =~ m{zzzzzzzzzz} ) + { + # \Seen \Deleted ... stay the same + push @labels_subfolder2, $label ; + } + else + { + # Remove surrounding quotes if any, to add them again in case of space + $label = join( '', quotewords('\s+', 0, $label ) ) ; + $label =~ s{$subfolder1/?}{} ; + if ( 'INBOX' eq $label ) + { + push @labels_subfolder2, q{"\\\\Inbox"} ; + } + elsif ( $label =~ m{\\} ) + { + push @labels_subfolder2, qq{"\\$label"} ; + } + elsif ( $label =~ m{ } ) + { + push @labels_subfolder2, qq{"$label"} ; + } + else + { + push @labels_subfolder2, $label ; + } + } + } + + my $labels_subfolder2 = join( ' ', sort uniq( @labels_subfolder2 ) ) ; + + return $labels_subfolder2 ; +} + +sub tests_labels_remove_special +{ + note( 'Entering tests_labels_remove_special()' ) ; + + is( undef, labels_remove_special( ), 'labels_remove_special: no parameters => undef' ) ; + is( '', labels_remove_special( '' ), 'labels_remove_special: empty string => empty string' ) ; + is( '', labels_remove_special( '"\\\\Inbox"' ), 'labels_remove_special:"\\\\Inbox" => empty string' ) ; + is( '', labels_remove_special( '"\\\\Inbox" "\\\\Starred"' ), 'labels_remove_special:"\\\\Inbox" "\\\\Starred" => empty string' ) ; + is( 'Bar Foo', labels_remove_special( 'Foo Bar' ), 'labels_remove_special:Foo Bar => Bar Foo' ) ; + is( 'Bar Foo', labels_remove_special( 'Foo Bar "\\\\Inbox"' ), 'labels_remove_special:Foo Bar "\\\\Inbox" => Bar Foo' ) ; + note( 'Leaving tests_labels_remove_special()' ) ; + return ; +} + + + + +sub labels_remove_special +{ + my $labels = shift ; + + if ( not defined $labels ) { return ; } + + my @labels = quotewords('\s+', 1, $labels ) ; + myprint( "labels before remove_non_folded: @labels\n" ) ; + my @labels_remove_special ; + + foreach my $label ( @labels ) + { + if ( $label =~ m{^\"\\\\} ) + { + # not kept + } + else + { + push @labels_remove_special, $label ; + } + } + + my $labels_remove_special = join( ' ', sort @labels_remove_special ) ; + + return $labels_remove_special ; +} + + +sub tests_labels_add_subfolder2 +{ + note( 'Entering tests_labels_add_subfolder2()' ) ; + is( undef, labels_add_subfolder2( ), 'labels_add_subfolder2: no parameters => undef' ) ; + is( 'Blabla', labels_add_subfolder2( 'Blabla' ), 'labels_add_subfolder2: one parameter Blabla => Blabla' ) ; + is( 'Blan blue', labels_add_subfolder2( 'Blan blue' ), 'labels_add_subfolder2: one parameter Blan blue => Blan blue' ) ; + is( '\Bla "Blan blan" Blabla', labels_add_subfolder2( '\Bla "Blan blan" Blabla' ), + 'labels_add_subfolder2: one parameter \Bla "Blan blan" Blabla => \Bla "Blan blan" Blabla' ) ; + + is( 'Subf/Bla', labels_add_subfolder2( 'Bla', 'Subf' ), 'labels_add_subfolder2: Bla Subf => "Subf/Bla"' ) ; + + + is( 'Subf/\Bla', labels_add_subfolder2( '\\\\Bla', 'Subf' ), 'labels_add_subfolder2: \Bla Subf => \Bla' ) ; + + is( 'Subf/Bla Subf/Kii', labels_add_subfolder2( 'Bla Kii', 'Subf' ), + 'labels_add_subfolder2: Bla Kii Subf => "Subf/Bla" "Subf/Kii"' ) ; + + is( 'Subf/Kii Subf/\Bla', labels_add_subfolder2( '\\\\Bla Kii', 'Subf' ), + 'labels_add_subfolder2: \Bla Kii Subf => \Bla Subf/Kii' ) ; + + is( '"Subf/Blan blan"', labels_add_subfolder2( '"Blan blan"', 'Subf' ), + 'labels_add_subfolder2: "Blan blan" Subf => "Subf/Blan blan"' ) ; + + is( '"Subf/Blan blan" Subf/Kii Subf/\Loo', labels_add_subfolder2( '\\\\Loo "Blan blan" Kii', 'Subf' ), + 'labels_add_subfolder2: \Loo "Blan blan" Kii + Subf => "Subf/Blan blan" Subf/Kii Subf/\Loo' ) ; + + # "\\Inbox" is special, add to subfolder INBOX also because Gmail will but ... + is( '"Subf/\\\\Inbox" Subf/INBOX', labels_add_subfolder2( '"\\\\Inbox"', 'Subf' ), + 'labels_add_subfolder2: "\\\\Inbox" Subf => "Subf/\\\\Inbox" Subf/INBOX' ) ; + + # but not with INBOX folder + is( '"Subf/\\\\Inbox"', labels_add_subfolder2( '"\\\\Inbox"', 'Subf', 'INBOX' ), + 'labels_add_subfolder2: "\\\\Inbox" Subf INBOX => "Subf/\\\\Inbox"' ) ; + + # two times => one time + is( '"Subf/\\\\Inbox" Subf/INBOX', labels_add_subfolder2( '"\\\\Inbox" "\\\\Inbox"', 'Subf' ), + 'labels_add_subfolder2: "\\\\Inbox" "\\\\Inbox" Subf => "Subf/\\\\Inbox"' ) ; + + is( '"Subf/\\\\Starred"', labels_add_subfolder2( '"\\\\Starred"', 'Subf' ), + 'labels_add_subfolder2: "\\\\Starred" Subf => "Subf/\\\\Starred"' ) ; + + note( 'Leaving tests_labels_add_subfolder2()' ) ; + return ; +} + +sub labels_add_subfolder2 +{ + my $labels = shift ; + my $subfolder2 = shift ; + my $h1_folder = shift || q{} ; + + if ( not defined $labels ) { return ; } + if ( not defined $subfolder2 ) { return $labels ; } + + # Isn't it messy? + if ( 'INBOX' eq $h1_folder ) + { + $labels .= ' "\\\\Inbox"' ; + } + + my @labels = uniq( quotewords('\s+', 1, $labels ) ) ; + myprint( "labels before subfolder2: @labels\n" ) ; + my @labels_subfolder2 ; + + + foreach my $label ( @labels ) + { + # Isn't it more messy? + if ( ( q{"\\\\Inbox"} eq $label ) and ( 'INBOX' ne $h1_folder ) ) + { + if ( $subfolder2 =~ m{ } ) + { + push @labels_subfolder2, qq{"$subfolder2/INBOX"} ; + } + else + { + push @labels_subfolder2, "$subfolder2/INBOX" ; + } + } + if ( $label =~ m{^\"\\\\} ) + { + # \Seen \Deleted ... stay the same + #push @labels_subfolder2, $label ; + # Remove surrounding quotes if any, to add them again + $label = join( '', quotewords('\s+', 0, $label ) ) ; + push @labels_subfolder2, qq{"$subfolder2/\\$label"} ; + + } + else + { + # Remove surrounding quotes if any, to add them again in case of space + $label = join( '', quotewords('\s+', 0, $label ) ) ; + if ( $label =~ m{ } ) + { + push @labels_subfolder2, qq{"$subfolder2/$label"} ; + } + else + { + push @labels_subfolder2, "$subfolder2/$label" ; + } + } + } + + my $labels_subfolder2 = join( ' ', sort @labels_subfolder2 ) ; + + return $labels_subfolder2 ; +} + +sub tests_labels +{ + note( 'Entering tests_labels()' ) ; + + is( undef, labels( ), 'labels: no parameters => undef' ) ; + is( undef, labels( undef ), 'labels: undef => undef' ) ; + require_ok( "Test::MockObject" ) ; + my $myimap = Test::MockObject->new( ) ; + + $myimap->mock( 'fetch_hash', + sub { + return( + { '1' => { + 'X-GM-LABELS' => '\Seen Blabla' + } + } + ) ; + } + ) ; + $myimap->mock( 'Debug' , sub { } ) ; + $myimap->mock( 'Unescape', sub { return Mail::IMAPClient::Unescape( @_ ) } ) ; # real one + + is( undef, labels( $myimap ), 'labels: one parameter => undef' ) ; + is( '\Seen Blabla', labels( $myimap, '1' ), 'labels: $mysync UID_1 => \Seen Blabla' ) ; + + note( 'Leaving tests_labels()' ) ; + return ; +} + +sub labels +{ + my ( $myimap, $uid ) = @ARG ; + + if ( not all_defined( $myimap, $uid ) ) { + return ; + } + + my $hash = $myimap->fetch_hash( [ $uid ], 'X-GM-LABELS' ) ; + + my $labels = $hash->{ $uid }->{ 'X-GM-LABELS' } ; + #$labels = $myimap->Unescape( $labels ) ; + return $labels ; +} + +sub tests_synclabels +{ + note( 'Entering tests_synclabels()' ) ; + + is( undef, synclabels( ), 'synclabels: no parameters => undef' ) ; + is( undef, synclabels( undef ), 'synclabels: undef => undef' ) ; + my $mysync ; + is( undef, synclabels( $mysync ), 'synclabels: var undef => undef' ) ; + + require_ok( "Test::MockObject" ) ; + $mysync = {} ; + + my $myimap1 = Test::MockObject->new( ) ; + $myimap1->mock( 'fetch_hash', + sub { + return( + { '1' => { + 'X-GM-LABELS' => '\Seen Blabla' + } + } + ) ; + } + ) ; + $myimap1->mock( 'Debug', sub { } ) ; + $myimap1->mock( 'Unescape', sub { return Mail::IMAPClient::Unescape( @_ ) } ) ; # real one + + my $myimap2 = Test::MockObject->new( ) ; + + $myimap2->mock( 'store', + sub { + return 1 ; + } + ) ; + + + $mysync->{imap1} = $myimap1 ; + $mysync->{imap2} = $myimap2 ; + + is( undef, synclabels( $mysync ), 'synclabels: fresh $mysync => undef' ) ; + + is( undef, synclabels( $mysync, '1' ), 'synclabels: $mysync UID_1 alone => undef' ) ; + is( 1, synclabels( $mysync, '1', '2' ), 'synclabels: $mysync UID_1 UID_2 => 1' ) ; + + note( 'Leaving tests_synclabels()' ) ; + return ; +} + + +sub synclabels +{ + my( $mysync, $uid1, $uid2 ) = @ARG ; + + if ( not all_defined( $mysync, $uid1, $uid2 ) ) { + return ; + } + my $myimap1 = $mysync->{ 'imap1' } || return ; + my $myimap2 = $mysync->{ 'imap2' } || return ; + + $mysync->{debuglabels} and $myimap1->Debug( 1 ) ; + my $labels1 = labels( $myimap1, $uid1 ) ; + $mysync->{debuglabels} and $myimap1->Debug( 0 ) ; + $mysync->{debuglabels} and myprint( "Host1 labels: $labels1\n" ) ; + + + + if ( $mysync->{ subfolder1 } and $labels1 ) + { + $labels1 = labels_remove_subfolder1( $labels1, $mysync->{ subfolder1 } ) ; + $mysync->{debuglabels} and myprint( "Host1 labels with subfolder1: $labels1\n" ) ; + } + + if ( $mysync->{ subfolder2 } and $labels1 ) + { + $labels1 = labels_add_subfolder2( $labels1, $mysync->{ subfolder2 } ) ; + $mysync->{debuglabels} and myprint( "Host1 labels with subfolder2: $labels1\n" ) ; + } + + my $store ; + if ( $labels1 and not $mysync->{ dry } ) + { + $mysync->{ debuglabels } and $myimap2->Debug( 1 ) ; + $store = $myimap2->store( $uid2, "X-GM-LABELS ($labels1)" ) ; + $mysync->{ debuglabels } and $myimap2->Debug( 0 ) ; + } + return $store ; +} + + +sub tests_resynclabels +{ + note( 'Entering tests_resynclabels()' ) ; + + is( undef, resynclabels( ), 'resynclabels: no parameters => undef' ) ; + is( undef, resynclabels( undef ), 'resynclabels: undef => undef' ) ; + my $mysync ; + is( undef, resynclabels( $mysync ), 'resynclabels: var undef => undef' ) ; + + my ( $h1_fir_ref, $h2_fir_ref ) ; + + $mysync->{ debuglabels } = 1 ; + $h1_fir_ref->{ 11 }->{ 'X-GM-LABELS' } = '\Seen Baa Kii' ; + $h2_fir_ref->{ 22 }->{ 'X-GM-LABELS' } = '\Seen Baa Kii' ; + + # labels are equal + is( 1, resynclabels( $mysync, 11, 22, $h1_fir_ref, $h2_fir_ref ), + 'resynclabels: $mysync UID_1 UID_2 labels are equal => 1' ) ; + + # labels are different + $h2_fir_ref->{ 22 }->{ 'X-GM-LABELS' } = '\Seen Zuu' ; + require_ok( "Test::MockObject" ) ; + my $myimap2 = Test::MockObject->new( ) ; + $myimap2->mock( 'store', + sub { + return 1 ; + } + ) ; + $myimap2->mock( 'Debug', sub { } ) ; + $mysync->{imap2} = $myimap2 ; + + is( 1, resynclabels( $mysync, 11, 22, $h1_fir_ref, $h2_fir_ref ), + 'resynclabels: $mysync UID_1 UID_2 labels are not equal => store => 1' ) ; + + note( 'Leaving tests_resynclabels()' ) ; + return ; +} + + + +sub resynclabels +{ + my( $mysync, $uid1, $uid2, $h1_fir_ref, $h2_fir_ref, $h1_folder ) = @ARG ; + + if ( not all_defined( $mysync, $uid1, $uid2, $h1_fir_ref, $h2_fir_ref ) ) { + return ; + } + + my $labels1 = $h1_fir_ref->{ $uid1 }->{ 'X-GM-LABELS' } || q{} ; + my $labels2 = $h2_fir_ref->{ $uid2 }->{ 'X-GM-LABELS' } || q{} ; + + if ( $mysync->{ subfolder1 } and $labels1 ) + { + $labels1 = labels_remove_subfolder1( $labels1, $mysync->{ subfolder1 } ) ; + } + + if ( $mysync->{ subfolder2 } and $labels1 ) + { + $labels1 = labels_add_subfolder2( $labels1, $mysync->{ subfolder2 }, $h1_folder ) ; + $labels2 = labels_remove_special( $labels2 ) ; + } + $mysync->{ debuglabels } and myprint( "Host1 labels fixed: $labels1\n" ) ; + $mysync->{ debuglabels } and myprint( "Host2 labels : $labels2\n" ) ; + + my $store ; + if ( $labels1 eq $labels2 ) + { + # no sync needed + $mysync->{ debuglabels } and myprint( "Labels are already equal\n" ) ; + return 1 ; + } + elsif ( not $mysync->{ dry } ) + { + # sync needed + $mysync->{debuglabels} and $mysync->{imap2}->Debug( 1 ) ; + $store = $mysync->{imap2}->store( $uid2, "X-GM-LABELS ($labels1)" ) ; + $mysync->{debuglabels} and $mysync->{imap2}->Debug( 0 ) ; + } + + return $store ; +} + +sub tests_uniq +{ + note( 'Entering tests_uniq()' ) ; + + is( 0, uniq( ), 'uniq: undef => 0' ) ; + is_deeply( [ 'one' ], [ uniq( 'one' ) ], 'uniq: one => one' ) ; + is_deeply( [ 'one' ], [ uniq( 'one', 'one' ) ], 'uniq: one one => one' ) ; + is_deeply( [ 'one', 'two' ], [ uniq( 'one', 'one', 'two', 'one', 'two' ) ], 'uniq: one one two one two => one two' ) ; + note( 'Leaving tests_uniq()' ) ; + return ; +} + +sub uniq +{ + my @list = @ARG ; + my %seen = ( ) ; + my @uniq = ( ) ; + foreach my $item ( @list ) { + if ( ! $seen{ $item } ) { + $seen{ $item } = 1 ; + push( @uniq, $item ) ; + } + } + return @uniq ; +} + + +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 { +sub tests_length_ref +{ note( 'Entering tests_length_ref()' ) ; my $notdefined ; @@ -6733,29 +8784,31 @@ sub tests_length_ref { return ; } -sub date_for_host2 { +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" ) ; + $sync->{ 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" ) ; + $sync->{ 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 = $sync->{imap1}->get_header( $h1_msg, 'Date' ) ; + $sync->{ 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" ) ; + $sync->{ debug } and myprint( "header date from host1: [$h1_date] (fixed)\n" ) ; } return( $h1_date ) ; } -sub flags_for_host2 { +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 ; @@ -6769,7 +8822,8 @@ sub flags_for_host2 { return( $h1_flags ) ; } -sub subject { +sub subject +{ my $string = shift ; my $subject = q{} ; @@ -6782,7 +8836,8 @@ sub subject { return( $subject ) ; } -sub tests_subject { +sub tests_subject +{ note( 'Entering tests_subject()' ) ; ok( q{} eq subject( q{} ), 'subject: null') ; @@ -6833,32 +8888,27 @@ EOF # GlobVar -# $sync # $max_msg_size_in_bytes -# $imap2 -# $imap1 -# $total_bytes_error -# $h1_nb_msg_processed # $h2_uidguess # ... # # -sub append_message_on_host2 { - my( $string_ref, $h1_fold, $h1_msg, $string_len, $h2_fold, $h1_size, $h1_flags, $h1_date, $cache_dir ) = @_ ; - myprint( debugmemory( $sync, " at A1" ) ) ; +sub append_message_on_host2 +{ + my( $mysync, $string_ref, $h1_fold, $h1_msg, $string_len, $h2_fold, $h1_size, $h1_flags, $h1_date, $cache_dir ) = @_ ; + myprint( debugmemory( $mysync, " at A1" ) ) ; my $new_id ; - if ( ! $sync->{dry} ) { + if ( ! $mysync->{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 ) ; - myprint( debugmemory( $sync, " at A2" ) ) ; + $new_id = $mysync->{imap2}->append_string( $h2_fold, ${ $string_ref }, $h1_flags, $h1_date ) ; + myprint( debugmemory( $mysync, " at A2" ) ) ; if ( ! $new_id){ my $subject = subject( ${ $string_ref } ) ; - my $error_imap = $imap2->LastError || q{} ; - my $error = "- msg $h1_fold/$h1_msg {$string_len} could not append ( Subject:[$subject], Date:[$h1_date], Size:[$h1_size] ) to folder $h2_fold: $error_imap\n" ; - errors_incr( $sync, $error ) ; - $total_bytes_error += $h1_size; - $h1_nb_msg_processed +=1 ; + my $error_imap = $mysync->{imap2}->LastError || q{} ; + my $error = "- msg $h1_fold/$h1_msg {$string_len} could not append ( Subject:[$subject], Date:[$h1_date], Size:[$h1_size], Flags:[$h1_flags] ) to folder $h2_fold: $error_imap\n" ; + errors_incr( $mysync, $error ) ; + $mysync->{ h1_nb_msg_processed } +=1 ; return ; } else{ @@ -6866,44 +8916,47 @@ sub append_message_on_host2 { # $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 ) ; + $new_id = lastuid( $mysync->{imap2}, $h2_fold, $h2_uidguess ) ; } + if ( $mysync->{ synclabels } ) { synclabels( $mysync, $h1_msg, $new_id ) } $h2_uidguess += 1 ; - $sync->{total_bytes_transferred} += $h1_size ; - $sync->{nb_msg_transferred} += 1 ; - $h1_nb_msg_processed +=1 ; + $mysync->{total_bytes_transferred} += $h1_size ; + $mysync->{nb_msg_transferred} += 1 ; + $mysync->{ h1_nb_msg_processed } +=1 ; - my $time_spent = timesince( $sync->{begin_transfer_time} ) ; - my $rate = bytes_display_string( $sync->{total_bytes_transferred} / $time_spent ) ; + my $time_spent = timesince( $mysync->{begin_transfer_time} ) ; + my $rate = bytes_display_string( $mysync->{total_bytes_transferred} / $time_spent ) ; my $eta = eta( $time_spent, - $h1_nb_msg_processed, $h1_nb_msg_start, $sync->{nb_msg_transferred} ) ; - my $amount_transferred = bytes_display_string( $sync->{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, $sync->{nb_msg_transferred}/$time_spent, $rate, + $mysync->{ h1_nb_msg_processed }, $h1_nb_msg_start, $mysync->{nb_msg_transferred} ) ; + my $amount_transferred = bytes_display_string( $mysync->{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, $mysync->{nb_msg_transferred}/$time_spent, $rate, $amount_transferred, $eta ); - sleep_if_needed( $sync ) ; + sleep_if_needed( $mysync ) ; 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 ( $delete1 ) { - delete_message_on_host1( $h1_msg, $h1_fold ) ; + if ( $mysync->{ delete1 } ) { + delete_message_on_host1( $mysync, $h1_fold, $mysync->{ expungeaftereach }, $h1_msg ) ; } #myprint( "PRESS ENTER" ) and my $a = <> ; + return( $new_id ) ; } } else{ $nb_msg_skipped_dry_mode += 1 ; - $h1_nb_msg_processed +=1 ; + $mysync->{ h1_nb_msg_processed } +=1 ; } return ; } -sub tests_sleep_if_needed { +sub tests_sleep_if_needed +{ note( 'Entering tests_sleep_if_needed()' ) ; is( undef, sleep_if_needed( ), 'sleep_if_needed: no args => undef' ) ; @@ -6925,7 +8978,7 @@ sub tests_sleep_if_needed { $mysync->{maxsleep} = 0.1 ; $mysync->{begin_transfer_time} = time - 2 ; # 2 s before again is( '0.10', sleep_if_needed( $mysync ), 'sleep_if_needed: total_bytes_transferred == 4000 since 2s but maxsleep 0.1s => sleep 0.1s' ) ; - + $mysync->{maxbytesafter} = 4000 ; $mysync->{begin_transfer_time} = time - 2 ; # 2 s before again is( 0, sleep_if_needed( $mysync ), 'sleep_if_needed: maxbytesafter == total_bytes_transferred => no sleep => 0' ) ; @@ -6935,7 +8988,8 @@ sub tests_sleep_if_needed { } -sub sleep_if_needed { +sub sleep_if_needed +{ my( $mysync ) = shift ; if ( ! $mysync ) { @@ -6947,11 +9001,11 @@ sub sleep_if_needed { ) { return ; } - + $mysync->{maxsleep} = defined $mysync->{maxsleep} ? $mysync->{maxsleep} : $MAX_SLEEP ; # Must be positive $mysync->{maxsleep} = max( 0, $mysync->{maxsleep} ) ; - + my $time_spent = timesince( $mysync->{begin_transfer_time} ) ; my $sleep_max_messages = sleep_max_messages( $mysync->{nb_msg_transferred}, $time_spent, $mysync->{maxmessagespersecond} ) ; @@ -6975,7 +9029,8 @@ sub sleep_if_needed { return 0 ; } -sub sleep_max_messages { +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 ) } ; @@ -6985,7 +9040,8 @@ sub sleep_max_messages { } -sub tests_sleep_max_messages { +sub tests_sleep_max_messages +{ note( 'Entering tests_sleep_max_messages()' ) ; ok( 0 == sleep_max_messages( 4, 2, undef ), 'sleep_max_messages: maxmessagespersecond = undef') ; @@ -7000,7 +9056,8 @@ sub tests_sleep_max_messages { } -sub sleep_max_bytes { +sub sleep_max_bytes +{ # how long we have to sleep to go under max_bytes_per_second my( $total_bytes_to_consider, $time_spent, $maxbytespersecond ) = @_ ; $total_bytes_to_consider ||= 0 ; @@ -7014,7 +9071,8 @@ sub sleep_max_bytes { } -sub tests_sleep_max_bytes { +sub tests_sleep_max_bytes +{ note( 'Entering tests_sleep_max_bytes()' ) ; ok( 0 == sleep_max_bytes( 4000, 2, undef ), 'sleep_max_bytes: maxbytespersecond == undef => sleep 0' ) ; @@ -7031,24 +9089,154 @@ sub tests_sleep_max_bytes { } +sub delete_message_on_host1 +{ + my( $mysync, $h1_fold, $expunge, @h1_msg ) = @_ ; + if ( ! $mysync->{ delete1 } ) { return ; } + if ( ! @h1_msg ) { return ; } + delete_messages_on_any( + $mysync, + $mysync->{imap1}, + "Host1: $h1_fold", + $expunge, + $split1, + @h1_msg ) ; + return ; +} +sub tests_operators_and_exclam_precedence +{ + note( 'Entering tests_operators_and_exclam_precedence()' ) ; -# 6 GlobVar: $sync $imap1 $h1_nb_msg_deleted $expunge1 -sub delete_message_on_host1 { - my( $h1_msg, $h1_fold ) = @_ ; + is( 1, ! 0, 'tests_operators_and_exclam_precedence: ! 0 => 1' ) ; + is( "", ! 1, 'tests_operators_and_exclam_precedence: ! 1 => ""' ) ; + is( 1, not( 0 ), 'tests_operators_and_exclam_precedence: not( 0 ) => 1' ) ; + is( "", not( 1 ), 'tests_operators_and_exclam_precedence: not( 1 ) => ""' ) ; + + # I wrote those tests to avoid perlcrit "Mixed high and low-precedence booleans" + # and change sub delete_messages_on_any() but got 4 more warnings... So now commented. + + #is( 0, ( ! 0 and 0 ), 'tests_operators_and_exclam_precedence: ! 0 and 0 ) => 0' ) ; + #is( 1, ( ! 0 and 1 ), 'tests_operators_and_exclam_precedence: ! 0 and 1 ) => 1' ) ; + #is( "", ( ! 1 and 0 ), 'tests_operators_and_exclam_precedence: ! 1 and 0 ) => ""' ) ; + #is( "", ( ! 1 and 1 ), 'tests_operators_and_exclam_precedence: ! 1 and 1 ) => ""' ) ; + + is( 0, ( ! 0 && 0 ), 'tests_operators_and_exclam_precedence: ! 0 && 0 ) => 0' ) ; + is( 1, ( ! 0 && 1 ), 'tests_operators_and_exclam_precedence: ! 0 && 1 ) => 1' ) ; + is( "", ( ! 1 && 0 ), 'tests_operators_and_exclam_precedence: ! 1 && 0 ) => ""' ) ; + is( "", ( ! 1 && 1 ), 'tests_operators_and_exclam_precedence: ! 1 && 1 ) => ""' ) ; + + is( 2, ( ! 0 && 2 ), 'tests_operators_and_exclam_precedence: ! 0 && 2 ) => 1' ) ; + + note( 'Leaving tests_operators_and_exclam_precedence()' ) ; + return ; +} + +sub delete_messages_on_any +{ + my( $mysync, $imap, $hostX_folder, $expunge, $split, @messages ) = @_ ; my $expunge_message = q{} ; - $expunge_message = 'and expunged' if ( $expungeaftereach and $expunge1 ) ; - myprint( "Host1 msg $h1_fold/$h1_msg marked deleted $expunge_message $sync->{dry_message}\n" ) ; - if ( ! $sync->{dry} ) { - $imap1->delete_message( $h1_msg ) ; - $h1_nb_msg_deleted += 1 ; - $imap1->expunge( ) if ( $expungeaftereach and $expunge1 ) ; + + my $dry_message = $mysync->{ dry_message } ; + $expunge_message = 'and expunged' if ( $expunge ) ; + # "Host1: msg " + + $imap->Debug( 1 ) ; + + while ( my @messages_part = splice @messages, 0, $split ) + { + foreach my $message ( @messages_part ) + { + myprint( "$hostX_folder/$message marking deleted $expunge_message $dry_message\n" ) ; + } + if ( ! $mysync->{dry} && @messages_part ) + { + my $nb_deleted = $imap->delete_message( $imap->Range( @messages_part ) ) ; + if ( defined $nb_deleted ) + { + $mysync->{ h1_nb_msg_deleted } += $nb_deleted ; + } + else + { + my $error_imap = $imap->LastError || q{} ; + my $error = join( q{}, "$hostX_folder folder, could not delete ", + scalar @messages_part, ' messages: ', $error_imap, "\n" ) ; + errors_incr( $mysync, $error ) ; + } + } } + + if ( $expunge ) { + uidexpunge_or_expunge( $mysync, $imap, @messages ) ; + } + + $imap->Debug( 0 ) ; + return ; } -sub eta { +sub tests_uidexpunge_or_expunge +{ + note( 'Entering tests_uidexpunge_or_expunge()' ) ; + + + is( undef, uidexpunge_or_expunge( ), 'uidexpunge_or_expunge: no args => undef' ) ; + my $mysync ; + is( undef, uidexpunge_or_expunge( $mysync ), 'uidexpunge_or_expunge: undef args => undef' ) ; + $mysync = {} ; + is( undef, uidexpunge_or_expunge( $mysync ), 'uidexpunge_or_expunge: arg empty => undef' ) ; + my $imap ; + is( undef, uidexpunge_or_expunge( $mysync, $imap ), 'uidexpunge_or_expunge: undef Mail-IMAPClient instance => undef' ) ; + + require_ok( "Test::MockObject" ) ; + $imap = Test::MockObject->new( ) ; + is( undef, uidexpunge_or_expunge( $mysync, $imap ), 'uidexpunge_or_expunge: no message (1) to uidexpunge => undef' ) ; + + my @messages = ( ) ; + is( undef, uidexpunge_or_expunge( $mysync, $imap, @messages ), 'uidexpunge_or_expunge: no message (2) to uidexpunge => undef' ) ; + + @messages = ( '2', '1' ) ; + $imap->mock( 'uidexpunge', sub { return ; } ) ; + $imap->mock( 'expunge', sub { return ; } ) ; + is( undef, uidexpunge_or_expunge( $mysync, $imap, @messages ), 'uidexpunge_or_expunge: uidexpunge failure => expunge failure => undef' ) ; + + $imap->mock( 'expunge', sub { return 1 ; } ) ; + is( 1, uidexpunge_or_expunge( $mysync, $imap, @messages ), 'uidexpunge_or_expunge: uidexpunge failure => expunge ok => 1' ) ; + + $imap->mock( 'uidexpunge', sub { return 1 ; } ) ; + is( 1, uidexpunge_or_expunge( $mysync, $imap, @messages ), 'uidexpunge_or_expunge: messages to uidexpunge ok => 1' ) ; + + note( 'Leaving tests_uidexpunge_or_expunge()' ) ; + return ; +} + +sub uidexpunge_or_expunge +{ + my $mysync = shift ; + my $imap = shift ; + my @messages = @ARG ; + + if ( ! $imap ) { return ; } ; + if ( ! @messages ) { return ; } ; + + # Doing uidexpunge + my @uidexpunge_result = $imap->uidexpunge( @messages ) ; + if ( @uidexpunge_result ) { + return 1 ; + } + # Failure so doing expunge + my $expunge_result = $imap->expunge( ) ; + if ( $expunge_result ) { + return 1 ; + } + # bad trip + return ; +} + + +sub eta +{ my( $my_time_spent, $h1_nb_processed, $my_h1_nb_msg_start, $nb_transferred ) = @_ ; return( q{} ) if not $foldersizes ; @@ -7058,7 +9246,8 @@ sub eta { return( mysprintf( 'ETA: %s %1.0f s %s/%s msgs left', $eta_date, $time_remaining, $nb_msg_remaining, $my_h1_nb_msg_start ) ) ; } -sub time_remaining { +sub time_remaining +{ my( $my_time_spent, $h1_nb_processed, $my_h1_nb_msg_start, $nb_transferred ) = @_ ; @@ -7067,7 +9256,8 @@ sub time_remaining { } -sub tests_time_remaining { +sub tests_time_remaining +{ note( 'Entering tests_time_remaining()' ) ; @@ -7080,7 +9270,8 @@ sub tests_time_remaining { } -sub cache_map { +sub cache_map +{ my ( $cache_files_ref, $h1_msgs_ref, $h2_msgs_ref ) = @_; my ( %map1_2, %map2_1, %done2 ) ; @@ -7121,7 +9312,8 @@ sub cache_map { return( \%map1_2, \%map2_1) ; } -sub tests_cache_map { +sub tests_cache_map +{ note( 'Entering tests_cache_map()' ) ; #$debugcache = 1 ; @@ -7163,14 +9355,16 @@ sub tests_cache_map { } -sub cache_dir_fix { +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 { +sub tests_cache_dir_fix +{ note( 'Entering tests_cache_dir_fix()' ) ; ok( 'lalala' eq cache_dir_fix('lalala'), 'cache_dir_fix: lalala -> lalala' ); @@ -7187,14 +9381,16 @@ sub tests_cache_dir_fix { return ; } -sub cache_dir_fix_win { +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 { +sub tests_cache_dir_fix_win +{ note( 'Entering tests_cache_dir_fix_win()' ) ; ok( 'lalala' eq cache_dir_fix_win('lalala'), 'cache_dir_fix_win: lalala -> lalala' ); @@ -7207,13 +9403,14 @@ sub tests_cache_dir_fix_win { -sub get_cache { +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" ) ; + $debugcache and myprint( "cache_dir : $cache_dir\n" ) ; if ( 'MSWin32' ne $OSNAME ) { @@ -7239,7 +9436,8 @@ sub get_cache { } -sub tests_get_cache { +sub tests_get_cache +{ note( 'Entering tests_get_cache()' ) ; ok( not( get_cache('/cache_no_exist') ), 'get_cache: /cache_no_exist' ); @@ -7330,7 +9528,8 @@ sub tests_get_cache { return ; } -sub match_a_cache_file { +sub match_a_cache_file +{ my $file = shift ; my ( $cache_uid1, $cache_uid2 ) ; @@ -7342,7 +9541,8 @@ sub match_a_cache_file { return( $cache_uid1, $cache_uid2 ) ; } -sub tests_match_a_cache_file { +sub tests_match_a_cache_file +{ note( 'Entering tests_match_a_cache_file()' ) ; my ( $tuid1, $tuid2 ) ; @@ -7378,7 +9578,8 @@ sub tests_match_a_cache_file { return ; } -sub clean_cache { +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" ) ; @@ -7404,7 +9605,8 @@ sub clean_cache { return( 1 ) ; } -sub tests_clean_cache { +sub tests_clean_cache +{ note( 'Entering 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' ) ; @@ -7458,7 +9660,8 @@ sub tests_clean_cache { return ; } -sub tests_clean_cache_2 { +sub tests_clean_cache_2 +{ note( 'Entering 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' ) ; @@ -7518,19 +9721,20 @@ sub tests_clean_cache_2 { -sub tests_mkpath { +sub tests_mkpath +{ note( 'Entering tests_mkpath()' ) ; ok( (-d 'W/tmp/tests/' or mkpath( 'W/tmp/tests/' )), 'mkpath: mkpath W/tmp/tests/' ) ; - + SKIP: { skip( 'Tests only for Unix', 10 ) 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" ) ), 'mkpath: mkpath 300 char' ) ; + ok( ( -d "W/tmp/tests/long/$long_path_unix" or mkpath( "W/tmp/tests/long/$long_path_unix" ) ), 'mkpath: mkpath 300 char' ) ; ok( -d "W/tmp/tests/long/$long_path_unix", 'mkpath: mkpath > 300 char verified' ) ; ok( ( -d "W/tmp/tests/long/$long_path_unix" and rmtree( 'W/tmp/tests/long/' ) ), 'mkpath: rmtree 300 char' ) ; ok( ! -d "W/tmp/tests/long/$long_path_unix", 'mkpath: rmtree 300 char verified' ) ; - + ok( ( -d 'W/tmp/tests/trailing_dots...' or mkpath( 'W/tmp/tests/trailing_dots...' ) ), 'mkpath: mkpath trailing_dots...' ) ; ok( -d 'W/tmp/tests/trailing_dots...', 'mkpath: mkpath trailing_dots... verified' ) ; ok( ( -d 'W/tmp/tests/trailing_dots...' and rmtree( 'W/tmp/tests/trailing_dots...' ) ), 'mkpath: rmtree trailing_dots...' ) ; @@ -7559,7 +9763,7 @@ sub tests_mkpath { # Without the eval the following mkpath 300 just kill the whole process without a whisper #myprint( "$long_path_300\n" ) ; - eval { ok( ( -d $long_path_300 or mkpath( $long_path_300 ) ), 'mkpath: create a path with 300 characters' ) ; } + eval { ok( ( -d $long_path_300 or mkpath( $long_path_300 ) ), 'mkpath: create a path with 300 characters' ) ; } or ok( 1, 'mkpath: can not create a path with 300 characters' ) ; ok( ( ( ! -d $long_path_300 ) or -d $long_path_300 and rmtree( $long_path_300 ) ), 'mkpath: rmtree the 300 character path' ) ; ok( 1, 'mkpath: still alive' ) ; @@ -7568,8 +9772,8 @@ sub tests_mkpath { ok( -d 'W/tmp/tests/trailing_dots...', 'mkpath: mkpath trailing_dots... verified' ) ; ok( ( -d 'W/tmp/tests/trailing_dots...' and rmtree( 'W/tmp/tests/trailing_dots...' ) ), 'mkpath: rmtree trailing_dots...' ) ; ok( ! -d 'W/tmp/tests/trailing_dots...', 'mkpath: rmtree trailing_dots... verified' ) ; - - + + } ; note( 'Leaving tests_mkpath()' ) ; @@ -7577,7 +9781,8 @@ sub tests_mkpath { return 1 ; } -sub tests_touch { +sub tests_touch +{ note( 'Entering tests_touch()' ) ; ok( (-d 'W/tmp/tests/' or mkpath( 'W/tmp/tests/' )), 'touch: mkpath W/tmp/tests/' ) ; @@ -7592,7 +9797,8 @@ sub tests_touch { } -sub touch { +sub touch +{ my @files = @_ ; my $failures = 0 ; @@ -7609,7 +9815,8 @@ sub touch { } -sub tests_tmpdir_has_colon_bug { +sub tests_tmpdir_has_colon_bug +{ note( 'Entering tests_tmpdir_has_colon_bug()' ) ; ok( 0 == tmpdir_has_colon_bug( q{} ), 'tmpdir_has_colon_bug: ' ) ; @@ -7621,7 +9828,8 @@ sub tests_tmpdir_has_colon_bug { return ; } -sub tmpdir_has_colon_bug { +sub tmpdir_has_colon_bug +{ my $path = shift ; my $path_filtered = filter_forbidden_characters( $path ) ; @@ -7632,7 +9840,8 @@ sub tmpdir_has_colon_bug { return( 0 ) ; } -sub tmpdir_fix_colon_bug { +sub tmpdir_fix_colon_bug +{ my $mysync = shift ; my $err = 0 ; if ( not (-d $mysync->{ tmpdir } and -r _ and -w _) ) { @@ -7679,10 +9888,10 @@ sub tmpdir_fix_colon_bug { } -sub tests_cache_folder { +sub tests_cache_folder +{ note( 'Entering 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' ) ; @@ -7697,11 +9906,12 @@ sub tests_cache_folder { return ; } -sub cache_folder { +sub cache_folder +{ my( $cache_base, $cache_dir, $h1_fold, $h2_fold ) = @_ ; - my $sep_1 = $h1_sep || '/'; - my $sep_2 = $h2_sep || '/'; + my $sep_1 = $sync->{ h1_sep } || '/'; + my $sep_2 = $sync->{ 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 ) ; @@ -7712,24 +9922,10 @@ sub cache_folder { return( $cache_folder ) ; } -sub filter_forbidden_characters { - my $string = shift ; - - if ( ! defined $string ) { return ; } - - if ( 'MSWin32' eq $OSNAME ) { - # Move trailing whitespace to _ " a b /c d " -> " a b_/c d_" - $string =~ s{\ (/|$)}{_$1}xg ; - } - $string =~ s{[\Q*|?:"<>\E\t\r\n\\]}{_}xg ; - #myprint( "[$string]\n" ) ; - return( $string ) ; -} - -sub tests_filter_forbidden_characters { +sub tests_filter_forbidden_characters +{ note( 'Entering 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' ) ; @@ -7756,14 +9952,23 @@ sub tests_filter_forbidden_characters { return ; } -sub convert_sep_to_slash { - my ( $folder, $sep ) = @_ ; +sub filter_forbidden_characters +{ + my $string = shift ; - $folder =~ s{\Q$sep\E}{/}xg ; - return( $folder ) ; + if ( ! defined $string ) { return ; } + + if ( 'MSWin32' eq $OSNAME ) { + # Move trailing whitespace to _ " a b /c d " -> " a b_/c d_" + $string =~ s{\ (/|$)}{_$1}xg ; + } + $string =~ s{[\Q*|?:"<>\E\t\r\n\\]}{_}xg ; + #myprint( "[$string]\n" ) ; + return( $string ) ; } -sub tests_convert_sep_to_slash { +sub tests_convert_sep_to_slash +{ note( 'Entering tests_convert_sep_to_slash()' ) ; @@ -7779,8 +9984,17 @@ sub tests_convert_sep_to_slash { return ; } +sub convert_sep_to_slash +{ + my ( $folder, $sep ) = @_ ; -sub tests_regexmess { + $folder =~ s{\Q$sep\E}{/}xg ; + return( $folder ) ; +} + + +sub tests_regexmess +{ note( 'Entering tests_regexmess()' ) ; ok( 'blabla' eq regexmess( 'blabla' ), 'regexmess, no regexmess, nothing to do' ) ; @@ -7826,7 +10040,7 @@ sub tests_regexmess { eq regexmess("\n" . 'From <tartanpion@machin.truc>'), 'From mbox 3 remove'); - #myprint( "[", regexmess("From zzz\n" . 'From <tartanpion@machin.truc>'), "]" ) ; + #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'); @@ -8266,23 +10480,25 @@ EOM } -sub regexmess { +sub regexmess +{ my ( $string ) = @_ ; foreach my $regexmess ( @regexmess ) { - $debug and myprint( "eval \$string =~ $regexmess\n" ) ; + $sync->{ debug } and myprint( "eval \$string =~ $regexmess\n" ) ; my $ret = eval "\$string =~ $regexmess ; 1" ; - #myprint( "eval [$ret]\n" ) ; + #myprint( "eval [$ret]\n" ) ; if ( ( not $ret ) or $EVAL_ERROR ) { - myprint( "Error: eval regexmess '$regexmess': $EVAL_ERROR" ) ; + myprint( "Error: eval regexmess '$regexmess': $EVAL_ERROR" ) ; return( undef ) ; } } - $debug and myprint( "$string\n" ) ; + $sync->{ debug } and myprint( "$string\n" ) ; return( $string ) ; } -sub tests_skipmess { +sub tests_skipmess +{ note( 'Entering tests_skipmess()' ) ; ok( not( defined skipmess( 'blabla' ) ), 'skipmess, no skipmess, no skip' ) ; @@ -8503,17 +10719,18 @@ EOM return ; } -sub skipmess { +sub skipmess +{ my ( $string ) = @_ ; my $match ; - #myprint( "$string\n" ) ; + #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" ) ; + $sync->{ debug } and myprint( "eval \$match = \$string =~ $skipmess\n" ) ; + my $ret = eval "\$match = \$string =~ $skipmess ; 1" ; + #myprint( "eval [$ret]\n" ) ; + $sync->{ debug } and myprint( "match [$match]\n" ) ; if ( ( not $ret ) or $EVAL_ERROR ) { - myprint( "Error: eval skipmess '$skipmess': $EVAL_ERROR" ) ; + myprint( "Error: eval skipmess '$skipmess': $EVAL_ERROR" ) ; return( undef ) ; } return( $match ) if ( $match ) ; @@ -8524,7 +10741,8 @@ sub skipmess { -sub tests_bytes_display_string { +sub tests_bytes_display_string +{ note( 'Entering tests_bytes_display_string()' ) ; @@ -8552,13 +10770,14 @@ sub tests_bytes_display_string { ok( '1048576.000 PiB' eq bytes_display_string( 1_180_591_620_717_411_303_424 ), 'bytes_display_string: 1_180_591_620_717_411_303_424' ) ; - #myprint( bytes_display_string( 1_180_591_620_717_411_303_424 ), "\n" ) ; + #myprint( bytes_display_string( 1_180_591_620_717_411_303_424 ), "\n" ) ; note( 'Leaving tests_bytes_display_string()' ) ; return ; } -sub bytes_display_string { +sub bytes_display_string +{ my ( $bytes ) = @_ ; my $readable_value = q{} ; @@ -8594,11 +10813,48 @@ sub bytes_display_string { } # if you have exabytes (EiB) of email to transfer, you have too much email! } - #myprint( "$bytes = $readable_value\n" ) ; + #myprint( "$bytes = $readable_value\n" ) ; return( $readable_value ) ; } -sub stats { + +sub tests_useheader_suggestion +{ + note( 'Entering tests_useheader_suggestion()' ) ; + + is( undef, useheader_suggestion( ), 'useheader_suggestion: no args => undef' ) ; + my $mysync = {} ; + + $mysync->{ h1_nb_msg_noheader } = 0 ; + is( q{}, useheader_suggestion( $mysync ), 'useheader_suggestion: h1_nb_msg_noheader count null => no suggestion' ) ; + $mysync->{ h1_nb_msg_noheader } = 2 ; + is( q{in order to sync those 2 unidentified messages, add option --addheader}, useheader_suggestion( $mysync ), + 'useheader_suggestion: h1_nb_msg_noheader count 2 => suggestion of --addheader' ) ; + + note( 'Leaving tests_useheader_suggestion()' ) ; + return ; +} + +sub useheader_suggestion +{ + my $mysync = shift ; + if ( ! defined $mysync->{ h1_nb_msg_noheader } ) + { + return ; + } + elsif ( 1 <= $mysync->{ h1_nb_msg_noheader } ) + { + return qq{in order to sync those $mysync->{ h1_nb_msg_noheader } unidentified messages, add option --addheader} ; + } + else + { + return q{} ; + } + return ; +} + +sub stats +{ my $mysync = shift ; if ( ! $mysync->{stats} ) { @@ -8615,74 +10871,76 @@ sub stats { my $memory_ratio = ($max_msg_size_in_bytes) ? mysprintf('%.1f', $memory_consumption_at_end / $max_msg_size_in_bytes) : 'NA' ; - - 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 : $mysync->{nb_msg_transferred} " ) ; + # my $useheader_suggestion = useheader_suggestion( $mysync ) ; + 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 : $mysync->{nb_msg_transferred} " ) ; myprint( "(could be $nb_msg_skipped_dry_mode without dry mode)" ) if ( $mysync->{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", + myprint( "Messages skipped : $mysync->{ 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 found crossduplicate on host2 : $mysync->{ h2_nb_msg_crossdup }\n" ) ; + myprint( "Messages void (noheader) on host1 : $mysync->{ h1_nb_msg_noheader } ", useheader_suggestion( $mysync ), "\n" ) ; + myprint( "Messages void (noheader) on host2 : $h2_nb_msg_noheader\n" ) ; + nb_messages_in_1_not_in_2( $mysync ) ; + nb_messages_in_2_not_in_1( $mysync ) ; + myprintf( "Messages found in host1 not in host2 : %s messages\n", $mysync->{ nb_messages_in_1_not_in_2 } ) ; + myprintf( "Messages found in host2 not in host1 : %s messages\n", $mysync->{ nb_messages_in_2_not_in_1 } ) ; + myprint( "Messages deleted on host1 : $mysync->{ h1_nb_msg_deleted }\n" ) ; + myprint( "Messages deleted on host2 : $h2_nb_msg_deleted\n" ) ; + myprintf( "Total bytes transferred : %s (%s)\n", $mysync->{total_bytes_transferred}, bytes_display_string( $mysync->{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 ) ) ; + myprintf( "Total bytes skipped : %s (%s)\n", + $mysync->{ total_bytes_skipped }, + bytes_display_string( $mysync->{ total_bytes_skipped } ) ) ; $timediff ||= 1 ; # No division per 0 - myprintf("Message rate : %.1f messages/s\n", $mysync->{nb_msg_transferred} / $timediff ) ; - myprintf("Average bandwidth rate : %.1f KiB/s\n", $mysync->{total_bytes_transferred} / $KIBI / $timediff ) ; - myprint( "Reconnections to host1 : $mysync->{imap1}->{IMAPSYNC_RECONNECT_COUNT}\n" ) ; - myprint( "Reconnections to host2 : $mysync->{imap2}->{IMAPSYNC_RECONNECT_COUNT}\n" ) ; - myprintf("Memory consumption at the end : %.1f MiB (started with %.1f MiB)\n", + myprintf("Message rate : %.1f messages/s\n", $mysync->{nb_msg_transferred} / $timediff ) ; + myprintf("Average bandwidth rate : %.1f KiB/s\n", $mysync->{total_bytes_transferred} / $KIBI / $timediff ) ; + myprint( "Reconnections to host1 : $mysync->{imap1}->{IMAPSYNC_RECONNECT_COUNT}\n" ) ; + myprint( "Reconnections to host2 : $mysync->{imap2}->{IMAPSYNC_RECONNECT_COUNT}\n" ) ; + myprintf("Memory consumption at the end : %.1f MiB (started with %.1f MiB)\n", $memory_consumption_at_end / $KIBI / $KIBI, $memory_consumption_at_start / $KIBI / $KIBI ) ; - myprintf("Biggest message : %s bytes (%s)\n", + myprint( "Load end is : " . ( join( q{ }, loadavg( ) ) || 'unknown' ), " on $mysync->{cpu_number} cores\n" ) ; + + 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" ) ; + 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, + 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, + 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 $mysync->{nb_errors} errors\n\n" ) ; - myprint( $warn_release, "\n" ) ; - myprint( homepage( ), "\n" ) ; + comment_on_final_diff_in_1_not_in_2( $mysync ) ; + comment_on_final_diff_in_2_not_in_1( $mysync ) ; + myprint( "Detected $mysync->{nb_errors} errors\n\n" ) ; + + myprint( $warn_release, "\n" ) ; + myprint( homepage( ), "\n" ) ; return ; } -sub diff_or_NA { +sub diff_or_NA +{ my( $n1, $n2 ) = @ARG ; if ( not defined $n1 or not defined $n2 ) { @@ -8697,7 +10955,8 @@ sub diff_or_NA { return( $n1 - $n2 ) ; } -sub match_number { +sub match_number +{ my $n = shift @ARG ; if ( not defined $n ) { @@ -8712,7 +10971,8 @@ sub match_number { } -sub tests_match_number { +sub tests_match_number +{ note( 'Entering tests_match_number()' ) ; @@ -8730,7 +10990,8 @@ sub tests_match_number { -sub tests_diff_or_NA { +sub tests_diff_or_NA +{ note( 'Entering tests_diff_or_NA()' ) ; @@ -8752,12 +11013,14 @@ sub tests_diff_or_NA { return ; } -sub homepage { +sub homepage +{ return( 'Homepage: https://imapsync.lamiral.info/' ) ; } -sub load_modules { +sub load_modules +{ if ( $sync->{ssl1} or $sync->{ssl2} or $sync->{tls1} @@ -8774,15 +11037,16 @@ sub load_modules { -sub parse_header_msg { +sub parse_header_msg +{ my ( $mysync, $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" ) ; + $mysync->{ debug } and myprint( "$side: uid $m_uid number of headers, 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" ) ; + 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 ; @@ -8790,7 +11054,7 @@ sub parse_header_msg { $head = decompose_header( $whole_header ) ; $headnum = scalar keys %{ $head } ; - $debug and myprint( "$side uid $m_uid head nb pass two: ", $headnum, "\n" ) ; + $mysync->{ debug } and myprint( "$side: uid $m_uid number of headers, pass two: ", $headnum, "\n" ) ; } #myprint( Data::Dumper->Dump( [ $head, \%useheader ] ) ) ; @@ -8801,7 +11065,7 @@ sub parse_header_msg { if ( ( ! $headstr ) and ( $mysync->{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" ) ; + myprint( "$side: uid $m_uid no header found so adding our own [$header]\n" ) ; $headstr .= uc $header ; $s_fir->{$m_uid}->{NO_HEADER} = 1; } @@ -8813,7 +11077,7 @@ sub parse_header_msg { 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" ) ; + $mysync->{ debug } and myprint( "$side: uid $m_uid sig $m_md5 size $size idate $idate\n" ) ; my $key ; if ($skipsize) { $key = "$m_md5"; @@ -8832,7 +11096,8 @@ sub parse_header_msg { return( 1 ) ; } -sub header_construct { +sub header_construct +{ my( $head, $side, $m_uid ) = @_ ; @@ -8846,10 +11111,10 @@ sub header_construct { my $H = header_line_normalize( $h, $val ) ; # show stuff in debug mode - $debug and myprint( "$side uid $m_uid header [$H]", "\n" ) ; + $sync->{ 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" ) ; + $sync->{ debug } and myprint( "$side uid $m_uid skipping header [$H]\n" ) ; next ; } $headstr .= "$H" ; @@ -8859,7 +11124,8 @@ sub header_construct { } -sub header_line_normalize { +sub header_line_normalize +{ my( $header_key, $header_val ) = @_ ; # no 8-bit data in headers ! @@ -8888,7 +11154,8 @@ sub header_line_normalize { return( $header_line ) ; } -sub tests_header_line_normalize { +sub tests_header_line_normalize +{ note( 'Entering tests_header_line_normalize()' ) ; @@ -8905,52 +11172,130 @@ sub tests_header_line_normalize { } -sub firstline { +sub tests_firstline +{ + note( 'Entering tests_firstline()' ) ; + + is( q{}, firstline( 'W/tmp/tests/noexist.txt' ), 'firstline: getting empty string from inexisting W/tmp/tests/noexist.txt' ) ; + + ok( (-d 'W/tmp/tests/' or mkpath( 'W/tmp/tests/' ) ), 'firstline: mkpath W/tmp/tests/' ) ; + + is( "blabla\n" , string_to_file( "blabla\n", 'W/tmp/tests/firstline.txt' ), 'firstline: put blabla in W/tmp/tests/firstline.txt' ) ; + is( 'blabla' , firstline( 'W/tmp/tests/firstline.txt' ), 'firstline: get blabla from W/tmp/tests/firstline.txt' ) ; + + is( q{} , string_to_file( q{}, 'W/tmp/tests/firstline2.txt' ), 'firstline: put empty string in W/tmp/tests/firstline2.txt' ) ; + is( q{} , firstline( 'W/tmp/tests/firstline2.txt' ), 'firstline: get empty string from W/tmp/tests/firstline2.txt' ) ; + + is( "\n" , string_to_file( "\n", 'W/tmp/tests/firstline3.txt' ), 'firstline: put CR in W/tmp/tests/firstline3.txt' ) ; + is( q{} , firstline( 'W/tmp/tests/firstline3.txt' ), 'firstline: get empty string from W/tmp/tests/firstline3.txt' ) ; + + is( "blabla\nTiti\n" , string_to_file( "blabla\nTiti\n", 'W/tmp/tests/firstline4.txt' ), 'firstline: put blabla\nTiti\n in W/tmp/tests/firstline4.txt' ) ; + is( 'blabla' , firstline( 'W/tmp/tests/firstline4.txt' ), 'firstline: get blabla from W/tmp/tests/firstline4.txt' ) ; + + note( 'Leaving tests_firstline()' ) ; + return ; +} + +sub firstline +{ # extract the first line of a file (without \n) + # return empty string if error or empty string - my( $file ) = @_ ; - my $line = q{} ; + my $file = shift ; + my $line ; - if ( ! -e $file ) { - myprint( "Cannot open file $file since it does not exist\n" ) ; - return ; - } - - open my $FILE, '<', $file or do { - myprint( "Error opening file $file : $OS_ERROR\n" ) ; - return ; - } ; - $line = <$FILE> || q{} ; - close $FILE ; - chomp $line ; + $line = nthline( $file, 1 ) ; return $line ; } -sub tests_firstline { - note( 'Entering tests_firstline()' ) ; - is( undef , firstline( 'W/tmp/tests/noexist.txt' ), 'tests_firstline: not getting blabla from W/tmp/tests/noexist.txt' ) ; - is( "blabla\n" , string_to_file( "blabla\n", 'W/tmp/tests/firstline.txt' ), 'tests_firstline: put blabla in W/tmp/tests/firstline.txt' ) ; - is( 'blabla' , firstline( 'W/tmp/tests/firstline.txt' ), 'tests_firstline: get blabla from W/tmp/tests/firstline.txt' ) ; - is( q{} , string_to_file( q{}, 'W/tmp/tests/firstline2.txt' ), 'tests_firstline: put empty string in W/tmp/tests/firstline2.txt' ) ; - is( q{} , firstline( 'W/tmp/tests/firstline2.txt' ), 'tests_firstline: get empty string from W/tmp/tests/firstline2.txt' ) ; - is( "\n" , string_to_file( "\n", 'W/tmp/tests/firstline3.txt' ), 'tests_firstline: put CR in W/tmp/tests/firstline3.txt' ) ; - is( q{} , firstline( 'W/tmp/tests/firstline3.txt' ), 'tests_firstline: get empty string from W/tmp/tests/firstline3.txt' ) ; - note( 'Leaving tests_firstline()' ) ; +sub tests_secondline +{ + note( 'Entering tests_secondline()' ) ; + + is( q{}, secondline( 'W/tmp/tests/noexist.txt' ), 'secondline: getting empty string from inexisting W/tmp/tests/noexist.txt' ) ; + is( q{}, secondline( 'W/tmp/tests/noexist.txt', 2 ), 'secondline: 2nd getting empty string from inexisting W/tmp/tests/noexist.txt' ) ; + + ok( (-d 'W/tmp/tests/' or mkpath( 'W/tmp/tests/' ) ), 'secondline: mkpath W/tmp/tests/' ) ; + + is( "L1\nL2\nL3\nL4\n" , string_to_file( "L1\nL2\nL3\nL4\n", 'W/tmp/tests/secondline.txt' ), 'secondline: put L1\nL2\nL3\nL4\n in W/tmp/tests/secondline.txt' ) ; + is( 'L2' , secondline( 'W/tmp/tests/secondline.txt' ), 'secondline: get L2 from W/tmp/tests/secondline.txt' ) ; + + + note( 'Leaving tests_secondline()' ) ; return ; } +sub secondline +{ + # extract the second line of a file (without \n) + # return empty string if error or empty string + + my $file = shift ; + my $line ; + + $line = nthline( $file, 2 ) ; + return $line ; +} + + + + +sub tests_nthline +{ + note( 'Entering tests_nthline()' ) ; + + is( q{}, nthline( 'W/tmp/tests/noexist.txt' ), 'nthline: getting empty string from inexisting W/tmp/tests/noexist.txt' ) ; + is( q{}, nthline( 'W/tmp/tests/noexist.txt', 2 ), 'nthline: 2nd getting empty string from inexisting W/tmp/tests/noexist.txt' ) ; + + ok( (-d 'W/tmp/tests/' or mkpath( 'W/tmp/tests/' ) ), 'nthline: mkpath W/tmp/tests/' ) ; + + is( "L1\nL2\nL3\nL4\n" , string_to_file( "L1\nL2\nL3\nL4\n", 'W/tmp/tests/nthline.txt' ), 'nthline: put L1\nL2\nL3\nL4\n in W/tmp/tests/nthline.txt' ) ; + is( 'L3' , nthline( 'W/tmp/tests/nthline.txt', 3 ), 'nthline: get L3 from W/tmp/tests/nthline.txt' ) ; + + + note( 'Leaving tests_nthline()' ) ; + return ; +} + + +sub nthline +{ + # extract the nth line of a file (without \n) + # return empty string if error or empty string + + my $file = shift ; + my $num = shift ; + + if ( ! all_defined( $file, $num ) ) { return q{} ; } + + my $line ; + + $line = ( file_to_array( $file ) )[$num - 1] ; + if ( ! defined $line ) + { + return q{} ; + } + else + { + chomp $line ; + return $line ; + } + +} + # Should be unit tested and then be used by file_to_string, refactoring file_to_string -sub file_to_array { +sub file_to_array +{ my( $file ) = shift ; my @string ; open my $FILE, '<', $file or do { - myprint( "Error reading file $file : $OS_ERROR" ) ; + myprint( "Error reading file $file : $OS_ERROR\n" ) ; return ; } ; @string = <$FILE> ; @@ -8959,7 +11304,8 @@ sub file_to_array { } -sub tests_file_to_string { +sub tests_file_to_string +{ note( 'Entering tests_file_to_string()' ) ; is( undef, file_to_string( ), 'file_to_string: no args => undef' ) ; @@ -8967,7 +11313,7 @@ sub tests_file_to_string { is( undef, file_to_string( '/' ), 'file_to_string: reading a directory => undef' ) ; ok( file_to_string( $PROGRAM_NAME ), 'file_to_string: reading myself' ) ; - ok( (-d 'W/tmp/tests/' or mkpath( 'W/tmp/tests/' ) ), 'file_to_string: mkpath W/tmp/tests/' ) ; + ok( (-d 'W/tmp/tests/' or mkpath( 'W/tmp/tests/' ) ), 'file_to_string: mkpath W/tmp/tests/' ) ; is( 'lilili', string_to_file( 'lilili', 'W/tmp/tests/canbewritten' ), 'file_to_string: string_to_file filling W/tmp/tests/canbewritten with lilili' ) ; is( 'lilili', file_to_string( 'W/tmp/tests/canbewritten' ), 'file_to_string: reading W/tmp/tests/canbewritten is lilili' ) ; @@ -8979,7 +11325,8 @@ sub tests_file_to_string { return ; } -sub file_to_string { +sub file_to_string +{ my $file = shift ; if ( ! $file ) { return ; } if ( ! -e $file ) { return ; } @@ -8997,7 +11344,8 @@ sub file_to_string { } -sub tests_string_to_file { +sub tests_string_to_file +{ note( 'Entering tests_string_to_file()' ) ; is( undef, string_to_file( ), 'string_to_file: no args => undef' ) ; @@ -9017,7 +11365,8 @@ sub tests_string_to_file { return ; } -sub string_to_file { +sub string_to_file +{ my( $string, $file ) = @_ ; if( ! defined $string ) { return ; } if( ! defined $file ) { return ; } @@ -9036,7 +11385,7 @@ sub string_to_file { return $string ; } -q^ +0 and <<'MULTILINE_COMMENT' ; This is a multiline comment. Based on David Carter discussion, to do: * Call parameters stay the same. @@ -9050,9 +11399,11 @@ OK * in case of CHILD_ERROR, return( undef, $error ) * 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. +MULTILINE_COMMENT +# End of multiline comment. -sub pipemess { +sub pipemess +{ my ( $string, @commands ) = @_ ; my $error = q{} ; foreach my $command ( @commands ) { @@ -9085,7 +11436,7 @@ sub pipemess { myprint( qq{STDERR of --pipemess "$command": $error_cmd\n} ) ; } } - #myprint( "[$string]\n" ) ; + #myprint( "[$string]\n" ) ; if ( wantarray ) { return ( $string, $error ) ; }else{ @@ -9095,7 +11446,8 @@ sub pipemess { -sub tests_pipemess { +sub tests_pipemess +{ note( 'Entering tests_pipemess()' ) ; @@ -9148,7 +11500,7 @@ sub tests_pipemess { ( $stringT, $errorT ) = pipemess( 'dontcare', 'true' ) ; is( $stringT, undef, 'pipemess: list context, true but no output, string' ) ; - like( $errorT, qr{\QFailure: --pipemess command "true" ended with "0" characters exit value "0" and STDERR ""\E}xm, 'pipemess: list context, true but no output, error' ) ; + like( $errorT, qr{\QFailure: --pipemess command "true" ended with "0" characters exit value "0" and STDERR ""\E}xm, 'pipemess: list context, true but no output, error' ) ; ( $stringT, $errorT ) = pipemess( 'dontcare', 'false' ) ; is( $stringT, undef, 'pipemess: list context, false and no output, string' ) ; @@ -9162,12 +11514,12 @@ sub tests_pipemess { ( $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"}xm, 'pipemess: list context, "no output STDERR blablabla", error' ) ; + like( $errorT, qr{blablabla"}xm, '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"}xm, 'pipemess: list context, "no output STDERR blablabla then false", error' ) ; + like( $errorT, qr{blablabla"}xm, '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' ) ; @@ -9191,9 +11543,10 @@ sub tests_pipemess { -sub tests_is_a_release_number { +sub tests_is_a_release_number +{ note( 'Entering tests_is_a_release_number()' ) ; - + is( undef, is_a_release_number( ), 'is_a_release_number: no args => undef' ) ; 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' ) ; @@ -9204,7 +11557,8 @@ sub tests_is_a_release_number { return ; } -sub is_a_release_number { +sub is_a_release_number +{ my $number = shift ; if ( ! defined $number ) { return ; } return( $number =~ m{^\d+\.\d+$}xo ) ; @@ -9212,7 +11566,8 @@ sub is_a_release_number { -sub imapsync_version_public { +sub imapsync_version_public +{ my $local_version = imapsync_version( $sync ) ; my $imapsync_basename = imapsync_basename( ) ; @@ -9237,7 +11592,8 @@ sub imapsync_version_public { return( $last_release ) ; } -sub not_long_imapsync_version_public { +sub not_long_imapsync_version_public +{ #myprint( "Entering not_long_imapsync_version_public\n" ) ; my $fake = shift ; @@ -9254,7 +11610,7 @@ sub not_long_imapsync_version_public { POSIX::sigaction(SIGALRM, POSIX::SigAction->new(sub { croak 'alarm' } ) ) - or myprint( "Error setting SIGALRM handler: $OS_ERROR\n" ) ; + or myprint( "Error setting SIGALRM handler: $OS_ERROR\n" ) ; } my $ret = eval { @@ -9262,12 +11618,12 @@ sub not_long_imapsync_version_public { { $val = imapsync_version_public( ) ; #sleep 4 ; - #myprint( "End of imapsync_version_public\n" ) ; + #myprint( "End of imapsync_version_public\n" ) ; } alarm 0 ; 1 ; } ; - #myprint( "eval [$ret]\n" ) ; + #myprint( "eval [$ret]\n" ) ; if ( ( not $ret ) or $EVAL_ERROR ) { #myprint( "$EVAL_ERROR" ) ; if ($EVAL_ERROR =~ /alarm/) { @@ -9283,7 +11639,8 @@ sub not_long_imapsync_version_public { } } -sub tests_not_long_imapsync_version_public { +sub tests_not_long_imapsync_version_public +{ note( 'Entering tests_not_long_imapsync_version_public()' ) ; @@ -9294,20 +11651,21 @@ sub tests_not_long_imapsync_version_public { return ; } -sub check_last_release { +sub check_last_release +{ my $fake = shift ; my $public_release = not_long_imapsync_version_public( $fake ) ; - $debug and myprint( "check_last_release: [$public_release]\n" ) ; + $sync->{ debug } and myprint( "check_last_release: [$public_release]\n" ) ; my $inline_help_when_on = '( Use --noreleasecheck to avoid this release check. )' ; - + if ( $public_release eq 'unknown' ) { return( 'Imapsync public release is unknown.' . $inline_help_when_on ) ; } - + if ( $public_release eq 'timeout' ) { return( 'Imapsync public release is unknown (timeout).' . $inline_help_when_on ) ; } - + if ( ! is_a_release_number( $public_release ) ) { return( "Imapsync public release is unknown ($public_release)." . $inline_help_when_on ) ; } @@ -9323,7 +11681,8 @@ sub check_last_release { return( 'really unknown' ) ; # Should never arrive here } -sub tests_check_last_release { +sub tests_check_last_release +{ note( 'Entering tests_check_last_release()' ) ; diag( check_last_release( 1.1 ) ) ; @@ -9342,7 +11701,8 @@ sub tests_check_last_release { return ; } -sub imapsync_version { +sub imapsync_version +{ my $mysync = shift ; my $rcs = $mysync->{rcs} ; my $version ; @@ -9352,7 +11712,8 @@ sub imapsync_version { } -sub tests_version_from_rcs { +sub tests_version_from_rcs +{ note( 'Entering tests_version_from_rcs()' ) ; is( undef, version_from_rcs( ), 'version_from_rcs: no args => UNKNOWN' ) ; @@ -9364,22 +11725,24 @@ sub tests_version_from_rcs { } -sub version_from_rcs { +sub version_from_rcs +{ my $rcs = shift ; if ( ! $rcs ) { return ; } - + my $version = 'UNKNOWN' ; if ( $rcs =~ m{,v\s+(\d+\.\d+)}mxso ) { $version = $1 } - + return( $version ) ; } -sub tests_imapsync_basename { +sub tests_imapsync_basename +{ note( 'Entering tests_imapsync_basename()' ) ; ok( imapsync_basename() =~ m/imapsync/, 'imapsync_basename: match imapsync'); @@ -9389,14 +11752,16 @@ sub tests_imapsync_basename { return ; } -sub imapsync_basename { +sub imapsync_basename +{ return basename( $PROGRAM_NAME ) ; } -sub localhost_info { +sub localhost_info +{ my $mysync = shift ; my( $infos ) = join( q{}, "Here is imapsync ", imapsync_version( $mysync ), @@ -9411,7 +11776,8 @@ sub localhost_info { return( $infos ) ; } -sub tests_cpu_number { +sub tests_cpu_number +{ note( 'Entering tests_cpu_number()' ) ; is( 1, is_an_integer( cpu_number( ) ), "cpu_number: is_an_integer" ) ; @@ -9424,7 +11790,8 @@ sub tests_cpu_number { return ; } -sub cpu_number { +sub cpu_number +{ my $cpu_number_forced = shift ; # Well, here 1 is better than 0 or undef @@ -9434,19 +11801,19 @@ sub cpu_number { if ( $ENV{"NUMBER_OF_PROCESSORS"} ) { # might be under a Windows system $cpu_number = $ENV{"NUMBER_OF_PROCESSORS"} ; - $debug and myprint( "Number of processors found by env var NUMBER_OF_PROCESSORS: $cpu_number\n" ) ; - }elsif ( 'darwin' eq $OSNAME ) { + $sync->{ debug } and myprint( "Number of processors found by env var NUMBER_OF_PROCESSORS: $cpu_number\n" ) ; + }elsif ( 'darwin' eq $OSNAME or 'freebsd' eq $OSNAME ) { $cpu_number = backtick( "sysctl -n hw.ncpu" ) ; chomp( $cpu_number ) ; - $debug and myprint( "Number of processors found by cmd 'sysctl -n hw.ncpu': $cpu_number\n" ) ; + $sync->{ debug } and myprint( "Number of processors found by cmd 'sysctl -n hw.ncpu': $cpu_number\n" ) ; }elsif ( ! -e '/proc/cpuinfo' ) { - $debug and myprint( "Number of processors not found so I might assume there is only 1\n" ) ; + $sync->{ debug } and myprint( "Number of processors not found so I might assume there is only 1\n" ) ; $cpu_number = 1 ; }elsif( @cpuinfo = file_to_array( '/proc/cpuinfo' ) ) { $cpu_number = grep { /^processor/mxs } @cpuinfo ; - $debug and myprint( "Number of processors found via /proc/cpuinfo: $cpu_number\n" ) ; + $sync->{ debug } and myprint( "Number of processors found via /proc/cpuinfo: $cpu_number\n" ) ; } - + if ( defined $cpu_number_forced ) { $cpu_number = $cpu_number_forced ; } @@ -9454,28 +11821,34 @@ sub cpu_number { } -sub tests_integer_or_1 { +sub tests_integer_or_1 +{ + note( 'Entering tests_integer_or_1()' ) ; is( 1, integer_or_1( ), 'integer_or_1: no args => 1' ) ; is( 1, integer_or_1( undef ), 'integer_or_1: undef => 1' ) ; is( $NUMBER_10, integer_or_1( $NUMBER_10 ), 'integer_or_1: 10 => 10' ) ; is( 1, integer_or_1( q{} ), 'integer_or_1: empty string => 1' ) ; is( 1, integer_or_1( 'lalala' ), 'integer_or_1: lalala => 1' ) ; + + note( 'Leaving tests_integer_or_1()' ) ; return ; } -sub integer_or_1 { +sub integer_or_1 +{ my $number = shift ; - if ( is_an_integer( $number ) ) { - return $number ; + if ( is_an_integer( $number ) ) { + return $number ; } # else return 1 ; } -sub tests_is_an_integer { +sub tests_is_an_integer +{ note( 'Entering tests_is_an_integer()' ) ; - + is( undef, is_an_integer( ), 'is_an_integer: no args => undef ' ) ; ok( is_an_integer( 1 ), 'is_an_integer: 1 => yes ') ; ok( is_an_integer( $NUMBER_42 ), 'is_an_integer: 42 => yes ') ; @@ -9491,7 +11864,8 @@ sub tests_is_an_integer { return ; } -sub is_an_integer { +sub is_an_integer +{ my $number = shift ; if ( ! defined $number ) { return ; } return( $number =~ m{^\d+$}xo ) ; @@ -9500,7 +11874,8 @@ sub is_an_integer { -sub tests_loadavg { +sub tests_loadavg +{ note( 'Entering tests_loadavg()' ) ; @@ -9535,10 +11910,14 @@ sub tests_loadavg { } -sub loadavg { +sub loadavg +{ if ( 'linux' eq $OSNAME ) { return ( loadavg_linux( @ARG ) ) ; } + if ( 'freebsd' eq $OSNAME ) { + return ( loadavg_freebsd( @ARG ) ) ; + } if ( 'darwin' eq $OSNAME ) { return ( loadavg_darwin( @ARG ) ) ; } @@ -9549,7 +11928,8 @@ sub loadavg { } -sub loadavg_linux { +sub loadavg_linux +{ my $line = shift ; if ( ! $line ) { @@ -9558,14 +11938,37 @@ sub loadavg_linux { my ( $avg_1_min, $avg_5_min, $avg_15_min, $current_runs ) = split /\s/mxs, $line ; if ( all_defined( $avg_1_min, $avg_5_min, $avg_15_min ) ) { - $debug and myprint( "System load: $avg_1_min $avg_5_min $avg_15_min $current_runs\n" ) ; + $sync->{ debug } and myprint( "System load: $avg_1_min $avg_5_min $avg_15_min $current_runs\n" ) ; return ( $avg_1_min, $avg_5_min, $avg_15_min, $current_runs ) ; } return ; } +sub loadavg_freebsd +{ + my $file = shift ; + # Example of output of command "sysctl vm.loadavg": + # vm.loadavg: { 0.15 0.08 0.08 } + my $loadavg ; -sub loadavg_darwin { + if ( ! defined $file ) { + eval { + $loadavg = `/sbin/sysctl vm.loadavg` ; + #myprint( "LOADAVG FREEBSD: $loadavg\n" ) ; + } ; + if ( $EVAL_ERROR ) { myprint( "[$EVAL_ERROR]\n" ) ; return ; } + }else{ + $loadavg = firstline( $file ) or return ; + } + + my ( $avg_1_min, $avg_5_min, $avg_15_min ) + = $loadavg =~ /vm\.loadavg\s*[:=]\s*\{?\s*(\d+\.?\d*)\s+(\d+\.?\d*)\s+(\d+\.?\d*)/mxs ; + $sync->{ debug } and myprint( "System load: $avg_1_min $avg_5_min $avg_15_min\n" ) ; + return ( $avg_1_min, $avg_5_min, $avg_15_min ) ; +} + +sub loadavg_darwin +{ my $file = shift ; # Example of output of command "sysctl vm.loadavg": # vm.loadavg: { 0.15 0.08 0.08 } @@ -9583,11 +11986,12 @@ sub loadavg_darwin { my ( $avg_1_min, $avg_5_min, $avg_15_min ) = $loadavg =~ /vm\.loadavg\s*[:=]\s*\{?\s*(\d+\.?\d*)\s+(\d+\.?\d*)\s+(\d+\.?\d*)/mxs ; - $debug and myprint( "System load: $avg_1_min $avg_5_min $avg_15_min\n" ) ; + $sync->{ debug } and myprint( "System load: $avg_1_min $avg_5_min $avg_15_min\n" ) ; return ( $avg_1_min, $avg_5_min, $avg_15_min ) ; } -sub loadavg_windows { +sub loadavg_windows +{ my $file = shift ; # Example of output of command "wmic cpu get loadpercentage": # LoadPercentage @@ -9609,7 +12013,7 @@ sub loadavg_windows { my $num = $1 ; $num /= 100 ; - $debug and myprint( "System load: $num\n" ) ; + $sync->{ debug } and myprint( "System load: $num\n" ) ; return ( $num ) ; } @@ -9618,39 +12022,65 @@ sub loadavg_windows { -sub tests_load_and_delay { +sub tests_load_and_delay +{ note( 'Entering tests_load_and_delay()' ) ; is( undef, load_and_delay( ), 'load_and_delay: no args => undef ' ) ; is( undef, load_and_delay( 1 ), 'load_and_delay: not 4 args => undef ' ) ; is( undef, load_and_delay( 0, 1, 1, 1 ), 'load_and_delay: division per 0 => undef ' ) ; is( 0, load_and_delay( 1, 1, 1, 1 ), 'load_and_delay: one core, loads are all 1 => ok ' ) ; + is( 0, load_and_delay( 1, 1, 1, 1, 'lalala' ), 'load_and_delay: five arguments is ok' ) ; is( 0, load_and_delay( 2, 2, 2, 2 ), 'load_and_delay: two core, loads are all 2 => ok ' ) ; is( 0, load_and_delay( 2, 2, 4, 5 ), 'load_and_delay: two core, load1m is 2 => ok ' ) ; - is( 0, load_and_delay( 1, 0, 0, 0 ), 'load_and_delay: one core, load1m=0 load5m=0 load15m=0 => 0 ' ) ; - is( 0, load_and_delay( 1, 0, 0, 2 ), 'load_and_delay: one core, load1m=0 load5m=0 load15m=2 => 0 ' ) ; - is( 0, load_and_delay( 1, 0, 2, 0 ), 'load_and_delay: one core, load1m=0 load5m=2 load15m=0 => 0 ' ) ; - is( 0, load_and_delay( 1, 0, 2, 2 ), 'load_and_delay: one core, load1m=0 load5m=2 load15m=2 => 0 ' ) ; - is( 1, load_and_delay( 1, 2, 0, 0 ), 'load_and_delay: one core, load1m=2 load5m=0 load15m=0 => 1 ' ) ; - is( 1, load_and_delay( 1, 2, 0, 2 ), 'load_and_delay: one core, load1m=2 load5m=0 load15m=2 => 1 ' ) ; - is( 5, load_and_delay( 1, 2, 2, 0 ), 'load_and_delay: one core, load1m=2 load5m=2 load15m=0 => 5 ' ) ; - is( 15, load_and_delay( 1, 2, 2, 2 ), 'load_and_delay: one core, load1m=2 load5m=2 load15m=2 => 15 ' ) ; +# Old behavior, rather strict + # is( 0, load_and_delay( 1, 0, 0, 0 ), 'load_and_delay: one core, load1m=0 load5m=0 load15m=0 => 0 ' ) ; + # is( 0, load_and_delay( 1, 0, 0, 2 ), 'load_and_delay: one core, load1m=0 load5m=0 load15m=2 => 0 ' ) ; + # is( 0, load_and_delay( 1, 0, 2, 0 ), 'load_and_delay: one core, load1m=0 load5m=2 load15m=0 => 0 ' ) ; + # is( 0, load_and_delay( 1, 0, 2, 2 ), 'load_and_delay: one core, load1m=0 load5m=2 load15m=2 => 0 ' ) ; + # is( 1, load_and_delay( 1, 2, 0, 0 ), 'load_and_delay: one core, load1m=2 load5m=0 load15m=0 => 1 ' ) ; + # is( 1, load_and_delay( 1, 2, 0, 2 ), 'load_and_delay: one core, load1m=2 load5m=0 load15m=2 => 1 ' ) ; + # is( 5, load_and_delay( 1, 2, 2, 0 ), 'load_and_delay: one core, load1m=2 load5m=2 load15m=0 => 5 ' ) ; + # is( 15, load_and_delay( 1, 2, 2, 2 ), 'load_and_delay: one core, load1m=2 load5m=2 load15m=2 => 15 ' ) ; - is( 0, load_and_delay( 4, 0, 2, 2 ), 'load_and_delay: four core, load1m=0 load5m=2 load15m=2 => 0 ' ) ; - is( 1, load_and_delay( 4, 8, 0, 0 ), 'load_and_delay: four core, load1m=2 load5m=0 load15m=0 => 1 ' ) ; - is( 1, load_and_delay( 4, 8, 0, 2 ), 'load_and_delay: four core, load1m=2 load5m=0 load15m=2 => 1 ' ) ; - is( 5, load_and_delay( 4, 8, 8, 0 ), 'load_and_delay: four core, load1m=2 load5m=2 load15m=0 => 5 ' ) ; - is( 15, load_and_delay( 4, 8, 8, 8 ), 'load_and_delay: four core, load1m=2 load5m=2 load15m=2 => 15 ' ) ; - is( 15, load_and_delay( 4, 8, 8, 8, 'lalala' ), 'load_and_delay: five arguments is ok' ) ; + # is( 0, load_and_delay( 4, 0, 2, 2 ), 'load_and_delay: four core, load1m=0 load5m=2 load15m=2 => 0 ' ) ; + # is( 1, load_and_delay( 4, 8, 0, 0 ), 'load_and_delay: four core, load1m=2 load5m=0 load15m=0 => 1 ' ) ; + # is( 1, load_and_delay( 4, 8, 0, 2 ), 'load_and_delay: four core, load1m=2 load5m=0 load15m=2 => 1 ' ) ; + # is( 5, load_and_delay( 4, 8, 8, 0 ), 'load_and_delay: four core, load1m=2 load5m=2 load15m=0 => 5 ' ) ; + # is( 15, load_and_delay( 4, 8, 8, 8 ), 'load_and_delay: four core, load1m=2 load5m=2 load15m=2 => 15 ' ) ; - note( 'Leaving tests_load_and_delay()' ) ; - return ; +# New behavior, tolerate more load + + is( 0, load_and_delay( 1, 0, 0, 0 ), 'load_and_delay: one core, load1m=0 load5m=0 load15m=0 => 0 ' ) ; + is( 0, load_and_delay( 1, 0, 0, 2 ), 'load_and_delay: one core, load1m=0 load5m=0 load15m=2 => 0 ' ) ; + is( 0, load_and_delay( 1, 0, 2, 0 ), 'load_and_delay: one core, load1m=0 load5m=2 load15m=0 => 0 ' ) ; + is( 0, load_and_delay( 1, 0, 2, 2 ), 'load_and_delay: one core, load1m=0 load5m=2 load15m=2 => 0 ' ) ; + is( 0, load_and_delay( 1, 2, 0, 0 ), 'load_and_delay: one core, load1m=2 load5m=0 load15m=0 => 1 ' ) ; + is( 0, load_and_delay( 1, 2, 0, 2 ), 'load_and_delay: one core, load1m=2 load5m=0 load15m=2 => 1 ' ) ; + is( 0, load_and_delay( 1, 2, 2, 0 ), 'load_and_delay: one core, load1m=2 load5m=2 load15m=0 => 5 ' ) ; + is( 0, load_and_delay( 1, 2, 2, 2 ), 'load_and_delay: one core, load1m=2 load5m=2 load15m=2 => 15 ' ) ; + + is( 1, load_and_delay( 1, 4, 0, 0 ), 'load_and_delay: one core, load1m=4 load5m=0 load15m=0 => 1 ' ) ; + is( 1, load_and_delay( 1, 4, 0, 4 ), 'load_and_delay: one core, load1m=4 load5m=0 load15m=4 => 1 ' ) ; + is( 5, load_and_delay( 1, 4, 4, 0 ), 'load_and_delay: one core, load1m=4 load5m=4 load15m=0 => 5 ' ) ; + is( 15, load_and_delay( 1, 4, 4, 4 ), 'load_and_delay: one core, load1m=4 load5m=4 load15m=4 => 15 ' ) ; + + is( 0, load_and_delay( 4, 0, 9, 9 ), 'load_and_delay: four core, load1m=0 load5m=9 load15m=9 => 0 ' ) ; + is( 1, load_and_delay( 4, 9, 0, 0 ), 'load_and_delay: four core, load1m=9 load5m=0 load15m=0 => 1 ' ) ; + is( 1, load_and_delay( 4, 9, 0, 9 ), 'load_and_delay: four core, load1m=9 load5m=0 load15m=9 => 1 ' ) ; + is( 5, load_and_delay( 4, 9, 9, 0 ), 'load_and_delay: four core, load1m=9 load5m=9 load15m=0 => 5 ' ) ; + is( 15, load_and_delay( 4, 9, 9, 9 ), 'load_and_delay: four core, load1m=9 load5m=9 load15m=9 => 15 ' ) ; + + note( 'Leaving tests_load_and_delay()' ) ; + return ; } -sub load_and_delay { +sub load_and_delay +{ # Basically return 0 if load is not heavy, ie <= 1 per processor + # Not enough arguments if ( 4 > scalar @ARG ) { return ; } my ( $cpu_num, $avg_1_min, $avg_5_min, $avg_15_min ) = @ARG ; @@ -9660,17 +12090,18 @@ sub load_and_delay { # Let divide by number of cores ( $avg_1_min, $avg_5_min, $avg_15_min ) = map { $_ / $cpu_num } ( $avg_1_min, $avg_5_min, $avg_15_min ) ; # One of avg ok => ok, for now it is a OR - if ( $avg_1_min <= 1 ) { return 0 ; } - if ( $avg_5_min <= 1 ) { return 1 ; } # Retry in 1 minute - if ( $avg_15_min <= 1 ) { return 5 ; } # Retry in 5 minutes + if ( $avg_1_min <= 2 ) { return 0 ; } + if ( $avg_5_min <= 2 ) { return 1 ; } # Retry in 1 minute + if ( $avg_15_min <= 2 ) { return 5 ; } # Retry in 5 minutes return 15 ; # Retry in 15 minutes } -sub ram_memory_info { +sub ram_memory_info +{ # In GigaBytes so division by 1024 * 1024 * 1024 - # + # return( - sprintf( "%.1f/%.1f free GiB of RAM", + sprintf( "%.1f/%.1f free GiB of RAM", Sys::MemInfo::get("freemem") / ( $KIBI ** 3 ), Sys::MemInfo::get("totalmem") / ( $KIBI ** 3 ), ) @@ -9679,20 +12110,22 @@ sub ram_memory_info { -sub tests_memory_stress { +sub tests_memory_stress +{ note( 'Entering tests_memory_stress()' ) ; - + is( undef, memory_stress( ), 'memory_stress: => undef' ) ; - + note( 'Leaving tests_memory_stress()' ) ; return ; } -sub memory_stress { +sub memory_stress +{ my $total_ram_in_MB = Sys::MemInfo::get("totalmem") / ( $KIBI * $KIBI ) ; my $i = 1 ; - + myprintf("Stress memory consumption before: %.1f MiB\n", memory_consumption( ) / $KIBI / $KIBI ) ; while ( $i < $total_ram_in_MB / 1.7 ) { $a .= "A" x 1000_000; $i++ } ; myprintf("Stress memory consumption after: %.1f MiB\n", memory_consumption( ) / $KIBI / $KIBI ) ; @@ -9700,7 +12133,8 @@ sub memory_stress { } -sub tests_memory_consumption { +sub tests_memory_consumption +{ note( 'Entering tests_memory_consumption()' ) ; like( memory_consumption( ), qr{\d+}xms,'memory_consumption no args') ; @@ -9717,33 +12151,36 @@ sub tests_memory_consumption { return ; } -sub memory_consumption { +sub memory_consumption +{ # memory consumed by imapsync until now in bytes return( ( memory_consumption_of_pids( ) )[0] ); } -sub debugmemory { +sub debugmemory +{ my $mysync = shift ; if ( ! $mysync->{debugmemory} ) { return q{} ; } - + my $precision = shift ; return( mysprintf( "Memory consumption$precision: %.1f MiB\n", memory_consumption( ) / $KIBI / $KIBI ) ) ; } -sub memory_consumption_of_pids { +sub memory_consumption_of_pids +{ my @pid = @_; @pid = ( @pid ) ? @pid : ( $PROCESS_ID ) ; - $debug and myprint( "memory_consumption_of_pids PIDs: @pid\n" ) ; + $sync->{ debug } and myprint( "memory_consumption_of_pids PIDs: @pid\n" ) ; my @val ; - if ( 'MSWin32' eq $OSNAME ) { + if ( ( 'MSWin32' eq $OSNAME ) or ( 'cygwin' eq $OSNAME ) ) { @val = memory_consumption_of_pids_win32( @pid ) ; }else{ # Unix my @ps = qx{ ps -o vsz -p @pid } ; #myprint( "ps: @ps" ) ; - + # Use IPC::Open3 from perlcrit -3 # It stalls on Darwin, don't understand why! #my @ps = backtick( "ps -o vsz -p @pid" ) ; @@ -9752,14 +12189,15 @@ sub memory_consumption_of_pids { shift @ps; # First line is column name "VSZ" chomp @ps; # convert to octets - + @val = map { $_ * $KIBI } @ps ; } - $debug and myprint "@val\n" ; + $sync->{ debug } and myprint( "@val\n" ) ; return( @val ) ; } -sub memory_consumption_of_pids_win32 { +sub memory_consumption_of_pids_win32 +{ # Windows my @PID = @_; my %PID; @@ -9791,7 +12229,8 @@ sub memory_consumption_of_pids_win32 { } -sub tests_backtick { +sub tests_backtick +{ note( 'Entering tests_backtick()' ) ; is( undef, backtick( ), 'backtick: no args' ) ; @@ -9803,11 +12242,11 @@ sub tests_backtick { @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]" ) ; + $sync->{ debug } and myprint( "[@output]" ) ; @output = backtick( 'echo Hello & echo World!' ) ; ok( "Hello \r\n" eq $output[0], 'backtick: echo Hello & echo World! line 1' ) ; ok( "World!\r\n" eq $output[1], 'backtick: echo Hello & echo World! line 2' ) ; - $debug and myprint( "[@output][$output[0]][$output[1]]" ) ; + $sync->{ debug } and myprint( "[@output][$output[0]][$output[1]]" ) ; # Scalar context ok( "Hello World!\r\n" eq backtick( 'echo Hello World!' ), 'backtick: echo Hello World! scalar' ) ; @@ -9821,11 +12260,11 @@ sub tests_backtick { my @output ; @output = backtick( 'echo Hello World!' ) ; ok( "Hello World!\n" eq $output[0], 'backtick: echo Hello World!' ) ; - $debug and myprint( "[@output]" ) ; + $sync->{ debug } and myprint( "[@output]" ) ; @output = backtick( "echo Hello\necho World!" ) ; ok( "Hello\n" eq $output[0], 'backtick: echo Hello; echo World! line 1' ) ; ok( "World!\n" eq $output[1], 'backtick: echo Hello; echo World! line 2' ) ; - $debug and myprint( "[@output]" ) ; + $sync->{ debug } and myprint( "[@output]" ) ; # Scalar context ok( "Hello World!\n" eq backtick( 'echo Hello World!' ), 'backtick: echo Hello World! scalar' ) ; @@ -9834,8 +12273,8 @@ sub tests_backtick { # Return error positive value, that's ok is( undef, backtick( 'false' ), 'backtick: false returns no output' ) ; my $mem = backtick( "ps -o vsz -p $PROCESS_ID" ) ; - $debug and myprint "MEM=$mem\n" ; - + $sync->{ debug } and myprint( "MEM=$mem\n" ) ; + } note( 'Leaving tests_backtick()' ) ; @@ -9843,7 +12282,8 @@ sub tests_backtick { } -sub backtick { +sub backtick +{ my $command = shift ; if ( ! $command ) { return ; } @@ -9856,7 +12296,7 @@ sub backtick { } ; if ( $EVAL_ERROR ) { myprint( $EVAL_ERROR ) ; - return ; + return ; } if ( ! $eval ) { return ; } if ( ! $pid ) { return ; } @@ -9876,7 +12316,91 @@ sub backtick { } -sub remove_not_num { + +sub tests_check_binary_embed_all_dyn_libs +{ + note( 'Entering tests_check_binary_embed_all_dyn_libs()' ) ; + + is( 1, check_binary_embed_all_dyn_libs( ), 'check_binary_embed_all_dyn_libs: no args => 1' ) ; + + note( 'Leaving tests_check_binary_embed_all_dyn_libs()' ) ; + + return ; +} + + +sub check_binary_embed_all_dyn_libs +{ + my @search_dyn_lib_locale = search_dyn_lib_locale( ) ; + + if ( @search_dyn_lib_locale ) + { + myprint( "Found myself $PROGRAM_NAME pid $PROCESS_ID using locale dynamic libraries that seems out of myself:\n" ) ; + myprint( @search_dyn_lib_locale ) ; + if ( $PROGRAM_NAME =~ m{imapsync_bin_Darwin} ) + { + return 0 ; + } + elsif ( $PROGRAM_NAME =~ m{imapsync.*\.exe} ) + { + return 0 ; + } + else + { + # is always ok for non binary + return 1 ; + } + } + else + { + # Found only embedded dynamic lib + myprint( "Found nothing\n" ) ; + return 1 ; + } +} + +sub search_dyn_lib_locale +{ + if ( 'darwin' eq $OSNAME ) + { + return search_dyn_lib_locale_darwin( ) ; + } + if ( 'linux' eq $OSNAME ) + { + return search_dyn_lib_locale_linux( ) ; + } + if ( 'MSWin32' eq $OSNAME ) + { + return search_dyn_lib_locale_MSWin32( ) ; + } +} + +sub search_dyn_lib_locale_darwin +{ + my $command = qq{ lsof -p $PID | grep ' REG ' | grep .dylib | grep -v '/par-' } ; + myprint( "Search non embeded dynamic libs with the command: $command\n" ) ; + return backtick( $command ) ; +} + +sub search_dyn_lib_locale_linux +{ + my $command = qq{ lsof -p $PID | grep ' REG ' | grep -v '/tmp/par-' | grep '\.so' } ; + myprint( "Search non embeded dynamic libs with the command: $command\n" ) ; + return backtick( $command ) ; +} + +sub search_dyn_lib_locale_MSWin32 +{ + my $command = qq{ Listdlls.exe $PID|findstr Strawberry } ; + # $command = qq{ Listdlls.exe $PID|findstr Strawberry } ; + myprint( "Search non embeded dynamic libs with the command: $command\n" ) ; + return qx( $command ) ; +} + + + +sub remove_not_num +{ my $string = shift ; $string =~ tr/0-9//cd ; @@ -9884,7 +12408,8 @@ sub remove_not_num { return( $string ) ; } -sub tests_remove_not_num { +sub tests_remove_not_num +{ note( 'Entering tests_remove_not_num()' ) ; ok( '123' eq remove_not_num( 123 ), 'remove_not_num( 123 )' ) ; @@ -9896,7 +12421,8 @@ sub tests_remove_not_num { return ; } -sub remove_Ko { +sub remove_Ko +{ my $string = shift; if ($string =~ /^(.*)\sKo$/xo) { return($1); @@ -9905,7 +12431,8 @@ sub remove_Ko { } } -sub remove_qq { +sub remove_qq +{ my $string = shift; if ($string =~ /^"(.*)"$/xo) { return($1); @@ -9914,7 +12441,8 @@ sub remove_qq { } } -sub memory_consumption_ratio { +sub memory_consumption_ratio +{ my ($base) = @_; $base ||= 1; @@ -9923,24 +12451,26 @@ sub memory_consumption_ratio { } -sub date_from_rcs { +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" ) ; + #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" ) ; + #myprint( "$d\n" ) ; } return( $d ) ; } -sub tests_date_from_rcs { +sub tests_date_from_rcs +{ note( 'Entering tests_date_from_rcs()' ) ; ok('19-Sep-2015 16:11:07 +0000' @@ -9950,7 +12480,8 @@ sub tests_date_from_rcs { return ; } -sub good_date { +sub good_date +{ # two incoming formats: # header Tue, 24 Aug 2010 16:00:00 +0200 # internal 24-Aug-2010 16:00:00 +0200 @@ -9963,7 +12494,7 @@ sub good_date { 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" ) ; + #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 ; @@ -10017,12 +12548,12 @@ sub good_date { 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" ) ; + #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" ) ; + #myprint( "$d\n" ) ; last SWITCH ; } @@ -10080,7 +12611,8 @@ sub good_date { } -sub tests_good_date { +sub tests_good_date +{ note( 'Entering tests_good_date()' ) ; ok(q{} eq good_date(), 'good_date no arg'); @@ -10102,15 +12634,15 @@ sub tests_good_date { 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 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('"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('"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' ) ; note( 'Leaving tests_good_date()' ) ; @@ -10118,7 +12650,8 @@ sub tests_good_date { } -sub tests_list_keys_in_2_not_in_1 { +sub tests_list_keys_in_2_not_in_1 +{ note( 'Entering tests_list_keys_in_2_not_in_1()' ) ; @@ -10135,62 +12668,197 @@ sub tests_list_keys_in_2_not_in_1 { return ; } -sub list_keys_in_2_not_in_1 { - - my $folders1_ref = shift; - my $folders2_ref = shift; +sub list_keys_in_2_not_in_1 +{ + my $hash_1_ref = shift; + my $hash_2_ref = shift; my @list; - foreach my $folder ( sort keys %{ $folders2_ref } ) { - next if exists $folders1_ref->{$folder}; - push @list, $folder; + foreach my $key ( sort keys %{ $hash_2_ref } ) { + #$debug and print "$folder\n" ; + next if exists $hash_1_ref->{$key} ; + push @list, $key ; } - return(@list); + #$debug and print "@list\n" ; + return( @list ) ; } -sub list_folders_in_2_not_in_1 { +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) ; + 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) ; + @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 ); + return( reverse @h2_folders_not_in_h1 ) ; +} + +sub tests_nb_messages_in_2_not_in_1 +{ + note( 'Entering tests_stats_across_folders()' ) ; + is( undef, nb_messages_in_2_not_in_1( ), 'nb_messages_in_2_not_in_1: no args => undef' ) ; + + my $mysync->{ h1_folders_of_md5 }->{ 'some_id_01' }->{ 'some_folder_01' } = 1 ; + is( 0, nb_messages_in_2_not_in_1( $mysync ), 'nb_messages_in_2_not_in_1: no messages in 2 => 0' ) ; + + $mysync->{ h1_folders_of_md5 }->{ 'some_id_in_1_and_2' }->{ 'some_folder_01' } = 2 ; + $mysync->{ h2_folders_of_md5 }->{ 'some_id_in_1_and_2' }->{ 'some_folder_02' } = 4 ; + + is( 0, nb_messages_in_2_not_in_1( $mysync ), 'nb_messages_in_2_not_in_1: a common message => 0' ) ; + + $mysync->{ h2_folders_of_md5 }->{ 'some_id_in_2_not_in_1' }->{ 'some_folder_02' } = 1 ; + is( 1, nb_messages_in_2_not_in_1( $mysync ), 'nb_messages_in_2_not_in_1: one message in_2_not_in_1 => 1' ) ; + + $mysync->{ h2_folders_of_md5 }->{ 'some_other_id_in_2_not_in_1' }->{ 'some_folder_02' } = 3 ; + is( 2, nb_messages_in_2_not_in_1( $mysync ), 'nb_messages_in_2_not_in_1: two messages in_2_not_in_1 => 2' ) ; + + note( 'Leaving tests_stats_across_folders()' ) ; + return ; +} + +sub nb_messages_in_2_not_in_1 +{ + my $mysync = shift ; + if ( not defined $mysync ) { return ; } + + $mysync->{ nb_messages_in_2_not_in_1 } = scalar( + list_keys_in_2_not_in_1( + $mysync->{ h1_folders_of_md5 }, + $mysync->{ h2_folders_of_md5 } ) ) ; + + return $mysync->{ nb_messages_in_2_not_in_1 } ; } -sub tests_match { - note( 'Entering tests_match()' ) ; +sub nb_messages_in_1_not_in_2 +{ + my $mysync = shift ; + if ( not defined $mysync ) { return ; } - # undef serie - is( undef, match( ), 'match: no args => undef' ) ; - is( undef, match( 'lalala' ), 'match: one args => undef' ) ; + $mysync->{ nb_messages_in_1_not_in_2 } = scalar( + list_keys_in_2_not_in_1( + $mysync->{ h2_folders_of_md5 }, + $mysync->{ h1_folders_of_md5 } ) ) ; - # This one gives 0 under a binary made by pp - # but 1 under "normal" Perl interpreter. So a PAR bug? - #is( 1, match( q{}, q{} ), 'match: q{} =~ q{} => 1' ) ; - - is( 1, match( 'lalala', 'lalala' ), 'match: lalala =~ lalala => 1' ) ; - is( 1, match( 'lalala', '^lalala' ), 'match: lalala =~ ^lalala => 1' ) ; - is( 1, match( 'lalala', 'lalala$' ), 'match: lalala =~ lalala$ => 1' ) ; - is( 1, match( 'lalala', '^lalala$' ), 'match: lalala =~ ^lalala$ => 1' ) ; - is( 1, match( '_lalala_', 'lalala' ), 'match: _lalala_ =~ lalala => 1' ) ; - is( 1, match( 'lalala', '.*' ), 'match: lalala =~ .* => 1' ) ; - is( 1, match( 'lalala', '.' ), 'match: lalala =~ . => 1' ) ; - is( 1, match( '/lalala/', '/lalala/' ), 'match: /lalala/ =~ /lalala/ => 1' ) ; + return $mysync->{ nb_messages_in_1_not_in_2 } ; +} - is( 0, match( 'lalala', 'ooo' ), 'match: lalala =~ ooo => 0' ) ; - is( 0, match( 'lalala', 'lal_ala' ), 'match: lalala =~ lal_ala => 0' ) ; - is( 0, match( 'lalala', '\.' ), 'match: lalala =~ \. => 0' ) ; - is( 0, match( 'lalalaX', '^lalala$' ), 'match: lalalaX =~ ^lalala$ => 0' ) ; - is( 0, match( 'lalala', '/lalala/' ), 'match: lalala =~ /lalala/ => 1' ) ; - is( 1, match( 'LALALA', '(?i:lalala)' ), 'match: LALALA =~ (?i:lalala) => 1' ) ; +sub comment_on_final_diff_in_1_not_in_2 +{ + my $mysync = shift ; + + if ( not defined $mysync + or $mysync->{ justfolders } + or $mysync->{ useuid } + ) + { + return ; + } + + my $nb_identified_h1_messages = scalar( keys %{ $mysync->{ h1_folders_of_md5 } } ) ; + my $nb_identified_h2_messages = scalar( keys %{ $mysync->{ h2_folders_of_md5 } } ) ; + $mysync->{ debug } and myprint( "nb_keys h1_folders_of_md5 $nb_identified_h1_messages\n" ) ; + $mysync->{ debug } and myprint( "nb_keys h2_folders_of_md5 $nb_identified_h2_messages\n" ) ; + + if ( 0 == $nb_identified_h1_messages ) { return ; } + + # Calculate if not yet done + if ( not defined $mysync->{ nb_messages_in_1_not_in_2 } ) + { + nb_messages_in_1_not_in_2( $mysync ) ; + } + + + if ( 0 == $mysync->{ nb_messages_in_1_not_in_2 } ) + { + myprint( "The sync looks good, all $nb_identified_h1_messages identified messages in host1 are on host2.\n" ) ; + } + else + { + myprint( "The sync is not finished, there are $mysync->{ nb_messages_in_1_not_in_2 } identified messages in host1 that are not on host2.\n" ) ; + } + + if ( 1 <= $mysync->{ h1_nb_msg_noheader } ) + { + myprint( "There are $mysync->{ h1_nb_msg_noheader } unidentified messages (usually Sent or Draft messages). To sync them add option --addheader\n" ) ; + } + + return ; +} + +sub comment_on_final_diff_in_2_not_in_1 +{ + my $mysync = shift ; + + if ( not defined $mysync + or $mysync->{ justfolders } + or $mysync->{ useuid } + ) + { + return ; + } + + my $nb_identified_h2_messages = scalar( keys %{ $mysync->{ h2_folders_of_md5 } } ) ; + # Calculate if not yet done + if ( not defined $mysync->{ nb_messages_in_2_not_in_1 } ) + { + nb_messages_in_2_not_in_1( $mysync ) ; + } + + if ( 0 == $mysync->{ nb_messages_in_2_not_in_1 } ) + { + myprint( "The sync is strict, all $nb_identified_h2_messages identified messages in host2 are on host1.\n" ) ; + } + else + { + myprint( "The sync is not strict, there are ", + $mysync->{ nb_messages_in_2_not_in_1 }, + " messages in host2 that are not on host1.", + " Use --delete2 to delete them and have a strict sync.\n" ) ; + } + return ; +} + + +sub tests_match +{ + note( 'Entering tests_match()' ) ; + + # undef serie + is( undef, match( ), 'match: no args => undef' ) ; + is( undef, match( 'lalala' ), 'match: one args => undef' ) ; + + # This one gives 0 under a binary made by pp + # but 1 under "normal" Perl interpreter. So a PAR bug? + #is( 1, match( q{}, q{} ), 'match: q{} =~ q{} => 1' ) ; + + is( 'lalala', match( 'lalala', 'lalala' ), 'match: lalala =~ lalala => lalala' ) ; + is( 'lalala', match( 'lalala', '^lalala' ), 'match: lalala =~ ^lalala => lalala' ) ; + is( 'lalala', match( 'lalala', 'lalala$' ), 'match: lalala =~ lalala$ => lalala' ) ; + is( 'lalala', match( 'lalala', '^lalala$' ), 'match: lalala =~ ^lalala$ => lalala' ) ; + is( '_lalala_', match( '_lalala_', 'lalala' ), 'match: _lalala_ =~ lalala => _lalala_' ) ; + is( 'lalala', match( 'lalala', '.*' ), 'match: lalala =~ .* => lalala' ) ; + is( 'lalala', match( 'lalala', '.' ), 'match: lalala =~ . => lalala' ) ; + is( '/lalala/', match( '/lalala/', '/lalala/' ), 'match: /lalala/ =~ /lalala/ => /lalala/' ) ; + + is( 0, match( 'foo', 's/foo/bar/g' ), 'match: foo =~ s/foo/bar/g => 0' ) ; + is( 's/foo/bar/g', match( 's/foo/bar/g', 's/foo/bar/g' ), 'match: s/foo/bar/g =~ s/foo/bar/g => s/foo/bar/g' ) ; + + + is( 0, match( 'lalala', 'ooo' ), 'match: lalala =~ ooo => 0' ) ; + is( 0, match( 'lalala', 'lal_ala' ), 'match: lalala =~ lal_ala => 0' ) ; + is( 0, match( 'lalala', '\.' ), 'match: lalala =~ \. => 0' ) ; + is( 0, match( 'lalalaX', '^lalala$' ), 'match: lalalaX =~ ^lalala$ => 0' ) ; + is( 0, match( 'lalala', '/lalala/' ), 'match: lalala =~ /lalala/ => 0' ) ; + + is( 'LALALA', match( 'LALALA', '(?i:lalala)' ), 'match: LALALA =~ (?i:lalala) => 1' ) ; is( undef, match( 'LALALA', '(?{`ls /`})' ), 'match: LALALA =~ (?{`ls /`}) => undef' ) ; - is( undef, match( 'LALALA', '(?{print "CACA"})' ), 'match: LALALA =~ (?{print "CACA"}) => undef' ) ; + is( undef, match( 'LALALA', '(?{print "CACA"})' ), 'match: LALALA =~ (?{print "CACA"}) => undef' ) ; is( undef, match( 'CACA', '(??{print "CACA"})' ), 'match: CACA =~ (??{print "CACA"}) => undef' ) ; note( 'Leaving tests_match()' ) ; @@ -10198,15 +12866,16 @@ sub tests_match { return ; } -sub match { +sub match +{ my( $var, $regex ) = @ARG ; # undef cases if ( ( ! defined $var ) or ( ! defined $regex ) ) { return ; } # normal cases - if ( eval { $var =~ $regex } ) { - return 1 ; + if ( eval { $var =~ qr{$regex} } ) { + return $var ; }elsif ( $EVAL_ERROR ) { myprint( "Fatal regex $regex\n" ) ; return ; @@ -10217,7 +12886,8 @@ sub match { } -sub tests_notmatch { +sub tests_notmatch +{ note( 'Entering tests_notmatch()' ) ; # undef serie @@ -10232,7 +12902,7 @@ sub tests_notmatch { # but 0 under "normal" Perl interpreter. So a PAR bug, same in tests_match . #is( 0, notmatch( q{}, q{} ), 'notmatch: q{} !~ q{} => 0' ) ; - is( 0, notmatch( 'lalala', 'lalala' ), 'notmatch: lalala !~ lalala => 0' ) ; + is( 0, notmatch( 'lalala', 'lalala' ), 'notmatch: lalala !~ lalala => 0' ) ; is( 0, notmatch( 'lalala', '^lalala' ), 'notmatch: lalala !~ ^lalala => 0' ) ; is( 0, notmatch( 'lalala', 'lalala$' ), 'notmatch: lalala !~ lalala$ => 0' ) ; is( 0, notmatch( 'lalala', '^lalala$' ), 'notmatch: lalala !~ ^lalala$ => 0' ) ; @@ -10251,7 +12921,8 @@ sub tests_notmatch { return ; } -sub notmatch { +sub notmatch +{ my( $var, $regex ) = @ARG ; # undef cases @@ -10270,30 +12941,32 @@ sub notmatch { } -sub delete_folders_in_2_not_in_1 { +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" ) ; + 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" ) ; + myprint( "Not deleting $folder because of --delete2foldersbutnot $delete2foldersbutnot\n" ) ; next ; } my $res = $sync->{dry} ; # always success in dry mode! - $imap2->unsubscribe( $folder ) if ( ! $sync->{dry} ) ; - $res = $imap2->delete( $folder ) if ( ! $sync->{dry} ) ; + $sync->{imap2}->unsubscribe( $folder ) if ( ! $sync->{dry} ) ; + $res = $sync->{imap2}->delete( $folder ) if ( ! $sync->{dry} ) ; if ( $res ) { - myprint( "Deleted $folder", "$sync->{dry_message}", "\n" ) ; + myprint( "Deleted $folder", "$sync->{dry_message}", "\n" ) ; }else{ - myprint( "Deleting $folder failed", "\n" ) ; + myprint( "Deleting $folder failed", "\n" ) ; } } return ; } -sub delete_folder { +sub delete_folder +{ my ( $mysync, $imap, $folder, $Side ) = @_ ; if ( ! $mysync ) { return ; } if ( ! $imap ) { return ; } @@ -10306,15 +12979,16 @@ sub delete_folder { $res = $imap->delete( $folder ) ; } if ( $res ) { - myprint( "$Side deleted $folder", $mysync->{dry_message}, "\n" ) ; + myprint( "$Side deleted $folder", $mysync->{dry_message}, "\n" ) ; return 1 ; }else{ - myprint( "$Side deleting $folder failed", "\n" ) ; + myprint( "$Side deleting $folder failed", "\n" ) ; return ; } } -sub delete1emptyfolders { +sub delete1emptyfolders +{ my $mysync = shift ; if ( ! $mysync ) { return ; } # abort if no parameter if ( ! $mysync->{delete1emptyfolders} ) { return ; } # abort if --delete1emptyfolders off @@ -10327,30 +13001,30 @@ sub delete1emptyfolders { foreach my $folder ( reverse sort @{ $mysync->{h1_folders_wanted} } ) { my $parenthood = $imap->is_parent( $folder ) ; if ( defined $parenthood and $parenthood ) { - myprint( "Host1 folder $folder has subfolders\n" ) ; + myprint( "Host1: folder $folder has subfolders\n" ) ; $folders_kept{ $folder }++ ; next ; } - my $nb_messages_select = examine_folder_and_count( $imap, $folder, 'Host1' ) ; + my $nb_messages_select = examine_folder_and_count( $mysync, $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" ) ; + 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" ) ; + 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" ) ; + myprint( "Host1: Not deleting $folder\n" ) ; $folders_kept{ $folder }++ ; next ; } - myprint( "Host1 deleting empty folder $folder\n" ) ; + 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... @@ -10366,7 +13040,8 @@ sub delete1emptyfolders { return ; } -sub remove_deleted_folders_from_wanted_list { +sub remove_deleted_folders_from_wanted_list +{ my ( $mysync, %folders_kept ) = @ARG ; my @h1_folders_wanted_init = @{ $mysync->{h1_folders_wanted} } ; @@ -10380,11 +13055,13 @@ sub remove_deleted_folders_from_wanted_list { return ; } -sub examine_folder_and_count { - my ( $imap, $folder, $Side ) = @_ ; + +sub examine_folder_and_count +{ + my ( $mysync, $imap, $folder, $Side ) = @_ ; $Side ||= 'HostX' ; - if ( ! examine_folder( $imap, $folder, $Side ) ) { + if ( ! examine_folder( $mysync, $imap, $folder, $Side ) ) { return ; } my $nb_messages_select = count_from_select( $imap->History ) ; @@ -10392,7 +13069,8 @@ sub examine_folder_and_count { } -sub tests_delete1emptyfolders { +sub tests_delete1emptyfolders +{ note( 'Entering tests_delete1emptyfolders()' ) ; @@ -10403,7 +13081,7 @@ sub tests_delete1emptyfolders { $syncT->{imap1} = $imapT ; is( undef, delete1emptyfolders( $syncT ), q{delete1emptyfolders: undef imap} ) ; - require Test::MockObject ; + require_ok( "Test::MockObject" ) ; $imapT = Test::MockObject->new( ) ; $syncT->{imap1} = $imapT ; @@ -10498,7 +13176,8 @@ sub tests_delete1emptyfolders { return ; } -sub tests_delete1emptyfolders_unit { +sub tests_delete1emptyfolders_unit +{ note( 'Entering tests_delete1emptyfolders_unit()' ) ; my $syncT = shift ; @@ -10519,20 +13198,20 @@ sub tests_delete1emptyfolders_unit { return ; } -sub extract_header { +sub extract_header +{ my $string = shift ; my ( $header ) = split /\n\n/x, $string ; if ( ! $header ) { return( q{} ) ; } - #myprint( "[$header]\n" ) ; + #myprint( "[$header]\n" ) ; return( $header ) ; } -sub tests_extract_header { +sub tests_extract_header +{ note( 'Entering tests_extract_header()' ) ; - - my $h = <<'EOM'; Message-Id: <20100428101817.A66CB162474E@plume.est.belle> Date: Wed, 28 Apr 2010 12:18:17 +0200 (CEST) @@ -10566,19 +13245,19 @@ sub decompose_header{ my ($key, $val ) ; my @line = split /\n|\r\n/x, $string ; foreach my $line ( @line ) { - #myprint( "DDD $line\n" ) ; + #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" ) ; + $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" ) ; + $debugdev and myprint( "DDD V [$val]\n" ) ; @{ $header->{ $key } }[ $LAST ] .= " $val" if $key ; # dirty line? }else{ @@ -10700,7 +13379,8 @@ EOH return ; } -sub tests_epoch { +sub tests_epoch +{ note( 'Entering tests_epoch()' ) ; ok( '1282658400' eq epoch( '24-Aug-2010 16:00:00 +0200' ), 'epoch 24-Aug-2010 16:00:00 +0200 -> 1282658400' ) ; @@ -10714,7 +13394,7 @@ sub tests_epoch { 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' ) ; - + is( '1280671200', epoch( '1-Aug-2010 14:01:00 +0001' ), 'epoch 1-Aug-2010 14:01:00 +0001 -> 1280671200' ) ; is( '946684800', epoch( '00-Jan-0000 00:00:00 +0000' ), 'epoch 1-Aug-2010 14:01:00 +0001 -> 1280671200' ) ; @@ -10722,7 +13402,8 @@ sub tests_epoch { return ; } -sub epoch { +sub epoch +{ # incoming format: # internal date 24-Aug-2010 16:00:00 +0200 @@ -10736,10 +13417,10 @@ sub epoch { 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" ) ; + #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" ) ; + #myprint( "( $mday, $month, $year, $hour, $min, $sec, $sign, $zone_h, $zone_m )\n" ) ; $sign = +1 if ( '+' eq $sign ) ; $sign = $MINUS_ONE if ( '-' eq $sign ) ; @@ -10756,7 +13437,8 @@ sub epoch { return( $time ) ; } -sub tests_add_header { +sub tests_add_header +{ note( 'Entering tests_add_header()' ) ; ok( 'Message-Id: <mistake@imapsync>' eq add_header(), 'add_header no arg' ) ; @@ -10766,7 +13448,8 @@ sub tests_add_header { return ; } -sub add_header { +sub add_header +{ my $header_uid = shift || 'mistake' ; my $header_Message_Id = 'Message-Id: <' . $header_uid . '@imapsync>' ; return( $header_Message_Id ) ; @@ -10775,7 +13458,8 @@ sub add_header { -sub tests_max_line_length { +sub tests_max_line_length +{ note( 'Entering tests_max_line_length()' ) ; ok( 0 == max_line_length( q{} ), 'max_line_length: 0 == null string' ) ; @@ -10797,7 +13481,8 @@ sub tests_max_line_length { return ; } -sub max_line_length { +sub max_line_length +{ my $string = shift ; my $max = 0 ; @@ -10808,7 +13493,8 @@ sub max_line_length { } -sub tests_setlogfile { +sub tests_setlogfile +{ note( 'Entering tests_setlogfile()' ) ; my $mysync = {} ; @@ -10860,7 +13546,7 @@ sub tests_setlogfile { is( "$DEFAULT_LOGDIR/1970_01_01_00_00_02_000_user1_user2_remote_abort.txt", setlogfile( $mysync ), "setlogfile: default is like $DEFAULT_LOGDIR/1970_01_01_00_00_02_000_user1_user2_remote_abort.txt" ) ; - + $mysync = { timestart => 2, user1 => 'user1', @@ -10894,37 +13580,39 @@ sub tests_setlogfile { "setlogfile: logdir undef, $DEFAULT_LOGDIR/1970_01_01_00_00_00_000_us_er1a_______b_u_ser2a_______b.txt" ) ; - + } ; note( 'Leaving tests_setlogfile()' ) ; return ; } -sub setlogfile { +sub setlogfile +{ my( $mysync ) = shift ; - + # When aborting another process the log file name finishes with "_abort.txt" my $abort_suffix = ( $mysync->{abort} ) ? '_abort' : q{} ; # When acting as a proxy the log file name finishes with "_remote.txt" # proxy mode is not done yet my $remote_suffix = ( $mysync->{remote} ) ? '_remote' : q{} ; - + my $suffix = ( filter_forbidden_characters( move_slash( $mysync->{user1} ) ) || q{} ) - . '_' - . ( filter_forbidden_characters( move_slash( $mysync->{user2} ) ) || q{} ) + . '_' + . ( filter_forbidden_characters( move_slash( $mysync->{user2} ) ) || q{} ) . $remote_suffix . $abort_suffix ; $mysync->{logdir} = defined $mysync->{logdir} ? $mysync->{logdir} : $DEFAULT_LOGDIR ; - - $mysync->{logfile} = defined $mysync->{logfile} - ? "$mysync->{logdir}/$mysync->{logfile}" + + $mysync->{logfile} = defined $mysync->{logfile} + ? "$mysync->{logdir}/$mysync->{logfile}" : logfile( $mysync->{timestart}, $suffix, $mysync->{logdir} ) ; return( $mysync->{logfile} ) ; } -sub tests_logfile { +sub tests_logfile +{ note( 'Entering tests_logfile()' ) ; SKIP: { @@ -10942,10 +13630,10 @@ sub tests_logfile { is( '2010_08_24_14_01_01_000_poupinette.txt', logfile( 1_282_658_461, 'poupinette' ), 'logfile: 1_282_658_461 poupinette => 2010_08_24_14_01_01_poupinette.txt' ) ; is( '2010_08_24_14_01_01_000_removeblanks.txt', logfile( 1_282_658_461, ' remove blanks ' ), 'logfile: 1_282_658_461 remove blanks => 2010_08_24_14_01_01_000_removeblanks' ) ; - is( '2010_08_24_14_01_01_234_poup.txt', logfile( 1_282_658_461.2347, 'poup' ), + is( '2010_08_24_14_01_01_234_poup.txt', logfile( 1_282_658_461.2347, 'poup' ), 'logfile: 1_282_658_461.2347 poup => 2010_08_24_14_01_01_234_poup.txt' ) ; - is( 'dirdir/2010_08_24_14_01_01_234_poup.txt', logfile( 1_282_658_461.2347, 'poup', 'dirdir' ), + is( 'dirdir/2010_08_24_14_01_01_234_poup.txt', logfile( 1_282_658_461.2347, 'poup', 'dirdir' ), 'logfile: 1_282_658_461.2347 poup dirdir => dirdir/2010_08_24_14_01_01_234_poup.txt' ) ; @@ -10959,7 +13647,8 @@ sub tests_logfile { } -sub logfile { +sub logfile +{ my ( $time, $suffix, $dir ) = @_ ; $time ||= 0 ; @@ -10979,7 +13668,8 @@ sub logfile { -sub tests_move_slash { +sub tests_move_slash +{ note( 'Entering tests_move_slash()' ) ; is( undef, move_slash( ), 'move_slash: no parameters => undef' ) ; @@ -10989,7 +13679,8 @@ sub tests_move_slash { return ; } -sub move_slash { +sub move_slash +{ my $string = shift ; if ( ! defined $string ) { return ; } @@ -11002,21 +13693,22 @@ sub move_slash { -sub tests_million_folders_baby_2 { +sub tests_million_folders_baby_2 +{ note( 'Entering tests_million_folders_baby_2()' ) ; my %long ; @long{ 1 .. 900_000 } = (1) x 900_000 ; - #myprint( %long, "\n" ) ; + #myprint( %long, "\n" ) ; my $pasglop = 0 ; foreach my $elem ( 1 .. 900_000 ) { - #$debug and myprint( "$elem " ) ; + #$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" ) ; + # myprint( "$pasglop\n" ) ; note( 'Leaving tests_million_folders_baby_2()' ) ; return ; @@ -11024,7 +13716,8 @@ sub tests_million_folders_baby_2 { -sub tests_always_fail { +sub tests_always_fail +{ note( 'Entering tests_always_fail()' ) ; is( 0, 1, 'always_fail: 0 is 1' ) ; @@ -11033,31 +13726,87 @@ sub tests_always_fail { return ; } -sub logfileprepa { - my $logfile = shift ; - my $dirname = dirname( $logfile ) ; - do_valid_directory( $dirname ) || return( 0 ) ; - return( 1 ) ; +sub tests_logfileprepa +{ + note( 'Entering tests_logfileprepa()' ) ; + + is( undef, logfileprepa( ), 'logfileprepa: no args => undef' ) ; + my $logfile = 'W/tmp/tests/tests_logfileprepa.txt' ; + is( 1, logfileprepa( $logfile ), 'logfileprepa: W/tmp/tests/tests_logfileprepa.txt => 1' ) ; + + note( 'Leaving tests_logfileprepa()' ) ; + return ; } -sub teelaunch { +sub logfileprepa +{ + my $logfile = shift ; + + if ( ! defined( $logfile ) ) + { + return ; + }else + { + #myprint( "[$logfile]\n" ) ; + my $dirname = dirname( $logfile ) ; + do_valid_directory( $dirname ) || return( 0 ) ; + return( 1 ) ; + } +} + + +sub tests_teelaunch +{ + note( 'Entering tests_teelaunch()' ) ; + + is( undef, teelaunch( ), 'teelaunch: no args => undef' ) ; + my $mysync = {} ; + is( undef, teelaunch( $mysync ), 'teelaunch: arg empty {} => undef' ) ; + $mysync->{logfile} = '' ; + is( undef, teelaunch( $mysync ), 'teelaunch: logfile empty string => undef' ) ; + $mysync->{logfile} = 'W/tmp/tests/tests_teelaunch.txt' ; + isa_ok( my $tee = teelaunch( $mysync ), 'IO::Tee' , 'teelaunch: logfile W/tmp/tests/tests_teelaunch.txt' ) ; + is( 1, print( $tee "Hi!\n" ), 'teelaunch: write Hi!') ; + is( "Hi!\n", file_to_string( 'W/tmp/tests/tests_teelaunch.txt' ), 'teelaunch: reading W/tmp/tests/tests_teelaunch.txt is Hi!\n' ) ; + is( 1, print( $tee "Hoo\n" ), 'teelaunch: write Hoo') ; + is( "Hi!\nHoo\n", file_to_string( 'W/tmp/tests/tests_teelaunch.txt' ), 'teelaunch: reading W/tmp/tests/tests_teelaunch.txt is Hi!\nHoo\n' ) ; + + note( 'Leaving tests_teelaunch()' ) ; + return ; +} + +sub teelaunch +{ my $mysync = shift ; + + if ( ! defined( $mysync ) ) + { + return ; + } + my $logfile = $mysync->{logfile} ; + + if ( ! $logfile ) + { + return ; + } + logfileprepa( $logfile ) || croak "Error no valid directory to write log file $logfile : $OS_ERROR" ; + # This is a log file opened during the whole sync + ## no critic (InputOutput::RequireBriefOpen) open my $logfile_handle, '>', $logfile or croak( "Can not open $logfile for write: $OS_ERROR" ) ; 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 $tee ; } -sub getpwuid_any_os { +sub getpwuid_any_os +{ my $uid = shift ; return( scalar getlogin ) if ( 'MSWin32' eq $OSNAME ) ; # Windows system @@ -11066,13 +13815,14 @@ sub getpwuid_any_os { } -sub simulong { +sub simulong +{ my $max_seconds = shift ; my $division = 5 ; - my $last = $division * $max_seconds ; - foreach my $i ( 1 .. ( $last ) ) { - myprint( "Are you still here $i/$last\n" ) ; - #myprint( "Are you still here $i/$last\n" . ( "Ah" x 40 . "\n") x 4000 ) ; + my $last_count = $division * $max_seconds ; + foreach my $i ( 1 .. ( $last_count ) ) { + myprint( "Are you still here $i/$last_count\n" ) ; + #myprint( "Are you still here $i/$last_count\n" . ( "Ah" x 40 . "\n") x 4000 ) ; sleep( 1 / $division ) ; } @@ -11081,20 +13831,22 @@ sub simulong { -sub printenv { +sub printenv +{ myprint( "Environment variables listing:\n", - ( map { "$_ => $ENV{$_}\n" } sort keys %ENV), - "Environment variables listing end\n" ) ; + ( map { "$_ => $ENV{$_}\n" } sort keys %ENV), + "Environment variables listing end\n" ) ; return ; } -sub testsexit { +sub testsexit +{ my $mysync = shift ; if ( ! ( $mysync->{ tests } or $mysync->{ testsdebug } or $mysync->{ testsunit } ) ) { return ; } my $test_builder = Test::More->builder ; - tests( $mysync ) ; + tests( $mysync ) ; testsdebug( $mysync ) ; testunitsession( $mysync ) ; @@ -11110,18 +13862,32 @@ sub testsexit { "List of failed tests:\n", $tests_failed ) ; exit $EXIT_TESTS_FAILED ; } - exit ; - #return ; + + cleanup_mess_from_tests( ) ; + # Cover is larger with --tests --testslive + if ( ! $mysync->{ testslive } ) + { + exit ; + } + # $eeee ; + return ; } -sub after_get_options { +sub cleanup_mess_from_tests +{ + undef @pipemess ; + return ; +} + +sub after_get_options +{ my $mysync = shift ; my $numopt = shift ; # exit with --help option or no option at all - $debug and myprint( "numopt:$numopt\n" ) ; - + $mysync->{ debug } and myprint( "numopt:$numopt\n" ) ; + if ( $help or not $numopt ) { myprint( usage( $mysync ) ) ; exit ; @@ -11130,21 +13896,82 @@ sub after_get_options { return ; } -sub easyany { +sub tests_remove_edging_blanks +{ + note( 'Entering tests_remove_edging_blanks()' ) ; + + is( undef, remove_edging_blanks( ), 'remove_edging_blanks: no args => undef' ) ; + is( 'abcd', remove_edging_blanks( 'abcd' ), 'remove_edging_blanks: abcd => abcd' ) ; + is( 'ab cd', remove_edging_blanks( ' ab cd ' ), 'remove_edging_blanks: " ab cd " => "ab cd"' ) ; + + note( 'Leaving tests_remove_edging_blanks()' ) ; + return ; +} + + + +sub remove_edging_blanks +{ + my $string = shift ; + if ( ! defined $string ) + { + return ; + } + $string =~ s,^ +| +$,,g ; + return $string ; +} + + +sub tests_sanitize +{ + note( 'Entering tests_remove_edging_blanks()' ) ; + + is( undef, sanitize( ), 'sanitize: no args => undef' ) ; + my $mysync = {} ; + + $mysync->{ host1 } = ' example.com ' ; + $mysync->{ user1 } = ' to to ' ; + $mysync->{ password1 } = ' sex is good! ' ; + is( undef, sanitize( $mysync ), 'sanitize: => undef' ) ; + is( 'example.com', $mysync->{ host1 }, 'sanitize: host1 " example.com " => "example.com"' ) ; + is( 'to to', $mysync->{ user1 }, 'sanitize: user1 " to to " => "to to"' ) ; + is( 'sex is good!', $mysync->{ password1 }, 'sanitize: password1 " sex is good! " => "sex is good!"' ) ; + note( 'Leaving tests_remove_edging_blanks()' ) ; + return ; +} + + +sub sanitize +{ + my $mysync = shift ; + if ( ! defined $mysync ) + { + return ; + } + + foreach my $parameter ( qw( host1 host2 user1 user2 password1 password2 ) ) + { + $mysync->{ $parameter } = remove_edging_blanks( $mysync->{ $parameter } ) ; + } + return ; +} + +sub easyany +{ my $mysync = shift ; # Gmail if ( $mysync->{gmail1} and $mysync->{gmail2} ) { - $debug and myprint( "gmail1 gmail2\n") ; + $mysync->{ debug } and myprint( "gmail1 gmail2\n") ; gmail12( $mysync ) ; return ; } if ( $mysync->{gmail1} ) { - $debug and myprint( "gmail1\n" ) ; + $mysync->{ debug } and myprint( "gmail1\n" ) ; gmail1( $mysync ) ; } if ( $mysync->{gmail2} ) { - $debug and myprint( "gmail2\n" ) ; + $mysync->{ debug } and myprint( "gmail2\n" ) ; gmail2( $mysync ) ; } # Office 365 @@ -11156,6 +13983,16 @@ sub easyany { office2( $mysync ) ; } + # Exchange + if ( $mysync->{exchange1} ) { + exchange1( $mysync ) ; + } + + if ( $mysync->{exchange2} ) { + exchange2( $mysync ) ; + } + + # Domino if ( $mysync->{domino1} ) { domino1( $mysync ) ; @@ -11165,135 +14002,185 @@ sub easyany { domino2( $mysync ) ; } - return ; } # From https://imapsync.lamiral.info/FAQ.d/FAQ.Gmail.txt -sub gmail12 { +sub gmail12 +{ my $mysync = shift ; # Gmail at host1 and host2 $mysync->{host1} ||= 'imap.gmail.com' ; $mysync->{ssl1} = ( defined $mysync->{ssl1} ) ? $mysync->{ssl1} : 1 ; $mysync->{host2} ||= 'imap.gmail.com' ; $mysync->{ssl2} = ( defined $mysync->{ssl2} ) ? $mysync->{ssl2} : 1 ; - $mysync->{maxbytespersecond} ||= 20_000 ; # should be 10_000 computed from by Gmail documentation + $mysync->{maxbytespersecond} ||= 20_000 ; # should be 10_000 when computed from Gmail documentation $mysync->{maxbytesafter} ||= 1_000_000_000 ; - $mysync->{automap} = ( defined $mysync->{automap} ) ? $mysync->{automap} : 1 ; - $mysync->{maxsleep} = $MAX_SLEEP ; - + $mysync->{automap} = ( defined $mysync->{automap} ) ? $mysync->{automap} : 1 ; + $mysync->{maxsleep} = ( defined $mysync->{maxsleep} ) ? $mysync->{maxsleep} : $MAX_SLEEP ; ; + $skipcrossduplicates = ( defined $skipcrossduplicates ) ? $skipcrossduplicates : 0 ; + $mysync->{ synclabels } = ( defined $mysync->{ synclabels } ) ? $mysync->{ synclabels } : 1 ; + $mysync->{ reynclabels } = ( defined $mysync->{ reynclabels } ) ? $mysync->{ reynclabels } : 1 ; push @exclude, '\[Gmail\]$' ; + push @folderlast, '[Gmail]/All Mail' ; return ; } -sub gmail1 { + +sub gmail1 +{ my $mysync = shift ; # Gmail at host2 $mysync->{host1} ||= 'imap.gmail.com' ; $mysync->{ssl1} = ( defined $mysync->{ssl1} ) ? $mysync->{ssl1} : 1 ; $mysync->{maxbytespersecond} ||= 40_000 ; # should be 20_000 computed from by Gmail documentation $mysync->{maxbytesafter} ||= 2_500_000_000 ; - $mysync->{automap} = ( defined $mysync->{automap} ) ? $mysync->{automap} : 1 ; + $mysync->{automap} = ( defined $mysync->{automap} ) ? $mysync->{automap} : 1 ; + $mysync->{maxsleep} = ( defined $mysync->{maxsleep} ) ? $mysync->{maxsleep} : $MAX_SLEEP ; ; $skipcrossduplicates = ( defined $skipcrossduplicates ) ? $skipcrossduplicates : 1 ; - $mysync->{maxsleep} = $MAX_SLEEP ; - + push @useheader, 'X-Gmail-Received', 'Message-Id' ; - push @regextrans2, 's,\[Gmail\].,,' ; - push @folderlast, '[Gmail]/All Mail' ; + push @{ $mysync->{ regextrans2 } }, 's,\[Gmail\].,,' ; + push @folderlast, '[Gmail]/All Mail' ; return ; } -sub gmail2 { +sub gmail2 +{ my $mysync = shift ; # Gmail at host2 $mysync->{host2} ||= 'imap.gmail.com' ; $mysync->{ssl2} = ( defined $mysync->{ssl2} ) ? $mysync->{ssl2} : 1 ; $mysync->{maxbytespersecond} ||= 20_000 ; # should be 10_000 computed from by Gmail documentation $mysync->{maxbytesafter} ||= 1_000_000_000 ; # In fact it is documented as half: 500_000_000 - $maxsize ||= 25_000_000 ; + #$mysync->{ maxsize } ||= 25_000_000 ; $mysync->{automap} = ( defined $mysync->{automap} ) ? $mysync->{automap} : 1 ; - $skipcrossduplicates = ( defined $skipcrossduplicates ) ? $skipcrossduplicates : 1 ; - $expunge1 = ( defined $expunge1 ) ? $expunge1 : 1 ; + #$skipcrossduplicates = ( defined $skipcrossduplicates ) ? $skipcrossduplicates : 1 ; + $mysync->{ expunge1 } = ( defined $mysync->{ expunge1 } ) ? $mysync->{ expunge1 } : 1 ; $mysync->{addheader} = ( defined $mysync->{addheader} ) ? $mysync->{addheader} : 1 ; - $mysync->{maxsleep} = $MAX_SLEEP ; - - push @exclude, '\[Gmail\]$' ; - push @useheader, 'X-Gmail-Received', 'Message-Id' ; - push @regextrans2, 's,\[Gmail\].,,' ; - push @regextrans2, 's/[ ]+/_/g' ; - push @regextrans2, q{s/['\\^"]/_/g} ; # Verified this - push @folderlast, "[Gmail]/All Mail" ; + $mysync->{maxsleep} = ( defined $mysync->{maxsleep} ) ? $mysync->{maxsleep} : $MAX_SLEEP ; ; + + $mysync->{maxsize} = ( defined $mysync->{maxsize} ) ? $mysync->{maxsize} : $GMAIL_MAXSIZE ; + + if ( ! $mysync->{noexclude} ) { + push @exclude, '\[Gmail\]$' ; + } + push @useheader, 'Message-Id' ; + push @{ $mysync->{ regextrans2 } }, 's,\[Gmail\].,,' ; + + # push @{ $mysync->{ regextrans2 } }, 's/[ ]+/_/g' ; # is now replaced + # by the two more specific following regexes, + # they remove just the beginning and trailing blanks, not all. + push @{ $mysync->{ regextrans2 } }, 's,^ +| +$,,g' ; + push @{ $mysync->{ regextrans2 } }, 's,/ +| +/,/,g' ; + # + push @{ $mysync->{ regextrans2 } }, q{s/['\\^"]/_/g} ; # Verified this + push @folderlast, '[Gmail]/All Mail' ; return ; } # From https://imapsync.lamiral.info/FAQ.d/FAQ.Exchange.txt -sub office1 { +sub office1 +{ # Office 365 at host1 my $mysync = shift ; - $debug and myprint( "office1 configuration\n" ) ; + output( $mysync, q{Option --office1 is like: --host1 outlook.office365.com --ssl1 --exclude "^Files$"} . "\n" ) ; + output( $mysync, "Option --office1 (cont) : unless overrided with --host1 otherhost --nossl1 --noexclude\n" ) ; $mysync->{host1} ||= 'outlook.office365.com' ; $mysync->{ssl1} = ( defined $mysync->{ssl1} ) ? $mysync->{ssl1} : 1 ; + if ( ! $mysync->{noexclude} ) { + push @exclude, '^Files$' ; + } return ; } -sub office2 { - # Office 365 at host1 - my $mysync = shift ; - $mysync->{host2} ||= 'outlook.office365.com' ; - $mysync->{ssl2} = ( defined $mysync->{ssl2} ) ? $mysync->{ssl2} : 1 ; - $maxsize ||= 45_000_000 ; - $mysync->{maxmessagespersecond} ||= 4 ; - push @regexflag, 's/\\Flagged//g' ; - $disarmreadreceipts = ( defined $disarmreadreceipts ) ? $disarmreadreceipts : 1 ; - push @regexmess, 's,(.{10500}),$1\r\n,g' ; - return ; + +sub office2 +{ + # Office 365 at host2 + my $mysync = shift ; + output( $mysync, qq{Option --office2 is like: --host2 outlook.office365.com --ssl2 --maxsize 45_000_000 --maxmessagespersecond 4\n} ) ; + output( $mysync, qq{Option --office2 (cont) : --disarmreadreceipts --regexmess "wrap 10500" --f1f2 "Files=Files_renamed_by_imapsync"\n} ) ; + output( $mysync, qq{Option --office2 (cont) : unless overrided with --host2 otherhost --nossl2 ... --nodisarmreadreceipts --noregexmess\n} ) ; + output( $mysync, qq{Option --office2 (cont) : and --nof1f2 to avoid Files folder renamed to Files_renamed_by_imapsync\n} ) ; + $mysync->{host2} ||= 'outlook.office365.com' ; + $mysync->{ssl2} = ( defined $mysync->{ssl2} ) ? $mysync->{ssl2} : 1 ; + $mysync->{ maxsize } ||= 45_000_000 ; + $mysync->{maxmessagespersecond} ||= 4 ; + #push @regexflag, 's/\\\\Flagged//g' ; # No problem without! tested 2018_09_10 + $disarmreadreceipts = ( defined $disarmreadreceipts ) ? $disarmreadreceipts : 1 ; + # I dislike double negation but here is one + if ( ! $mysync->{noregexmess} ) + { + push @regexmess, 's,(.{10500}),$1\r\n,g' ; + } + # and another... + if ( ! $mysync->{nof1f2} ) + { + push @{ $mysync->{f1f2} }, 'Files=Files_renamed_by_imapsync' ; + } + return ; } -sub exchange1 { - # Exchange 2010/2013 at host1 - - # Well nothing to do so far - return ; +sub exchange1 +{ + # Exchange 2010/2013 at host1 + my $mysync = shift ; + output( $mysync, "Option --exchange1 does nothing (except printing this line...)\n" ) ; + # Well nothing to do so far + return ; } -sub exchange2 { - # Exchange 2010/2013 at host2 - my $mysync = shift ; - $maxsize ||= 10_000_000 ; - $mysync->{maxmessagespersecond} ||= 4 ; - push @regexflag, 's/\\Flagged//g' ; - $disarmreadreceipts = ( defined $disarmreadreceipts ) ? $disarmreadreceipts : 1 ; - push @regexmess, 's,(.{10500}),$1\r\n,g' ; - return ; +sub exchange2 +{ + # Exchange 2010/2013 at host2 + my $mysync = shift ; + output( $mysync, "Option --exchange2 is like: --maxsize 10_000_000 --maxmessagespersecond 4 --disarmreadreceipts\n" ) ; + output( $mysync, "Option --exchange2 (cont) : --regexflag del Flagged --regexmess wrap 10500\n" ) ; + output( $mysync, "Option --exchange2 (cont) : unless overrided with --maxsize xxx --nodisarmreadreceipts --noregexflag --noregexmess\n" ) ; + $mysync->{ maxsize } ||= 10_000_000 ; + $mysync->{maxmessagespersecond} ||= 4 ; + $disarmreadreceipts = ( defined $disarmreadreceipts ) ? $disarmreadreceipts : 1 ; + # I dislike double negation but here are two + if ( ! $mysync->{noregexflag} ) { + push @regexflag, 's/\\\\Flagged//g' ; + } + if ( ! $mysync->{noregexmess} ) { + push @regexmess, 's,(.{10500}),$1\r\n,g' ; + } + return ; } -sub domino1 { +sub domino1 +{ # Domino at host1 my $mysync = shift ; - $sep1 = q{\\} ; + $mysync->{ sep1 } = q{\\} ; $prefix1 = q{} ; $messageidnodomain = ( defined $messageidnodomain ) ? $messageidnodomain : 1 ; return ; } -sub domino2 { +sub domino2 +{ # Domino at host1 my $mysync = shift ; - $sep2 = q{\\} ; + $mysync->{ sep2 } = q{\\} ; $prefix2 = q{} ; $messageidnodomain = ( defined $messageidnodomain ) ? $messageidnodomain : 1 ; - push @regextrans2, 's,^Inbox\\\\(.*),$1,i' ; + push @{ $mysync->{ regextrans2 } }, 's,^Inbox\\\\(.*),$1,i' ; return ; } -sub tests_resolv { +sub tests_resolv +{ note( 'Entering tests_resolv()' ) ; - + # is( , resolv( ), 'resolv: => ' ) ; is( undef, resolv( ), 'resolv: no args => undef' ) ; is( undef, resolv( '' ), 'resolv: empty string => undef' ) ; @@ -11301,7 +14188,7 @@ sub tests_resolv { is( '127.0.0.1', resolv( '127.0.0.1' ), 'resolv: 127.0.0.1 => 127.0.0.1' ) ; is( '127.0.0.1', resolv( 'localhost' ), 'resolv: localhost => 127.0.0.1' ) ; is( '5.135.158.182', resolv( 'imapsync.lamiral.info' ), 'resolv: imapsync.lamiral.info => 5.135.158.182' ) ; - + # ip6-localhost ( in /etc/hosts ) is( '::1', resolv( 'ip6-localhost' ), 'resolv: ip6-localhost => ::1' ) ; is( '::1', resolv( '::1' ), 'resolv: ::1 => ::1' ) ; @@ -11311,64 +14198,67 @@ sub tests_resolv { # ks3 is( '2001:41d0:8:bebd::1', resolv( '2001:41d0:8:bebd::1' ), 'resolv: 2001:41d0:8:bebd::1 => 2001:41d0:8:bebd::1' ) ; is( '2001:41d0:8:bebd::1', resolv( 'ks3ipv6.lamiral.info' ), 'resolv: ks3ipv6.lamiral.info => 2001:41d0:8:bebd::1' ) ; - - + + note( 'Leaving tests_resolv()' ) ; return ; } -sub resolv { +sub resolv +{ my $host = shift @ARG ; - + if ( ! $host ) { return ; } my $addr ; if ( defined &Socket::getaddrinfo ) { $addr = resolv_with_getaddrinfo( $host ) ; return( $addr ) ; } - - - + + + my $iaddr = inet_aton( $host ) ; if ( ! $iaddr ) { return ; } $addr = inet_ntoa( $iaddr ) ; - + return $addr ; } -sub resolv_with_getaddrinfo { +sub resolv_with_getaddrinfo +{ my $host = shift @ARG ; - + if ( ! $host ) { return ; } - my ( $err, @res ) = Socket::getaddrinfo( $host, "", { socktype => Socket::SOCK_RAW } ) ; - if ( $err ) { - myprint( "Cannot getaddrinfo of $host: $err\n" ) ; + my ( $err_getaddrinfo, @res ) = Socket::getaddrinfo( $host, "", { socktype => Socket::SOCK_RAW } ) ; + if ( $err_getaddrinfo ) { + myprint( "Cannot getaddrinfo of $host: $err_getaddrinfo\n" ) ; return ; } my @addr ; while( my $ai = shift @res ) { - my ( $err, $ipaddr ) = Socket::getnameinfo( $ai->{addr}, Socket::NI_NUMERICHOST(), Socket::NIx_NOSERV() ) ; - if ( $err ) { - myprint( "Cannot getnameinfo of $host: $err\n" ) ; + my ( $err_getnameinfo, $ipaddr ) = Socket::getnameinfo( $ai->{addr}, Socket::NI_NUMERICHOST(), Socket::NIx_NOSERV() ) ; + if ( $err_getnameinfo ) { + myprint( "Cannot getnameinfo of $host: $err_getnameinfo\n" ) ; return ; } - $debug and myprint( "$host => $ipaddr\n" ) ; + $sync->{ debug } and myprint( "$host => $ipaddr\n" ) ; push @addr, $ipaddr ; - - my ( $err_r, $reverse ) = Socket::getnameinfo( $ai->{addr}, 0, Socket::NIx_NOSERV() ) ; - $debug and myprint( "$host => $ipaddr => $reverse\n" ) ; + my $reverse ; + ( $err_getnameinfo, $reverse ) = Socket::getnameinfo( $ai->{addr}, 0, Socket::NIx_NOSERV() ) ; + $sync->{ debug } and myprint( "$host => $ipaddr => $reverse\n" ) ; } - + return $addr[0] ; } -sub tests_resolvrev { +sub tests_resolvrev +{ note( 'Entering tests_resolvrev()' ) ; - + # is( , resolvrev( ), 'resolvrev: => ' ) ; is( undef, resolvrev( ), 'resolvrev: no args => undef' ) ; is( undef, resolvrev( '' ), 'resolvrev: empty string => undef' ) ; @@ -11376,7 +14266,7 @@ sub tests_resolvrev { is( 'localhost', resolvrev( '127.0.0.1' ), 'resolvrev: 127.0.0.1 => localhost' ) ; is( 'localhost', resolvrev( 'localhost' ), 'resolvrev: localhost => localhost' ) ; is( 'ks.lamiral.info', resolvrev( 'imapsync.lamiral.info' ), 'resolvrev: imapsync.lamiral.info => ks.lamiral.info' ) ; - + # ip6-localhost ( in /etc/hosts ) is( 'ip6-localhost', resolvrev( 'ip6-localhost' ), 'resolvrev: ip6-localhost => ip6-localhost' ) ; is( 'ip6-localhost', resolvrev( '::1' ), 'resolvrev: ::1 => ip6-localhost' ) ; @@ -11386,28 +14276,30 @@ sub tests_resolvrev { # ks3 is( 'ks3ipv6.lamiral.info', resolvrev( '2001:41d0:8:bebd::1' ), 'resolvrev: 2001:41d0:8:bebd::1 => ks3ipv6.lamiral.info' ) ; is( 'ks3ipv6.lamiral.info', resolvrev( 'ks3ipv6.lamiral.info' ), 'resolvrev: ks3ipv6.lamiral.info => ks3ipv6.lamiral.info' ) ; - - + + note( 'Leaving tests_resolvrev()' ) ; return ; } -sub resolvrev { +sub resolvrev +{ my $host = shift @ARG ; - + if ( ! $host ) { return ; } if ( defined &Socket::getaddrinfo ) { my $name = resolvrev_with_getaddrinfo( $host ) ; return( $name ) ; } - + return ; } -sub resolvrev_with_getaddrinfo { +sub resolvrev_with_getaddrinfo +{ my $host = shift @ARG ; - + if ( ! $host ) { return ; } my ( $err, @res ) = Socket::getaddrinfo( $host, "", { socktype => Socket::SOCK_RAW } ) ; @@ -11423,16 +14315,17 @@ sub resolvrev_with_getaddrinfo { myprint( "Cannot getnameinfo of $host: $err\n" ) ; return ; } - $debug and myprint( "$host => $reverse\n" ) ; + $sync->{ debug } and myprint( "$host => $reverse\n" ) ; push @name, $reverse ; } - + return $name[0] ; } -sub tests_imapsping { +sub tests_imapsping +{ note( 'Entering tests_imapsping()' ) ; is( undef, imapsping( ), 'imapsping: no args => undef' ) ; @@ -11443,12 +14336,14 @@ sub tests_imapsping { return ; } -sub imapsping { +sub imapsping +{ my $host = shift ; return tcpping( $host, $IMAP_SSL_PORT ) ; } -sub tests_tcpping { +sub tests_tcpping +{ note( 'Entering tests_tcpping()' ) ; is( undef, tcpping( ), 'tcpping: no args => undef' ) ; @@ -11472,7 +14367,8 @@ sub tests_tcpping { return ; } -sub tcpping { +sub tcpping +{ if ( 2 != scalar( @ARG ) ) { return ; } @@ -11489,8 +14385,8 @@ sub tcpping { $p->hires( 1 ) ; my ($ping_ok, $rtt, $ip ) = $p->ping( $host, $mytimeout ) ; if ( ! defined $ping_ok ) { return ; } - my $rtt_approx = sprintf( "%.3f", $rtt ) ; - $debug and myprint( "Host $host timeout $mytimeout port $port ok $ping_ok ip $ip acked in $rtt_approx s\n" ) ; + my $rtt_approx = sprintf( "%.3f", $rtt ) ; + $sync->{ debug } and myprint( "Host $host timeout $mytimeout port $port ok $ping_ok ip $ip acked in $rtt_approx s\n" ) ; $p->close( ) ; if( $ping_ok ) { return 1 ; @@ -11499,7 +14395,8 @@ sub tcpping { } } -sub tests_sslcheck { +sub tests_sslcheck +{ note( 'Entering tests_sslcheck()' ) ; my $mysync ; @@ -11537,7 +14434,7 @@ sub tests_sslcheck { host1 => 'imapsync.lamiral.info', host2 => 'test2.lamiral.info', } ; - + is( 2, sslcheck( $mysync ), 'sslcheck: imapsync.lamiral.info + test2.lamiral.info => 2' ) ; $mysync = { @@ -11546,21 +14443,22 @@ sub tests_sslcheck { host2 => 'test2.lamiral.info', tls1 => 1, } ; - + is( 1, sslcheck( $mysync ), 'sslcheck: imapsync.lamiral.info + test2.lamiral.info + tls1 => 1' ) ; - + note( 'Leaving tests_sslcheck()' ) ; return ; } -sub sslcheck { +sub sslcheck +{ my $mysync = shift ; if ( ! $mysync->{sslcheck} ) { return ; } my $nb_on = 0 ; - $debug and myprint( "sslcheck\n" ) ; + $mysync->{ debug } and myprint( "sslcheck\n" ) ; if ( ( ! defined $mysync->{port1} ) and @@ -11602,30 +14500,33 @@ sub sslcheck { } -sub testslive { +sub testslive +{ my $mysync = shift ; - $mysync->{host1} = 'test1.lamiral.info' ; - $mysync->{user1} = 'test1' ; - $mysync->{password1} = 'secret1' ; - $mysync->{host2} = 'test2.lamiral.info' ; - $mysync->{user2} = 'test2' ; - $mysync->{password2} ='secret2' ; + $mysync->{host1} ||= 'test1.lamiral.info' ; + $mysync->{user1} ||= 'test1' ; + $mysync->{password1} ||= 'secret1' ; + $mysync->{host2} ||= 'test2.lamiral.info' ; + $mysync->{user2} ||= 'test2' ; + $mysync->{password2} ||= 'secret2' ; return ; } -sub testslive6 { +sub testslive6 +{ my $mysync = shift ; - $mysync->{host1} = 'ks2ipv6.lamiral.info' ; - $mysync->{user1} = 'test1' ; - $mysync->{password1} = 'secret1' ; - $mysync->{host2} = 'ks2ipv6.lamiral.info' ; - $mysync->{user2} = 'test2' ; - $mysync->{password2} ='secret2' ; + $mysync->{host1} ||= 'ks2ipv6.lamiral.info' ; + $mysync->{user1} ||= 'test1' ; + $mysync->{password1} ||= 'secret1' ; + $mysync->{host2} ||= 'ks2ipv6.lamiral.info' ; + $mysync->{user2} ||= 'test2' ; + $mysync->{password2} ||= 'secret2' ; return ; } -sub tests_backslash_caret { +sub tests_backslash_caret +{ note( 'Entering tests_backslash_caret()' ) ; is( "lalala", backslash_caret( "lalala" ), 'backslash_caret: lalala => lalala' ) ; @@ -11643,56 +14544,62 @@ sub tests_backslash_caret { return ; } -sub backslash_caret { +sub backslash_caret +{ my $string = shift ; - + $string =~ s{\\ $ }{^}gxms ; return $string ; } -sub tests_split_around_equal { +sub tests_split_around_equal +{ note( 'Entering tests_split_around_equal()' ) ; + is( undef, split_around_equal( ), 'split_around_equal: no args => undef' ) ; is_deeply( { toto => 'titi' }, { split_around_equal( 'toto=titi' ) }, 'split_around_equal: toto=titi => toto => titi' ) ; is_deeply( { A => 'B', C => 'D' }, { split_around_equal( 'A=B=C=D' ) }, 'split_around_equal: toto=titi => toto => titi' ) ; is_deeply( { A => 'B', C => 'D' }, { split_around_equal( 'A=B', 'C=D' ) }, 'split_around_equal: A=B C=D => A => B, C=>D' ) ; - + note( 'Leaving tests_split_around_equal()' ) ; return ; } -sub split_around_equal { +sub split_around_equal +{ if ( ! @ARG ) { return ; } ; return map { split /=/mxs, $_ } @ARG ; - + } -sub tests_sig_install { +sub tests_sig_install +{ note( 'Entering tests_sig_install()' ) ; - my $mysync ; + + my $mysync ; is( undef, sig_install( ), 'sig_install: no args => undef' ) ; is( undef, sig_install( $mysync ), 'sig_install: arg undef => undef' ) ; $mysync = { } ; is( undef, sig_install( $mysync ), 'sig_install: empty hash => undef' ) ; - + SKIP: { Readonly my $SKIP_15 => 15 ; if ( 'MSWin32' eq $OSNAME ) { skip( 'Tests only for Unix', $SKIP_15 ) ; } # Default to ignore USR1 USR2 in case future install fails local $SIG{ USR1 } = local $SIG{ USR2 } = sub { } ; kill( 'USR1', $PROCESS_ID ) ; - + $mysync->{ debugsig } = 1 ; - # Assign USR1 to call sub tototo + # Assign USR1 to call sub tototo # Surely a better value than undef should be returned when doing real signal stuff is( undef, sig_install( $mysync, \&tototo, 'USR1' ), 'sig_install: USR1 tototo' ) ; is( 1, kill( 'USR1', $PROCESS_ID ), 'sig_install: kill USR1 myself 1' ) ; is( 1, $mysync->{ tototo_calls }, 'sig_install: tototo call nb 1' ) ; - - # Assign USR2 to call sub tototo + + # Assign USR2 to call sub tototo is( undef, sig_install( $mysync, \&tototo, 'USR2' ), 'sig_install: USR2 tototo' ) ; is( 1, kill( 'USR2', $PROCESS_ID ), 'sig_install: kill USR2 myself 1' ) ; @@ -11701,87 +14608,104 @@ sub tests_sig_install { is( 1, kill( 'USR1', $PROCESS_ID ), 'sig_install: kill USR1 myself 2' ) ; is( 3, $mysync->{ tototo_calls }, 'sig_install: tototo call nb 3' ) ; - + local $SIG{ USR1 } = local $SIG{ USR2 } = sub { } ; is( 1, kill( 'USR1', $PROCESS_ID ), 'sig_install: kill USR1 myself 3' ) ; is( 3, $mysync->{ tototo_calls }, 'sig_install: tototo call still nb 3' ) ; - - # Assign USR1 + USR2 to call sub tototo + + # Assign USR1 + USR2 to call sub tototo is( undef, sig_install( $mysync, \&tototo, 'USR1', 'USR2' ), 'sig_install: USR1 USR2 tototo' ) ; is( 1, kill( 'USR1', $PROCESS_ID ), 'sig_install: kill USR1 myself 4' ) ; is( 4, $mysync->{ tototo_calls }, 'sig_install: tototo call now nb 4' ) ; - + is( 1, kill( 'USR2', $PROCESS_ID ), 'sig_install: kill USR1 myself 2' ) ; is( 5, $mysync->{ tototo_calls }, 'sig_install: tototo call now nb 5' ) ; } - - note( 'Leaving tests_sig_install()' ) ; + + note( 'Leaving tests_sig_install()' ) ; return ; } - -sub sig_install { +# +sub sig_install +{ my $mysync = shift ; if ( ! $mysync ) { return ; } my $mysub = shift ; if ( ! $mysub ) { return ; } - + my @signals = @ARG ; - - $mysync->{ debugsig } and myprint( "In sig_install with $mysync and $mysub\n" ) ; - + + $mysync->{ debugsig } and myprint( "In sig_install with $mysync and $mysub\n" ) ; + my $subsignal = sub { my $signame = shift ; - $mysync->{ debugsig } and myprint( "In subsignal with $signame and $mysync\n" ) ; + $mysync->{ debugsig } and myprint( "In subsignal with $signame and $mysync\n" ) ; &$mysub( $mysync, $signame ) ; } ; foreach my $signal ( @signals ) { $mysync->{ debugsig } and myprint( "Installing signal $signal for $subsignal\n") ; output( $mysync, "kill -$signal $PROCESS_ID # special behavior\n" ) ; + ## no critic (RequireLocalizedPunctuationVars) $SIG{ $signal } = $subsignal ; } return ; } -sub tototo { +sub tototo +{ my $mysync = shift ; myprint("In tototo with @ARG\n" ) ; $mysync->{ tototo_calls } += 1 ; return ; } -sub tests_toggle_sleep { +sub mygetppid +{ + if ( 'MSWin32' eq $OSNAME ) { + return( 'unknown under MSWin32 (too complicated)' ) ; + } else { + # Unix + return( getppid( ) ) ; + } +} + + + +sub tests_toggle_sleep +{ note( 'Entering tests_toggle_sleep()' ) ; + is( undef, toggle_sleep( ), 'toggle_sleep: no args => undef' ) ; my $mysync ; is( undef, toggle_sleep( $mysync ), 'toggle_sleep: undef => undef' ) ; $mysync = { } ; is( undef, toggle_sleep( $mysync ), 'toggle_sleep: no maxsleep => undef' ) ; - + $mysync->{maxsleep} = 3 ; is( 0, toggle_sleep( $mysync ), 'toggle_sleep: 3 => 0' ) ; - + is( $MAX_SLEEP, toggle_sleep( $mysync ), "toggle_sleep: 0 => $MAX_SLEEP" ) ; is( 0, toggle_sleep( $mysync ), "toggle_sleep: $MAX_SLEEP => 0" ) ; is( $MAX_SLEEP, toggle_sleep( $mysync ), "toggle_sleep: 0 => $MAX_SLEEP" ) ; is( 0, toggle_sleep( $mysync ), "toggle_sleep: $MAX_SLEEP => 0" ) ; - + SKIP: { Readonly my $SKIP_9 => 9 ; if ( 'MSWin32' eq $OSNAME ) { skip( 'Tests only for Unix', $SKIP_9 ) ; } # Default to ignore USR1 USR2 in case future install fails local $SIG{ USR1 } = sub { } ; kill( 'USR1', $PROCESS_ID ) ; - + $mysync->{ debugsig } = 1 ; - # Assign USR1 to call sub toggle_sleep + # Assign USR1 to call sub toggle_sleep is( undef, sig_install( $mysync, \&toggle_sleep, 'USR1' ), 'toggle_sleep: install USR1 toggle_sleep' ) ; - + $mysync->{maxsleep} = 4 ; is( 1, kill( 'USR1', $PROCESS_ID ), 'toggle_sleep: kill USR1 myself' ) ; is( 0, $mysync->{ maxsleep }, 'toggle_sleep: toggle_sleep called => sleeps are 0s' ) ; @@ -11795,28 +14719,30 @@ sub tests_toggle_sleep { is( 1, kill( 'USR1', $PROCESS_ID ), 'toggle_sleep: kill USR1 myself again' ) ; is( $MAX_SLEEP, $mysync->{ maxsleep }, "toggle_sleep: toggle_sleep called => sleeps are ${MAX_SLEEP}s" ) ; } - + note( 'Leaving tests_toggle_sleep()' ) ; return ; } -sub toggle_sleep { +sub toggle_sleep +{ my $mysync = shift ; - + myprint("In toggle_sleep with @ARG\n" ) ; if ( !defined( $mysync ) ) { return ; } if ( !defined( $mysync->{maxsleep} ) ) { return ; } - + $mysync->{ maxsleep } = max( 0, $MAX_SLEEP - $mysync->{maxsleep} ) ; myprint("Resetting maxsleep to ", $mysync->{maxsleep}, "s\n" ) ; return $mysync->{maxsleep} ; } -sub mypod2usage { +sub mypod2usage +{ my $fh_pod2usage = shift ; - + pod2usage( -exitval => 'NOEXIT', -noperldoc => 1, @@ -11830,21 +14756,22 @@ sub mypod2usage { return ; } -sub usage { +sub usage +{ my $mysync = shift ; - + if ( ! defined $mysync ) { return ; } - + my $usage = q{} ; my $usage_from_pod ; my $usage_footer = usage_footer( $mysync ) ; # pod2usage writes on a filehandle only and I want a variable - open my $fh_pod2usage, ">", \$usage_from_pod + open my $fh_pod2usage, ">", \$usage_from_pod or do { warn $OS_ERROR ; return ; } ; mypod2usage( $fh_pod2usage ) ; close $fh_pod2usage ; - + if ( 'MSWin32' eq $OSNAME ) { $usage_from_pod = backslash_caret( $usage_from_pod ) ; } @@ -11853,7 +14780,10 @@ sub usage { return( $usage ) ; } -sub tests_usage { +sub tests_usage +{ + note( 'Entering tests_usage()' ) ; + my $usage ; like( $usage = usage( $sync ), qr/Name:/, 'usage: contains Name:' ) ; myprint( $usage ) ; @@ -11862,13 +14792,16 @@ sub tests_usage { like( $usage, qr/imapsync/, 'usage: contains imapsync' ) ; is( undef, usage( ), 'usage: no args => undef' ) ; + + note( 'Leaving tests_usage()' ) ; return ; } -sub usage_footer { +sub usage_footer +{ my $mysync = shift ; - + my $footer = q{} ; my $localhost_info = localhost_info( $mysync ) ; @@ -11876,11 +14809,11 @@ sub usage_footer { my $homepage = homepage( ) ; my $imapsync_release = $STR_use_releasecheck ; - + if ( $mysync->{releasecheck} ) { $imapsync_release = check_last_release( ) ; } - + $footer = qq{$localhost_info $rcs $imapsync_release @@ -11891,7 +14824,8 @@ $homepage -sub usage_complete { +sub usage_complete +{ # Unused, I guess this function could be deleted my $usage = <<'EOF' ; --skipheader reg : Don't take into account header keyword @@ -11909,14 +14843,15 @@ sub usage_complete { --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. + default is like --split1 100. --split2 int : same thing on host2. --nofixInboxINBOX : Don't fix Inbox INBOX mapping. EOF return( $usage ) ; } -sub myGetOptions { +sub myGetOptions +{ # Started as a copy of Luke Ross Getopt::Long::CGI # https://metacpan.org/release/Getopt-Long-CGI @@ -11926,7 +14861,7 @@ sub myGetOptions { my $mysync = shift @ARG ; my $arguments_ref = shift @ARG ; my %options = @ARG ; - + my $mycgi = $mysync->{cgi} ; if ( not under_cgi_context() ) { @@ -11936,7 +14871,7 @@ sub myGetOptions { } # We must be in CGI context now - if ( !defined( $mycgi ) ) { return ; } + if ( ! defined( $mycgi ) ) { return ; } my $badthings = 0 ; foreach my $key ( sort keys %options ) { @@ -11986,20 +14921,38 @@ sub myGetOptions { } if ( ( $3 || q{} ) eq '@' ) { @{ ${$val} } = @values ; + my @option = map +( "--$name", "$_" ), @values ; + push @{ $mysync->{ cmdcgi } }, @option ; } elsif ( ref( $val ) eq 'ARRAY' ) { @{$val} = @values ; } - else { - ${$val} = $values[0] ; + elsif ( my $value = $values[0] ) + { + ${$val} = $value ; + push @{ $mysync->{ cmdcgi } }, "--$name", $value ; + } + else + { + } } } - else { + else + { # Checkbox # Considers only --name # Should consider also --no-name and --noname - ${$val} = $mycgi->param( $name ) ? 1 : undef ; + my $value = $mycgi->param( $name ) ; + if ( $value ) + { + ${$val} = 1 ; + push @{ $mysync->{ cmdcgi } }, "--$name" ; + } + else + { + ${$val} = undef ; + } } } if ( $badthings ) { @@ -12013,7 +14966,8 @@ sub myGetOptions { my @blabla ; # just used to check get_options_cgi() with an array -sub tests_get_options_cgi_context { +sub tests_get_options_cgi_context +{ note( 'Entering tests_get_options_cgi()' ) ; # Temporary, have to think harder about testing CGI context in command line --tests @@ -12051,7 +15005,7 @@ sub tests_get_options_cgi_context { is( 36, get_options( $mysync, ), 'get_options cgi context: QUERY_STRING => 36' ) ; is( 'test1', $mysync->{user1}, 'get_options cgi context: $mysync->{user1} => test1' ) ; #local $ENV{'QUERY_STRING'} = undef ; - + # Testing @ $mysync->{cgi} = CGI->new( 'blabla=fd1' ) ; get_options( $mysync ) ; @@ -12063,15 +15017,15 @@ sub tests_get_options_cgi_context { # Testing s@ as ref $mysync->{cgi} = CGI->new( 'folder=fd1' ) ; get_options( $mysync ) ; - is_deeply( [ 'fd1' ], $mysync->{folder}, 'get_options cgi context: $mysync->{folder} => fd1' ) ; + is_deeply( [ 'fd1' ], $mysync->{ folder }, 'get_options cgi context: $mysync->{ folder } => fd1' ) ; $mysync->{cgi} = CGI->new( 'folder=fd1&folder=fd2' ) ; get_options( $mysync ) ; - is_deeply( [ 'fd1', 'fd2' ], $mysync->{folder}, 'get_options cgi context: $mysync->{folder} => fd1, fd2' ) ; + is_deeply( [ 'fd1', 'fd2' ], $mysync->{ folder }, 'get_options cgi context: $mysync->{ folder } => fd1, fd2' ) ; # Testing % $mysync->{cgi} = CGI->new( 'f1f2h=s1=d1&f1f2h=s2=d2&f1f2h=s3=d3' ) ; get_options( $mysync ) ; - + is_deeply( { 's1' => 'd1', 's2' => 'd2', 's3' => 'd3' }, $mysync->{f1f2h}, 'get_options cgi context: f1f2h => s1=d1 s2=d2 s3=d3' ) ; @@ -12083,29 +15037,36 @@ sub tests_get_options_cgi_context { $mysync->{cgi} = CGI->new( 'host1=example.com' ) ; get_options( $mysync ) ; is( 'example.com', $mysync->{host1}, 'get_options cgi context: --host1=example.com => $mysync->{host1} => example.com' ) ; - + #myprint( Data::Dumper->Dump( [ $mysync ] ) ) ; $mysync->{cgi} = CGI->new( 'simulong=' ) ; get_options( $mysync ) ; is( undef, $mysync->{simulong}, 'get_options cgi context: --simulong= => $mysync->{simulong} => undef' ) ; - + $mysync->{cgi} = CGI->new( 'simulong' ) ; get_options( $mysync ) ; is( undef, $mysync->{simulong}, 'get_options cgi context: --simulong => $mysync->{simulong} => undef' ) ; - + $mysync->{cgi} = CGI->new( 'simulong=4' ) ; get_options( $mysync ) ; is( 4, $mysync->{simulong}, 'get_options cgi context: --simulong=4 => $mysync->{simulong} => 4' ) ; - is( undef, $mysync->{folder}, 'get_options cgi context: --simulong=4 => $mysync->{folder} => undef' ) ; + is( undef, $mysync->{ folder }, 'get_options cgi context: --simulong=4 => $mysync->{ folder } => undef' ) ; #myprint( Data::Dumper->Dump( [ $mysync ] ) ) ; - + + $mysync ={} ; + $mysync->{cgi} = CGI->new( 'justfoldersizes=on' ) ; + get_options( $mysync ) ; + is( 1, $mysync->{ justfoldersizes }, 'get_options cgi context: --justfoldersizes=1 => justfoldersizes => 1' ) ; + myprint( Data::Dumper->Dump( [ $mysync ] ) ) ; + note( 'Leaving tests_get_options_cgi_context()' ) ; return ; } -sub get_options_cgi { +sub get_options_cgi +{ # In CGI context arguments are not in @ARGV but in QUERY_STRING variable (with GET). my $mysync = shift @ARG ; $mysync->{cgi} || return ; @@ -12144,19 +15105,28 @@ sub get_options_cgi { 'domino2' => \$mysync->{domino2}, 'f1f2=s@' => \$mysync->{f1f2}, 'f1f2h=s%' => \$mysync->{f1f2h}, - 'folder=s@' => \$mysync->{folder}, + 'folder=s@' => \$mysync->{ folder }, 'blabla=s' => \@blabla, 'testslive!' => \$mysync->{testslive}, 'testslive6!' => \$mysync->{testslive6}, 'releasecheck!' => \$mysync->{releasecheck}, 'simulong=i' => \$mysync->{simulong}, + 'debugsleep=f' => \$mysync->{debugsleep}, + 'subfolder1=s' => \$mysync->{ subfolder1 }, + 'subfolder2=s' => \$mysync->{ subfolder2 }, + 'justfolders!' => \$mysync->{ justfolders }, + 'justfoldersizes!' => \$mysync->{ justfoldersizes }, + 'delete1!' => \$mysync->{ delete1 }, + 'delete2!' => \$mysync->{ delete2 }, + 'delete2duplicates!' => \$mysync->{ delete2duplicates }, + 'tail!' => \$mysync->{tail}, -# blabla and f1f2h=s% could be removed but -# tests_get_options_cgi() should be split before +# blabla and f1f2h=s% could be removed but +# tests_get_options_cgi() should be split before # with a sub tests_myGetOptions() ) ; - $debug and output( $mysync, "get options: [$opt_ret][$numopt]\n" ) ; + $mysync->{ debug } and output( $mysync, "get options: [$opt_ret][$numopt]\n" ) ; if ( ! $opt_ret ) { return ; @@ -12164,23 +15134,24 @@ sub get_options_cgi { return $numopt ; } -sub get_options_cmd { - my $mysync = shift @ARG ; - my @arguments = @ARG ; - my $mycgi = $mysync->{cgi} ; - # final 0 is used to print usage when no option is given on command line +sub get_options_cmd +{ + my $mysync = shift @ARG ; + my @arguments = @ARG ; + my $mycgi = $mysync->{cgi} ; + # final 0 is used to print usage when no option is given on command line my $numopt = scalar @arguments || 0 ; my $argv = join "\x00", @arguments ; if ( $argv =~ m/-delete\x002/x ) { - output( $mysync, "May be you mean --delete2 instead of --delete 2\n" ) ; + output( $mysync, "May be you mean --delete2 instead of --delete 2\n" ) ; return ; } $mysync->{f1f2h} = {} ; my $opt_ret = myGetOptions( - $mysync, - \@arguments, - 'debug!' => \$debug, + $mysync, + \@arguments, + 'debug!' => \$mysync->{ debug }, 'debuglist!' => \$debuglist, 'debugcontent!' => \$debugcontent, 'debugsleep=f' => \$mysync->{debugsleep}, @@ -12193,10 +15164,11 @@ sub get_options_cmd { 'debugfolders!' => \$mysync->{debugfolders}, 'debugssl=i' => \$mysync->{debugssl}, 'debugcgi!' => \$debugcgi, - 'debugenv' => \$mysync->{debugenv}, - 'debugsig' => \$mysync->{debugsig}, + 'debugenv!' => \$mysync->{debugenv}, + 'debugsig!' => \$mysync->{debugsig}, + 'debuglabels!' => \$mysync->{debuglabels}, 'simulong=i' => \$mysync->{simulong}, - 'abort' => \$mysync->{abort}, + 'abort' => \$mysync->{abort}, 'host1=s' => \$mysync->{host1}, 'host2=s' => \$mysync->{host2}, 'port1=i' => \$mysync->{port1}, @@ -12205,59 +15177,66 @@ sub get_options_cmd { 'inet6|ipv6' => \$mysync->{inet6}, 'user1=s' => \$mysync->{user1}, 'user2=s' => \$mysync->{user2}, - 'gmail1' => \$mysync->{gmail1}, - 'gmail2' => \$mysync->{gmail2}, - 'office1' => \$mysync->{office1}, - 'office2' => \$mysync->{office2}, - 'exchange1' => \$mysync->{exchange1}, - 'exchange2' => \$mysync->{exchange2}, - 'domino1' => \$mysync->{domino1}, - 'domino2' => \$mysync->{domino2}, + 'gmail1' => \$mysync->{gmail1}, + 'gmail2' => \$mysync->{gmail2}, + 'office1' => \$mysync->{office1}, + 'office2' => \$mysync->{office2}, + 'exchange1' => \$mysync->{exchange1}, + 'exchange2' => \$mysync->{exchange2}, + 'domino1' => \$mysync->{domino1}, + 'domino2' => \$mysync->{domino2}, 'domain1=s' => \$domain1, 'domain2=s' => \$domain2, 'password1=s' => \$mysync->{password1}, 'password2=s' => \$mysync->{password2}, - 'passfile1=s' => \$passfile1, - 'passfile2=s' => \$passfile2, + 'passfile1=s' => \$mysync->{ passfile1 }, + 'passfile2=s' => \$mysync->{ passfile2 }, 'authmd5!' => \$authmd5, 'authmd51!' => \$authmd51, 'authmd52!' => \$authmd52, - 'sep1=s' => \$sep1, - 'sep2=s' => \$sep2, - 'folder=s@' => \$mysync->{folder}, + 'sep1=s' => \$mysync->{ sep1 }, + 'sep2=s' => \$mysync->{ sep2 }, + 'sanitize!' => \$mysync->{ sanitize }, + 'folder=s@' => \$mysync->{ folder }, 'folderrec=s' => \@folderrec, 'include=s' => \@include, 'exclude=s' => \@exclude, + 'noexclude' => \$mysync->{noexclude}, 'folderfirst=s' => \@folderfirst, 'folderlast=s' => \@folderlast, 'prefix1=s' => \$prefix1, 'prefix2=s' => \$prefix2, - 'subfolder2=s' => \$subfolder2, - 'fixslash2!' => \$fixslash2, + 'subfolder1=s' => \$mysync->{ subfolder1 }, + 'subfolder2=s' => \$mysync->{ subfolder2 }, + 'fixslash2!' => \$mysync->{ fixslash2 }, 'fixInboxINBOX!' => \$fixInboxINBOX, - 'regextrans2=s' => \@regextrans2, + 'regextrans2=s@' => \$mysync->{ regextrans2 }, 'mixfolders!' => \$mixfolders, 'skipemptyfolders!' => \$skipemptyfolders, 'regexmess=s' => \@regexmess, + 'noregexmess' => \$mysync->{noregexmess}, 'skipmess=s' => \@skipmess, 'pipemess=s' => \@pipemess, 'pipemesscheck!' => \$pipemesscheck, 'disarmreadreceipts!' => \$disarmreadreceipts, 'regexflag=s' => \@regexflag, + 'noregexflag' => \$mysync->{noregexflag}, 'filterflags!' => \$filterflags, 'flagscase!' => \$flagscase, 'syncflagsaftercopy!' => \$syncflagsaftercopy, - 'resyncflags!' => \$mysync->{resyncflags}, - 'delete|delete1!' => \$delete1, - 'delete2!' => \$delete2, - 'delete2duplicates!' => \$delete2duplicates, + 'resyncflags!' => \$mysync->{ resyncflags }, + 'synclabels!' => \$mysync->{ synclabels }, + 'resynclabels!' => \$mysync->{ resynclabels }, + 'delete|delete1!' => \$mysync->{ delete1 }, + 'delete2!' => \$mysync->{ delete2 }, + 'delete2duplicates!' => \$mysync->{ delete2duplicates }, 'delete2folders!' => \$delete2folders, 'delete2foldersonly=s' => \$delete2foldersonly, 'delete2foldersbutnot=s' => \$delete2foldersbutnot, 'syncinternaldates!' => \$syncinternaldates, 'idatefromheader!' => \$idatefromheader, 'syncacls!' => \$syncacls, - 'maxsize=i' => \$maxsize, + 'maxsize=i' => \$mysync->{ maxsize }, 'minsize=i' => \$minsize, 'maxage=i' => \$maxage, 'minage=i' => \$minage, @@ -12267,15 +15246,15 @@ sub get_options_cmd { 'foldersizes!' => \$foldersizes, 'foldersizesatend!' => \$foldersizesatend, 'dry!' => \$mysync->{dry}, - 'expunge1|expunge!' => \$expunge1, - 'expunge2!' => \$expunge2, - 'uidexpunge2!' => \$uidexpunge2, - 'subscribed!' => \$subscribed, + 'expunge1|expunge!' => \$mysync->{ expunge1 }, + 'expunge2!' => \$mysync->{ expunge2 }, + 'uidexpunge2!' => \$mysync->{ uidexpunge2 }, + 'subscribed' => \$subscribed, 'subscribe!' => \$subscribe, 'subscribeall|subscribe_all!' => \$subscribeall, 'justbanner!' => \$justbanner, - 'justfolders!'=> \$justfolders, - 'justfoldersizes!' => \$justfoldersizes, + 'justfolders!'=> \$mysync->{ justfolders }, + 'justfoldersizes!' => \$mysync->{ justfoldersizes }, 'fast!' => \$fast, 'version' => \$mysync->{version}, 'help' => \$help, @@ -12290,7 +15269,7 @@ sub get_options_cmd { 'allowsizemismatch!' => \$allowsizemismatch, 'fastio1!' => \$fastio1, 'fastio2!' => \$fastio2, - 'sslcheck!' => \$mysync->{sslcheck}, + 'sslcheck!' => \$mysync->{sslcheck}, 'ssl1!' => \$mysync->{ssl1}, 'ssl2!' => \$mysync->{ssl2}, 'ssl1_ssl_version=s' => \$mysync->{h1}->{sslargs}->{SSL_version}, @@ -12314,7 +15293,7 @@ sub get_options_cmd { 'reconnectretry2=i' => \$reconnectretry2, 'tests!' => \$mysync->{ tests }, 'testsdebug|tests_debug!' => \$mysync->{ testsdebug }, - 'testsunit=s@' => \$mysync->{testsunit}, + 'testsunit=s@' => \$mysync->{testsunit}, 'testslive!' => \$mysync->{testslive}, 'testslive6!' => \$mysync->{testslive6}, 'justlogin!' => \$mysync->{justlogin}, @@ -12332,11 +15311,11 @@ sub get_options_cmd { 'debugcache!' => \$debugcache, 'useuid!' => \$useuid, 'addheader!' => \$mysync->{addheader}, - 'exitwhenover=i' => \$exitwhenover, + 'exitwhenover=i' => \$mysync->{ exitwhenover }, 'checkselectable!' => \$mysync->{ checkselectable }, 'checkfoldersexist!' => \$mysync->{ checkfoldersexist }, 'checkmessageexists!' => \$checkmessageexists, - 'expungeaftereach!' => \$expungeaftereach, + 'expungeaftereach!' => \$mysync->{ expungeaftereach }, 'abletosearch!' => \$mysync->{abletosearch}, 'abletosearch1!' => \$mysync->{abletosearch1}, 'abletosearch2!' => \$mysync->{abletosearch2}, @@ -12349,11 +15328,12 @@ sub get_options_cmd { 'create_folder_old!' => \$create_folder_old, 'maxmessagespersecond=f' => \$mysync->{maxmessagespersecond}, 'maxbytespersecond=i' => \$mysync->{maxbytespersecond}, - 'maxbytesafter=i' => \$mysync->{maxbytesafter}, - 'maxsleep=f' => \$mysync->{maxsleep}, + 'maxbytesafter=i' => \$mysync->{maxbytesafter}, + 'maxsleep=f' => \$mysync->{maxsleep}, 'skipcrossduplicates!' => \$skipcrossduplicates, 'debugcrossduplicates!' => \$debugcrossduplicates, 'log!' => \$mysync->{log}, + 'tail!' => \$mysync->{tail}, 'logfile=s' => \$mysync->{logfile}, 'logdir=s' => \$mysync->{logdir}, 'errorsmax=i' => \$mysync->{errorsmax}, @@ -12363,27 +15343,33 @@ sub get_options_cmd { 'justautomap!' => \$mysync->{justautomap}, 'id!' => \$mysync->{id}, 'f1f2=s@' => \$mysync->{f1f2}, + 'nof1f2' => \$mysync->{nof1f2}, 'f1f2h=s%' => \$mysync->{f1f2h}, 'justfolderlists!' => \$mysync->{justfolderlists}, 'delete1emptyfolders' => \$mysync->{delete1emptyfolders}, ) ; #myprint( Data::Dumper->Dump( [ $mysync ] ) ) ; - $debug and output( $mysync, "get options: [$opt_ret][$numopt]\n" ) ; + $mysync->{ debug } and output( $mysync, "get options: [$opt_ret][$numopt]\n" ) ; my $numopt_after = scalar @arguments ; #myprint( "get options: [$opt_ret][$numopt][$numopt_after]\n" ) ; if ( $numopt_after ) { - myprint( "Extra arguments found: @arguments\n", "It usually means a quoting issue in the command line\n" ) ; + myprint( + "Extra arguments found: @arguments\n", + "It usually means a quoting issue in the command line ", + "or some misspelling options.\n", + ) ; return ; } - if ( ! $opt_ret ) { - return ; - } - return $numopt ; + if ( ! $opt_ret ) { + return ; + } + return $numopt ; } - -sub tests_get_options { + +sub tests_get_options +{ note( 'Entering tests_get_options()' ) ; # CAVEAT: still setting global variables, be careful @@ -12397,45 +15383,46 @@ sub tests_get_options { # * options not known # * --delete 2 input # * number of arguments or QUERY_STRING length - my $mysync3 = { } ; - is( undef, get_options( $mysync3, qw( --noexist ) ), 'get_options: --noexist => undef' ) ; - is( undef, $mysync3->{ noexist }, 'get_options: --noexist => undef' ) ; - $mysync3 = { } ; - is( undef, get_options( $mysync3, qw( --lalala --noexist --version ) ), 'get_options: --lalala --noexist --version => undef' ) ; - is( 1, $mysync3->{ version }, 'get_options: --version => 1' ) ; - is( undef, $mysync3->{ noexist }, 'get_options: --noexist => undef' ) ; - $mysync3 = { } ; - is( 1, get_options( $mysync3, qw( --delete2 ) ), 'get_options: --delete2 => 1' ) ; - is( 1, $delete2, 'get_options: --delete2 => $delete2 = 1' ) ; - $mysync3 = { } ; - is( undef, get_options( $mysync3, qw( --delete 2 ) ), 'get_options: --delete 2 => undef' ) ; - is( undef, $delete1, 'get_options: --delete 2 => $delete1 still undef ; good!' ) ; - $mysync3 = { } ; - is( undef, get_options( $mysync3, "--delete 2" ), 'get_options: --delete 2 => undef' ) ; + my $mysync = { } ; + is( undef, get_options( $mysync, qw( --noexist ) ), 'get_options: --noexist => undef' ) ; + is( undef, $mysync->{ noexist }, 'get_options: --noexist => undef' ) ; + $mysync = { } ; + is( undef, get_options( $mysync, qw( --lalala --noexist --version ) ), 'get_options: --lalala --noexist --version => undef' ) ; + is( 1, $mysync->{ version }, 'get_options: --version => 1' ) ; + is( undef, $mysync->{ noexist }, 'get_options: --noexist => undef' ) ; + $mysync = { } ; + is( 1, get_options( $mysync, qw( --delete2 ) ), 'get_options: --delete2 => 1' ) ; + is( 1, $mysync->{ delete2 }, 'get_options: --delete2 => var delete2 = 1' ) ; + $mysync = { } ; + is( undef, get_options( $mysync, qw( --delete 2 ) ), 'get_options: --delete 2 => var undef' ) ; + is( undef, $mysync->{ delete1 }, 'get_options: --delete 2 => var still undef ; good!' ) ; + $mysync = { } ; + is( undef, get_options( $mysync, "--delete 2" ), 'get_options: --delete 2 => undef' ) ; - is( 1, get_options( $mysync3, "--version" ), 'get_options: --version => 1' ) ; - is( 1, get_options( $mysync3, "--help" ), 'get_options: --help => 1' ) ; - - is( undef, get_options( $mysync3, qw( --noexist --version ) ), 'get_options: --debug --noexist --version => undef' ) ; - is( 1, get_options( $mysync3, qw( --version ) ), 'get_options: --version => 1' ) ; - is( undef, get_options( $mysync3, qw( extra ) ), 'get_options: extra => undef' ) ; - is( undef, get_options( $mysync3, qw( extra1 --version extra2 ) ), 'get_options: extra1 --version extra2 => undef' ) ; + is( 1, get_options( $mysync, "--version" ), 'get_options: --version => 1' ) ; + is( 1, get_options( $mysync, "--help" ), 'get_options: --help => 1' ) ; + + is( undef, get_options( $mysync, qw( --noexist --version ) ), 'get_options: --debug --noexist --version => undef' ) ; + is( 1, get_options( $mysync, qw( --version ) ), 'get_options: --version => 1' ) ; + is( undef, get_options( $mysync, qw( extra ) ), 'get_options: extra => undef' ) ; + is( undef, get_options( $mysync, qw( extra1 --version extra2 ) ), 'get_options: extra1 --version extra2 => undef' ) ; + + $mysync = { } ; + is( 2, get_options( $mysync, qw( --host1 HOST_01) ), 'get_options: --host1 HOST_01 => 1' ) ; + is( 'HOST_01', $mysync->{ host1 }, 'get_options: --host1 HOST_01 => HOST_01' ) ; + #myprint( Data::Dumper->Dump( [ $mysync ] ) ) ; - $mysync3 = { } ; - is( 2, get_options( $mysync3, qw( --host1 HOST_01) ), 'get_options: --host1 HOST_01 => 1' ) ; - is( 'HOST_01', $mysync3->{ host1 }, 'get_options: --host1 HOST_01 => HOST_01' ) ; - #myprint( Data::Dumper->Dump( [ $mysync3 ] ) ) ; - note( 'Leaving tests_get_options()' ) ; return ; } -sub get_options { +sub get_options +{ my $mysync = shift @ARG ; my @arguments = @ARG ; - #myprint( "1 mysync: ", Data::Dumper->Dump( [ $mysync ] ) ) ; + #myprint( "1 mysync: ", Data::Dumper->Dump( [ $mysync ] ) ) ; my $ret ; if ( under_cgi_context( ) ) { # CGI context @@ -12444,13 +15431,13 @@ sub get_options { # Command line context ; $ret = get_options_cmd( $mysync, @arguments ) ; } ; - #myprint( "2 mysync: ", Data::Dumper->Dump( [ $mysync ] ) ) ; + #myprint( "2 mysync: ", Data::Dumper->Dump( [ $mysync ] ) ) ; foreach my $key ( sort keys %{ $mysync } ) { if ( ! defined $mysync->{$key} ) { delete $mysync->{$key} ; next ; } - if ( 'ARRAY' eq ref( $mysync->{$key} ) + if ( 'ARRAY' eq ref( $mysync->{$key} ) and 0 == scalar( @{ $mysync->{$key} } ) ) { delete $mysync->{$key} ; } @@ -12458,14 +15445,15 @@ sub get_options { return $ret ; } -sub testunitsession { +sub testunitsession +{ my $mysync = shift ; - + if ( ! $mysync ) { return ; } if ( ! $mysync->{ testsunit } ) { return ; } my @functions = @{ $mysync->{ testsunit } } ; - + if ( ! @functions ) { return ; } SKIP: { @@ -12476,7 +15464,8 @@ sub testunitsession { return ; } -sub tests_count_0s { +sub tests_count_0s +{ note( 'Entering tests_count_zeros()' ) ; is( 0, count_0s( ), 'count_0s: no parameters => 0' ) ; is( 1, count_0s( 0 ), 'count_0s: 0 => 1' ) ; @@ -12486,17 +15475,20 @@ sub tests_count_0s { note( 'Leaving tests_count_zeros()' ) ; return ; } -sub count_0s { +sub count_0s +{ my @array = @ARG ; - + if ( ! @array ) { return 0 ; } my $nb_zeros = 0 ; map { $_ == 0 and $nb_zeros += 1 } @array ; return $nb_zeros ; } -sub tests_report_failures { +sub tests_report_failures +{ note( 'Entering tests_report_failures()' ) ; + is( undef, report_failures( ), 'report_failures: no parameters => undef' ) ; is( "nb 1 - first\n", report_failures( ({'ok' => 0, name => 'first'}) ), 'report_failures: "first" failed => nb 1 - first' ) ; is( q{}, report_failures( ( {'ok' => 1, name => 'first'} ) ), 'report_failures: "first" success =>' ) ; @@ -12506,11 +15498,12 @@ sub tests_report_failures { return ; } -sub report_failures { +sub report_failures +{ my @details = @ARG ; - + if ( ! @details ) { return ; } - + my $counter = 1 ; my $report = q{} ; foreach my $details ( @details ) { @@ -12524,15 +15517,19 @@ sub report_failures { } -sub tests_true { +sub tests_true +{ note( 'Entering tests_true()' ) ; + is( 1, 1, 'true: 1 is 1' ) ; note( 'Leaving tests_true()' ) ; return ; } -sub tests_testsunit { +sub tests_testsunit +{ note( 'Entering tests_testunit()' ) ; + is( undef, testsunit( ), 'testsunit: no parameters => undef' ) ; is( undef, testsunit( undef ), 'testsunit: an undef parameter => undef' ) ; is( undef, testsunit( q{} ), 'testsunit: an empty parameter => undef' ) ; @@ -12542,139 +15539,63 @@ sub tests_testsunit { return ; } -sub testsunit { +sub testsunit +{ my @functions = @ARG ; - + if ( ! @functions ) { # myprint( "testsunit warning: no argument given\n" ) ; - return ; + return ; } - + foreach my $function ( @functions ) { if ( ! $function ) { myprint( "testsunit warning: argument is empty\n" ) ; - next ; + next ; } if ( ! exists &$function ) { myprint( "testsunit warning: function $function does not exist\n" ) ; - next ; + next ; } if ( ! defined &$function ) { myprint( "testsunit warning: function $function is not defined\n" ) ; - next ; + next ; } my $function_ref = \&{ $function } ; - &$function_ref() ; + &$function_ref() ; } return ; } -sub testsdebug { - my $mysync = shift ; - if ( ! $mysync->{ testsdebug } ) { return ; } - SKIP: { - if ( ! $mysync->{ testsdebug } ) { - skip 'No test in normal run' ; - } - - note( 'Entering testsdebug()' ) ; - ok( ( ( not -d 'W/tmp/tests' ) or rmtree( 'W/tmp/tests/' ) ), 'testsdebug: rmtree W/tmp/tests' ) ; - #tests_bytes_display_string( ) ; - #tests_ucsecond( ) ; - #tests_mkpath( ) ; - #tests_format_for_imap_arg( ) ; - #tests_is_a_release_number( ) ; - #tests_delete1emptyfolders( ) ; - #tests_memory_consumption( ) ; - #tests_imap2_folder_name() ; - #tests_length_ref( ) ; - #tests_diff_or_NA( ) ; - #tests_match_number( ) ; - #tests_all_defined( ) ; - #tests_guess_separator( ) ; - #tests_message_for_host2( ) ; - #tests_special_from_folders_hash( ) ; - #tests_do_valid_directory( ) ; - #tests_notmatch( ) ; - #tests_match( ) ; - #tests_get_options( ) ; - #tests_rand32( ) ; - #tests_string_to_file( ) ; - #tests_hashsynclocal( ) ; - #tests_output( ) ; - #tests_output_reset_with( ) ; - #tests_output_start( ) ; - #tests_hashsync( ) ; - #tests_check_last_release( ) ; - #tests_cpu_number( ) ; - #tests_load_and_delay( ) ; - #tests_loadavg( ) ; - #tests_backtick( ) ; - #tests_firstline( ) ; - #tests_pipemess( ) ; - #tests_not_long_imapsync_version_public( ) ; - #tests_get_options_cgi( ) ; - #tests_guess_special( ) ; - ####tests_reconnect_if_needed( ) ; - #tests_reconnect_12_if_needed( ) ; - #tests_sleep_max_bytes( ) ; - #tests_file_to_string( ) ; - #tests_under_cgi_context( ) ; - #tests_umask( ) ; - #tests_umask_str( ) ; - #tests_set_umask( ) ; - #tests_createhashfileifneeded( ) ; - #tests_filter_forbidden_characters( ) ; - #tests_logfile( ) ; - #tests_setlogfile( ) ; - #tests_move_slash( ) ; - #tests_testsunit( ) ; - #tests_always_fail( ) ; - #tests_count_0s( ) ; - #tests_report_failures( ) ; - #tests_max( ) ; - #tests_min( ) ; - #tests_sleep_if_needed( ) ; - #tests_imapsping( ) ; - #tests_tcpping( ) ; - #tests_sslcheck( ) ; - #tests_resolv( ) ; - #tests_resolvrev( ) ; - #tests_connect_socket( ) ; - #tests_probe_imapssl( ) ; - #tests_mailimapclient_connect( ) ; - #tests_guess_prefix( ) ; - #tests_usage( ) ; - #tests_version_from_rcs( ) ; - #tests_mailimapclient_connect_bug( ) ; # it fails with Mail-IMAPClient <= 3.39 - #tests_backslash_caret( ) ; - tests_write_pidfile( ) ; - tests_remove_pidfile_not_running( ) ; - tests_match_a_pid_number( ) ; - note( 'Leaving testsdebug()' ) ; +sub testsdebug +{ + # Now a little obsolete since there is + # imapsync ... --testsunit "anyfunction" + my $mysync = shift ; + if ( ! $mysync->{ testsdebug } ) { return ; } + SKIP: { + if ( ! $mysync->{ testsdebug } ) { + skip 'No test in normal run' ; + } + + note( 'Entering testsdebug()' ) ; + ok( ( ( not -d 'W/tmp/tests' ) or rmtree( 'W/tmp/tests/' ) ), 'testsdebug: rmtree W/tmp/tests' ) ; + tests_check_binary_embed_all_dyn_libs( ) ; + note( 'Leaving testsdebug()' ) ; done_testing( ) ; } return ; } -sub tests_template { - note( 'Entering tests_template()' ) ; - is( undef, undef, 'template: undef is undef' ) ; - is_deeply( {}, {}, 'template: a hash is a hash' ) ; - is_deeply( [], [], 'template: an array is an array' ) ; - note( 'Leaving tests_template()' ) ; - return ; -} +sub tests +{ + my $mysync = shift ; + if ( ! $mysync->{ tests } ) { return ; } - -sub tests { - my $mysync = shift ; - if ( ! $mysync->{ tests } ) { return ; } - - SKIP: { + SKIP: { skip 'No test in normal run' if ( ! $mysync->{ tests } ) ; - note( 'Entering tests()' ) ; + note( 'Entering tests()' ) ; tests_folder_routines( ) ; tests_compare_lists( ) ; tests_regexmess( ) ; @@ -12797,12 +15718,54 @@ sub tests { tests_template( ) ; tests_split_around_equal( ) ; tests_toggle_sleep( ) ; + tests_labels( ) ; + tests_synclabels( ) ; + tests_uidexpunge_or_expunge( ) ; + tests_appendlimit_from_capability( ) ; + tests_maxsize_setting( ) ; + tests_mock_capability( ) ; + tests_appendlimit( ) ; + tests_capability_of( ) ; + tests_search_in_array( ) ; + tests_operators_and_exclam_precedence( ) ; + tests_teelaunch( ) ; + tests_logfileprepa( ) ; + tests_useheader_suggestion( ) ; + tests_nb_messages_in_2_not_in_1( ) ; + tests_labels_add_subfolder2( ) ; + tests_labels_remove_subfolder1( ) ; + tests_resynclabels( ) ; + tests_labels_remove_special( ) ; + tests_uniq( ) ; + tests_remove_from_requested_folders( ) ; + tests_errors_log( ) ; + tests_add_subfolder1_to_folderrec( ) ; + tests_sanitize_subfolder( ) ; + tests_remove_edging_blanks( ) ; + tests_sanitize( ) ; + tests_remove_last_char_if_is( ) ; + tests_check_binary_embed_all_dyn_libs( ) ; + tests_nthline( ) ; + tests_secondline( ) ; + tests_tail( ) ; #tests_always_fail( ) ; - done_testing( 1181 ) ; + done_testing( 1441 ) ; note( 'Leaving tests()' ) ; } return ; } +sub tests_template +{ + note( 'Entering tests_template()' ) ; + + is( undef, undef, 'template: undef is undef' ) ; + is_deeply( {}, {}, 'template: a hash is a hash' ) ; + is_deeply( [], [], 'template: an array is an array' ) ; + note( 'Leaving tests_template()' ) ; + return ; +} + + diff --git a/data/Dockerfiles/dovecot/imapsync_cron.pl b/data/Dockerfiles/dovecot/imapsync_cron.pl old mode 100755 new mode 100644 index 4fad97ab..2b61545e --- a/data/Dockerfiles/dovecot/imapsync_cron.pl +++ b/data/Dockerfiles/dovecot/imapsync_cron.pl @@ -5,11 +5,11 @@ use LockFile::Simple qw(lock trylock unlock); use Proc::ProcessTable; use Data::Dumper qw(Dumper); use IPC::Run 'run'; -use String::Util 'trim'; use File::Temp; use Try::Tiny; use sigtrap 'handler' => \&sig_handler, qw(INT TERM KILL QUIT); +sub trim { my $s = shift; $s =~ s/^\s+|\s+$//g; return $s }; my $t = Proc::ProcessTable->new; my $imapsync_running = grep { $_->{cmndline} =~ /^\/usr\/bin\/perl \/usr\/local\/bin\/imapsync\s/ } @{$t->table}; if ($imapsync_running eq 1) @@ -19,11 +19,20 @@ if ($imapsync_running eq 1) } sub qqw($) { - my @values = split('(?=--)', $_[0]); + my @params = (); + my @values = split(/(?=--)/, $_[0]); foreach my $val (@values) { + my @tmpparam = split(/ /, $val, 2); + foreach my $tmpval (@tmpparam) { + if ($tmpval ne '') { + push @params, $tmpval; + } + } + } + foreach my $val (@params) { $val=trim($val); } - return @values + return @params; } $run_dir="/tmp"; @@ -101,10 +110,6 @@ while ($row = $sth->fetchrow_arrayref()) { $timeout1 = @$row[19]; $timeout2 = @$row[20]; - $is_running = $dbh->prepare("UPDATE imapsync SET is_running = 1 WHERE id = ?"); - $is_running->bind_param( 1, ${id} ); - $is_running->execute(); - if ($enc1 eq "TLS") { $enc1 = "--tls1"; } elsif ($enc1 eq "SSL") { $enc1 = "--ssl1"; } else { undef $enc1; } my $template = $run_dir . '/imapsync.XXXXXXX'; @@ -118,43 +123,53 @@ while ($row = $sth->fetchrow_arrayref()) { my $custom_params_ref = \@custom_params_a; my $generated_cmds = [ "/usr/local/bin/imapsync", - "--tmpdir", "/tmp", - "--nofoldersizes", - ($timeout1 gt "0" ? () : ('--timeout1', $timeout1)), - ($timeout2 gt "0" ? () : ('--timeout2', $timeout2)), - ($exclude eq "" ? () : ("--exclude", $exclude)), - ($subfolder2 eq "" ? () : ('--subfolder2', $subfolder2)), - ($maxage eq "0" ? () : ('--maxage', $maxage)), - ($maxbytespersecond eq "0" ? () : ('--maxbytespersecond', $maxbytespersecond)), - ($delete2duplicates ne "1" ? () : ('--delete2duplicates')), - ($subscribeall ne "1" ? () : ('--subscribeall')), - ($delete1 ne "1" ? () : ('--delete')), + "--tmpdir", "/tmp", + "--nofoldersizes", + ($timeout1 gt "0" ? () : ('--timeout1', $timeout1)), + ($timeout2 gt "0" ? () : ('--timeout2', $timeout2)), + ($exclude eq "" ? () : ("--exclude", $exclude)), + ($subfolder2 eq "" ? () : ('--subfolder2', $subfolder2)), + ($maxage eq "0" ? () : ('--maxage', $maxage)), + ($maxbytespersecond eq "0" ? () : ('--maxbytespersecond', $maxbytespersecond)), + ($delete2duplicates ne "1" ? () : ('--delete2duplicates')), + ($subscribeall ne "1" ? () : ('--subscribeall')), + ($delete1 ne "1" ? () : ('--delete')), ($delete2 ne "1" ? () : ('--delete2')), ($automap ne "1" ? () : ('--automap')), ($skipcrossduplicates ne "1" ? () : ('--skipcrossduplicates')), - (!defined($enc1) ? () : ($enc1)), - "--host1", $host1, - "--user1", $user1, - "--passfile1", $passfile1->filename, - "--port1", $port1, - "--host2", "localhost", - "--user2", $user2 . '*' . trim($master_user), - "--passfile2", $passfile2->filename, - '--no-modulesversion']; + (!defined($enc1) ? () : ($enc1)), + "--host1", $host1, + "--user1", $user1, + "--passfile1", $passfile1->filename, + "--port1", $port1, + "--host2", "localhost", + "--user2", $user2 . '*' . trim($master_user), + "--passfile2", $passfile2->filename, + '--no-modulesversion', + '--noreleasecheck']; try { + $is_running = $dbh->prepare("UPDATE imapsync SET is_running = 1 WHERE id = ?"); + $is_running->bind_param( 1, ${id} ); + $is_running->execute(); + run [@$generated_cmds, @$custom_params_ref], '&>', \my $stdout; - $update = $dbh->prepare("UPDATE imapsync SET returned_text = ?, last_run = NOW(), is_running = 0 WHERE id = ?"); + + $update = $dbh->prepare("UPDATE imapsync SET returned_text = ? WHERE id = ?"); $update->bind_param( 1, ${stdout} ); $update->bind_param( 2, ${id} ); $update->execute(); } catch { - $update = $dbh->prepare("UPDATE imapsync SET returned_text = 'Could not start or finish imapsync', last_run = NOW(), is_running = 0 WHERE id = ?"); + $update = $dbh->prepare("UPDATE imapsync SET returned_text = 'Could not start or finish imapsync' WHERE id = ?"); + $update->bind_param( 1, ${id} ); + $update->execute(); + } finally { + $update = $dbh->prepare("UPDATE imapsync SET last_run = NOW(), is_running = 0 WHERE id = ?"); $update->bind_param( 1, ${id} ); $update->execute(); - $lockmgr->unlock($lock_file); }; + } $sth->finish(); diff --git a/data/Dockerfiles/dovecot/quarantine_notify.py b/data/Dockerfiles/dovecot/quarantine_notify.py index 28a7aabe..b1af332a 100755 --- a/data/Dockerfiles/dovecot/quarantine_notify.py +++ b/data/Dockerfiles/dovecot/quarantine_notify.py @@ -83,13 +83,14 @@ def notify_rcpt(rcpt, msg_count, quarantine_acl): msg.attach(html_part) msg['To'] = str(rcpt) text = msg.as_string() - server.sendmail(msg['From'], msg['To'], text) + server.sendmail(msg['From'].encode("ascii", errors="ignore"), msg['To'], text) server.quit() for res in meta_query: query_mysql('UPDATE quarantine SET notified = 1 WHERE id = "%d"' % (res['id']), update = True) r.hset('Q_LAST_NOTIFIED', record['rcpt'], time_now) break except Exception as ex: + server.quit() print '%s' % (ex) time.sleep(3) diff --git a/data/Dockerfiles/dovecot/quota_notify.py b/data/Dockerfiles/dovecot/quota_notify.py index f5df7639..669adec2 100755 --- a/data/Dockerfiles/dovecot/quota_notify.py +++ b/data/Dockerfiles/dovecot/quota_notify.py @@ -54,7 +54,7 @@ try: msg.attach(text_part) msg.attach(html_part) msg['To'] = username - p = Popen(['/usr/local/libexec/dovecot/dovecot-lda', '-d', username, '-o', '"plugin/quota=maildir:User quota:noenforcing"'], stdout=PIPE, stdin=PIPE, stderr=STDOUT) + p = Popen(['/usr/lib/dovecot/dovecot-lda', '-d', username, '-o', '"plugin/quota=maildir:User quota:noenforcing"'], stdout=PIPE, stdin=PIPE, stderr=STDOUT) p.communicate(input=msg.as_string()) except Exception as ex: diff --git a/data/Dockerfiles/dovecot/sa-rules.sh b/data/Dockerfiles/dovecot/sa-rules.sh index 0cea240c..d208722a 100755 --- a/data/Dockerfiles/dovecot/sa-rules.sh +++ b/data/Dockerfiles/dovecot/sa-rules.sh @@ -1,25 +1,41 @@ #!/bin/bash + +# Create temp directories +[[ ! -d /tmp/sa-rules-schaal ]] && mkdir -p /tmp/sa-rules-schaal [[ ! -d /tmp/sa-rules-heinlein ]] && mkdir -p /tmp/sa-rules-heinlein -if [[ ! -f /etc/rspamd/custom/sa-rules-heinlein ]]; then + +# Hash current SA rules +if [[ ! -f /etc/rspamd/custom/sa-rules ]]; then HASH_SA_RULES=0 else - HASH_SA_RULES=$(cat /etc/rspamd/custom/sa-rules-heinlein | md5sum | cut -d' ' -f1) + HASH_SA_RULES=$(cat /etc/rspamd/custom/sa-rules | md5sum | cut -d' ' -f1) fi -curl --connect-timeout 15 --max-time 30 http://www.spamassassin.heinlein-support.de/$(dig txt 1.4.3.spamassassin.heinlein-support.de +short | tr -d '"').tar.gz --output /tmp/sa-rules.tar.gz -if [[ -f /tmp/sa-rules.tar.gz ]]; then - tar xfvz /tmp/sa-rules.tar.gz -C /tmp/sa-rules-heinlein - # create complete list of rules in a single file - cat /tmp/sa-rules-heinlein/*cf > /etc/rspamd/custom/sa-rules-heinlein - # Only restart rspamd-mailcow when rules changed - if [[ $(cat /etc/rspamd/custom/sa-rules-heinlein | md5sum | cut -d' ' -f1) != ${HASH_SA_RULES} ]]; then - CONTAINER_NAME=rspamd-mailcow - CONTAINER_ID=$(curl --silent --insecure https://dockerapi/containers/json | \ - jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], id: .Id}" | \ - jq -rc "select( .name | tostring | contains(\"${CONTAINER_NAME}\")) | .id") - if [[ ! -z ${CONTAINER_ID} ]]; then - curl --silent --insecure -XPOST --connect-timeout 15 --max-time 120 https://dockerapi/containers/${CONTAINER_ID}/restart - fi +# Deploy +## Heinlein +curl --connect-timeout 15 --max-time 30 http://www.spamassassin.heinlein-support.de/$(dig txt 1.4.3.spamassassin.heinlein-support.de +short | tr -d '"').tar.gz --output /tmp/sa-rules-heinlein.tar.gz +if gzip -t /tmp/sa-rules-heinlein.tar.gz; then + tar xfvz /tmp/sa-rules-heinlein.tar.gz -C /tmp/sa-rules-heinlein + cat /tmp/sa-rules-heinlein/*cf > /etc/rspamd/custom/sa-rules +fi +## Schaal +curl --connect-timeout 15 --max-time 30 http://sa.schaal-it.net/$(dig txt 1.4.3.sa.schaal-it.net +short | tr -d '"').tar.gz --output /tmp/sa-rules-schaal.tar.gz +if gzip -t /tmp/sa-rules-schaal.tar.gz; then + tar xfvz /tmp/sa-rules-schaal.tar.gz -C /tmp/sa-rules-schaal + # Append, do not overwrite + cat /tmp/sa-rules-schaal/*cf >> /etc/rspamd/custom/sa-rules +fi + +if [[ "$(cat /etc/rspamd/custom/sa-rules | md5sum | cut -d' ' -f1)" != "${HASH_SA_RULES}" ]]; then + CONTAINER_NAME=rspamd-mailcow + CONTAINER_ID=$(curl --silent --insecure https://dockerapi/containers/json | \ + jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], id: .Id}" | \ + jq -rc "select( .name | tostring | contains(\"${CONTAINER_NAME}\")) | .id") + if [[ ! -z ${CONTAINER_ID} ]]; then + curl --silent --insecure -XPOST --connect-timeout 15 --max-time 120 https://dockerapi/containers/${CONTAINER_ID}/restart fi fi -rm -rf /tmp/sa-rules-heinlein /tmp/sa-rules.tar.gz + +# Cleanup +rm -rf /tmp/sa-rules-heinlein /tmp/sa-rules-heinlein.tar.gz +rm -rf /tmp/sa-rules-schaal /tmp/sa-rules-schaal.tar.gz diff --git a/data/Dockerfiles/dovecot/supervisord.conf b/data/Dockerfiles/dovecot/supervisord.conf index 2e3026a0..2d91b55a 100644 --- a/data/Dockerfiles/dovecot/supervisord.conf +++ b/data/Dockerfiles/dovecot/supervisord.conf @@ -12,7 +12,7 @@ stderr_logfile_maxbytes=0 autostart=true [program:dovecot] -command=/usr/local/sbin/dovecot -F +command=/usr/sbin/dovecot -F autorestart=true [program:cron] diff --git a/data/Dockerfiles/dovecot/syslog-ng.conf b/data/Dockerfiles/dovecot/syslog-ng.conf index d788d3e0..b4bc7156 100644 --- a/data/Dockerfiles/dovecot/syslog-ng.conf +++ b/data/Dockerfiles/dovecot/syslog-ng.conf @@ -31,10 +31,10 @@ destination d_redis_f2b_channel { ); }; filter f_mail { facility(mail); }; -filter f_not_watchdog { not message("172\.22\.1\.248"); }; +#filter f_not_watchdog { not message("172\.22\.1\.248"); }; log { source(s_src); - filter(f_not_watchdog); +# filter(f_not_watchdog); destination(d_stdout); filter(f_mail); destination(d_redis_ui_log); diff --git a/data/Dockerfiles/dovecot/trim_logs.sh b/data/Dockerfiles/dovecot/trim_logs.sh index 2fec55d3..b916827e 100755 --- a/data/Dockerfiles/dovecot/trim_logs.sh +++ b/data/Dockerfiles/dovecot/trim_logs.sh @@ -15,4 +15,4 @@ catch_non_zero "/usr/bin/redis-cli -h redis LTRIM NETFILTER_LOG 0 __LOG_LINES__" catch_non_zero "/usr/bin/redis-cli -h redis LTRIM AUTODISCOVER_LOG 0 __LOG_LINES__" catch_non_zero "/usr/bin/redis-cli -h redis LTRIM API_LOG 0 __LOG_LINES__" catch_non_zero "/usr/bin/redis-cli -h redis LTRIM RL_LOG 0 __LOG_LINES__" - +catch_non_zero "/usr/bin/redis-cli -h redis LTRIM WATCHDOG_LOG 0 __LOG_LINES__" diff --git a/data/Dockerfiles/netfilter/Dockerfile b/data/Dockerfiles/netfilter/Dockerfile index 92eaa39f..55594de6 100644 --- a/data/Dockerfiles/netfilter/Dockerfile +++ b/data/Dockerfiles/netfilter/Dockerfile @@ -1,13 +1,16 @@ -FROM alpine:3.9 +FROM alpine:3.10 LABEL maintainer "Andre Peters <andre.peters@servercow.de>" ENV XTABLES_LIBDIR /usr/lib/xtables ENV PYTHON_IPTABLES_XTABLES_VERSION 12 ENV IPTABLES_LIBDIR /usr/lib -RUN apk add -U python2 python-dev py-pip gcc musl-dev iptables ip6tables tzdata \ - && pip2 install --upgrade python-iptables==0.13.0 redis ipaddress \ - && apk del python-dev py2-pip gcc +RUN echo 'http://dl-cdn.alpinelinux.org/alpine/v3.9/main' >> /etc/apk/repositories \ + && apk add --virtual .build-deps gcc python3-dev libffi-dev openssl-dev \ + && apk add -U python3 iptables=1.6.2-r1 ip6tables=1.6.2-r1 tzdata musl-dev \ + && pip3 install --upgrade pip python-iptables redis ipaddress dnspython \ +# && pip3 install --upgrade pip python-iptables==0.13.0 redis ipaddress dnspython \ + && apk del .build-deps COPY server.py / -CMD ["python2", "-u", "/server.py"] +CMD ["python3", "-u", "/server.py"] diff --git a/data/Dockerfiles/netfilter/server.py b/data/Dockerfiles/netfilter/server.py index f43122ea..78cafc68 100644 --- a/data/Dockerfiles/netfilter/server.py +++ b/data/Dockerfiles/netfilter/server.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python2 +#!/usr/bin/env python3 import re import os @@ -6,19 +6,22 @@ import time import atexit import signal import ipaddress +from collections import Counter from random import randint from threading import Thread from threading import Lock import redis import json import iptc +import dns.resolver +import dns.exception while True: try: r = redis.StrictRedis(host=os.getenv('IPV4_NETWORK', '172.22.1') + '.249', decode_responses=True, port=6379, db=0) r.ping() except Exception as ex: - print '%s - trying again in 3 seconds' % (ex) + print('%s - trying again in 3 seconds' % (ex)) time.sleep(3) else: break @@ -31,13 +34,34 @@ RULES[2] = '-login: Disconnected \(auth failed, .+\): user=.*, method=.+, rip=([ RULES[3] = '-login: Aborted login \(tried to use disallowed .+\): user=.+, rip=([0-9a-f\.:]+), lip.+' RULES[4] = 'SOGo.+ Login from \'([0-9a-f\.:]+)\' for user .+ might not have worked' RULES[5] = 'mailcow UI: Invalid password for .+ by ([0-9a-f\.:]+)' -#RULES[6] = '-login: Aborted login \(no auth .+\): user=.+, rip=([0-9a-f\.:]+), lip.+' +RULES[6] = '([0-9a-f\.:]+) \"GET \/SOGo\/.* HTTP.+\" 403 .+' +#RULES[7] = '-login: Aborted login \(no auth .+\): user=.+, rip=([0-9a-f\.:]+), lip.+' + +WHITELIST = [] +BLACKLIST= [] bans = {} -log = {} + quit_now = False lock = Lock() +def log(priority, message): + tolog = {} + tolog['time'] = int(round(time.time())) + tolog['priority'] = priority + tolog['message'] = message + r.lpush('NETFILTER_LOG', json.dumps(tolog, ensure_ascii=False)) + print(message) + +def logWarn(message): + log('warn', message) + +def logCrit(message): + log('crit', message) + +def logInfo(message): + log('info', message) + def refreshF2boptions(): global f2boptions global quit_now @@ -58,8 +82,8 @@ def refreshF2boptions(): try: f2boptions = {} f2boptions = json.loads(r.get('F2B_OPTIONS')) - except ValueError, e: - print 'Error loading F2B options: F2B_OPTIONS is not json' + except ValueError: + print('Error loading F2B options: F2B_OPTIONS is not json') quit_now = True if r.exists('F2B_LOG'): @@ -84,18 +108,10 @@ def mailcowChainOrder(): if item.target.name == 'MAILCOW': target_found = True if position != 0: - log['time'] = int(round(time.time())) - log['priority'] = 'crit' - log['message'] = 'Error in ' + chain.name + ' chain order, restarting container' - r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False)) - print log['message'] + logCrit('Error in %s chain order, restarting container' % (chain.name)) quit_now = True if not target_found: - log['time'] = int(round(time.time())) - log['priority'] = 'crit' - log['message'] = 'Error in ' + chain.name + ' chain: MAILCOW target not found, restarting container' - r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False)) - print log['message'] + logCrit('Error in %s chain: MAILCOW target not found, restarting container' % (chain.name)) quit_now = True def ban(address): @@ -106,28 +122,28 @@ def ban(address): RETRY_WINDOW = int(f2boptions['retry_window']) NETBAN_IPV4 = '/' + str(f2boptions['netban_ipv4']) NETBAN_IPV6 = '/' + str(f2boptions['netban_ipv6']) - WHITELIST = r.hgetall('F2B_WHITELIST') - ip = ipaddress.ip_address(address.decode('ascii')) + ip = ipaddress.ip_address(address) if type(ip) is ipaddress.IPv6Address and ip.ipv4_mapped: ip = ip.ipv4_mapped address = str(ip) if ip.is_private or ip.is_loopback: return - self_network = ipaddress.ip_network(address.decode('ascii')) - if WHITELIST: - for wl_key in WHITELIST: - wl_net = ipaddress.ip_network(wl_key.decode('ascii'), False) + self_network = ipaddress.ip_network(address) + + with lock: + temp_whitelist = set(WHITELIST) + + if temp_whitelist: + for wl_key in temp_whitelist: + wl_net = ipaddress.ip_network(wl_key, False) + if wl_net.overlaps(self_network): - log['time'] = int(round(time.time())) - log['priority'] = 'info' - log['message'] = 'Address %s is whitelisted by rule %s' % (self_network, wl_net) - r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False)) - print 'Address %s is whitelisted by rule %s' % (self_network, wl_net) + logInfo('Address %s is whitelisted by rule %s' % (self_network, wl_net)) return - net = ipaddress.ip_network((address + (NETBAN_IPV4 if type(ip) is ipaddress.IPv4Address else NETBAN_IPV6)).decode('ascii'), strict=False) + net = ipaddress.ip_network((address + (NETBAN_IPV4 if type(ip) is ipaddress.IPv4Address else NETBAN_IPV6)), strict=False) net = str(net) if not net in bans or time.time() - bans[net]['last_attempt'] > RETRY_WINDOW: @@ -142,11 +158,8 @@ def ban(address): active_window = time.time() - bans[net]['last_attempt'] if bans[net]['attempts'] >= MAX_ATTEMPTS: - log['time'] = int(round(time.time())) - log['priority'] = 'crit' - log['message'] = 'Banning %s' % net - r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False)) - print 'Banning %s for %d minutes' % (net, BAN_TIME / 60) + cur_time = int(round(time.time())) + logCrit('Banning %s for %d minutes' % (net, BAN_TIME / 60)) if type(ip) is ipaddress.IPv4Address: with lock: chain = iptc.Chain(iptc.Table(iptc.Table.FILTER), 'MAILCOW') @@ -165,29 +178,18 @@ def ban(address): rule.target = target if rule not in chain.rules: chain.insert_rule(rule) - r.hset('F2B_ACTIVE_BANS', '%s' % net, log['time'] + BAN_TIME) + r.hset('F2B_ACTIVE_BANS', '%s' % net, cur_time + BAN_TIME) else: - log['time'] = int(round(time.time())) - log['priority'] = 'warn' - log['message'] = '%d more attempts in the next %d seconds until %s is banned' % (MAX_ATTEMPTS - bans[net]['attempts'], RETRY_WINDOW, net) - r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False)) - print '%d more attempts in the next %d seconds until %s is banned' % (MAX_ATTEMPTS - bans[net]['attempts'], RETRY_WINDOW, net) + logWarn('%d more attempts in the next %d seconds until %s is banned' % (MAX_ATTEMPTS - bans[net]['attempts'], RETRY_WINDOW, net)) def unban(net): global lock - log['time'] = int(round(time.time())) - log['priority'] = 'info' - r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False)) if not net in bans: - log['message'] = '%s is not banned, skipping unban and deleting from queue (if any)' % net - r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False)) - print '%s is not banned, skipping unban and deleting from queue (if any)' % net + logInfo('%s is not banned, skipping unban and deleting from queue (if any)' % net) r.hdel('F2B_QUEUE_UNBAN', '%s' % net) return - log['message'] = 'Unbanning %s' % net - r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False)) - print 'Unbanning %s' % net - if type(ipaddress.ip_network(net.decode('ascii'))) is ipaddress.IPv4Network: + logInfo('Unbanning %s' % net) + if type(ipaddress.ip_network(net)) is ipaddress.IPv4Network: with lock: chain = iptc.Chain(iptc.Table(iptc.Table.FILTER), 'MAILCOW') rule = iptc.Rule() @@ -210,17 +212,47 @@ def unban(net): if net in bans: del bans[net] +def permBan(net, unban=False): + global lock + + if type(ipaddress.ip_network(net, strict=False)) is ipaddress.IPv4Network: + with lock: + chain = iptc.Chain(iptc.Table(iptc.Table.FILTER), 'MAILCOW') + rule = iptc.Rule() + rule.src = net + target = iptc.Target(rule, "REJECT") + rule.target = target + if rule not in chain.rules and not unban: + logCrit('Add host/network %s to blacklist' % net) + chain.insert_rule(rule) + r.hset('F2B_PERM_BANS', '%s' % net, int(round(time.time()))) + elif rule in chain.rules and unban: + logCrit('Remove host/network %s from blacklist' % net) + chain.delete_rule(rule) + r.hdel('F2B_PERM_BANS', '%s' % net) + else: + with lock: + chain = iptc.Chain(iptc.Table6(iptc.Table6.FILTER), 'MAILCOW') + rule = iptc.Rule6() + rule.src = net + target = iptc.Target(rule, "REJECT") + rule.target = target + if rule not in chain.rules and not unban: + logCrit('Add host/network %s to blacklist' % net) + chain.insert_rule(rule) + r.hset('F2B_PERM_BANS', '%s' % net, int(round(time.time()))) + elif rule in chain.rules and unban: + logCrit('Remove host/network %s from blacklist' % net) + chain.delete_rule(rule) + r.hdel('F2B_PERM_BANS', '%s' % net) + def quit(signum, frame): global quit_now quit_now = True def clear(): global lock - log['time'] = int(round(time.time())) - log['priority'] = 'info' - log['message'] = 'Clearing all bans' - r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False)) - print 'Clearing all bans' + logInfo('Clearing all bans') for net in bans.copy(): unban(net) with lock: @@ -249,28 +281,20 @@ def clear(): pubsub.unsubscribe() def watch(): - log['time'] = int(round(time.time())) - log['priority'] = 'info' - log['message'] = 'Watching Redis channel F2B_CHANNEL' - r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False)) + logInfo('Watching Redis channel F2B_CHANNEL') pubsub.subscribe('F2B_CHANNEL') - print 'Subscribing to Redis channel F2B_CHANNEL' while not quit_now: for item in pubsub.listen(): - for rule_id, rule_regex in RULES.iteritems(): + for rule_id, rule_regex in RULES.items(): if item['data'] and item['type'] == 'message': result = re.search(rule_regex, item['data']) if result: addr = result.group(1) - ip = ipaddress.ip_address(addr.decode('ascii')) + ip = ipaddress.ip_address(addr) if ip.is_private or ip.is_loopback: continue - print '%s matched rule id %d' % (addr, rule_id) - log['time'] = int(round(time.time())) - log['priority'] = 'warn' - log['message'] = '%s matched rule id %d' % (addr, rule_id) - r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False)) + logWarn('%s matched rule id %d' % (addr, rule_id)) ban(addr) def snat4(snat_target): @@ -294,11 +318,7 @@ def snat4(snat_target): chain = iptc.Chain(table, 'POSTROUTING') table.autocommit = False if get_snat4_rule() not in chain.rules: - log['time'] = int(round(time.time())) - log['priority'] = 'info' - log['message'] = 'Added POSTROUTING rule for source network ' + get_snat4_rule().src + ' to SNAT target ' + snat_target - r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False)) - print log['message'] + logCrit('Added POSTROUTING rule for source network %s to SNAT target %s' % (get_snat4_rule().src, snat_target)) chain.insert_rule(get_snat4_rule()) table.commit() else: @@ -309,7 +329,7 @@ def snat4(snat_target): table.commit() table.autocommit = True except: - print 'Error running SNAT4, retrying...' + print('Error running SNAT4, retrying...') def snat6(snat_target): global lock @@ -332,11 +352,7 @@ def snat6(snat_target): chain = iptc.Chain(table, 'POSTROUTING') table.autocommit = False if get_snat6_rule() not in chain.rules: - log['time'] = int(round(time.time())) - log['priority'] = 'info' - log['message'] = 'Added POSTROUTING rule for source network ' + get_snat6_rule().src + ' to SNAT target ' + snat_target - r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False)) - print log['message'] + logInfo('Added POSTROUTING rule for source network %s to SNAT target %s' % (get_snat6_rule().src, snat_target)) chain.insert_rule(get_snat6_rule()) table.commit() else: @@ -347,14 +363,14 @@ def snat6(snat_target): table.commit() table.autocommit = True except: - print 'Error running SNAT6, retrying...' + print('Error running SNAT6, retrying...') def autopurge(): while not quit_now: time.sleep(10) refreshF2boptions() - BAN_TIME = f2boptions['ban_time'] - MAX_ATTEMPTS = f2boptions['max_attempts'] + BAN_TIME = int(f2boptions['ban_time']) + MAX_ATTEMPTS = int(f2boptions['max_attempts']) QUEUE_UNBAN = r.hgetall('F2B_QUEUE_UNBAN') if QUEUE_UNBAN: for net in QUEUE_UNBAN: @@ -364,9 +380,101 @@ def autopurge(): if time.time() - bans[net]['last_attempt'] > BAN_TIME: unban(net) +def isIpNetwork(address): + try: + ipaddress.ip_network(address, False) + except ValueError: + return False + return True + + +def genNetworkList(list): + resolver = dns.resolver.Resolver() + hostnames = [] + networks = [] + + for key in list: + if isIpNetwork(key): + networks.append(key) + else: + hostnames.append(key) + + for hostname in hostnames: + hostname_ips = [] + for rdtype in ['A', 'AAAA']: + try: + answer = resolver.query(qname=hostname, rdtype=rdtype, lifetime=3) + except dns.exception.Timeout: + logInfo('Hostname %s timedout on resolve' % hostname) + break + except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer): + continue + except dns.exception.DNSException as dnsexception: + logInfo('%s' % dnsexception) + continue + + for rdata in answer: + hostname_ips.append(rdata.to_text()) + + networks.extend(hostname_ips) + + return set(networks) + +def whitelistUpdate(): + global lock + global quit_now + global WHITELIST + + while not quit_now: + start_time = time.time() + list = r.hgetall('F2B_WHITELIST') + + new_whitelist = [] + + if list: + new_whitelist = genNetworkList(list) + + with lock: + if Counter(new_whitelist) != Counter(WHITELIST): + WHITELIST = new_whitelist + logInfo('Whitelist was changed, it has %s entries' % len(WHITELIST)) + + time.sleep(60.0 - ((time.time() - start_time) % 60.0)) + +def blacklistUpdate(): + global quit_now + global BLACKLIST + + while not quit_now: + start_time = time.time() + list = r.hgetall('F2B_BLACKLIST') + + new_blacklist = [] + + if list: + new_blacklist = genNetworkList(list) + + if Counter(new_blacklist) != Counter(BLACKLIST): + addban = set(new_blacklist).difference(BLACKLIST) + delban = set(BLACKLIST).difference(new_blacklist) + + BLACKLIST = new_blacklist + logInfo('Blacklist was changed, it has %s entries' % len(BLACKLIST)) + + if addban: + for net in addban: + permBan(net=net) + + if delban: + for net in delban: + permBan(net=net, unban=True) + + + time.sleep(60.0 - ((time.time() - start_time) % 60.0)) + def initChain(): # Is called before threads start, no locking - print "Initializing mailcow netfilter chain" + print("Initializing mailcow netfilter chain") # IPv4 if not iptc.Chain(iptc.Table(iptc.Table.FILTER), "MAILCOW") in iptc.Table(iptc.Table.FILTER).chains: iptc.Table(iptc.Table.FILTER).create_chain("MAILCOW") @@ -391,38 +499,7 @@ def initChain(): rule.target = target if rule not in chain.rules: chain.insert_rule(rule) - # Apply blacklist - BLACKLIST = r.hgetall('F2B_BLACKLIST') - if BLACKLIST: - for bl_key in BLACKLIST: - if type(ipaddress.ip_network(bl_key.decode('ascii'), strict=False)) is ipaddress.IPv4Network: - chain = iptc.Chain(iptc.Table(iptc.Table.FILTER), 'MAILCOW') - rule = iptc.Rule() - rule.src = bl_key - target = iptc.Target(rule, "REJECT") - rule.target = target - if rule not in chain.rules: - log['time'] = int(round(time.time())) - log['priority'] = 'crit' - log['message'] = 'Blacklisting host/network %s' % bl_key - r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False)) - print log['message'] - chain.insert_rule(rule) - r.hset('F2B_PERM_BANS', '%s' % bl_key, int(round(time.time()))) - else: - chain = iptc.Chain(iptc.Table6(iptc.Table6.FILTER), 'MAILCOW') - rule = iptc.Rule6() - rule.src = bl_key - target = iptc.Target(rule, "REJECT") - rule.target = target - if rule not in chain.rules: - log['time'] = int(round(time.time())) - log['priority'] = 'crit' - log['message'] = 'Blacklisting host/network %s' % bl_key - r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False)) - print log['message'] - chain.insert_rule(rule) - r.hset('F2B_PERM_BANS', '%s' % bl_key, int(round(time.time()))) + if __name__ == '__main__': @@ -437,25 +514,25 @@ if __name__ == '__main__': if os.getenv('SNAT_TO_SOURCE') and os.getenv('SNAT_TO_SOURCE') is not 'n': try: - snat_ip = os.getenv('SNAT_TO_SOURCE').decode('ascii') + snat_ip = os.getenv('SNAT_TO_SOURCE') snat_ipo = ipaddress.ip_address(snat_ip) if type(snat_ipo) is ipaddress.IPv4Address: snat4_thread = Thread(target=snat4,args=(snat_ip,)) snat4_thread.daemon = True snat4_thread.start() except ValueError: - print os.getenv('SNAT_TO_SOURCE') + ' is not a valid IPv4 address' + print(os.getenv('SNAT_TO_SOURCE') + ' is not a valid IPv4 address') if os.getenv('SNAT6_TO_SOURCE') and os.getenv('SNAT6_TO_SOURCE') is not 'n': try: - snat_ip = os.getenv('SNAT6_TO_SOURCE').decode('ascii') + snat_ip = os.getenv('SNAT6_TO_SOURCE') snat_ipo = ipaddress.ip_address(snat_ip) if type(snat_ipo) is ipaddress.IPv6Address: snat6_thread = Thread(target=snat6,args=(snat_ip,)) snat6_thread.daemon = True snat6_thread.start() except ValueError: - print os.getenv('SNAT6_TO_SOURCE') + ' is not a valid IPv6 address' + print(os.getenv('SNAT6_TO_SOURCE') + ' is not a valid IPv6 address') autopurge_thread = Thread(target=autopurge) autopurge_thread.daemon = True @@ -464,6 +541,14 @@ if __name__ == '__main__': mailcowchainwatch_thread = Thread(target=mailcowChainOrder) mailcowchainwatch_thread.daemon = True mailcowchainwatch_thread.start() + + blacklistupdate_thread = Thread(target=blacklistUpdate) + blacklistupdate_thread.daemon = True + blacklistupdate_thread.start() + + whitelistupdate_thread = Thread(target=whitelistUpdate) + whitelistupdate_thread.daemon = True + whitelistupdate_thread.start() signal.signal(signal.SIGTERM, quit) atexit.register(clear) diff --git a/data/Dockerfiles/olefy/Dockerfile b/data/Dockerfiles/olefy/Dockerfile new file mode 100644 index 00000000..01ebe9ed --- /dev/null +++ b/data/Dockerfiles/olefy/Dockerfile @@ -0,0 +1,19 @@ +FROM alpine:3.10 +LABEL maintainer "Andre Peters <andre.peters@servercow.de>" + +WORKDIR /app + +#RUN addgroup -S olefy && adduser -S olefy -G olefy \ +RUN apk add --virtual .build-deps gcc python3-dev musl-dev libffi-dev openssl-dev \ + && apk add --update --no-cache python3 openssl tzdata libmagic \ + && pip3 install --upgrade pip \ + && pip3 install --upgrade oletools asyncio python-magic \ + && apk del .build-deps + +ADD https://raw.githubusercontent.com/HeinleinSupport/olefy/master/olefy.py /app/ + +RUN chown -R nobody:nobody /app /tmp + +USER nobody + +CMD ["python3", "-u", "/app/olefy.py"] diff --git a/data/Dockerfiles/phpfpm/Dockerfile b/data/Dockerfiles/phpfpm/Dockerfile index de31031a..ea8502d7 100644 --- a/data/Dockerfiles/phpfpm/Dockerfile +++ b/data/Dockerfiles/phpfpm/Dockerfile @@ -1,11 +1,11 @@ -FROM php:7.3-fpm-alpine3.8 +FROM php:7.3-fpm-alpine3.10 LABEL maintainer "Andre Peters <andre.peters@servercow.de>" -ENV APCU_PECL 5.1.16 -ENV IMAGICK_PECL 3.4.3 +ENV APCU_PECL 5.1.17 +ENV IMAGICK_PECL 3.4.4 #ENV MAILPARSE_PECL 3.0.2 ENV MEMCACHED_PECL 3.1.3 -ENV REDIS_PECL 4.2.0 +ENV REDIS_PECL 5.0.1 RUN apk add -U --no-cache autoconf \ bash \ @@ -53,13 +53,14 @@ RUN apk add -U --no-cache autoconf \ && docker-php-ext-enable apcu imagick memcached mailparse redis \ && pecl clear-cache \ && docker-php-ext-configure intl \ + && docker-php-ext-configure exif \ && docker-php-ext-configure gd \ --with-gd \ --enable-gd-native-ttf \ --with-freetype-dir=/usr/include/ \ --with-png-dir=/usr/include/ \ --with-jpeg-dir=/usr/include/ \ - && docker-php-ext-install -j 4 gd gettext intl ldap opcache pcntl pdo pdo_mysql soap sockets xmlrpc zip \ + && docker-php-ext-install -j 4 exif gd gettext intl ldap opcache pcntl pdo pdo_mysql soap sockets xmlrpc zip \ && docker-php-ext-configure imap --with-imap --with-imap-ssl \ && docker-php-ext-install -j 4 imap \ && apk del --purge autoconf \ diff --git a/data/Dockerfiles/phpfpm/docker-entrypoint.sh b/data/Dockerfiles/phpfpm/docker-entrypoint.sh index 76c4035e..cde365ff 100755 --- a/data/Dockerfiles/phpfpm/docker-entrypoint.sh +++ b/data/Dockerfiles/phpfpm/docker-entrypoint.sh @@ -19,29 +19,48 @@ if [[ -z $(redis-cli --raw -h redis-mailcow GET Q_RELEASE_FORMAT) ]]; then redis-cli --raw -h redis-mailcow SET Q_RELEASE_FORMAT raw fi +# Set max age of q items - if unset + +if [[ -z $(redis-cli --raw -h redis-mailcow GET Q_MAX_AGE) ]]; then + redis-cli --raw -h redis-mailcow SET Q_MAX_AGE 365 +fi + # Check of mysql_upgrade CONTAINER_ID= # Todo: Better check if upgrade failed # This can happen due to a broken sogo_view [ -s /mysql_upgrade_loop ] && SQL_LOOP_C=$(cat /mysql_upgrade_loop) -CONTAINER_ID=$(curl --silent --insecure https://dockerapi/containers/json | jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], id: .Id}" | jq -rc "select( .name | tostring | contains(\"mysql-mailcow\")) | .id") -if [[ ! -z "${CONTAINER_ID}" ]] && [[ "${CONTAINER_ID}" =~ [^a-zA-Z0-9] ]]; then - SQL_UPGRADE_RETURN=$(curl --silent --insecure -XPOST https://dockerapi/containers/${CONTAINER_ID}/exec -d '{"cmd":"system", "task":"mysql_upgrade"}' --silent -H 'Content-type: application/json' | jq -r .type) - if [[ ${SQL_UPGRADE_RETURN} == 'warning' ]]; then - if [ -z ${SQL_LOOP_C} ]; then - echo 1 > /mysql_upgrade_loop - echo "MySQL applied an upgrade, restarting PHP-FPM..." - exit 1 +until [[ ! -z "${CONTAINER_ID}" ]] && [[ "${CONTAINER_ID}" =~ ^[[:alnum:]]*$ ]]; do + CONTAINER_ID=$(curl --silent --insecure https://dockerapi/containers/json | jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], id: .Id}" 2> /dev/null | jq -rc "select( .name | tostring | contains(\"mysql-mailcow\")) | .id" 2> /dev/null) +done +echo "MySQL @ ${CONTAINER_ID}" +SQL_UPGRADE_RETURN=$(curl --silent --insecure -XPOST https://dockerapi/containers/${CONTAINER_ID}/exec -d '{"cmd":"system", "task":"mysql_upgrade"}' --silent -H 'Content-type: application/json' | jq -r .type) +if [[ ${SQL_UPGRADE_RETURN} == 'warning' ]]; then + if [ -z ${SQL_LOOP_C} ]; then + echo 1 > /mysql_upgrade_loop + echo "MySQL applied an upgrade" + POSTFIX=($(curl --silent --insecure https://dockerapi/containers/json | jq -r '.[] | {name: .Config.Labels["com.docker.compose.service"], id: .Id}' | jq -rc 'select( .name | tostring | contains("postfix-mailcow")) | .id' | tr "\n" " ")) + if [[ -z ${POSTFIX} ]]; then + echo "Could not determine Postfix container ID, skipping Postfix restart." else - rm /mysql_upgrade_loop - echo "MySQL was not applied previously, skipping. Restart php-fpm-mailcow to retry or run mysql_upgrade manually." - while ! mysqladmin status --socket=/var/run/mysqld/mysqld.sock -u${DBUSER} -p${DBPASS} --silent; do - echo "Waiting for SQL to return..." - sleep 2 - done + echo "Restarting Postfix" + curl -X POST --silent --insecure https://dockerapi/containers/${POSTFIX}/restart | jq -r '.msg' + echo "Sleeping 10 seconds..." + sleep 10 fi + echo "Restarting PHP-FPM, bye" + exit 1 + else + rm /mysql_upgrade_loop + echo "MySQL was not applied previously, skipping. Restart php-fpm-mailcow to retry or run mysql_upgrade manually." + while ! mysqladmin status --socket=/var/run/mysqld/mysqld.sock -u${DBUSER} -p${DBPASS} --silent; do + echo "Waiting for SQL to return..." + sleep 2 + done fi +else + echo "MySQL is up-to-date" fi # Trigger db init diff --git a/data/Dockerfiles/postfix/Dockerfile b/data/Dockerfiles/postfix/Dockerfile index 05f2c3c7..77173028 100644 --- a/data/Dockerfiles/postfix/Dockerfile +++ b/data/Dockerfiles/postfix/Dockerfile @@ -1,4 +1,4 @@ -FROM ubuntu:bionic +FROM debian:buster-slim LABEL maintainer "Andre Peters <andre.peters@servercow.de>" ARG DEBIAN_FRONTEND=noninteractive @@ -9,12 +9,17 @@ RUN dpkg-divert --local --rename --add /sbin/initctl \ && dpkg-divert --local --rename --add /usr/bin/ischroot \ && ln -sf /bin/true /usr/bin/ischroot -RUN apt-get update && apt-get install -y --no-install-recommends \ +# Add groups and users before installing Postfix to not break compatibility +RUN groupadd -g 102 postfix \ + && groupadd -g 103 postdrop \ + && useradd -g postfix -u 101 -d /var/spool/postfix -s /usr/sbin/nologin postfix \ + && apt-get update && apt-get install -y --no-install-recommends \ ca-certificates \ curl \ dirmngr \ gnupg \ libsasl2-modules \ + mariadb-client \ perl \ postfix \ postfix-mysql \ @@ -32,18 +37,9 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ && printf '#!/bin/bash\n/usr/sbin/postconf -c /opt/postfix/conf "$@"' > /usr/local/sbin/postconf \ && chmod +x /usr/local/sbin/postconf -RUN addgroup --system --gid 600 zeyple \ - && adduser --system --home /var/lib/zeyple --no-create-home --uid 600 --gid 600 --disabled-login zeyple \ - && touch /var/log/zeyple.log \ - && chown zeyple: /var/log/zeyple.log \ - && mkdir -p /opt/mailman/var/data \ - && touch /opt/mailman/var/data/postfix_lmtp \ - && touch /opt/mailman/var/data/postfix_domains - -COPY zeyple.py /usr/local/bin/zeyple.py -COPY zeyple.conf /etc/zeyple.conf COPY supervisord.conf /etc/supervisor/supervisord.conf COPY syslog-ng.conf /etc/syslog-ng/syslog-ng.conf +COPY stop-supervisor.sh /usr/local/sbin/stop-supervisor.sh COPY postfix.sh /opt/postfix.sh COPY rspamd-pipe-ham /usr/local/bin/rspamd-pipe-ham COPY rspamd-pipe-spam /usr/local/bin/rspamd-pipe-spam diff --git a/data/Dockerfiles/postfix/postfix.sh b/data/Dockerfiles/postfix/postfix.sh index 6ec5cc1d..3d060e8c 100755 --- a/data/Dockerfiles/postfix/postfix.sh +++ b/data/Dockerfiles/postfix/postfix.sh @@ -4,14 +4,23 @@ trap "postfix stop" EXIT [[ ! -d /opt/postfix/conf/sql/ ]] && mkdir -p /opt/postfix/conf/sql/ +# Wait for MySQL to warm-up +while ! mysqladmin status --socket=/var/run/mysqld/mysqld.sock -u${DBUSER} -p${DBPASS} --silent; do + echo "Waiting for database to come up..." + sleep 2 +done + cat <<EOF > /etc/aliases +# Autogenerated by mailcow null: /dev/null +watchdog: /dev/null ham: "|/usr/local/bin/rspamd-pipe-ham" spam: "|/usr/local/bin/rspamd-pipe-spam" EOF newaliases; cat <<EOF > /opt/postfix/conf/sql/mysql_relay_recipient_maps.cf +# Autogenerated by mailcow user = ${DBUSER} password = ${DBPASS} hosts = unix:/var/run/mysqld/mysqld.sock @@ -30,6 +39,7 @@ query = SELECT DISTINCT EOF cat <<EOF > /opt/postfix/conf/sql/mysql_tls_policy_override_maps.cf +# Autogenerated by mailcow user = ${DBUSER} password = ${DBPASS} hosts = unix:/var/run/mysqld/mysqld.sock @@ -38,6 +48,7 @@ query = SELECT CONCAT(policy, ' ', parameters) AS tls_policy FROM tls_policy_ove EOF cat <<EOF > /opt/postfix/conf/sql/mysql_tls_enforce_in_policy.cf +# Autogenerated by mailcow user = ${DBUSER} password = ${DBPASS} hosts = unix:/var/run/mysqld/mysqld.sock @@ -55,6 +66,7 @@ query = SELECT IF(EXISTS( EOF cat <<EOF > /opt/postfix/conf/sql/mysql_sender_dependent_default_transport_maps.cf +# Autogenerated by mailcow user = ${DBUSER} password = ${DBPASS} hosts = unix:/var/run/mysqld/mysqld.sock @@ -86,6 +98,7 @@ query = SELECT GROUP_CONCAT(transport SEPARATOR '') AS transport_maps EOF cat <<EOF > /opt/postfix/conf/sql/mysql_transport_maps.cf +# Autogenerated by mailcow user = ${DBUSER} password = ${DBPASS} hosts = unix:/var/run/mysqld/mysqld.sock @@ -95,7 +108,18 @@ query = SELECT CONCAT('smtp_via_transport_maps:', nexthop) AS transport FROM tra AND destination = '%s'; EOF +cat <<EOF > /opt/postfix/conf/sql/mysql_virtual_resource_maps.cf +# Autogenerated by mailcow +user = ${DBUSER} +password = ${DBPASS} +hosts = unix:/var/run/mysqld/mysqld.sock +dbname = ${DBNAME} +query = SELECT 'null@localhost' FROM mailbox + WHERE kind REGEXP 'location|thing|group' AND username = '%s'; +EOF + cat <<EOF > /opt/postfix/conf/sql/mysql_sasl_passwd_maps_sender_dependent.cf +# Autogenerated by mailcow user = ${DBUSER} password = ${DBPASS} hosts = unix:/var/run/mysqld/mysqld.sock @@ -104,8 +128,8 @@ query = SELECT CONCAT_WS(':', username, password) AS auth_data FROM relayhosts WHERE id IN ( SELECT relayhost FROM domain WHERE CONCAT('@', domain) = '%s' - OR '%s' IN ( - SELECT CONCAT('@', alias_domain) FROM alias_domain + OR domain IN ( + SELECT target_domain FROM alias_domain WHERE CONCAT('@', alias_domain) = '%s' ) ) AND active = '1' @@ -113,6 +137,7 @@ query = SELECT CONCAT_WS(':', username, password) AS auth_data FROM relayhosts EOF cat <<EOF > /opt/postfix/conf/sql/mysql_sasl_passwd_maps_transport_maps.cf +# Autogenerated by mailcow user = ${DBUSER} password = ${DBPASS} hosts = unix:/var/run/mysqld/mysqld.sock @@ -124,18 +149,8 @@ query = SELECT CONCAT_WS(':', username, password) AS auth_data FROM transports LIMIT 1; EOF -cat <<EOF > /opt/postfix/conf/sql/mysql_virtual_alias_domain_catchall_maps.cf -user = ${DBUSER} -password = ${DBPASS} -hosts = unix:/var/run/mysqld/mysqld.sock -dbname = ${DBNAME} -query = SELECT goto FROM alias, alias_domain - WHERE alias_domain.alias_domain = '%d' - AND alias.address = CONCAT('@', alias_domain.target_domain) - AND alias.active = 1 AND alias_domain.active='1' -EOF - cat <<EOF > /opt/postfix/conf/sql/mysql_virtual_alias_domain_maps.cf +# Autogenerated by mailcow user = ${DBUSER} password = ${DBPASS} hosts = unix:/var/run/mysqld/mysqld.sock @@ -148,6 +163,7 @@ query = SELECT username FROM mailbox, alias_domain EOF cat <<EOF > /opt/postfix/conf/sql/mysql_virtual_alias_maps.cf +# Autogenerated by mailcow user = ${DBUSER} password = ${DBPASS} hosts = unix:/var/run/mysqld/mysqld.sock @@ -158,6 +174,7 @@ query = SELECT goto FROM alias EOF cat <<EOF > /opt/postfix/conf/sql/mysql_recipient_bcc_maps.cf +# Autogenerated by mailcow user = ${DBUSER} password = ${DBPASS} hosts = unix:/var/run/mysqld/mysqld.sock @@ -169,6 +186,7 @@ query = SELECT bcc_dest FROM bcc_maps EOF cat <<EOF > /opt/postfix/conf/sql/mysql_sender_bcc_maps.cf +# Autogenerated by mailcow user = ${DBUSER} password = ${DBPASS} hosts = unix:/var/run/mysqld/mysqld.sock @@ -180,6 +198,7 @@ query = SELECT bcc_dest FROM bcc_maps EOF cat <<EOF > /opt/postfix/conf/sql/mysql_recipient_canonical_maps.cf +# Autogenerated by mailcow user = ${DBUSER} password = ${DBPASS} hosts = unix:/var/run/mysqld/mysqld.sock @@ -190,6 +209,7 @@ query = SELECT new_dest FROM recipient_maps EOF cat <<EOF > /opt/postfix/conf/sql/mysql_virtual_domains_maps.cf +# Autogenerated by mailcow user = ${DBUSER} password = ${DBPASS} hosts = unix:/var/run/mysqld/mysqld.sock @@ -203,6 +223,7 @@ query = SELECT alias_domain from alias_domain WHERE alias_domain='%s' AND active EOF cat <<EOF > /opt/postfix/conf/sql/mysql_virtual_mailbox_maps.cf +# Autogenerated by mailcow user = ${DBUSER} password = ${DBPASS} hosts = unix:/var/run/mysqld/mysqld.sock @@ -211,6 +232,7 @@ query = SELECT CONCAT(JSON_UNQUOTE(JSON_EXTRACT(attributes, '$.mailbox_format')) EOF cat <<EOF > /opt/postfix/conf/sql/mysql_virtual_relay_domain_maps.cf +# Autogenerated by mailcow user = ${DBUSER} password = ${DBPASS} hosts = unix:/var/run/mysqld/mysqld.sock @@ -219,6 +241,7 @@ query = SELECT domain FROM domain WHERE domain='%s' AND backupmx = '1' AND activ EOF cat <<EOF > /opt/postfix/conf/sql/mysql_virtual_sender_acl.cf +# Autogenerated by mailcow user = ${DBUSER} password = ${DBPASS} hosts = unix:/var/run/mysqld/mysqld.sock @@ -260,6 +283,7 @@ query = SELECT goto FROM alias EOF cat <<EOF > /opt/postfix/conf/sql/mysql_virtual_spamalias_maps.cf +# Autogenerated by mailcow user = ${DBUSER} password = ${DBPASS} hosts = unix:/var/run/mysqld/mysqld.sock @@ -269,10 +293,11 @@ query = SELECT goto FROM spamalias AND validity >= UNIX_TIMESTAMP() EOF -# Reset GPG key permissions -mkdir -p /var/lib/zeyple/keys -chmod 700 /var/lib/zeyple/keys -chown -R 600:600 /var/lib/zeyple/keys +sed -i '/User overrides/q' /opt/postfix/conf/main.cf +echo >> /opt/postfix/conf/main.cf +if [ -f /opt/postfix/conf/extra.cf ]; then + cat /opt/postfix/conf/extra.cf >> /opt/postfix/conf/main.cf +fi # Fix Postfix permissions chown -R root:postfix /opt/postfix/conf/sql/ @@ -282,7 +307,7 @@ chgrp -R postdrop /var/spool/postfix/maildrop postfix set-permissions # Check Postfix configuration -postconf -c /opt/postfix/conf +postconf -c /opt/postfix/conf > /dev/null if [[ $? != 0 ]]; then echo "Postfix configuration error, refusing to start." diff --git a/data/Dockerfiles/postfix/supervisord.conf b/data/Dockerfiles/postfix/supervisord.conf index 27494bd6..134a6c6d 100644 --- a/data/Dockerfiles/postfix/supervisord.conf +++ b/data/Dockerfiles/postfix/supervisord.conf @@ -1,4 +1,5 @@ [supervisord] +pidfile=/var/run/supervisord.pid nodaemon=true user=root @@ -12,6 +13,10 @@ autostart=true [program:postfix] command=/opt/postfix.sh +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 autorestart=true [eventlistener:processes] diff --git a/data/Dockerfiles/postfix/syslog-ng.conf b/data/Dockerfiles/postfix/syslog-ng.conf index 5d11a3b7..21044719 100644 --- a/data/Dockerfiles/postfix/syslog-ng.conf +++ b/data/Dockerfiles/postfix/syslog-ng.conf @@ -1,9 +1,10 @@ -@version: 3.13 +@version: 3.19 @include "scl.conf" options { chain_hostnames(off); flush_lines(0); use_dns(no); + dns_cache(no); use_fqdn(no); owner("root"); group("adm"); perm(0640); stats_freq(0); diff --git a/data/Dockerfiles/postfix/zeyple.conf b/data/Dockerfiles/postfix/zeyple.conf deleted file mode 100644 index cc176a0e..00000000 --- a/data/Dockerfiles/postfix/zeyple.conf +++ /dev/null @@ -1,9 +0,0 @@ -[zeyple] -log_file = /dev/null - -[gpg] -home = /var/lib/zeyple/keys - -[relay] -host = localhost -port = 10026 diff --git a/data/Dockerfiles/postfix/zeyple.py b/data/Dockerfiles/postfix/zeyple.py deleted file mode 100755 index bb218831..00000000 --- a/data/Dockerfiles/postfix/zeyple.py +++ /dev/null @@ -1,274 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -import sys -import os -import logging -import email -import email.mime.multipart -import email.mime.application -import email.encoders -import smtplib -import copy -from io import BytesIO - -try: - from configparser import SafeConfigParser # Python 3 -except ImportError: - from ConfigParser import SafeConfigParser # Python 2 - -import gpgme - -# Boiler plate to avoid dependency on six -# BBB: Python 2.7 support -PY3K = sys.version_info > (3, 0) - - -def message_from_binary(message): - if PY3K: - return email.message_from_bytes(message) - else: - return email.message_from_string(message) - - -def as_binary_string(email): - if PY3K: - return email.as_bytes() - else: - return email.as_string() - - -def encode_string(string): - if isinstance(string, bytes): - return string - else: - return string.encode('utf-8') - - -__title__ = 'Zeyple' -__version__ = '1.2.0' -__author__ = 'Cédric Félizard' -__license__ = 'AGPLv3+' -__copyright__ = 'Copyright 2012-2016 Cédric Félizard' - - -class Zeyple: - """Zeyple Encrypts Your Precious Log Emails""" - - def __init__(self, config_fname='zeyple.conf'): - self.config = self.load_configuration(config_fname) - - log_file = self.config.get('zeyple', 'log_file') - logging.basicConfig( - filename=log_file, level=logging.DEBUG, - format='%(asctime)s %(process)s %(levelname)s %(message)s' - ) - logging.info("Zeyple ready to encrypt outgoing emails") - - def load_configuration(self, filename): - """Reads and parses the config file""" - - config = SafeConfigParser() - config.read([ - os.path.join('/etc/', filename), - filename, - ]) - if not config.sections(): - raise IOError('Cannot open config file.') - return config - - @property - def gpg(self): - protocol = gpgme.PROTOCOL_OpenPGP - - if self.config.has_option('gpg', 'executable'): - executable = self.config.get('gpg', 'executable') - else: - executable = None # Default value - - home_dir = self.config.get('gpg', 'home') - - ctx = gpgme.Context() - ctx.set_engine_info(protocol, executable, home_dir) - ctx.armor = True - - return ctx - - def process_message(self, message_data, recipients): - """Encrypts the message with recipient keys""" - message_data = encode_string(message_data) - - in_message = message_from_binary(message_data) - logging.info( - "Processing outgoing message %s", in_message['Message-id']) - - if not recipients: - logging.warn("Cannot find any recipients, ignoring") - - sent_messages = [] - for recipient in recipients: - logging.info("Recipient: %s", recipient) - - key_id = self._user_key(recipient) - logging.info("Key ID: %s", key_id) - - if key_id: - out_message = self._encrypt_message(in_message, key_id) - - # Delete Content-Transfer-Encoding if present to default to - # "7bit" otherwise Thunderbird seems to hang in some cases. - del out_message["Content-Transfer-Encoding"] - else: - logging.warn("No keys found, message will be sent unencrypted") - out_message = copy.copy(in_message) - - self._add_zeyple_header(out_message) - self._send_message(out_message, recipient) - sent_messages.append(out_message) - - return sent_messages - - def _get_version_part(self): - ret = email.mime.application.MIMEApplication( - 'Version: 1\n', - 'pgp-encrypted', - email.encoders.encode_noop, - ) - ret.add_header( - 'Content-Description', - "PGP/MIME version identification", - ) - return ret - - def _get_encrypted_part(self, payload): - ret = email.mime.application.MIMEApplication( - payload, - 'octet-stream', - email.encoders.encode_noop, - name="encrypted.asc", - ) - ret.add_header('Content-Description', "OpenPGP encrypted message") - ret.add_header( - 'Content-Disposition', - 'inline', - filename='encrypted.asc', - ) - return ret - - def _encrypt_message(self, in_message, key_id): - if in_message.is_multipart(): - # get the body (after the first \n\n) - payload = in_message.as_string().split("\n\n", 1)[1].strip() - - # prepend the Content-Type including the boundary - content_type = "Content-Type: " + in_message["Content-Type"] - payload = content_type + "\n\n" + payload - - message = email.message.Message() - message.set_payload(payload) - - payload = message.get_payload() - - else: - payload = in_message.get_payload() - payload = encode_string(payload) - - quoted_printable = email.charset.Charset('ascii') - quoted_printable.body_encoding = email.charset.QP - - message = email.mime.nonmultipart.MIMENonMultipart( - 'text', 'plain', charset='utf-8' - ) - message.set_payload(payload, charset=quoted_printable) - - mixed = email.mime.multipart.MIMEMultipart( - 'mixed', - None, - [message], - ) - - # remove superfluous header - del mixed['MIME-Version'] - - payload = as_binary_string(mixed) - - encrypted_payload = self._encrypt_payload(payload, [key_id]) - - version = self._get_version_part() - encrypted = self._get_encrypted_part(encrypted_payload) - - out_message = copy.copy(in_message) - out_message.preamble = "This is an OpenPGP/MIME encrypted " \ - "message (RFC 4880 and 3156)" - - if 'Content-Type' not in out_message: - out_message['Content-Type'] = 'multipart/encrypted' - else: - out_message.replace_header( - 'Content-Type', - 'multipart/encrypted', - ) - - out_message.set_param('protocol', 'application/pgp-encrypted') - out_message.set_payload([version, encrypted]) - - return out_message - - def _encrypt_payload(self, payload, key_ids): - """Encrypts the payload with the given keys""" - payload = encode_string(payload) - - plaintext = BytesIO(payload) - ciphertext = BytesIO() - - self.gpg.armor = True - - recipient = [self.gpg.get_key(key_id) for key_id in key_ids] - - self.gpg.encrypt(recipient, gpgme.ENCRYPT_ALWAYS_TRUST, - plaintext, ciphertext) - - return ciphertext.getvalue() - - def _user_key(self, email): - """Returns the GPG key for the given email address""" - logging.info("Trying to encrypt for %s", email) - keys = [key for key in self.gpg.keylist(email)] - - if keys: - key = keys.pop() # NOTE: looks like keys[0] is the master key - key_id = key.subkeys[0].keyid - return key_id - - return None - - def _add_zeyple_header(self, message): - if self.config.has_option('zeyple', 'add_header') and \ - self.config.getboolean('zeyple', 'add_header'): - message.add_header( - 'X-Zeyple', - "processed by {0} v{1}".format(__title__, __version__) - ) - - def _send_message(self, message, recipient): - """Sends the given message through the SMTP relay""" - logging.info("Sending message %s", message['Message-id']) - - smtp = smtplib.SMTP(self.config.get('relay', 'host'), - self.config.get('relay', 'port')) - - smtp.sendmail(message['From'], recipient, message.as_string()) - smtp.quit() - - logging.info("Message %s sent", message['Message-id']) - - -if __name__ == '__main__': - recipients = sys.argv[1:] - - # BBB: Python 2.7 support - binary_stdin = sys.stdin.buffer if PY3K else sys.stdin - message = binary_stdin.read() - - zeyple = Zeyple() - zeyple.process_message(message, recipients) diff --git a/data/Dockerfiles/rspamd/Dockerfile b/data/Dockerfiles/rspamd/Dockerfile index a2b5f7f8..58cd1ac5 100644 --- a/data/Dockerfiles/rspamd/Dockerfile +++ b/data/Dockerfiles/rspamd/Dockerfile @@ -1,27 +1,29 @@ -FROM ubuntu:bionic +FROM debian:buster-slim LABEL maintainer "Andre Peters <andre.peters@servercow.de>" ARG DEBIAN_FRONTEND=noninteractive +ARG CODENAME=buster ENV LC_ALL C RUN apt-get update && apt-get install -y \ tzdata \ - ca-certificates \ - gnupg2 \ - apt-transport-https \ - && apt-key adv --fetch-keys https://rspamd.com/apt/gpg.key \ - && echo "deb https://rspamd.com/apt-stable/ bionic main" > /etc/apt/sources.list.d/rspamd.list \ - && apt-get update && apt-get install -y rspamd \ - && rm -rf /var/lib/apt/lists/* \ - && echo '.include $LOCAL_CONFDIR/local.d/rspamd.conf.local' > /etc/rspamd/rspamd.conf.local \ - && apt-get autoremove --purge \ - && apt-get clean \ - && mkdir -p /run/rspamd \ - && chown _rspamd:_rspamd /run/rspamd + ca-certificates \ + gnupg2 \ + apt-transport-https \ + dnsutils \ + && apt-key adv --fetch-keys https://rspamd.com/apt-stable/gpg.key \ + && echo "deb [arch=amd64] https://rspamd.com/apt-stable/ $CODENAME main" > /etc/apt/sources.list.d/rspamd.list \ + && echo "deb-src [arch=amd64] https://rspamd.com/apt-stable/ $CODENAME main" >> /etc/apt/sources.list.d/rspamd.list \ + && apt-get update \ + && apt-get --no-install-recommends -y install rspamd \ + && rm -rf /var/lib/apt/lists/* \ + && apt-get autoremove --purge \ + && apt-get clean \ + && mkdir -p /run/rspamd \ + && chown _rspamd:_rspamd /run/rspamd COPY settings.conf /etc/rspamd/settings.conf COPY docker-entrypoint.sh /docker-entrypoint.sh -COPY metadata_exporter.lua /usr/share/rspamd/lua/metadata_exporter.lua ENTRYPOINT ["/docker-entrypoint.sh"] diff --git a/data/Dockerfiles/rspamd/docker-entrypoint.sh b/data/Dockerfiles/rspamd/docker-entrypoint.sh index 6288550d..cb7d0563 100755 --- a/data/Dockerfiles/rspamd/docker-entrypoint.sh +++ b/data/Dockerfiles/rspamd/docker-entrypoint.sh @@ -1,9 +1,37 @@ #!/bin/bash -chown -R _rspamd:_rspamd /var/lib/rspamd /etc/rspamd/local.d /etc/rspamd/override.d /etc/rspamd/custom +mkdir -p /etc/rspamd/plugins.d \ + /etc/rspamd/custom + +touch /etc/rspamd/rspamd.conf.local \ + /etc/rspamd/rspamd.conf.override + chmod 755 /var/lib/rspamd -[[ ! -f /etc/rspamd/override.d/worker-controller-password.inc ]] && echo '# Placeholder' > /etc/rspamd/override.d/worker-controller-password.inc -chown _rspamd:_rspamd /etc/rspamd/override.d/worker-controller-password.inc -[[ ! -f /etc/rspamd/custom/sa-rules-heinlein ]] && echo '# to be auto-filled by dovecot-mailcow' > /etc/rspamd/custom/sa-rules-heinlein + +[[ ! -f /etc/rspamd/override.d/worker-controller-password.inc ]] && echo '# Autogenerated by mailcow' > /etc/rspamd/override.d/worker-controller-password.inc +[[ ! -f /etc/rspamd/custom/sa-rules-heinlein ]] && echo '# Autogenerated by mailcow' > /etc/rspamd/custom/sa-rules-heinlein +[[ ! -f /etc/rspamd/custom/dovecot_trusted.map ]] && echo '# Autogenerated by mailcow' > /etc/rspamd/custom/dovecot_trusted.map + +DOVECOT_V4= +DOVECOT_V6= +until [[ ! -z ${DOVECOT_V4} ]]; do + DOVECOT_V4=$(dig a dovecot +short) + DOVECOT_V6=$(dig aaaa dovecot +short) + [[ ! -z ${DOVECOT_V4} ]] && break; + echo "Waiting for Dovecot" + sleep 3 +done +echo ${DOVECOT_V4}/32 > /etc/rspamd/custom/dovecot_trusted.map +if [[ ! -z ${DOVECOT_V6} ]]; then + echo ${DOVECOT_V6}/128 >> /etc/rspamd/custom/dovecot_trusted.map +fi + +chown -R _rspamd:_rspamd /var/lib/rspamd \ + /etc/rspamd/local.d \ + /etc/rspamd/override.d \ + /etc/rspamd/custom \ + /etc/rspamd/rspamd.conf.local \ + /etc/rspamd/rspamd.conf.override \ + /etc/rspamd/plugins.d exec "$@" diff --git a/data/Dockerfiles/sogo/Dockerfile b/data/Dockerfiles/sogo/Dockerfile index 085f1bc2..6d57aa4b 100644 --- a/data/Dockerfiles/sogo/Dockerfile +++ b/data/Dockerfiles/sogo/Dockerfile @@ -3,7 +3,7 @@ LABEL maintainer "Andre Peters <andre.peters@servercow.de>" ARG DEBIAN_FRONTEND=noninteractive ENV LC_ALL C -ENV GOSU_VERSION 1.9 +ENV GOSU_VERSION 1.11 # Prerequisites RUN apt-get update && apt-get install -y --no-install-recommends \ @@ -13,6 +13,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ gettext \ gnupg \ mysql-client \ + rsync \ supervisor \ syslog-ng \ syslog-ng-core \ @@ -22,23 +23,19 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ psmisc \ wget \ patch \ - && rm -rf /var/lib/apt/lists/* \ && 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" \ && chmod +x /usr/local/bin/gosu \ - && gosu nobody true - -RUN mkdir /usr/share/doc/sogo \ + && gosu nobody true \ + && mkdir /usr/share/doc/sogo \ && touch /usr/share/doc/sogo/empty.sh \ && apt-key adv --keyserver keyserver.ubuntu.com --recv-key 0x810273C4 \ && echo "deb http://packages.inverse.ca/SOGo/nightly/4/debian/ stretch stretch" > /etc/apt/sources.list.d/sogo.list \ - && apt-get update && apt-get install -y --force-yes \ + && apt-get update && apt-get install -y --no-install-recommends \ sogo \ sogo-activesync \ + && apt-get autoclean \ && rm -rf /var/lib/apt/lists/* \ - && echo '* * * * * sogo /usr/sbin/sogo-ealarms-notify -p /etc/sogo/sieve.creds 2>/dev/null' > /etc/cron.d/sogo \ - && echo '* * * * * sogo /usr/sbin/sogo-tool expire-sessions 60' >> /etc/cron.d/sogo \ - && echo '0 0 * * * sogo /usr/sbin/sogo-tool update-autoreply -p /etc/sogo/sieve.creds' >> /etc/cron.d/sogo \ && touch /etc/default/locale COPY ./bootstrap-sogo.sh /bootstrap-sogo.sh @@ -51,7 +48,3 @@ RUN chmod +x /bootstrap-sogo.sh \ /usr/local/sbin/stop-supervisor.sh CMD exec /usr/bin/supervisord -c /etc/supervisor/supervisord.conf - -VOLUME /usr/lib/GNUstep/SOGo/ - -RUN rm -rf /tmp/* /var/tmp/* diff --git a/data/Dockerfiles/sogo/bootstrap-sogo.sh b/data/Dockerfiles/sogo/bootstrap-sogo.sh index 5072a306..b85c9de4 100755 --- a/data/Dockerfiles/sogo/bootstrap-sogo.sh +++ b/data/Dockerfiles/sogo/bootstrap-sogo.sh @@ -14,11 +14,11 @@ do done # Wait for updated schema -DBV_NOW=$(mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT version FROM versions;" -BN) +DBV_NOW=$(mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT version FROM versions WHERE application = 'db_schema';" -BN) DBV_NEW=$(grep -oE '\$db_version = .*;' init_db.inc.php | sed 's/$db_version = //g;s/;//g' | cut -d \" -f2) while [[ ${DBV_NOW} != ${DBV_NEW} ]]; do echo "Waiting for schema update..." - DBV_NOW=$(mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT version FROM versions;" -BN) + DBV_NOW=$(mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT version FROM versions WHERE application = 'db_schema';" -BN) DBV_NEW=$(grep -oE '\$db_version = .*;' init_db.inc.php | sed 's/$db_version = //g;s/;//g' | cut -d \" -f2) sleep 5 done @@ -30,10 +30,11 @@ mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e while [[ ${VIEW_OK} != 'OK' ]]; do mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} << EOF -CREATE VIEW sogo_view (c_uid, domain, c_name, c_password, c_cn, mail, aliases, ad_aliases, kind, multiple_bookings) AS -SELECT mailbox.username, mailbox.domain, mailbox.username, if(json_extract(attributes, '$.force_pw_update') LIKE '%0%', if(json_extract(attributes, '$.sogo_access') LIKE '%1%', password, 'invalid'), 'invalid'), mailbox.name, mailbox.username, IFNULL(GROUP_CONCAT(ga.aliases SEPARATOR ' '), ''), IFNULL(gda.ad_alias, ''), mailbox.kind, mailbox.multiple_bookings FROM mailbox +CREATE VIEW sogo_view (c_uid, domain, c_name, c_password, c_cn, mail, aliases, ad_aliases, ext_acl, kind, multiple_bookings) AS +SELECT mailbox.username, mailbox.domain, mailbox.username, if(json_extract(attributes, '$.force_pw_update') LIKE '%0%', if(json_extract(attributes, '$.sogo_access') LIKE '%1%', password, '{SSHA256}A123A123A321A321A321B321B321B123B123B321B432F123E321123123321321'), '{SSHA256}A123A123A321A321A321B321B321B123B123B321B432F123E321123123321321'), mailbox.name, mailbox.username, IFNULL(GROUP_CONCAT(ga.aliases SEPARATOR ' '), ''), IFNULL(gda.ad_alias, ''), IFNULL(external_acl.send_as_acl, ''), mailbox.kind, mailbox.multiple_bookings FROM mailbox LEFT OUTER JOIN grouped_mail_aliases ga ON ga.username REGEXP CONCAT('(^|,)', mailbox.username, '($|,)') LEFT OUTER JOIN grouped_domain_alias_address gda ON gda.username = mailbox.username +LEFT OUTER JOIN grouped_sender_acl_external external_acl ON external_acl.username = mailbox.username WHERE mailbox.active = '1' GROUP BY mailbox.username; EOF @@ -51,7 +52,7 @@ while [[ ${STATIC_VIEW_OK} != 'OK' ]]; do if [[ ! -z $(mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -B -e "SELECT 'OK' FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = '_sogo_static_view'") ]]; then STATIC_VIEW_OK=OK echo "Updating _sogo_static_view content..." - mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -B -e "REPLACE INTO _sogo_static_view (c_uid, domain, c_name, c_password, c_cn, mail, aliases, ad_aliases, kind, multiple_bookings) SELECT c_uid, domain, c_name, c_password, c_cn, mail, aliases, ad_aliases, kind, multiple_bookings from sogo_view;" + mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -B -e "REPLACE INTO _sogo_static_view (c_uid, domain, c_name, c_password, c_cn, mail, aliases, ad_aliases, ext_acl, kind, multiple_bookings) SELECT c_uid, domain, c_name, c_password, c_cn, mail, aliases, ad_aliases, ext_acl, kind, multiple_bookings from sogo_view;" mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -B -e "DELETE FROM _sogo_static_view WHERE c_uid NOT IN (SELECT username FROM mailbox WHERE active = '1')" else echo "Waiting for database initialization..." @@ -83,9 +84,16 @@ EOF done -mkdir -p /var/lib/sogo/GNUstep/Defaults/ +if [[ "${ALLOW_ADMIN_EMAIL_LOGIN}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then + TRUST_PROXY="YES" +else + TRUST_PROXY="NO" +fi +# cat /dev/urandom seems to hang here occasionally and is not recommended anyway, better use openssl +RAND_PASS=$(openssl rand -base64 16 | tr -dc _A-Z-a-z-0-9) # Generate plist header with timezone data +mkdir -p /var/lib/sogo/GNUstep/Defaults/ 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"> @@ -93,6 +101,12 @@ cat <<EOF > /var/lib/sogo/GNUstep/Defaults/sogod.plist <dict> <key>OCSAclURL</key> <string>mysql://${DBUSER}:${DBPASS}@%2Fvar%2Frun%2Fmysqld%2Fmysqld.sock/${DBNAME}/sogo_acl</string> + <key>SOGoIMAPServer</key> + <string>imaps://${IPV4_NETWORK}.250:993</string> + <key>SOGoTrustProxyAuthentication</key> + <string>${TRUST_PROXY}</string> + <key>SOGoEncryptionKey</key> + <string>${RAND_PASS}</string> <key>OCSCacheFolderURL</key> <string>mysql://${DBUSER}:${DBPASS}@%2Fvar%2Frun%2Fmysqld%2Fmysqld.sock/${DBNAME}/sogo_cache_folder</string> <key>OCSEMailAlarmsFolderURL</key> @@ -125,6 +139,7 @@ while read -r line gal <array> <string>aliases</string> <string>ad_aliases</string> + <string>ext_acl</string> </array> <key>KindFieldName</key> <string>kind</string> @@ -168,19 +183,29 @@ chown sogo:sogo -R /var/lib/sogo/ chmod 600 /var/lib/sogo/GNUstep/Defaults/sogod.plist # Patch ACLs -if [[ ${ACL_ANYONE} == 'allow' ]]; then - #enable any or authenticated targets for ACL - if patch -R -sfN --dry-run /usr/lib/GNUstep/SOGo/Templates/UIxAclEditor.wox < /acl.diff > /dev/null; then - patch -R /usr/lib/GNUstep/SOGo/Templates/UIxAclEditor.wox < /acl.diff; - fi -else - #disable any or authenticated targets for ACL - if patch -sfN --dry-run /usr/lib/GNUstep/SOGo/Templates/UIxAclEditor.wox < /acl.diff > /dev/null; then - patch /usr/lib/GNUstep/SOGo/Templates/UIxAclEditor.wox < /acl.diff; - fi -fi +#if [[ ${ACL_ANYONE} == 'allow' ]]; then +# #enable any or authenticated targets for ACL +# if patch -R -sfN --dry-run /usr/lib/GNUstep/SOGo/Templates/UIxAclEditor.wox < /acl.diff > /dev/null; then +# patch -R /usr/lib/GNUstep/SOGo/Templates/UIxAclEditor.wox < /acl.diff; +# fi +#else +# #disable any or authenticated targets for ACL +# if patch -sfN --dry-run /usr/lib/GNUstep/SOGo/Templates/UIxAclEditor.wox < /acl.diff > /dev/null; then +# patch /usr/lib/GNUstep/SOGo/Templates/UIxAclEditor.wox < /acl.diff; +# fi +#fi # Copy logo, if any [[ -f /etc/sogo/sogo-full.svg ]] && cp /etc/sogo/sogo-full.svg /usr/lib/GNUstep/SOGo/WebServerResources/img/sogo-full.svg +# Rsync web content +echo "Syncing web content with named volume" +rsync -a /usr/lib/GNUstep/SOGo/. /sogo_web/ + +# Creating cronjobs +echo "* * * * * sogo /usr/sbin/sogo-ealarms-notify -p /etc/sogo/sieve.creds 2>/dev/null" > /etc/cron.d/sogo +echo "* * * * * sogo /usr/sbin/sogo-tool expire-sessions ${SOGO_EXPIRE_SESSION}" >> /etc/cron.d/sogo +echo "0 0 * * * sogo /usr/sbin/sogo-tool update-autoreply -p /etc/sogo/sieve.creds" >> /etc/cron.d/sogo + + exec gosu sogo /usr/sbin/sogod diff --git a/data/Dockerfiles/solr/Dockerfile b/data/Dockerfiles/solr/Dockerfile index 67cd3384..d3ae98a8 100644 --- a/data/Dockerfiles/solr/Dockerfile +++ b/data/Dockerfiles/solr/Dockerfile @@ -1,9 +1,25 @@ -FROM solr:7-alpine -USER root -COPY docker-entrypoint.sh / +FROM solr:7.7-slim -RUN apk --no-cache add su-exec curl tzdata \ +USER root + +ENV GOSU_VERSION 1.11 + +COPY docker-entrypoint.sh / +COPY solr-config-7.7.0.xml / +COPY solr-schema-7.7.0.xml / + +RUN 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" \ + && chmod +x /usr/local/bin/gosu \ + && gosu nobody true \ + && apt-get update && apt-get install -y --no-install-recommends \ + tzdata \ + curl \ + bash \ + && apt-get autoclean \ + && rm -rf /var/lib/apt/lists/* \ && chmod +x /docker-entrypoint.sh \ - && /docker-entrypoint.sh --bootstrap + && sync \ + && bash /docker-entrypoint.sh --bootstrap ENTRYPOINT ["/docker-entrypoint.sh"] diff --git a/data/Dockerfiles/solr/docker-entrypoint.sh b/data/Dockerfiles/solr/docker-entrypoint.sh index 108f8b5a..1c5c6f51 100755 --- a/data/Dockerfiles/solr/docker-entrypoint.sh +++ b/data/Dockerfiles/solr/docker-entrypoint.sh @@ -18,403 +18,44 @@ fi set -e -# allow easier debugging with `docker run -e VERBOSE=yes` -if [[ "$VERBOSE" = "yes" ]]; then - set -x -fi - # run the optional initdb . /opt/docker-solr/scripts/run-initdb -function solr_config() { - curl -XPOST http://localhost:8983/solr/dovecot/schema -H 'Content-type:application/json' -d '{ - "add-field-type":{ - "name":"long", - "class":"solr.TrieLongField" - }, - "add-field-type":{ - "name":"dovecot_text", - "class":"solr.TextField", - "autoGeneratePhraseQueries":true, - "positionIncrementGap":100, - "indexAnalyser":{ - "charFilter":{ - "class":"solr.MappingCharFilterFactory", - "mapping":"mapping-FoldToASCII.txt" - }, - "charFilter":{ - "class":"solr.MappingCharFilterFactory", - "mapping":"mapping-ISOLatin1Accent.txt" - }, - "charFilter":{ - "class":"solr.HTMLStripCharFilterFactory" - }, - "tokenizer":{ - "class":"solr.StandardTokenizerFactory" - }, - "filter":{ - "class":"solr.StopFilterFactory", - "words":"stopwords.txt", - "ignoreCase":true - }, - "filter":{ - "class":"solr.WordDelimiterGraphFilterFactory", - "generateWordParts":1, - "generateNumberParts":1, - "splitOnCaseChange":1, - "splitOnNumerics":1, - "catenateWords":1, - "catenateNumbers":1, - "catenateAll":1 - }, - "filter":{ - "class":"solr.FlattenGraphFilterFactory" - }, - "filter":{ - "class":"solr.LowerCaseFilterFactory" - }, - "filter":{ - "class":"solr.KeywordMarkerFilterFactory", - "protected":"protwords.txt" - }, - "filter":{ - "class":"solr.PorterStemFilterFactory" - } - }, - "queryAnalyzer":{ - "tokenizer":{ - "class":"solr.StandardTokenizerFactory" - }, - "filter":{ - "class":"solr.SynonymGraphFilterFactory", - "expand":true, - "ignoreCase":true, - "synonyms":synonyms.txt - }, - "filter":{ - "class":"solr.FlattenGraphFilterFactory" - }, - "filter":{ - "class":"solr.StopFilterFactory", - "words":"stopwords.txt", - "ignoreCase":true - }, - "filter":{ - "class":"solr.WordDelimiterGraphFilterFactory", - "generateWordParts":1, - "generateNumberParts":1, - "splitOnCaseChange":1, - "splitOnNumerics":1, - "catenateWords":1, - "catenateNumbers":1, - "catenateAll":1 - }, - "filter":{ - "class":"solr.LowerCaseFilterFactory" - }, - "filter":{ - "class":"solr.KeywordMarkerFilterFactory", - "protected":"protwords.txt" - }, - "filter":{ - "class":"solr.PorterStemFilterFactory" - } - } - }, - "add-field":{ - "name":"uid", - "type":"long", - "indexed":true, - "stored":true, - "required":true - }, - "add-field":{ - "name":"box", - "type":"string", - "indexed":true, - "stored":true, - "required":true - }, - "add-field":{ - "name":"user", - "type":"string", - "indexed":true, - "stored":true, - "required":true - }, - "add-field":{ - "name":"hdr", - "type":"dovecot_text", - "indexed":true, - "stored":false - - }, - "add-field":{ - "name":"body", - "type":"dovecot_text", - "indexed":true, - "stored":false - }, - "add-field":{ - "name":"from", - "type":"dovecot_text", - "indexed":true, - "stored":false - }, - "add-field":{ - "name":"to", - "type":"dovecot_text", - "indexed":true, - "stored":false - }, - "add-field":{ - "name":"cc", - "type":"dovecot_text", - "indexed":true, - "stored":false - }, - "add-field":{ - "name":"bcc", - "type":"dovecot_text", - "indexed":true, - "stored":false - }, - "add-field":{ - "name":"subject", - "type":"dovecot_text", - "indexed":true, - "stored":false - } - }' - - curl -XPOST http://localhost:8983/solr/dovecot/schema -H 'Content-type:application/json' -d '{ - "replace-field-type":{ - "name":"long", - "class":"solr.TrieLongField" - }, - "replace-field-type":{ - "name":"dovecot_text", - "class":"solr.TextField", - "autoGeneratePhraseQueries":true, - "positionIncrementGap":100, - "indexAnalyser":{ - "charFilter":{ - "class":"solr.MappingCharFilterFactory", - "mapping":"mapping-FoldToASCII.txt" - }, - "charFilter":{ - "class":"solr.MappingCharFilterFactory", - "mapping":"mapping-ISOLatin1Accent.txt" - }, - "charFilter":{ - "class":"solr.HTMLStripCharFilterFactory" - }, - "tokenizer":{ - "class":"solr.StandardTokenizerFactory" - }, - "filter":{ - "class":"solr.StopFilterFactory", - "words":"stopwords.txt", - "ignoreCase":true - }, - "filter":{ - "class":"solr.WordDelimiterGraphFilterFactory", - "generateWordParts":1, - "generateNumberParts":1, - "splitOnCaseChange":1, - "splitOnNumerics":1, - "catenateWords":1, - "catenateNumbers":1, - "catenateAll":1 - }, - "filter":{ - "class":"solr.FlattenGraphFilterFactory" - }, - "filter":{ - "class":"solr.LowerCaseFilterFactory" - }, - "filter":{ - "class":"solr.KeywordMarkerFilterFactory", - "protected":"protwords.txt" - }, - "filter":{ - "class":"solr.PorterStemFilterFactory" - } - }, - "queryAnalyzer":{ - "tokenizer":{ - "class":"solr.StandardTokenizerFactory" - }, - "filter":{ - "class":"solr.SynonymGraphFilterFactory", - "expand":true, - "ignoreCase":true, - "synonyms":synonyms.txt - }, - "filter":{ - "class":"solr.FlattenGraphFilterFactory" - }, - "filter":{ - "class":"solr.StopFilterFactory", - "words":"stopwords.txt", - "ignoreCase":true - }, - "filter":{ - "class":"solr.WordDelimiterGraphFilterFactory", - "generateWordParts":1, - "generateNumberParts":1, - "splitOnCaseChange":1, - "splitOnNumerics":1, - "catenateWords":1, - "catenateNumbers":1, - "catenateAll":1 - }, - "filter":{ - "class":"solr.LowerCaseFilterFactory" - }, - "filter":{ - "class":"solr.KeywordMarkerFilterFactory", - "protected":"protwords.txt" - }, - "filter":{ - "class":"solr.PorterStemFilterFactory" - } - } - }, - "replace-field":{ - "name":"uid", - "type":"long", - "indexed":true, - "stored":true, - "required":true - }, - "replace-field":{ - "name":"box", - "type":"string", - "indexed":true, - "stored":true, - "required":true - }, - "replace-field":{ - "name":"user", - "type":"string", - "indexed":true, - "stored":true, - "required":true - }, - "replace-field":{ - "name":"hdr", - "type":"dovecot_text", - "indexed":true, - "stored":false - - }, - "replace-field":{ - "name":"body", - "type":"dovecot_text", - "indexed":true, - "stored":false - }, - "replace-field":{ - "name":"from", - "type":"dovecot_text", - "indexed":true, - "stored":false - }, - "replace-field":{ - "name":"to", - "type":"dovecot_text", - "indexed":true, - "stored":false - }, - "replace-field":{ - "name":"cc", - "type":"dovecot_text", - "indexed":true, - "stored":false - }, - "replace-field":{ - "name":"bcc", - "type":"dovecot_text", - "indexed":true, - "stored":false - }, - "replace-field":{ - "name":"subject", - "type":"dovecot_text", - "indexed":true, - "stored":false - } - }' - - curl -XPOST http://localhost:8983/solr/dovecot/config -H 'Content-type:application/json' -d '{ - "update-requesthandler":{ - "name":"/select", - "class":"solr.SearchHandler", - "defaults":{ - "wt":"xml" - } - } - }' - - curl -XPOST http://localhost:8983/solr/dovecot/config/updateHandler -d '{ - "set-property": { - "updateHandler.autoSoftCommit.maxDocs":500, - "updateHandler.autoSoftCommit.maxTime":120000, - "updateHandler.autoCommit.maxDocs":200, - "updateHandler.autoCommit.maxTime":1800000, - "updateHandler.autoCommit.openSearcher":false - } - }' -} - # fixing volume permission - -[[ -d /opt/solr/server/solr/dovecot/data ]] && chown -R solr:solr /opt/solr/server/solr/dovecot/data +[[ -d /opt/solr/server/solr/dovecot-fts/data ]] && chown -R solr:solr /opt/solr/server/solr/dovecot-fts/data if [[ "${1}" != "--bootstrap" ]]; then sed -i '/SOLR_HEAP=/c\SOLR_HEAP="'${SOLR_HEAP:-1024}'m"' /opt/solr/bin/solr.in.sh else sed -i '/SOLR_HEAP=/c\SOLR_HEAP="256m"' /opt/solr/bin/solr.in.sh fi -# start a Solr so we can use the Schema API, but only on localhost, -# so that clients don't see Solr until we have configured it. +if [[ "${1}" == "--bootstrap" ]]; then + echo "Creating initial configuration" + echo "Modifying default config set" + cp /solr-config-7.7.0.xml /opt/solr/server/solr/configsets/_default/conf/solrconfig.xml + cp /solr-schema-7.7.0.xml /opt/solr/server/solr/configsets/_default/conf/schema.xml + rm /opt/solr/server/solr/configsets/_default/conf/managed-schema -echo "Starting local Solr instance to setup configuration" -su-exec solr start-local-solr + echo "Starting local Solr instance to setup configuration" + gosu solr start-local-solr -# keep a sentinel file so we don't try to create the core a second time -# for example when we restart a container. - -SENTINEL=/opt/docker-solr/core_created - -if [[ -f ${SENTINEL} ]]; then - echo "skipping core creation" -else - echo "Creating core \"dovecot\"" - su-exec solr /opt/solr/bin/solr create -c "dovecot" + echo "Creating core \"dovecot-fts\"" + gosu solr /opt/solr/bin/solr create -c "dovecot-fts" # See https://github.com/docker-solr/docker-solr/issues/27 echo "Checking core" while ! wget -O - 'http://localhost:8983/solr/admin/cores?action=STATUS' | grep -q instanceDir; do echo "Could not find any cores, waiting..." - sleep 5 + sleep 3 done - echo "Created core \"dovecot\"" - touch ${SENTINEL} -fi -echo "Starting configuration" -while ! wget -O - 'http://localhost:8983/solr/admin/cores?action=STATUS' | grep -q instanceDir; do - echo "Waiting for Solr..." - sleep 5 -done -solr_config -echo "Stopping local Solr" -su-exec solr stop-local-solr + echo "Created core \"dovecot-fts\"" + + echo "Stopping local Solr" + gosu solr stop-local-solr -if [[ "${1}" == "--bootstrap" ]]; then exit 0 -else - exec su-exec solr solr-foreground fi + +exec gosu solr solr-foreground + diff --git a/data/Dockerfiles/solr/solr-config-7.7.0.xml b/data/Dockerfiles/solr/solr-config-7.7.0.xml new file mode 100644 index 00000000..3661874d --- /dev/null +++ b/data/Dockerfiles/solr/solr-config-7.7.0.xml @@ -0,0 +1,289 @@ +<?xml version="1.0" encoding="UTF-8" ?> + +<!-- This is the default config with stuff non-essential to Dovecot removed. --> + +<config> + <!-- Controls what version of Lucene various components of Solr + adhere to. Generally, you want to use the latest version to + get all bug fixes and improvements. It is highly recommended + that you fully re-index after changing this setting as it can + affect both how text is indexed and queried. + --> + <luceneMatchVersion>7.7.0</luceneMatchVersion> + + <!-- A 'dir' option by itself adds any files found in the directory + to the classpath, this is useful for including all jars in a + directory. + + When a 'regex' is specified in addition to a 'dir', only the + files in that directory which completely match the regex + (anchored on both ends) will be included. + + If a 'dir' option (with or without a regex) is used and nothing + is found that matches, a warning will be logged. + + The examples below can be used to load some solr-contribs along + with their external dependencies. + --> + <lib dir="${solr.install.dir:../../../..}/contrib/extraction/lib" regex=".*\.jar" /> + <lib dir="${solr.install.dir:../../../..}/dist/" regex="solr-cell-\d.*\.jar" /> + + <lib dir="${solr.install.dir:../../../..}/contrib/clustering/lib/" regex=".*\.jar" /> + <lib dir="${solr.install.dir:../../../..}/dist/" regex="solr-clustering-\d.*\.jar" /> + + <lib dir="${solr.install.dir:../../../..}/contrib/langid/lib/" regex=".*\.jar" /> + <lib dir="${solr.install.dir:../../../..}/dist/" regex="solr-langid-\d.*\.jar" /> + + <lib dir="${solr.install.dir:../../../..}/contrib/velocity/lib" regex=".*\.jar" /> + <lib dir="${solr.install.dir:../../../..}/dist/" regex="solr-velocity-\d.*\.jar" /> + + <!-- Data Directory + + Used to specify an alternate directory to hold all index data + other than the default ./data under the Solr home. If + replication is in use, this should match the replication + configuration. + --> + <dataDir>${solr.data.dir:}</dataDir> + + <!-- The default high-performance update handler --> + <updateHandler class="solr.DirectUpdateHandler2"> + + <!-- Enables a transaction log, used for real-time get, durability, and + and solr cloud replica recovery. The log can grow as big as + uncommitted changes to the index, so use of a hard autoCommit + is recommended (see below). + "dir" - the target directory for transaction logs, defaults to the + solr data directory. + "numVersionBuckets" - sets the number of buckets used to keep + track of max version values when checking for re-ordered + updates; increase this value to reduce the cost of + synchronizing access to version buckets during high-volume + indexing, this requires 8 bytes (long) * numVersionBuckets + of heap space per Solr core. + --> + <updateLog> + <str name="dir">${solr.ulog.dir:}</str> + <int name="numVersionBuckets">${solr.ulog.numVersionBuckets:65536}</int> + </updateLog> + + <!-- AutoCommit + + Perform a hard commit automatically under certain conditions. + Instead of enabling autoCommit, consider using "commitWithin" + when adding documents. + + http://wiki.apache.org/solr/UpdateXmlMessages + + maxDocs - Maximum number of documents to add since the last + commit before automatically triggering a new commit. + + maxTime - Maximum amount of time in ms that is allowed to pass + since a document was added before automatically + triggering a new commit. + openSearcher - if false, the commit causes recent index changes + to be flushed to stable storage, but does not cause a new + searcher to be opened to make those changes visible. + + If the updateLog is enabled, then it's highly recommended to + have some sort of hard autoCommit to limit the log size. + --> + <autoCommit> + <maxTime>${solr.autoCommit.maxTime:15000}</maxTime> + <openSearcher>false</openSearcher> + </autoCommit> + + <!-- softAutoCommit is like autoCommit except it causes a + 'soft' commit which only ensures that changes are visible + but does not ensure that data is synced to disk. This is + faster and more near-realtime friendly than a hard commit. + --> + <autoSoftCommit> + <maxTime>${solr.autoSoftCommit.maxTime:-1}</maxTime> + </autoSoftCommit> + + <!-- Update Related Event Listeners + + Various IndexWriter related events can trigger Listeners to + take actions. + + postCommit - fired after every commit or optimize command + postOptimize - fired after every optimize command + --> + + </updateHandler> + + <!-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + Query section - these settings control query time things like caches + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ --> + <query> + <!-- Solr Internal Query Caches + + There are two implementations of cache available for Solr, + LRUCache, based on a synchronized LinkedHashMap, and + FastLRUCache, based on a ConcurrentHashMap. + + FastLRUCache has faster gets and slower puts in single + threaded operation and thus is generally faster than LRUCache + when the hit ratio of the cache is high (> 75%), and may be + faster under other scenarios on multi-cpu systems. + --> + + <!-- Filter Cache + + Cache used by SolrIndexSearcher for filters (DocSets), + unordered sets of *all* documents that match a query. When a + new searcher is opened, its caches may be prepopulated or + "autowarmed" using data from caches in the old searcher. + autowarmCount is the number of items to prepopulate. For + LRUCache, the autowarmed items will be the most recently + accessed items. + + Parameters: + class - the SolrCache implementation LRUCache or + (LRUCache or FastLRUCache) + size - the maximum number of entries in the cache + initialSize - the initial capacity (number of entries) of + the cache. (see java.util.HashMap) + autowarmCount - the number of entries to prepopulate from + and old cache. + maxRamMB - the maximum amount of RAM (in MB) that this cache is allowed + to occupy. Note that when this option is specified, the size + and initialSize parameters are ignored. + --> + <filterCache class="solr.FastLRUCache" + size="512" + initialSize="512" + autowarmCount="0"/> + + <!-- Query Result Cache + + Caches results of searches - ordered lists of document ids + (DocList) based on a query, a sort, and the range of documents requested. + Additional supported parameter by LRUCache: + maxRamMB - the maximum amount of RAM (in MB) that this cache is allowed + to occupy + --> + <queryResultCache class="solr.LRUCache" + size="512" + initialSize="512" + autowarmCount="0"/> + + <!-- Document Cache + + Caches Lucene Document objects (the stored fields for each + document). Since Lucene internal document ids are transient, + this cache will not be autowarmed. + --> + <documentCache class="solr.LRUCache" + size="512" + initialSize="512" + autowarmCount="0"/> + + <!-- custom cache currently used by block join --> + <cache name="perSegFilter" + class="solr.search.LRUCache" + size="10" + initialSize="0" + autowarmCount="10" + regenerator="solr.NoOpRegenerator" /> + + <!-- Lazy Field Loading + + If true, stored fields that are not requested will be loaded + lazily. This can result in a significant speed improvement + if the usual case is to not load all stored fields, + especially if the skipped fields are large compressed text + fields. + --> + <enableLazyFieldLoading>true</enableLazyFieldLoading> + + <!-- Result Window Size + + An optimization for use with the queryResultCache. When a search + is requested, a superset of the requested number of document ids + are collected. For example, if a search for a particular query + requests matching documents 10 through 19, and queryWindowSize is 50, + then documents 0 through 49 will be collected and cached. Any further + requests in that range can be satisfied via the cache. + --> + <queryResultWindowSize>20</queryResultWindowSize> + + <!-- Maximum number of documents to cache for any entry in the + queryResultCache. + --> + <queryResultMaxDocsCached>200</queryResultMaxDocsCached> + + <!-- Use Cold Searcher + + If a search request comes in and there is no current + registered searcher, then immediately register the still + warming searcher and use it. If "false" then all requests + will block until the first searcher is done warming. + --> + <useColdSearcher>false</useColdSearcher> + + </query> + + + <!-- Request Dispatcher + + This section contains instructions for how the SolrDispatchFilter + should behave when processing requests for this SolrCore. + + --> + <requestDispatcher> + <httpCaching never304="true" /> + </requestDispatcher> + + <!-- Request Handlers + + http://wiki.apache.org/solr/SolrRequestHandler + + Incoming queries will be dispatched to a specific handler by name + based on the path specified in the request. + + If a Request Handler is declared with startup="lazy", then it will + not be initialized until the first request that uses it. + + --> + <!-- SearchHandler + + http://wiki.apache.org/solr/SearchHandler + + For processing Search Queries, the primary Request Handler + provided with Solr is "SearchHandler" It delegates to a sequent + of SearchComponents (see below) and supports distributed + queries across multiple shards + --> + <requestHandler name="/select" class="solr.SearchHandler"> + <!-- default values for query parameters can be specified, these + will be overridden by parameters in the request + --> + <lst name="defaults"> + <str name="echoParams">explicit</str> + <int name="rows">10</int> + </lst> + </requestHandler> + + <initParams path="/update/**,/select"> + <lst name="defaults"> + <str name="df">_text_</str> + </lst> + </initParams> + + <!-- Response Writers + + http://wiki.apache.org/solr/QueryResponseWriter + + Request responses will be written using the writer specified by + the 'wt' request parameter matching the name of a registered + writer. + + The "default" writer is the default and will be used if 'wt' is + not specified in the request. + --> + <queryResponseWriter name="xml" + default="true" + class="solr.XMLResponseWriter" /> +</config> diff --git a/data/Dockerfiles/solr/solr-schema-7.7.0.xml b/data/Dockerfiles/solr/solr-schema-7.7.0.xml new file mode 100644 index 00000000..2c2e6343 --- /dev/null +++ b/data/Dockerfiles/solr/solr-schema-7.7.0.xml @@ -0,0 +1,49 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<schema name="dovecot-fts" version="2.0"> + <fieldType name="string" class="solr.StrField" omitNorms="true" sortMissingLast="true"/> + <fieldType name="long" class="solr.LongPointField" positionIncrementGap="0"/> + <fieldType name="boolean" class="solr.BoolField" sortMissingLast="true"/> + + <fieldType name="text" class="solr.TextField" autoGeneratePhraseQueries="true" positionIncrementGap="100"> + <analyzer type="index"> + <tokenizer class="solr.StandardTokenizerFactory"/> + <filter class="solr.EdgeNGramFilterFactory" minGramSize="3" maxGramSize="20"/> + <filter class="solr.StopFilterFactory" words="stopwords.txt" ignoreCase="true"/> + <filter class="solr.WordDelimiterGraphFilterFactory" catenateNumbers="1" generateNumberParts="1" splitOnCaseChange="1" generateWordParts="1" splitOnNumerics="1" catenateAll="1" catenateWords="1"/> + <filter class="solr.FlattenGraphFilterFactory"/> + <filter class="solr.LowerCaseFilterFactory"/> + <filter class="solr.KeywordMarkerFilterFactory" protected="protwords.txt"/> + <filter class="solr.PorterStemFilterFactory"/> + </analyzer> + <analyzer type="query"> + <tokenizer class="solr.StandardTokenizerFactory"/> + <filter class="solr.SynonymGraphFilterFactory" expand="true" ignoreCase="true" synonyms="synonyms.txt"/> + <filter class="solr.FlattenGraphFilterFactory"/> + <filter class="solr.StopFilterFactory" words="stopwords.txt" ignoreCase="true"/> + <filter class="solr.WordDelimiterGraphFilterFactory" catenateNumbers="1" generateNumberParts="1" splitOnCaseChange="1" generateWordParts="1" splitOnNumerics="1" catenateAll="1" catenateWords="1"/> + <filter class="solr.LowerCaseFilterFactory"/> + <filter class="solr.KeywordMarkerFilterFactory" protected="protwords.txt"/> + <filter class="solr.PorterStemFilterFactory"/> + </analyzer> + </fieldType> + + <field name="id" type="string" indexed="true" required="true" stored="true"/> + <field name="uid" type="long" indexed="true" required="true" stored="true"/> + <field name="box" type="string" indexed="true" required="true" stored="true"/> + <field name="user" type="string" indexed="true" required="true" stored="true"/> + + <field name="hdr" type="text" indexed="true" stored="false"/> + <field name="body" type="text" indexed="true" stored="false"/> + + <field name="from" type="text" indexed="true" stored="false"/> + <field name="to" type="text" indexed="true" stored="false"/> + <field name="cc" type="text" indexed="true" stored="false"/> + <field name="bcc" type="text" indexed="true" stored="false"/> + <field name="subject" type="text" indexed="true" stored="false"/> + + <!-- Used by Solr internally: --> + <field name="_version_" type="long" indexed="true" stored="true"/> + + <uniqueKey>id</uniqueKey> +</schema> diff --git a/data/Dockerfiles/unbound/Dockerfile b/data/Dockerfiles/unbound/Dockerfile index 7658a8f8..35c6fc0d 100644 --- a/data/Dockerfiles/unbound/Dockerfile +++ b/data/Dockerfiles/unbound/Dockerfile @@ -1,4 +1,4 @@ -FROM alpine:3.9 +FROM alpine:3.10 LABEL maintainer "Andre Peters <andre.peters@servercow.de>" diff --git a/data/Dockerfiles/watchdog/Dockerfile b/data/Dockerfiles/watchdog/Dockerfile index 7ab29b68..a884944b 100644 --- a/data/Dockerfiles/watchdog/Dockerfile +++ b/data/Dockerfiles/watchdog/Dockerfile @@ -1,4 +1,4 @@ -FROM alpine:3.9 +FROM alpine:3.10 LABEL maintainer "André Peters <andre.peters@servercow.de>" # Installation @@ -7,11 +7,13 @@ RUN apk add --update \ nagios-plugins-tcp \ nagios-plugins-http \ nagios-plugins-ping \ + mariadb-client \ curl \ bash \ coreutils \ jq \ fcgi \ + openssl \ nagios-plugins-mysql \ nagios-plugins-dns \ nagios-plugins-disk \ @@ -26,11 +28,13 @@ RUN apk add --update \ perl-term-readkey \ tini \ tzdata \ + whois \ && curl https://raw.githubusercontent.com/mludvig/smtp-cli/v3.9/smtp-cli -o /smtp-cli \ && chmod +x smtp-cli COPY watchdog.sh /watchdog.sh -ENTRYPOINT ["/sbin/tini", "-g", "--"] +#ENTRYPOINT ["/sbin/tini", "-g", "--"] # Less verbose + CMD /watchdog.sh 2> /dev/null diff --git a/data/Dockerfiles/watchdog/watchdog.sh b/data/Dockerfiles/watchdog/watchdog.sh index ed9b568d..932f8ef5 100755 --- a/data/Dockerfiles/watchdog/watchdog.sh +++ b/data/Dockerfiles/watchdog/watchdog.sh @@ -5,6 +5,8 @@ trap "kill 0" EXIT # Prepare BACKGROUND_TASKS=() +echo "Waiting for containers to settle..." +sleep 10 if [[ "${USE_WATCHDOG}" =~ ^([nN][oO]|[nN])+$ ]]; then echo -e "$(date) - USE_WATCHDOG=n, skipping watchdog..." @@ -17,7 +19,28 @@ if [[ ! -p /tmp/com_pipe ]]; then mkfifo /tmp/com_pipe fi +# Wait for containers +while ! mysqladmin status --socket=/var/run/mysqld/mysqld.sock -u${DBUSER} -p${DBPASS} --silent; do + echo "Waiting for SQL..." + sleep 2 +done + +until [[ $(redis-cli -h redis-mailcow PING) == "PONG" ]]; do + echo "Waiting for Redis..." + sleep 2 +done + +redis-cli -h redis-mailcow DEL F2B_RES > /dev/null + # Common functions +array_diff() { + # https://stackoverflow.com/questions/2312762, Alex Offshore + eval local ARR1=\(\"\${$2[@]}\"\) + eval local ARR2=\(\"\${$3[@]}\"\) + local IFS=$'\n' + mapfile -t $1 < <(comm -23 <(echo "${ARR1[*]}" | sort) <(echo "${ARR2[*]}" | sort)) +} + progress() { SERVICE=${1} TOTAL=${2} @@ -37,7 +60,7 @@ progress() { log_msg() { if [[ ${2} != "no_redis" ]]; then redis-cli -h redis LPUSH WATCHDOG_LOG "{\"time\":\"$(date +%s)\",\"message\":\"$(printf '%s' "${1}" | \ - tr '%&;$"_[]{}-\r\n' ' ')\"}" > /dev/null + tr '\r\n%&;$"_[]{}-' ' ')\"}" > /dev/null fi echo $(date) $(printf '%s\n' "${1}") } @@ -46,6 +69,13 @@ function mail_error() { [[ -z ${1} ]] && return 1 [[ -z ${2} ]] && BODY="Service was restarted on $(date), please check your mailcow installation." || BODY="$(date) - ${2}" WATCHDOG_NOTIFY_EMAIL=$(echo "${WATCHDOG_NOTIFY_EMAIL}" | sed 's/"//;s|"$||') + # Some exceptions for subject and body formats + if [[ ${1} == "fail2ban" ]]; then + SUBJECT="${BODY}" + BODY="Please see netfilter-mailcow for more details and triggered rules." + else + SUBJECT="Watchdog ALERT: ${1}" + fi IFS=',' read -r -a MAIL_RCPTS <<< "${WATCHDOG_NOTIFY_EMAIL}" for rcpt in "${MAIL_RCPTS[@]}"; do RCPT_DOMAIN= @@ -56,15 +86,15 @@ function mail_error() { log_msg "Cannot determine MX for ${rcpt}, skipping email notification..." return 1 fi - [ -f "/tmp/${1}" ] && ATTACH="--attach /tmp/${1}@text/plain" || ATTACH= - ./smtp-cli --missing-modules-ok \ - --subject="Watchdog: ${1} hit the error rate limit" \ + [ -f "/tmp/${1}" ] && BODY="/tmp/${1}" + timeout 10s ./smtp-cli --missing-modules-ok \ + --charset=UTF-8 \ + --subject="${SUBJECT}" \ --body-plain="${BODY}" \ --to=${rcpt} \ --from="watchdog@${MAILCOW_HOSTNAME}" \ --server="${RCPT_MX}" \ - --hello-host=${MAILCOW_HOSTNAME} \ - ${ATTACH} + --hello-host=${MAILCOW_HOSTNAME} log_msg "Sent notification email to ${rcpt}" done } @@ -111,11 +141,11 @@ get_container_ip() { nginx_checks() { err_count=0 diff_c=0 - THRESHOLD=16 + THRESHOLD=5 # Reduce error count by 2 after restarting an unhealthy container trap "[ ${err_count} -gt 1 ] && err_count=$(( ${err_count} - 2 ))" USR1 while [ ${err_count} -lt ${THRESHOLD} ]; do - cat /dev/null > /tmp/nginx-mailcow + touch /tmp/nginx-mailcow; echo "$(tail -50 /tmp/nginx-mailcow)" > /tmp/nginx-mailcow host_ip=$(get_container_ip nginx-mailcow) err_c_cur=${err_count} /usr/lib/nagios/plugins/check_http -4 -H ${host_ip} -u / -p 8081 2>> /tmp/nginx-mailcow 1>&2; err_count=$(( ${err_count} + $? )) @@ -127,7 +157,7 @@ nginx_checks() { sleep 1 else diff_c=0 - sleep $(( ( RANDOM % 30 ) + 10 )) + sleep $(( ( RANDOM % 60 ) + 20 )) fi done return 1 @@ -136,11 +166,11 @@ nginx_checks() { unbound_checks() { err_count=0 diff_c=0 - THRESHOLD=8 + THRESHOLD=5 # Reduce error count by 2 after restarting an unhealthy container trap "[ ${err_count} -gt 1 ] && err_count=$(( ${err_count} - 2 ))" USR1 while [ ${err_count} -lt ${THRESHOLD} ]; do - cat /dev/null > /tmp/unbound-mailcow + touch /tmp/unbound-mailcow; echo "$(tail -50 /tmp/unbound-mailcow)" > /tmp/unbound-mailcow host_ip=$(get_container_ip unbound-mailcow) err_c_cur=${err_count} /usr/lib/nagios/plugins/check_dns -s ${host_ip} -H stackoverflow.com 2>> /tmp/unbound-mailcow 1>&2; err_count=$(( ${err_count} + $? )) @@ -159,7 +189,32 @@ unbound_checks() { sleep 1 else diff_c=0 - sleep $(( ( RANDOM % 30 ) + 10 )) + sleep $(( ( RANDOM % 60 ) + 20 )) + fi + done + return 1 +} + +redis_checks() { + err_count=0 + diff_c=0 + THRESHOLD=5 + # Reduce error count by 2 after restarting an unhealthy container + trap "[ ${err_count} -gt 1 ] && err_count=$(( ${err_count} - 2 ))" USR1 + while [ ${err_count} -lt ${THRESHOLD} ]; do + touch /tmp/redis-mailcow; echo "$(tail -50 /tmp/redis-mailcow)" > /tmp/redis-mailcow + host_ip=$(get_container_ip redis-mailcow) + err_c_cur=${err_count} + /usr/lib/nagios/plugins/check_tcp -4 -H redis-mailcow -p 6379 -E -s "PING\n" -q "QUIT" -e "PONG" 2>> /tmp/redis-mailcow 1>&2; err_count=$(( ${err_count} + $? )) + [ ${err_c_cur} -eq ${err_count} ] && [ ! $((${err_count} - 1)) -lt 0 ] && err_count=$((${err_count} - 1)) diff_c=1 + [ ${err_c_cur} -ne ${err_count} ] && diff_c=$(( ${err_c_cur} - ${err_count} )) + progress "Redis" ${THRESHOLD} $(( ${THRESHOLD} - ${err_count} )) ${diff_c} + if [[ $? == 10 ]]; then + diff_c=0 + sleep 1 + else + diff_c=0 + sleep $(( ( RANDOM % 60 ) + 20 )) fi done return 1 @@ -168,11 +223,11 @@ unbound_checks() { mysql_checks() { err_count=0 diff_c=0 - THRESHOLD=12 + THRESHOLD=5 # Reduce error count by 2 after restarting an unhealthy container trap "[ ${err_count} -gt 1 ] && err_count=$(( ${err_count} - 2 ))" USR1 while [ ${err_count} -lt ${THRESHOLD} ]; do - cat /dev/null > /tmp/mysql-mailcow + touch /tmp/mysql-mailcow; echo "$(tail -50 /tmp/mysql-mailcow)" > /tmp/mysql-mailcow host_ip=$(get_container_ip mysql-mailcow) err_c_cur=${err_count} /usr/lib/nagios/plugins/check_mysql -s /var/run/mysqld/mysqld.sock -u ${DBUSER} -p ${DBPASS} -d ${DBNAME} 2>> /tmp/mysql-mailcow 1>&2; err_count=$(( ${err_count} + $? )) @@ -185,7 +240,7 @@ mysql_checks() { sleep 1 else diff_c=0 - sleep $(( ( RANDOM % 30 ) + 10 )) + sleep $(( ( RANDOM % 60 ) + 20 )) fi done return 1 @@ -194,11 +249,11 @@ mysql_checks() { sogo_checks() { err_count=0 diff_c=0 - THRESHOLD=10 + THRESHOLD=5 # Reduce error count by 2 after restarting an unhealthy container trap "[ ${err_count} -gt 1 ] && err_count=$(( ${err_count} - 2 ))" USR1 while [ ${err_count} -lt ${THRESHOLD} ]; do - cat /dev/null > /tmp/sogo-mailcow + touch /tmp/sogo-mailcow; echo "$(tail -50 /tmp/sogo-mailcow)" > /tmp/sogo-mailcow host_ip=$(get_container_ip sogo-mailcow) err_c_cur=${err_count} /usr/lib/nagios/plugins/check_http -4 -H ${host_ip} -u /SOGo.index/ -p 20000 -R "SOGo\.MainUI" 2>> /tmp/sogo-mailcow 1>&2; err_count=$(( ${err_count} + $? )) @@ -210,7 +265,7 @@ sogo_checks() { sleep 1 else diff_c=0 - sleep $(( ( RANDOM % 30 ) + 10 )) + sleep $(( ( RANDOM % 60 ) + 20 )) fi done return 1 @@ -223,10 +278,10 @@ postfix_checks() { # Reduce error count by 2 after restarting an unhealthy container trap "[ ${err_count} -gt 1 ] && err_count=$(( ${err_count} - 2 ))" USR1 while [ ${err_count} -lt ${THRESHOLD} ]; do - cat /dev/null > /tmp/postfix-mailcow + touch /tmp/postfix-mailcow; echo "$(tail -50 /tmp/postfix-mailcow)" > /tmp/postfix-mailcow host_ip=$(get_container_ip postfix-mailcow) err_c_cur=${err_count} - /usr/lib/nagios/plugins/check_smtp -4 -H ${host_ip} -p 589 -f "watchdog@invalid" -C "RCPT TO:null@localhost" -C DATA -C . -R 250 2>> /tmp/postfix-mailcow 1>&2; err_count=$(( ${err_count} + $? )) + /usr/lib/nagios/plugins/check_smtp -4 -H ${host_ip} -p 589 -f "watchdog@invalid" -C "RCPT TO:watchdog@localhost" -C DATA -C . -R 250 2>> /tmp/postfix-mailcow 1>&2; err_count=$(( ${err_count} + $? )) /usr/lib/nagios/plugins/check_smtp -4 -H ${host_ip} -p 589 -S 2>> /tmp/postfix-mailcow 1>&2; err_count=$(( ${err_count} + $? )) [ ${err_c_cur} -eq ${err_count} ] && [ ! $((${err_count} - 1)) -lt 0 ] && err_count=$((${err_count} - 1)) diff_c=1 [ ${err_c_cur} -ne ${err_count} ] && diff_c=$(( ${err_c_cur} - ${err_count} )) @@ -236,7 +291,7 @@ postfix_checks() { sleep 1 else diff_c=0 - sleep $(( ( RANDOM % 30 ) + 10 )) + sleep $(( ( RANDOM % 60 ) + 20 )) fi done return 1 @@ -245,11 +300,11 @@ postfix_checks() { clamd_checks() { err_count=0 diff_c=0 - THRESHOLD=5 + THRESHOLD=15 # Reduce error count by 2 after restarting an unhealthy container trap "[ ${err_count} -gt 1 ] && err_count=$(( ${err_count} - 2 ))" USR1 while [ ${err_count} -lt ${THRESHOLD} ]; do - cat /dev/null > /tmp/clamd-mailcow + touch /tmp/clamd-mailcow; echo "$(tail -50 /tmp/clamd-mailcow)" > /tmp/clamd-mailcow host_ip=$(get_container_ip clamd-mailcow) err_c_cur=${err_count} /usr/lib/nagios/plugins/check_clamd -4 -H ${host_ip} 2>> /tmp/clamd-mailcow 1>&2; err_count=$(( ${err_count} + $? )) @@ -261,7 +316,7 @@ clamd_checks() { sleep 1 else diff_c=0 - sleep $(( ( RANDOM % 30 ) + 10 )) + sleep $(( ( RANDOM % 120 ) + 20 )) fi done return 1 @@ -270,11 +325,11 @@ clamd_checks() { dovecot_checks() { err_count=0 diff_c=0 - THRESHOLD=20 + THRESHOLD=12 # Reduce error count by 2 after restarting an unhealthy container trap "[ ${err_count} -gt 1 ] && err_count=$(( ${err_count} - 2 ))" USR1 while [ ${err_count} -lt ${THRESHOLD} ]; do - cat /dev/null > /tmp/dovecot-mailcow + touch /tmp/dovecot-mailcow; echo "$(tail -50 /tmp/dovecot-mailcow)" > /tmp/dovecot-mailcow host_ip=$(get_container_ip dovecot-mailcow) err_c_cur=${err_count} /usr/lib/nagios/plugins/check_smtp -4 -H ${host_ip} -p 24 -f "watchdog@invalid" -C "RCPT TO:<watchdog@invalid>" -L -R "User doesn't exist" 2>> /tmp/dovecot-mailcow 1>&2; err_count=$(( ${err_count} + $? )) @@ -290,7 +345,7 @@ dovecot_checks() { sleep 1 else diff_c=0 - sleep $(( ( RANDOM % 30 ) + 10 )) + sleep $(( ( RANDOM % 60 ) + 20 )) fi done return 1 @@ -303,7 +358,7 @@ phpfpm_checks() { # Reduce error count by 2 after restarting an unhealthy container trap "[ ${err_count} -gt 1 ] && err_count=$(( ${err_count} - 2 ))" USR1 while [ ${err_count} -lt ${THRESHOLD} ]; do - cat /dev/null > /tmp/php-fpm-mailcow + touch /tmp/php-fpm-mailcow; echo "$(tail -50 /tmp/php-fpm-mailcow)" > /tmp/php-fpm-mailcow host_ip=$(get_container_ip php-fpm-mailcow) err_c_cur=${err_count} /usr/lib/nagios/plugins/check_tcp -H ${host_ip} -p 9001 2>> /tmp/php-fpm-mailcow 1>&2; err_count=$(( ${err_count} + $? )) @@ -316,7 +371,7 @@ phpfpm_checks() { sleep 1 else diff_c=0 - sleep $(( ( RANDOM % 30 ) + 10 )) + sleep $(( ( RANDOM % 60 ) + 20 )) fi done return 1 @@ -344,7 +399,73 @@ ratelimit_checks() { sleep 1 else diff_c=0 - sleep $(( ( RANDOM % 30 ) + 10 )) + sleep $(( ( RANDOM % 60 ) + 20 )) + fi + done + return 1 +} + +fail2ban_checks() { + err_count=0 + diff_c=0 + THRESHOLD=1 + F2B_LOG_STATUS=($(redis-cli -h redis-mailcow --raw HKEYS F2B_ACTIVE_BANS)) + F2B_RES= + # Reduce error count by 2 after restarting an unhealthy container + trap "[ ${err_count} -gt 1 ] && err_count=$(( ${err_count} - 2 ))" USR1 + while [ ${err_count} -lt ${THRESHOLD} ]; do + err_c_cur=${err_count} + F2B_LOG_STATUS_PREV=(${F2B_LOG_STATUS[@]}) + F2B_LOG_STATUS=($(redis-cli -h redis-mailcow --raw HKEYS F2B_ACTIVE_BANS)) + array_diff F2B_RES F2B_LOG_STATUS F2B_LOG_STATUS_PREV + if [[ ! -z "${F2B_RES}" ]]; then + err_count=$(( ${err_count} + 1 )) + echo -n "${F2B_RES[@]}" | tr -cd "[a-fA-F0-9.:/] " | timeout 3s redis-cli -x -h redis-mailcow SET F2B_RES > /dev/null + if [ $? -ne 0 ]; then + redis-cli -x -h redis-mailcow DEL F2B_RES + fi + fi + [ ${err_c_cur} -eq ${err_count} ] && [ ! $((${err_count} - 1)) -lt 0 ] && err_count=$((${err_count} - 1)) diff_c=1 + [ ${err_c_cur} -ne ${err_count} ] && diff_c=$(( ${err_c_cur} - ${err_count} )) + progress "Fail2ban" ${THRESHOLD} $(( ${THRESHOLD} - ${err_count} )) ${diff_c} + if [[ $? == 10 ]]; then + diff_c=0 + sleep 1 + else + diff_c=0 + sleep $(( ( RANDOM % 60 ) + 20 )) + fi + done + return 1 +} + +acme_checks() { + err_count=0 + diff_c=0 + THRESHOLD=1 + ACME_LOG_STATUS=$(redis-cli -h redis GET ACME_FAIL_TIME) + if [[ -z "${ACME_LOG_STATUS}" ]]; then + redis-cli -h redis SET ACME_FAIL_TIME 0 + ACME_LOG_STATUS=0 + fi + # Reduce error count by 2 after restarting an unhealthy container + trap "[ ${err_count} -gt 1 ] && err_count=$(( ${err_count} - 2 ))" USR1 + while [ ${err_count} -lt ${THRESHOLD} ]; do + err_c_cur=${err_count} + ACME_LOG_STATUS_PREV=${ACME_LOG_STATUS} + ACME_LOG_STATUS=$(redis-cli -h redis GET ACME_FAIL_TIME) + if [[ ${ACME_LOG_STATUS_PREV} != ${ACME_LOG_STATUS} ]]; then + err_count=$(( ${err_count} + 1 )) + fi + [ ${err_c_cur} -eq ${err_count} ] && [ ! $((${err_count} - 1)) -lt 0 ] && err_count=$((${err_count} - 1)) diff_c=1 + [ ${err_c_cur} -ne ${err_count} ] && diff_c=$(( ${err_c_cur} - ${err_count} )) + progress "ACME" ${THRESHOLD} $(( ${THRESHOLD} - ${err_count} )) ${diff_c} + if [[ $? == 10 ]]; then + diff_c=0 + sleep 1 + else + diff_c=0 + sleep $(( ( RANDOM % 60 ) + 20 )) fi done return 1 @@ -358,10 +479,11 @@ ipv6nat_checks() { trap "[ ${err_count} -gt 1 ] && err_count=$(( ${err_count} - 2 ))" USR1 while [ ${err_count} -lt ${THRESHOLD} ]; do err_c_cur=${err_count} - IPV6NAT_CONTAINER_ID=$(curl --silent --insecure https://dockerapi/containers/json | jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], id: .Id}" | jq -rc "select( .name | tostring | contains(\"ipv6nat-mailcow\")) | .id") + CONTAINERS=$(curl --silent --insecure https://dockerapi/containers/json) + IPV6NAT_CONTAINER_ID=$(echo ${CONTAINERS} | jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], id: .Id}" | jq -rc "select( .name | tostring | contains(\"ipv6nat-mailcow\")) | .id") if [[ ! -z ${IPV6NAT_CONTAINER_ID} ]]; then - LATEST_STARTED="$(curl --silent --insecure https://dockerapi/containers/json | jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], StartedAt: .State.StartedAt}" | jq -rc "select( .name | tostring | contains(\"ipv6nat-mailcow\") | not)" | jq -rc .StartedAt | xargs -n1 date +%s -d | sort | tail -n1)" - LATEST_IPV6NAT="$(curl --silent --insecure https://dockerapi/containers/json | jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], StartedAt: .State.StartedAt}" | jq -rc "select( .name | tostring | contains(\"ipv6nat-mailcow\"))" | jq -rc .StartedAt | xargs -n1 date +%s -d | sort | tail -n1)" + LATEST_STARTED="$(echo ${CONTAINERS} | jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], StartedAt: .State.StartedAt}" | jq -rc "select( .name | tostring | contains(\"ipv6nat-mailcow\") | not)" | jq -rc .StartedAt | xargs -n1 date +%s -d | sort | tail -n1)" + LATEST_IPV6NAT="$(echo ${CONTAINERS} | jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], StartedAt: .State.StartedAt}" | jq -rc "select( .name | tostring | contains(\"ipv6nat-mailcow\"))" | jq -rc .StartedAt | xargs -n1 date +%s -d | sort | tail -n1)" DIFFERENCE_START_TIME=$(expr ${LATEST_IPV6NAT} - ${LATEST_STARTED} 2>/dev/null) if [[ "${DIFFERENCE_START_TIME}" -lt 30 ]]; then err_count=$(( ${err_count} + 1 )) @@ -372,15 +494,16 @@ ipv6nat_checks() { progress "IPv6 NAT" ${THRESHOLD} $(( ${THRESHOLD} - ${err_count} )) ${diff_c} if [[ $? == 10 ]]; then diff_c=0 - sleep 1 + sleep 30 else diff_c=0 - sleep 3600 + sleep 300 fi done return 1 } + rspamd_checks() { err_count=0 diff_c=0 @@ -388,15 +511,14 @@ rspamd_checks() { # Reduce error count by 2 after restarting an unhealthy container trap "[ ${err_count} -gt 1 ] && err_count=$(( ${err_count} - 2 ))" USR1 while [ ${err_count} -lt ${THRESHOLD} ]; do - cat /dev/null > /tmp/rspamd-mailcow + touch /tmp/rspamd-mailcow; echo "$(tail -50 /tmp/rspamd-mailcow)" > /tmp/rspamd-mailcow host_ip=$(get_container_ip rspamd-mailcow) err_c_cur=${err_count} - SCORE=$(/usr/bin/curl -s --data-binary @- --unix-socket /var/lib/rspamd/rspamd.sock http://rspamd/scan -d ' -To: null@localhost + SCORE=$(echo 'To: null@localhost From: watchdog@localhost Empty -' | jq -rc .required_score) +' | usr/bin/curl -s --data-binary @- --unix-socket /var/lib/rspamd/rspamd.sock http://rspamd/scan | jq -rc .required_score) if [[ ${SCORE} != "9999" ]]; then echo "Rspamd settings check failed" 2>> /tmp/rspamd-mailcow 1>&2 err_count=$(( ${err_count} + 1)) @@ -406,13 +528,49 @@ Empty [ ${err_c_cur} -eq ${err_count} ] && [ ! $((${err_count} - 1)) -lt 0 ] && err_count=$((${err_count} - 1)) diff_c=1 [ ${err_c_cur} -ne ${err_count} ] && diff_c=$(( ${err_c_cur} - ${err_count} )) progress "Rspamd" ${THRESHOLD} $(( ${THRESHOLD} - ${err_count} )) ${diff_c} - diff_c=0 - sleep $(( ( RANDOM % 30 ) + 10 )) + if [[ $? == 10 ]]; then + diff_c=0 + sleep 1 + else + diff_c=0 + sleep $(( ( RANDOM % 60 ) + 20 )) + fi done return 1 } +olefy_checks() { + err_count=0 + diff_c=0 + THRESHOLD=5 + # Reduce error count by 2 after restarting an unhealthy container + trap "[ ${err_count} -gt 1 ] && err_count=$(( ${err_count} - 2 ))" USR1 + while [ ${err_count} -lt ${THRESHOLD} ]; do + touch /tmp/olefy-mailcow; echo "$(tail -50 /tmp/olefy-mailcow)" > /tmp/olefy-mailcow + host_ip=$(get_container_ip olefy-mailcow) + err_c_cur=${err_count} + /usr/lib/nagios/plugins/check_tcp -4 -H ${host_ip} -p 10055 2>> /tmp/olefy-mailcow 1>&2; err_count=$(( ${err_count} + $? )) + [ ${err_c_cur} -eq ${err_count} ] && [ ! $((${err_count} - 1)) -lt 0 ] && err_count=$((${err_count} - 1)) diff_c=1 + [ ${err_c_cur} -ne ${err_count} ] && diff_c=$(( ${err_c_cur} - ${err_count} )) + progress "Olefy" ${THRESHOLD} $(( ${THRESHOLD} - ${err_count} )) ${diff_c} + if [[ $? == 10 ]]; then + diff_c=0 + sleep 1 + else + diff_c=0 + sleep $(( ( RANDOM % 60 ) + 20 )) + fi + done + return 1 +} + +# Notify about start +if [[ ! -z ${WATCHDOG_NOTIFY_EMAIL} ]]; then + mail_error "watchdog-mailcow" "Watchdog started monitoring mailcow." +fi + # Create watchdog agents + ( while true; do if ! nginx_checks; then @@ -421,7 +579,9 @@ while true; do fi done ) & -BACKGROUND_TASKS+=($!) +PID=$! +echo "Spawned nginx_checks with PID ${PID}" +BACKGROUND_TASKS+=(${PID}) ( while true; do @@ -431,7 +591,21 @@ while true; do fi done ) & -BACKGROUND_TASKS+=($!) +PID=$! +echo "Spawned mysql_checks with PID ${PID}" +BACKGROUND_TASKS+=(${PID}) + +( +while true; do + if ! redis_checks; then + log_msg "Redis hit error limit" + echo redis-mailcow > /tmp/com_pipe + fi +done +) & +PID=$! +echo "Spawned redis_checks with PID ${PID}" +BACKGROUND_TASKS+=(${PID}) ( while true; do @@ -441,7 +615,9 @@ while true; do fi done ) & -BACKGROUND_TASKS+=($!) +PID=$! +echo "Spawned phpfpm_checks with PID ${PID}" +BACKGROUND_TASKS+=(${PID}) ( while true; do @@ -451,7 +627,9 @@ while true; do fi done ) & -BACKGROUND_TASKS+=($!) +PID=$! +echo "Spawned sogo_checks with PID ${PID}" +BACKGROUND_TASKS+=(${PID}) if [ ${CHECK_UNBOUND} -eq 1 ]; then ( @@ -462,7 +640,9 @@ while true; do fi done ) & -BACKGROUND_TASKS+=($!) +PID=$! +echo "Spawned unbound_checks with PID ${PID}" +BACKGROUND_TASKS+=(${PID}) fi if [[ "${SKIP_CLAMD}" =~ ^([nN][oO]|[nN])+$ ]]; then @@ -474,7 +654,9 @@ while true; do fi done ) & -BACKGROUND_TASKS+=($!) +PID=$! +echo "Spawned clamd_checks with PID ${PID}" +BACKGROUND_TASKS+=(${PID}) fi ( @@ -485,7 +667,9 @@ while true; do fi done ) & -BACKGROUND_TASKS+=($!) +PID=$! +echo "Spawned postfix_checks with PID ${PID}" +BACKGROUND_TASKS+=(${PID}) ( while true; do @@ -495,7 +679,9 @@ while true; do fi done ) & -BACKGROUND_TASKS+=($!) +PID=$! +echo "Spawned dovecot_checks with PID ${PID}" +BACKGROUND_TASKS+=(${PID}) ( while true; do @@ -505,7 +691,9 @@ while true; do fi done ) & -BACKGROUND_TASKS+=($!) +PID=$! +echo "Spawned rspamd_checks with PID ${PID}" +BACKGROUND_TASKS+=(${PID}) ( while true; do @@ -515,7 +703,45 @@ while true; do fi done ) & -BACKGROUND_TASKS+=($!) +PID=$! +echo "Spawned ratelimit_checks with PID ${PID}" +BACKGROUND_TASKS+=(${PID}) + +( +while true; do + if ! fail2ban_checks; then + log_msg "Fail2ban hit error limit" + echo fail2ban > /tmp/com_pipe + fi +done +) & +PID=$! +echo "Spawned fail2ban_checks with PID ${PID}" +BACKGROUND_TASKS+=(${PID}) + +#( +#while true; do +# if ! olefy_checks; then +# log_msg "Olefy hit error limit" +# echo olefy-mailcow > /tmp/com_pipe +# fi +#done +#) & +#PID=$! +#echo "Spawned olefy_checks with PID ${PID}" +#BACKGROUND_TASKS+=(${PID}) + +( +while true; do + if ! acme_checks; then + log_msg "ACME client hit error limit" + echo acme-mailcow > /tmp/com_pipe + fi +done +) & +PID=$! +echo "Spawned acme_checks with PID ${PID}" +BACKGROUND_TASKS+=(${PID}) ( while true; do @@ -525,7 +751,9 @@ while true; do fi done ) & -BACKGROUND_TASKS+=($!) +PID=$! +echo "Spawned ipv6nat_checks with PID ${PID}" +BACKGROUND_TASKS+=(${PID}) # Monitor watchdog agents, stop script when agents fails and wait for respawn by Docker (restart:always:n) ( @@ -556,25 +784,43 @@ while true; do done ) & -# Restart container when threshold limit reached +# Actions when threshold limit is reached while true; do CONTAINER_ID= HAS_INITDB= read com_pipe_answer </tmp/com_pipe + if [ -s "/tmp/${com_pipe_answer}" ]; then + cat "/tmp/${com_pipe_answer}" + fi if [[ ${com_pipe_answer} == "ratelimit" ]]; then log_msg "At least one ratelimit was applied" - [[ ! -z ${WATCHDOG_NOTIFY_EMAIL} ]] && mail_error "${com_pipe_answer}" "No further information available." - elif [[ ${com_pipe_answer} =~ .+-mailcow ]] || [[ ${com_pipe_answer} == "ipv6nat-mailcow" ]]; then + [[ ! -z ${WATCHDOG_NOTIFY_EMAIL} ]] && mail_error "${com_pipe_answer}" "Please see mailcow UI logs for further information." + elif [[ ${com_pipe_answer} == "acme-mailcow" ]]; then + log_msg "acme-mailcow did not complete successfully" + [[ ! -z ${WATCHDOG_NOTIFY_EMAIL} ]] && mail_error "${com_pipe_answer}" "Please check acme-mailcow for further information." + elif [[ ${com_pipe_answer} == "fail2ban" ]]; then + F2B_RES=($(timeout 4s redis-cli -h redis-mailcow --raw GET F2B_RES 2> /dev/null)) + if [[ ! -z "${F2B_RES}" ]]; then + redis-cli -h redis-mailcow DEL F2B_RES > /dev/null + host= + for host in "${F2B_RES[@]}"; do + log_msg "Banned ${host}" + rm /tmp/fail2ban 2> /dev/null + timeout 2s whois "${host}" > /tmp/fail2ban + [[ ! -z ${WATCHDOG_NOTIFY_EMAIL} ]] && [[ ${WATCHDOG_NOTIFY_BAN} =~ ^([yY][eE][sS]|[yY])+$ ]] && mail_error "${com_pipe_answer}" "IP ban: ${host}" + done + fi + elif [[ ${com_pipe_answer} =~ .+-mailcow ]]; then kill -STOP ${BACKGROUND_TASKS[*]} - sleep 3 + sleep 10 CONTAINER_ID=$(curl --silent --insecure https://dockerapi/containers/json | jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], id: .Id}" | jq -rc "select( .name | tostring | contains(\"${com_pipe_answer}\")) | .id") if [[ ! -z ${CONTAINER_ID} ]]; then if [[ "${com_pipe_answer}" == "php-fpm-mailcow" ]]; then HAS_INITDB=$(curl --silent --insecure -XPOST https://dockerapi/containers/${CONTAINER_ID}/top | jq '.msg.Processes[] | contains(["php -c /usr/local/etc/php -f /web/inc/init_db.inc.php"])' | grep true) fi S_RUNNING=$(($(date +%s) - $(curl --silent --insecure https://dockerapi/containers/${CONTAINER_ID}/json | jq .State.StartedAt | xargs -n1 date +%s -d))) - if [ ${S_RUNNING} -lt 120 ]; then - log_msg "Container is running for less than 120 seconds, skipping action..." + if [ ${S_RUNNING} -lt 360 ]; then + log_msg "Container is running for less than 360 seconds, skipping action..." elif [[ ! -z ${HAS_INITDB} ]]; then log_msg "Database is being initialized by php-fpm-mailcow, not restarting but delaying checks for a minute..." sleep 60 @@ -589,6 +835,7 @@ while true; do fi fi kill -CONT ${BACKGROUND_TASKS[*]} + sleep 1 kill -USR1 ${BACKGROUND_TASKS[*]} fi done diff --git a/data/assets/nextcloud/nextcloud.conf b/data/assets/nextcloud/nextcloud.conf index cf90a32b..243cc406 100644 --- a/data/assets/nextcloud/nextcloud.conf +++ b/data/assets/nextcloud/nextcloud.conf @@ -75,7 +75,7 @@ server { deny all; } - location ~ ^/(?:index|remote|public|cron|core/ajax/update|status|ocs/v[12]|updater/.+|ocs-provider/.+)\.php(?:$|/) { + location ~ ^/(?:index|remote|public|cron|core/ajax/update|status|ocs/v[12]|updater/.+|oc[ms]-provider/.+)\.php(?:$|/) { fastcgi_split_path_info ^(.+\.php)(/.*)$; include fastcgi_params; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; @@ -90,12 +90,12 @@ server { fastcgi_read_timeout 1200; } - location ~ ^/(?:updater|ocs-provider)(?:$|/) { + location ~ ^/(?:updater|oc[ms]-provider)(?:$|/) { try_files $uri/ =404; index index.php; } - location ~ \.(?:css|js|woff|svg|gif)$ { + location ~ \.(?:css|js|woff2?|svg|gif)$ { try_files $uri /index.php$uri$is_args$args; add_header Cache-Control "public, max-age=15778463"; add_header X-Content-Type-Options nosniff; diff --git a/data/assets/nextcloud/occ b/data/assets/nextcloud/occ index 2ae08001..5113ac01 100755 --- a/data/assets/nextcloud/occ +++ b/data/assets/nextcloud/occ @@ -1,2 +1,2 @@ #!/bin/bash -docker exec -it -u www-data $(docker ps -f name=php-fpm-mailcow -q) /web/nextcloud/occ ${@} +docker exec -it -u www-data $(docker ps -f name=php-fpm-mailcow -q) php /web/nextcloud/occ ${@} diff --git a/data/assets/nextcloud/site.nextcloud.custom b/data/assets/nextcloud/site.nextcloud.custom deleted file mode 100644 index 6ac29902..00000000 --- a/data/assets/nextcloud/site.nextcloud.custom +++ /dev/null @@ -1,44 +0,0 @@ - location ^~ /nextcloud { - location /nextcloud { - rewrite ^ /nextcloud/index.php$uri; - } - location ~ ^/nextcloud/(?:build|tests|config|lib|3rdparty|templates|data)/ { - deny all; - } - location ~ ^/nextcloud/(?:\.|autotest|occ|issue|indie|db_|console) { - deny all; - } - location ~ ^/nextcloud/(?:index|remote|public|cron|core/ajax/update|status|ocs/v[12]|updater/.+|ocs-provider/.+)\.php(?:$|/) { - fastcgi_split_path_info ^(.+\.php)(/.*)$; - include fastcgi_params; - fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; - fastcgi_param PATH_INFO $fastcgi_path_info; - fastcgi_param HTTPS on; - fastcgi_param modHeadersAvailable true; - fastcgi_param front_controller_active true; - fastcgi_pass phpfpm:9002; - fastcgi_intercept_errors on; - fastcgi_request_buffering off; - client_max_body_size 0; - fastcgi_read_timeout 1200; - } - location ~ ^/nextcloud/(?:updater|ocs-provider)(?:$|/) { - try_files $uri/ =404; - index index.php; - } - location ~ \.(?:css|js|woff|svg|gif)$ { - try_files $uri /nextcloud/index.php$uri$is_args$args; - add_header Cache-Control "public, max-age=15778463"; - add_header X-Content-Type-Options nosniff; - add_header X-XSS-Protection "1; mode=block"; - add_header X-Robots-Tag none; - add_header X-Download-Options noopen; - add_header X-Permitted-Cross-Domain-Policies none; - add_header X-Frame-Options "SAMEORIGIN"; - access_log off; - } - location ~ \.(?:png|html|ttf|ico|jpg|jpeg)$ { - try_files $uri /nextcloud/index.php$uri$is_args$args; - access_log off; - } - } diff --git a/data/conf/dovecot/dovecot.conf b/data/conf/dovecot/dovecot.conf index d693d39c..51e58710 100644 --- a/data/conf/dovecot/dovecot.conf +++ b/data/conf/dovecot/dovecot.conf @@ -3,7 +3,7 @@ # -------------------------------------------------------------------------- # LDAP example: #passdb { -# args = /usr/local/etc/dovecot/ldap/passdb.conf +# args = /etc/dovecot/ldap/passdb.conf # driver = ldap #} @@ -20,7 +20,7 @@ disable_plaintext_auth = 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 = </usr/local/etc/dovecot/mail_plugins +mail_plugins = </etc/dovecot/mail_plugins mail_attachment_fs = crypt:set_prefix=mail_crypt_global:posix: mail_attachment_dir = /var/attachments mail_attachment_min_size = 128k @@ -34,7 +34,7 @@ ssl_prefer_server_ciphers = yes ssl_cipher_list = ALL:!ADH:!LOW:!SSLv2:!SSLv3:!EXP:!aNULL:!eNULL:!3DES:!MD5:!PSK:!DSS:!RC4:!SEED:!IDEA:+HIGH:+MEDIUM # Default in Dovecot 2.3 -ssl_options = no_compression +ssl_options = no_compression no_ticket # New in Dovecot 2.3 ssl_dh=</etc/ssl/mail/dhparams.pem @@ -47,12 +47,12 @@ mail_shared_explicit_inbox = yes mail_prefetch_count = 30 passdb { driver = passwd-file - args = /usr/local/etc/dovecot/dovecot-master.passwd + args = /etc/dovecot/dovecot-master.passwd master = yes pass = yes } passdb { - args = /usr/local/etc/dovecot/sql/dovecot-dict-sql-passdb.conf + args = /etc/dovecot/sql/dovecot-dict-sql-passdb.conf driver = sql result_success = return-ok result_failure = continue @@ -60,7 +60,7 @@ passdb { } passdb { driver = passwd-file - args = /usr/local/etc/dovecot/dovecot-master.passwd + args = /etc/dovecot/dovecot-master.passwd skip = authenticated } # Set doveadm_password=your-secret-password in data/conf/dovecot/extra.conf (create if missing) @@ -206,14 +206,6 @@ namespace inbox { } prefix = } -namespace { - type = shared - separator = / - prefix = Shared/%%u/ - location = maildir:%%h/:INDEX=~/Shared/%%u;CONTROL=~/Shared/%%u - subscriptions = no - list = children -} protocols = imap sieve lmtp pop3 service dict { unix_listener dict { @@ -282,52 +274,53 @@ ssl_cert = </etc/ssl/mail/cert.pem ssl_key = </etc/ssl/mail/key.pem userdb { driver = passwd-file - args = /usr/local/etc/dovecot/dovecot-master.userdb + args = /etc/dovecot/dovecot-master.userdb } userdb { - args = /usr/local/etc/dovecot/sql/dovecot-dict-sql-userdb.conf + args = /etc/dovecot/sql/dovecot-dict-sql-userdb.conf driver = sql skip = found } protocol imap { - mail_plugins = </usr/local/etc/dovecot/mail_plugins_imap + mail_plugins = </etc/dovecot/mail_plugins_imap imap_metadata = yes } mail_attribute_dict = file:%h/dovecot-attributes protocol lmtp { - mail_plugins = </usr/local/etc/dovecot/mail_plugins_lmtp - auth_socket_path = /usr/local/var/run/dovecot/auth-master + mail_plugins = </etc/dovecot/mail_plugins_lmtp + auth_socket_path = /var/run/dovecot/auth-master } protocol sieve { managesieve_logout_format = bytes=%i/%o } plugin { # Allow "any" or "authenticated" to be used in ACLs - acl_anyone = </usr/local/etc/dovecot/acl_anyone + acl_anyone = </etc/dovecot/acl_anyone acl_shared_dict = file:/var/vmail/shared-mailboxes.db acl = vfile fts = solr fts_autoindex = yes - fts_solr = url=http://solr:8983/solr/dovecot/ + fts_solr = url=http://solr:8983/solr/dovecot-fts/ quota = dict:Userquota::proxy::sqlquota quota_rule2 = Trash:storage=+100%% sieve = /var/vmail/sieve/%u.sieve sieve_plugins = sieve_imapsieve sieve_extprograms sieve_vacation_send_from_recipient = yes + sieve_redirect_envelope_from = recipient # From elsewhere to Spam folder imapsieve_mailbox1_name = Junk imapsieve_mailbox1_causes = COPY - imapsieve_mailbox1_before = file:/usr/local/lib/dovecot/sieve/report-spam.sieve + imapsieve_mailbox1_before = file:/usr/lib/dovecot/sieve/report-spam.sieve # END # From Spam folder to elsewhere imapsieve_mailbox2_name = * imapsieve_mailbox2_from = Junk imapsieve_mailbox2_causes = COPY - imapsieve_mailbox2_before = file:/usr/local/lib/dovecot/sieve/report-ham.sieve + imapsieve_mailbox2_before = file:/usr/lib/dovecot/sieve/report-ham.sieve # END quota_warning = storage=95%% quota-warning 95 %u quota_warning2 = storage=80%% quota-warning 80 %u - sieve_pipe_bin_dir = /usr/local/lib/dovecot/sieve + sieve_pipe_bin_dir = /usr/lib/dovecot/sieve sieve_global_extensions = +vnd.dovecot.pipe +vnd.dovecot.execute sieve_extensions = +notify +imapflags +vacation-seconds sieve_max_script_size = 1M @@ -338,9 +331,10 @@ plugin { sieve_vacation_min_period = 5s sieve_vacation_max_period = 0 sieve_vacation_default_period = 60s - sieve_before = dict:proxy::sieve_before;name=active;bindir=/var/vmail/sieve_before_bindir + sieve_before = /var/vmail/sieve/global_sieve_before.sieve + sieve_before2 = dict:proxy::sieve_before;name=active;bindir=/var/vmail/sieve_before_bindir sieve_after = dict:proxy::sieve_after;name=active;bindir=/var/vmail/sieve_after_bindir - sieve_after2 = /var/vmail/sieve/global.sieve + sieve_after2 = /var/vmail/sieve/global_sieve_after.sieve # -- Global keys mail_crypt_global_private_key = </mail_crypt/ecprivkey.pem @@ -363,9 +357,9 @@ service quota-warning { } } dict { - sqlquota = mysql:/usr/local/etc/dovecot/sql/dovecot-dict-sql-quota.conf - sieve_after = mysql:/usr/local/etc/dovecot/sql/dovecot-dict-sql-sieve_after.conf - sieve_before = mysql:/usr/local/etc/dovecot/sql/dovecot-dict-sql-sieve_before.conf + sqlquota = mysql:/etc/dovecot/sql/dovecot-dict-sql-quota.conf + sieve_after = mysql:/etc/dovecot/sql/dovecot-dict-sql-sieve_after.conf + sieve_before = mysql:/etc/dovecot/sql/dovecot-dict-sql-sieve_before.conf } remote 127.0.0.1 { disable_plaintext_auth = no @@ -384,9 +378,12 @@ service stats { } } imap_max_line_length = 2 M -auth_cache_verify_password_with_worker = yes -auth_cache_negative_ttl = 0 -auth_cache_ttl = 30 s -auth_cache_size = 2 M -!include_try /usr/local/etc/dovecot/extra.conf +#auth_cache_verify_password_with_worker = yes +#auth_cache_negative_ttl = 0 +#auth_cache_ttl = 30 s +#auth_cache_size = 2 M +!include_try /etc/dovecot/extra.conf +!include_try /etc/dovecot/sogo-sso.conf +!include_try /etc/dovecot/shared_namespace.conf default_client_limit = 10400 +default_vsz_limit = 1024 M diff --git a/data/conf/dovecot/sieve_after b/data/conf/dovecot/global_sieve_after similarity index 74% rename from data/conf/dovecot/sieve_after rename to data/conf/dovecot/global_sieve_after index 2e0cfe08..e047136e 100644 --- a/data/conf/dovecot/sieve_after +++ b/data/conf/dovecot/global_sieve_after @@ -1,3 +1,6 @@ +# global_sieve_after script +# global_sieve_before -> user sieve_before (mailcow UI) -> user sieve_after (mailcow UI) -> global_sieve_after + require "fileinto"; require "mailbox"; require "variables"; diff --git a/data/conf/dovecot/global_sieve_before b/data/conf/dovecot/global_sieve_before new file mode 100644 index 00000000..e6a523d4 --- /dev/null +++ b/data/conf/dovecot/global_sieve_before @@ -0,0 +1,2 @@ +# global_sieve_before script +# global_sieve_before -> user sieve_before (mailcow UI) -> user sieve_after (mailcow UI) -> global_sieve_after diff --git a/data/conf/nginx/site.conf b/data/conf/nginx/site.conf index 8b8959d5..ccb94fa0 100644 --- a/data/conf/nginx/site.conf +++ b/data/conf/nginx/site.conf @@ -34,6 +34,7 @@ server { client_max_body_size 0; + listen 127.0.0.1:65510; include /etc/nginx/conf.d/listen_plain.active; include /etc/nginx/conf.d/listen_ssl.active; include /etc/nginx/conf.d/server_name.active; @@ -142,7 +143,19 @@ server { try_files /autoconfig.php =404; } + # auth_request endpoint if ALLOW_ADMIN_EMAIL_LOGIN is set + location /sogo-auth-verify { + internal; + proxy_set_header X-Original-URI $request_uri; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header Host $http_host; + proxy_set_header Content-Length ""; + proxy_pass http://127.0.0.1:65510/sogo-auth; + proxy_pass_request_body off; + } + location ^~ /Microsoft-Server-ActiveSync { + include /etc/nginx/conf.d/sogo_proxy_auth.active; include /etc/nginx/conf.d/sogo_eas.active; proxy_connect_timeout 4000; proxy_next_upstream timeout error; @@ -165,6 +178,7 @@ server { } location ^~ /SOGo { + include /etc/nginx/conf.d/sogo_proxy_auth.active; include /etc/nginx/conf.d/sogo.active; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; diff --git a/data/conf/nginx/templates/sogo.auth_request.template.sh b/data/conf/nginx/templates/sogo.auth_request.template.sh new file mode 100644 index 00000000..f6d2d98e --- /dev/null +++ b/data/conf/nginx/templates/sogo.auth_request.template.sh @@ -0,0 +1,10 @@ +if printf "%s\n" "${ALLOW_ADMIN_EMAIL_LOGIN}" | grep -E '^([yY][eE][sS]|[yY])+$' >/dev/null; then + echo 'auth_request /sogo-auth-verify; +auth_request_set $user $upstream_http_x_user; +auth_request_set $auth $upstream_http_x_auth; +auth_request_set $auth_type $upstream_http_x_auth_type; +proxy_set_header x-webobjects-remote-user "$user"; +proxy_set_header Authorization "$auth"; +proxy_set_header x-webobjects-auth-type "$auth_type"; +' +fi diff --git a/data/conf/phpfpm/sogo-sso/.gitkeep b/data/conf/phpfpm/sogo-sso/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/data/conf/postfix/anonymize_headers.pcre b/data/conf/postfix/anonymize_headers.pcre index 099094d9..7842b9e0 100644 --- a/data/conf/postfix/anonymize_headers.pcre +++ b/data/conf/postfix/anonymize_headers.pcre @@ -1,8 +1,11 @@ if /^\s*Received:.*Authenticated sender.*\(Postcow\)/ -/^\s*Received:.*Authenticated sender:(.+)/ - REPLACE Received: from localhost (localhost [127.0.0.1]) (Authenticated sender:$1 +#/^Received: from .*? \([\w-.]* \[.*?\]\)\s+\(Authenticated sender: (.+)\)\s+by.+\(Postcow\) with (E?SMTPS?A?) id ([A-F0-9]+).+;.*?/ +/^Received: from .*? \([\w-.]* \[.*?\]\)(.*|\n.*)\(Authenticated sender: (.+)\)\s+by.+\(Postcow\) with (.*)/ + REPLACE Received: from [127.0.0.1] (localhost [127.0.0.1]) by localhost (Mailerdaemon) with $2 endif /^\s*X-Enigmail/ IGNORE /^\s*X-Mailer/ IGNORE /^\s*X-Originating-IP/ IGNORE /^\s*X-Forward/ IGNORE +# Not removing UA by default, might be signed +#/^\s*User-Agent/ IGNORE diff --git a/data/conf/postfix/local_transport b/data/conf/postfix/local_transport new file mode 100644 index 00000000..6dd21011 --- /dev/null +++ b/data/conf/postfix/local_transport @@ -0,0 +1,2 @@ +/watchdog@localhost$/ watchdog_discard: +/localhost$/ local: diff --git a/data/conf/postfix/main.cf b/data/conf/postfix/main.cf index 83a252d8..50fcdfa8 100644 --- a/data/conf/postfix/main.cf +++ b/data/conf/postfix/main.cf @@ -1,3 +1,6 @@ +# -------------------------------------------------------------------------- +# Please create a file "extra.cf" for persistent overrides to main.cf +# -------------------------------------------------------------------------- biff = no append_dot_mydomain = no smtpd_tls_cert_file = /etc/ssl/mail/cert.pem @@ -6,7 +9,10 @@ smtpd_use_tls=yes smtpd_tls_received_header = 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 +smtpd_relay_restrictions = permit_mynetworks, + permit_sasl_authenticated, + defer_unauth_destination +# alias maps are auto-generated in postfix.sh on startup alias_maps = hash:/etc/aliases alias_database = hash:/etc/aliases relayhost = @@ -19,20 +25,53 @@ bounce_queue_lifetime = 1d broken_sasl_auth_clients = yes disable_vrfy_command = yes maximal_backoff_time = 1800s -maximal_queue_lifetime = 1d +maximal_queue_lifetime = 5d +delay_warning_time = 4h message_size_limit = 104857600 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, tcp:127.0.0.1:10027 +postscreen_access_list = permit_mynetworks, + cidr:/opt/postfix/conf/postscreen_access.cidr, + tcp:127.0.0.1:10027 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_sites = wl.mailspike.net=127.0.0.[18;19;20]*-2 + hostkarma.junkemailfilter.com=127.0.0.1*-2 + list.dnswl.org=127.0.[0..255].0*-2 + list.dnswl.org=127.0.[0..255].1*-4 + list.dnswl.org=127.0.[0..255].2*-6 + list.dnswl.org=127.0.[0..255].3*-8 + ix.dnsbl.manitu.net*2 + bl.spamcop.net*2 + hostkarma.junkemailfilter.com=127.0.0.2*4 + hostkarma.junkemailfilter.com=127.0.0.3*2 + hostkarma.junkemailfilter.com=127.0.0.4*3 + hostkarma.junkemailfilter.com=127.0.1.2*1 + backscatter.spameatingmonkey.net*2 + bl.ipv6.spameatingmonkey.net*2 + bl.spameatingmonkey.net*2 + b.barracudacentral.org=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*2 + hostkarma.junkemailfilter.com=127.0.1.2*1 +postscreen_dnsbl_threshold = 5 postscreen_dnsbl_ttl = 5m postscreen_greet_action = enforce postscreen_greet_banner = $smtpd_banner @@ -40,16 +79,10 @@ 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_policy_override_maps.cf, - proxy:mysql:/opt/postfix/conf/sql/mysql_sender_dependent_default_transport_maps.cf, - proxy:mysql:/opt/postfix/conf/sql/mysql_transport_maps.cf, - proxy:mysql:/opt/postfix/conf/sql/mysql_sasl_passwd_maps_sender_dependent.cf, - proxy:mysql:/opt/postfix/conf/sql/mysql_sasl_passwd_maps_transport_maps.cf, +proxy_read_maps = proxy:mysql:/opt/postfix/conf/sql/mysql_sasl_passwd_maps_transport_maps.cf, proxy:mysql:/opt/postfix/conf/sql/mysql_tls_enforce_in_policy.cf, - proxy:mysql:/opt/postfix/conf/sql/mysql_sender_bcc_maps.cf, - proxy:mysql:/opt/postfix/conf/sql/mysql_recipient_bcc_maps.cf, - proxy:mysql:/opt/postfix/conf/sql/mysql_recipient_canonical_maps.cf, + $sender_dependent_default_transport_maps, + $smtp_tls_policy_maps, $local_recipient_maps, $mydestination, $virtual_alias_maps, @@ -60,11 +93,14 @@ proxy_read_maps = proxy:mysql:/opt/postfix/conf/sql/mysql_virtual_sender_acl.cf, $relay_domains, $canonical_maps, $sender_canonical_maps, + $sender_bcc_maps, + $recipient_bcc_maps, $recipient_canonical_maps, $relocated_maps, $transport_maps, $mynetworks, - $smtpd_sender_login_maps + $smtpd_sender_login_maps, + $smtp_sasl_password_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 @@ -81,33 +117,47 @@ 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_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, permit_sasl_authenticated, reject_unlisted_sender, reject_unknown_sender_domain +smtpd_sender_restrictions = reject_authenticated_sender_login_mismatch, + permit_mynetworks, + 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 = auto smtpd_tls_exclude_ciphers = ECDHE-RSA-RC4-SHA, RC4, aNULL, DES-CBC3-SHA, ECDHE-RSA-DES-CBC3-SHA, EDH-RSA-DES-CBC3-SHA smtpd_tls_loglevel = 1 -smtp_tls_mandatory_protocols = !SSLv2, !SSLv3 + +# Mandatory protocols and ciphers are used when a connections is enforced to use TLS +# Does _not_ apply to enforced incoming TLS settings per mailbox +smtp_tls_mandatory_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1 +lmtp_tls_mandatory_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1 +smtpd_tls_mandatory_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1 +smtpd_tls_mandatory_ciphers = high + smtp_tls_protocols = !SSLv2, !SSLv3 -lmtp_tls_mandatory_protocols = !SSLv2, !SSLv3 -lmtp_tls_protocols = !SSLv2, !SSLv2, !SSLv3 -smtpd_tls_mandatory_protocols = !SSLv2, !SSLv3 +lmtp_tls_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1 smtpd_tls_protocols = !SSLv2, !SSLv3 + smtpd_tls_security_level = may tls_preempt_cipherlist = yes tls_ssl_options = NO_COMPRESSION -smtpd_tls_mandatory_ciphers = high virtual_alias_maps = proxy:mysql:/opt/postfix/conf/sql/mysql_virtual_alias_maps.cf, + proxy:mysql:/opt/postfix/conf/sql/mysql_virtual_resource_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 + proxy:mysql:/opt/postfix/conf/sql/mysql_virtual_alias_domain_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 @@ -123,7 +173,6 @@ smtpd_milters = inet:rspamd:9900 non_smtpd_milters = inet:rspamd:9900 milter_mail_macros = i {mail_addr} {client_addr} {client_name} {auth_authen} mydestination = localhost.localdomain, localhost -#content_filter=zeyple # Prefere IPv4, useful for v4-only envs smtp_address_preference = ipv4 smtp_sender_dependent_authentication = yes @@ -134,5 +183,14 @@ smtp_sasl_mechanism_filter = plain, login smtp_tls_policy_maps=proxy:mysql:/opt/postfix/conf/sql/mysql_tls_policy_override_maps.cf smtp_header_checks = pcre:/opt/postfix/conf/anonymize_headers.pcre mail_name = Postcow -transport_maps = proxy:mysql:/opt/postfix/conf/sql/mysql_transport_maps.cf +# local_transport map catches local destinations and prevents routing local dests when the next map would route "*" +transport_maps = pcre:/opt/postfix/conf/local_transport, + proxy:mysql:/opt/postfix/conf/sql/mysql_transport_maps.cf smtp_sasl_auth_soft_bounce = no +postscreen_discard_ehlo_keywords = silent-discard, dsn +compatibility_level = 2 +smtputf8_enable = no + +# DO NOT EDIT ANYTHING BELOW # +# User overrides # + diff --git a/data/conf/postfix/master.cf b/data/conf/postfix/master.cf index 79642f6d..b664bbd5 100644 --- a/data/conf/postfix/master.cf +++ b/data/conf/postfix/master.cf @@ -1,29 +1,47 @@ +# inter-mx with postscreen on 25/tcp smtp inet n - n - 1 postscreen smtpd pass - - n - - smtpd -o smtpd_helo_restrictions=permit_mynetworks,reject_non_fqdn_helo_hostname -o smtpd_sasl_auth_enable=no + -o smtpd_sender_restrictions=permit_mynetworks,reject_unlisted_sender,reject_unknown_sender_domain + +# smtpd tls-wrapped (smtps) on 465/tcp smtps inet n - n - - smtpd -o smtpd_tls_wrappermode=yes -o smtpd_client_restrictions=permit_mynetworks,permit_sasl_authenticated,reject + -o smtpd_tls_mandatory_protocols=!SSLv2,!SSLv3 -o tls_preempt_cipherlist=yes + +# smtpd with starttls on 587/tcp 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 smtpd_tls_mandatory_protocols=!SSLv2,!SSLv3 -o tls_preempt_cipherlist=yes + +# used by SOGo +# smtpd_sender_restrictions should match main.cf, but with check_sasl_access prepended for login-as-mailbox-user function 588 inet n - n - - smtpd -o smtpd_client_restrictions=permit_mynetworks,permit_sasl_authenticated,reject -o smtpd_tls_auth_only=no -o smtpd_sender_restrictions=check_sasl_access,regexp:/opt/postfix/conf/allow_mailcow_local.regexp,reject_authenticated_sender_login_mismatch,permit_mynetworks,permit_sasl_authenticated,reject_unlisted_sender,reject_unknown_sender_domain + +# used to reinject quarantine mails 590 inet n - n - - smtpd -o smtpd_client_restrictions=permit_mynetworks,reject -o smtpd_tls_auth_only=no -o smtpd_milters= -o non_smtpd_milters= + +# enforced smtp connector 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 + +# smtp connector used, when a transport map matched +# this helps to have different sasl maps than we have with sender dependent transport maps smtp_via_transport_maps unix - - n - - smtp -o smtp_sasl_password_maps=proxy:mysql:/opt/postfix/conf/sql/mysql_sasl_passwd_maps_transport_maps.cf @@ -55,25 +73,12 @@ scache unix - - n - 1 scache maildrop unix - n n - - pipe flags=DRhu user=vmail argv=/usr/bin/maildrop -d ${recipient} -# start zeyple -zeyple unix - n n - - pipe - user=zeyple argv=/usr/local/bin/zeyple.py ${recipient} -127.0.0.1:10026 inet n - n - 10 smtpd - -o content_filter= - -o receive_override_options=no_unknown_recipient_checks,no_header_body_checks,no_milters - -o smtpd_helo_restrictions= - -o smtpd_client_restrictions= - -o smtpd_sender_restrictions= - -o smtpd_recipient_restrictions=permit_mynetworks,reject - -o mynetworks=127.0.0.0/8 - -o smtpd_authorized_xforward_hosts=127.0.0.0/8 -# end zeyple - # start whitelist_fwd 127.0.0.1:10027 inet n n n - 0 spawn user=nobody argv=/usr/local/bin/whitelist_forwardinghosts.sh # end whitelist_fwd # start watchdog-specific +# logs to local7 (hidden) 589 inet n - n - - smtpd -o smtpd_client_restrictions=permit_mynetworks,reject -o syslog_name=watchdog diff --git a/data/conf/rspamd/custom/bad_words.map b/data/conf/rspamd/custom/bad_words.map new file mode 100644 index 00000000..5f19d2a9 --- /dev/null +++ b/data/conf/rspamd/custom/bad_words.map @@ -0,0 +1,44 @@ +/\ssex\s/i +/\svagina\s/i +/\serotic\s/i +/\serection\s/i +/\ssexy\s/i +/\spenis\s/i +/\sass\s/i +/\sviagra\s/i +/\stits\s/i +/\stitty\s/i +/\stitties\s/i +/\scum\s/i +/\ssperm\s/i +/\sslut\s/i +/\sporn\s/i +/\scock\s/i +/\spharma\s/i +/\spharmacy\s/i +/\sseo\s/i +/\smarketing\s/i +/\sjackpot\s/i +/\slotto\s/i +/\slottery\s/i +/pillenversand/i +/\skredithilfe\s/i +/\skapital\s/i +/\skrankenversicherung\s/i +/bitcoin/i +/pädophil/i +/paedophil/i +/freiberufler/i +/unternehmer/i +/masturbieren/i +/trojaner/i +/malware/i +/\sscooter\s/i +/\sescooter\s/i +/\se-scooter\s/i +/testost/i +/\spotenz\s/i +/potenzmittel/i +/rezeptfrei/i +/apotheke/i +/web\sdevelopment/i diff --git a/data/conf/rspamd/custom/fishy_tlds.map b/data/conf/rspamd/custom/fishy_tlds.map new file mode 100644 index 00000000..01c7c86d --- /dev/null +++ b/data/conf/rspamd/custom/fishy_tlds.map @@ -0,0 +1,66 @@ +/.+\.accountant$/i +/.+\.art$/i +/.+\.asia$/i +/.+\.bid$/i +/.+\.biz$/i +/.+\.care$/i +/.+\.cf$/i +/.+\.cl$/i +/.+\.click$/i +/.+\.cloud$/i +/.+\.co$/i +/.+\.construction$/i +/.+\.country$/i +/.+\.cricket$/i +/.+\.date$/i +/.+\.desi$/i +/.+\.download$/i +/.+\.estate$/i +/.+\.faith$/i +/.+\.fit$/i +/.+\.flights$/i +/.+\.ga$/i +/.+\.gdn$/i +/.+\.gq$/i +/.+\.guru$/i +/.+\.icu$/i +/.+\.id$/i +/.+\.info$/i +/.+\.in.net$/i +/.+\.ir$/i +/.+\.jetzt$/i +/.+\.kim$/i +/.+\.life$/i +/.+\.link$/i +/.+\.loan$/i +/.+\.mk$/i +/.+\.ml$/i +/.+\.ninja$/i +/.+\.online$/i +/.+\.ooo$/i +/.+\.party$/i +/.+\.pro$/i +/.+\.ps$/i +/.+\.pw$/i +/.+\.racing$/i +/.+\.review$/i +/.+\.rocks$/i +/.+\.ryukyu$/i +/.+\.science$/i +/.+\.site$/i +/.+\.space$/i +/.+\.stream$/i +/.+\.sucks$/i +/.+\.tk$/i +/.+\.top$/i +/.+\.topica\.com$/i +/.+\.town$/i +/.+\.trade$/i +/.+\.uno$/i +/.+\.vip$/i +/.+\.webcam$/i +/.+\.website$/i +/.+\.win$/i +/.+\.work$/i +/.+\.world$/i +/.+\.xyz$/i diff --git a/data/conf/rspamd/custom/ip_wl.map b/data/conf/rspamd/custom/ip_wl.map new file mode 100644 index 00000000..c8bb5529 --- /dev/null +++ b/data/conf/rspamd/custom/ip_wl.map @@ -0,0 +1,4 @@ +# IP whitelist +# 127.0.0.1 +# 1.2.3.4 +# ... diff --git a/data/conf/rspamd/dynmaps/settings.php b/data/conf/rspamd/dynmaps/settings.php index 4d78456e..0bf9f519 100644 --- a/data/conf/rspamd/dynmaps/settings.php +++ b/data/conf/rspamd/dynmaps/settings.php @@ -6,6 +6,8 @@ then any of these will trigger the rule. If a rule is triggered then no more rul */ header('Content-Type: text/plain'); require_once "vars.inc.php"; +// Getting headers sent by the client. +//$headers = apache_request_headers(); ini_set('error_reporting', 0); @@ -25,6 +27,23 @@ catch (PDOException $e) { exit; } +// Check if db changed and return header +/*$stmt = $pdo->prepare("SELECT UNIX_TIMESTAMP(UPDATE_TIME) AS `db_update_time` FROM information_schema.tables + WHERE `TABLE_NAME` = 'filterconf' + AND TABLE_SCHEMA = :dbname;"); +$stmt->execute(array( + ':dbname' => $database_name +)); +$db_update_time = $stmt->fetch(PDO::FETCH_ASSOC)['db_update_time']; + +if (isset($headers['If-Modified-Since']) && (strtotime($headers['If-Modified-Since']) == $db_update_time)) { + header('Last-Modified: '.gmdate('D, d M Y H:i:s', $db_update_time).' GMT', true, 304); + exit; +} else { + header('Last-Modified: '.gmdate('D, d M Y H:i:s', $db_update_time).' GMT', true, 200); +} +*/ + function parse_email($email) { if (!filter_var($email, FILTER_VALIDATE_EMAIL)) return false; $a = strrpos($email, '@'); @@ -43,7 +62,9 @@ function wl_by_sogo() { if (!filter_var($contact, FILTER_VALIDATE_EMAIL)) { continue; } - $rcpt[$row['user']][] = '/^' . str_replace('/', '\/', $contact) . '$/i'; + // Explicit from, no mime_from, no regex - envelope must match + // mailcow white and blacklists also cover mime_from + $rcpt[$row['user']][] = str_replace('/', '\/', $contact); } } return $rcpt; @@ -67,7 +88,7 @@ function ucl_rcpts($object, $type) { if (!empty($local) && !empty($domain)) { $rcpt[] = '/^' . str_replace('/', '\/', $local) . '[+].*' . str_replace('/', '\/', $domain) . '$/i'; } - $rcpt[] = '/^' . str_replace('/', '\/', $row['address']) . '$/i'; + $rcpt[] = str_replace('/', '\/', $row['address']); } // Aliases by alias domains $stmt = $pdo->prepare("SELECT CONCAT(`local_part`, '@', `alias_domain`.`alias_domain`) AS `alias` FROM `mailbox` @@ -85,7 +106,7 @@ function ucl_rcpts($object, $type) { if (!empty($local) && !empty($domain)) { $rcpt[] = '/^' . str_replace('/', '\/', $local) . '[+].*' . str_replace('/', '\/', $domain) . '$/i'; } - $rcpt[] = '/^' . str_replace('/', '\/', $row['alias']) . '$/i'; + $rcpt[] = str_replace('/', '\/', $row['alias']); } } } @@ -107,8 +128,8 @@ function ucl_rcpts($object, $type) { settings { watchdog { priority = 10; - rcpt = "/null@localhost/i"; - from = "/watchdog@localhost/i"; + rcpt_mime = "/null@localhost/i"; + from_mime = "/watchdog@localhost/i"; apply "default" { actions { reject = 9999.0; @@ -199,12 +220,13 @@ while ($row = array_shift($rows)) { ?> whitelist_<?=$username_sane;?> { <?php + $list_items = array(); $stmt = $pdo->prepare("SELECT `value` FROM `filterconf` WHERE `object`= :object AND `option` = 'whitelist_from'"); $stmt->execute(array(':object' => $row['object'])); $list_items = $stmt->fetchAll(PDO::FETCH_ASSOC); - while ($item = array_shift($list_items)) { + foreach ($list_items as $item) { ?> from = "/<?='^' . str_replace('\*', '.*', preg_quote($item['value'], '/')) . '$' ;?>/i"; <?php @@ -237,24 +259,13 @@ while ($row = array_shift($rows)) { "MAILCOW_WHITE" ] } - whitelist_header_<?=$username_sane;?> { + whitelist_mime_<?=$username_sane;?> { <?php - $header_from = array(); - $stmt = $pdo->prepare("SELECT `value` FROM `filterconf` - WHERE `object`= :object - AND `option` = 'whitelist_from'"); - $stmt->execute(array(':object' => $row['object'])); - $list_items = $stmt->fetchAll(PDO::FETCH_ASSOC); + foreach ($list_items as $item) { ?> - header = { + from_mime = "/<?='^' . str_replace('\*', '.*', preg_quote($item['value'], '/')) . '$' ;?>/i"; <?php - while ($item = array_shift($list_items)) { - $header_from[] = str_replace('\*', '.*', preg_quote($item['value'], '/')); } -?> - "From" = "/(<?=implode('|', $header_from);?>)/i"; - } -<?php if (!filter_var(trim($row['object']), FILTER_VALIDATE_EMAIL)) { ?> priority = 5; @@ -297,13 +308,13 @@ while ($row = array_shift($rows)) { ?> blacklist_<?=$username_sane;?> { <?php - $items[] = array(); + $list_items = array(); $stmt = $pdo->prepare("SELECT `value` FROM `filterconf` WHERE `object`= :object AND `option` = 'blacklist_from'"); $stmt->execute(array(':object' => $row['object'])); $list_items = $stmt->fetchAll(PDO::FETCH_ASSOC); - while ($item = array_shift($list_items)) { + foreach ($list_items as $item) { ?> from = "/<?='^' . str_replace('\*', '.*', preg_quote($item['value'], '/')) . '$' ;?>/i"; <?php @@ -338,22 +349,11 @@ while ($row = array_shift($rows)) { } blacklist_header_<?=$username_sane;?> { <?php - $header_from = array(); - $stmt = $pdo->prepare("SELECT `value` FROM `filterconf` - WHERE `object`= :object - AND `option` = 'blacklist_from'"); - $stmt->execute(array(':object' => $row['object'])); - $list_items = $stmt->fetchAll(PDO::FETCH_ASSOC); + foreach ($list_items as $item) { ?> - header = { + from_mime = "/<?='^' . str_replace('\*', '.*', preg_quote($item['value'], '/')) . '$' ;?>/i"; <?php - while ($item = array_shift($list_items)) { - $header_from[] = str_replace('\*', '.*', preg_quote($item['value'], '/')); } -?> - "From" = "/(<?=implode('|', $header_from);?>)/i"; - } -<?php if (!filter_var(trim($row['object']), FILTER_VALIDATE_EMAIL)) { ?> priority = 5; diff --git a/data/conf/rspamd/local.d/arc.conf b/data/conf/rspamd/local.d/arc.conf index e8d95871..277e0cc0 100644 --- a/data/conf/rspamd/local.d/arc.conf +++ b/data/conf/rspamd/local.d/arc.conf @@ -28,3 +28,5 @@ use_redis = true; key_prefix = "DKIM_PRIV_KEYS"; # Selector map selector_prefix = "DKIM_SELECTORS"; +sign_inbound = true; +use_domain_sign_inbound = "recipient"; diff --git a/data/conf/rspamd/local.d/composites.conf b/data/conf/rspamd/local.d/composites.conf index d775c4f6..036737de 100644 --- a/data/conf/rspamd/local.d/composites.conf +++ b/data/conf/rspamd/local.d/composites.conf @@ -16,3 +16,17 @@ SOGO_CONTACT_EXCLUDE_FWD_HOST { SOGO_CONTACT_SPOOFED { expression = "(R_SPF_PERMFAIL | R_SPF_SOFTFAIL | R_SPF_FAIL) & ~SOGO_CONTACT"; } +SPOOFED_UNAUTH { + expression = "!MAILCOW_AUTH & !MAILCOW_WHITE & !R_SPF_ALLOW & !DMARC_POLICY_ALLOW & !ARC_ALLOW & !SIEVE_HOST & MAILCOW_DOMAIN_HEADER_FROM"; + score = 5.0; +} +# Only apply to inbound unauthed and not whitelisted +OLEFY_MACRO { + expression = "!MAILCOW_AUTH & !MAILCOW_WHITE & OLETOOLS"; + score = 20.0; + policy = "remove_weight"; +} +BAD_WORD_BAD_TLD { + expression = "FISHY_TLD & BAD_WORDS" + score = 10.0; +} diff --git a/data/conf/rspamd/local.d/external_services.conf b/data/conf/rspamd/local.d/external_services.conf new file mode 100644 index 00000000..bed4d917 --- /dev/null +++ b/data/conf/rspamd/local.d/external_services.conf @@ -0,0 +1,7 @@ +oletools { + # default olefy settings + servers = "olefy:10055"; + # needs to be set explicitly for Rspamd < 1.9.5 + scan_mime_parts = true; + # mime-part regex matching in content-type or filename +} diff --git a/data/conf/rspamd/local.d/metadata_exporter.conf b/data/conf/rspamd/local.d/metadata_exporter.conf index afe5c7e1..eaf9a5b2 100644 --- a/data/conf/rspamd/local.d/metadata_exporter.conf +++ b/data/conf/rspamd/local.d/metadata_exporter.conf @@ -20,7 +20,7 @@ return function(task) if ratelimited then return true end - return + return false end EOD; } diff --git a/data/conf/rspamd/local.d/milter_headers.conf b/data/conf/rspamd/local.d/milter_headers.conf index 7f21b8e0..d8d40258 100644 --- a/data/conf/rspamd/local.d/milter_headers.conf +++ b/data/conf/rspamd/local.d/milter_headers.conf @@ -13,6 +13,7 @@ routines { authentication-results { header = "Authentication-Results"; remove = 1; + add_smtp_user = false; spf_symbols { pass = "R_SPF_ALLOW"; fail = "R_SPF_FAIL"; diff --git a/data/conf/rspamd/local.d/multimap.conf b/data/conf/rspamd/local.d/multimap.conf index 7752b813..c27370b0 100644 --- a/data/conf/rspamd/local.d/multimap.conf +++ b/data/conf/rspamd/local.d/multimap.conf @@ -83,3 +83,39 @@ GLOBAL_RCPT_BL { prefilter = true; action = "reject"; } + +SIEVE_HOST { + type = "ip"; + map = "$LOCAL_CONFDIR/custom/dovecot_trusted.map"; + symbols_set = ["SIEVE_HOST"]; +} + +MAILCOW_DOMAIN_HEADER_FROM { + type = "header"; + header = "from"; + filter = "email:domain"; + map = "redis://DOMAIN_MAP"; +} + +IP_WHITELIST { + type = "ip"; + map = "$LOCAL_CONFDIR/custom/ip_wl.map"; + prefilter = "true"; + action = "accept"; +} + +FISHY_TLD { + type = "from"; + filter = "email:domain"; + map = "${LOCAL_CONFDIR}/custom/fishy_tlds.map"; + regexp = true; + score = 0.1; +} + +BAD_WORDS { + type = "content"; + filter = "text"; + map = "${LOCAL_CONFDIR}/custom/bad_words.map"; + regexp = true; + score = 0.1; +} diff --git a/data/conf/rspamd/local.d/policies_group.conf b/data/conf/rspamd/local.d/policies_group.conf index f1c2b9f1..7f128fec 100644 --- a/data/conf/rspamd/local.d/policies_group.conf +++ b/data/conf/rspamd/local.d/policies_group.conf @@ -11,7 +11,13 @@ symbols = { "R_DKIM_REJECT" { score = 10.0; } - "R_DKIM_PERMFAIL" { - score = 10.0; + "DMARC_POLICY_REJECT" { + weight = 20.0; + } + "DMARC_POLICY_QUARANTINE" { + weight = 10.0; + } + "DMARC_POLICY_SOFTFAIL" { + weight = 2.0; } } diff --git a/data/conf/rspamd/local.d/rbl.conf b/data/conf/rspamd/local.d/rbl.conf new file mode 100644 index 00000000..3936cbbf --- /dev/null +++ b/data/conf/rspamd/local.d/rbl.conf @@ -0,0 +1,10 @@ +rbls { + uceprotect1 { + symbol = "RBL_UCEPROTECT_LEVEL1"; + rbl = "dnsbl-1.uceprotect.net"; + } + uceprotect2 { + symbol = "RBL_UCEPROTECT_LEVEL2"; + rbl = "dnsbl-2.uceprotect.net"; + } +} diff --git a/data/conf/rspamd/local.d/rbl_group.conf b/data/conf/rspamd/local.d/rbl_group.conf new file mode 100644 index 00000000..86c4e023 --- /dev/null +++ b/data/conf/rspamd/local.d/rbl_group.conf @@ -0,0 +1,8 @@ +symbols = { + "RBL_UCEPROTECT_LEVEL1" { + score = 3.5; + } + "RBL_UCEPROTECT_LEVEL2" { + score = 1.5; + } +} diff --git a/data/conf/rspamd/local.d/rspamd.conf.local b/data/conf/rspamd/local.d/rspamd.conf.local deleted file mode 100644 index 0662c47d..00000000 --- a/data/conf/rspamd/local.d/rspamd.conf.local +++ /dev/null @@ -1,16 +0,0 @@ -# rspamd.conf.local - -worker "fuzzy" { - # Socket to listen on (UDP and TCP from rspamd 1.3) - bind_socket = "*:11445"; - allow_update = ["127.0.0.1", "::1"]; - # Number of processes to serve this storage (useful for read scaling) - count = 2; - # Backend ("sqlite" or "redis" - default "sqlite") - backend = "redis"; - # Hashes storage time (3 months) - expire = 90d; - # Synchronize updates to the storage each minute - sync = 1min; -} - diff --git a/data/conf/rspamd/local.d/spamassassin.conf b/data/conf/rspamd/local.d/spamassassin.conf new file mode 100644 index 00000000..d091af63 --- /dev/null +++ b/data/conf/rspamd/local.d/spamassassin.conf @@ -0,0 +1 @@ +ruleset = "/etc/rspamd/custom/sa-rules"; diff --git a/data/conf/rspamd/local.d/statistics_group.conf b/data/conf/rspamd/local.d/statistics_group.conf index 160c65fa..49c952bd 100644 --- a/data/conf/rspamd/local.d/statistics_group.conf +++ b/data/conf/rspamd/local.d/statistics_group.conf @@ -1,10 +1,10 @@ symbols = { "BAYES_SPAM" { - weight = 8.5; + weight = 2.5; description = "Message probably spam, probability: "; } "BAYES_HAM" { - weight = -12.5; + weight = -10.5; description = "Message probably ham, probability: "; } } diff --git a/data/conf/rspamd/meta_exporter/pipe.php b/data/conf/rspamd/meta_exporter/pipe.php index 3e29d207..31f6037f 100644 --- a/data/conf/rspamd/meta_exporter/pipe.php +++ b/data/conf/rspamd/meta_exporter/pipe.php @@ -84,6 +84,9 @@ $rcpt_final_mailboxes = array(); // Loop through all rcpts foreach (json_decode($rcpts, true) as $rcpt) { + // Remove tag + $rcpt = preg_replace('/^(.*?)\+.*(@.*)$/', '$1$2', $rcpt); + // Break rcpt into local part and domain part $parsed_rcpt = parse_email($rcpt); @@ -128,6 +131,14 @@ foreach (json_decode($rcpts, true) as $rcpt) { )); $gotos = $stmt->fetch(PDO::FETCH_ASSOC)['goto']; } + if (empty($gotos)) { + $stmt = $pdo->prepare("SELECT `target_domain` FROM `alias_domain` WHERE `alias_domain` = :rcpt AND `active` = '1'"); + $stmt->execute(array(':rcpt' => $parsed_rcpt['domain'])); + $goto_branch = $stmt->fetch(PDO::FETCH_ASSOC)['target_domain']; + if ($goto_branch) { + $gotos = $parsed_rcpt['local'] . '@' . $goto_branch; + } + } $gotos_array = explode(',', $gotos); $loop_c = 0; @@ -156,8 +167,18 @@ foreach (json_decode($rcpts, true) as $rcpt) { $stmt = $pdo->prepare("SELECT `goto` FROM `alias` WHERE `address` = :goto AND `active` = '1'"); $stmt->execute(array(':goto' => $goto)); $goto_branch = $stmt->fetch(PDO::FETCH_ASSOC)['goto']; - error_log("QUARANTINE: quarantine pipe: goto address " . $goto . " is a alias branch for " . $goto_branch); - $goto_branch_array = explode(',', $goto_branch); + if ($goto_branch) { + error_log("QUARANTINE: quarantine pipe: goto address " . $goto . " is a alias branch for " . $goto_branch); + $goto_branch_array = explode(',', $goto_branch); + } else { + $stmt = $pdo->prepare("SELECT `target_domain` FROM `alias_domain` WHERE `alias_domain` = :domain AND `active` AND '1'"); + $stmt->execute(array(':domain' => $parsed_goto['domain'])); + $goto_branch = $stmt->fetch(PDO::FETCH_ASSOC)['target_domain']; + if ($goto_branch) { + error_log("QUARANTINE: quarantine pipe: goto domain " . $parsed_gto['domain'] . " is a domain alias branch for " . $goto_branch); + $goto_branch_array = array($parsed_gto['local'] . '@' . $goto_branch); + } + } } } // goto item was processed, unset diff --git a/data/conf/rspamd/override.d/ratelimit.conf b/data/conf/rspamd/override.d/ratelimit.conf index ccd083d4..f02d2d3c 100644 --- a/data/conf/rspamd/override.d/ratelimit.conf +++ b/data/conf/rspamd/override.d/ratelimit.conf @@ -1,8 +1,8 @@ rates { # Format: "1 / 1h" or "20 / 1m" etc. - global ratelimits are disabled by default - to = "45 / 1m"; - to_ip = "360 / 1m"; - to_ip_from = "180 / 1m"; + to = "100 / 1s"; + to_ip = "100 / 1s"; + to_ip_from = "100 / 1s"; bounce_to = "100 / 1s"; bounce_to_ip = "100 / 1s"; } diff --git a/data/conf/rspamd/override.d/worker-controller-password.inc b/data/conf/rspamd/override.d/worker-controller-password.inc deleted file mode 100644 index 9a5984d1..00000000 --- a/data/conf/rspamd/override.d/worker-controller-password.inc +++ /dev/null @@ -1 +0,0 @@ -# Placeholder diff --git a/data/conf/rspamd/override.d/worker-fuzzy.inc b/data/conf/rspamd/override.d/worker-fuzzy.inc new file mode 100644 index 00000000..09b39c93 --- /dev/null +++ b/data/conf/rspamd/override.d/worker-fuzzy.inc @@ -0,0 +1,12 @@ +# Socket to listen on (UDP and TCP from rspamd 1.3) +bind_socket = "*:11445"; +allow_update = ["127.0.0.1", "::1"]; +# Number of processes to serve this storage (useful for read scaling) +count = 2; +# Backend ("sqlite" or "redis" - default "sqlite") +backend = "redis"; +# Hashes storage time (3 months) +expire = 90d; +# Synchronize updates to the storage each minute +sync = 1min; + diff --git a/data/conf/rspamd/override.d/worker-proxy.inc b/data/conf/rspamd/override.d/worker-proxy.inc index 0df926a7..92527f2b 100644 --- a/data/conf/rspamd/override.d/worker-proxy.inc +++ b/data/conf/rspamd/override.d/worker-proxy.inc @@ -1,6 +1,6 @@ bind_socket = "rspamd:9900"; milter = true; -upstream { +upstream "local" { name = "localhost"; default = true; hosts = "rspamd:11333" diff --git a/data/conf/rspamd/plugins.d/README.md b/data/conf/rspamd/plugins.d/README.md new file mode 100644 index 00000000..1516cf2d --- /dev/null +++ b/data/conf/rspamd/plugins.d/README.md @@ -0,0 +1 @@ +This is where you should copy any rspamd custom module diff --git a/data/conf/rspamd/rspamd.conf.local b/data/conf/rspamd/rspamd.conf.local new file mode 100644 index 00000000..9f2f8f1d --- /dev/null +++ b/data/conf/rspamd/rspamd.conf.local @@ -0,0 +1 @@ +# rspamd.conf.local diff --git a/data/conf/rspamd/rspamd.conf.override b/data/conf/rspamd/rspamd.conf.override new file mode 100644 index 00000000..d033e8e2 --- /dev/null +++ b/data/conf/rspamd/rspamd.conf.override @@ -0,0 +1,2 @@ +# rspamd.conf.override + diff --git a/data/conf/sogo/sogo.conf b/data/conf/sogo/sogo.conf index aa1a86ec..f9e9e077 100644 --- a/data/conf/sogo/sogo.conf +++ b/data/conf/sogo/sogo.conf @@ -26,7 +26,6 @@ // (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"; diff --git a/data/conf/unbound/unbound.conf b/data/conf/unbound/unbound.conf index e2e3e9eb..f67729b3 100644 --- a/data/conf/unbound/unbound.conf +++ b/data/conf/unbound/unbound.conf @@ -32,6 +32,7 @@ server: hide-version: yes max-udp-size: 4096 msg-buffer-size: 65552 + unwanted-reply-threshold: 10000 remote-control: control-enable: yes diff --git a/data/web/admin.php b/data/web/admin.php index 6ca89e97..5c79aab4 100644 --- a/data/web/admin.php +++ b/data/web/admin.php @@ -5,6 +5,9 @@ if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == "admi require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php'; $_SESSION['return_to'] = $_SERVER['REQUEST_URI']; $tfa_data = get_tfa(); +if (!isset($_SESSION['gal']) && $license_cache = $redis->Get('LICENSE_STATUS_CACHE')) { + $_SESSION['gal'] = json_decode($license_cache, true); +} ?> <div class="container"> @@ -76,8 +79,40 @@ $tfa_data = get_tfa(); </select> </div> </div> - <legend data-target="#api" style="margin-top:40px;cursor:pointer" class="arrow-toggle" unselectable="on" data-toggle="collapse"> - <span style="font-size:12px" class="arrow rotate glyphicon glyphicon-menu-down"></span> API (experimental, work in progress) + + <legend data-target="#license" class="arrow-toggle" unselectable="on" data-toggle="collapse"> + <span style="font-size:12px" class="arrow rotate glyphicon glyphicon-menu-down"></span> <?=$lang['admin']['guid_and_license'];?> + </legend> + <div id="license" class="collapse in"> + <form class="form-horizontal" autocapitalize="none" autocorrect="off" role="form" method="post"> + <div class="form-group"> + <label class="control-label col-sm-3" for="guid"><?=$lang['admin']['guid'];?>:</label> + <div class="col-sm-9"> + <div class="input-group"> + <span class="input-group-addon"> + <span class="glyphicon <?=(isset($_SESSION['gal']['valid']) && $_SESSION['gal']['valid'] === "true") ? 'glyphicon-heart text-danger' : 'glyphicon-remove';?>" aria-hidden="true"></span> + </span> + <input type="text" id="guid" class="form-control" value="<?=license('guid');?>" readonly> + </div> + <p class="help-block"> + <?=$lang['admin']['customer_id'];?>: <?=(isset($_SESSION['gal']['c'])) ? $_SESSION['gal']['c'] : '?';?> - + <?=$lang['admin']['service_id'];?>: <?=(isset($_SESSION['gal']['s'])) ? $_SESSION['gal']['s'] : '?';?> + </p> + </div> + </div> + <div class="form-group"> + <div class="col-sm-offset-3 col-sm-9"> + <p class="help-block"><?=$lang['admin']['license_info'];?></p> + <div class="btn-group"> + <button class="btn btn-sm btn-success" name="license_validate_now" type="submit" href="#"><?=$lang['admin']['validate_license_now'];?></button> + </div> + </div> + </div> + </form> + </div> + + <legend data-target="#api" class="arrow-toggle" unselectable="on" data-toggle="collapse"> + <span style="font-size:12px" class="arrow rotate glyphicon glyphicon-menu-down"></span> API </legend> <?php $api = admin_api('get'); @@ -105,6 +140,7 @@ $tfa_data = get_tfa(); </div> <div class="form-group"> <div class="col-sm-offset-3 col-sm-9"> + <p class="help-block"><?=$lang['admin']['api_info'];?></p> <div class="btn-group"> <button class="btn btn-default" name="admin_api" type="submit" href="#"><span class="glyphicon glyphicon-check"></span> <?=$lang['admin']['save'];?></button> <button class="btn btn-info" name="admin_api_regen_key" type="submit" href="#"><?=$lang['admin']['regen_api_key'];?></button> @@ -113,6 +149,7 @@ $tfa_data = get_tfa(); </div> </form> </div> + </div> </div> @@ -252,7 +289,7 @@ $tfa_data = get_tfa(); <form class="form" data-id="transport" role="form" method="post"> <div class="form-group"> <label for="destination"><?=$lang['admin']['destination'];?></label> - <input class="form-control input-sm" name="destination" placeholder='example.org, .example.org, *, box@example.org' required> + <input class="form-control input-sm" name="destination" placeholder='<?=$lang['admin']['transport_dest_format'];?>' required> </div> <div class="form-group"> <label for="nexthop"><?=$lang['admin']['nexthop'];?></label> @@ -266,6 +303,16 @@ $tfa_data = get_tfa(); <label for="password"><?=$lang['admin']['password'];?></label> <input class="form-control" name="password"> </div> + <!-- <div class="form-group"> + <label> + <input type="checkbox" name="lookup_mx" value="1"> <?=$lang['admin']['lookup_mx'];?> + </label> + </div> --> + <div class="form-group"> + <label> + <input type="checkbox" name="active" value="1"> <?=$lang['admin']['active'];?> + </label> + </div> <p class="help-block"><?=$lang['admin']['credentials_transport_warning'];?></p> <button class="btn btn-default" data-action="add_item" data-id="transport" data-api-url='add/transport' data-api-attr='{}' href="#"><span class="glyphicon glyphicon-plus"></span> <?=$lang['admin']['add'];?></button> </form> @@ -326,7 +373,7 @@ $tfa_data = get_tfa(); else { ?> <div class="row"> - <div class="col-md-1"><input class="dkim_missing" type="checkbox" data-id="dkim" name="multi_select" value="<?=$domain;?>" disabled /></div> + <div class="col-md-1"><input class="dkim_missing" type="checkbox" data-id="dkim" name="multi_select" value="<?=$domain;?>" disabled /></div> <div class="col-md-3"> <p><?=$lang['admin']['domain'];?>: <strong><?=htmlspecialchars($domain);?></strong><br /><span class="label label-danger"><?=$lang['admin']['dkim_key_missing'];?></span></p> </div> @@ -600,13 +647,14 @@ $tfa_data = get_tfa(); </span></p> <?php endforeach; + ?> + <hr> + <?php endif; if (!empty($f2b_data['perm_bans'])): foreach ($f2b_data['perm_bans'] as $perm_bans): ?> - <p> - <span class="label label-danger" style="padding:4px;font-size:85%;"><span class="glyphicon glyphicon-filter"></span> <?=$perm_bans?></span> - </p> + <span class="label label-danger" style="padding: 0.1em 0.4em 0.1em;"><span class="glyphicon glyphicon-filter"></span> <?=$perm_bans?></span> <?php endforeach; endif; @@ -621,30 +669,36 @@ $tfa_data = get_tfa(); <?php $q_data = quarantine('settings');?> <form class="form" data-id="quarantine" role="form" method="post"> <div class="row"> - <div class="col-sm-6"> + <div class="col-sm-4"> <div class="form-group"> <label for="retention_size"><?=$lang['admin']['quarantine_retention_size'];?></label> <input type="number" class="form-control" name="retention_size" value="<?=$q_data['retention_size'];?>" placeholder="0" required> </div> </div> - <div class="col-sm-6"> + <div class="col-sm-4"> <div class="form-group"> <label for="max_size"><?=$lang['admin']['quarantine_max_size'];?></label> <input type="number" class="form-control" name="max_size" value="<?=$q_data['max_size'];?>" placeholder="0" required> </div> </div> + <div class="col-sm-4"> + <div class="form-group"> + <label for="max_age"><?=$lang['admin']['quarantine_max_age'];?></label> + <input type="number" class="form-control" name="max_age" value="<?=$q_data['max_age'];?>" min="1" required> + </div> + </div> </div> <div class="row"> <div class="col-sm-6"> <div class="form-group"> <label for="sender"><?=$lang['admin']['quarantine_notification_sender'];?>:</label> - <input type="text" class="form-control" name="sender" value="<?=$q_data['sender'];?>" placeholder="quarantine@localhost"> + <input type="text" class="form-control" name="sender" value="<?=htmlspecialchars($q_data['sender']);?>" placeholder="quarantine@localhost"> </div> </div> <div class="col-sm-6"> <div class="form-group"> <label for="subject"><?=$lang['admin']['quarantine_notification_subject'];?>:</label> - <input type="text" class="form-control" name="subject" value="<?=$q_data['subject'];?>" placeholder="Spam Quarantine Notification"> + <input type="text" class="form-control" name="subject" value="<?=htmlspecialchars($q_data['subject']);?>" placeholder="Spam Quarantine Notification"> </div> </div> </div> @@ -699,13 +753,13 @@ $tfa_data = get_tfa(); <div class="col-sm-6"> <div class="form-group"> <label for="sender"><?=$lang['admin']['quarantine_notification_sender'];?>:</label> - <input type="text" class="form-control" name="sender" value="<?=$qw_data['sender'];?>" placeholder="quota-warning@localhost"> + <input type="text" class="form-control" name="sender" value="<?=htmlspecialchars($qw_data['sender']);?>" placeholder="quota-warning@localhost"> </div> </div> <div class="col-sm-6"> <div class="form-group"> <label for="subject"><?=$lang['admin']['quarantine_notification_subject'];?>:</label> - <input type="text" class="form-control" name="subject" value="<?=$qw_data['subject'];?>" placeholder="Quota warning"> + <input type="text" class="form-control" name="subject" value="<?=htmlspecialchars($qw_data['subject']);?>" placeholder="Quota warning"> </div> </div> </div> @@ -746,6 +800,7 @@ $tfa_data = get_tfa(); <div id="active_settings_map" class="collapse" > <textarea autocorrect="off" spellcheck="false" autocapitalize="none" class="form-control textarea-code" rows="20" name="settings_map" readonly><?=file_get_contents('http://nginx:8081/settings.php');?></textarea> </div> + <br> <?php $rsettings = rsettings('get'); ?> <form class="form" data-id="rsettings" role="form" method="post"> <div class="row"> @@ -796,11 +851,11 @@ $tfa_data = get_tfa(); <input type="hidden" name="active" value="0"> <div class="form-group"> <label for="desc"><?=$lang['admin']['rsetting_desc'];?>:</label> - <input type="text" class="form-control" name="desc" value="<?=$rsetting_details['desc'];?>"> + <input type="text" class="form-control" name="desc" value="<?=htmlspecialchars($rsetting_details['desc']);?>"> </div> <div class="form-group"> <label for="content"><?=$lang['admin']['rsetting_content'];?>:</label> - <textarea class="form-control" name="content" rows="10"><?=$rsetting_details['content'];?></textarea> + <textarea class="form-control" name="content" rows="10"><?=htmlspecialchars($rsetting_details['content']);?></textarea> </div> <div class="form-group"> <label> @@ -876,7 +931,7 @@ $tfa_data = get_tfa(); <td><input class="input-sm form-control" data-id="app_links" type="text" name="href" required value="<?=$val;?>"></td> <td><a href="#" role="button" class="btn btn-xs btn-default" type="button"><?=$lang['admin']['remove_row'];?></a></td> </tr> - <?php + <?php endforeach; } foreach ($MAILCOW_APPS as $app): diff --git a/data/web/css/build/001-bootstrap.min.css b/data/web/css/build/001-bootstrap.min.css index 9a1ea64d..769bf30c 100644 --- a/data/web/css/build/001-bootstrap.min.css +++ b/data/web/css/build/001-bootstrap.min.css @@ -1,10 +1,11 @@ - * bootswatch v3.3.7 +/*! + * bootswatch v3.4.1 * Homepage: http://bootswatch.com - * Copyright 2012-2017 Thomas Park + * Copyright 2012-2019 Thomas Park * Licensed under MIT * Based on Bootstrap *//*! - * Bootstrap v3.3.7 (http://getbootstrap.com) - * Copyright 2011-2016 Twitter, Inc. + * Bootstrap v3.4.1 (https://getbootstrap.com/) + * Copyright 2011-2019 Twitter, Inc. * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) - *//*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background-color:transparent}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:bold}dfn{font-style:italic}h1{font-size:2em;margin:0.67em 0}mark{background:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-0.5em}sub{bottom:-0.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;height:0}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace, monospace;font-size:1em}button,input,optgroup,select,textarea{color:inherit;font:inherit;margin:0}button{overflow:visible}button,select{text-transform:none}button,html input[type="button"],input[type="reset"],input[type="submit"]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}input{line-height:normal}input[type="checkbox"],input[type="radio"]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;padding:0}input[type="number"]::-webkit-inner-spin-button,input[type="number"]::-webkit-outer-spin-button{height:auto}input[type="search"]{-webkit-appearance:textfield;-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box}input[type="search"]::-webkit-search-cancel-button,input[type="search"]::-webkit-search-decoration{-webkit-appearance:none}fieldset{border:1px solid #c0c0c0;margin:0 2px;padding:0.35em 0.625em 0.75em}legend{border:0;padding:0}textarea{overflow:auto}optgroup{font-weight:bold}table{border-collapse:collapse;border-spacing:0}td,th{padding:0}/*! Source: https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css */@media print{*,*:before,*:after{background:transparent !important;color:#000 !important;-webkit-box-shadow:none !important;box-shadow:none !important;text-shadow:none !important}a,a:visited{text-decoration:underline}a[href]:after{content:" (" attr(href) ")"}abbr[title]:after{content:" (" attr(title) ")"}a[href^="#"]:after,a[href^="javascript:"]:after{content:""}pre,blockquote{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}tr,img{page-break-inside:avoid}img{max-width:100% !important}p,h2,h3{orphans:3;widows:3}h2,h3{page-break-after:avoid}.navbar{display:none}.btn>.caret,.dropup>.btn>.caret{border-top-color:#000 !important}.label{border:1px solid #000}.table{border-collapse:collapse !important}.table td,.table th{background-color:#fff !important}.table-bordered th,.table-bordered td{border:1px solid #ddd !important}}@font-face{font-family:'Glyphicons Halflings';src:url('/fonts/glyphicons-halflings-regular.eot');src:url('/fonts/glyphicons-halflings-regular.eot?#iefix') format('embedded-opentype'),url('/fonts/glyphicons-halflings-regular.woff2') format('woff2'),url('/fonts/glyphicons-halflings-regular.woff') format('woff'),url('/fonts/glyphicons-halflings-regular.ttf') format('truetype'),url('/fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular') format('svg')}.glyphicon{position:relative;top:1px;display:inline-block;font-family:'Glyphicons Halflings';font-style:normal;font-weight:normal;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.glyphicon-asterisk:before{content:"\002a"}.glyphicon-plus:before{content:"\002b"}.glyphicon-euro:before,.glyphicon-eur:before{content:"\20ac"}.glyphicon-minus:before{content:"\2212"}.glyphicon-cloud:before{content:"\2601"}.glyphicon-envelope:before{content:"\2709"}.glyphicon-pencil:before{content:"\270f"}.glyphicon-glass:before{content:"\e001"}.glyphicon-music:before{content:"\e002"}.glyphicon-search:before{content:"\e003"}.glyphicon-heart:before{content:"\e005"}.glyphicon-star:before{content:"\e006"}.glyphicon-star-empty:before{content:"\e007"}.glyphicon-user:before{content:"\e008"}.glyphicon-film:before{content:"\e009"}.glyphicon-th-large:before{content:"\e010"}.glyphicon-th:before{content:"\e011"}.glyphicon-th-list:before{content:"\e012"}.glyphicon-ok:before{content:"\e013"}.glyphicon-remove:before{content:"\e014"}.glyphicon-zoom-in:before{content:"\e015"}.glyphicon-zoom-out:before{content:"\e016"}.glyphicon-off:before{content:"\e017"}.glyphicon-signal:before{content:"\e018"}.glyphicon-cog:before{content:"\e019"}.glyphicon-trash:before{content:"\e020"}.glyphicon-home:before{content:"\e021"}.glyphicon-file:before{content:"\e022"}.glyphicon-time:before{content:"\e023"}.glyphicon-road:before{content:"\e024"}.glyphicon-download-alt:before{content:"\e025"}.glyphicon-download:before{content:"\e026"}.glyphicon-upload:before{content:"\e027"}.glyphicon-inbox:before{content:"\e028"}.glyphicon-play-circle:before{content:"\e029"}.glyphicon-repeat:before{content:"\e030"}.glyphicon-refresh:before{content:"\e031"}.glyphicon-list-alt:before{content:"\e032"}.glyphicon-lock:before{content:"\e033"}.glyphicon-flag:before{content:"\e034"}.glyphicon-headphones:before{content:"\e035"}.glyphicon-volume-off:before{content:"\e036"}.glyphicon-volume-down:before{content:"\e037"}.glyphicon-volume-up:before{content:"\e038"}.glyphicon-qrcode:before{content:"\e039"}.glyphicon-barcode:before{content:"\e040"}.glyphicon-tag:before{content:"\e041"}.glyphicon-tags:before{content:"\e042"}.glyphicon-book:before{content:"\e043"}.glyphicon-bookmark:before{content:"\e044"}.glyphicon-print:before{content:"\e045"}.glyphicon-camera:before{content:"\e046"}.glyphicon-font:before{content:"\e047"}.glyphicon-bold:before{content:"\e048"}.glyphicon-italic:before{content:"\e049"}.glyphicon-text-height:before{content:"\e050"}.glyphicon-text-width:before{content:"\e051"}.glyphicon-align-left:before{content:"\e052"}.glyphicon-align-center:before{content:"\e053"}.glyphicon-align-right:before{content:"\e054"}.glyphicon-align-justify:before{content:"\e055"}.glyphicon-list:before{content:"\e056"}.glyphicon-indent-left:before{content:"\e057"}.glyphicon-indent-right:before{content:"\e058"}.glyphicon-facetime-video:before{content:"\e059"}.glyphicon-picture:before{content:"\e060"}.glyphicon-map-marker:before{content:"\e062"}.glyphicon-adjust:before{content:"\e063"}.glyphicon-tint:before{content:"\e064"}.glyphicon-edit:before{content:"\e065"}.glyphicon-share:before{content:"\e066"}.glyphicon-check:before{content:"\e067"}.glyphicon-move:before{content:"\e068"}.glyphicon-step-backward:before{content:"\e069"}.glyphicon-fast-backward:before{content:"\e070"}.glyphicon-backward:before{content:"\e071"}.glyphicon-play:before{content:"\e072"}.glyphicon-pause:before{content:"\e073"}.glyphicon-stop:before{content:"\e074"}.glyphicon-forward:before{content:"\e075"}.glyphicon-fast-forward:before{content:"\e076"}.glyphicon-step-forward:before{content:"\e077"}.glyphicon-eject:before{content:"\e078"}.glyphicon-chevron-left:before{content:"\e079"}.glyphicon-chevron-right:before{content:"\e080"}.glyphicon-plus-sign:before{content:"\e081"}.glyphicon-minus-sign:before{content:"\e082"}.glyphicon-remove-sign:before{content:"\e083"}.glyphicon-ok-sign:before{content:"\e084"}.glyphicon-question-sign:before{content:"\e085"}.glyphicon-info-sign:before{content:"\e086"}.glyphicon-screenshot:before{content:"\e087"}.glyphicon-remove-circle:before{content:"\e088"}.glyphicon-ok-circle:before{content:"\e089"}.glyphicon-ban-circle:before{content:"\e090"}.glyphicon-arrow-left:before{content:"\e091"}.glyphicon-arrow-right:before{content:"\e092"}.glyphicon-arrow-up:before{content:"\e093"}.glyphicon-arrow-down:before{content:"\e094"}.glyphicon-share-alt:before{content:"\e095"}.glyphicon-resize-full:before{content:"\e096"}.glyphicon-resize-small:before{content:"\e097"}.glyphicon-exclamation-sign:before{content:"\e101"}.glyphicon-gift:before{content:"\e102"}.glyphicon-leaf:before{content:"\e103"}.glyphicon-fire:before{content:"\e104"}.glyphicon-eye-open:before{content:"\e105"}.glyphicon-eye-close:before{content:"\e106"}.glyphicon-warning-sign:before{content:"\e107"}.glyphicon-plane:before{content:"\e108"}.glyphicon-calendar:before{content:"\e109"}.glyphicon-random:before{content:"\e110"}.glyphicon-comment:before{content:"\e111"}.glyphicon-magnet:before{content:"\e112"}.glyphicon-chevron-up:before{content:"\e113"}.glyphicon-chevron-down:before{content:"\e114"}.glyphicon-retweet:before{content:"\e115"}.glyphicon-shopping-cart:before{content:"\e116"}.glyphicon-folder-close:before{content:"\e117"}.glyphicon-folder-open:before{content:"\e118"}.glyphicon-resize-vertical:before{content:"\e119"}.glyphicon-resize-horizontal:before{content:"\e120"}.glyphicon-hdd:before{content:"\e121"}.glyphicon-bullhorn:before{content:"\e122"}.glyphicon-bell:before{content:"\e123"}.glyphicon-certificate:before{content:"\e124"}.glyphicon-thumbs-up:before{content:"\e125"}.glyphicon-thumbs-down:before{content:"\e126"}.glyphicon-hand-right:before{content:"\e127"}.glyphicon-hand-left:before{content:"\e128"}.glyphicon-hand-up:before{content:"\e129"}.glyphicon-hand-down:before{content:"\e130"}.glyphicon-circle-arrow-right:before{content:"\e131"}.glyphicon-circle-arrow-left:before{content:"\e132"}.glyphicon-circle-arrow-up:before{content:"\e133"}.glyphicon-circle-arrow-down:before{content:"\e134"}.glyphicon-globe:before{content:"\e135"}.glyphicon-wrench:before{content:"\e136"}.glyphicon-tasks:before{content:"\e137"}.glyphicon-filter:before{content:"\e138"}.glyphicon-briefcase:before{content:"\e139"}.glyphicon-fullscreen:before{content:"\e140"}.glyphicon-dashboard:before{content:"\e141"}.glyphicon-paperclip:before{content:"\e142"}.glyphicon-heart-empty:before{content:"\e143"}.glyphicon-link:before{content:"\e144"}.glyphicon-phone:before{content:"\e145"}.glyphicon-pushpin:before{content:"\e146"}.glyphicon-usd:before{content:"\e148"}.glyphicon-gbp:before{content:"\e149"}.glyphicon-sort:before{content:"\e150"}.glyphicon-sort-by-alphabet:before{content:"\e151"}.glyphicon-sort-by-alphabet-alt:before{content:"\e152"}.glyphicon-sort-by-order:before{content:"\e153"}.glyphicon-sort-by-order-alt:before{content:"\e154"}.glyphicon-sort-by-attributes:before{content:"\e155"}.glyphicon-sort-by-attributes-alt:before{content:"\e156"}.glyphicon-unchecked:before{content:"\e157"}.glyphicon-expand:before{content:"\e158"}.glyphicon-collapse-down:before{content:"\e159"}.glyphicon-collapse-up:before{content:"\e160"}.glyphicon-log-in:before{content:"\e161"}.glyphicon-flash:before{content:"\e162"}.glyphicon-log-out:before{content:"\e163"}.glyphicon-new-window:before{content:"\e164"}.glyphicon-record:before{content:"\e165"}.glyphicon-save:before{content:"\e166"}.glyphicon-open:before{content:"\e167"}.glyphicon-saved:before{content:"\e168"}.glyphicon-import:before{content:"\e169"}.glyphicon-export:before{content:"\e170"}.glyphicon-send:before{content:"\e171"}.glyphicon-floppy-disk:before{content:"\e172"}.glyphicon-floppy-saved:before{content:"\e173"}.glyphicon-floppy-remove:before{content:"\e174"}.glyphicon-floppy-save:before{content:"\e175"}.glyphicon-floppy-open:before{content:"\e176"}.glyphicon-credit-card:before{content:"\e177"}.glyphicon-transfer:before{content:"\e178"}.glyphicon-cutlery:before{content:"\e179"}.glyphicon-header:before{content:"\e180"}.glyphicon-compressed:before{content:"\e181"}.glyphicon-earphone:before{content:"\e182"}.glyphicon-phone-alt:before{content:"\e183"}.glyphicon-tower:before{content:"\e184"}.glyphicon-stats:before{content:"\e185"}.glyphicon-sd-video:before{content:"\e186"}.glyphicon-hd-video:before{content:"\e187"}.glyphicon-subtitles:before{content:"\e188"}.glyphicon-sound-stereo:before{content:"\e189"}.glyphicon-sound-dolby:before{content:"\e190"}.glyphicon-sound-5-1:before{content:"\e191"}.glyphicon-sound-6-1:before{content:"\e192"}.glyphicon-sound-7-1:before{content:"\e193"}.glyphicon-copyright-mark:before{content:"\e194"}.glyphicon-registration-mark:before{content:"\e195"}.glyphicon-cloud-download:before{content:"\e197"}.glyphicon-cloud-upload:before{content:"\e198"}.glyphicon-tree-conifer:before{content:"\e199"}.glyphicon-tree-deciduous:before{content:"\e200"}.glyphicon-cd:before{content:"\e201"}.glyphicon-save-file:before{content:"\e202"}.glyphicon-open-file:before{content:"\e203"}.glyphicon-level-up:before{content:"\e204"}.glyphicon-copy:before{content:"\e205"}.glyphicon-paste:before{content:"\e206"}.glyphicon-alert:before{content:"\e209"}.glyphicon-equalizer:before{content:"\e210"}.glyphicon-king:before{content:"\e211"}.glyphicon-queen:before{content:"\e212"}.glyphicon-pawn:before{content:"\e213"}.glyphicon-bishop:before{content:"\e214"}.glyphicon-knight:before{content:"\e215"}.glyphicon-baby-formula:before{content:"\e216"}.glyphicon-tent:before{content:"\26fa"}.glyphicon-blackboard:before{content:"\e218"}.glyphicon-bed:before{content:"\e219"}.glyphicon-apple:before{content:"\f8ff"}.glyphicon-erase:before{content:"\e221"}.glyphicon-hourglass:before{content:"\231b"}.glyphicon-lamp:before{content:"\e223"}.glyphicon-duplicate:before{content:"\e224"}.glyphicon-piggy-bank:before{content:"\e225"}.glyphicon-scissors:before{content:"\e226"}.glyphicon-bitcoin:before{content:"\e227"}.glyphicon-btc:before{content:"\e227"}.glyphicon-xbt:before{content:"\e227"}.glyphicon-yen:before{content:"\00a5"}.glyphicon-jpy:before{content:"\00a5"}.glyphicon-ruble:before{content:"\20bd"}.glyphicon-rub:before{content:"\20bd"}.glyphicon-scale:before{content:"\e230"}.glyphicon-ice-lolly:before{content:"\e231"}.glyphicon-ice-lolly-tasted:before{content:"\e232"}.glyphicon-education:before{content:"\e233"}.glyphicon-option-horizontal:before{content:"\e234"}.glyphicon-option-vertical:before{content:"\e235"}.glyphicon-menu-hamburger:before{content:"\e236"}.glyphicon-modal-window:before{content:"\e237"}.glyphicon-oil:before{content:"\e238"}.glyphicon-grain:before{content:"\e239"}.glyphicon-sunglasses:before{content:"\e240"}.glyphicon-text-size:before{content:"\e241"}.glyphicon-text-color:before{content:"\e242"}.glyphicon-text-background:before{content:"\e243"}.glyphicon-object-align-top:before{content:"\e244"}.glyphicon-object-align-bottom:before{content:"\e245"}.glyphicon-object-align-horizontal:before{content:"\e246"}.glyphicon-object-align-left:before{content:"\e247"}.glyphicon-object-align-vertical:before{content:"\e248"}.glyphicon-object-align-right:before{content:"\e249"}.glyphicon-triangle-right:before{content:"\e250"}.glyphicon-triangle-left:before{content:"\e251"}.glyphicon-triangle-bottom:before{content:"\e252"}.glyphicon-triangle-top:before{content:"\e253"}.glyphicon-console:before{content:"\e254"}.glyphicon-superscript:before{content:"\e255"}.glyphicon-subscript:before{content:"\e256"}.glyphicon-menu-left:before{content:"\e257"}.glyphicon-menu-right:before{content:"\e258"}.glyphicon-menu-down:before{content:"\e259"}.glyphicon-menu-up:before{content:"\e260"}*{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}*:before,*:after{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:10px;-webkit-tap-highlight-color:rgba(0,0,0,0)}body{font-family:"Source Sans Pro","Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;line-height:1.42857143;color:#555555;background-color:#ffffff}input,button,select,textarea{font-family:inherit;font-size:inherit;line-height:inherit}a{color:#158cba;text-decoration:none}a:hover,a:focus{color:#158cba;text-decoration:underline}a:focus{outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}figure{margin:0}img{vertical-align:middle}.img-responsive,.thumbnail>img,.thumbnail a>img,.carousel-inner>.item>img,.carousel-inner>.item>a>img{display:block;max-width:100%;height:auto}.img-rounded{border-radius:5px}.img-thumbnail{padding:4px;line-height:1.42857143;background-color:#ffffff;border:1px solid #eeeeee;border-radius:4px;-webkit-transition:all .2s ease-in-out;-o-transition:all .2s ease-in-out;transition:all .2s ease-in-out;display:inline-block;max-width:100%;height:auto}.img-circle{border-radius:50%}hr{margin-top:20px;margin-bottom:20px;border:0;border-top:1px solid #eeeeee}.sr-only{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0, 0, 0, 0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}[role="button"]{cursor:pointer}h1,h2,h3,h4,h5,h6,.h1,.h2,.h3,.h4,.h5,.h6{font-family:inherit;font-weight:400;line-height:1.1;color:#333333}h1 small,h2 small,h3 small,h4 small,h5 small,h6 small,.h1 small,.h2 small,.h3 small,.h4 small,.h5 small,.h6 small,h1 .small,h2 .small,h3 .small,h4 .small,h5 .small,h6 .small,.h1 .small,.h2 .small,.h3 .small,.h4 .small,.h5 .small,.h6 .small{font-weight:normal;line-height:1;color:#999999}h1,.h1,h2,.h2,h3,.h3{margin-top:20px;margin-bottom:10px}h1 small,.h1 small,h2 small,.h2 small,h3 small,.h3 small,h1 .small,.h1 .small,h2 .small,.h2 .small,h3 .small,.h3 .small{font-size:65%}h4,.h4,h5,.h5,h6,.h6{margin-top:10px;margin-bottom:10px}h4 small,.h4 small,h5 small,.h5 small,h6 small,.h6 small,h4 .small,.h4 .small,h5 .small,.h5 .small,h6 .small,.h6 .small{font-size:75%}h1,.h1{font-size:36px}h2,.h2{font-size:30px}h3,.h3{font-size:24px}h4,.h4{font-size:18px}h5,.h5{font-size:14px}h6,.h6{font-size:12px}p{margin:0 0 10px}.lead{margin-bottom:20px;font-size:16px;font-weight:300;line-height:1.4}@media (min-width:768px){.lead{font-size:21px}}small,.small{font-size:85%}mark,.mark{background-color:#ff851b;padding:.2em}.text-left{text-align:left}.text-right{text-align:right}.text-center{text-align:center}.text-justify{text-align:justify}.text-nowrap{white-space:nowrap}.text-lowercase{text-transform:lowercase}.text-uppercase{text-transform:uppercase}.text-capitalize{text-transform:capitalize}.text-muted{color:#999999}.text-primary{color:#158cba}a.text-primary:hover,a.text-primary:focus{color:#106a8c}.text-success{color:#ffffff}a.text-success:hover,a.text-success:focus{color:#e6e6e6}.text-info{color:#ffffff}a.text-info:hover,a.text-info:focus{color:#e6e6e6}.text-warning{color:#ffffff}a.text-warning:hover,a.text-warning:focus{color:#e6e6e6}.text-danger{color:#ffffff}a.text-danger:hover,a.text-danger:focus{color:#e6e6e6}.bg-primary{color:#fff;background-color:#158cba}a.bg-primary:hover,a.bg-primary:focus{background-color:#106a8c}.bg-success{background-color:#28b62c}a.bg-success:hover,a.bg-success:focus{background-color:#1f8c22}.bg-info{background-color:#75caeb}a.bg-info:hover,a.bg-info:focus{background-color:#48b9e5}.bg-warning{background-color:#ff851b}a.bg-warning:hover,a.bg-warning:focus{background-color:#e76b00}.bg-danger{background-color:#ff4136}a.bg-danger:hover,a.bg-danger:focus{background-color:#ff1103}.page-header{padding-bottom:9px;margin:40px 0 20px;border-bottom:1px solid #eeeeee}ul,ol{margin-top:0;margin-bottom:10px}ul ul,ol ul,ul ol,ol ol{margin-bottom:0}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none;margin-left:-5px}.list-inline>li{display:inline-block;padding-left:5px;padding-right:5px}dl{margin-top:0;margin-bottom:20px}dt,dd{line-height:1.42857143}dt{font-weight:bold}dd{margin-left:0}@media (min-width:768px){.dl-horizontal dt{float:left;width:160px;clear:left;text-align:right;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dl-horizontal dd{margin-left:180px}}abbr[title],abbr[data-original-title]{cursor:help;border-bottom:1px dotted #999999}.initialism{font-size:90%;text-transform:uppercase}blockquote{padding:10px 20px;margin:0 0 20px;font-size:17.5px;border-left:5px solid #eeeeee}blockquote p:last-child,blockquote ul:last-child,blockquote ol:last-child{margin-bottom:0}blockquote footer,blockquote small,blockquote .small{display:block;font-size:80%;line-height:1.42857143;color:#999999}blockquote footer:before,blockquote small:before,blockquote .small:before{content:'\2014 \00A0'}.blockquote-reverse,blockquote.pull-right{padding-right:15px;padding-left:0;border-right:5px solid #eeeeee;border-left:0;text-align:right}.blockquote-reverse footer:before,blockquote.pull-right footer:before,.blockquote-reverse small:before,blockquote.pull-right small:before,.blockquote-reverse .small:before,blockquote.pull-right .small:before{content:''}.blockquote-reverse footer:after,blockquote.pull-right footer:after,.blockquote-reverse small:after,blockquote.pull-right small:after,.blockquote-reverse .small:after,blockquote.pull-right .small:after{content:'\00A0 \2014'}address{margin-bottom:20px;font-style:normal;line-height:1.42857143}code,kbd,pre,samp{font-family:Menlo,Monaco,Consolas,"Courier New",monospace}code{padding:2px 4px;font-size:90%;color:#c7254e;background-color:#f9f2f4;border-radius:4px}kbd{padding:2px 4px;font-size:90%;color:#ffffff;background-color:#333333;border-radius:2px;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,0.25);box-shadow:inset 0 -1px 0 rgba(0,0,0,0.25)}kbd kbd{padding:0;font-size:100%;font-weight:bold;-webkit-box-shadow:none;box-shadow:none}pre{display:block;padding:9.5px;margin:0 0 10px;font-size:13px;line-height:1.42857143;word-break:break-all;word-wrap:break-word;color:#333333;background-color:#f5f5f5;border:1px solid #cccccc;border-radius:4px}pre code{padding:0;font-size:inherit;color:inherit;white-space:pre-wrap;background-color:transparent;border-radius:0}.pre-scrollable{max-height:340px;overflow-y:scroll}.container{margin-right:auto;margin-left:auto;padding-left:15px;padding-right:15px}@media (min-width:768px){.container{width:750px}}@media (min-width:992px){.container{width:970px}}@media (min-width:1450px){.container{width:1170px}}.container-fluid{margin-right:auto;margin-left:auto;padding-left:15px;padding-right:15px}.row{margin-left:-15px;margin-right:-15px}.col-xs-1,.col-sm-1,.col-md-1,.col-lg-1,.col-xs-2,.col-sm-2,.col-md-2,.col-lg-2,.col-xs-3,.col-sm-3,.col-md-3,.col-lg-3,.col-xs-4,.col-sm-4,.col-md-4,.col-lg-4,.col-xs-5,.col-sm-5,.col-md-5,.col-lg-5,.col-xs-6,.col-sm-6,.col-md-6,.col-lg-6,.col-xs-7,.col-sm-7,.col-md-7,.col-lg-7,.col-xs-8,.col-sm-8,.col-md-8,.col-lg-8,.col-xs-9,.col-sm-9,.col-md-9,.col-lg-9,.col-xs-10,.col-sm-10,.col-md-10,.col-lg-10,.col-xs-11,.col-sm-11,.col-md-11,.col-lg-11,.col-xs-12,.col-sm-12,.col-md-12,.col-lg-12{position:relative;min-height:1px;padding-left:15px;padding-right:15px}.col-xs-1,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9,.col-xs-10,.col-xs-11,.col-xs-12{float:left}.col-xs-12{width:100%}.col-xs-11{width:91.66666667%}.col-xs-10{width:83.33333333%}.col-xs-9{width:75%}.col-xs-8{width:66.66666667%}.col-xs-7{width:58.33333333%}.col-xs-6{width:50%}.col-xs-5{width:41.66666667%}.col-xs-4{width:33.33333333%}.col-xs-3{width:25%}.col-xs-2{width:16.66666667%}.col-xs-1{width:8.33333333%}.col-xs-pull-12{right:100%}.col-xs-pull-11{right:91.66666667%}.col-xs-pull-10{right:83.33333333%}.col-xs-pull-9{right:75%}.col-xs-pull-8{right:66.66666667%}.col-xs-pull-7{right:58.33333333%}.col-xs-pull-6{right:50%}.col-xs-pull-5{right:41.66666667%}.col-xs-pull-4{right:33.33333333%}.col-xs-pull-3{right:25%}.col-xs-pull-2{right:16.66666667%}.col-xs-pull-1{right:8.33333333%}.col-xs-pull-0{right:auto}.col-xs-push-12{left:100%}.col-xs-push-11{left:91.66666667%}.col-xs-push-10{left:83.33333333%}.col-xs-push-9{left:75%}.col-xs-push-8{left:66.66666667%}.col-xs-push-7{left:58.33333333%}.col-xs-push-6{left:50%}.col-xs-push-5{left:41.66666667%}.col-xs-push-4{left:33.33333333%}.col-xs-push-3{left:25%}.col-xs-push-2{left:16.66666667%}.col-xs-push-1{left:8.33333333%}.col-xs-push-0{left:auto}.col-xs-offset-12{margin-left:100%}.col-xs-offset-11{margin-left:91.66666667%}.col-xs-offset-10{margin-left:83.33333333%}.col-xs-offset-9{margin-left:75%}.col-xs-offset-8{margin-left:66.66666667%}.col-xs-offset-7{margin-left:58.33333333%}.col-xs-offset-6{margin-left:50%}.col-xs-offset-5{margin-left:41.66666667%}.col-xs-offset-4{margin-left:33.33333333%}.col-xs-offset-3{margin-left:25%}.col-xs-offset-2{margin-left:16.66666667%}.col-xs-offset-1{margin-left:8.33333333%}.col-xs-offset-0{margin-left:0%}@media (min-width:768px){.col-sm-1,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-sm-10,.col-sm-11,.col-sm-12{float:left}.col-sm-12{width:100%}.col-sm-11{width:91.66666667%}.col-sm-10{width:83.33333333%}.col-sm-9{width:75%}.col-sm-8{width:66.66666667%}.col-sm-7{width:58.33333333%}.col-sm-6{width:50%}.col-sm-5{width:41.66666667%}.col-sm-4{width:33.33333333%}.col-sm-3{width:25%}.col-sm-2{width:16.66666667%}.col-sm-1{width:8.33333333%}.col-sm-pull-12{right:100%}.col-sm-pull-11{right:91.66666667%}.col-sm-pull-10{right:83.33333333%}.col-sm-pull-9{right:75%}.col-sm-pull-8{right:66.66666667%}.col-sm-pull-7{right:58.33333333%}.col-sm-pull-6{right:50%}.col-sm-pull-5{right:41.66666667%}.col-sm-pull-4{right:33.33333333%}.col-sm-pull-3{right:25%}.col-sm-pull-2{right:16.66666667%}.col-sm-pull-1{right:8.33333333%}.col-sm-pull-0{right:auto}.col-sm-push-12{left:100%}.col-sm-push-11{left:91.66666667%}.col-sm-push-10{left:83.33333333%}.col-sm-push-9{left:75%}.col-sm-push-8{left:66.66666667%}.col-sm-push-7{left:58.33333333%}.col-sm-push-6{left:50%}.col-sm-push-5{left:41.66666667%}.col-sm-push-4{left:33.33333333%}.col-sm-push-3{left:25%}.col-sm-push-2{left:16.66666667%}.col-sm-push-1{left:8.33333333%}.col-sm-push-0{left:auto}.col-sm-offset-12{margin-left:100%}.col-sm-offset-11{margin-left:91.66666667%}.col-sm-offset-10{margin-left:83.33333333%}.col-sm-offset-9{margin-left:75%}.col-sm-offset-8{margin-left:66.66666667%}.col-sm-offset-7{margin-left:58.33333333%}.col-sm-offset-6{margin-left:50%}.col-sm-offset-5{margin-left:41.66666667%}.col-sm-offset-4{margin-left:33.33333333%}.col-sm-offset-3{margin-left:25%}.col-sm-offset-2{margin-left:16.66666667%}.col-sm-offset-1{margin-left:8.33333333%}.col-sm-offset-0{margin-left:0%}}@media (min-width:992px){.col-md-1,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-md-10,.col-md-11,.col-md-12{float:left}.col-md-12{width:100%}.col-md-11{width:91.66666667%}.col-md-10{width:83.33333333%}.col-md-9{width:75%}.col-md-8{width:66.66666667%}.col-md-7{width:58.33333333%}.col-md-6{width:50%}.col-md-5{width:41.66666667%}.col-md-4{width:33.33333333%}.col-md-3{width:25%}.col-md-2{width:16.66666667%}.col-md-1{width:8.33333333%}.col-md-pull-12{right:100%}.col-md-pull-11{right:91.66666667%}.col-md-pull-10{right:83.33333333%}.col-md-pull-9{right:75%}.col-md-pull-8{right:66.66666667%}.col-md-pull-7{right:58.33333333%}.col-md-pull-6{right:50%}.col-md-pull-5{right:41.66666667%}.col-md-pull-4{right:33.33333333%}.col-md-pull-3{right:25%}.col-md-pull-2{right:16.66666667%}.col-md-pull-1{right:8.33333333%}.col-md-pull-0{right:auto}.col-md-push-12{left:100%}.col-md-push-11{left:91.66666667%}.col-md-push-10{left:83.33333333%}.col-md-push-9{left:75%}.col-md-push-8{left:66.66666667%}.col-md-push-7{left:58.33333333%}.col-md-push-6{left:50%}.col-md-push-5{left:41.66666667%}.col-md-push-4{left:33.33333333%}.col-md-push-3{left:25%}.col-md-push-2{left:16.66666667%}.col-md-push-1{left:8.33333333%}.col-md-push-0{left:auto}.col-md-offset-12{margin-left:100%}.col-md-offset-11{margin-left:91.66666667%}.col-md-offset-10{margin-left:83.33333333%}.col-md-offset-9{margin-left:75%}.col-md-offset-8{margin-left:66.66666667%}.col-md-offset-7{margin-left:58.33333333%}.col-md-offset-6{margin-left:50%}.col-md-offset-5{margin-left:41.66666667%}.col-md-offset-4{margin-left:33.33333333%}.col-md-offset-3{margin-left:25%}.col-md-offset-2{margin-left:16.66666667%}.col-md-offset-1{margin-left:8.33333333%}.col-md-offset-0{margin-left:0%}}@media (min-width:1450px){.col-lg-1,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-lg-10,.col-lg-11,.col-lg-12{float:left}.col-lg-12{width:100%}.col-lg-11{width:91.66666667%}.col-lg-10{width:83.33333333%}.col-lg-9{width:75%}.col-lg-8{width:66.66666667%}.col-lg-7{width:58.33333333%}.col-lg-6{width:50%}.col-lg-5{width:41.66666667%}.col-lg-4{width:33.33333333%}.col-lg-3{width:25%}.col-lg-2{width:16.66666667%}.col-lg-1{width:8.33333333%}.col-lg-pull-12{right:100%}.col-lg-pull-11{right:91.66666667%}.col-lg-pull-10{right:83.33333333%}.col-lg-pull-9{right:75%}.col-lg-pull-8{right:66.66666667%}.col-lg-pull-7{right:58.33333333%}.col-lg-pull-6{right:50%}.col-lg-pull-5{right:41.66666667%}.col-lg-pull-4{right:33.33333333%}.col-lg-pull-3{right:25%}.col-lg-pull-2{right:16.66666667%}.col-lg-pull-1{right:8.33333333%}.col-lg-pull-0{right:auto}.col-lg-push-12{left:100%}.col-lg-push-11{left:91.66666667%}.col-lg-push-10{left:83.33333333%}.col-lg-push-9{left:75%}.col-lg-push-8{left:66.66666667%}.col-lg-push-7{left:58.33333333%}.col-lg-push-6{left:50%}.col-lg-push-5{left:41.66666667%}.col-lg-push-4{left:33.33333333%}.col-lg-push-3{left:25%}.col-lg-push-2{left:16.66666667%}.col-lg-push-1{left:8.33333333%}.col-lg-push-0{left:auto}.col-lg-offset-12{margin-left:100%}.col-lg-offset-11{margin-left:91.66666667%}.col-lg-offset-10{margin-left:83.33333333%}.col-lg-offset-9{margin-left:75%}.col-lg-offset-8{margin-left:66.66666667%}.col-lg-offset-7{margin-left:58.33333333%}.col-lg-offset-6{margin-left:50%}.col-lg-offset-5{margin-left:41.66666667%}.col-lg-offset-4{margin-left:33.33333333%}.col-lg-offset-3{margin-left:25%}.col-lg-offset-2{margin-left:16.66666667%}.col-lg-offset-1{margin-left:8.33333333%}.col-lg-offset-0{margin-left:0%}}table{background-color:transparent}caption{padding-top:8px;padding-bottom:8px;color:#999999;text-align:left}th{text-align:left}.table{width:100%;max-width:100%;margin-bottom:20px}.table>thead>tr>th,.table>tbody>tr>th,.table>tfoot>tr>th,.table>thead>tr>td,.table>tbody>tr>td,.table>tfoot>tr>td{padding:8px;line-height:1.42857143;vertical-align:top;border-top:1px solid #eeeeee}.table>thead>tr>th{vertical-align:bottom;border-bottom:2px solid #eeeeee}.table>caption+thead>tr:first-child>th,.table>colgroup+thead>tr:first-child>th,.table>thead:first-child>tr:first-child>th,.table>caption+thead>tr:first-child>td,.table>colgroup+thead>tr:first-child>td,.table>thead:first-child>tr:first-child>td{border-top:0}.table>tbody+tbody{border-top:2px solid #eeeeee}.table .table{background-color:#ffffff}.table-condensed>thead>tr>th,.table-condensed>tbody>tr>th,.table-condensed>tfoot>tr>th,.table-condensed>thead>tr>td,.table-condensed>tbody>tr>td,.table-condensed>tfoot>tr>td{padding:5px}.table-bordered{border:1px solid #eeeeee}.table-bordered>thead>tr>th,.table-bordered>tbody>tr>th,.table-bordered>tfoot>tr>th,.table-bordered>thead>tr>td,.table-bordered>tbody>tr>td,.table-bordered>tfoot>tr>td{border:1px solid #eeeeee}.table-bordered>thead>tr>th,.table-bordered>thead>tr>td{border-bottom-width:2px}.table-striped>tbody>tr:nth-of-type(odd){background-color:#f9f9f9}.table-hover>tbody>tr:hover{background-color:#f5f5f5}table col[class*="col-"]{position:static;float:none;display:table-column}table td[class*="col-"],table th[class*="col-"]{position:static;float:none;display:table-cell}.table>thead>tr>td.active,.table>tbody>tr>td.active,.table>tfoot>tr>td.active,.table>thead>tr>th.active,.table>tbody>tr>th.active,.table>tfoot>tr>th.active,.table>thead>tr.active>td,.table>tbody>tr.active>td,.table>tfoot>tr.active>td,.table>thead>tr.active>th,.table>tbody>tr.active>th,.table>tfoot>tr.active>th{background-color:#f5f5f5}.table-hover>tbody>tr>td.active:hover,.table-hover>tbody>tr>th.active:hover,.table-hover>tbody>tr.active:hover>td,.table-hover>tbody>tr:hover>.active,.table-hover>tbody>tr.active:hover>th{background-color:#e8e8e8}.table>thead>tr>td.success,.table>tbody>tr>td.success,.table>tfoot>tr>td.success,.table>thead>tr>th.success,.table>tbody>tr>th.success,.table>tfoot>tr>th.success,.table>thead>tr.success>td,.table>tbody>tr.success>td,.table>tfoot>tr.success>td,.table>thead>tr.success>th,.table>tbody>tr.success>th,.table>tfoot>tr.success>th{background-color:#28b62c}.table-hover>tbody>tr>td.success:hover,.table-hover>tbody>tr>th.success:hover,.table-hover>tbody>tr.success:hover>td,.table-hover>tbody>tr:hover>.success,.table-hover>tbody>tr.success:hover>th{background-color:#23a127}.table>thead>tr>td.info,.table>tbody>tr>td.info,.table>tfoot>tr>td.info,.table>thead>tr>th.info,.table>tbody>tr>th.info,.table>tfoot>tr>th.info,.table>thead>tr.info>td,.table>tbody>tr.info>td,.table>tfoot>tr.info>td,.table>thead>tr.info>th,.table>tbody>tr.info>th,.table>tfoot>tr.info>th{background-color:#75caeb}.table-hover>tbody>tr>td.info:hover,.table-hover>tbody>tr>th.info:hover,.table-hover>tbody>tr.info:hover>td,.table-hover>tbody>tr:hover>.info,.table-hover>tbody>tr.info:hover>th{background-color:#5fc1e8}.table>thead>tr>td.warning,.table>tbody>tr>td.warning,.table>tfoot>tr>td.warning,.table>thead>tr>th.warning,.table>tbody>tr>th.warning,.table>tfoot>tr>th.warning,.table>thead>tr.warning>td,.table>tbody>tr.warning>td,.table>tfoot>tr.warning>td,.table>thead>tr.warning>th,.table>tbody>tr.warning>th,.table>tfoot>tr.warning>th{background-color:#ff851b}.table-hover>tbody>tr>td.warning:hover,.table-hover>tbody>tr>th.warning:hover,.table-hover>tbody>tr.warning:hover>td,.table-hover>tbody>tr:hover>.warning,.table-hover>tbody>tr.warning:hover>th{background-color:#ff7701}.table>thead>tr>td.danger,.table>tbody>tr>td.danger,.table>tfoot>tr>td.danger,.table>thead>tr>th.danger,.table>tbody>tr>th.danger,.table>tfoot>tr>th.danger,.table>thead>tr.danger>td,.table>tbody>tr.danger>td,.table>tfoot>tr.danger>td,.table>thead>tr.danger>th,.table>tbody>tr.danger>th,.table>tfoot>tr.danger>th{background-color:#ff4136}.table-hover>tbody>tr>td.danger:hover,.table-hover>tbody>tr>th.danger:hover,.table-hover>tbody>tr.danger:hover>td,.table-hover>tbody>tr:hover>.danger,.table-hover>tbody>tr.danger:hover>th{background-color:#ff291c}.table-responsive{overflow-x:auto;min-height:0.01%}@media screen and (max-width:767px){.table-responsive{width:100%;margin-bottom:15px;overflow-y:hidden;-ms-overflow-style:-ms-autohiding-scrollbar;border:1px solid #eeeeee}.table-responsive>.table{margin-bottom:0}.table-responsive>.table>thead>tr>th,.table-responsive>.table>tbody>tr>th,.table-responsive>.table>tfoot>tr>th,.table-responsive>.table>thead>tr>td,.table-responsive>.table>tbody>tr>td,.table-responsive>.table>tfoot>tr>td{white-space:nowrap}.table-responsive>.table-bordered{border:0}.table-responsive>.table-bordered>thead>tr>th:first-child,.table-responsive>.table-bordered>tbody>tr>th:first-child,.table-responsive>.table-bordered>tfoot>tr>th:first-child,.table-responsive>.table-bordered>thead>tr>td:first-child,.table-responsive>.table-bordered>tbody>tr>td:first-child,.table-responsive>.table-bordered>tfoot>tr>td:first-child{border-left:0}.table-responsive>.table-bordered>thead>tr>th:last-child,.table-responsive>.table-bordered>tbody>tr>th:last-child,.table-responsive>.table-bordered>tfoot>tr>th:last-child,.table-responsive>.table-bordered>thead>tr>td:last-child,.table-responsive>.table-bordered>tbody>tr>td:last-child,.table-responsive>.table-bordered>tfoot>tr>td:last-child{border-right:0}.table-responsive>.table-bordered>tbody>tr:last-child>th,.table-responsive>.table-bordered>tfoot>tr:last-child>th,.table-responsive>.table-bordered>tbody>tr:last-child>td,.table-responsive>.table-bordered>tfoot>tr:last-child>td{border-bottom:0}}fieldset{padding:0;margin:0;border:0;min-width:0}legend{display:block;width:100%;padding:0;margin-bottom:20px;font-size:21px;line-height:inherit;color:#333333;border:0;border-bottom:1px solid #e5e5e5}label{display:inline-block;max-width:100%;margin-bottom:5px;font-weight:bold}input[type="search"]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}input[type="radio"],input[type="checkbox"]{margin:4px 0 0;margin-top:1px \9;line-height:normal}input[type="file"]{display:block}input[type="range"]{display:block;width:100%}select[multiple],select[size]{height:auto}input[type="file"]:focus,input[type="radio"]:focus,input[type="checkbox"]:focus{outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}output{display:block;padding-top:8px;font-size:14px;line-height:1.42857143;color:#555555}.form-control{display:block;width:100%;height:38px;padding:7px 12px;font-size:14px;line-height:1.42857143;color:#555555;background-color:#ffffff;background-image:none;border:1px solid #e7e7e7;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-webkit-transition:border-color ease-in-out .15s,-webkit-box-shadow ease-in-out .15s;-o-transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s}.form-control:focus{border-color:#66afe9;outline:0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 8px rgba(102,175,233,0.6);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 8px rgba(102,175,233,0.6)}.form-control::-moz-placeholder{color:#999999;opacity:1}.form-control:-ms-input-placeholder{color:#999999}.form-control::-webkit-input-placeholder{color:#999999}.form-control::-ms-expand{border:0;background-color:transparent}.form-control[disabled],.form-control[readonly],fieldset[disabled] .form-control{background-color:#eeeeee;opacity:1}.form-control[disabled],fieldset[disabled] .form-control{cursor:not-allowed}textarea.form-control{height:auto}input[type="search"]{-webkit-appearance:none}@media screen and (-webkit-min-device-pixel-ratio:0){input[type="date"].form-control,input[type="time"].form-control,input[type="datetime-local"].form-control,input[type="month"].form-control{line-height:38px}input[type="date"].input-sm,input[type="time"].input-sm,input[type="datetime-local"].input-sm,input[type="month"].input-sm,.input-group-sm input[type="date"],.input-group-sm input[type="time"],.input-group-sm input[type="datetime-local"],.input-group-sm input[type="month"]{line-height:28px}input[type="date"].input-lg,input[type="time"].input-lg,input[type="datetime-local"].input-lg,input[type="month"].input-lg,.input-group-lg input[type="date"],.input-group-lg input[type="time"],.input-group-lg input[type="datetime-local"],.input-group-lg input[type="month"]{line-height:52px}}.form-group{margin-bottom:15px}.radio,.checkbox{position:relative;display:block;margin-top:10px;margin-bottom:10px}.radio label,.checkbox label{min-height:20px;padding-left:20px;margin-bottom:0;font-weight:normal;cursor:pointer}.radio input[type="radio"],.radio-inline input[type="radio"],.checkbox input[type="checkbox"],.checkbox-inline input[type="checkbox"]{position:absolute;margin-left:-20px;margin-top:4px \9}.radio+.radio,.checkbox+.checkbox{margin-top:-5px}.radio-inline,.checkbox-inline{position:relative;display:inline-block;padding-left:20px;margin-bottom:0;vertical-align:middle;font-weight:normal;cursor:pointer}.radio-inline+.radio-inline,.checkbox-inline+.checkbox-inline{margin-top:0;margin-left:10px}input[type="radio"][disabled],input[type="checkbox"][disabled],input[type="radio"].disabled,input[type="checkbox"].disabled,fieldset[disabled] input[type="radio"],fieldset[disabled] input[type="checkbox"]{cursor:not-allowed}.radio-inline.disabled,.checkbox-inline.disabled,fieldset[disabled] .radio-inline,fieldset[disabled] .checkbox-inline{cursor:not-allowed}.radio.disabled label,.checkbox.disabled label,fieldset[disabled] .radio label,fieldset[disabled] .checkbox label{cursor:not-allowed}.form-control-static{padding-top:8px;padding-bottom:8px;margin-bottom:0;min-height:34px}.form-control-static.input-lg,.form-control-static.input-sm{padding-left:0;padding-right:0}.input-sm{height:28px;padding:4px 10px;font-size:12px;line-height:1.5;border-radius:2px}select.input-sm{height:28px;line-height:28px}textarea.input-sm,select[multiple].input-sm{height:auto}.form-group-sm .form-control{height:28px;padding:4px 10px;font-size:12px;line-height:1.5;border-radius:2px}.form-group-sm select.form-control{height:28px;line-height:28px}.form-group-sm textarea.form-control,.form-group-sm select[multiple].form-control{height:auto}.form-group-sm .form-control-static{height:28px;min-height:32px;padding:5px 10px;font-size:12px;line-height:1.5}.input-lg{height:52px;padding:13px 16px;font-size:18px;line-height:1.3333333;border-radius:5px}select.input-lg{height:52px;line-height:52px}textarea.input-lg,select[multiple].input-lg{height:auto}.form-group-lg .form-control{height:52px;padding:13px 16px;font-size:18px;line-height:1.3333333;border-radius:5px}.form-group-lg select.form-control{height:52px;line-height:52px}.form-group-lg textarea.form-control,.form-group-lg select[multiple].form-control{height:auto}.form-group-lg .form-control-static{height:52px;min-height:38px;padding:14px 16px;font-size:18px;line-height:1.3333333}.has-feedback{position:relative}.has-feedback .form-control{padding-right:47.5px}.form-control-feedback{position:absolute;top:0;right:0;z-index:2;display:block;width:38px;height:38px;line-height:38px;text-align:center;pointer-events:none}.input-lg+.form-control-feedback,.input-group-lg+.form-control-feedback,.form-group-lg .form-control+.form-control-feedback{width:52px;height:52px;line-height:52px}.input-sm+.form-control-feedback,.input-group-sm+.form-control-feedback,.form-group-sm .form-control+.form-control-feedback{width:28px;height:28px;line-height:28px}.has-success .help-block,.has-success .control-label,.has-success .radio,.has-success .checkbox,.has-success .radio-inline,.has-success .checkbox-inline,.has-success.radio label,.has-success.checkbox label,.has-success.radio-inline label,.has-success.checkbox-inline label{color:#ffffff}.has-success .form-control{border-color:#ffffff;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.has-success .form-control:focus{border-color:#e6e6e6;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #fff;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #fff}.has-success .input-group-addon{color:#ffffff;border-color:#ffffff;background-color:#28b62c}.has-success .form-control-feedback{color:#ffffff}.has-warning .help-block,.has-warning .control-label,.has-warning .radio,.has-warning .checkbox,.has-warning .radio-inline,.has-warning .checkbox-inline,.has-warning.radio label,.has-warning.checkbox label,.has-warning.radio-inline label,.has-warning.checkbox-inline label{color:#ffffff}.has-warning .form-control{border-color:#ffffff;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.has-warning .form-control:focus{border-color:#e6e6e6;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #fff;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #fff}.has-warning .input-group-addon{color:#ffffff;border-color:#ffffff;background-color:#ff851b}.has-warning .form-control-feedback{color:#ffffff}.has-error .help-block,.has-error .control-label,.has-error .radio,.has-error .checkbox,.has-error .radio-inline,.has-error .checkbox-inline,.has-error.radio label,.has-error.checkbox label,.has-error.radio-inline label,.has-error.checkbox-inline label{color:#ffffff}.has-error .form-control{border-color:#ffffff;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.has-error .form-control:focus{border-color:#e6e6e6;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #fff;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #fff}.has-error .input-group-addon{color:#ffffff;border-color:#ffffff;background-color:#ff4136}.has-error .form-control-feedback{color:#ffffff}.has-feedback label~.form-control-feedback{top:25px}.has-feedback label.sr-only~.form-control-feedback{top:0}.help-block{display:block;margin-top:5px;margin-bottom:10px;color:#959595}@media (min-width:768px){.form-inline .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .form-control-static{display:inline-block}.form-inline .input-group{display:inline-table;vertical-align:middle}.form-inline .input-group .input-group-addon,.form-inline .input-group .input-group-btn,.form-inline .input-group .form-control{width:auto}.form-inline .input-group>.form-control{width:100%}.form-inline .control-label{margin-bottom:0;vertical-align:middle}.form-inline .radio,.form-inline .checkbox{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.form-inline .radio label,.form-inline .checkbox label{padding-left:0}.form-inline .radio input[type="radio"],.form-inline .checkbox input[type="checkbox"]{position:relative;margin-left:0}.form-inline .has-feedback .form-control-feedback{top:0}}.form-horizontal .radio,.form-horizontal .checkbox,.form-horizontal .radio-inline,.form-horizontal .checkbox-inline{margin-top:0;margin-bottom:0;padding-top:8px}.form-horizontal .radio,.form-horizontal .checkbox{min-height:28px}.form-horizontal .form-group{margin-left:-15px;margin-right:-15px}@media (min-width:768px){.form-horizontal .control-label{text-align:right;margin-bottom:0;padding-top:8px}}.form-horizontal .has-feedback .form-control-feedback{right:15px}@media (min-width:768px){.form-horizontal .form-group-lg .control-label{padding-top:14px;font-size:18px}}@media (min-width:768px){.form-horizontal .form-group-sm .control-label{padding-top:5px;font-size:12px}}.btn{display:inline-block;margin-bottom:0;font-weight:normal;text-align:center;vertical-align:middle;-ms-touch-action:manipulation;touch-action:manipulation;cursor:pointer;background-image:none;border:1px solid transparent;white-space:nowrap;padding:7px 12px;font-size:14px;line-height:1.42857143;border-radius:4px;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.btn:focus,.btn:active:focus,.btn.active:focus,.btn.focus,.btn:active.focus,.btn.active.focus{outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}.btn:hover,.btn:focus,.btn.focus{color:#555555;text-decoration:none}.btn:active,.btn.active{outline:0;background-image:none;-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,0.125);box-shadow:inset 0 3px 5px rgba(0,0,0,0.125)}.btn.disabled,.btn[disabled],fieldset[disabled] .btn{cursor:not-allowed;opacity:0.65;filter:alpha(opacity=65);-webkit-box-shadow:none;box-shadow:none}a.btn.disabled,fieldset[disabled] a.btn{pointer-events:none}.btn-default{color:#555555;background-color:#eeeeee;border-color:#e2e2e2}.btn-default:focus,.btn-default.focus{color:#555555;background-color:#d5d5d5;border-color:#a2a2a2}.btn-default:hover{color:#555555;background-color:#d5d5d5;border-color:#c3c3c3}.btn-default:active,.btn-default.active,.open>.dropdown-toggle.btn-default{color:#555555;background-color:#d5d5d5;border-color:#c3c3c3}.btn-default:active:hover,.btn-default.active:hover,.open>.dropdown-toggle.btn-default:hover,.btn-default:active:focus,.btn-default.active:focus,.open>.dropdown-toggle.btn-default:focus,.btn-default:active.focus,.btn-default.active.focus,.open>.dropdown-toggle.btn-default.focus{color:#555555;background-color:#c3c3c3;border-color:#a2a2a2}.btn-default:active,.btn-default.active,.open>.dropdown-toggle.btn-default{background-image:none}.btn-default.disabled:hover,.btn-default[disabled]:hover,fieldset[disabled] .btn-default:hover,.btn-default.disabled:focus,.btn-default[disabled]:focus,fieldset[disabled] .btn-default:focus,.btn-default.disabled.focus,.btn-default[disabled].focus,fieldset[disabled] .btn-default.focus{background-color:#eeeeee;border-color:#e2e2e2}.btn-default .badge{color:#eeeeee;background-color:#555555}.btn-primary{color:#ffffff;background-color:#158cba;border-color:#127ba3}.btn-primary:focus,.btn-primary.focus{color:#ffffff;background-color:#106a8c;border-color:#052531}.btn-primary:hover{color:#ffffff;background-color:#106a8c;border-color:#0c516c}.btn-primary:active,.btn-primary.active,.open>.dropdown-toggle.btn-primary{color:#ffffff;background-color:#106a8c;border-color:#0c516c}.btn-primary:active:hover,.btn-primary.active:hover,.open>.dropdown-toggle.btn-primary:hover,.btn-primary:active:focus,.btn-primary.active:focus,.open>.dropdown-toggle.btn-primary:focus,.btn-primary:active.focus,.btn-primary.active.focus,.open>.dropdown-toggle.btn-primary.focus{color:#ffffff;background-color:#0c516c;border-color:#052531}.btn-primary:active,.btn-primary.active,.open>.dropdown-toggle.btn-primary{background-image:none}.btn-primary.disabled:hover,.btn-primary[disabled]:hover,fieldset[disabled] .btn-primary:hover,.btn-primary.disabled:focus,.btn-primary[disabled]:focus,fieldset[disabled] .btn-primary:focus,.btn-primary.disabled.focus,.btn-primary[disabled].focus,fieldset[disabled] .btn-primary.focus{background-color:#158cba;border-color:#127ba3}.btn-primary .badge{color:#158cba;background-color:#ffffff}.btn-success{color:#ffffff;background-color:#28b62c;border-color:#23a127}.btn-success:focus,.btn-success.focus{color:#ffffff;background-color:#1f8c22;border-color:#0c390e}.btn-success:hover{color:#ffffff;background-color:#1f8c22;border-color:#186f1b}.btn-success:active,.btn-success.active,.open>.dropdown-toggle.btn-success{color:#ffffff;background-color:#1f8c22;border-color:#186f1b}.btn-success:active:hover,.btn-success.active:hover,.open>.dropdown-toggle.btn-success:hover,.btn-success:active:focus,.btn-success.active:focus,.open>.dropdown-toggle.btn-success:focus,.btn-success:active.focus,.btn-success.active.focus,.open>.dropdown-toggle.btn-success.focus{color:#ffffff;background-color:#186f1b;border-color:#0c390e}.btn-success:active,.btn-success.active,.open>.dropdown-toggle.btn-success{background-image:none}.btn-success.disabled:hover,.btn-success[disabled]:hover,fieldset[disabled] .btn-success:hover,.btn-success.disabled:focus,.btn-success[disabled]:focus,fieldset[disabled] .btn-success:focus,.btn-success.disabled.focus,.btn-success[disabled].focus,fieldset[disabled] .btn-success.focus{background-color:#28b62c;border-color:#23a127}.btn-success .badge{color:#28b62c;background-color:#ffffff}.btn-info{color:#ffffff;background-color:#75caeb;border-color:#5fc1e8}.btn-info:focus,.btn-info.focus{color:#ffffff;background-color:#48b9e5;border-color:#1984ae}.btn-info:hover{color:#ffffff;background-color:#48b9e5;border-color:#29ade0}.btn-info:active,.btn-info.active,.open>.dropdown-toggle.btn-info{color:#ffffff;background-color:#48b9e5;border-color:#29ade0}.btn-info:active:hover,.btn-info.active:hover,.open>.dropdown-toggle.btn-info:hover,.btn-info:active:focus,.btn-info.active:focus,.open>.dropdown-toggle.btn-info:focus,.btn-info:active.focus,.btn-info.active.focus,.open>.dropdown-toggle.btn-info.focus{color:#ffffff;background-color:#29ade0;border-color:#1984ae}.btn-info:active,.btn-info.active,.open>.dropdown-toggle.btn-info{background-image:none}.btn-info.disabled:hover,.btn-info[disabled]:hover,fieldset[disabled] .btn-info:hover,.btn-info.disabled:focus,.btn-info[disabled]:focus,fieldset[disabled] .btn-info:focus,.btn-info.disabled.focus,.btn-info[disabled].focus,fieldset[disabled] .btn-info.focus{background-color:#75caeb;border-color:#5fc1e8}.btn-info .badge{color:#75caeb;background-color:#ffffff}.btn-warning{color:#ffffff;background-color:#ff851b;border-color:#ff7701}.btn-warning:focus,.btn-warning.focus{color:#ffffff;background-color:#e76b00;border-color:#813c00}.btn-warning:hover{color:#ffffff;background-color:#e76b00;border-color:#c35b00}.btn-warning:active,.btn-warning.active,.open>.dropdown-toggle.btn-warning{color:#ffffff;background-color:#e76b00;border-color:#c35b00}.btn-warning:active:hover,.btn-warning.active:hover,.open>.dropdown-toggle.btn-warning:hover,.btn-warning:active:focus,.btn-warning.active:focus,.open>.dropdown-toggle.btn-warning:focus,.btn-warning:active.focus,.btn-warning.active.focus,.open>.dropdown-toggle.btn-warning.focus{color:#ffffff;background-color:#c35b00;border-color:#813c00}.btn-warning:active,.btn-warning.active,.open>.dropdown-toggle.btn-warning{background-image:none}.btn-warning.disabled:hover,.btn-warning[disabled]:hover,fieldset[disabled] .btn-warning:hover,.btn-warning.disabled:focus,.btn-warning[disabled]:focus,fieldset[disabled] .btn-warning:focus,.btn-warning.disabled.focus,.btn-warning[disabled].focus,fieldset[disabled] .btn-warning.focus{background-color:#ff851b;border-color:#ff7701}.btn-warning .badge{color:#ff851b;background-color:#ffffff}.btn-danger{color:#ffffff;background-color:#ff4136;border-color:#ff291c}.btn-danger:focus,.btn-danger.focus{color:#ffffff;background-color:#ff1103;border-color:#9c0900}.btn-danger:hover{color:#ffffff;background-color:#ff1103;border-color:#de0c00}.btn-danger:active,.btn-danger.active,.open>.dropdown-toggle.btn-danger{color:#ffffff;background-color:#ff1103;border-color:#de0c00}.btn-danger:active:hover,.btn-danger.active:hover,.open>.dropdown-toggle.btn-danger:hover,.btn-danger:active:focus,.btn-danger.active:focus,.open>.dropdown-toggle.btn-danger:focus,.btn-danger:active.focus,.btn-danger.active.focus,.open>.dropdown-toggle.btn-danger.focus{color:#ffffff;background-color:#de0c00;border-color:#9c0900}.btn-danger:active,.btn-danger.active,.open>.dropdown-toggle.btn-danger{background-image:none}.btn-danger.disabled:hover,.btn-danger[disabled]:hover,fieldset[disabled] .btn-danger:hover,.btn-danger.disabled:focus,.btn-danger[disabled]:focus,fieldset[disabled] .btn-danger:focus,.btn-danger.disabled.focus,.btn-danger[disabled].focus,fieldset[disabled] .btn-danger.focus{background-color:#ff4136;border-color:#ff291c}.btn-danger .badge{color:#ff4136;background-color:#ffffff}.btn-link{color:#158cba;font-weight:normal;border-radius:0}.btn-link,.btn-link:active,.btn-link.active,.btn-link[disabled],fieldset[disabled] .btn-link{background-color:transparent;-webkit-box-shadow:none;box-shadow:none}.btn-link,.btn-link:hover,.btn-link:focus,.btn-link:active{border-color:transparent}.btn-link:hover,.btn-link:focus{color:#158cba;text-decoration:underline;background-color:transparent}.btn-link[disabled]:hover,fieldset[disabled] .btn-link:hover,.btn-link[disabled]:focus,fieldset[disabled] .btn-link:focus{color:#999999;text-decoration:none}.btn-lg,.btn-group-lg>.btn{padding:13px 16px;font-size:18px;line-height:1.3333333;border-radius:5px}.btn-sm,.btn-group-sm>.btn{padding:4px 10px;font-size:12px;line-height:1.5;border-radius:2px}.btn-xs,.btn-group-xs>.btn{padding:1px 5px;font-size:12px;line-height:1.5;border-radius:2px}.btn-block{display:block;width:100%}.btn-block+.btn-block{margin-top:5px}input[type="submit"].btn-block,input[type="reset"].btn-block,input[type="button"].btn-block{width:100%}.fade{opacity:0;-webkit-transition:opacity 0.15s linear;-o-transition:opacity 0.15s linear;transition:opacity 0.15s linear}.fade.in{opacity:1}.collapse{display:none}.collapse.in{display:block}tr.collapse.in{display:table-row}tbody.collapse.in{display:table-row-group}.collapsing{position:relative;height:0;overflow:hidden;-webkit-transition-property:height, visibility;-o-transition-property:height, visibility;transition-property:height, visibility;-webkit-transition-duration:0.35s;-o-transition-duration:0.35s;transition-duration:0.35s;-webkit-transition-timing-function:ease;-o-transition-timing-function:ease;transition-timing-function:ease}.caret{display:inline-block;width:0;height:0;margin-left:2px;vertical-align:middle;border-top:4px dashed;border-top:4px solid \9;border-right:4px solid transparent;border-left:4px solid transparent}.dropup,.dropdown{position:relative}.dropdown-toggle:focus{outline:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:160px;padding:5px 0;margin:2px 0 0;list-style:none;font-size:14px;text-align:left;background-color:#ffffff;border:1px solid #cccccc;border:1px solid #e7e7e7;border-radius:4px;-webkit-box-shadow:0 6px 12px rgba(0,0,0,0.175);box-shadow:0 6px 12px rgba(0,0,0,0.175);-webkit-background-clip:padding-box;background-clip:padding-box}.dropdown-menu.pull-right{right:0;left:auto}.dropdown-menu .divider{height:1px;margin:9px 0;overflow:hidden;background-color:#eeeeee}.dropdown-menu>li>a{display:block;padding:3px 20px;clear:both;font-weight:normal;line-height:1.42857143;color:#999999;white-space:nowrap}.dropdown-menu>li>a:hover,.dropdown-menu>li>a:focus{text-decoration:none;color:#333333;background-color:transparent}.dropdown-menu>.active>a,.dropdown-menu>.active>a:hover,.dropdown-menu>.active>a:focus{color:#ffffff;text-decoration:none;outline:0;background-color:#158cba}.dropdown-menu>.disabled>a,.dropdown-menu>.disabled>a:hover,.dropdown-menu>.disabled>a:focus{color:#eeeeee}.dropdown-menu>.disabled>a:hover,.dropdown-menu>.disabled>a:focus{text-decoration:none;background-color:transparent;background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);cursor:not-allowed}.open>.dropdown-menu{display:block}.open>a{outline:0}.dropdown-menu-right{left:auto;right:0}.dropdown-menu-left{left:0;right:auto}.dropdown-header{display:block;padding:3px 20px;font-size:12px;line-height:1.42857143;color:#999999;white-space:nowrap}.dropdown-backdrop{position:fixed;left:0;right:0;bottom:0;top:0;z-index:990}.pull-right>.dropdown-menu{right:0;left:auto}.dropup .caret,.navbar-fixed-bottom .dropdown .caret{border-top:0;border-bottom:4px dashed;border-bottom:4px solid \9;content:""}.dropup .dropdown-menu,.navbar-fixed-bottom .dropdown .dropdown-menu{top:auto;bottom:100%;margin-bottom:2px}@media (min-width:768px){.navbar-right .dropdown-menu{left:auto;right:0}.navbar-right .dropdown-menu-left{left:0;right:auto}}.btn-group,.btn-group-vertical{position:relative;display:inline-block;vertical-align:middle}.btn-group>.btn,.btn-group-vertical>.btn{position:relative;float:left}.btn-group>.btn:hover,.btn-group-vertical>.btn:hover,.btn-group>.btn:focus,.btn-group-vertical>.btn:focus,.btn-group>.btn:active,.btn-group-vertical>.btn:active,.btn-group>.btn.active,.btn-group-vertical>.btn.active{z-index:2}.btn-group .btn+.btn,.btn-group .btn+.btn-group,.btn-group .btn-group+.btn,.btn-group .btn-group+.btn-group{margin-left:-1px}.btn-toolbar{margin-left:-5px}.btn-toolbar .btn,.btn-toolbar .btn-group,.btn-toolbar .input-group{float:left}.btn-toolbar>.btn,.btn-toolbar>.btn-group,.btn-toolbar>.input-group{margin-left:5px}.btn-group>.btn:not(:first-child):not(:last-child):not(.dropdown-toggle){border-radius:0}.btn-group>.btn:first-child{margin-left:0}.btn-group>.btn:first-child:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-top-right-radius:0}.btn-group>.btn:last-child:not(:first-child),.btn-group>.dropdown-toggle:not(:first-child){border-bottom-left-radius:0;border-top-left-radius:0}.btn-group>.btn-group{float:left}.btn-group>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-bottom-right-radius:0;border-top-right-radius:0}.btn-group>.btn-group:last-child:not(:first-child)>.btn:first-child{border-bottom-left-radius:0;border-top-left-radius:0}.btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle{outline:0}.btn-group>.btn+.dropdown-toggle{padding-left:8px;padding-right:8px}.btn-group>.btn-lg+.dropdown-toggle{padding-left:12px;padding-right:12px}.btn-group.open .dropdown-toggle{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,0.125);box-shadow:inset 0 3px 5px rgba(0,0,0,0.125)}.btn-group.open .dropdown-toggle.btn-link{-webkit-box-shadow:none;box-shadow:none}.btn .caret{margin-left:0}.btn-lg .caret{border-width:5px 5px 0;border-bottom-width:0}.dropup .btn-lg .caret{border-width:0 5px 5px}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group,.btn-group-vertical>.btn-group>.btn{display:block;float:none;width:100%;max-width:100%}.btn-group-vertical>.btn-group>.btn{float:none}.btn-group-vertical>.btn+.btn,.btn-group-vertical>.btn+.btn-group,.btn-group-vertical>.btn-group+.btn,.btn-group-vertical>.btn-group+.btn-group{margin-top:-1px;margin-left:0}.btn-group-vertical>.btn:not(:first-child):not(:last-child){border-radius:0}.btn-group-vertical>.btn:first-child:not(:last-child){border-top-right-radius:4px;border-top-left-radius:4px;border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn:last-child:not(:first-child){border-top-right-radius:0;border-top-left-radius:0;border-bottom-right-radius:4px;border-bottom-left-radius:4px}.btn-group-vertical>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group-vertical>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group-vertical>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:last-child:not(:first-child)>.btn:first-child{border-top-right-radius:0;border-top-left-radius:0}.btn-group-justified{display:table;width:100%;table-layout:fixed;border-collapse:separate}.btn-group-justified>.btn,.btn-group-justified>.btn-group{float:none;display:table-cell;width:1%}.btn-group-justified>.btn-group .btn{width:100%}.btn-group-justified>.btn-group .dropdown-menu{left:auto}[data-toggle="buttons"]>.btn input[type="radio"],[data-toggle="buttons"]>.btn-group>.btn input[type="radio"],[data-toggle="buttons"]>.btn input[type="checkbox"],[data-toggle="buttons"]>.btn-group>.btn input[type="checkbox"]{position:absolute;clip:rect(0, 0, 0, 0);pointer-events:none}.input-group{position:relative;display:table;border-collapse:separate}.input-group[class*="col-"]{float:none;padding-left:0;padding-right:0}.input-group .form-control{position:relative;z-index:2;float:left;width:100%;margin-bottom:0}.input-group .form-control:focus{z-index:3}.input-group-lg>.form-control,.input-group-lg>.input-group-addon,.input-group-lg>.input-group-btn>.btn{height:52px;padding:13px 16px;font-size:18px;line-height:1.3333333;border-radius:5px}select.input-group-lg>.form-control,select.input-group-lg>.input-group-addon,select.input-group-lg>.input-group-btn>.btn{height:52px;line-height:52px}textarea.input-group-lg>.form-control,textarea.input-group-lg>.input-group-addon,textarea.input-group-lg>.input-group-btn>.btn,select[multiple].input-group-lg>.form-control,select[multiple].input-group-lg>.input-group-addon,select[multiple].input-group-lg>.input-group-btn>.btn{height:auto}.input-group-sm>.form-control,.input-group-sm>.input-group-addon,.input-group-sm>.input-group-btn>.btn{height:28px;padding:4px 10px;font-size:12px;line-height:1.5;border-radius:2px}select.input-group-sm>.form-control,select.input-group-sm>.input-group-addon,select.input-group-sm>.input-group-btn>.btn{height:28px;line-height:28px}textarea.input-group-sm>.form-control,textarea.input-group-sm>.input-group-addon,textarea.input-group-sm>.input-group-btn>.btn,select[multiple].input-group-sm>.form-control,select[multiple].input-group-sm>.input-group-addon,select[multiple].input-group-sm>.input-group-btn>.btn{height:auto}.input-group-addon,.input-group-btn,.input-group .form-control{display:table-cell}.input-group-addon:not(:first-child):not(:last-child),.input-group-btn:not(:first-child):not(:last-child),.input-group .form-control:not(:first-child):not(:last-child){border-radius:0}.input-group-addon,.input-group-btn{width:1%;white-space:nowrap;vertical-align:middle}.input-group-addon{padding:7px 12px;font-size:14px;font-weight:normal;line-height:1;color:#555555;text-align:center;background-color:#eeeeee;border:1px solid #e7e7e7;border-radius:4px}.input-group-addon.input-sm{padding:4px 10px;font-size:12px;border-radius:2px}.input-group-addon.input-lg{padding:13px 16px;font-size:18px;border-radius:5px}.input-group-addon input[type="radio"],.input-group-addon input[type="checkbox"]{margin-top:0}.input-group .form-control:first-child,.input-group-addon:first-child,.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group>.btn,.input-group-btn:first-child>.dropdown-toggle,.input-group-btn:last-child>.btn:not(:last-child):not(.dropdown-toggle),.input-group-btn:last-child>.btn-group:not(:last-child)>.btn{border-bottom-right-radius:0;border-top-right-radius:0}.input-group-addon:first-child{border-right:0}.input-group .form-control:last-child,.input-group-addon:last-child,.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group>.btn,.input-group-btn:last-child>.dropdown-toggle,.input-group-btn:first-child>.btn:not(:first-child),.input-group-btn:first-child>.btn-group:not(:first-child)>.btn{border-bottom-left-radius:0;border-top-left-radius:0}.input-group-addon:last-child{border-left:0}.input-group-btn{position:relative;font-size:0;white-space:nowrap}.input-group-btn>.btn{position:relative}.input-group-btn>.btn+.btn{margin-left:-1px}.input-group-btn>.btn:hover,.input-group-btn>.btn:focus,.input-group-btn>.btn:active{z-index:2}.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group{margin-right:-1px}.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group{z-index:2;margin-left:-1px}.nav{margin-bottom:0;padding-left:0;list-style:none}.nav>li{position:relative;display:block}.nav>li>a{position:relative;display:block;padding:10px 15px}.nav>li>a:hover,.nav>li>a:focus{text-decoration:none;background-color:#ffffff}.nav>li.disabled>a{color:#999999}.nav>li.disabled>a:hover,.nav>li.disabled>a:focus{color:#999999;text-decoration:none;background-color:transparent;cursor:not-allowed}.nav .open>a,.nav .open>a:hover,.nav .open>a:focus{background-color:#ffffff;border-color:#158cba}.nav .nav-divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.nav>li>a>img{max-width:none}.nav-tabs{border-bottom:1px solid #e7e7e7}.nav-tabs>li{float:left;margin-bottom:-1px}.nav-tabs>li>a{margin-right:2px;line-height:1.42857143;border:1px solid transparent;border-radius:4px 4px 0 0}.nav-tabs>li>a:hover{border-color:#eeeeee #eeeeee #e7e7e7}.nav-tabs>li.active>a,.nav-tabs>li.active>a:hover,.nav-tabs>li.active>a:focus{color:#555555;background-color:#ffffff;border:1px solid #e7e7e7;border-bottom-color:transparent;cursor:default}.nav-tabs.nav-justified{width:100%;border-bottom:0}.nav-tabs.nav-justified>li{float:none}.nav-tabs.nav-justified>li>a{text-align:center;margin-bottom:5px}.nav-tabs.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media (min-width:768px){.nav-tabs.nav-justified>li{display:table-cell;width:1%}.nav-tabs.nav-justified>li>a{margin-bottom:0}}.nav-tabs.nav-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:hover,.nav-tabs.nav-justified>.active>a:focus{border:1px solid #e7e7e7}@media (min-width:768px){.nav-tabs.nav-justified>li>a{border-bottom:1px solid #e7e7e7;border-radius:4px 4px 0 0}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:hover,.nav-tabs.nav-justified>.active>a:focus{border-bottom-color:#ffffff}}.nav-pills>li{float:left}.nav-pills>li>a{border-radius:4px}.nav-pills>li+li{margin-left:2px}.nav-pills>li.active>a,.nav-pills>li.active>a:hover,.nav-pills>li.active>a:focus{color:#ffffff;background-color:#158cba}.nav-stacked>li{float:none}.nav-stacked>li+li{margin-top:2px;margin-left:0}.nav-justified{width:100%}.nav-justified>li{float:none}.nav-justified>li>a{text-align:center;margin-bottom:5px}.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media (min-width:768px){.nav-justified>li{display:table-cell;width:1%}.nav-justified>li>a{margin-bottom:0}}.nav-tabs-justified{border-bottom:0}.nav-tabs-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:hover,.nav-tabs-justified>.active>a:focus{border:1px solid #e7e7e7}@media (min-width:768px){.nav-tabs-justified>li>a{border-bottom:1px solid #e7e7e7;border-radius:4px 4px 0 0}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:hover,.nav-tabs-justified>.active>a:focus{border-bottom-color:#ffffff}}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-right-radius:0;border-top-left-radius:0}.navbar{position:relative;min-height:50px;margin-bottom:20px;border:1px solid transparent}@media (min-width:768px){.navbar{border-radius:4px}}@media (min-width:768px){.navbar-header{float:left}}.navbar-collapse{overflow-x:visible;padding-right:15px;padding-left:15px;border-top:1px solid transparent;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.1);box-shadow:inset 0 1px 0 rgba(255,255,255,0.1);-webkit-overflow-scrolling:touch}.navbar-collapse.in{overflow-y:auto}@media (min-width:768px){.navbar-collapse{width:auto;border-top:0;-webkit-box-shadow:none;box-shadow:none}.navbar-collapse.collapse{display:block !important;height:auto !important;padding-bottom:0;overflow:visible !important}.navbar-collapse.in{overflow-y:visible}.navbar-fixed-top .navbar-collapse,.navbar-static-top .navbar-collapse,.navbar-fixed-bottom .navbar-collapse{padding-left:0;padding-right:0}}.navbar-fixed-top .navbar-collapse,.navbar-fixed-bottom .navbar-collapse{max-height:340px}@media (max-device-width:480px) and (orientation:landscape){.navbar-fixed-top .navbar-collapse,.navbar-fixed-bottom .navbar-collapse{max-height:200px}}.container>.navbar-header,.container-fluid>.navbar-header,.container>.navbar-collapse,.container-fluid>.navbar-collapse{margin-right:-15px;margin-left:-15px}@media (min-width:768px){.container>.navbar-header,.container-fluid>.navbar-header,.container>.navbar-collapse,.container-fluid>.navbar-collapse{margin-right:0;margin-left:0}}.navbar-static-top{z-index:1000;border-width:0 0 1px}@media (min-width:768px){.navbar-static-top{border-radius:0}}.navbar-fixed-top,.navbar-fixed-bottom{position:fixed;right:0;left:0;z-index:1030}@media (min-width:768px){.navbar-fixed-top,.navbar-fixed-bottom{border-radius:0}}.navbar-fixed-top{top:0;border-width:0 0 1px}.navbar-fixed-bottom{bottom:0;margin-bottom:0;border-width:1px 0 0}.navbar-brand{float:left;padding:15px 15px;font-size:18px;line-height:20px;height:50px}.navbar-brand:hover,.navbar-brand:focus{text-decoration:none}.navbar-brand>img{display:block}@media (min-width:768px){.navbar>.container .navbar-brand,.navbar>.container-fluid .navbar-brand{margin-left:-15px}}.navbar-toggle{position:relative;float:right;margin-right:15px;padding:9px 10px;margin-top:8px;margin-bottom:8px;background-color:transparent;background-image:none;border:1px solid transparent;border-radius:4px}.navbar-toggle:focus{outline:0}.navbar-toggle .icon-bar{display:block;width:22px;height:2px;border-radius:1px}.navbar-toggle .icon-bar+.icon-bar{margin-top:4px}@media (min-width:768px){.navbar-toggle{display:none}}.navbar-nav{margin:7.5px -15px}.navbar-nav>li>a{padding-top:10px;padding-bottom:10px;line-height:20px}@media (max-width:767px){.navbar-nav .open .dropdown-menu{position:static;float:none;width:auto;margin-top:0;background-color:transparent;border:0;-webkit-box-shadow:none;box-shadow:none}.navbar-nav .open .dropdown-menu>li>a,.navbar-nav .open .dropdown-menu .dropdown-header{padding:5px 15px 5px 25px}.navbar-nav .open .dropdown-menu>li>a{line-height:20px}.navbar-nav .open .dropdown-menu>li>a:hover,.navbar-nav .open .dropdown-menu>li>a:focus{background-image:none}}@media (min-width:768px){.navbar-nav{float:left;margin:0}.navbar-nav>li{float:left}.navbar-nav>li>a{padding-top:15px;padding-bottom:15px}}.navbar-form{margin-left:-15px;margin-right:-15px;padding:10px 15px;border-top:1px solid transparent;border-bottom:1px solid transparent;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1);box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1);margin-top:6px;margin-bottom:6px}@media (min-width:768px){.navbar-form .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.navbar-form .form-control{display:inline-block;width:auto;vertical-align:middle}.navbar-form .form-control-static{display:inline-block}.navbar-form .input-group{display:inline-table;vertical-align:middle}.navbar-form .input-group .input-group-addon,.navbar-form .input-group .input-group-btn,.navbar-form .input-group .form-control{width:auto}.navbar-form .input-group>.form-control{width:100%}.navbar-form .control-label{margin-bottom:0;vertical-align:middle}.navbar-form .radio,.navbar-form .checkbox{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.navbar-form .radio label,.navbar-form .checkbox label{padding-left:0}.navbar-form .radio input[type="radio"],.navbar-form .checkbox input[type="checkbox"]{position:relative;margin-left:0}.navbar-form .has-feedback .form-control-feedback{top:0}}@media (max-width:767px){.navbar-form .form-group{margin-bottom:5px}.navbar-form .form-group:last-child{margin-bottom:0}}@media (min-width:768px){.navbar-form{width:auto;border:0;margin-left:0;margin-right:0;padding-top:0;padding-bottom:0;-webkit-box-shadow:none;box-shadow:none}}.navbar-nav>li>.dropdown-menu{margin-top:0;border-top-right-radius:0;border-top-left-radius:0}.navbar-fixed-bottom .navbar-nav>li>.dropdown-menu{margin-bottom:0;border-top-right-radius:4px;border-top-left-radius:4px;border-bottom-right-radius:0;border-bottom-left-radius:0}.navbar-btn{margin-top:6px;margin-bottom:6px}.navbar-btn.btn-sm{margin-top:11px;margin-bottom:11px}.navbar-btn.btn-xs{margin-top:14px;margin-bottom:14px}.navbar-text{margin-top:15px;margin-bottom:15px}@media (min-width:768px){.navbar-text{float:left;margin-left:15px;margin-right:15px}}@media (min-width:768px){.navbar-left{float:left !important}.navbar-right{float:right !important;margin-right:-15px}.navbar-right~.navbar-right{margin-right:0}}.navbar-default{background-color:#f8f8f8;border-color:#e7e7e7}.navbar-default .navbar-brand{color:#333333}.navbar-default .navbar-brand:hover,.navbar-default .navbar-brand:focus{color:#333333;background-color:transparent}.navbar-default .navbar-text{color:#555555}.navbar-default .navbar-nav>li>a{color:#999999}.navbar-default .navbar-nav>li>a:hover,.navbar-default .navbar-nav>li>a:focus{color:#333333;background-color:transparent}.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.active>a:hover,.navbar-default .navbar-nav>.active>a:focus{color:#333333;background-color:transparent}.navbar-default .navbar-nav>.disabled>a,.navbar-default .navbar-nav>.disabled>a:hover,.navbar-default .navbar-nav>.disabled>a:focus{color:#eeeeee;background-color:transparent}.navbar-default .navbar-toggle{border-color:#eeeeee}.navbar-default .navbar-toggle:hover,.navbar-default .navbar-toggle:focus{background-color:#ffffff}.navbar-default .navbar-toggle .icon-bar{background-color:#999999}.navbar-default .navbar-collapse,.navbar-default .navbar-form{border-color:#e7e7e7}.navbar-default .navbar-nav>.open>a,.navbar-default .navbar-nav>.open>a:hover,.navbar-default .navbar-nav>.open>a:focus{background-color:transparent;color:#333333}@media (max-width:767px){.navbar-default .navbar-nav .open .dropdown-menu>li>a{color:#999999}.navbar-default .navbar-nav .open .dropdown-menu>li>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>li>a:focus{color:#333333;background-color:transparent}.navbar-default .navbar-nav .open .dropdown-menu>.active>a,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:focus{color:#333333;background-color:transparent}.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:focus{color:#eeeeee;background-color:transparent}}.navbar-default .navbar-link{color:#999999}.navbar-default .navbar-link:hover{color:#333333}.navbar-default .btn-link{color:#999999}.navbar-default .btn-link:hover,.navbar-default .btn-link:focus{color:#333333}.navbar-default .btn-link[disabled]:hover,fieldset[disabled] .navbar-default .btn-link:hover,.navbar-default .btn-link[disabled]:focus,fieldset[disabled] .navbar-default .btn-link:focus{color:#eeeeee}.navbar-inverse{background-color:#ffffff;border-color:#e6e6e6}.navbar-inverse .navbar-brand{color:#999999}.navbar-inverse .navbar-brand:hover,.navbar-inverse .navbar-brand:focus{color:#333333;background-color:transparent}.navbar-inverse .navbar-text{color:#999999}.navbar-inverse .navbar-nav>li>a{color:#999999}.navbar-inverse .navbar-nav>li>a:hover,.navbar-inverse .navbar-nav>li>a:focus{color:#333333;background-color:transparent}.navbar-inverse .navbar-nav>.active>a,.navbar-inverse .navbar-nav>.active>a:hover,.navbar-inverse .navbar-nav>.active>a:focus{color:#333333;background-color:transparent}.navbar-inverse .navbar-nav>.disabled>a,.navbar-inverse .navbar-nav>.disabled>a:hover,.navbar-inverse .navbar-nav>.disabled>a:focus{color:#eeeeee;background-color:transparent}.navbar-inverse .navbar-toggle{border-color:#eeeeee}.navbar-inverse .navbar-toggle:hover,.navbar-inverse .navbar-toggle:focus{background-color:#eeeeee}.navbar-inverse .navbar-toggle .icon-bar{background-color:#999999}.navbar-inverse .navbar-collapse,.navbar-inverse .navbar-form{border-color:#ededed}.navbar-inverse .navbar-nav>.open>a,.navbar-inverse .navbar-nav>.open>a:hover,.navbar-inverse .navbar-nav>.open>a:focus{background-color:transparent;color:#333333}@media (max-width:767px){.navbar-inverse .navbar-nav .open .dropdown-menu>.dropdown-header{border-color:#e6e6e6}.navbar-inverse .navbar-nav .open .dropdown-menu .divider{background-color:#e6e6e6}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a{color:#999999}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:focus{color:#333333;background-color:transparent}.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:focus{color:#333333;background-color:transparent}.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:focus{color:#eeeeee;background-color:transparent}}.navbar-inverse .navbar-link{color:#999999}.navbar-inverse .navbar-link:hover{color:#333333}.navbar-inverse .btn-link{color:#999999}.navbar-inverse .btn-link:hover,.navbar-inverse .btn-link:focus{color:#333333}.navbar-inverse .btn-link[disabled]:hover,fieldset[disabled] .navbar-inverse .btn-link:hover,.navbar-inverse .btn-link[disabled]:focus,fieldset[disabled] .navbar-inverse .btn-link:focus{color:#eeeeee}.breadcrumb{padding:8px 15px;margin-bottom:20px;list-style:none;background-color:#fafafa;border-radius:4px}.breadcrumb>li{display:inline-block}.breadcrumb>li+li:before{content:">\00a0";padding:0 5px;color:#999999}.breadcrumb>.active{color:#999999}.pagination{display:inline-block;padding-left:0;margin:20px 0;border-radius:4px}.pagination>li{display:inline}.pagination>li>a,.pagination>li>span{position:relative;float:left;padding:7px 12px;line-height:1.42857143;text-decoration:none;color:#555555;background-color:#eeeeee;border:1px solid #e2e2e2;margin-left:-1px}.pagination>li:first-child>a,.pagination>li:first-child>span{margin-left:0;border-bottom-left-radius:4px;border-top-left-radius:4px}.pagination>li:last-child>a,.pagination>li:last-child>span{border-bottom-right-radius:4px;border-top-right-radius:4px}.pagination>li>a:hover,.pagination>li>span:hover,.pagination>li>a:focus,.pagination>li>span:focus{z-index:2;color:#555555;background-color:#eeeeee;border-color:#e2e2e2}.pagination>.active>a,.pagination>.active>span,.pagination>.active>a:hover,.pagination>.active>span:hover,.pagination>.active>a:focus,.pagination>.active>span:focus{z-index:3;color:#ffffff;background-color:#158cba;border-color:#127ba3;cursor:default}.pagination>.disabled>span,.pagination>.disabled>span:hover,.pagination>.disabled>span:focus,.pagination>.disabled>a,.pagination>.disabled>a:hover,.pagination>.disabled>a:focus{color:#999999;background-color:#eeeeee;border-color:#e2e2e2;cursor:not-allowed}.pagination-lg>li>a,.pagination-lg>li>span{padding:13px 16px;font-size:18px;line-height:1.3333333}.pagination-lg>li:first-child>a,.pagination-lg>li:first-child>span{border-bottom-left-radius:5px;border-top-left-radius:5px}.pagination-lg>li:last-child>a,.pagination-lg>li:last-child>span{border-bottom-right-radius:5px;border-top-right-radius:5px}.pagination-sm>li>a,.pagination-sm>li>span{padding:4px 10px;font-size:12px;line-height:1.5}.pagination-sm>li:first-child>a,.pagination-sm>li:first-child>span{border-bottom-left-radius:2px;border-top-left-radius:2px}.pagination-sm>li:last-child>a,.pagination-sm>li:last-child>span{border-bottom-right-radius:2px;border-top-right-radius:2px}.pager{padding-left:0;margin:20px 0;list-style:none;text-align:center}.pager li{display:inline}.pager li>a,.pager li>span{display:inline-block;padding:5px 14px;background-color:#eeeeee;border:1px solid #e2e2e2;border-radius:15px}.pager li>a:hover,.pager li>a:focus{text-decoration:none;background-color:#eeeeee}.pager .next>a,.pager .next>span{float:right}.pager .previous>a,.pager .previous>span{float:left}.pager .disabled>a,.pager .disabled>a:hover,.pager .disabled>a:focus,.pager .disabled>span{color:#999999;background-color:#eeeeee;cursor:not-allowed}.label{display:inline;padding:.2em .6em .3em;font-size:75%;font-weight:bold;line-height:1;color:#ffffff;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25em}a.label:hover,a.label:focus{color:#ffffff;text-decoration:none;cursor:pointer}.label:empty{display:none}.btn .label{position:relative;top:-1px}.label-default{background-color:#999999}.label-default[href]:hover,.label-default[href]:focus{background-color:#808080}.label-primary{background-color:#158cba}.label-primary[href]:hover,.label-primary[href]:focus{background-color:#106a8c}.label-success{background-color:#28b62c}.label-success[href]:hover,.label-success[href]:focus{background-color:#1f8c22}.label-info{background-color:#75caeb}.label-info[href]:hover,.label-info[href]:focus{background-color:#48b9e5}.label-warning{background-color:#ff851b}.label-warning[href]:hover,.label-warning[href]:focus{background-color:#e76b00}.label-danger{background-color:#ff4136}.label-danger[href]:hover,.label-danger[href]:focus{background-color:#ff1103}.badge{display:inline-block;min-width:10px;padding:3px 7px;font-size:12px;font-weight:normal;color:#ffffff;line-height:1;vertical-align:middle;white-space:nowrap;text-align:center;background-color:#158cba;border-radius:10px}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.btn-xs .badge,.btn-group-xs>.btn .badge{top:0;padding:1px 5px}a.badge:hover,a.badge:focus{color:#ffffff;text-decoration:none;cursor:pointer}.list-group-item.active>.badge,.nav-pills>.active>a>.badge{color:#158cba;background-color:#ffffff}.list-group-item>.badge{float:right}.list-group-item>.badge+.badge{margin-right:5px}.nav-pills>li>a>.badge{margin-left:3px}.jumbotron{padding-top:30px;padding-bottom:30px;margin-bottom:30px;color:inherit;background-color:#fafafa}.jumbotron h1,.jumbotron .h1{color:inherit}.jumbotron p{margin-bottom:15px;font-size:21px;font-weight:200}.jumbotron>hr{border-top-color:#e1e1e1}.container .jumbotron,.container-fluid .jumbotron{border-radius:5px;padding-left:15px;padding-right:15px}.jumbotron .container{max-width:100%}@media screen and (min-width:768px){.jumbotron{padding-top:48px;padding-bottom:48px}.container .jumbotron,.container-fluid .jumbotron{padding-left:60px;padding-right:60px}.jumbotron h1,.jumbotron .h1{font-size:63px}}.thumbnail{display:block;padding:4px;margin-bottom:20px;line-height:1.42857143;background-color:#ffffff;border:1px solid #eeeeee;border-radius:4px;-webkit-transition:border .2s ease-in-out;-o-transition:border .2s ease-in-out;transition:border .2s ease-in-out}.thumbnail>img,.thumbnail a>img{margin-left:auto;margin-right:auto}a.thumbnail:hover,a.thumbnail:focus,a.thumbnail.active{border-color:#158cba}.thumbnail .caption{padding:9px;color:#555555}.alert{padding:15px;margin-bottom:20px;border:1px solid transparent;border-radius:4px}.alert h4{margin-top:0;color:inherit}.alert .alert-link{font-weight:bold}.alert>p,.alert>ul{margin-bottom:0}.alert>p+p{margin-top:5px}.alert-dismissable,.alert-dismissible{padding-right:35px}.alert-dismissable .close,.alert-dismissible .close{position:relative;top:-2px;right:-21px;color:inherit}.alert-success{background-color:#28b62c;border-color:#24a528;color:#ffffff}.alert-success hr{border-top-color:#209023}.alert-success .alert-link{color:#e6e6e6}.alert-info{background-color:#75caeb;border-color:#40b5e3;color:#ffffff}.alert-info hr{border-top-color:#29ade0}.alert-info .alert-link{color:#e6e6e6}.alert-warning{background-color:#ff851b;border-color:#ff7701;color:#ffffff}.alert-warning hr{border-top-color:#e76b00}.alert-warning .alert-link{color:#e6e6e6}.alert-danger{background-color:#ff4136;border-color:#ff1103;color:#ffffff}.alert-danger hr{border-top-color:#e90d00}.alert-danger .alert-link{color:#e6e6e6}@-webkit-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@-o-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}.progress{overflow:hidden;height:20px;margin-bottom:20px;background-color:#fafafa;border-radius:4px;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,0.1);box-shadow:inset 0 1px 2px rgba(0,0,0,0.1)}.progress-bar{float:left;width:0%;height:100%;font-size:12px;line-height:20px;color:#ffffff;text-align:center;background-color:#158cba;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,0.15);box-shadow:inset 0 -1px 0 rgba(0,0,0,0.15);-webkit-transition:width 0.6s ease;-o-transition:width 0.6s ease;transition:width 0.6s ease}.progress-striped .progress-bar,.progress-bar-striped{background-image:-webkit-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);-webkit-background-size:40px 40px;background-size:40px 40px}.progress.active .progress-bar,.progress-bar.active{-webkit-animation:progress-bar-stripes 2s linear infinite;-o-animation:progress-bar-stripes 2s linear infinite;animation:progress-bar-stripes 2s linear infinite}.progress-bar-success{background-color:#28b62c}.progress-striped .progress-bar-success{background-image:-webkit-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent)}.progress-bar-info{background-color:#75caeb}.progress-striped .progress-bar-info{background-image:-webkit-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent)}.progress-bar-warning{background-color:#ff851b}.progress-striped .progress-bar-warning{background-image:-webkit-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent)}.progress-bar-danger{background-color:#ff4136}.progress-striped .progress-bar-danger{background-image:-webkit-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent)}.media{margin-top:15px}.media:first-child{margin-top:0}.media,.media-body{zoom:1;overflow:hidden}.media-body{width:10000px}.media-object{display:block}.media-object.img-thumbnail{max-width:none}.media-right,.media>.pull-right{padding-left:10px}.media-left,.media>.pull-left{padding-right:10px}.media-left,.media-right,.media-body{display:table-cell;vertical-align:top}.media-middle{vertical-align:middle}.media-bottom{vertical-align:bottom}.media-heading{margin-top:0;margin-bottom:5px}.media-list{padding-left:0;list-style:none}.list-group{margin-bottom:20px;padding-left:0}.list-group-item{position:relative;display:block;padding:10px 15px;margin-bottom:-1px;background-color:#ffffff;border:1px solid #eeeeee}.list-group-item:first-child{border-top-right-radius:4px;border-top-left-radius:4px}.list-group-item:last-child{margin-bottom:0;border-bottom-right-radius:4px;border-bottom-left-radius:4px}a.list-group-item,button.list-group-item{color:#555555}a.list-group-item .list-group-item-heading,button.list-group-item .list-group-item-heading{color:#333333}a.list-group-item:hover,button.list-group-item:hover,a.list-group-item:focus,button.list-group-item:focus{text-decoration:none;color:#555555;background-color:#f5f5f5}button.list-group-item{width:100%;text-align:left}.list-group-item.disabled,.list-group-item.disabled:hover,.list-group-item.disabled:focus{background-color:#eeeeee;color:#999999;cursor:not-allowed}.list-group-item.disabled .list-group-item-heading,.list-group-item.disabled:hover .list-group-item-heading,.list-group-item.disabled:focus .list-group-item-heading{color:inherit}.list-group-item.disabled .list-group-item-text,.list-group-item.disabled:hover .list-group-item-text,.list-group-item.disabled:focus .list-group-item-text{color:#999999}.list-group-item.active,.list-group-item.active:hover,.list-group-item.active:focus{z-index:2;color:#ffffff;background-color:#158cba;border-color:#158cba}.list-group-item.active .list-group-item-heading,.list-group-item.active:hover .list-group-item-heading,.list-group-item.active:focus .list-group-item-heading,.list-group-item.active .list-group-item-heading>small,.list-group-item.active:hover .list-group-item-heading>small,.list-group-item.active:focus .list-group-item-heading>small,.list-group-item.active .list-group-item-heading>.small,.list-group-item.active:hover .list-group-item-heading>.small,.list-group-item.active:focus .list-group-item-heading>.small{color:inherit}.list-group-item.active .list-group-item-text,.list-group-item.active:hover .list-group-item-text,.list-group-item.active:focus .list-group-item-text{color:#a6dff5}.list-group-item-success{color:#ffffff;background-color:#28b62c}a.list-group-item-success,button.list-group-item-success{color:#ffffff}a.list-group-item-success .list-group-item-heading,button.list-group-item-success .list-group-item-heading{color:inherit}a.list-group-item-success:hover,button.list-group-item-success:hover,a.list-group-item-success:focus,button.list-group-item-success:focus{color:#ffffff;background-color:#23a127}a.list-group-item-success.active,button.list-group-item-success.active,a.list-group-item-success.active:hover,button.list-group-item-success.active:hover,a.list-group-item-success.active:focus,button.list-group-item-success.active:focus{color:#fff;background-color:#ffffff;border-color:#ffffff}.list-group-item-info{color:#ffffff;background-color:#75caeb}a.list-group-item-info,button.list-group-item-info{color:#ffffff}a.list-group-item-info .list-group-item-heading,button.list-group-item-info .list-group-item-heading{color:inherit}a.list-group-item-info:hover,button.list-group-item-info:hover,a.list-group-item-info:focus,button.list-group-item-info:focus{color:#ffffff;background-color:#5fc1e8}a.list-group-item-info.active,button.list-group-item-info.active,a.list-group-item-info.active:hover,button.list-group-item-info.active:hover,a.list-group-item-info.active:focus,button.list-group-item-info.active:focus{color:#fff;background-color:#ffffff;border-color:#ffffff}.list-group-item-warning{color:#ffffff;background-color:#ff851b}a.list-group-item-warning,button.list-group-item-warning{color:#ffffff}a.list-group-item-warning .list-group-item-heading,button.list-group-item-warning .list-group-item-heading{color:inherit}a.list-group-item-warning:hover,button.list-group-item-warning:hover,a.list-group-item-warning:focus,button.list-group-item-warning:focus{color:#ffffff;background-color:#ff7701}a.list-group-item-warning.active,button.list-group-item-warning.active,a.list-group-item-warning.active:hover,button.list-group-item-warning.active:hover,a.list-group-item-warning.active:focus,button.list-group-item-warning.active:focus{color:#fff;background-color:#ffffff;border-color:#ffffff}.list-group-item-danger{color:#ffffff;background-color:#ff4136}a.list-group-item-danger,button.list-group-item-danger{color:#ffffff}a.list-group-item-danger .list-group-item-heading,button.list-group-item-danger .list-group-item-heading{color:inherit}a.list-group-item-danger:hover,button.list-group-item-danger:hover,a.list-group-item-danger:focus,button.list-group-item-danger:focus{color:#ffffff;background-color:#ff291c}a.list-group-item-danger.active,button.list-group-item-danger.active,a.list-group-item-danger.active:hover,button.list-group-item-danger.active:hover,a.list-group-item-danger.active:focus,button.list-group-item-danger.active:focus{color:#fff;background-color:#ffffff;border-color:#ffffff}.list-group-item-heading{margin-top:0;margin-bottom:5px}.list-group-item-text{margin-bottom:0;line-height:1.3}.panel{margin-bottom:20px;background-color:#ffffff;border:1px solid transparent;border-radius:4px;-webkit-box-shadow:0 1px 1px rgba(0,0,0,0.05);box-shadow:0 1px 1px rgba(0,0,0,0.05)}.panel-body{padding:15px}.panel-heading{padding:10px 15px;border-bottom:1px solid transparent;border-top-right-radius:3px;border-top-left-radius:3px}.panel-heading>.dropdown .dropdown-toggle{color:inherit}.panel-title{margin-top:0;margin-bottom:0;font-size:16px;color:inherit}.panel-title>a,.panel-title>small,.panel-title>.small,.panel-title>small>a,.panel-title>.small>a{color:inherit}.panel-footer{padding:10px 15px;background-color:#f5f5f5;border-top:1px solid transparent;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.list-group,.panel>.panel-collapse>.list-group{margin-bottom:0}.panel>.list-group .list-group-item,.panel>.panel-collapse>.list-group .list-group-item{border-width:1px 0;border-radius:0}.panel>.list-group:first-child .list-group-item:first-child,.panel>.panel-collapse>.list-group:first-child .list-group-item:first-child{border-top:0;border-top-right-radius:3px;border-top-left-radius:3px}.panel>.list-group:last-child .list-group-item:last-child,.panel>.panel-collapse>.list-group:last-child .list-group-item:last-child{border-bottom:0;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.panel-heading+.panel-collapse>.list-group .list-group-item:first-child{border-top-right-radius:0;border-top-left-radius:0}.panel-heading+.list-group .list-group-item:first-child{border-top-width:0}.list-group+.panel-footer{border-top-width:0}.panel>.table,.panel>.table-responsive>.table,.panel>.panel-collapse>.table{margin-bottom:0}.panel>.table caption,.panel>.table-responsive>.table caption,.panel>.panel-collapse>.table caption{padding-left:15px;padding-right:15px}.panel>.table:first-child,.panel>.table-responsive:first-child>.table:first-child{border-top-right-radius:3px;border-top-left-radius:3px}.panel>.table:first-child>thead:first-child>tr:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child{border-top-left-radius:3px;border-top-right-radius:3px}.panel>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table:first-child>thead:first-child>tr:first-child th:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:first-child{border-top-left-radius:3px}.panel>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table:first-child>thead:first-child>tr:first-child th:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:last-child{border-top-right-radius:3px}.panel>.table:last-child,.panel>.table-responsive:last-child>.table:last-child{border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.table:last-child>tbody:last-child>tr:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child{border-bottom-left-radius:3px;border-bottom-right-radius:3px}.panel>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:first-child{border-bottom-left-radius:3px}.panel>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:last-child{border-bottom-right-radius:3px}.panel>.panel-body+.table,.panel>.panel-body+.table-responsive,.panel>.table+.panel-body,.panel>.table-responsive+.panel-body{border-top:1px solid #eeeeee}.panel>.table>tbody:first-child>tr:first-child th,.panel>.table>tbody:first-child>tr:first-child td{border-top:0}.panel>.table-bordered,.panel>.table-responsive>.table-bordered{border:0}.panel>.table-bordered>thead>tr>th:first-child,.panel>.table-responsive>.table-bordered>thead>tr>th:first-child,.panel>.table-bordered>tbody>tr>th:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:first-child,.panel>.table-bordered>tfoot>tr>th:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:first-child,.panel>.table-bordered>thead>tr>td:first-child,.panel>.table-responsive>.table-bordered>thead>tr>td:first-child,.panel>.table-bordered>tbody>tr>td:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:first-child,.panel>.table-bordered>tfoot>tr>td:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:first-child{border-left:0}.panel>.table-bordered>thead>tr>th:last-child,.panel>.table-responsive>.table-bordered>thead>tr>th:last-child,.panel>.table-bordered>tbody>tr>th:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:last-child,.panel>.table-bordered>tfoot>tr>th:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:last-child,.panel>.table-bordered>thead>tr>td:last-child,.panel>.table-responsive>.table-bordered>thead>tr>td:last-child,.panel>.table-bordered>tbody>tr>td:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:last-child,.panel>.table-bordered>tfoot>tr>td:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:last-child{border-right:0}.panel>.table-bordered>thead>tr:first-child>td,.panel>.table-responsive>.table-bordered>thead>tr:first-child>td,.panel>.table-bordered>tbody>tr:first-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>td,.panel>.table-bordered>thead>tr:first-child>th,.panel>.table-responsive>.table-bordered>thead>tr:first-child>th,.panel>.table-bordered>tbody>tr:first-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>th{border-bottom:0}.panel>.table-bordered>tbody>tr:last-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>td,.panel>.table-bordered>tfoot>tr:last-child>td,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>td,.panel>.table-bordered>tbody>tr:last-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>th,.panel>.table-bordered>tfoot>tr:last-child>th,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>th{border-bottom:0}.panel>.table-responsive{border:0;margin-bottom:0}.panel-group{margin-bottom:20px}.panel-group .panel{margin-bottom:0;border-radius:4px}.panel-group .panel+.panel{margin-top:5px}.panel-group .panel-heading{border-bottom:0}.panel-group .panel-heading+.panel-collapse>.panel-body,.panel-group .panel-heading+.panel-collapse>.list-group{border-top:1px solid transparent}.panel-group .panel-footer{border-top:0}.panel-group .panel-footer+.panel-collapse .panel-body{border-bottom:1px solid transparent}.panel-default{border-color:transparent}.panel-default>.panel-heading{color:#333333;background-color:#f5f5f5;border-color:transparent}.panel-default>.panel-heading+.panel-collapse>.panel-body{border-top-color:transparent}.panel-default>.panel-heading .badge{color:#f5f5f5;background-color:#333333}.panel-default>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:transparent}.panel-primary{border-color:transparent}.panel-primary>.panel-heading{color:#ffffff;background-color:#158cba;border-color:transparent}.panel-primary>.panel-heading+.panel-collapse>.panel-body{border-top-color:transparent}.panel-primary>.panel-heading .badge{color:#158cba;background-color:#ffffff}.panel-primary>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:transparent}.panel-success{border-color:transparent}.panel-success>.panel-heading{color:#ffffff;background-color:#28b62c;border-color:transparent}.panel-success>.panel-heading+.panel-collapse>.panel-body{border-top-color:transparent}.panel-success>.panel-heading .badge{color:#28b62c;background-color:#ffffff}.panel-success>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:transparent}.panel-info{border-color:transparent}.panel-info>.panel-heading{color:#ffffff;background-color:#75caeb;border-color:transparent}.panel-info>.panel-heading+.panel-collapse>.panel-body{border-top-color:transparent}.panel-info>.panel-heading .badge{color:#75caeb;background-color:#ffffff}.panel-info>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:transparent}.panel-warning{border-color:transparent}.panel-warning>.panel-heading{color:#ffffff;background-color:#ff851b;border-color:transparent}.panel-warning>.panel-heading+.panel-collapse>.panel-body{border-top-color:transparent}.panel-warning>.panel-heading .badge{color:#ff851b;background-color:#ffffff}.panel-warning>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:transparent}.panel-danger{border-color:transparent}.panel-danger>.panel-heading{color:#ffffff;background-color:#ff4136;border-color:transparent}.panel-danger>.panel-heading+.panel-collapse>.panel-body{border-top-color:transparent}.panel-danger>.panel-heading .badge{color:#ff4136;background-color:#ffffff}.panel-danger>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:transparent}.embed-responsive{position:relative;display:block;height:0;padding:0;overflow:hidden}.embed-responsive .embed-responsive-item,.embed-responsive iframe,.embed-responsive embed,.embed-responsive object,.embed-responsive video{position:absolute;top:0;left:0;bottom:0;height:100%;width:100%;border:0}.embed-responsive-16by9{padding-bottom:56.25%}.embed-responsive-4by3{padding-bottom:75%}.well{min-height:20px;padding:19px;margin-bottom:20px;background-color:#fafafa;border:1px solid #e8e8e8;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.05);box-shadow:inset 0 1px 1px rgba(0,0,0,0.05)}.well blockquote{border-color:#ddd;border-color:rgba(0,0,0,0.15)}.well-lg{padding:24px;border-radius:5px}.well-sm{padding:9px;border-radius:2px}.close{float:right;font-size:21px;font-weight:bold;line-height:1;color:#ffffff;text-shadow:0 1px 0 #ffffff;opacity:0.2;filter:alpha(opacity=20)}.close:hover,.close:focus{color:#ffffff;text-decoration:none;cursor:pointer;opacity:0.5;filter:alpha(opacity=50)}button.close{padding:0;cursor:pointer;background:transparent;border:0;-webkit-appearance:none}.modal-open{overflow:hidden}.modal{display:none;overflow:hidden;position:fixed;top:0;right:0;bottom:0;left:0;z-index:1050;-webkit-overflow-scrolling:touch;outline:0}.modal.fade .modal-dialog{-webkit-transform:translate(0, -25%);-ms-transform:translate(0, -25%);-o-transform:translate(0, -25%);transform:translate(0, -25%);-webkit-transition:-webkit-transform .3s ease-out;-o-transition:-o-transform .3s ease-out;transition:transform .3s ease-out}.modal.in .modal-dialog{-webkit-transform:translate(0, 0);-ms-transform:translate(0, 0);-o-transform:translate(0, 0);transform:translate(0, 0)}.modal-open .modal{overflow-x:hidden;overflow-y:auto}.modal-dialog{position:relative;width:auto;margin:10px}.modal-content{position:relative;background-color:#ffffff;border:1px solid #eeeeee;border:1px solid rgba(0,0,0,0.05);border-radius:5px;-webkit-box-shadow:0 3px 9px rgba(0,0,0,0.5);box-shadow:0 3px 9px rgba(0,0,0,0.5);-webkit-background-clip:padding-box;background-clip:padding-box;outline:0}.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;background-color:#000000}.modal-backdrop.fade{opacity:0;filter:alpha(opacity=0)}.modal-backdrop.in{opacity:0.5;filter:alpha(opacity=50)}.modal-header{padding:15px;border-bottom:1px solid #e5e5e5}.modal-header .close{margin-top:-2px}.modal-title{margin:0;line-height:1.42857143}.modal-body{position:relative;padding:20px}.modal-footer{padding:20px;text-align:right;border-top:1px solid #e5e5e5}.modal-footer .btn+.btn{margin-left:5px;margin-bottom:0}.modal-footer .btn-group .btn+.btn{margin-left:-1px}.modal-footer .btn-block+.btn-block{margin-left:0}.modal-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}@media (min-width:768px){.modal-dialog{width:600px;margin:30px auto}.modal-content{-webkit-box-shadow:0 5px 15px rgba(0,0,0,0.5);box-shadow:0 5px 15px rgba(0,0,0,0.5)}.modal-sm{width:300px}}@media (min-width:992px){.modal-lg{width:900px}}.tooltip{position:absolute;z-index:1070;display:block;font-family:"Source Sans Pro","Helvetica Neue",Helvetica,Arial,sans-serif;font-style:normal;font-weight:normal;letter-spacing:normal;line-break:auto;line-height:1.42857143;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;white-space:normal;word-break:normal;word-spacing:normal;word-wrap:normal;font-size:12px;opacity:0;filter:alpha(opacity=0)}.tooltip.in{opacity:0.9;filter:alpha(opacity=90)}.tooltip.top{margin-top:-3px;padding:5px 0}.tooltip.right{margin-left:3px;padding:0 5px}.tooltip.bottom{margin-top:3px;padding:5px 0}.tooltip.left{margin-left:-3px;padding:0 5px}.tooltip-inner{max-width:200px;padding:3px 8px;color:#ffffff;text-align:center;background-color:#000000;border-radius:4px}.tooltip-arrow{position:absolute;width:0;height:0;border-color:transparent;border-style:solid}.tooltip.top .tooltip-arrow{bottom:0;left:50%;margin-left:-5px;border-width:5px 5px 0;border-top-color:#000000}.tooltip.top-left .tooltip-arrow{bottom:0;right:5px;margin-bottom:-5px;border-width:5px 5px 0;border-top-color:#000000}.tooltip.top-right .tooltip-arrow{bottom:0;left:5px;margin-bottom:-5px;border-width:5px 5px 0;border-top-color:#000000}.tooltip.right .tooltip-arrow{top:50%;left:0;margin-top:-5px;border-width:5px 5px 5px 0;border-right-color:#000000}.tooltip.left .tooltip-arrow{top:50%;right:0;margin-top:-5px;border-width:5px 0 5px 5px;border-left-color:#000000}.tooltip.bottom .tooltip-arrow{top:0;left:50%;margin-left:-5px;border-width:0 5px 5px;border-bottom-color:#000000}.tooltip.bottom-left .tooltip-arrow{top:0;right:5px;margin-top:-5px;border-width:0 5px 5px;border-bottom-color:#000000}.tooltip.bottom-right .tooltip-arrow{top:0;left:5px;margin-top:-5px;border-width:0 5px 5px;border-bottom-color:#000000}.popover{position:absolute;top:0;left:0;z-index:1060;display:none;max-width:276px;padding:1px;font-family:"Source Sans Pro","Helvetica Neue",Helvetica,Arial,sans-serif;font-style:normal;font-weight:normal;letter-spacing:normal;line-break:auto;line-height:1.42857143;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;white-space:normal;word-break:normal;word-spacing:normal;word-wrap:normal;font-size:14px;background-color:#ffffff;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid #cccccc;border:1px solid rgba(0,0,0,0.2);border-radius:5px;-webkit-box-shadow:0 5px 10px rgba(0,0,0,0.2);box-shadow:0 5px 10px rgba(0,0,0,0.2)}.popover.top{margin-top:-10px}.popover.right{margin-left:10px}.popover.bottom{margin-top:10px}.popover.left{margin-left:-10px}.popover-title{margin:0;padding:8px 14px;font-size:14px;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;border-radius:4px 4px 0 0}.popover-content{padding:9px 14px}.popover>.arrow,.popover>.arrow:after{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.popover>.arrow{border-width:11px}.popover>.arrow:after{border-width:10px;content:""}.popover.top>.arrow{left:50%;margin-left:-11px;border-bottom-width:0;border-top-color:#999999;border-top-color:rgba(0,0,0,0.25);bottom:-11px}.popover.top>.arrow:after{content:" ";bottom:1px;margin-left:-10px;border-bottom-width:0;border-top-color:#ffffff}.popover.right>.arrow{top:50%;left:-11px;margin-top:-11px;border-left-width:0;border-right-color:#999999;border-right-color:rgba(0,0,0,0.25)}.popover.right>.arrow:after{content:" ";left:1px;bottom:-10px;border-left-width:0;border-right-color:#ffffff}.popover.bottom>.arrow{left:50%;margin-left:-11px;border-top-width:0;border-bottom-color:#999999;border-bottom-color:rgba(0,0,0,0.25);top:-11px}.popover.bottom>.arrow:after{content:" ";top:1px;margin-left:-10px;border-top-width:0;border-bottom-color:#ffffff}.popover.left>.arrow{top:50%;right:-11px;margin-top:-11px;border-right-width:0;border-left-color:#999999;border-left-color:rgba(0,0,0,0.25)}.popover.left>.arrow:after{content:" ";right:1px;border-right-width:0;border-left-color:#ffffff;bottom:-10px}.carousel{position:relative}.carousel-inner{position:relative;overflow:hidden;width:100%}.carousel-inner>.item{display:none;position:relative;-webkit-transition:.6s ease-in-out left;-o-transition:.6s ease-in-out left;transition:.6s ease-in-out left}.carousel-inner>.item>img,.carousel-inner>.item>a>img{line-height:1}@media all and (transform-3d),(-webkit-transform-3d){.carousel-inner>.item{-webkit-transition:-webkit-transform .6s ease-in-out;-o-transition:-o-transform .6s ease-in-out;transition:transform .6s ease-in-out;-webkit-backface-visibility:hidden;backface-visibility:hidden;-webkit-perspective:1000px;perspective:1000px}.carousel-inner>.item.next,.carousel-inner>.item.active.right{-webkit-transform:translate3d(100%, 0, 0);transform:translate3d(100%, 0, 0);left:0}.carousel-inner>.item.prev,.carousel-inner>.item.active.left{-webkit-transform:translate3d(-100%, 0, 0);transform:translate3d(-100%, 0, 0);left:0}.carousel-inner>.item.next.left,.carousel-inner>.item.prev.right,.carousel-inner>.item.active{-webkit-transform:translate3d(0, 0, 0);transform:translate3d(0, 0, 0);left:0}}.carousel-inner>.active,.carousel-inner>.next,.carousel-inner>.prev{display:block}.carousel-inner>.active{left:0}.carousel-inner>.next,.carousel-inner>.prev{position:absolute;top:0;width:100%}.carousel-inner>.next{left:100%}.carousel-inner>.prev{left:-100%}.carousel-inner>.next.left,.carousel-inner>.prev.right{left:0}.carousel-inner>.active.left{left:-100%}.carousel-inner>.active.right{left:100%}.carousel-control{position:absolute;top:0;left:0;bottom:0;width:15%;opacity:0.5;filter:alpha(opacity=50);font-size:20px;color:#ffffff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,0.6);background-color:rgba(0,0,0,0)}.carousel-control.left{background-image:-webkit-linear-gradient(left, rgba(0,0,0,0.5) 0, rgba(0,0,0,0.0001) 100%);background-image:-o-linear-gradient(left, rgba(0,0,0,0.5) 0, rgba(0,0,0,0.0001) 100%);background-image:-webkit-gradient(linear, left top, right top, from(rgba(0,0,0,0.5)), to(rgba(0,0,0,0.0001)));background-image:linear-gradient(to right, rgba(0,0,0,0.5) 0, rgba(0,0,0,0.0001) 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#80000000', endColorstr='#00000000', GradientType=1)}.carousel-control.right{left:auto;right:0;background-image:-webkit-linear-gradient(left, rgba(0,0,0,0.0001) 0, rgba(0,0,0,0.5) 100%);background-image:-o-linear-gradient(left, rgba(0,0,0,0.0001) 0, rgba(0,0,0,0.5) 100%);background-image:-webkit-gradient(linear, left top, right top, from(rgba(0,0,0,0.0001)), to(rgba(0,0,0,0.5)));background-image:linear-gradient(to right, rgba(0,0,0,0.0001) 0, rgba(0,0,0,0.5) 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#80000000', GradientType=1)}.carousel-control:hover,.carousel-control:focus{outline:0;color:#ffffff;text-decoration:none;opacity:0.9;filter:alpha(opacity=90)}.carousel-control .icon-prev,.carousel-control .icon-next,.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right{position:absolute;top:50%;margin-top:-10px;z-index:5;display:inline-block}.carousel-control .icon-prev,.carousel-control .glyphicon-chevron-left{left:50%;margin-left:-10px}.carousel-control .icon-next,.carousel-control .glyphicon-chevron-right{right:50%;margin-right:-10px}.carousel-control .icon-prev,.carousel-control .icon-next{width:20px;height:20px;line-height:1;font-family:serif}.carousel-control .icon-prev:before{content:'\2039'}.carousel-control .icon-next:before{content:'\203a'}.carousel-indicators{position:absolute;bottom:10px;left:50%;z-index:15;width:60%;margin-left:-30%;padding-left:0;list-style:none;text-align:center}.carousel-indicators li{display:inline-block;width:10px;height:10px;margin:1px;text-indent:-999px;border:1px solid #ffffff;border-radius:10px;cursor:pointer;background-color:#000 \9;background-color:rgba(0,0,0,0)}.carousel-indicators .active{margin:0;width:12px;height:12px;background-color:#ffffff}.carousel-caption{position:absolute;left:15%;right:15%;bottom:20px;z-index:10;padding-top:20px;padding-bottom:20px;color:#ffffff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,0.6)}.carousel-caption .btn{text-shadow:none}@media screen and (min-width:768px){.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right,.carousel-control .icon-prev,.carousel-control .icon-next{width:30px;height:30px;margin-top:-10px;font-size:30px}.carousel-control .glyphicon-chevron-left,.carousel-control .icon-prev{margin-left:-10px}.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next{margin-right:-10px}.carousel-caption{left:20%;right:20%;padding-bottom:30px}.carousel-indicators{bottom:20px}}.clearfix:before,.clearfix:after,.dl-horizontal dd:before,.dl-horizontal dd:after,.container:before,.container:after,.container-fluid:before,.container-fluid:after,.row:before,.row:after,.form-horizontal .form-group:before,.form-horizontal .form-group:after,.btn-toolbar:before,.btn-toolbar:after,.btn-group-vertical>.btn-group:before,.btn-group-vertical>.btn-group:after,.nav:before,.nav:after,.navbar:before,.navbar:after,.navbar-header:before,.navbar-header:after,.navbar-collapse:before,.navbar-collapse:after,.pager:before,.pager:after,.panel-body:before,.panel-body:after,.modal-header:before,.modal-header:after,.modal-footer:before,.modal-footer:after{content:" ";display:table}.clearfix:after,.dl-horizontal dd:after,.container:after,.container-fluid:after,.row:after,.form-horizontal .form-group:after,.btn-toolbar:after,.btn-group-vertical>.btn-group:after,.nav:after,.navbar:after,.navbar-header:after,.navbar-collapse:after,.pager:after,.panel-body:after,.modal-header:after,.modal-footer:after{clear:both}.center-block{display:block;margin-left:auto;margin-right:auto}.pull-right{float:right !important}.pull-left{float:left !important}.hide{display:none !important}.show{display:block !important}.invisible{visibility:hidden}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.hidden{display:none !important}.affix{position:fixed}@-ms-viewport{width:device-width}.visible-xs,.visible-sm,.visible-md,.visible-lg{display:none !important}.visible-xs-block,.visible-xs-inline,.visible-xs-inline-block,.visible-sm-block,.visible-sm-inline,.visible-sm-inline-block,.visible-md-block,.visible-md-inline,.visible-md-inline-block,.visible-lg-block,.visible-lg-inline,.visible-lg-inline-block{display:none !important}@media (max-width:767px){.visible-xs{display:block !important}table.visible-xs{display:table !important}tr.visible-xs{display:table-row !important}th.visible-xs,td.visible-xs{display:table-cell !important}}@media (max-width:767px){.visible-xs-block{display:block !important}}@media (max-width:767px){.visible-xs-inline{display:inline !important}}@media (max-width:767px){.visible-xs-inline-block{display:inline-block !important}}@media (min-width:768px) and (max-width:991px){.visible-sm{display:block !important}table.visible-sm{display:table !important}tr.visible-sm{display:table-row !important}th.visible-sm,td.visible-sm{display:table-cell !important}}@media (min-width:768px) and (max-width:991px){.visible-sm-block{display:block !important}}@media (min-width:768px) and (max-width:991px){.visible-sm-inline{display:inline !important}}@media (min-width:768px) and (max-width:991px){.visible-sm-inline-block{display:inline-block !important}}@media (min-width:992px) and (max-width:1449px){.visible-md{display:block !important}table.visible-md{display:table !important}tr.visible-md{display:table-row !important}th.visible-md,td.visible-md{display:table-cell !important}}@media (min-width:992px) and (max-width:1449px){.visible-md-block{display:block !important}}@media (min-width:992px) and (max-width:1449px){.visible-md-inline{display:inline !important}}@media (min-width:992px) and (max-width:1449px){.visible-md-inline-block{display:inline-block !important}}@media (min-width:1450px){.visible-lg{display:block !important}table.visible-lg{display:table !important}tr.visible-lg{display:table-row !important}th.visible-lg,td.visible-lg{display:table-cell !important}}@media (min-width:1450px){.visible-lg-block{display:block !important}}@media (min-width:1450px){.visible-lg-inline{display:inline !important}}@media (min-width:1450px){.visible-lg-inline-block{display:inline-block !important}}@media (max-width:767px){.hidden-xs{display:none !important}}@media (min-width:768px) and (max-width:991px){.hidden-sm{display:none !important}}@media (min-width:992px) and (max-width:1449px){.hidden-md{display:none !important}}@media (min-width:1450px){.hidden-lg{display:none !important}}.visible-print{display:none !important}@media print{.visible-print{display:block !important}table.visible-print{display:table !important}tr.visible-print{display:table-row !important}th.visible-print,td.visible-print{display:table-cell !important}}.visible-print-block{display:none !important}@media print{.visible-print-block{display:block !important}}.visible-print-inline{display:none !important}@media print{.visible-print-inline{display:inline !important}}.visible-print-inline-block{display:none !important}@media print{.visible-print-inline-block{display:inline-block !important}}@media print{.hidden-print{display:none !important}}.navbar{border-width:0 1px 4px 1px}.btn{padding:9px 12px 7px;border-width:0 1px 4px 1px;font-size:12px;font-weight:bold;line-height:1.5;text-transform:uppercase}.btn:hover{margin-top:1px;border-bottom-width:3px}.btn:active{margin-top:2px;border-bottom-width:2px;-webkit-box-shadow:none;box-shadow:none}.btn-lg,.btn-group-lg>.btn{padding:15px 16px 13px;line-height:15px}.btn-sm,.btn-group-sm>.btn{padding:6px 10px 4px}.btn-xs,.btn-group-xs>.btn{padding:3px 5px 1px}.btn-default:hover,.btn-default:focus,.btn-group.open .dropdown-toggle.btn-default{background-color:#eeeeee;border-color:#e2e2e2}.btn-primary:hover,.btn-primary:focus,.btn-group.open .dropdown-toggle.btn-primary{background-color:#158cba;border-color:#127ba3}.btn-success:hover,.btn-success:focus,.btn-group.open .dropdown-toggle.btn-success{background-color:#28b62c;border-color:#23a127}.btn-info:hover,.btn-info:focus,.btn-group.open .dropdown-toggle.btn-info{background-color:#75caeb;border-color:#5fc1e8}.btn-warning:hover,.btn-warning:focus,.btn-group.open .dropdown-toggle.btn-warning{background-color:#ff851b;border-color:#ff7701}.btn-danger:hover,.btn-danger:focus,.btn-group.open .dropdown-toggle.btn-danger{background-color:#ff4136;border-color:#ff291c}.btn-group.open .dropdown-toggle{-webkit-box-shadow:none;box-shadow:none}.navbar-btn:hover{margin-top:8px}.navbar-btn:active{margin-top:9px}.navbar-btn.btn-sm:hover{margin-top:11px}.navbar-btn.btn-sm:active{margin-top:12px}.navbar-btn.btn-xs:hover{margin-top:15px}.navbar-btn.btn-xs:active{margin-top:16px}.btn-group-vertical .btn+.btn:hover{border-top-width:1px}.btn-group-vertical .btn+.btn:active{border-top-width:2px}.text-primary,.text-primary:hover{color:#158cba}.text-success,.text-success:hover{color:#28b62c}.text-danger,.text-danger:hover{color:#ff4136}.text-warning,.text-warning:hover{color:#ff851b}.text-info,.text-info:hover{color:#75caeb}table a:not(.btn),.table a:not(.btn){text-decoration:underline}table .dropdown-menu a,.table .dropdown-menu a{text-decoration:none}table .success,.table .success,table .warning,.table .warning,table .danger,.table .danger,table .info,.table .info{color:#fff}table .success a:not(.btn),.table .success a:not(.btn),table .warning a:not(.btn),.table .warning a:not(.btn),table .danger a:not(.btn),.table .danger a:not(.btn),table .info a:not(.btn),.table .info a:not(.btn){color:#fff}table:not(.table-bordered)>thead>tr>th,.table:not(.table-bordered)>thead>tr>th,table:not(.table-bordered)>tbody>tr>th,.table:not(.table-bordered)>tbody>tr>th,table:not(.table-bordered)>tfoot>tr>th,.table:not(.table-bordered)>tfoot>tr>th,table:not(.table-bordered)>thead>tr>td,.table:not(.table-bordered)>thead>tr>td,table:not(.table-bordered)>tbody>tr>td,.table:not(.table-bordered)>tbody>tr>td,table:not(.table-bordered)>tfoot>tr>td,.table:not(.table-bordered)>tfoot>tr>td{border-color:transparent}.form-control{-webkit-box-shadow:inset 0 2px 0 rgba(0,0,0,0.075);box-shadow:inset 0 2px 0 rgba(0,0,0,0.075)}label{font-weight:normal}.has-warning .help-block,.has-warning .control-label,.has-warning .radio,.has-warning .checkbox,.has-warning .radio-inline,.has-warning .checkbox-inline,.has-warning.radio label,.has-warning.checkbox label,.has-warning.radio-inline label,.has-warning.checkbox-inline label,.has-warning .form-control-feedback{color:#ff851b}.has-warning .form-control,.has-warning .form-control:focus{border:1px solid #ff851b;-webkit-box-shadow:inset 0 2px 0 rgba(0,0,0,0.075);box-shadow:inset 0 2px 0 rgba(0,0,0,0.075)}.has-warning .input-group-addon{border:1px solid #ff851b}.has-error .help-block,.has-error .control-label,.has-error .radio,.has-error .checkbox,.has-error .radio-inline,.has-error .checkbox-inline,.has-error.radio label,.has-error.checkbox label,.has-error.radio-inline label,.has-error.checkbox-inline label,.has-error .form-control-feedback{color:#ff4136}.has-error .form-control,.has-error .form-control:focus{border:1px solid #ff4136;-webkit-box-shadow:inset 0 2px 0 rgba(0,0,0,0.075);box-shadow:inset 0 2px 0 rgba(0,0,0,0.075)}.has-error .input-group-addon{border:1px solid #ff4136}.has-success .help-block,.has-success .control-label,.has-success .radio,.has-success .checkbox,.has-success .radio-inline,.has-success .checkbox-inline,.has-success.radio label,.has-success.checkbox label,.has-success.radio-inline label,.has-success.checkbox-inline label,.has-success .form-control-feedback{color:#28b62c}.has-success .form-control,.has-success .form-control:focus{border:1px solid #28b62c;-webkit-box-shadow:inset 0 2px 0 rgba(0,0,0,0.075);box-shadow:inset 0 2px 0 rgba(0,0,0,0.075)}.has-success .input-group-addon{border:1px solid #28b62c}.nav .open>a,.nav .open>a:hover,.nav .open>a:focus{border-color:transparent}.nav-tabs>li>a{margin-top:6px;border-color:#e7e7e7;color:#333333;-webkit-transition:all .2s ease-in-out;-o-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.nav-tabs>li>a:hover,.nav-tabs>li>a:focus,.nav-tabs>li.active>a,.nav-tabs>li.active>a:hover,.nav-tabs>li.active>a:focus,.nav-tabs .open>a,.nav-tabs .open>a:hover,.nav-tabs .open>a:focus{padding-bottom:16px;margin-top:0}.nav-tabs .open>a,.nav-tabs .open>a:hover,.nav-tabs .open>a:focus{border-color:#e7e7e7}.nav-tabs>li.disabled>a:hover,.nav-tabs>li.disabled>a:focus{padding-top:10px;padding-bottom:10px;margin-top:6px}.nav-tabs.nav-justified>li{vertical-align:bottom}.dropdown-menu{margin-top:0;border-width:0 1px 4px 1px;border-top-width:1px;-webkit-box-shadow:none;box-shadow:none}.breadcrumb{border-color:#ededed;border-style:solid;border-width:0 1px 4px 1px}.pagination>li>a,.pager>li>a,.pagination>li>span,.pager>li>span{position:relative;top:0;border-width:0 1px 4px 1px;color:#555555;font-size:12px;font-weight:bold;text-transform:uppercase}.pagination>li>a:hover,.pager>li>a:hover,.pagination>li>span:hover,.pager>li>span:hover{top:1px;border-bottom-width:3px}.pagination>li>a:active,.pager>li>a:active,.pagination>li>span:active,.pager>li>span:active{top:2px;border-bottom-width:2px}.pagination>.disabled>a:hover,.pager>.disabled>a:hover,.pagination>.disabled>span:hover,.pager>.disabled>span:hover{top:0;border-width:0 1px 4px 1px}.pagination>.disabled>a:active,.pager>.disabled>a:active,.pagination>.disabled>span:active,.pager>.disabled>span:active{top:0;border-width:0 1px 4px 1px}.pager>li>a,.pager>li>span,.pager>.disabled>a,.pager>.disabled>span,.pager>li>a:hover,.pager>li>span:hover,.pager>.disabled>a:hover,.pager>.disabled>span:hover,.pager>li>a:active,.pager>li>span:active,.pager>.disabled>a:active,.pager>.disabled>span:active{border-left-width:2px;border-right-width:2px}.close{color:#fff;text-decoration:none;opacity:0.4}.close:hover,.close:focus{color:#fff;opacity:1}.alert{border-width:0 1px 4px 1px}.alert .alert-link{font-weight:normal;color:#fff;text-decoration:underline}.label{font-weight:normal}.progress{border:1px solid #e7e7e7;-webkit-box-shadow:inset 0 2px 0 rgba(0,0,0,0.1);box-shadow:inset 0 2px 0 rgba(0,0,0,0.1)}.progress-bar{-webkit-box-shadow:inset 0 -4px 0 rgba(0,0,0,0.15);box-shadow:inset 0 -4px 0 rgba(0,0,0,0.15)}.well{border:1px solid #e7e7e7;-webkit-box-shadow:inset 0 2px 0 rgba(0,0,0,0.05);box-shadow:inset 0 2px 0 rgba(0,0,0,0.05)}a.list-group-item.active,a.list-group-item.active:hover,a.list-group-item.active:focus{border-color:#eeeeee}a.list-group-item-success.active{background-color:#28b62c}a.list-group-item-success.active:hover,a.list-group-item-success.active:focus{background-color:#23a127}a.list-group-item-warning.active{background-color:#ff851b}a.list-group-item-warning.active:hover,a.list-group-item-warning.active:focus{background-color:#ff7701}a.list-group-item-danger.active{background-color:#ff4136}a.list-group-item-danger.active:hover,a.list-group-item-danger.active:focus{background-color:#ff291c}.jumbotron{border:1px solid #e7e7e7;-webkit-box-shadow:inset 0 2px 0 rgba(0,0,0,0.05);box-shadow:inset 0 2px 0 rgba(0,0,0,0.05)}.panel{border:1px solid #e7e7e7;border-width:0 1px 4px 1px}.panel-default .close{color:#555555}.modal .close{color:#555555}.popover{color:#555555} \ No newline at end of file + *//*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background-color:transparent}a:active,a:hover{outline:0}abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}b,strong{font-weight:bold}dfn{font-style:italic}h1{font-size:2em;margin:0.67em 0}mark{background:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-0.5em}sub{bottom:-0.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{box-sizing:content-box;height:0}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace, monospace;font-size:1em}button,input,optgroup,select,textarea{color:inherit;font:inherit;margin:0}button{overflow:visible}button,select{text-transform:none}button,html input[type="button"],input[type="reset"],input[type="submit"]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}input{line-height:normal}input[type="checkbox"],input[type="radio"]{box-sizing:border-box;padding:0}input[type="number"]::-webkit-inner-spin-button,input[type="number"]::-webkit-outer-spin-button{height:auto}input[type="search"]{-webkit-appearance:textfield;box-sizing:content-box}input[type="search"]::-webkit-search-cancel-button,input[type="search"]::-webkit-search-decoration{-webkit-appearance:none}fieldset{border:1px solid #c0c0c0;margin:0 2px;padding:0.35em 0.625em 0.75em}legend{border:0;padding:0}textarea{overflow:auto}optgroup{font-weight:bold}table{border-collapse:collapse;border-spacing:0}td,th{padding:0}/*! Source: https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css */@media print{*,*:before,*:after{color:#000 !important;text-shadow:none !important;background:transparent !important;box-shadow:none !important}a,a:visited{text-decoration:underline}a[href]:after{content:" (" attr(href) ")"}abbr[title]:after{content:" (" attr(title) ")"}a[href^="#"]:after,a[href^="javascript:"]:after{content:""}pre,blockquote{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}tr,img{page-break-inside:avoid}img{max-width:100% !important}p,h2,h3{orphans:3;widows:3}h2,h3{page-break-after:avoid}.navbar{display:none}.btn>.caret,.dropup>.btn>.caret{border-top-color:#000 !important}.label{border:1px solid #000}.table{border-collapse:collapse !important}.table td,.table th{background-color:#fff !important}.table-bordered th,.table-bordered td{border:1px solid #ddd !important}}@font-face{font-family:"Glyphicons Halflings";src:url("/fonts/glyphicons-halflings-regular.eot");src:url("/fonts/glyphicons-halflings-regular.eot?#iefix") format("embedded-opentype"),url("/fonts/glyphicons-halflings-regular.woff2") format("woff2"),url("/fonts/glyphicons-halflings-regular.woff") format("woff"),url("/fonts/glyphicons-halflings-regular.ttf") format("truetype"),url("/fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular") format("svg")}.glyphicon{position:relative;top:1px;display:inline-block;font-family:"Glyphicons Halflings";font-style:normal;font-weight:400;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.glyphicon-asterisk:before{content:"\002a"}.glyphicon-plus:before{content:"\002b"}.glyphicon-euro:before,.glyphicon-eur:before{content:"\20ac"}.glyphicon-minus:before{content:"\2212"}.glyphicon-cloud:before{content:"\2601"}.glyphicon-envelope:before{content:"\2709"}.glyphicon-pencil:before{content:"\270f"}.glyphicon-glass:before{content:"\e001"}.glyphicon-music:before{content:"\e002"}.glyphicon-search:before{content:"\e003"}.glyphicon-heart:before{content:"\e005"}.glyphicon-star:before{content:"\e006"}.glyphicon-star-empty:before{content:"\e007"}.glyphicon-user:before{content:"\e008"}.glyphicon-film:before{content:"\e009"}.glyphicon-th-large:before{content:"\e010"}.glyphicon-th:before{content:"\e011"}.glyphicon-th-list:before{content:"\e012"}.glyphicon-ok:before{content:"\e013"}.glyphicon-remove:before{content:"\e014"}.glyphicon-zoom-in:before{content:"\e015"}.glyphicon-zoom-out:before{content:"\e016"}.glyphicon-off:before{content:"\e017"}.glyphicon-signal:before{content:"\e018"}.glyphicon-cog:before{content:"\e019"}.glyphicon-trash:before{content:"\e020"}.glyphicon-home:before{content:"\e021"}.glyphicon-file:before{content:"\e022"}.glyphicon-time:before{content:"\e023"}.glyphicon-road:before{content:"\e024"}.glyphicon-download-alt:before{content:"\e025"}.glyphicon-download:before{content:"\e026"}.glyphicon-upload:before{content:"\e027"}.glyphicon-inbox:before{content:"\e028"}.glyphicon-play-circle:before{content:"\e029"}.glyphicon-repeat:before{content:"\e030"}.glyphicon-refresh:before{content:"\e031"}.glyphicon-list-alt:before{content:"\e032"}.glyphicon-lock:before{content:"\e033"}.glyphicon-flag:before{content:"\e034"}.glyphicon-headphones:before{content:"\e035"}.glyphicon-volume-off:before{content:"\e036"}.glyphicon-volume-down:before{content:"\e037"}.glyphicon-volume-up:before{content:"\e038"}.glyphicon-qrcode:before{content:"\e039"}.glyphicon-barcode:before{content:"\e040"}.glyphicon-tag:before{content:"\e041"}.glyphicon-tags:before{content:"\e042"}.glyphicon-book:before{content:"\e043"}.glyphicon-bookmark:before{content:"\e044"}.glyphicon-print:before{content:"\e045"}.glyphicon-camera:before{content:"\e046"}.glyphicon-font:before{content:"\e047"}.glyphicon-bold:before{content:"\e048"}.glyphicon-italic:before{content:"\e049"}.glyphicon-text-height:before{content:"\e050"}.glyphicon-text-width:before{content:"\e051"}.glyphicon-align-left:before{content:"\e052"}.glyphicon-align-center:before{content:"\e053"}.glyphicon-align-right:before{content:"\e054"}.glyphicon-align-justify:before{content:"\e055"}.glyphicon-list:before{content:"\e056"}.glyphicon-indent-left:before{content:"\e057"}.glyphicon-indent-right:before{content:"\e058"}.glyphicon-facetime-video:before{content:"\e059"}.glyphicon-picture:before{content:"\e060"}.glyphicon-map-marker:before{content:"\e062"}.glyphicon-adjust:before{content:"\e063"}.glyphicon-tint:before{content:"\e064"}.glyphicon-edit:before{content:"\e065"}.glyphicon-share:before{content:"\e066"}.glyphicon-check:before{content:"\e067"}.glyphicon-move:before{content:"\e068"}.glyphicon-step-backward:before{content:"\e069"}.glyphicon-fast-backward:before{content:"\e070"}.glyphicon-backward:before{content:"\e071"}.glyphicon-play:before{content:"\e072"}.glyphicon-pause:before{content:"\e073"}.glyphicon-stop:before{content:"\e074"}.glyphicon-forward:before{content:"\e075"}.glyphicon-fast-forward:before{content:"\e076"}.glyphicon-step-forward:before{content:"\e077"}.glyphicon-eject:before{content:"\e078"}.glyphicon-chevron-left:before{content:"\e079"}.glyphicon-chevron-right:before{content:"\e080"}.glyphicon-plus-sign:before{content:"\e081"}.glyphicon-minus-sign:before{content:"\e082"}.glyphicon-remove-sign:before{content:"\e083"}.glyphicon-ok-sign:before{content:"\e084"}.glyphicon-question-sign:before{content:"\e085"}.glyphicon-info-sign:before{content:"\e086"}.glyphicon-screenshot:before{content:"\e087"}.glyphicon-remove-circle:before{content:"\e088"}.glyphicon-ok-circle:before{content:"\e089"}.glyphicon-ban-circle:before{content:"\e090"}.glyphicon-arrow-left:before{content:"\e091"}.glyphicon-arrow-right:before{content:"\e092"}.glyphicon-arrow-up:before{content:"\e093"}.glyphicon-arrow-down:before{content:"\e094"}.glyphicon-share-alt:before{content:"\e095"}.glyphicon-resize-full:before{content:"\e096"}.glyphicon-resize-small:before{content:"\e097"}.glyphicon-exclamation-sign:before{content:"\e101"}.glyphicon-gift:before{content:"\e102"}.glyphicon-leaf:before{content:"\e103"}.glyphicon-fire:before{content:"\e104"}.glyphicon-eye-open:before{content:"\e105"}.glyphicon-eye-close:before{content:"\e106"}.glyphicon-warning-sign:before{content:"\e107"}.glyphicon-plane:before{content:"\e108"}.glyphicon-calendar:before{content:"\e109"}.glyphicon-random:before{content:"\e110"}.glyphicon-comment:before{content:"\e111"}.glyphicon-magnet:before{content:"\e112"}.glyphicon-chevron-up:before{content:"\e113"}.glyphicon-chevron-down:before{content:"\e114"}.glyphicon-retweet:before{content:"\e115"}.glyphicon-shopping-cart:before{content:"\e116"}.glyphicon-folder-close:before{content:"\e117"}.glyphicon-folder-open:before{content:"\e118"}.glyphicon-resize-vertical:before{content:"\e119"}.glyphicon-resize-horizontal:before{content:"\e120"}.glyphicon-hdd:before{content:"\e121"}.glyphicon-bullhorn:before{content:"\e122"}.glyphicon-bell:before{content:"\e123"}.glyphicon-certificate:before{content:"\e124"}.glyphicon-thumbs-up:before{content:"\e125"}.glyphicon-thumbs-down:before{content:"\e126"}.glyphicon-hand-right:before{content:"\e127"}.glyphicon-hand-left:before{content:"\e128"}.glyphicon-hand-up:before{content:"\e129"}.glyphicon-hand-down:before{content:"\e130"}.glyphicon-circle-arrow-right:before{content:"\e131"}.glyphicon-circle-arrow-left:before{content:"\e132"}.glyphicon-circle-arrow-up:before{content:"\e133"}.glyphicon-circle-arrow-down:before{content:"\e134"}.glyphicon-globe:before{content:"\e135"}.glyphicon-wrench:before{content:"\e136"}.glyphicon-tasks:before{content:"\e137"}.glyphicon-filter:before{content:"\e138"}.glyphicon-briefcase:before{content:"\e139"}.glyphicon-fullscreen:before{content:"\e140"}.glyphicon-dashboard:before{content:"\e141"}.glyphicon-paperclip:before{content:"\e142"}.glyphicon-heart-empty:before{content:"\e143"}.glyphicon-link:before{content:"\e144"}.glyphicon-phone:before{content:"\e145"}.glyphicon-pushpin:before{content:"\e146"}.glyphicon-usd:before{content:"\e148"}.glyphicon-gbp:before{content:"\e149"}.glyphicon-sort:before{content:"\e150"}.glyphicon-sort-by-alphabet:before{content:"\e151"}.glyphicon-sort-by-alphabet-alt:before{content:"\e152"}.glyphicon-sort-by-order:before{content:"\e153"}.glyphicon-sort-by-order-alt:before{content:"\e154"}.glyphicon-sort-by-attributes:before{content:"\e155"}.glyphicon-sort-by-attributes-alt:before{content:"\e156"}.glyphicon-unchecked:before{content:"\e157"}.glyphicon-expand:before{content:"\e158"}.glyphicon-collapse-down:before{content:"\e159"}.glyphicon-collapse-up:before{content:"\e160"}.glyphicon-log-in:before{content:"\e161"}.glyphicon-flash:before{content:"\e162"}.glyphicon-log-out:before{content:"\e163"}.glyphicon-new-window:before{content:"\e164"}.glyphicon-record:before{content:"\e165"}.glyphicon-save:before{content:"\e166"}.glyphicon-open:before{content:"\e167"}.glyphicon-saved:before{content:"\e168"}.glyphicon-import:before{content:"\e169"}.glyphicon-export:before{content:"\e170"}.glyphicon-send:before{content:"\e171"}.glyphicon-floppy-disk:before{content:"\e172"}.glyphicon-floppy-saved:before{content:"\e173"}.glyphicon-floppy-remove:before{content:"\e174"}.glyphicon-floppy-save:before{content:"\e175"}.glyphicon-floppy-open:before{content:"\e176"}.glyphicon-credit-card:before{content:"\e177"}.glyphicon-transfer:before{content:"\e178"}.glyphicon-cutlery:before{content:"\e179"}.glyphicon-header:before{content:"\e180"}.glyphicon-compressed:before{content:"\e181"}.glyphicon-earphone:before{content:"\e182"}.glyphicon-phone-alt:before{content:"\e183"}.glyphicon-tower:before{content:"\e184"}.glyphicon-stats:before{content:"\e185"}.glyphicon-sd-video:before{content:"\e186"}.glyphicon-hd-video:before{content:"\e187"}.glyphicon-subtitles:before{content:"\e188"}.glyphicon-sound-stereo:before{content:"\e189"}.glyphicon-sound-dolby:before{content:"\e190"}.glyphicon-sound-5-1:before{content:"\e191"}.glyphicon-sound-6-1:before{content:"\e192"}.glyphicon-sound-7-1:before{content:"\e193"}.glyphicon-copyright-mark:before{content:"\e194"}.glyphicon-registration-mark:before{content:"\e195"}.glyphicon-cloud-download:before{content:"\e197"}.glyphicon-cloud-upload:before{content:"\e198"}.glyphicon-tree-conifer:before{content:"\e199"}.glyphicon-tree-deciduous:before{content:"\e200"}.glyphicon-cd:before{content:"\e201"}.glyphicon-save-file:before{content:"\e202"}.glyphicon-open-file:before{content:"\e203"}.glyphicon-level-up:before{content:"\e204"}.glyphicon-copy:before{content:"\e205"}.glyphicon-paste:before{content:"\e206"}.glyphicon-alert:before{content:"\e209"}.glyphicon-equalizer:before{content:"\e210"}.glyphicon-king:before{content:"\e211"}.glyphicon-queen:before{content:"\e212"}.glyphicon-pawn:before{content:"\e213"}.glyphicon-bishop:before{content:"\e214"}.glyphicon-knight:before{content:"\e215"}.glyphicon-baby-formula:before{content:"\e216"}.glyphicon-tent:before{content:"\26fa"}.glyphicon-blackboard:before{content:"\e218"}.glyphicon-bed:before{content:"\e219"}.glyphicon-apple:before{content:"\f8ff"}.glyphicon-erase:before{content:"\e221"}.glyphicon-hourglass:before{content:"\231b"}.glyphicon-lamp:before{content:"\e223"}.glyphicon-duplicate:before{content:"\e224"}.glyphicon-piggy-bank:before{content:"\e225"}.glyphicon-scissors:before{content:"\e226"}.glyphicon-bitcoin:before{content:"\e227"}.glyphicon-btc:before{content:"\e227"}.glyphicon-xbt:before{content:"\e227"}.glyphicon-yen:before{content:"\00a5"}.glyphicon-jpy:before{content:"\00a5"}.glyphicon-ruble:before{content:"\20bd"}.glyphicon-rub:before{content:"\20bd"}.glyphicon-scale:before{content:"\e230"}.glyphicon-ice-lolly:before{content:"\e231"}.glyphicon-ice-lolly-tasted:before{content:"\e232"}.glyphicon-education:before{content:"\e233"}.glyphicon-option-horizontal:before{content:"\e234"}.glyphicon-option-vertical:before{content:"\e235"}.glyphicon-menu-hamburger:before{content:"\e236"}.glyphicon-modal-window:before{content:"\e237"}.glyphicon-oil:before{content:"\e238"}.glyphicon-grain:before{content:"\e239"}.glyphicon-sunglasses:before{content:"\e240"}.glyphicon-text-size:before{content:"\e241"}.glyphicon-text-color:before{content:"\e242"}.glyphicon-text-background:before{content:"\e243"}.glyphicon-object-align-top:before{content:"\e244"}.glyphicon-object-align-bottom:before{content:"\e245"}.glyphicon-object-align-horizontal:before{content:"\e246"}.glyphicon-object-align-left:before{content:"\e247"}.glyphicon-object-align-vertical:before{content:"\e248"}.glyphicon-object-align-right:before{content:"\e249"}.glyphicon-triangle-right:before{content:"\e250"}.glyphicon-triangle-left:before{content:"\e251"}.glyphicon-triangle-bottom:before{content:"\e252"}.glyphicon-triangle-top:before{content:"\e253"}.glyphicon-console:before{content:"\e254"}.glyphicon-superscript:before{content:"\e255"}.glyphicon-subscript:before{content:"\e256"}.glyphicon-menu-left:before{content:"\e257"}.glyphicon-menu-right:before{content:"\e258"}.glyphicon-menu-down:before{content:"\e259"}.glyphicon-menu-up:before{content:"\e260"}*{box-sizing:border-box}*:before,*:after{box-sizing:border-box}html{font-size:10px;-webkit-tap-highlight-color:rgba(0,0,0,0)}body{font-family:"Source Sans Pro","Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;line-height:1.42857143;color:#555555;background-color:#ffffff}input,button,select,textarea{font-family:inherit;font-size:inherit;line-height:inherit}a{color:#158cba;text-decoration:none}a:hover,a:focus{color:#158cba;text-decoration:underline}a:focus{outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}figure{margin:0}img{vertical-align:middle}.img-responsive,.thumbnail>img,.thumbnail a>img,.carousel-inner>.item>img,.carousel-inner>.item>a>img{display:block;max-width:100%;height:auto}.img-rounded{border-radius:5px}.img-thumbnail{padding:4px;line-height:1.42857143;background-color:#ffffff;border:1px solid #eeeeee;border-radius:4px;transition:all .2s ease-in-out;display:inline-block;max-width:100%;height:auto}.img-circle{border-radius:50%}hr{margin-top:20px;margin-bottom:20px;border:0;border-top:1px solid #eeeeee}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0, 0, 0, 0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}[role="button"]{cursor:pointer}h1,h2,h3,h4,h5,h6,.h1,.h2,.h3,.h4,.h5,.h6{font-family:inherit;font-weight:400;line-height:1.1;color:#333333}h1 small,h2 small,h3 small,h4 small,h5 small,h6 small,.h1 small,.h2 small,.h3 small,.h4 small,.h5 small,.h6 small,h1 .small,h2 .small,h3 .small,h4 .small,h5 .small,h6 .small,.h1 .small,.h2 .small,.h3 .small,.h4 .small,.h5 .small,.h6 .small{font-weight:400;line-height:1;color:#999999}h1,.h1,h2,.h2,h3,.h3{margin-top:20px;margin-bottom:10px}h1 small,.h1 small,h2 small,.h2 small,h3 small,.h3 small,h1 .small,.h1 .small,h2 .small,.h2 .small,h3 .small,.h3 .small{font-size:65%}h4,.h4,h5,.h5,h6,.h6{margin-top:10px;margin-bottom:10px}h4 small,.h4 small,h5 small,.h5 small,h6 small,.h6 small,h4 .small,.h4 .small,h5 .small,.h5 .small,h6 .small,.h6 .small{font-size:75%}h1,.h1{font-size:36px}h2,.h2{font-size:30px}h3,.h3{font-size:24px}h4,.h4{font-size:18px}h5,.h5{font-size:14px}h6,.h6{font-size:12px}p{margin:0 0 10px}.lead{margin-bottom:20px;font-size:16px;font-weight:300;line-height:1.4}@media (min-width:768px){.lead{font-size:21px}}small,.small{font-size:85%}mark,.mark{padding:.2em;background-color:#ff851b}.text-left{text-align:left}.text-right{text-align:right}.text-center{text-align:center}.text-justify{text-align:justify}.text-nowrap{white-space:nowrap}.text-lowercase{text-transform:lowercase}.text-uppercase{text-transform:uppercase}.text-capitalize{text-transform:capitalize}.text-muted{color:#999999}.text-primary{color:#158cba}a.text-primary:hover,a.text-primary:focus{color:#106a8c}.text-success{color:#ffffff}a.text-success:hover,a.text-success:focus{color:#e6e6e6}.text-info{color:#ffffff}a.text-info:hover,a.text-info:focus{color:#e6e6e6}.text-warning{color:#ffffff}a.text-warning:hover,a.text-warning:focus{color:#e6e6e6}.text-danger{color:#ffffff}a.text-danger:hover,a.text-danger:focus{color:#e6e6e6}.bg-primary{color:#fff;background-color:#158cba}a.bg-primary:hover,a.bg-primary:focus{background-color:#106a8c}.bg-success{background-color:#28b62c}a.bg-success:hover,a.bg-success:focus{background-color:#1f8c22}.bg-info{background-color:#75caeb}a.bg-info:hover,a.bg-info:focus{background-color:#48b9e5}.bg-warning{background-color:#ff851b}a.bg-warning:hover,a.bg-warning:focus{background-color:#e76b00}.bg-danger{background-color:#ff4136}a.bg-danger:hover,a.bg-danger:focus{background-color:#ff1103}.page-header{padding-bottom:9px;margin:40px 0 20px;border-bottom:1px solid #eeeeee}ul,ol{margin-top:0;margin-bottom:10px}ul ul,ol ul,ul ol,ol ol{margin-bottom:0}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none;margin-left:-5px}.list-inline>li{display:inline-block;padding-right:5px;padding-left:5px}dl{margin-top:0;margin-bottom:20px}dt,dd{line-height:1.42857143}dt{font-weight:700}dd{margin-left:0}@media (min-width:768px){.dl-horizontal dt{float:left;width:160px;clear:left;text-align:right;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dl-horizontal dd{margin-left:180px}}abbr[title],abbr[data-original-title]{cursor:help}.initialism{font-size:90%;text-transform:uppercase}blockquote{padding:10px 20px;margin:0 0 20px;font-size:17.5px;border-left:5px solid #eeeeee}blockquote p:last-child,blockquote ul:last-child,blockquote ol:last-child{margin-bottom:0}blockquote footer,blockquote small,blockquote .small{display:block;font-size:80%;line-height:1.42857143;color:#999999}blockquote footer:before,blockquote small:before,blockquote .small:before{content:"\2014 \00A0"}.blockquote-reverse,blockquote.pull-right{padding-right:15px;padding-left:0;text-align:right;border-right:5px solid #eeeeee;border-left:0}.blockquote-reverse footer:before,blockquote.pull-right footer:before,.blockquote-reverse small:before,blockquote.pull-right small:before,.blockquote-reverse .small:before,blockquote.pull-right .small:before{content:""}.blockquote-reverse footer:after,blockquote.pull-right footer:after,.blockquote-reverse small:after,blockquote.pull-right small:after,.blockquote-reverse .small:after,blockquote.pull-right .small:after{content:"\00A0 \2014"}address{margin-bottom:20px;font-style:normal;line-height:1.42857143}code,kbd,pre,samp{font-family:Menlo,Monaco,Consolas,"Courier New",monospace}code{padding:2px 4px;font-size:90%;color:#c7254e;background-color:#f9f2f4;border-radius:4px}kbd{padding:2px 4px;font-size:90%;color:#ffffff;background-color:#333333;border-radius:2px;box-shadow:inset 0 -1px 0 rgba(0,0,0,0.25)}kbd kbd{padding:0;font-size:100%;font-weight:700;box-shadow:none}pre{display:block;padding:9.5px;margin:0 0 10px;font-size:13px;line-height:1.42857143;color:#333333;word-break:break-all;word-wrap:break-word;background-color:#f5f5f5;border:1px solid #cccccc;border-radius:4px}pre code{padding:0;font-size:inherit;color:inherit;white-space:pre-wrap;background-color:transparent;border-radius:0}.pre-scrollable{max-height:340px;overflow-y:scroll}.container{padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}@media (min-width:768px){.container{width:750px}}@media (min-width:992px){.container{width:970px}}@media (min-width:1200px){.container{width:1170px}}.container-fluid{padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}.row{margin-right:-15px;margin-left:-15px}.row-no-gutters{margin-right:0;margin-left:0}.row-no-gutters [class*="col-"]{padding-right:0;padding-left:0}.col-xs-1,.col-sm-1,.col-md-1,.col-lg-1,.col-xs-2,.col-sm-2,.col-md-2,.col-lg-2,.col-xs-3,.col-sm-3,.col-md-3,.col-lg-3,.col-xs-4,.col-sm-4,.col-md-4,.col-lg-4,.col-xs-5,.col-sm-5,.col-md-5,.col-lg-5,.col-xs-6,.col-sm-6,.col-md-6,.col-lg-6,.col-xs-7,.col-sm-7,.col-md-7,.col-lg-7,.col-xs-8,.col-sm-8,.col-md-8,.col-lg-8,.col-xs-9,.col-sm-9,.col-md-9,.col-lg-9,.col-xs-10,.col-sm-10,.col-md-10,.col-lg-10,.col-xs-11,.col-sm-11,.col-md-11,.col-lg-11,.col-xs-12,.col-sm-12,.col-md-12,.col-lg-12{position:relative;min-height:1px;padding-right:15px;padding-left:15px}.col-xs-1,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9,.col-xs-10,.col-xs-11,.col-xs-12{float:left}.col-xs-12{width:100%}.col-xs-11{width:91.66666667%}.col-xs-10{width:83.33333333%}.col-xs-9{width:75%}.col-xs-8{width:66.66666667%}.col-xs-7{width:58.33333333%}.col-xs-6{width:50%}.col-xs-5{width:41.66666667%}.col-xs-4{width:33.33333333%}.col-xs-3{width:25%}.col-xs-2{width:16.66666667%}.col-xs-1{width:8.33333333%}.col-xs-pull-12{right:100%}.col-xs-pull-11{right:91.66666667%}.col-xs-pull-10{right:83.33333333%}.col-xs-pull-9{right:75%}.col-xs-pull-8{right:66.66666667%}.col-xs-pull-7{right:58.33333333%}.col-xs-pull-6{right:50%}.col-xs-pull-5{right:41.66666667%}.col-xs-pull-4{right:33.33333333%}.col-xs-pull-3{right:25%}.col-xs-pull-2{right:16.66666667%}.col-xs-pull-1{right:8.33333333%}.col-xs-pull-0{right:auto}.col-xs-push-12{left:100%}.col-xs-push-11{left:91.66666667%}.col-xs-push-10{left:83.33333333%}.col-xs-push-9{left:75%}.col-xs-push-8{left:66.66666667%}.col-xs-push-7{left:58.33333333%}.col-xs-push-6{left:50%}.col-xs-push-5{left:41.66666667%}.col-xs-push-4{left:33.33333333%}.col-xs-push-3{left:25%}.col-xs-push-2{left:16.66666667%}.col-xs-push-1{left:8.33333333%}.col-xs-push-0{left:auto}.col-xs-offset-12{margin-left:100%}.col-xs-offset-11{margin-left:91.66666667%}.col-xs-offset-10{margin-left:83.33333333%}.col-xs-offset-9{margin-left:75%}.col-xs-offset-8{margin-left:66.66666667%}.col-xs-offset-7{margin-left:58.33333333%}.col-xs-offset-6{margin-left:50%}.col-xs-offset-5{margin-left:41.66666667%}.col-xs-offset-4{margin-left:33.33333333%}.col-xs-offset-3{margin-left:25%}.col-xs-offset-2{margin-left:16.66666667%}.col-xs-offset-1{margin-left:8.33333333%}.col-xs-offset-0{margin-left:0%}@media (min-width:768px){.col-sm-1,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-sm-10,.col-sm-11,.col-sm-12{float:left}.col-sm-12{width:100%}.col-sm-11{width:91.66666667%}.col-sm-10{width:83.33333333%}.col-sm-9{width:75%}.col-sm-8{width:66.66666667%}.col-sm-7{width:58.33333333%}.col-sm-6{width:50%}.col-sm-5{width:41.66666667%}.col-sm-4{width:33.33333333%}.col-sm-3{width:25%}.col-sm-2{width:16.66666667%}.col-sm-1{width:8.33333333%}.col-sm-pull-12{right:100%}.col-sm-pull-11{right:91.66666667%}.col-sm-pull-10{right:83.33333333%}.col-sm-pull-9{right:75%}.col-sm-pull-8{right:66.66666667%}.col-sm-pull-7{right:58.33333333%}.col-sm-pull-6{right:50%}.col-sm-pull-5{right:41.66666667%}.col-sm-pull-4{right:33.33333333%}.col-sm-pull-3{right:25%}.col-sm-pull-2{right:16.66666667%}.col-sm-pull-1{right:8.33333333%}.col-sm-pull-0{right:auto}.col-sm-push-12{left:100%}.col-sm-push-11{left:91.66666667%}.col-sm-push-10{left:83.33333333%}.col-sm-push-9{left:75%}.col-sm-push-8{left:66.66666667%}.col-sm-push-7{left:58.33333333%}.col-sm-push-6{left:50%}.col-sm-push-5{left:41.66666667%}.col-sm-push-4{left:33.33333333%}.col-sm-push-3{left:25%}.col-sm-push-2{left:16.66666667%}.col-sm-push-1{left:8.33333333%}.col-sm-push-0{left:auto}.col-sm-offset-12{margin-left:100%}.col-sm-offset-11{margin-left:91.66666667%}.col-sm-offset-10{margin-left:83.33333333%}.col-sm-offset-9{margin-left:75%}.col-sm-offset-8{margin-left:66.66666667%}.col-sm-offset-7{margin-left:58.33333333%}.col-sm-offset-6{margin-left:50%}.col-sm-offset-5{margin-left:41.66666667%}.col-sm-offset-4{margin-left:33.33333333%}.col-sm-offset-3{margin-left:25%}.col-sm-offset-2{margin-left:16.66666667%}.col-sm-offset-1{margin-left:8.33333333%}.col-sm-offset-0{margin-left:0%}}@media (min-width:992px){.col-md-1,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-md-10,.col-md-11,.col-md-12{float:left}.col-md-12{width:100%}.col-md-11{width:91.66666667%}.col-md-10{width:83.33333333%}.col-md-9{width:75%}.col-md-8{width:66.66666667%}.col-md-7{width:58.33333333%}.col-md-6{width:50%}.col-md-5{width:41.66666667%}.col-md-4{width:33.33333333%}.col-md-3{width:25%}.col-md-2{width:16.66666667%}.col-md-1{width:8.33333333%}.col-md-pull-12{right:100%}.col-md-pull-11{right:91.66666667%}.col-md-pull-10{right:83.33333333%}.col-md-pull-9{right:75%}.col-md-pull-8{right:66.66666667%}.col-md-pull-7{right:58.33333333%}.col-md-pull-6{right:50%}.col-md-pull-5{right:41.66666667%}.col-md-pull-4{right:33.33333333%}.col-md-pull-3{right:25%}.col-md-pull-2{right:16.66666667%}.col-md-pull-1{right:8.33333333%}.col-md-pull-0{right:auto}.col-md-push-12{left:100%}.col-md-push-11{left:91.66666667%}.col-md-push-10{left:83.33333333%}.col-md-push-9{left:75%}.col-md-push-8{left:66.66666667%}.col-md-push-7{left:58.33333333%}.col-md-push-6{left:50%}.col-md-push-5{left:41.66666667%}.col-md-push-4{left:33.33333333%}.col-md-push-3{left:25%}.col-md-push-2{left:16.66666667%}.col-md-push-1{left:8.33333333%}.col-md-push-0{left:auto}.col-md-offset-12{margin-left:100%}.col-md-offset-11{margin-left:91.66666667%}.col-md-offset-10{margin-left:83.33333333%}.col-md-offset-9{margin-left:75%}.col-md-offset-8{margin-left:66.66666667%}.col-md-offset-7{margin-left:58.33333333%}.col-md-offset-6{margin-left:50%}.col-md-offset-5{margin-left:41.66666667%}.col-md-offset-4{margin-left:33.33333333%}.col-md-offset-3{margin-left:25%}.col-md-offset-2{margin-left:16.66666667%}.col-md-offset-1{margin-left:8.33333333%}.col-md-offset-0{margin-left:0%}}@media (min-width:1200px){.col-lg-1,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-lg-10,.col-lg-11,.col-lg-12{float:left}.col-lg-12{width:100%}.col-lg-11{width:91.66666667%}.col-lg-10{width:83.33333333%}.col-lg-9{width:75%}.col-lg-8{width:66.66666667%}.col-lg-7{width:58.33333333%}.col-lg-6{width:50%}.col-lg-5{width:41.66666667%}.col-lg-4{width:33.33333333%}.col-lg-3{width:25%}.col-lg-2{width:16.66666667%}.col-lg-1{width:8.33333333%}.col-lg-pull-12{right:100%}.col-lg-pull-11{right:91.66666667%}.col-lg-pull-10{right:83.33333333%}.col-lg-pull-9{right:75%}.col-lg-pull-8{right:66.66666667%}.col-lg-pull-7{right:58.33333333%}.col-lg-pull-6{right:50%}.col-lg-pull-5{right:41.66666667%}.col-lg-pull-4{right:33.33333333%}.col-lg-pull-3{right:25%}.col-lg-pull-2{right:16.66666667%}.col-lg-pull-1{right:8.33333333%}.col-lg-pull-0{right:auto}.col-lg-push-12{left:100%}.col-lg-push-11{left:91.66666667%}.col-lg-push-10{left:83.33333333%}.col-lg-push-9{left:75%}.col-lg-push-8{left:66.66666667%}.col-lg-push-7{left:58.33333333%}.col-lg-push-6{left:50%}.col-lg-push-5{left:41.66666667%}.col-lg-push-4{left:33.33333333%}.col-lg-push-3{left:25%}.col-lg-push-2{left:16.66666667%}.col-lg-push-1{left:8.33333333%}.col-lg-push-0{left:auto}.col-lg-offset-12{margin-left:100%}.col-lg-offset-11{margin-left:91.66666667%}.col-lg-offset-10{margin-left:83.33333333%}.col-lg-offset-9{margin-left:75%}.col-lg-offset-8{margin-left:66.66666667%}.col-lg-offset-7{margin-left:58.33333333%}.col-lg-offset-6{margin-left:50%}.col-lg-offset-5{margin-left:41.66666667%}.col-lg-offset-4{margin-left:33.33333333%}.col-lg-offset-3{margin-left:25%}.col-lg-offset-2{margin-left:16.66666667%}.col-lg-offset-1{margin-left:8.33333333%}.col-lg-offset-0{margin-left:0%}}table{background-color:transparent}table col[class*="col-"]{position:static;display:table-column;float:none}table td[class*="col-"],table th[class*="col-"]{position:static;display:table-cell;float:none}caption{padding-top:8px;padding-bottom:8px;color:#999999;text-align:left}th{text-align:left}.table{width:100%;max-width:100%;margin-bottom:20px}.table>thead>tr>th,.table>tbody>tr>th,.table>tfoot>tr>th,.table>thead>tr>td,.table>tbody>tr>td,.table>tfoot>tr>td{padding:8px;line-height:1.42857143;vertical-align:top;border-top:1px solid #eeeeee}.table>thead>tr>th{vertical-align:bottom;border-bottom:2px solid #eeeeee}.table>caption+thead>tr:first-child>th,.table>colgroup+thead>tr:first-child>th,.table>thead:first-child>tr:first-child>th,.table>caption+thead>tr:first-child>td,.table>colgroup+thead>tr:first-child>td,.table>thead:first-child>tr:first-child>td{border-top:0}.table>tbody+tbody{border-top:2px solid #eeeeee}.table .table{background-color:#ffffff}.table-condensed>thead>tr>th,.table-condensed>tbody>tr>th,.table-condensed>tfoot>tr>th,.table-condensed>thead>tr>td,.table-condensed>tbody>tr>td,.table-condensed>tfoot>tr>td{padding:5px}.table-bordered{border:1px solid #eeeeee}.table-bordered>thead>tr>th,.table-bordered>tbody>tr>th,.table-bordered>tfoot>tr>th,.table-bordered>thead>tr>td,.table-bordered>tbody>tr>td,.table-bordered>tfoot>tr>td{border:1px solid #eeeeee}.table-bordered>thead>tr>th,.table-bordered>thead>tr>td{border-bottom-width:2px}.table-striped>tbody>tr:nth-of-type(odd){background-color:#f9f9f9}.table-hover>tbody>tr:hover{background-color:#f5f5f5}.table>thead>tr>td.active,.table>tbody>tr>td.active,.table>tfoot>tr>td.active,.table>thead>tr>th.active,.table>tbody>tr>th.active,.table>tfoot>tr>th.active,.table>thead>tr.active>td,.table>tbody>tr.active>td,.table>tfoot>tr.active>td,.table>thead>tr.active>th,.table>tbody>tr.active>th,.table>tfoot>tr.active>th{background-color:#f5f5f5}.table-hover>tbody>tr>td.active:hover,.table-hover>tbody>tr>th.active:hover,.table-hover>tbody>tr.active:hover>td,.table-hover>tbody>tr:hover>.active,.table-hover>tbody>tr.active:hover>th{background-color:#e8e8e8}.table>thead>tr>td.success,.table>tbody>tr>td.success,.table>tfoot>tr>td.success,.table>thead>tr>th.success,.table>tbody>tr>th.success,.table>tfoot>tr>th.success,.table>thead>tr.success>td,.table>tbody>tr.success>td,.table>tfoot>tr.success>td,.table>thead>tr.success>th,.table>tbody>tr.success>th,.table>tfoot>tr.success>th{background-color:#28b62c}.table-hover>tbody>tr>td.success:hover,.table-hover>tbody>tr>th.success:hover,.table-hover>tbody>tr.success:hover>td,.table-hover>tbody>tr:hover>.success,.table-hover>tbody>tr.success:hover>th{background-color:#23a127}.table>thead>tr>td.info,.table>tbody>tr>td.info,.table>tfoot>tr>td.info,.table>thead>tr>th.info,.table>tbody>tr>th.info,.table>tfoot>tr>th.info,.table>thead>tr.info>td,.table>tbody>tr.info>td,.table>tfoot>tr.info>td,.table>thead>tr.info>th,.table>tbody>tr.info>th,.table>tfoot>tr.info>th{background-color:#75caeb}.table-hover>tbody>tr>td.info:hover,.table-hover>tbody>tr>th.info:hover,.table-hover>tbody>tr.info:hover>td,.table-hover>tbody>tr:hover>.info,.table-hover>tbody>tr.info:hover>th{background-color:#5fc1e8}.table>thead>tr>td.warning,.table>tbody>tr>td.warning,.table>tfoot>tr>td.warning,.table>thead>tr>th.warning,.table>tbody>tr>th.warning,.table>tfoot>tr>th.warning,.table>thead>tr.warning>td,.table>tbody>tr.warning>td,.table>tfoot>tr.warning>td,.table>thead>tr.warning>th,.table>tbody>tr.warning>th,.table>tfoot>tr.warning>th{background-color:#ff851b}.table-hover>tbody>tr>td.warning:hover,.table-hover>tbody>tr>th.warning:hover,.table-hover>tbody>tr.warning:hover>td,.table-hover>tbody>tr:hover>.warning,.table-hover>tbody>tr.warning:hover>th{background-color:#ff7701}.table>thead>tr>td.danger,.table>tbody>tr>td.danger,.table>tfoot>tr>td.danger,.table>thead>tr>th.danger,.table>tbody>tr>th.danger,.table>tfoot>tr>th.danger,.table>thead>tr.danger>td,.table>tbody>tr.danger>td,.table>tfoot>tr.danger>td,.table>thead>tr.danger>th,.table>tbody>tr.danger>th,.table>tfoot>tr.danger>th{background-color:#ff4136}.table-hover>tbody>tr>td.danger:hover,.table-hover>tbody>tr>th.danger:hover,.table-hover>tbody>tr.danger:hover>td,.table-hover>tbody>tr:hover>.danger,.table-hover>tbody>tr.danger:hover>th{background-color:#ff291c}.table-responsive{min-height:.01%;overflow-x:auto}@media screen and (max-width:767px){.table-responsive{width:100%;margin-bottom:15px;overflow-y:hidden;-ms-overflow-style:-ms-autohiding-scrollbar;border:1px solid #eeeeee}.table-responsive>.table{margin-bottom:0}.table-responsive>.table>thead>tr>th,.table-responsive>.table>tbody>tr>th,.table-responsive>.table>tfoot>tr>th,.table-responsive>.table>thead>tr>td,.table-responsive>.table>tbody>tr>td,.table-responsive>.table>tfoot>tr>td{white-space:nowrap}.table-responsive>.table-bordered{border:0}.table-responsive>.table-bordered>thead>tr>th:first-child,.table-responsive>.table-bordered>tbody>tr>th:first-child,.table-responsive>.table-bordered>tfoot>tr>th:first-child,.table-responsive>.table-bordered>thead>tr>td:first-child,.table-responsive>.table-bordered>tbody>tr>td:first-child,.table-responsive>.table-bordered>tfoot>tr>td:first-child{border-left:0}.table-responsive>.table-bordered>thead>tr>th:last-child,.table-responsive>.table-bordered>tbody>tr>th:last-child,.table-responsive>.table-bordered>tfoot>tr>th:last-child,.table-responsive>.table-bordered>thead>tr>td:last-child,.table-responsive>.table-bordered>tbody>tr>td:last-child,.table-responsive>.table-bordered>tfoot>tr>td:last-child{border-right:0}.table-responsive>.table-bordered>tbody>tr:last-child>th,.table-responsive>.table-bordered>tfoot>tr:last-child>th,.table-responsive>.table-bordered>tbody>tr:last-child>td,.table-responsive>.table-bordered>tfoot>tr:last-child>td{border-bottom:0}}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;padding:0;margin-bottom:20px;font-size:21px;line-height:inherit;color:#333333;border:0;border-bottom:1px solid #e5e5e5}label{display:inline-block;max-width:100%;margin-bottom:5px;font-weight:700}input[type="search"]{box-sizing:border-box;-webkit-appearance:none;appearance:none}input[type="radio"],input[type="checkbox"]{margin:4px 0 0;margin-top:1px \9;line-height:normal}input[type="radio"][disabled],input[type="checkbox"][disabled],input[type="radio"].disabled,input[type="checkbox"].disabled,fieldset[disabled] input[type="radio"],fieldset[disabled] input[type="checkbox"]{cursor:not-allowed}input[type="file"]{display:block}input[type="range"]{display:block;width:100%}select[multiple],select[size]{height:auto}input[type="file"]:focus,input[type="radio"]:focus,input[type="checkbox"]:focus{outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}output{display:block;padding-top:8px;font-size:14px;line-height:1.42857143;color:#555555}.form-control{display:block;width:100%;height:38px;padding:7px 12px;font-size:14px;line-height:1.42857143;color:#555555;background-color:#ffffff;background-image:none;border:1px solid #e7e7e7;border-radius:4px;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s}.form-control:focus{border-color:#66afe9;outline:0;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 8px rgba(102,175,233,0.6)}.form-control::-moz-placeholder{color:#999999;opacity:1}.form-control:-ms-input-placeholder{color:#999999}.form-control::-webkit-input-placeholder{color:#999999}.form-control::-ms-expand{background-color:transparent;border:0}.form-control[disabled],.form-control[readonly],fieldset[disabled] .form-control{background-color:#eeeeee;opacity:1}.form-control[disabled],fieldset[disabled] .form-control{cursor:not-allowed}textarea.form-control{height:auto}@media screen and (-webkit-min-device-pixel-ratio:0){input[type="date"].form-control,input[type="time"].form-control,input[type="datetime-local"].form-control,input[type="month"].form-control{line-height:38px}input[type="date"].input-sm,input[type="time"].input-sm,input[type="datetime-local"].input-sm,input[type="month"].input-sm,.input-group-sm input[type="date"],.input-group-sm input[type="time"],.input-group-sm input[type="datetime-local"],.input-group-sm input[type="month"]{line-height:28px}input[type="date"].input-lg,input[type="time"].input-lg,input[type="datetime-local"].input-lg,input[type="month"].input-lg,.input-group-lg input[type="date"],.input-group-lg input[type="time"],.input-group-lg input[type="datetime-local"],.input-group-lg input[type="month"]{line-height:52px}}.form-group{margin-bottom:15px}.radio,.checkbox{position:relative;display:block;margin-top:10px;margin-bottom:10px}.radio.disabled label,.checkbox.disabled label,fieldset[disabled] .radio label,fieldset[disabled] .checkbox label{cursor:not-allowed}.radio label,.checkbox label{min-height:20px;padding-left:20px;margin-bottom:0;font-weight:400;cursor:pointer}.radio input[type="radio"],.radio-inline input[type="radio"],.checkbox input[type="checkbox"],.checkbox-inline input[type="checkbox"]{position:absolute;margin-top:4px \9;margin-left:-20px}.radio+.radio,.checkbox+.checkbox{margin-top:-5px}.radio-inline,.checkbox-inline{position:relative;display:inline-block;padding-left:20px;margin-bottom:0;font-weight:400;vertical-align:middle;cursor:pointer}.radio-inline.disabled,.checkbox-inline.disabled,fieldset[disabled] .radio-inline,fieldset[disabled] .checkbox-inline{cursor:not-allowed}.radio-inline+.radio-inline,.checkbox-inline+.checkbox-inline{margin-top:0;margin-left:10px}.form-control-static{min-height:34px;padding-top:8px;padding-bottom:8px;margin-bottom:0}.form-control-static.input-lg,.form-control-static.input-sm{padding-right:0;padding-left:0}.input-sm{height:28px;padding:4px 10px;font-size:12px;line-height:1.5;border-radius:2px}select.input-sm{height:28px;line-height:28px}textarea.input-sm,select[multiple].input-sm{height:auto}.form-group-sm .form-control{height:28px;padding:4px 10px;font-size:12px;line-height:1.5;border-radius:2px}.form-group-sm select.form-control{height:28px;line-height:28px}.form-group-sm textarea.form-control,.form-group-sm select[multiple].form-control{height:auto}.form-group-sm .form-control-static{height:28px;min-height:32px;padding:5px 10px;font-size:12px;line-height:1.5}.input-lg{height:52px;padding:13px 16px;font-size:18px;line-height:1.3333333;border-radius:5px}select.input-lg{height:52px;line-height:52px}textarea.input-lg,select[multiple].input-lg{height:auto}.form-group-lg .form-control{height:52px;padding:13px 16px;font-size:18px;line-height:1.3333333;border-radius:5px}.form-group-lg select.form-control{height:52px;line-height:52px}.form-group-lg textarea.form-control,.form-group-lg select[multiple].form-control{height:auto}.form-group-lg .form-control-static{height:52px;min-height:38px;padding:14px 16px;font-size:18px;line-height:1.3333333}.has-feedback{position:relative}.has-feedback .form-control{padding-right:47.5px}.form-control-feedback{position:absolute;top:0;right:0;z-index:2;display:block;width:38px;height:38px;line-height:38px;text-align:center;pointer-events:none}.input-lg+.form-control-feedback,.input-group-lg+.form-control-feedback,.form-group-lg .form-control+.form-control-feedback{width:52px;height:52px;line-height:52px}.input-sm+.form-control-feedback,.input-group-sm+.form-control-feedback,.form-group-sm .form-control+.form-control-feedback{width:28px;height:28px;line-height:28px}.has-success .help-block,.has-success .control-label,.has-success .radio,.has-success .checkbox,.has-success .radio-inline,.has-success .checkbox-inline,.has-success.radio label,.has-success.checkbox label,.has-success.radio-inline label,.has-success.checkbox-inline label{color:#ffffff}.has-success .form-control{border-color:#ffffff;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.has-success .form-control:focus{border-color:#e6e6e6;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #fff}.has-success .input-group-addon{color:#ffffff;background-color:#28b62c;border-color:#ffffff}.has-success .form-control-feedback{color:#ffffff}.has-warning .help-block,.has-warning .control-label,.has-warning .radio,.has-warning .checkbox,.has-warning .radio-inline,.has-warning .checkbox-inline,.has-warning.radio label,.has-warning.checkbox label,.has-warning.radio-inline label,.has-warning.checkbox-inline label{color:#ffffff}.has-warning .form-control{border-color:#ffffff;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.has-warning .form-control:focus{border-color:#e6e6e6;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #fff}.has-warning .input-group-addon{color:#ffffff;background-color:#ff851b;border-color:#ffffff}.has-warning .form-control-feedback{color:#ffffff}.has-error .help-block,.has-error .control-label,.has-error .radio,.has-error .checkbox,.has-error .radio-inline,.has-error .checkbox-inline,.has-error.radio label,.has-error.checkbox label,.has-error.radio-inline label,.has-error.checkbox-inline label{color:#ffffff}.has-error .form-control{border-color:#ffffff;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.has-error .form-control:focus{border-color:#e6e6e6;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #fff}.has-error .input-group-addon{color:#ffffff;background-color:#ff4136;border-color:#ffffff}.has-error .form-control-feedback{color:#ffffff}.has-feedback label~.form-control-feedback{top:25px}.has-feedback label.sr-only~.form-control-feedback{top:0}.help-block{display:block;margin-top:5px;margin-bottom:10px;color:#959595}@media (min-width:768px){.form-inline .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .form-control-static{display:inline-block}.form-inline .input-group{display:inline-table;vertical-align:middle}.form-inline .input-group .input-group-addon,.form-inline .input-group .input-group-btn,.form-inline .input-group .form-control{width:auto}.form-inline .input-group>.form-control{width:100%}.form-inline .control-label{margin-bottom:0;vertical-align:middle}.form-inline .radio,.form-inline .checkbox{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.form-inline .radio label,.form-inline .checkbox label{padding-left:0}.form-inline .radio input[type="radio"],.form-inline .checkbox input[type="checkbox"]{position:relative;margin-left:0}.form-inline .has-feedback .form-control-feedback{top:0}}.form-horizontal .radio,.form-horizontal .checkbox,.form-horizontal .radio-inline,.form-horizontal .checkbox-inline{padding-top:8px;margin-top:0;margin-bottom:0}.form-horizontal .radio,.form-horizontal .checkbox{min-height:28px}.form-horizontal .form-group{margin-right:-15px;margin-left:-15px}@media (min-width:768px){.form-horizontal .control-label{padding-top:8px;margin-bottom:0;text-align:right}}.form-horizontal .has-feedback .form-control-feedback{right:15px}@media (min-width:768px){.form-horizontal .form-group-lg .control-label{padding-top:14px;font-size:18px}}@media (min-width:768px){.form-horizontal .form-group-sm .control-label{padding-top:5px;font-size:12px}}.btn{display:inline-block;margin-bottom:0;font-weight:normal;text-align:center;white-space:nowrap;vertical-align:middle;-ms-touch-action:manipulation;touch-action:manipulation;cursor:pointer;background-image:none;border:1px solid transparent;padding:7px 12px;font-size:14px;line-height:1.42857143;border-radius:4px;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.btn:focus,.btn:active:focus,.btn.active:focus,.btn.focus,.btn:active.focus,.btn.active.focus{outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}.btn:hover,.btn:focus,.btn.focus{color:#555555;text-decoration:none}.btn:active,.btn.active{background-image:none;outline:0;box-shadow:inset 0 3px 5px rgba(0,0,0,0.125)}.btn.disabled,.btn[disabled],fieldset[disabled] .btn{cursor:not-allowed;filter:alpha(opacity=65);opacity:0.65;box-shadow:none}a.btn.disabled,fieldset[disabled] a.btn{pointer-events:none}.btn-default{color:#555555;background-color:#eeeeee;border-color:#e2e2e2}.btn-default:focus,.btn-default.focus{color:#555555;background-color:#d5d5d5;border-color:#a2a2a2}.btn-default:hover{color:#555555;background-color:#d5d5d5;border-color:#c3c3c3}.btn-default:active,.btn-default.active,.open>.dropdown-toggle.btn-default{color:#555555;background-color:#d5d5d5;background-image:none;border-color:#c3c3c3}.btn-default:active:hover,.btn-default.active:hover,.open>.dropdown-toggle.btn-default:hover,.btn-default:active:focus,.btn-default.active:focus,.open>.dropdown-toggle.btn-default:focus,.btn-default:active.focus,.btn-default.active.focus,.open>.dropdown-toggle.btn-default.focus{color:#555555;background-color:#c3c3c3;border-color:#a2a2a2}.btn-default.disabled:hover,.btn-default[disabled]:hover,fieldset[disabled] .btn-default:hover,.btn-default.disabled:focus,.btn-default[disabled]:focus,fieldset[disabled] .btn-default:focus,.btn-default.disabled.focus,.btn-default[disabled].focus,fieldset[disabled] .btn-default.focus{background-color:#eeeeee;border-color:#e2e2e2}.btn-default .badge{color:#eeeeee;background-color:#555555}.btn-primary{color:#ffffff;background-color:#158cba;border-color:#127ba3}.btn-primary:focus,.btn-primary.focus{color:#ffffff;background-color:#106a8c;border-color:#052531}.btn-primary:hover{color:#ffffff;background-color:#106a8c;border-color:#0c516c}.btn-primary:active,.btn-primary.active,.open>.dropdown-toggle.btn-primary{color:#ffffff;background-color:#106a8c;background-image:none;border-color:#0c516c}.btn-primary:active:hover,.btn-primary.active:hover,.open>.dropdown-toggle.btn-primary:hover,.btn-primary:active:focus,.btn-primary.active:focus,.open>.dropdown-toggle.btn-primary:focus,.btn-primary:active.focus,.btn-primary.active.focus,.open>.dropdown-toggle.btn-primary.focus{color:#ffffff;background-color:#0c516c;border-color:#052531}.btn-primary.disabled:hover,.btn-primary[disabled]:hover,fieldset[disabled] .btn-primary:hover,.btn-primary.disabled:focus,.btn-primary[disabled]:focus,fieldset[disabled] .btn-primary:focus,.btn-primary.disabled.focus,.btn-primary[disabled].focus,fieldset[disabled] .btn-primary.focus{background-color:#158cba;border-color:#127ba3}.btn-primary .badge{color:#158cba;background-color:#ffffff}.btn-success{color:#ffffff;background-color:#28b62c;border-color:#23a127}.btn-success:focus,.btn-success.focus{color:#ffffff;background-color:#1f8c22;border-color:#0c390e}.btn-success:hover{color:#ffffff;background-color:#1f8c22;border-color:#186f1b}.btn-success:active,.btn-success.active,.open>.dropdown-toggle.btn-success{color:#ffffff;background-color:#1f8c22;background-image:none;border-color:#186f1b}.btn-success:active:hover,.btn-success.active:hover,.open>.dropdown-toggle.btn-success:hover,.btn-success:active:focus,.btn-success.active:focus,.open>.dropdown-toggle.btn-success:focus,.btn-success:active.focus,.btn-success.active.focus,.open>.dropdown-toggle.btn-success.focus{color:#ffffff;background-color:#186f1b;border-color:#0c390e}.btn-success.disabled:hover,.btn-success[disabled]:hover,fieldset[disabled] .btn-success:hover,.btn-success.disabled:focus,.btn-success[disabled]:focus,fieldset[disabled] .btn-success:focus,.btn-success.disabled.focus,.btn-success[disabled].focus,fieldset[disabled] .btn-success.focus{background-color:#28b62c;border-color:#23a127}.btn-success .badge{color:#28b62c;background-color:#ffffff}.btn-info{color:#ffffff;background-color:#75caeb;border-color:#5fc1e8}.btn-info:focus,.btn-info.focus{color:#ffffff;background-color:#48b9e5;border-color:#1984ae}.btn-info:hover{color:#ffffff;background-color:#48b9e5;border-color:#29ade0}.btn-info:active,.btn-info.active,.open>.dropdown-toggle.btn-info{color:#ffffff;background-color:#48b9e5;background-image:none;border-color:#29ade0}.btn-info:active:hover,.btn-info.active:hover,.open>.dropdown-toggle.btn-info:hover,.btn-info:active:focus,.btn-info.active:focus,.open>.dropdown-toggle.btn-info:focus,.btn-info:active.focus,.btn-info.active.focus,.open>.dropdown-toggle.btn-info.focus{color:#ffffff;background-color:#29ade0;border-color:#1984ae}.btn-info.disabled:hover,.btn-info[disabled]:hover,fieldset[disabled] .btn-info:hover,.btn-info.disabled:focus,.btn-info[disabled]:focus,fieldset[disabled] .btn-info:focus,.btn-info.disabled.focus,.btn-info[disabled].focus,fieldset[disabled] .btn-info.focus{background-color:#75caeb;border-color:#5fc1e8}.btn-info .badge{color:#75caeb;background-color:#ffffff}.btn-warning{color:#ffffff;background-color:#ff851b;border-color:#ff7701}.btn-warning:focus,.btn-warning.focus{color:#ffffff;background-color:#e76b00;border-color:#813c00}.btn-warning:hover{color:#ffffff;background-color:#e76b00;border-color:#c35b00}.btn-warning:active,.btn-warning.active,.open>.dropdown-toggle.btn-warning{color:#ffffff;background-color:#e76b00;background-image:none;border-color:#c35b00}.btn-warning:active:hover,.btn-warning.active:hover,.open>.dropdown-toggle.btn-warning:hover,.btn-warning:active:focus,.btn-warning.active:focus,.open>.dropdown-toggle.btn-warning:focus,.btn-warning:active.focus,.btn-warning.active.focus,.open>.dropdown-toggle.btn-warning.focus{color:#ffffff;background-color:#c35b00;border-color:#813c00}.btn-warning.disabled:hover,.btn-warning[disabled]:hover,fieldset[disabled] .btn-warning:hover,.btn-warning.disabled:focus,.btn-warning[disabled]:focus,fieldset[disabled] .btn-warning:focus,.btn-warning.disabled.focus,.btn-warning[disabled].focus,fieldset[disabled] .btn-warning.focus{background-color:#ff851b;border-color:#ff7701}.btn-warning .badge{color:#ff851b;background-color:#ffffff}.btn-danger{color:#ffffff;background-color:#ff4136;border-color:#ff291c}.btn-danger:focus,.btn-danger.focus{color:#ffffff;background-color:#ff1103;border-color:#9c0900}.btn-danger:hover{color:#ffffff;background-color:#ff1103;border-color:#de0c00}.btn-danger:active,.btn-danger.active,.open>.dropdown-toggle.btn-danger{color:#ffffff;background-color:#ff1103;background-image:none;border-color:#de0c00}.btn-danger:active:hover,.btn-danger.active:hover,.open>.dropdown-toggle.btn-danger:hover,.btn-danger:active:focus,.btn-danger.active:focus,.open>.dropdown-toggle.btn-danger:focus,.btn-danger:active.focus,.btn-danger.active.focus,.open>.dropdown-toggle.btn-danger.focus{color:#ffffff;background-color:#de0c00;border-color:#9c0900}.btn-danger.disabled:hover,.btn-danger[disabled]:hover,fieldset[disabled] .btn-danger:hover,.btn-danger.disabled:focus,.btn-danger[disabled]:focus,fieldset[disabled] .btn-danger:focus,.btn-danger.disabled.focus,.btn-danger[disabled].focus,fieldset[disabled] .btn-danger.focus{background-color:#ff4136;border-color:#ff291c}.btn-danger .badge{color:#ff4136;background-color:#ffffff}.btn-link{font-weight:400;color:#158cba;border-radius:0}.btn-link,.btn-link:active,.btn-link.active,.btn-link[disabled],fieldset[disabled] .btn-link{background-color:transparent;box-shadow:none}.btn-link,.btn-link:hover,.btn-link:focus,.btn-link:active{border-color:transparent}.btn-link:hover,.btn-link:focus{color:#158cba;text-decoration:underline;background-color:transparent}.btn-link[disabled]:hover,fieldset[disabled] .btn-link:hover,.btn-link[disabled]:focus,fieldset[disabled] .btn-link:focus{color:#999999;text-decoration:none}.btn-lg,.btn-group-lg>.btn{padding:13px 16px;font-size:18px;line-height:1.3333333;border-radius:5px}.btn-sm,.btn-group-sm>.btn{padding:4px 10px;font-size:12px;line-height:1.5;border-radius:2px}.btn-xs,.btn-group-xs>.btn{padding:1px 5px;font-size:12px;line-height:1.5;border-radius:2px}.btn-block{display:block;width:100%}.btn-block+.btn-block{margin-top:5px}input[type="submit"].btn-block,input[type="reset"].btn-block,input[type="button"].btn-block{width:100%}.fade{opacity:0;transition:opacity 0.15s linear}.fade.in{opacity:1}.collapse{display:none}.collapse.in{display:block}tr.collapse.in{display:table-row}tbody.collapse.in{display:table-row-group}.collapsing{position:relative;height:0;overflow:hidden;transition-property:height, visibility;transition-duration:0.35s;transition-timing-function:ease}.caret{display:inline-block;width:0;height:0;margin-left:2px;vertical-align:middle;border-top:4px dashed;border-top:4px solid \9;border-right:4px solid transparent;border-left:4px solid transparent}.dropup,.dropdown{position:relative}.dropdown-toggle:focus{outline:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:160px;padding:5px 0;margin:2px 0 0;font-size:14px;text-align:left;list-style:none;background-color:#ffffff;background-clip:padding-box;border:1px solid #cccccc;border:1px solid #e7e7e7;border-radius:4px;box-shadow:0 6px 12px rgba(0,0,0,0.175)}.dropdown-menu.pull-right{right:0;left:auto}.dropdown-menu .divider{height:1px;margin:9px 0;overflow:hidden;background-color:#eeeeee}.dropdown-menu>li>a{display:block;padding:3px 20px;clear:both;font-weight:400;line-height:1.42857143;color:#999999;white-space:nowrap}.dropdown-menu>li>a:hover,.dropdown-menu>li>a:focus{color:#333333;text-decoration:none;background-color:transparent}.dropdown-menu>.active>a,.dropdown-menu>.active>a:hover,.dropdown-menu>.active>a:focus{color:#ffffff;text-decoration:none;background-color:#158cba;outline:0}.dropdown-menu>.disabled>a,.dropdown-menu>.disabled>a:hover,.dropdown-menu>.disabled>a:focus{color:#eeeeee}.dropdown-menu>.disabled>a:hover,.dropdown-menu>.disabled>a:focus{text-decoration:none;cursor:not-allowed;background-color:transparent;background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.open>.dropdown-menu{display:block}.open>a{outline:0}.dropdown-menu-right{right:0;left:auto}.dropdown-menu-left{right:auto;left:0}.dropdown-header{display:block;padding:3px 20px;font-size:12px;line-height:1.42857143;color:#999999;white-space:nowrap}.dropdown-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:990}.pull-right>.dropdown-menu{right:0;left:auto}.dropup .caret,.navbar-fixed-bottom .dropdown .caret{content:"";border-top:0;border-bottom:4px dashed;border-bottom:4px solid \9}.dropup .dropdown-menu,.navbar-fixed-bottom .dropdown .dropdown-menu{top:auto;bottom:100%;margin-bottom:2px}@media (min-width:768px){.navbar-right .dropdown-menu{right:0;left:auto}.navbar-right .dropdown-menu-left{right:auto;left:0}}.btn-group,.btn-group-vertical{position:relative;display:inline-block;vertical-align:middle}.btn-group>.btn,.btn-group-vertical>.btn{position:relative;float:left}.btn-group>.btn:hover,.btn-group-vertical>.btn:hover,.btn-group>.btn:focus,.btn-group-vertical>.btn:focus,.btn-group>.btn:active,.btn-group-vertical>.btn:active,.btn-group>.btn.active,.btn-group-vertical>.btn.active{z-index:2}.btn-group .btn+.btn,.btn-group .btn+.btn-group,.btn-group .btn-group+.btn,.btn-group .btn-group+.btn-group{margin-left:-1px}.btn-toolbar{margin-left:-5px}.btn-toolbar .btn,.btn-toolbar .btn-group,.btn-toolbar .input-group{float:left}.btn-toolbar>.btn,.btn-toolbar>.btn-group,.btn-toolbar>.input-group{margin-left:5px}.btn-group>.btn:not(:first-child):not(:last-child):not(.dropdown-toggle){border-radius:0}.btn-group>.btn:first-child{margin-left:0}.btn-group>.btn:first-child:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn:last-child:not(:first-child),.btn-group>.dropdown-toggle:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.btn-group>.btn-group{float:left}.btn-group>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:last-child:not(:first-child)>.btn:first-child{border-top-left-radius:0;border-bottom-left-radius:0}.btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle{outline:0}.btn-group>.btn+.dropdown-toggle{padding-right:8px;padding-left:8px}.btn-group>.btn-lg+.dropdown-toggle{padding-right:12px;padding-left:12px}.btn-group.open .dropdown-toggle{box-shadow:inset 0 3px 5px rgba(0,0,0,0.125)}.btn-group.open .dropdown-toggle.btn-link{box-shadow:none}.btn .caret{margin-left:0}.btn-lg .caret{border-width:5px 5px 0;border-bottom-width:0}.dropup .btn-lg .caret{border-width:0 5px 5px}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group,.btn-group-vertical>.btn-group>.btn{display:block;float:none;width:100%;max-width:100%}.btn-group-vertical>.btn-group>.btn{float:none}.btn-group-vertical>.btn+.btn,.btn-group-vertical>.btn+.btn-group,.btn-group-vertical>.btn-group+.btn,.btn-group-vertical>.btn-group+.btn-group{margin-top:-1px;margin-left:0}.btn-group-vertical>.btn:not(:first-child):not(:last-child){border-radius:0}.btn-group-vertical>.btn:first-child:not(:last-child){border-top-left-radius:4px;border-top-right-radius:4px;border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn:last-child:not(:first-child){border-top-left-radius:0;border-top-right-radius:0;border-bottom-right-radius:4px;border-bottom-left-radius:4px}.btn-group-vertical>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group-vertical>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group-vertical>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:last-child:not(:first-child)>.btn:first-child{border-top-left-radius:0;border-top-right-radius:0}.btn-group-justified{display:table;width:100%;table-layout:fixed;border-collapse:separate}.btn-group-justified>.btn,.btn-group-justified>.btn-group{display:table-cell;float:none;width:1%}.btn-group-justified>.btn-group .btn{width:100%}.btn-group-justified>.btn-group .dropdown-menu{left:auto}[data-toggle="buttons"]>.btn input[type="radio"],[data-toggle="buttons"]>.btn-group>.btn input[type="radio"],[data-toggle="buttons"]>.btn input[type="checkbox"],[data-toggle="buttons"]>.btn-group>.btn input[type="checkbox"]{position:absolute;clip:rect(0, 0, 0, 0);pointer-events:none}.input-group{position:relative;display:table;border-collapse:separate}.input-group[class*="col-"]{float:none;padding-right:0;padding-left:0}.input-group .form-control{position:relative;z-index:2;float:left;width:100%;margin-bottom:0}.input-group .form-control:focus{z-index:3}.input-group-lg>.form-control,.input-group-lg>.input-group-addon,.input-group-lg>.input-group-btn>.btn{height:52px;padding:13px 16px;font-size:18px;line-height:1.3333333;border-radius:5px}select.input-group-lg>.form-control,select.input-group-lg>.input-group-addon,select.input-group-lg>.input-group-btn>.btn{height:52px;line-height:52px}textarea.input-group-lg>.form-control,textarea.input-group-lg>.input-group-addon,textarea.input-group-lg>.input-group-btn>.btn,select[multiple].input-group-lg>.form-control,select[multiple].input-group-lg>.input-group-addon,select[multiple].input-group-lg>.input-group-btn>.btn{height:auto}.input-group-sm>.form-control,.input-group-sm>.input-group-addon,.input-group-sm>.input-group-btn>.btn{height:28px;padding:4px 10px;font-size:12px;line-height:1.5;border-radius:2px}select.input-group-sm>.form-control,select.input-group-sm>.input-group-addon,select.input-group-sm>.input-group-btn>.btn{height:28px;line-height:28px}textarea.input-group-sm>.form-control,textarea.input-group-sm>.input-group-addon,textarea.input-group-sm>.input-group-btn>.btn,select[multiple].input-group-sm>.form-control,select[multiple].input-group-sm>.input-group-addon,select[multiple].input-group-sm>.input-group-btn>.btn{height:auto}.input-group-addon,.input-group-btn,.input-group .form-control{display:table-cell}.input-group-addon:not(:first-child):not(:last-child),.input-group-btn:not(:first-child):not(:last-child),.input-group .form-control:not(:first-child):not(:last-child){border-radius:0}.input-group-addon,.input-group-btn{width:1%;white-space:nowrap;vertical-align:middle}.input-group-addon{padding:7px 12px;font-size:14px;font-weight:400;line-height:1;color:#555555;text-align:center;background-color:#eeeeee;border:1px solid #e7e7e7;border-radius:4px}.input-group-addon.input-sm{padding:4px 10px;font-size:12px;border-radius:2px}.input-group-addon.input-lg{padding:13px 16px;font-size:18px;border-radius:5px}.input-group-addon input[type="radio"],.input-group-addon input[type="checkbox"]{margin-top:0}.input-group .form-control:first-child,.input-group-addon:first-child,.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group>.btn,.input-group-btn:first-child>.dropdown-toggle,.input-group-btn:last-child>.btn:not(:last-child):not(.dropdown-toggle),.input-group-btn:last-child>.btn-group:not(:last-child)>.btn{border-top-right-radius:0;border-bottom-right-radius:0}.input-group-addon:first-child{border-right:0}.input-group .form-control:last-child,.input-group-addon:last-child,.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group>.btn,.input-group-btn:last-child>.dropdown-toggle,.input-group-btn:first-child>.btn:not(:first-child),.input-group-btn:first-child>.btn-group:not(:first-child)>.btn{border-top-left-radius:0;border-bottom-left-radius:0}.input-group-addon:last-child{border-left:0}.input-group-btn{position:relative;font-size:0;white-space:nowrap}.input-group-btn>.btn{position:relative}.input-group-btn>.btn+.btn{margin-left:-1px}.input-group-btn>.btn:hover,.input-group-btn>.btn:focus,.input-group-btn>.btn:active{z-index:2}.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group{margin-right:-1px}.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group{z-index:2;margin-left:-1px}.nav{padding-left:0;margin-bottom:0;list-style:none}.nav>li{position:relative;display:block}.nav>li>a{position:relative;display:block;padding:10px 15px}.nav>li>a:hover,.nav>li>a:focus{text-decoration:none;background-color:#ffffff}.nav>li.disabled>a{color:#999999}.nav>li.disabled>a:hover,.nav>li.disabled>a:focus{color:#999999;text-decoration:none;cursor:not-allowed;background-color:transparent}.nav .open>a,.nav .open>a:hover,.nav .open>a:focus{background-color:#ffffff;border-color:#158cba}.nav .nav-divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.nav>li>a>img{max-width:none}.nav-tabs{border-bottom:1px solid #e7e7e7}.nav-tabs>li{float:left;margin-bottom:-1px}.nav-tabs>li>a{margin-right:2px;line-height:1.42857143;border:1px solid transparent;border-radius:4px 4px 0 0}.nav-tabs>li>a:hover{border-color:#eeeeee #eeeeee #e7e7e7}.nav-tabs>li.active>a,.nav-tabs>li.active>a:hover,.nav-tabs>li.active>a:focus{color:#555555;cursor:default;background-color:#ffffff;border:1px solid #e7e7e7;border-bottom-color:transparent}.nav-tabs.nav-justified{width:100%;border-bottom:0}.nav-tabs.nav-justified>li{float:none}.nav-tabs.nav-justified>li>a{margin-bottom:5px;text-align:center}.nav-tabs.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media (min-width:768px){.nav-tabs.nav-justified>li{display:table-cell;width:1%}.nav-tabs.nav-justified>li>a{margin-bottom:0}}.nav-tabs.nav-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:hover,.nav-tabs.nav-justified>.active>a:focus{border:1px solid #e7e7e7}@media (min-width:768px){.nav-tabs.nav-justified>li>a{border-bottom:1px solid #e7e7e7;border-radius:4px 4px 0 0}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:hover,.nav-tabs.nav-justified>.active>a:focus{border-bottom-color:#ffffff}}.nav-pills>li{float:left}.nav-pills>li>a{border-radius:4px}.nav-pills>li+li{margin-left:2px}.nav-pills>li.active>a,.nav-pills>li.active>a:hover,.nav-pills>li.active>a:focus{color:#ffffff;background-color:#158cba}.nav-stacked>li{float:none}.nav-stacked>li+li{margin-top:2px;margin-left:0}.nav-justified{width:100%}.nav-justified>li{float:none}.nav-justified>li>a{margin-bottom:5px;text-align:center}.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media (min-width:768px){.nav-justified>li{display:table-cell;width:1%}.nav-justified>li>a{margin-bottom:0}}.nav-tabs-justified{border-bottom:0}.nav-tabs-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:hover,.nav-tabs-justified>.active>a:focus{border:1px solid #e7e7e7}@media (min-width:768px){.nav-tabs-justified>li>a{border-bottom:1px solid #e7e7e7;border-radius:4px 4px 0 0}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:hover,.nav-tabs-justified>.active>a:focus{border-bottom-color:#ffffff}}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-left-radius:0;border-top-right-radius:0}.navbar{position:relative;min-height:50px;margin-bottom:20px;border:1px solid transparent}@media (min-width:768px){.navbar{border-radius:4px}}@media (min-width:768px){.navbar-header{float:left}}.navbar-collapse{padding-right:15px;padding-left:15px;overflow-x:visible;border-top:1px solid transparent;box-shadow:inset 0 1px 0 rgba(255,255,255,0.1);-webkit-overflow-scrolling:touch}.navbar-collapse.in{overflow-y:auto}@media (min-width:768px){.navbar-collapse{width:auto;border-top:0;box-shadow:none}.navbar-collapse.collapse{display:block !important;height:auto !important;padding-bottom:0;overflow:visible !important}.navbar-collapse.in{overflow-y:visible}.navbar-fixed-top .navbar-collapse,.navbar-static-top .navbar-collapse,.navbar-fixed-bottom .navbar-collapse{padding-right:0;padding-left:0}}.navbar-fixed-top,.navbar-fixed-bottom{position:fixed;right:0;left:0;z-index:1030}.navbar-fixed-top .navbar-collapse,.navbar-fixed-bottom .navbar-collapse{max-height:340px}@media (max-device-width:480px) and (orientation:landscape){.navbar-fixed-top .navbar-collapse,.navbar-fixed-bottom .navbar-collapse{max-height:200px}}@media (min-width:768px){.navbar-fixed-top,.navbar-fixed-bottom{border-radius:0}}.navbar-fixed-top{top:0;border-width:0 0 1px}.navbar-fixed-bottom{bottom:0;margin-bottom:0;border-width:1px 0 0}.container>.navbar-header,.container-fluid>.navbar-header,.container>.navbar-collapse,.container-fluid>.navbar-collapse{margin-right:-15px;margin-left:-15px}@media (min-width:768px){.container>.navbar-header,.container-fluid>.navbar-header,.container>.navbar-collapse,.container-fluid>.navbar-collapse{margin-right:0;margin-left:0}}.navbar-static-top{z-index:1000;border-width:0 0 1px}@media (min-width:768px){.navbar-static-top{border-radius:0}}.navbar-brand{float:left;height:50px;padding:15px 15px;font-size:18px;line-height:20px}.navbar-brand:hover,.navbar-brand:focus{text-decoration:none}.navbar-brand>img{display:block}@media (min-width:768px){.navbar>.container .navbar-brand,.navbar>.container-fluid .navbar-brand{margin-left:-15px}}.navbar-toggle{position:relative;float:right;padding:9px 10px;margin-right:15px;margin-top:8px;margin-bottom:8px;background-color:transparent;background-image:none;border:1px solid transparent;border-radius:4px}.navbar-toggle:focus{outline:0}.navbar-toggle .icon-bar{display:block;width:22px;height:2px;border-radius:1px}.navbar-toggle .icon-bar+.icon-bar{margin-top:4px}@media (min-width:768px){.navbar-toggle{display:none}}.navbar-nav{margin:7.5px -15px}.navbar-nav>li>a{padding-top:10px;padding-bottom:10px;line-height:20px}@media (max-width:767px){.navbar-nav .open .dropdown-menu{position:static;float:none;width:auto;margin-top:0;background-color:transparent;border:0;box-shadow:none}.navbar-nav .open .dropdown-menu>li>a,.navbar-nav .open .dropdown-menu .dropdown-header{padding:5px 15px 5px 25px}.navbar-nav .open .dropdown-menu>li>a{line-height:20px}.navbar-nav .open .dropdown-menu>li>a:hover,.navbar-nav .open .dropdown-menu>li>a:focus{background-image:none}}@media (min-width:768px){.navbar-nav{float:left;margin:0}.navbar-nav>li{float:left}.navbar-nav>li>a{padding-top:15px;padding-bottom:15px}}.navbar-form{padding:10px 15px;margin-right:-15px;margin-left:-15px;border-top:1px solid transparent;border-bottom:1px solid transparent;box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1);margin-top:6px;margin-bottom:6px}@media (min-width:768px){.navbar-form .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.navbar-form .form-control{display:inline-block;width:auto;vertical-align:middle}.navbar-form .form-control-static{display:inline-block}.navbar-form .input-group{display:inline-table;vertical-align:middle}.navbar-form .input-group .input-group-addon,.navbar-form .input-group .input-group-btn,.navbar-form .input-group .form-control{width:auto}.navbar-form .input-group>.form-control{width:100%}.navbar-form .control-label{margin-bottom:0;vertical-align:middle}.navbar-form .radio,.navbar-form .checkbox{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.navbar-form .radio label,.navbar-form .checkbox label{padding-left:0}.navbar-form .radio input[type="radio"],.navbar-form .checkbox input[type="checkbox"]{position:relative;margin-left:0}.navbar-form .has-feedback .form-control-feedback{top:0}}@media (max-width:767px){.navbar-form .form-group{margin-bottom:5px}.navbar-form .form-group:last-child{margin-bottom:0}}@media (min-width:768px){.navbar-form{width:auto;padding-top:0;padding-bottom:0;margin-right:0;margin-left:0;border:0;box-shadow:none}}.navbar-nav>li>.dropdown-menu{margin-top:0;border-top-left-radius:0;border-top-right-radius:0}.navbar-fixed-bottom .navbar-nav>li>.dropdown-menu{margin-bottom:0;border-top-left-radius:4px;border-top-right-radius:4px;border-bottom-right-radius:0;border-bottom-left-radius:0}.navbar-btn{margin-top:6px;margin-bottom:6px}.navbar-btn.btn-sm{margin-top:11px;margin-bottom:11px}.navbar-btn.btn-xs{margin-top:14px;margin-bottom:14px}.navbar-text{margin-top:15px;margin-bottom:15px}@media (min-width:768px){.navbar-text{float:left;margin-right:15px;margin-left:15px}}@media (min-width:768px){.navbar-left{float:left !important}.navbar-right{float:right !important;margin-right:-15px}.navbar-right~.navbar-right{margin-right:0}}.navbar-default{background-color:#f8f8f8;border-color:#e7e7e7}.navbar-default .navbar-brand{color:#333333}.navbar-default .navbar-brand:hover,.navbar-default .navbar-brand:focus{color:#333333;background-color:transparent}.navbar-default .navbar-text{color:#555555}.navbar-default .navbar-nav>li>a{color:#999999}.navbar-default .navbar-nav>li>a:hover,.navbar-default .navbar-nav>li>a:focus{color:#333333;background-color:transparent}.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.active>a:hover,.navbar-default .navbar-nav>.active>a:focus{color:#333333;background-color:transparent}.navbar-default .navbar-nav>.disabled>a,.navbar-default .navbar-nav>.disabled>a:hover,.navbar-default .navbar-nav>.disabled>a:focus{color:#eeeeee;background-color:transparent}.navbar-default .navbar-nav>.open>a,.navbar-default .navbar-nav>.open>a:hover,.navbar-default .navbar-nav>.open>a:focus{color:#333333;background-color:transparent}@media (max-width:767px){.navbar-default .navbar-nav .open .dropdown-menu>li>a{color:#999999}.navbar-default .navbar-nav .open .dropdown-menu>li>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>li>a:focus{color:#333333;background-color:transparent}.navbar-default .navbar-nav .open .dropdown-menu>.active>a,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:focus{color:#333333;background-color:transparent}.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:focus{color:#eeeeee;background-color:transparent}}.navbar-default .navbar-toggle{border-color:#eeeeee}.navbar-default .navbar-toggle:hover,.navbar-default .navbar-toggle:focus{background-color:#ffffff}.navbar-default .navbar-toggle .icon-bar{background-color:#999999}.navbar-default .navbar-collapse,.navbar-default .navbar-form{border-color:#e7e7e7}.navbar-default .navbar-link{color:#999999}.navbar-default .navbar-link:hover{color:#333333}.navbar-default .btn-link{color:#999999}.navbar-default .btn-link:hover,.navbar-default .btn-link:focus{color:#333333}.navbar-default .btn-link[disabled]:hover,fieldset[disabled] .navbar-default .btn-link:hover,.navbar-default .btn-link[disabled]:focus,fieldset[disabled] .navbar-default .btn-link:focus{color:#eeeeee}.navbar-inverse{background-color:#ffffff;border-color:#e6e6e6}.navbar-inverse .navbar-brand{color:#999999}.navbar-inverse .navbar-brand:hover,.navbar-inverse .navbar-brand:focus{color:#333333;background-color:transparent}.navbar-inverse .navbar-text{color:#999999}.navbar-inverse .navbar-nav>li>a{color:#999999}.navbar-inverse .navbar-nav>li>a:hover,.navbar-inverse .navbar-nav>li>a:focus{color:#333333;background-color:transparent}.navbar-inverse .navbar-nav>.active>a,.navbar-inverse .navbar-nav>.active>a:hover,.navbar-inverse .navbar-nav>.active>a:focus{color:#333333;background-color:transparent}.navbar-inverse .navbar-nav>.disabled>a,.navbar-inverse .navbar-nav>.disabled>a:hover,.navbar-inverse .navbar-nav>.disabled>a:focus{color:#eeeeee;background-color:transparent}.navbar-inverse .navbar-nav>.open>a,.navbar-inverse .navbar-nav>.open>a:hover,.navbar-inverse .navbar-nav>.open>a:focus{color:#333333;background-color:transparent}@media (max-width:767px){.navbar-inverse .navbar-nav .open .dropdown-menu>.dropdown-header{border-color:#e6e6e6}.navbar-inverse .navbar-nav .open .dropdown-menu .divider{background-color:#e6e6e6}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a{color:#999999}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:focus{color:#333333;background-color:transparent}.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:focus{color:#333333;background-color:transparent}.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:focus{color:#eeeeee;background-color:transparent}}.navbar-inverse .navbar-toggle{border-color:#eeeeee}.navbar-inverse .navbar-toggle:hover,.navbar-inverse .navbar-toggle:focus{background-color:#eeeeee}.navbar-inverse .navbar-toggle .icon-bar{background-color:#999999}.navbar-inverse .navbar-collapse,.navbar-inverse .navbar-form{border-color:#ededed}.navbar-inverse .navbar-link{color:#999999}.navbar-inverse .navbar-link:hover{color:#333333}.navbar-inverse .btn-link{color:#999999}.navbar-inverse .btn-link:hover,.navbar-inverse .btn-link:focus{color:#333333}.navbar-inverse .btn-link[disabled]:hover,fieldset[disabled] .navbar-inverse .btn-link:hover,.navbar-inverse .btn-link[disabled]:focus,fieldset[disabled] .navbar-inverse .btn-link:focus{color:#eeeeee}.breadcrumb{padding:8px 15px;margin-bottom:20px;list-style:none;background-color:#fafafa;border-radius:4px}.breadcrumb>li{display:inline-block}.breadcrumb>li+li:before{padding:0 5px;color:#999999;content:">\00a0"}.breadcrumb>.active{color:#999999}.pagination{display:inline-block;padding-left:0;margin:20px 0;border-radius:4px}.pagination>li{display:inline}.pagination>li>a,.pagination>li>span{position:relative;float:left;padding:7px 12px;margin-left:-1px;line-height:1.42857143;color:#555555;text-decoration:none;background-color:#eeeeee;border:1px solid #e2e2e2}.pagination>li>a:hover,.pagination>li>span:hover,.pagination>li>a:focus,.pagination>li>span:focus{z-index:2;color:#555555;background-color:#eeeeee;border-color:#e2e2e2}.pagination>li:first-child>a,.pagination>li:first-child>span{margin-left:0;border-top-left-radius:4px;border-bottom-left-radius:4px}.pagination>li:last-child>a,.pagination>li:last-child>span{border-top-right-radius:4px;border-bottom-right-radius:4px}.pagination>.active>a,.pagination>.active>span,.pagination>.active>a:hover,.pagination>.active>span:hover,.pagination>.active>a:focus,.pagination>.active>span:focus{z-index:3;color:#ffffff;cursor:default;background-color:#158cba;border-color:#127ba3}.pagination>.disabled>span,.pagination>.disabled>span:hover,.pagination>.disabled>span:focus,.pagination>.disabled>a,.pagination>.disabled>a:hover,.pagination>.disabled>a:focus{color:#999999;cursor:not-allowed;background-color:#eeeeee;border-color:#e2e2e2}.pagination-lg>li>a,.pagination-lg>li>span{padding:13px 16px;font-size:18px;line-height:1.3333333}.pagination-lg>li:first-child>a,.pagination-lg>li:first-child>span{border-top-left-radius:5px;border-bottom-left-radius:5px}.pagination-lg>li:last-child>a,.pagination-lg>li:last-child>span{border-top-right-radius:5px;border-bottom-right-radius:5px}.pagination-sm>li>a,.pagination-sm>li>span{padding:4px 10px;font-size:12px;line-height:1.5}.pagination-sm>li:first-child>a,.pagination-sm>li:first-child>span{border-top-left-radius:2px;border-bottom-left-radius:2px}.pagination-sm>li:last-child>a,.pagination-sm>li:last-child>span{border-top-right-radius:2px;border-bottom-right-radius:2px}.pager{padding-left:0;margin:20px 0;text-align:center;list-style:none}.pager li{display:inline}.pager li>a,.pager li>span{display:inline-block;padding:5px 14px;background-color:#eeeeee;border:1px solid #e2e2e2;border-radius:15px}.pager li>a:hover,.pager li>a:focus{text-decoration:none;background-color:#eeeeee}.pager .next>a,.pager .next>span{float:right}.pager .previous>a,.pager .previous>span{float:left}.pager .disabled>a,.pager .disabled>a:hover,.pager .disabled>a:focus,.pager .disabled>span{color:#999999;cursor:not-allowed;background-color:#eeeeee}.label{display:inline;padding:.2em .6em .3em;font-size:75%;font-weight:700;line-height:1;color:#ffffff;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25em}a.label:hover,a.label:focus{color:#ffffff;text-decoration:none;cursor:pointer}.label:empty{display:none}.btn .label{position:relative;top:-1px}.label-default{background-color:#999999}.label-default[href]:hover,.label-default[href]:focus{background-color:#808080}.label-primary{background-color:#158cba}.label-primary[href]:hover,.label-primary[href]:focus{background-color:#106a8c}.label-success{background-color:#28b62c}.label-success[href]:hover,.label-success[href]:focus{background-color:#1f8c22}.label-info{background-color:#75caeb}.label-info[href]:hover,.label-info[href]:focus{background-color:#48b9e5}.label-warning{background-color:#ff851b}.label-warning[href]:hover,.label-warning[href]:focus{background-color:#e76b00}.label-danger{background-color:#ff4136}.label-danger[href]:hover,.label-danger[href]:focus{background-color:#ff1103}.badge{display:inline-block;min-width:10px;padding:3px 7px;font-size:12px;font-weight:normal;line-height:1;color:#ffffff;text-align:center;white-space:nowrap;vertical-align:middle;background-color:#158cba;border-radius:10px}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.btn-xs .badge,.btn-group-xs>.btn .badge{top:0;padding:1px 5px}a.badge:hover,a.badge:focus{color:#ffffff;text-decoration:none;cursor:pointer}.list-group-item.active>.badge,.nav-pills>.active>a>.badge{color:#158cba;background-color:#ffffff}.list-group-item>.badge{float:right}.list-group-item>.badge+.badge{margin-right:5px}.nav-pills>li>a>.badge{margin-left:3px}.jumbotron{padding-top:30px;padding-bottom:30px;margin-bottom:30px;color:inherit;background-color:#fafafa}.jumbotron h1,.jumbotron .h1{color:inherit}.jumbotron p{margin-bottom:15px;font-size:21px;font-weight:200}.jumbotron>hr{border-top-color:#e1e1e1}.container .jumbotron,.container-fluid .jumbotron{padding-right:15px;padding-left:15px;border-radius:5px}.jumbotron .container{max-width:100%}@media screen and (min-width:768px){.jumbotron{padding-top:48px;padding-bottom:48px}.container .jumbotron,.container-fluid .jumbotron{padding-right:60px;padding-left:60px}.jumbotron h1,.jumbotron .h1{font-size:63px}}.thumbnail{display:block;padding:4px;margin-bottom:20px;line-height:1.42857143;background-color:#ffffff;border:1px solid #eeeeee;border-radius:4px;transition:border .2s ease-in-out}.thumbnail>img,.thumbnail a>img{margin-right:auto;margin-left:auto}a.thumbnail:hover,a.thumbnail:focus,a.thumbnail.active{border-color:#158cba}.thumbnail .caption{padding:9px;color:#555555}.alert{padding:15px;margin-bottom:20px;border:1px solid transparent;border-radius:4px}.alert h4{margin-top:0;color:inherit}.alert .alert-link{font-weight:bold}.alert>p,.alert>ul{margin-bottom:0}.alert>p+p{margin-top:5px}.alert-dismissable,.alert-dismissible{padding-right:35px}.alert-dismissable .close,.alert-dismissible .close{position:relative;top:-2px;right:-21px;color:inherit}.alert-success{color:#ffffff;background-color:#28b62c;border-color:#24a528}.alert-success hr{border-top-color:#209023}.alert-success .alert-link{color:#e6e6e6}.alert-info{color:#ffffff;background-color:#75caeb;border-color:#40b5e3}.alert-info hr{border-top-color:#29ade0}.alert-info .alert-link{color:#e6e6e6}.alert-warning{color:#ffffff;background-color:#ff851b;border-color:#ff7701}.alert-warning hr{border-top-color:#e76b00}.alert-warning .alert-link{color:#e6e6e6}.alert-danger{color:#ffffff;background-color:#ff4136;border-color:#ff1103}.alert-danger hr{border-top-color:#e90d00}.alert-danger .alert-link{color:#e6e6e6}@-webkit-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}.progress{height:20px;margin-bottom:20px;overflow:hidden;background-color:#fafafa;border-radius:4px;box-shadow:inset 0 1px 2px rgba(0,0,0,0.1)}.progress-bar{float:left;width:0%;height:100%;font-size:12px;line-height:20px;color:#ffffff;text-align:center;background-color:#158cba;box-shadow:inset 0 -1px 0 rgba(0,0,0,0.15);transition:width 0.6s ease}.progress-striped .progress-bar,.progress-bar-striped{background-image:linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-size:40px 40px}.progress.active .progress-bar,.progress-bar.active{-webkit-animation:progress-bar-stripes 2s linear infinite;animation:progress-bar-stripes 2s linear infinite}.progress-bar-success{background-color:#28b62c}.progress-striped .progress-bar-success{background-image:linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent)}.progress-bar-info{background-color:#75caeb}.progress-striped .progress-bar-info{background-image:linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent)}.progress-bar-warning{background-color:#ff851b}.progress-striped .progress-bar-warning{background-image:linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent)}.progress-bar-danger{background-color:#ff4136}.progress-striped .progress-bar-danger{background-image:linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent)}.media{margin-top:15px}.media:first-child{margin-top:0}.media,.media-body{overflow:hidden;zoom:1}.media-body{width:10000px}.media-object{display:block}.media-object.img-thumbnail{max-width:none}.media-right,.media>.pull-right{padding-left:10px}.media-left,.media>.pull-left{padding-right:10px}.media-left,.media-right,.media-body{display:table-cell;vertical-align:top}.media-middle{vertical-align:middle}.media-bottom{vertical-align:bottom}.media-heading{margin-top:0;margin-bottom:5px}.media-list{padding-left:0;list-style:none}.list-group{padding-left:0;margin-bottom:20px}.list-group-item{position:relative;display:block;padding:10px 15px;margin-bottom:-1px;background-color:#ffffff;border:1px solid #eeeeee}.list-group-item:first-child{border-top-left-radius:4px;border-top-right-radius:4px}.list-group-item:last-child{margin-bottom:0;border-bottom-right-radius:4px;border-bottom-left-radius:4px}.list-group-item.disabled,.list-group-item.disabled:hover,.list-group-item.disabled:focus{color:#999999;cursor:not-allowed;background-color:#eeeeee}.list-group-item.disabled .list-group-item-heading,.list-group-item.disabled:hover .list-group-item-heading,.list-group-item.disabled:focus .list-group-item-heading{color:inherit}.list-group-item.disabled .list-group-item-text,.list-group-item.disabled:hover .list-group-item-text,.list-group-item.disabled:focus .list-group-item-text{color:#999999}.list-group-item.active,.list-group-item.active:hover,.list-group-item.active:focus{z-index:2;color:#ffffff;background-color:#158cba;border-color:#158cba}.list-group-item.active .list-group-item-heading,.list-group-item.active:hover .list-group-item-heading,.list-group-item.active:focus .list-group-item-heading,.list-group-item.active .list-group-item-heading>small,.list-group-item.active:hover .list-group-item-heading>small,.list-group-item.active:focus .list-group-item-heading>small,.list-group-item.active .list-group-item-heading>.small,.list-group-item.active:hover .list-group-item-heading>.small,.list-group-item.active:focus .list-group-item-heading>.small{color:inherit}.list-group-item.active .list-group-item-text,.list-group-item.active:hover .list-group-item-text,.list-group-item.active:focus .list-group-item-text{color:#a6dff5}a.list-group-item,button.list-group-item{color:#555555}a.list-group-item .list-group-item-heading,button.list-group-item .list-group-item-heading{color:#333333}a.list-group-item:hover,button.list-group-item:hover,a.list-group-item:focus,button.list-group-item:focus{color:#555555;text-decoration:none;background-color:#f5f5f5}button.list-group-item{width:100%;text-align:left}.list-group-item-success{color:#ffffff;background-color:#28b62c}a.list-group-item-success,button.list-group-item-success{color:#ffffff}a.list-group-item-success .list-group-item-heading,button.list-group-item-success .list-group-item-heading{color:inherit}a.list-group-item-success:hover,button.list-group-item-success:hover,a.list-group-item-success:focus,button.list-group-item-success:focus{color:#ffffff;background-color:#23a127}a.list-group-item-success.active,button.list-group-item-success.active,a.list-group-item-success.active:hover,button.list-group-item-success.active:hover,a.list-group-item-success.active:focus,button.list-group-item-success.active:focus{color:#fff;background-color:#ffffff;border-color:#ffffff}.list-group-item-info{color:#ffffff;background-color:#75caeb}a.list-group-item-info,button.list-group-item-info{color:#ffffff}a.list-group-item-info .list-group-item-heading,button.list-group-item-info .list-group-item-heading{color:inherit}a.list-group-item-info:hover,button.list-group-item-info:hover,a.list-group-item-info:focus,button.list-group-item-info:focus{color:#ffffff;background-color:#5fc1e8}a.list-group-item-info.active,button.list-group-item-info.active,a.list-group-item-info.active:hover,button.list-group-item-info.active:hover,a.list-group-item-info.active:focus,button.list-group-item-info.active:focus{color:#fff;background-color:#ffffff;border-color:#ffffff}.list-group-item-warning{color:#ffffff;background-color:#ff851b}a.list-group-item-warning,button.list-group-item-warning{color:#ffffff}a.list-group-item-warning .list-group-item-heading,button.list-group-item-warning .list-group-item-heading{color:inherit}a.list-group-item-warning:hover,button.list-group-item-warning:hover,a.list-group-item-warning:focus,button.list-group-item-warning:focus{color:#ffffff;background-color:#ff7701}a.list-group-item-warning.active,button.list-group-item-warning.active,a.list-group-item-warning.active:hover,button.list-group-item-warning.active:hover,a.list-group-item-warning.active:focus,button.list-group-item-warning.active:focus{color:#fff;background-color:#ffffff;border-color:#ffffff}.list-group-item-danger{color:#ffffff;background-color:#ff4136}a.list-group-item-danger,button.list-group-item-danger{color:#ffffff}a.list-group-item-danger .list-group-item-heading,button.list-group-item-danger .list-group-item-heading{color:inherit}a.list-group-item-danger:hover,button.list-group-item-danger:hover,a.list-group-item-danger:focus,button.list-group-item-danger:focus{color:#ffffff;background-color:#ff291c}a.list-group-item-danger.active,button.list-group-item-danger.active,a.list-group-item-danger.active:hover,button.list-group-item-danger.active:hover,a.list-group-item-danger.active:focus,button.list-group-item-danger.active:focus{color:#fff;background-color:#ffffff;border-color:#ffffff}.list-group-item-heading{margin-top:0;margin-bottom:5px}.list-group-item-text{margin-bottom:0;line-height:1.3}.panel{margin-bottom:20px;background-color:#ffffff;border:1px solid transparent;border-radius:4px;box-shadow:0 1px 1px rgba(0,0,0,0.05)}.panel-body{padding:15px}.panel-heading{padding:10px 15px;border-bottom:1px solid transparent;border-top-left-radius:3px;border-top-right-radius:3px}.panel-heading>.dropdown .dropdown-toggle{color:inherit}.panel-title{margin-top:0;margin-bottom:0;font-size:16px;color:inherit}.panel-title>a,.panel-title>small,.panel-title>.small,.panel-title>small>a,.panel-title>.small>a{color:inherit}.panel-footer{padding:10px 15px;background-color:#f5f5f5;border-top:1px solid transparent;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.list-group,.panel>.panel-collapse>.list-group{margin-bottom:0}.panel>.list-group .list-group-item,.panel>.panel-collapse>.list-group .list-group-item{border-width:1px 0;border-radius:0}.panel>.list-group:first-child .list-group-item:first-child,.panel>.panel-collapse>.list-group:first-child .list-group-item:first-child{border-top:0;border-top-left-radius:3px;border-top-right-radius:3px}.panel>.list-group:last-child .list-group-item:last-child,.panel>.panel-collapse>.list-group:last-child .list-group-item:last-child{border-bottom:0;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.panel-heading+.panel-collapse>.list-group .list-group-item:first-child{border-top-left-radius:0;border-top-right-radius:0}.panel-heading+.list-group .list-group-item:first-child{border-top-width:0}.list-group+.panel-footer{border-top-width:0}.panel>.table,.panel>.table-responsive>.table,.panel>.panel-collapse>.table{margin-bottom:0}.panel>.table caption,.panel>.table-responsive>.table caption,.panel>.panel-collapse>.table caption{padding-right:15px;padding-left:15px}.panel>.table:first-child,.panel>.table-responsive:first-child>.table:first-child{border-top-left-radius:3px;border-top-right-radius:3px}.panel>.table:first-child>thead:first-child>tr:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child{border-top-left-radius:3px;border-top-right-radius:3px}.panel>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table:first-child>thead:first-child>tr:first-child th:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:first-child{border-top-left-radius:3px}.panel>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table:first-child>thead:first-child>tr:first-child th:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:last-child{border-top-right-radius:3px}.panel>.table:last-child,.panel>.table-responsive:last-child>.table:last-child{border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.table:last-child>tbody:last-child>tr:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child{border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:first-child{border-bottom-left-radius:3px}.panel>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:last-child{border-bottom-right-radius:3px}.panel>.panel-body+.table,.panel>.panel-body+.table-responsive,.panel>.table+.panel-body,.panel>.table-responsive+.panel-body{border-top:1px solid #eeeeee}.panel>.table>tbody:first-child>tr:first-child th,.panel>.table>tbody:first-child>tr:first-child td{border-top:0}.panel>.table-bordered,.panel>.table-responsive>.table-bordered{border:0}.panel>.table-bordered>thead>tr>th:first-child,.panel>.table-responsive>.table-bordered>thead>tr>th:first-child,.panel>.table-bordered>tbody>tr>th:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:first-child,.panel>.table-bordered>tfoot>tr>th:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:first-child,.panel>.table-bordered>thead>tr>td:first-child,.panel>.table-responsive>.table-bordered>thead>tr>td:first-child,.panel>.table-bordered>tbody>tr>td:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:first-child,.panel>.table-bordered>tfoot>tr>td:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:first-child{border-left:0}.panel>.table-bordered>thead>tr>th:last-child,.panel>.table-responsive>.table-bordered>thead>tr>th:last-child,.panel>.table-bordered>tbody>tr>th:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:last-child,.panel>.table-bordered>tfoot>tr>th:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:last-child,.panel>.table-bordered>thead>tr>td:last-child,.panel>.table-responsive>.table-bordered>thead>tr>td:last-child,.panel>.table-bordered>tbody>tr>td:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:last-child,.panel>.table-bordered>tfoot>tr>td:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:last-child{border-right:0}.panel>.table-bordered>thead>tr:first-child>td,.panel>.table-responsive>.table-bordered>thead>tr:first-child>td,.panel>.table-bordered>tbody>tr:first-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>td,.panel>.table-bordered>thead>tr:first-child>th,.panel>.table-responsive>.table-bordered>thead>tr:first-child>th,.panel>.table-bordered>tbody>tr:first-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>th{border-bottom:0}.panel>.table-bordered>tbody>tr:last-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>td,.panel>.table-bordered>tfoot>tr:last-child>td,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>td,.panel>.table-bordered>tbody>tr:last-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>th,.panel>.table-bordered>tfoot>tr:last-child>th,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>th{border-bottom:0}.panel>.table-responsive{margin-bottom:0;border:0}.panel-group{margin-bottom:20px}.panel-group .panel{margin-bottom:0;border-radius:4px}.panel-group .panel+.panel{margin-top:5px}.panel-group .panel-heading{border-bottom:0}.panel-group .panel-heading+.panel-collapse>.panel-body,.panel-group .panel-heading+.panel-collapse>.list-group{border-top:1px solid transparent}.panel-group .panel-footer{border-top:0}.panel-group .panel-footer+.panel-collapse .panel-body{border-bottom:1px solid transparent}.panel-default{border-color:transparent}.panel-default>.panel-heading{color:#333333;background-color:#f5f5f5;border-color:transparent}.panel-default>.panel-heading+.panel-collapse>.panel-body{border-top-color:transparent}.panel-default>.panel-heading .badge{color:#f5f5f5;background-color:#333333}.panel-default>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:transparent}.panel-primary{border-color:transparent}.panel-primary>.panel-heading{color:#ffffff;background-color:#158cba;border-color:transparent}.panel-primary>.panel-heading+.panel-collapse>.panel-body{border-top-color:transparent}.panel-primary>.panel-heading .badge{color:#158cba;background-color:#ffffff}.panel-primary>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:transparent}.panel-success{border-color:transparent}.panel-success>.panel-heading{color:#ffffff;background-color:#28b62c;border-color:transparent}.panel-success>.panel-heading+.panel-collapse>.panel-body{border-top-color:transparent}.panel-success>.panel-heading .badge{color:#28b62c;background-color:#ffffff}.panel-success>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:transparent}.panel-info{border-color:transparent}.panel-info>.panel-heading{color:#ffffff;background-color:#75caeb;border-color:transparent}.panel-info>.panel-heading+.panel-collapse>.panel-body{border-top-color:transparent}.panel-info>.panel-heading .badge{color:#75caeb;background-color:#ffffff}.panel-info>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:transparent}.panel-warning{border-color:transparent}.panel-warning>.panel-heading{color:#ffffff;background-color:#ff851b;border-color:transparent}.panel-warning>.panel-heading+.panel-collapse>.panel-body{border-top-color:transparent}.panel-warning>.panel-heading .badge{color:#ff851b;background-color:#ffffff}.panel-warning>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:transparent}.panel-danger{border-color:transparent}.panel-danger>.panel-heading{color:#ffffff;background-color:#ff4136;border-color:transparent}.panel-danger>.panel-heading+.panel-collapse>.panel-body{border-top-color:transparent}.panel-danger>.panel-heading .badge{color:#ff4136;background-color:#ffffff}.panel-danger>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:transparent}.embed-responsive{position:relative;display:block;height:0;padding:0;overflow:hidden}.embed-responsive .embed-responsive-item,.embed-responsive iframe,.embed-responsive embed,.embed-responsive object,.embed-responsive video{position:absolute;top:0;bottom:0;left:0;width:100%;height:100%;border:0}.embed-responsive-16by9{padding-bottom:56.25%}.embed-responsive-4by3{padding-bottom:75%}.well{min-height:20px;padding:19px;margin-bottom:20px;background-color:#fafafa;border:1px solid #e8e8e8;border-radius:4px;box-shadow:inset 0 1px 1px rgba(0,0,0,0.05)}.well blockquote{border-color:#ddd;border-color:rgba(0,0,0,0.15)}.well-lg{padding:24px;border-radius:5px}.well-sm{padding:9px;border-radius:2px}.close{float:right;font-size:21px;font-weight:bold;line-height:1;color:#ffffff;text-shadow:0 1px 0 #ffffff;filter:alpha(opacity=20);opacity:0.2}.close:hover,.close:focus{color:#ffffff;text-decoration:none;cursor:pointer;filter:alpha(opacity=50);opacity:0.5}button.close{padding:0;cursor:pointer;background:transparent;border:0;-webkit-appearance:none;appearance:none}.modal-open{overflow:hidden}.modal{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1050;display:none;overflow:hidden;-webkit-overflow-scrolling:touch;outline:0}.modal.fade .modal-dialog{-webkit-transform:translate(0, -25%);transform:translate(0, -25%);transition:-webkit-transform .3s ease-out;transition:transform .3s ease-out}.modal.in .modal-dialog{-webkit-transform:translate(0, 0);transform:translate(0, 0)}.modal-open .modal{overflow-x:hidden;overflow-y:auto}.modal-dialog{position:relative;width:auto;margin:10px}.modal-content{position:relative;background-color:#ffffff;background-clip:padding-box;border:1px solid #eeeeee;border:1px solid rgba(0,0,0,0.05);border-radius:5px;box-shadow:0 3px 9px rgba(0,0,0,0.5);outline:0}.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;background-color:#000000}.modal-backdrop.fade{filter:alpha(opacity=0);opacity:0}.modal-backdrop.in{filter:alpha(opacity=50);opacity:0.5}.modal-header{padding:15px;border-bottom:1px solid #e5e5e5}.modal-header .close{margin-top:-2px}.modal-title{margin:0;line-height:1.42857143}.modal-body{position:relative;padding:20px}.modal-footer{padding:20px;text-align:right;border-top:1px solid #e5e5e5}.modal-footer .btn+.btn{margin-bottom:0;margin-left:5px}.modal-footer .btn-group .btn+.btn{margin-left:-1px}.modal-footer .btn-block+.btn-block{margin-left:0}.modal-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}@media (min-width:768px){.modal-dialog{width:600px;margin:30px auto}.modal-content{box-shadow:0 5px 15px rgba(0,0,0,0.5)}.modal-sm{width:300px}}@media (min-width:992px){.modal-lg{width:900px}}.tooltip{position:absolute;z-index:1070;display:block;font-family:"Source Sans Pro","Helvetica Neue",Helvetica,Arial,sans-serif;font-style:normal;font-weight:400;line-height:1.42857143;line-break:auto;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;word-wrap:normal;white-space:normal;font-size:12px;filter:alpha(opacity=0);opacity:0}.tooltip.in{filter:alpha(opacity=90);opacity:0.9}.tooltip.top{padding:5px 0;margin-top:-3px}.tooltip.right{padding:0 5px;margin-left:3px}.tooltip.bottom{padding:5px 0;margin-top:3px}.tooltip.left{padding:0 5px;margin-left:-3px}.tooltip.top .tooltip-arrow{bottom:0;left:50%;margin-left:-5px;border-width:5px 5px 0;border-top-color:#000000}.tooltip.top-left .tooltip-arrow{right:5px;bottom:0;margin-bottom:-5px;border-width:5px 5px 0;border-top-color:#000000}.tooltip.top-right .tooltip-arrow{bottom:0;left:5px;margin-bottom:-5px;border-width:5px 5px 0;border-top-color:#000000}.tooltip.right .tooltip-arrow{top:50%;left:0;margin-top:-5px;border-width:5px 5px 5px 0;border-right-color:#000000}.tooltip.left .tooltip-arrow{top:50%;right:0;margin-top:-5px;border-width:5px 0 5px 5px;border-left-color:#000000}.tooltip.bottom .tooltip-arrow{top:0;left:50%;margin-left:-5px;border-width:0 5px 5px;border-bottom-color:#000000}.tooltip.bottom-left .tooltip-arrow{top:0;right:5px;margin-top:-5px;border-width:0 5px 5px;border-bottom-color:#000000}.tooltip.bottom-right .tooltip-arrow{top:0;left:5px;margin-top:-5px;border-width:0 5px 5px;border-bottom-color:#000000}.tooltip-inner{max-width:200px;padding:3px 8px;color:#ffffff;text-align:center;background-color:#000000;border-radius:4px}.tooltip-arrow{position:absolute;width:0;height:0;border-color:transparent;border-style:solid}.popover{position:absolute;top:0;left:0;z-index:1060;display:none;max-width:276px;padding:1px;font-family:"Source Sans Pro","Helvetica Neue",Helvetica,Arial,sans-serif;font-style:normal;font-weight:400;line-height:1.42857143;line-break:auto;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;word-wrap:normal;white-space:normal;font-size:14px;background-color:#ffffff;background-clip:padding-box;border:1px solid #cccccc;border:1px solid rgba(0,0,0,0.2);border-radius:5px;box-shadow:0 5px 10px rgba(0,0,0,0.2)}.popover.top{margin-top:-10px}.popover.right{margin-left:10px}.popover.bottom{margin-top:10px}.popover.left{margin-left:-10px}.popover>.arrow{border-width:11px}.popover>.arrow,.popover>.arrow:after{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.popover>.arrow:after{content:"";border-width:10px}.popover.top>.arrow{bottom:-11px;left:50%;margin-left:-11px;border-top-color:#999999;border-top-color:rgba(0,0,0,0.25);border-bottom-width:0}.popover.top>.arrow:after{bottom:1px;margin-left:-10px;content:" ";border-top-color:#ffffff;border-bottom-width:0}.popover.right>.arrow{top:50%;left:-11px;margin-top:-11px;border-right-color:#999999;border-right-color:rgba(0,0,0,0.25);border-left-width:0}.popover.right>.arrow:after{bottom:-10px;left:1px;content:" ";border-right-color:#ffffff;border-left-width:0}.popover.bottom>.arrow{top:-11px;left:50%;margin-left:-11px;border-top-width:0;border-bottom-color:#999999;border-bottom-color:rgba(0,0,0,0.25)}.popover.bottom>.arrow:after{top:1px;margin-left:-10px;content:" ";border-top-width:0;border-bottom-color:#ffffff}.popover.left>.arrow{top:50%;right:-11px;margin-top:-11px;border-right-width:0;border-left-color:#999999;border-left-color:rgba(0,0,0,0.25)}.popover.left>.arrow:after{right:1px;bottom:-10px;content:" ";border-right-width:0;border-left-color:#ffffff}.popover-title{padding:8px 14px;margin:0;font-size:14px;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;border-radius:4px 4px 0 0}.popover-content{padding:9px 14px}.carousel{position:relative}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner>.item{position:relative;display:none;transition:.6s ease-in-out left}.carousel-inner>.item>img,.carousel-inner>.item>a>img{line-height:1}@media all and (transform-3d),(-webkit-transform-3d){.carousel-inner>.item{transition:-webkit-transform .6s ease-in-out;transition:transform .6s ease-in-out;-webkit-backface-visibility:hidden;backface-visibility:hidden;-webkit-perspective:1000px;perspective:1000px}.carousel-inner>.item.next,.carousel-inner>.item.active.right{-webkit-transform:translate3d(100%, 0, 0);transform:translate3d(100%, 0, 0);left:0}.carousel-inner>.item.prev,.carousel-inner>.item.active.left{-webkit-transform:translate3d(-100%, 0, 0);transform:translate3d(-100%, 0, 0);left:0}.carousel-inner>.item.next.left,.carousel-inner>.item.prev.right,.carousel-inner>.item.active{-webkit-transform:translate3d(0, 0, 0);transform:translate3d(0, 0, 0);left:0}}.carousel-inner>.active,.carousel-inner>.next,.carousel-inner>.prev{display:block}.carousel-inner>.active{left:0}.carousel-inner>.next,.carousel-inner>.prev{position:absolute;top:0;width:100%}.carousel-inner>.next{left:100%}.carousel-inner>.prev{left:-100%}.carousel-inner>.next.left,.carousel-inner>.prev.right{left:0}.carousel-inner>.active.left{left:-100%}.carousel-inner>.active.right{left:100%}.carousel-control{position:absolute;top:0;bottom:0;left:0;width:15%;font-size:20px;color:#ffffff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,0.6);background-color:rgba(0,0,0,0);filter:alpha(opacity=50);opacity:0.5}.carousel-control.left{background-image:linear-gradient(to right, rgba(0,0,0,0.5) 0, rgba(0,0,0,0.0001) 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#80000000', endColorstr='#00000000', GradientType=1);background-repeat:repeat-x}.carousel-control.right{right:0;left:auto;background-image:linear-gradient(to right, rgba(0,0,0,0.0001) 0, rgba(0,0,0,0.5) 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#80000000', GradientType=1);background-repeat:repeat-x}.carousel-control:hover,.carousel-control:focus{color:#ffffff;text-decoration:none;outline:0;filter:alpha(opacity=90);opacity:0.9}.carousel-control .icon-prev,.carousel-control .icon-next,.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right{position:absolute;top:50%;z-index:5;display:inline-block;margin-top:-10px}.carousel-control .icon-prev,.carousel-control .glyphicon-chevron-left{left:50%;margin-left:-10px}.carousel-control .icon-next,.carousel-control .glyphicon-chevron-right{right:50%;margin-right:-10px}.carousel-control .icon-prev,.carousel-control .icon-next{width:20px;height:20px;font-family:serif;line-height:1}.carousel-control .icon-prev:before{content:"\2039"}.carousel-control .icon-next:before{content:"\203a"}.carousel-indicators{position:absolute;bottom:10px;left:50%;z-index:15;width:60%;padding-left:0;margin-left:-30%;text-align:center;list-style:none}.carousel-indicators li{display:inline-block;width:10px;height:10px;margin:1px;text-indent:-999px;cursor:pointer;background-color:#000 \9;background-color:rgba(0,0,0,0);border:1px solid #ffffff;border-radius:10px}.carousel-indicators .active{width:12px;height:12px;margin:0;background-color:#ffffff}.carousel-caption{position:absolute;right:15%;bottom:20px;left:15%;z-index:10;padding-top:20px;padding-bottom:20px;color:#ffffff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,0.6)}.carousel-caption .btn{text-shadow:none}@media screen and (min-width:768px){.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right,.carousel-control .icon-prev,.carousel-control .icon-next{width:30px;height:30px;margin-top:-10px;font-size:30px}.carousel-control .glyphicon-chevron-left,.carousel-control .icon-prev{margin-left:-10px}.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next{margin-right:-10px}.carousel-caption{right:20%;left:20%;padding-bottom:30px}.carousel-indicators{bottom:20px}}.clearfix:before,.clearfix:after,.dl-horizontal dd:before,.dl-horizontal dd:after,.container:before,.container:after,.container-fluid:before,.container-fluid:after,.row:before,.row:after,.form-horizontal .form-group:before,.form-horizontal .form-group:after,.btn-toolbar:before,.btn-toolbar:after,.btn-group-vertical>.btn-group:before,.btn-group-vertical>.btn-group:after,.nav:before,.nav:after,.navbar:before,.navbar:after,.navbar-header:before,.navbar-header:after,.navbar-collapse:before,.navbar-collapse:after,.pager:before,.pager:after,.panel-body:before,.panel-body:after,.modal-header:before,.modal-header:after,.modal-footer:before,.modal-footer:after{display:table;content:" "}.clearfix:after,.dl-horizontal dd:after,.container:after,.container-fluid:after,.row:after,.form-horizontal .form-group:after,.btn-toolbar:after,.btn-group-vertical>.btn-group:after,.nav:after,.navbar:after,.navbar-header:after,.navbar-collapse:after,.pager:after,.panel-body:after,.modal-header:after,.modal-footer:after{clear:both}.center-block{display:block;margin-right:auto;margin-left:auto}.pull-right{float:right !important}.pull-left{float:left !important}.hide{display:none !important}.show{display:block !important}.invisible{visibility:hidden}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.hidden{display:none !important}.affix{position:fixed}@-ms-viewport{width:device-width}.visible-xs,.visible-sm,.visible-md,.visible-lg{display:none !important}.visible-xs-block,.visible-xs-inline,.visible-xs-inline-block,.visible-sm-block,.visible-sm-inline,.visible-sm-inline-block,.visible-md-block,.visible-md-inline,.visible-md-inline-block,.visible-lg-block,.visible-lg-inline,.visible-lg-inline-block{display:none !important}@media (max-width:767px){.visible-xs{display:block !important}table.visible-xs{display:table !important}tr.visible-xs{display:table-row !important}th.visible-xs,td.visible-xs{display:table-cell !important}}@media (max-width:767px){.visible-xs-block{display:block !important}}@media (max-width:767px){.visible-xs-inline{display:inline !important}}@media (max-width:767px){.visible-xs-inline-block{display:inline-block !important}}@media (min-width:768px) and (max-width:991px){.visible-sm{display:block !important}table.visible-sm{display:table !important}tr.visible-sm{display:table-row !important}th.visible-sm,td.visible-sm{display:table-cell !important}}@media (min-width:768px) and (max-width:991px){.visible-sm-block{display:block !important}}@media (min-width:768px) and (max-width:991px){.visible-sm-inline{display:inline !important}}@media (min-width:768px) and (max-width:991px){.visible-sm-inline-block{display:inline-block !important}}@media (min-width:992px) and (max-width:1199px){.visible-md{display:block !important}table.visible-md{display:table !important}tr.visible-md{display:table-row !important}th.visible-md,td.visible-md{display:table-cell !important}}@media (min-width:992px) and (max-width:1199px){.visible-md-block{display:block !important}}@media (min-width:992px) and (max-width:1199px){.visible-md-inline{display:inline !important}}@media (min-width:992px) and (max-width:1199px){.visible-md-inline-block{display:inline-block !important}}@media (min-width:1200px){.visible-lg{display:block !important}table.visible-lg{display:table !important}tr.visible-lg{display:table-row !important}th.visible-lg,td.visible-lg{display:table-cell !important}}@media (min-width:1200px){.visible-lg-block{display:block !important}}@media (min-width:1200px){.visible-lg-inline{display:inline !important}}@media (min-width:1200px){.visible-lg-inline-block{display:inline-block !important}}@media (max-width:767px){.hidden-xs{display:none !important}}@media (min-width:768px) and (max-width:991px){.hidden-sm{display:none !important}}@media (min-width:992px) and (max-width:1199px){.hidden-md{display:none !important}}@media (min-width:1200px){.hidden-lg{display:none !important}}.visible-print{display:none !important}@media print{.visible-print{display:block !important}table.visible-print{display:table !important}tr.visible-print{display:table-row !important}th.visible-print,td.visible-print{display:table-cell !important}}.visible-print-block{display:none !important}@media print{.visible-print-block{display:block !important}}.visible-print-inline{display:none !important}@media print{.visible-print-inline{display:inline !important}}.visible-print-inline-block{display:none !important}@media print{.visible-print-inline-block{display:inline-block !important}}@media print{.hidden-print{display:none !important}}.navbar{border-width:0 1px 4px 1px}.btn{padding:9px 12px 7px;border-width:0 1px 4px 1px;font-size:12px;font-weight:bold;text-transform:uppercase}.btn:hover{margin-top:1px;border-bottom-width:3px}.btn:active{margin-top:2px;border-bottom-width:2px;box-shadow:none}.btn-lg,.btn-group-lg>.btn{padding:15px 16px 13px;line-height:15px}.btn-sm,.btn-group-sm>.btn{padding:6px 10px 4px}.btn-xs,.btn-group-xs>.btn{padding:3px 5px 1px}.btn-default:hover,.btn-default:focus,.btn-group.open .dropdown-toggle.btn-default{background-color:#eeeeee;border-color:#e2e2e2}.btn-primary:hover,.btn-primary:focus,.btn-group.open .dropdown-toggle.btn-primary{background-color:#158cba;border-color:#127ba3}.btn-success:hover,.btn-success:focus,.btn-group.open .dropdown-toggle.btn-success{background-color:#28b62c;border-color:#23a127}.btn-info:hover,.btn-info:focus,.btn-group.open .dropdown-toggle.btn-info{background-color:#75caeb;border-color:#5fc1e8}.btn-warning:hover,.btn-warning:focus,.btn-group.open .dropdown-toggle.btn-warning{background-color:#ff851b;border-color:#ff7701}.btn-danger:hover,.btn-danger:focus,.btn-group.open .dropdown-toggle.btn-danger{background-color:#ff4136;border-color:#ff291c}.btn-group.open .dropdown-toggle{box-shadow:none}.navbar-btn:hover{margin-top:8px}.navbar-btn:active{margin-top:9px}.navbar-btn.btn-sm:hover{margin-top:11px}.navbar-btn.btn-sm:active{margin-top:12px}.navbar-btn.btn-xs:hover{margin-top:15px}.navbar-btn.btn-xs:active{margin-top:16px}.btn-group-vertical .btn+.btn:hover{border-top-width:1px}.btn-group-vertical .btn+.btn:active{border-top-width:2px}.text-primary,.text-primary:hover{color:#158cba}.text-success,.text-success:hover{color:#28b62c}.text-danger,.text-danger:hover{color:#ff4136}.text-warning,.text-warning:hover{color:#ff851b}.text-info,.text-info:hover{color:#75caeb}table a:not(.btn),.table a:not(.btn){text-decoration:underline}table .dropdown-menu a,.table .dropdown-menu a{text-decoration:none}table .success,.table .success,table .warning,.table .warning,table .danger,.table .danger,table .info,.table .info{color:#fff}table .success a:not(.btn),.table .success a:not(.btn),table .warning a:not(.btn),.table .warning a:not(.btn),table .danger a:not(.btn),.table .danger a:not(.btn),table .info a:not(.btn),.table .info a:not(.btn){color:#fff}table:not(.table-bordered)>thead>tr>th,.table:not(.table-bordered)>thead>tr>th,table:not(.table-bordered)>tbody>tr>th,.table:not(.table-bordered)>tbody>tr>th,table:not(.table-bordered)>tfoot>tr>th,.table:not(.table-bordered)>tfoot>tr>th,table:not(.table-bordered)>thead>tr>td,.table:not(.table-bordered)>thead>tr>td,table:not(.table-bordered)>tbody>tr>td,.table:not(.table-bordered)>tbody>tr>td,table:not(.table-bordered)>tfoot>tr>td,.table:not(.table-bordered)>tfoot>tr>td{border-color:transparent}.form-control{box-shadow:inset 0 2px 0 rgba(0,0,0,0.075)}label{font-weight:normal}.has-warning .help-block,.has-warning .control-label,.has-warning .radio,.has-warning .checkbox,.has-warning .radio-inline,.has-warning .checkbox-inline,.has-warning.radio label,.has-warning.checkbox label,.has-warning.radio-inline label,.has-warning.checkbox-inline label,.has-warning .form-control-feedback{color:#ff851b}.has-warning .form-control,.has-warning .form-control:focus{border:1px solid #ff851b;box-shadow:inset 0 2px 0 rgba(0,0,0,0.075)}.has-warning .input-group-addon{border:1px solid #ff851b}.has-error .help-block,.has-error .control-label,.has-error .radio,.has-error .checkbox,.has-error .radio-inline,.has-error .checkbox-inline,.has-error.radio label,.has-error.checkbox label,.has-error.radio-inline label,.has-error.checkbox-inline label,.has-error .form-control-feedback{color:#ff4136}.has-error .form-control,.has-error .form-control:focus{border:1px solid #ff4136;box-shadow:inset 0 2px 0 rgba(0,0,0,0.075)}.has-error .input-group-addon{border:1px solid #ff4136}.has-success .help-block,.has-success .control-label,.has-success .radio,.has-success .checkbox,.has-success .radio-inline,.has-success .checkbox-inline,.has-success.radio label,.has-success.checkbox label,.has-success.radio-inline label,.has-success.checkbox-inline label,.has-success .form-control-feedback{color:#28b62c}.has-success .form-control,.has-success .form-control:focus{border:1px solid #28b62c;box-shadow:inset 0 2px 0 rgba(0,0,0,0.075)}.has-success .input-group-addon{border:1px solid #28b62c}.nav .open>a,.nav .open>a:hover,.nav .open>a:focus{border-color:transparent}.nav-tabs>li>a{margin-top:6px;border-color:#e7e7e7;color:#333333;transition:all .2s ease-in-out}.nav-tabs>li>a:hover,.nav-tabs>li>a:focus,.nav-tabs>li.active>a,.nav-tabs>li.active>a:hover,.nav-tabs>li.active>a:focus,.nav-tabs .open>a,.nav-tabs .open>a:hover,.nav-tabs .open>a:focus{padding-bottom:16px;margin-top:0}.nav-tabs .open>a,.nav-tabs .open>a:hover,.nav-tabs .open>a:focus{border-color:#e7e7e7}.nav-tabs>li.disabled>a:hover,.nav-tabs>li.disabled>a:focus{padding-top:10px;padding-bottom:10px;margin-top:6px}.nav-tabs.nav-justified>li{vertical-align:bottom}.dropdown-menu{margin-top:0;border-width:0 1px 4px 1px;border-top-width:1px;box-shadow:none}.breadcrumb{border-color:#ededed;border-style:solid;border-width:0 1px 4px 1px}.pagination>li>a,.pager>li>a,.pagination>li>span,.pager>li>span{position:relative;top:0;border-width:0 1px 4px 1px;color:#555555;font-size:12px;font-weight:bold;text-transform:uppercase}.pagination>li>a:hover,.pager>li>a:hover,.pagination>li>span:hover,.pager>li>span:hover{top:1px;border-bottom-width:3px}.pagination>li>a:active,.pager>li>a:active,.pagination>li>span:active,.pager>li>span:active{top:2px;border-bottom-width:2px}.pagination>.disabled>a:hover,.pager>.disabled>a:hover,.pagination>.disabled>span:hover,.pager>.disabled>span:hover{top:0;border-width:0 1px 4px 1px}.pagination>.disabled>a:active,.pager>.disabled>a:active,.pagination>.disabled>span:active,.pager>.disabled>span:active{top:0;border-width:0 1px 4px 1px}.pager>li>a,.pager>li>span,.pager>.disabled>a,.pager>.disabled>span,.pager>li>a:hover,.pager>li>span:hover,.pager>.disabled>a:hover,.pager>.disabled>span:hover,.pager>li>a:active,.pager>li>span:active,.pager>.disabled>a:active,.pager>.disabled>span:active{border-left-width:2px;border-right-width:2px}.close{color:#fff;text-decoration:none;opacity:0.4}.close:hover,.close:focus{color:#fff;opacity:1}.alert{border-width:0 1px 4px 1px}.alert .alert-link{font-weight:normal;color:#fff;text-decoration:underline}.label{font-weight:normal}.progress{border:1px solid #e7e7e7;box-shadow:inset 0 2px 0 rgba(0,0,0,0.1)}.progress-bar{box-shadow:inset 0 -4px 0 rgba(0,0,0,0.15)}.well{border:1px solid #e7e7e7;box-shadow:inset 0 2px 0 rgba(0,0,0,0.05)}a.list-group-item.active,a.list-group-item.active:hover,a.list-group-item.active:focus{border-color:#eeeeee}a.list-group-item-success.active{background-color:#28b62c}a.list-group-item-success.active:hover,a.list-group-item-success.active:focus{background-color:#23a127}a.list-group-item-warning.active{background-color:#ff851b}a.list-group-item-warning.active:hover,a.list-group-item-warning.active:focus{background-color:#ff7701}a.list-group-item-danger.active{background-color:#ff4136}a.list-group-item-danger.active:hover,a.list-group-item-danger.active:focus{background-color:#ff291c}.jumbotron{border:1px solid #e7e7e7;box-shadow:inset 0 2px 0 rgba(0,0,0,0.05)}.panel{border:1px solid #e7e7e7;border-width:0 1px 4px 1px}.panel-default .close{color:#555555}.modal .close{color:#555555}.popover{color:#555555} \ No newline at end of file diff --git a/data/web/css/build/004-bootstrap-slider.min.css b/data/web/css/build/004-bootstrap-slider.min.css index 81c905da..f8e395be 100644 --- a/data/web/css/build/004-bootstrap-slider.min.css +++ b/data/web/css/build/004-bootstrap-slider.min.css @@ -1,5 +1,5 @@ /*! ======================================================= - VERSION 10.6.0 + VERSION 10.6.1 ========================================================= */ /*! ========================================================= * bootstrap-slider.js diff --git a/data/web/css/build/008-mailcow.css b/data/web/css/build/008-mailcow.css index 5a51bfae..902a644c 100644 --- a/data/web/css/build/008-mailcow.css +++ b/data/web/css/build/008-mailcow.css @@ -42,6 +42,9 @@ .btn { text-transform: none; } +.btn * { + pointer-events: none; +} .textarea-code { font-family:Consolas,Monaco,Lucida Console,Liberation Mono,DejaVu Sans Mono,Bitstream Vera Sans Mono,Courier New, monospace; background:transparent !important; diff --git a/data/web/css/site/admin.css b/data/web/css/site/admin.css index cadc4a3a..33708376 100644 --- a/data/web/css/site/admin.css +++ b/data/web/css/site/admin.css @@ -70,4 +70,10 @@ body.modal-open { } .table-condensed > thead > tr > th, .table-condensed > tbody > tr > th, .table-condensed > tfoot > tr > th, .table-condensed > thead > tr > td, .table-condensed > tbody > tr > td, .table-condensed > tfoot > tr > td { padding: 3px; -} \ No newline at end of file +} +table tbody tr { + cursor: pointer; +} +table tbody tr td input[type="checkbox"] { + cursor: pointer; +} diff --git a/data/web/css/site/index.css b/data/web/css/site/index.css new file mode 100644 index 00000000..10f7c0d7 --- /dev/null +++ b/data/web/css/site/index.css @@ -0,0 +1,5 @@ +@media (max-width: 500px) { + #top { + padding-top: 15px !important; + } +} \ No newline at end of file diff --git a/data/web/css/site/mailbox.css b/data/web/css/site/mailbox.css index 80ff063b..63c26c5f 100644 --- a/data/web/css/site/mailbox.css +++ b/data/web/css/site/mailbox.css @@ -9,7 +9,7 @@ table.footable>tbody>tr.footable-empty>td { overflow: visible !important; } .table-responsive { - overflow: auto !important; + overflow: inherit !important; } @media screen and (max-width: 767px) { .table-responsive { @@ -53,3 +53,9 @@ table.footable>tbody>tr.footable-empty>td { font-family:Consolas,Monaco,Lucida Console,Liberation Mono,DejaVu Sans Mono,Bitstream Vera Sans Mono,Courier New, monospace; font-size:smaller; } +table tbody tr { + cursor: pointer; +} +table tbody tr td input[type="checkbox"] { + cursor: pointer; +} diff --git a/data/web/css/site/quarantine.css b/data/web/css/site/quarantine.css index 5d0fa1ef..0ea3eeb8 100644 --- a/data/web/css/site/quarantine.css +++ b/data/web/css/site/quarantine.css @@ -48,4 +48,20 @@ table.footable>tbody>tr.footable-empty>td { background-color: #d4d4d4; border-radius: 50%; display: inline-block; -} \ No newline at end of file +} + +span.mail-address-item { + background-color: #f5f5f5; + border-radius: 4px; + border: 1px solid #ccc; + padding: 2px 7px; + margin-right: 7px; +} + +table tbody tr { + cursor: pointer; +} + +table tbody tr td input[type="checkbox"] { + cursor: pointer; +} diff --git a/data/web/css/site/user.css b/data/web/css/site/user.css index aa3ddad7..62428d0c 100644 --- a/data/web/css/site/user.css +++ b/data/web/css/site/user.css @@ -39,4 +39,12 @@ table.footable>tbody>tr.footable-empty>td { } body { overflow-y:scroll; +} + +table tbody tr { + cursor: pointer; +} + +table tbody tr td input[type="checkbox"] { + cursor: pointer; } \ No newline at end of file diff --git a/data/web/edit.php b/data/web/edit.php index c44464df..694d8fdc 100644 --- a/data/web/edit.php +++ b/data/web/edit.php @@ -273,6 +273,12 @@ if (isset($_SESSION['mailcow_cc_role'])) { <input type="number" class="form-control" name="mailboxes" value="<?=intval($result['max_num_mboxes_for_domain']);?>"> </div> </div> + <div class="form-group"> + <label class="control-label col-sm-2" for="defquota"><?=$lang['edit']['mailbox_quota_def'];?></label> + <div class="col-sm-10"> + <input type="number" class="form-control" name="defquota" value="<?=intval($result['def_quota_for_mbox'] / 1048576);?>"> + </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"> @@ -379,7 +385,6 @@ if (isset($_SESSION['mailcow_cc_role'])) { <div class="btn-group" data-acl="<?=$_SESSION['acl']['spam_policy'];?>"> <a class="btn btn-sm btn-default" id="toggle_multi_select_all" data-id="policy_wl_domain" href="#"><span class="glyphicon glyphicon-check" aria-hidden="true"></span> <?=$lang['mailbox']['toggle_all'];?></a> <a class="btn btn-sm btn-danger" data-action="delete_selected" data-id="policy_wl_domain" data-api-url='delete/domain-policy' href="#"><?=$lang['mailbox']['remove'];?></a></li> - </ul> </div> </div> <form class="form-inline" data-id="add_wl_policy_domain"> @@ -401,7 +406,6 @@ if (isset($_SESSION['mailcow_cc_role'])) { <div class="btn-group" data-acl="<?=$_SESSION['acl']['spam_policy'];?>"> <a class="btn btn-sm btn-default" id="toggle_multi_select_all" data-id="policy_bl_domain" href="#"><span class="glyphicon glyphicon-check" aria-hidden="true"></span> <?=$lang['mailbox']['toggle_all'];?></a> <a class="btn btn-sm btn-danger" data-action="delete_selected" data-id="policy_bl_domain" data-api-url='delete/domain-policy' href="#"><?=$lang['mailbox']['remove'];?></a></li> - </ul> </div> </div> <form class="form-inline" data-id="add_bl_policy_domain"> @@ -502,6 +506,7 @@ if (isset($_SESSION['mailcow_cc_role'])) { $mailbox = html_entity_decode(rawurldecode($_GET["mailbox"])); $result = mailbox('get', 'mailbox_details', $mailbox); $rl = ratelimit('get', 'mailbox', $mailbox); + $quarantine_notification = mailbox('get', 'quarantine_notification', $mailbox); if (!empty($result)) { ?> <h4><?=$lang['edit']['mailbox'];?></h4> @@ -511,21 +516,22 @@ if (isset($_SESSION['mailcow_cc_role'])) { <input type="hidden" value="0" name="force_pw_update"> <input type="hidden" value="0" name="sogo_access"> <div class="form-group"> - <label class="control-label col-sm-2" for="name"><?=$lang['edit']['full_name'];?>:</label> + <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" 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'];?>: + <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" style="width:100%" min="1" max="<?=intval($result['max_new_quota'] / 1048576);?>" value="<?=intval($result['quota']) / 1048576;?>" class="form-control"> + <input type="number" name="quota" style="width:100%" min="0" max="<?=intval($result['max_new_quota'] / 1048576);?>" value="<?=intval($result['quota']) / 1048576;?>" class="form-control"> + <small class="help-block">0 = ∞</small> </div> </div> <div class="form-group"> - <label class="control-label col-sm-2" for="sender_acl"><?=$lang['edit']['sender_acl'];?>:</label> + <label class="control-label col-sm-2" for="sender_acl"><?=$lang['edit']['sender_acl'];?></label> <div class="col-sm-10"> <select data-live-search="true" data-width="100%" style="width:100%" id="editSelectSenderACL" name="sender_acl" size="10" multiple> <?php @@ -537,7 +543,7 @@ if (isset($_SESSION['mailcow_cc_role'])) { <?php endforeach; - foreach ($sender_acl_handles['sender_acl_addresses']['ro'] as $domain): + foreach ($sender_acl_handles['sender_acl_addresses']['ro'] as $alias): ?> <option data-subtext="Admin" disabled selected><?=htmlspecialchars($alias);?></option> <?php @@ -573,9 +579,51 @@ if (isset($_SESSION['mailcow_cc_role'])) { <?php endforeach; + // Generated here, but used in extended_sender_acl + if (!empty($sender_acl_handles['external_sender_aliases'])) { + $ext_sender_acl = implode(', ', $sender_acl_handles['external_sender_aliases']); + } + else { + $ext_sender_acl = ''; + } + ?> </select> <div style="display:none" id="sender_acl_disabled"><?=$lang['edit']['sender_acl_disabled'];?></div> + <small class="help-block"><?=$lang['edit']['sender_acl_info'];?></small> + </div> + </div> + <div class="form-group"> + <label class="control-label col-sm-2" for="sender_acl"><?=$lang['user']['quarantine_notification'];?></label> + <div class="col-sm-10"> + <div class="btn-group" data-acl="<?=$_SESSION['acl']['quarantine_notification'];?>"> + <button type="button" class="btn btn-sm btn-default <?=($quarantine_notification == "never") ? "active" : null;?>" + data-action="edit_selected" + data-item="<?= htmlentities($mailbox); ?>" + data-id="quarantine_notification" + data-api-url='edit/quarantine_notification' + data-api-attr='{"quarantine_notification":"never"}'><?=$lang['user']['never'];?></button> + <button type="button" class="btn btn-sm btn-default <?=($quarantine_notification == "hourly") ? "active" : null;?>" + data-action="edit_selected" + data-item="<?= htmlentities($mailbox); ?>" + data-id="quarantine_notification" + data-api-url='edit/quarantine_notification' + data-api-attr='{"quarantine_notification":"hourly"}'><?=$lang['user']['hourly'];?></button> + <button type="button" class="btn btn-sm btn-default <?=($quarantine_notification == "daily") ? "active" : null;?>" + data-action="edit_selected" + data-item="<?= htmlentities($mailbox); ?>" + data-id="quarantine_notification" + data-api-url='edit/quarantine_notification' + data-api-attr='{"quarantine_notification":"daily"}'><?=$lang['user']['daily'];?></button> + <button type="button" class="btn btn-sm btn-default <?=($quarantine_notification == "weekly") ? "active" : null;?>" + data-action="edit_selected" + data-item="<?= htmlentities($mailbox); ?>" + data-id="quarantine_notification" + data-api-url='edit/quarantine_notification' + data-api-attr='{"quarantine_notification":"weekly"}'><?=$lang['user']['weekly'];?></button> + </div> + <div style="display:none" id="user_acl_q_notify_disabled"><?=$lang['edit']['user_acl_q_notify_disabled'];?></div> + <p class="help-block"><small><?=$lang['user']['quarantine_notification_info'];?></small></p> </div> </div> <div class="form-group"> @@ -590,6 +638,13 @@ if (isset($_SESSION['mailcow_cc_role'])) { <input type="password" class="form-control" name="password2"> </div> </div> + <div data-acl="<?=$_SESSION['acl']['extend_sender_acl'];?>" class="form-group"> + <label class="control-label col-sm-2" for="extended_sender_acl"><?=$lang['edit']['extended_sender_acl'];?></label> + <div class="col-sm-10"> + <input type="text" class="form-control" name="extended_sender_acl" value="<?=empty($ext_sender_acl) ? '' : $ext_sender_acl; ?>" placeholder="user1@example.com, user2@example.org, @example.com, ..."> + <small class="help-block"><?=$lang['edit']['extended_sender_acl_info'];?></small> + </div> + </div> <div class="form-group"> <div class="col-sm-offset-2 col-sm-10"> <div class="checkbox"> @@ -639,6 +694,7 @@ if (isset($_SESSION['mailcow_cc_role'])) { <div class="form-group"> <button class="btn btn-default" data-action="edit_selected" data-id="mboxratelimit" data-item="<?=htmlspecialchars($mailbox);?>" data-api-url='edit/rl-mbox' data-api-attr='{}' href="#"><?=$lang['admin']['save'];?></button> </div> + <p class="help-block"><?=$lang['edit']['mbox_rl_info'];?></p> </div> </div> </form> @@ -681,6 +737,7 @@ if (isset($_SESSION['mailcow_cc_role'])) { <label class="control-label col-sm-2" for="hostname"><?=$lang['add']['hostname'];?></label> <div class="col-sm-10"> <input type="text" class="form-control" name="hostname" value="<?=htmlspecialchars($result['hostname'], ENT_QUOTES, 'UTF-8');?>" required> + <p class="help-block"><?=$lang['add']['relayhost_wrapped_tls_info'];?></p> </div> </div> <div class="form-group"> @@ -784,7 +841,7 @@ if (isset($_SESSION['mailcow_cc_role'])) { </div> </div> <div class="form-group"> - <label class="control-label col-sm-2" for="domain"><?=$lang['edit']['kind'];?>:</label> + <label class="control-label col-sm-2" for="domain"><?=$lang['edit']['kind'];?></label> <div class="col-sm-10"> <select name="kind" title="<?=$lang['edit']['select'];?>" required> <option value="location" <?=($result['kind'] == "location") ? "selected" : null;?>>Location</option> @@ -794,7 +851,7 @@ if (isset($_SESSION['mailcow_cc_role'])) { </div> </div> <div class="form-group"> - <label class="control-label col-sm-2" for="multiple_bookings_select"><?=$lang['add']['multiple_bookings'];?>:</label> + <label class="control-label col-sm-2" for="multiple_bookings_select"><?=$lang['add']['multiple_bookings'];?></label> <div class="col-sm-10"> <select name="multiple_bookings_select" id="editSelectMultipleBookings" title="<?=$lang['add']['select'];?>" required> <option value="0" <?=($result['multiple_bookings'] == 0) ? "selected" : null;?>><?=$lang['mailbox']['booking_0'];?></option> @@ -1027,7 +1084,7 @@ if (isset($_SESSION['mailcow_cc_role'])) { </div> </div> <div class="form-group"> - <label class="control-label col-sm-2" for="enc1"><?=$lang['edit']['encryption'];?>:</label> + <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> @@ -1086,7 +1143,8 @@ if (isset($_SESSION['mailcow_cc_role'])) { <div class="form-group"> <label class="control-label col-sm-2" for="custom_params"><?=$lang['add']['custom_params'];?></label> <div class="col-sm-10"> - <input type="text" class="form-control" name="custom_params" id="custom_params" value="<?=htmlspecialchars($result['custom_params'], ENT_QUOTES, 'UTF-8');?>"> + <input type="text" class="form-control" name="custom_params" id="custom_params" value="<?=htmlspecialchars($result['custom_params'], ENT_QUOTES, 'UTF-8');?>" placeholder="--dry --some-param=xy --other-param=yx"> + <small class="help-block"><?=$lang['add']['custom_params_hint'];?></small> </div> </div> <div class="form-group"> @@ -1162,13 +1220,13 @@ if (isset($_SESSION['mailcow_cc_role'])) { <form class="form-horizontal" data-id="editfilter" role="form" method="post"> <input type="hidden" value="0" name="active"> <div class="form-group"> - <label class="control-label col-sm-2" for="script_desc"><?=$lang['edit']['sieve_desc'];?>:</label> + <label class="control-label col-sm-2" for="script_desc"><?=$lang['edit']['sieve_desc'];?></label> <div class="col-sm-10"> <input type="text" class="form-control" name="script_desc" id="script_desc" value="<?=htmlspecialchars($result['script_desc'], ENT_QUOTES, 'UTF-8');?>" required maxlength="255"> </div> </div> <div class="form-group"> - <label class="control-label col-sm-2" for="filter_type"><?=$lang['edit']['sieve_type'];?>:</label> + <label class="control-label col-sm-2" for="filter_type"><?=$lang['edit']['sieve_type'];?></label> <div class="col-sm-10"> <select id="addFilterType" name="filter_type" id="filter_type" required> <option value="prefilter" <?=($result['filter_type'] == 'prefilter') ? 'selected' : null;?>>Prefilter</option> diff --git a/data/web/inc/ajax/dns_diagnostics.php b/data/web/inc/ajax/dns_diagnostics.php index 8d9f74d7..5f8e9aae 100644 --- a/data/web/inc/ajax/dns_diagnostics.php +++ b/data/web/inc/ajax/dns_diagnostics.php @@ -75,7 +75,7 @@ if (!isset($autodiscover_config['sieve'])) { } // Init records array -$spf_link = '<a href="http://www.openspf.org/SPF_Record_Syntax" target="_blank">SPF Record Syntax</a><br />'; +$spf_link = '<a href="https://en.wikipedia.org/wiki/Sender_Policy_Framework" target="_blank">SPF Record Syntax</a><br />'; $dmarc_link = '<a href="https://www.kitterman.com/dmarc/assistant.html" target="_blank">DMARC Assistant</a>'; $records = array(); diff --git a/data/web/inc/ajax/qitem_details.php b/data/web/inc/ajax/qitem_details.php index 71d32cc9..3c82ee6a 100644 --- a/data/web/inc/ajax/qitem_details.php +++ b/data/web/inc/ajax/qitem_details.php @@ -3,8 +3,9 @@ session_start(); header("Content-Type: application/json"); require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php'; if (!isset($_SESSION['mailcow_cc_role'])) { - exit(); + exit(); } + function rrmdir($src) { $dir = opendir($src); while(false !== ( $file = readdir($dir)) ) { @@ -21,6 +22,13 @@ function rrmdir($src) { closedir($dir); rmdir($src); } +function addAddresses(&$list, $mail, $headerName) { + $addresses = $mail->getAddresses($headerName); + foreach ($addresses as $address) { + $list[] = array('address' => $address['address'], 'type' => $headerName); + } +} + if (!empty($_GET['id']) && ctype_alnum($_GET['id'])) { $tmpdir = '/tmp/' . $_GET['id'] . '/'; $mailc = quarantine('details', $_GET['id']); @@ -36,6 +44,16 @@ if (!empty($_GET['id']) && ctype_alnum($_GET['id'])) { $html2text = new Html2Text\Html2Text(); // Load msg to parser $mail_parser->setText($mailc['msg']); + + // Get mail recipients + { + $recipientsList = array(); + addAddresses($recipientsList, $mail_parser, 'to'); + addAddresses($recipientsList, $mail_parser, 'cc'); + addAddresses($recipientsList, $mail_parser, 'bcc'); + $data['recipients'] = $recipientsList; + } + // Get text/plain content $data['text_plain'] = $mail_parser->getMessageBody('text'); // Get html content and convert to text diff --git a/data/web/inc/ajax/qr_gen.php b/data/web/inc/ajax/qr_gen.php new file mode 100644 index 00000000..a39c3f64 --- /dev/null +++ b/data/web/inc/ajax/qr_gen.php @@ -0,0 +1,13 @@ +<?php +session_start(); +require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php'; +header('Content-Type: text/plain'); +if (!isset($_SESSION['mailcow_cc_role'])) { + exit(); +} + +if (isset($_GET['token']) && ctype_alnum($_GET['token'])) { + echo $tfa->getQRCodeImageAsDataUri($_SESSION['mailcow_cc_username'], $_GET['token']); +} + +?> diff --git a/data/web/inc/ajax/transport_check.php b/data/web/inc/ajax/transport_check.php index cbf253a9..f959ee35 100644 --- a/data/web/inc/ajax/transport_check.php +++ b/data/web/inc/ajax/transport_check.php @@ -58,6 +58,11 @@ if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == "admi ) ); $mail->SMTPDebug = 3; + // smtp: and smtp_enforced_tls: do not support wrapped tls, todo? + // change postfix map to detect wrapped tls or add a checkbox to toggle wrapped tls + // if ($port == 465) { + // $mail->SMTPSecure = "ssl"; + // } $mail->Debugoutput = function($str, $level) { foreach(preg_split("/((\r?\n)|(\r\n?)|\n)/", $str) as $line){ if (empty($line)) { continue; } diff --git a/data/web/inc/footer.inc.php b/data/web/inc/footer.inc.php index 365cf7da..f1cca54c 100644 --- a/data/web/inc/footer.inc.php +++ b/data/web/inc/footer.inc.php @@ -26,6 +26,10 @@ $(window).load(function() { $(".overlay").hide(); }); $(document).ready(function() { + $(document).on('shown.bs.modal', function(e) { + modal_id = $(e.relatedTarget).data('target'); + $(modal_id).attr("aria-hidden","false"); + }); // TFA, CSRF, Alerts in footer.inc.php // Other general functions in mailcow.js <?php @@ -93,6 +97,15 @@ $(document).ready(function() { } if ($(this).val() == "totp") { $('#TOTPModal').modal('show'); + request_token = $('#tfa-qr-img').data('totp-secret'); + $.ajax({ + url: '/inc/ajax/qr_gen.php', + data: { + token: request_token, + }, + }).done(function (result) { + $("#tfa-qr-img").attr("src", result); + }); $("option:selected").prop("selected", false); } if ($(this).val() == "u2f") { diff --git a/data/web/inc/functions.address_rewriting.inc.php b/data/web/inc/functions.address_rewriting.inc.php index df67d5b7..7caf65e3 100644 --- a/data/web/inc/functions.address_rewriting.inc.php +++ b/data/web/inc/functions.address_rewriting.inc.php @@ -69,7 +69,7 @@ function bcc($_action, $_data = null, $attr = null) { $_SESSION['return'][] = array( 'type' => 'danger', 'log' => array(__FUNCTION__, $_action, $_data, $_attr), - 'msg' => 'bcc_must_be_email' + 'msg' => array('bcc_must_be_email', htmlspecialchars($bcc_dest)) ); return false; } diff --git a/data/web/inc/functions.fail2ban.inc.php b/data/web/inc/functions.fail2ban.inc.php index 1cde10d3..d6440d1c 100644 --- a/data/web/inc/functions.fail2ban.inc.php +++ b/data/web/inc/functions.fail2ban.inc.php @@ -9,6 +9,11 @@ function valid_network($network) { } return false; } + +function valid_hostname($hostname) { + return filter_var($hostname, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME); +} + function fail2ban($_action, $_data = null) { global $redis; global $lang; @@ -188,7 +193,7 @@ function fail2ban($_action, $_data = null) { $wl_array = array_map('trim', preg_split( "/( |,|;|\n)/", $wl)); if (is_array($wl_array)) { foreach ($wl_array as $wl_item) { - if (valid_network($wl_item)) { + if (valid_network($wl_item) || valid_hostname($wl_item)) { $redis->hSet('F2B_WHITELIST', $wl_item, 1); } } @@ -198,7 +203,7 @@ function fail2ban($_action, $_data = null) { $bl_array = array_map('trim', preg_split( "/( |,|;|\n)/", $bl)); if (is_array($bl_array)) { foreach ($bl_array as $bl_item) { - if (valid_network($bl_item)) { + if (valid_network($bl_item) || valid_hostname($bl_item)) { $redis->hSet('F2B_BLACKLIST', $bl_item, 1); } } diff --git a/data/web/inc/functions.inc.php b/data/web/inc/functions.inc.php index 34872aef..90a5b638 100644 --- a/data/web/inc/functions.inc.php +++ b/data/web/inc/functions.inc.php @@ -1,4 +1,12 @@ <?php +function isset_has_content($var) { + if (isset($var) && $var != "") { + return true; + } + else { + return false; + } +} function hash_password($password) { $salt_str = bin2hex(openssl_random_pseudo_bytes(8)); return "{SSHA256}".base64_encode(hash('sha256', $password . $salt_str, true) . $salt_str); @@ -248,6 +256,25 @@ function hasMailboxObjectAccess($username, $role, $object) { } return false; } +function hasAliasObjectAccess($username, $role, $object) { + global $pdo; + if (!filter_var(html_entity_decode(rawurldecode($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; + } + $stmt = $pdo->prepare("SELECT `domain` FROM `alias` WHERE `address` = :object"); + $stmt->execute(array(':object' => $object)); + $row = $stmt->fetch(PDO::FETCH_ASSOC); + if (isset($row['domain']) && hasDomainAccess($username, $role, $row['domain'])) { + return true; + } + return false; +} function pem_to_der($pem_key) { // Need to remove BEGIN/END PUBLIC KEY $lines = explode("\n", trim($pem_key)); @@ -525,8 +552,8 @@ function update_sogo_static_view() { WHERE TABLE_NAME = 'sogo_view'"); $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); if ($num_results != 0) { - $stmt = $pdo->query("REPLACE INTO _sogo_static_view (`c_uid`, `domain`, `c_name`, `c_password`, `c_cn`, `mail`, `aliases`, `ad_aliases`, `kind`, `multiple_bookings`) - SELECT `c_uid`, `domain`, `c_name`, `c_password`, `c_cn`, `mail`, `aliases`, `ad_aliases`, `kind`, `multiple_bookings` from sogo_view"); + $stmt = $pdo->query("REPLACE INTO _sogo_static_view (`c_uid`, `domain`, `c_name`, `c_password`, `c_cn`, `mail`, `aliases`, `ad_aliases`, `ext_acl`, `kind`, `multiple_bookings`) + SELECT `c_uid`, `domain`, `c_name`, `c_password`, `c_cn`, `mail`, `aliases`, `ad_aliases`, `ext_acl`, `kind`, `multiple_bookings` from sogo_view"); $stmt = $pdo->query("DELETE FROM _sogo_static_view WHERE `c_uid` NOT IN (SELECT `username` FROM `mailbox` WHERE `active` = '1');"); } flush_memcached(); @@ -668,7 +695,7 @@ function user_get_alias_details($username) { while ($row = array_shift($run)) { $data['aliases_also_send_as'] = $row['send_as']; } - $stmt = $pdo->prepare("SELECT IFNULL(CONCAT(GROUP_CONCAT(DISTINCT `send_as` SEPARATOR ', '), ', ', GROUP_CONCAT(DISTINCT CONCAT('@',`alias_domain`) SEPARATOR ', ')), '✘') AS `send_as` FROM `sender_acl` LEFT JOIN `alias_domain` ON `alias_domain`.`target_domain` = TRIM(LEADING '@' FROM `send_as`) WHERE `logged_in_as` = :username AND `send_as` LIKE '@%';"); + $stmt = $pdo->prepare("SELECT CONCAT_WS(', ', IFNULL(GROUP_CONCAT(DISTINCT `send_as` SEPARATOR ', '), '✘'), GROUP_CONCAT(DISTINCT CONCAT('@',`alias_domain`) SEPARATOR ', ')) AS `send_as` FROM `sender_acl` LEFT JOIN `alias_domain` ON `alias_domain`.`target_domain` = TRIM(LEADING '@' FROM `send_as`) 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)) { @@ -1196,6 +1223,69 @@ function admin_api($action, $data = null) { 'msg' => 'admin_api_modified' ); } +function license($action, $data = null) { + global $pdo; + global $redis; + global $lang; + if ($_SESSION['mailcow_cc_role'] != "admin") { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__), + 'msg' => 'access_denied' + ); + return false; + } + switch ($action) { + case "verify": + // Keep result until revalidate button is pressed or session expired + $stmt = $pdo->query("SELECT `version` FROM `versions` WHERE `application` = 'GUID'"); + $versions = $stmt->fetch(PDO::FETCH_ASSOC); + $post = array('guid' => $versions['version']); + $curl = curl_init('https://verify.mailcow.email'); + curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); + curl_setopt($curl, CURLOPT_CONNECTTIMEOUT, 10); + curl_setopt($curl, CURLOPT_POSTFIELDS, $post); + $response = curl_exec($curl); + curl_close($curl); + $json_return = json_decode($response, true); + if ($response && $json_return) { + if ($json_return['response'] === "ok") { + $_SESSION['gal']['valid'] = "true"; + $_SESSION['gal']['c'] = $json_return['c']; + $_SESSION['gal']['s'] = $json_return['s']; + } + elseif ($json_return['response'] === "invalid") { + $_SESSION['gal']['valid'] = "false"; + $_SESSION['gal']['c'] = $lang['mailbox']['no']; + $_SESSION['gal']['s'] = $lang['mailbox']['no']; + } + } + else { + $_SESSION['gal']['valid'] = "false"; + $_SESSION['gal']['c'] = $lang['danger']['temp_error']; + $_SESSION['gal']['s'] = $lang['danger']['temp_error']; + } + try { + // json_encode needs "true"/"false" instead of true/false, to not encode it to 0 or 1 + $redis->Set('LICENSE_STATUS_CACHE', json_encode($_SESSION['gal'])); + } + catch (RedisException $e) { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $_action, $_data_log), + 'msg' => array('redis_error', $e) + ); + return false; + } + return $_SESSION['gal']['valid']; + break; + case "guid": + $stmt = $pdo->query("SELECT `version` FROM `versions` WHERE `application` = 'GUID'"); + $versions = $stmt->fetch(PDO::FETCH_ASSOC); + return $versions['version']; + break; + } +} function rspamd_ui($action, $data = null) { global $lang; if ($_SESSION['mailcow_cc_role'] != "admin") { @@ -1477,7 +1567,7 @@ function solr_status() { $endpoint = 'http://solr:8983/solr/admin/cores'; $params = array( 'action' => 'STATUS', - 'core' => 'dovecot', + 'core' => 'dovecot-fts', 'indexInfo' => 'true' ); $url = $endpoint . '?' . http_build_query($params); @@ -1494,7 +1584,7 @@ function solr_status() { else { curl_close($curl); $status = json_decode($response, true); - return (!empty($status['status']['dovecot'])) ? $status['status']['dovecot'] : false; + return (!empty($status['status']['dovecot-fts'])) ? $status['status']['dovecot-fts'] : false; } return false; } diff --git a/data/web/inc/functions.mailbox.inc.php b/data/web/inc/functions.mailbox.inc.php index 753e1702..a7fcaf13 100644 --- a/data/web/inc/functions.mailbox.inc.php +++ b/data/web/inc/functions.mailbox.inc.php @@ -326,9 +326,18 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { $description = $_data['description']; $aliases = $_data['aliases']; $mailboxes = $_data['mailboxes']; + $defquota = $_data['defquota']; $maxquota = $_data['maxquota']; $restart_sogo = $_data['restart_sogo']; $quota = $_data['quota']; + if ($defquota > $maxquota) { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), + 'msg' => 'mailbox_defquota_exceeds_mailbox_maxquota' + ); + return false; + } if ($maxquota > $quota) { $_SESSION['return'][] = array( 'type' => 'danger', @@ -337,6 +346,14 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { ); return false; } + if ($defquota == "0" || empty($defquota)) { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), + 'msg' => 'defquota_empty' + ); + return false; + } if ($maxquota == "0" || empty($maxquota)) { $_SESSION['return'][] = array( 'type' => 'danger', @@ -392,13 +409,18 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { ); return false; } - $stmt = $pdo->prepare("INSERT INTO `domain` (`domain`, `description`, `aliases`, `mailboxes`, `maxquota`, `quota`, `backupmx`, `gal`, `active`, `relay_all_recipients`) - VALUES (:domain, :description, :aliases, :mailboxes, :maxquota, :quota, :backupmx, :gal, :active, :relay_all_recipients)"); + $stmt = $pdo->prepare("DELETE FROM `sender_acl` WHERE `external` = 1 AND `send_as` LIKE :domain"); + $stmt->execute(array( + ':domain' => '%@' . $domain + )); + $stmt = $pdo->prepare("INSERT INTO `domain` (`domain`, `description`, `aliases`, `mailboxes`, `defquota`, `maxquota`, `quota`, `backupmx`, `gal`, `active`, `relay_all_recipients`) + VALUES (:domain, :description, :aliases, :mailboxes, :defquota, :maxquota, :quota, :backupmx, :gal, :active, :relay_all_recipients)"); $stmt->execute(array( ':domain' => $domain, ':description' => $description, ':aliases' => $aliases, ':mailboxes' => $mailboxes, + ':defquota' => $defquota, ':maxquota' => $maxquota, ':quota' => $quota, ':backupmx' => $backupmx, @@ -561,7 +583,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), 'msg' => array('is_alias_or_mailbox', htmlspecialchars($address)) ); - return false; + continue; } $stmt = $pdo->prepare("SELECT `domain` FROM `domain` WHERE `domain`= :domain1 OR `domain` = (SELECT `target_domain` FROM `alias_domain` WHERE `alias_domain` = :domain2)"); @@ -573,7 +595,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), 'msg' => array('domain_not_found', htmlspecialchars($domain)) ); - return false; + continue; } $stmt = $pdo->prepare("SELECT `address` FROM `spamalias` WHERE `address`= :address"); @@ -585,7 +607,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), 'msg' => array('is_spam_alias', htmlspecialchars($address)) ); - return false; + continue; } if ((!filter_var($address, FILTER_VALIDATE_EMAIL) === true) && !empty($local_part)) { $_SESSION['return'][] = array( @@ -593,7 +615,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), 'msg' => 'alias_invalid' ); - return false; + continue; } if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $domain)) { $_SESSION['return'][] = array( @@ -601,7 +623,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), 'msg' => 'access_denied' ); - return false; + continue; } $stmt = $pdo->prepare("INSERT INTO `alias` (`address`, `public_comment`, `private_comment`, `goto`, `domain`, `active`) VALUES (:address, :public_comment, :private_comment, :goto, :domain, :active)"); @@ -692,6 +714,18 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { ); continue; } + $stmt = $pdo->prepare("SELECT `domain` FROM `domain` + WHERE `domain`= :target_domain AND `backupmx` = '1'"); + $stmt->execute(array(':target_domain' => $target_domain)); + $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); + if ($num_results == 1) { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), + 'msg' => array('targetd_relay_domain', htmlspecialchars($target_domain)) + ); + continue; + } $stmt = $pdo->prepare("SELECT `alias_domain` FROM `alias_domain` WHERE `alias_domain`= :alias_domain UNION SELECT `domain` FROM `domain` WHERE `domain`= :alias_domain_in_domain"); @@ -705,6 +739,10 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { ); continue; } + $stmt = $pdo->prepare("DELETE FROM `sender_acl` WHERE `external` = 1 AND `send_as` LIKE :domain"); + $stmt->execute(array( + ':domain' => '%@' . $domain + )); $stmt = $pdo->prepare("INSERT INTO `alias_domain` (`alias_domain`, `target_domain`, `active`) VALUES (:alias_domain, :target_domain, :active)"); $stmt->execute(array( @@ -756,7 +794,15 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { $password = $_data['password']; $password2 = $_data['password2']; $name = ltrim(rtrim($_data['name'], '>'), '<'); - $quota_m = filter_var($_data['quota'], FILTER_SANITIZE_NUMBER_FLOAT); + $quota_m = intval($_data['quota']); + if ((!isset($_SESSION['acl']['unlimited_quota']) || $_SESSION['acl']['unlimited_quota'] != "1") && $quota_m === 0) { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), + 'msg' => 'unlimited_quota_acl' + ); + return false; + } if (empty($name)) { $name = $local_part; } @@ -844,14 +890,6 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { ); return false; } - if (!is_numeric($quota_m) || $quota_m == "0") { - $_SESSION['return'][] = array( - 'type' => 'danger', - 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), - 'msg' => 'quota_not_0_not_numeric' - ); - return false; - } if (!empty($password) && !empty($password2)) { if (!preg_match('/' . $GLOBALS['PASSWD_REGEP'] . '/', $password)) { $_SESSION['return'][] = array( @@ -1695,25 +1733,27 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { ); continue; } - $stmt = $pdo->prepare("SELECT `address` FROM `alias` - WHERE `address`= :address OR `address` IN ( - SELECT `username` FROM `mailbox`, `alias_domain` - WHERE ( - `alias_domain`.`alias_domain` = :address_d - AND `mailbox`.`username` = CONCAT(:address_l, '@', alias_domain.target_domain)))"); - $stmt->execute(array( - ':address' => $address, - ':address_l' => $local_part, - ':address_d' => $domain - )); - $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); - if ($num_results != 0) { - $_SESSION['return'][] = array( - 'type' => 'danger', - 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), - 'msg' => array('is_alias_or_mailbox', htmlspecialchars($address)) - ); - continue; + if (strtolower($is_now['address']) != strtolower($address)) { + $stmt = $pdo->prepare("SELECT `address` FROM `alias` + WHERE `address`= :address OR `address` IN ( + SELECT `username` FROM `mailbox`, `alias_domain` + WHERE ( + `alias_domain`.`alias_domain` = :address_d + AND `mailbox`.`username` = CONCAT(:address_l, '@', alias_domain.target_domain)))"); + $stmt->execute(array( + ':address' => $address, + ':address_l' => $local_part, + ':address_d' => $domain + )); + $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); + if ($num_results != 0) { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), + 'msg' => array('is_alias_or_mailbox', htmlspecialchars($address)) + ); + continue; + } } $stmt = $pdo->prepare("SELECT `domain` FROM `domain` WHERE `domain`= :domain1 OR `domain` = (SELECT `target_domain` FROM `alias_domain` WHERE `alias_domain` = :domain2)"); @@ -1773,6 +1813,14 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { unset($gotos[$i]); continue; } + // Delete from sender_acl to prevent duplicates + $stmt = $pdo->prepare("DELETE FROM `sender_acl` WHERE + `logged_in_as` = :goto AND + `send_as` = :address"); + $stmt->execute(array( + ':goto' => $goto, + ':address' => $address + )); } $gotos = array_filter($gotos); $goto = implode(",", $gotos); @@ -1859,6 +1907,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { $relayhost = (isset($_data['relayhost'])) ? intval($_data['relayhost']) : $is_now['relayhost']; $aliases = (!empty($_data['aliases'])) ? $_data['aliases'] : $is_now['max_num_aliases_for_domain']; $mailboxes = (isset($_data['mailboxes']) && $_data['mailboxes'] != '') ? intval($_data['mailboxes']) : $is_now['max_num_mboxes_for_domain']; + $defquota = (isset($_data['defquota']) && $_data['defquota'] != '') ? intval($_data['defquota']) : ($is_now['def_quota_for_mbox'] / 1048576); $maxquota = (!empty($_data['maxquota'])) ? $_data['maxquota'] : ($is_now['max_quota_for_mbox'] / 1048576); $quota = (!empty($_data['quota'])) ? $_data['quota'] : ($is_now['max_quota_for_domain'] / 1048576); $description = (!empty($_data['description'])) ? $_data['description'] : $is_now['description']; @@ -1890,6 +1939,22 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { )"); $stmt->execute(array(':domain' => $domain)); $AliasData = $stmt->fetch(PDO::FETCH_ASSOC); + if ($defquota > $maxquota) { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), + 'msg' => 'mailbox_defquota_exceeds_mailbox_maxquota' + ); + continue; + } + if ($defquota == "0" || empty($defquota)) { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), + 'msg' => 'defquota_empty' + ); + continue; + } if ($maxquota > $quota) { $_SESSION['return'][] = array( 'type' => 'danger', @@ -1944,6 +2009,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { `gal` = :gal, `active` = :active, `quota` = :quota, + `defquota` = :defquota, `maxquota` = :maxquota, `relayhost` = :relayhost, `mailboxes` = :mailboxes, @@ -1956,6 +2022,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { ':gal' => $gal, ':active' => $active, ':quota' => $quota, + ':defquota' => $defquota, ':maxquota' => $maxquota, ':relayhost' => $relayhost, ':mailboxes' => $mailboxes, @@ -1993,9 +2060,9 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { $active = (isset($_data['active'])) ? intval($_data['active']) : $is_now['active_int']; (int)$force_pw_update = (isset($_data['force_pw_update'])) ? intval($_data['force_pw_update']) : intval($is_now['attributes']['force_pw_update']); (int)$sogo_access = (isset($_data['sogo_access'])) ? intval($_data['sogo_access']) : intval($is_now['attributes']['sogo_access']); + (int)$quota_m = (isset_has_content($_data['quota'])) ? intval($_data['quota']) : ($is_now['quota'] / 1048576); $name = (!empty($_data['name'])) ? ltrim(rtrim($_data['name'], '>'), '<') : $is_now['name']; $domain = $is_now['domain']; - $quota_m = (!empty($_data['quota'])) ? $_data['quota'] : ($is_now['quota'] / 1048576); $quota_b = $quota_m * 1048576; $password = (!empty($_data['password'])) ? $_data['password'] : null; $password2 = (!empty($_data['password2'])) ? $_data['password2'] : null; @@ -2008,6 +2075,15 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { ); continue; } + // if already 0 == ok + if ((!isset($_SESSION['acl']['unlimited_quota']) || $_SESSION['acl']['unlimited_quota'] != "1") && ($quota_m == 0 && $is_now['quota'] != 0)) { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), + 'msg' => 'unlimited_quota_acl' + ); + return false; + } $stmt = $pdo->prepare("SELECT `quota`, `maxquota` FROM `domain` WHERE `domain` = :domain"); @@ -2021,14 +2097,6 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { ); continue; } - if (!is_numeric($quota_m) || $quota_m == "0") { - $_SESSION['return'][] = array( - 'type' => 'danger', - 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), - 'msg' => array('quota_not_0_not_numeric', htmlspecialchars($quota_m)) - ); - continue; - } if ($quota_m > $DomainData['maxquota']) { $_SESSION['return'][] = array( 'type' => 'danger', @@ -2045,6 +2113,75 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { ); continue; } + $extra_acls = array(); + if (isset($_data['extended_sender_acl'])) { + if (!isset($_SESSION['acl']['extend_sender_acl']) || $_SESSION['acl']['extend_sender_acl'] != "1" ) { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), + 'msg' => 'access_denied' + ); + return false; + } + $extra_acls = array_map('trim', preg_split( "/( |,|;|\n)/", $_data['extended_sender_acl'])); + foreach ($extra_acls as $i => &$extra_acl) { + if (empty($extra_acl)) { + continue; + } + if (substr($extra_acl, 0, 1) === "@") { + $extra_acl = ltrim($extra_acl, '@'); + } + if (!filter_var($extra_acl, FILTER_VALIDATE_EMAIL) && !is_valid_domain_name($extra_acl)) { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), + 'msg' => array('extra_acl_invalid', htmlspecialchars($extra_acl)) + ); + unset($extra_acls[$i]); + continue; + } + $domains = array_merge(mailbox('get', 'domains'), mailbox('get', 'alias_domains')); + if (filter_var($extra_acl, FILTER_VALIDATE_EMAIL)) { + $extra_acl_domain = idn_to_ascii(substr(strstr($extra_acl, '@'), 1), 0, INTL_IDNA_VARIANT_UTS46); + if (in_array($extra_acl_domain, $domains)) { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), + 'msg' => array('extra_acl_invalid_domain', $extra_acl_domain) + ); + unset($extra_acls[$i]); + continue; + } + } + else { + if (in_array($extra_acl, $domains)) { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), + 'msg' => array('extra_acl_invalid_domain', $extra_acl_domain) + ); + unset($extra_acls[$i]); + continue; + } + $extra_acl = '@' . $extra_acl; + } + } + $extra_acls = array_filter($extra_acls); + $extra_acls = array_values($extra_acls); + $extra_acls = array_unique($extra_acls); + $stmt = $pdo->prepare("DELETE FROM `sender_acl` WHERE `external` = 1 AND `logged_in_as` = :username"); + $stmt->execute(array( + ':username' => $username + )); + foreach ($extra_acls as $sender_acl_external) { + $stmt = $pdo->prepare("INSERT INTO `sender_acl` (`send_as`, `logged_in_as`, `external`) + VALUES (:sender_acl, :username, 1)"); + $stmt->execute(array( + ':sender_acl' => $sender_acl_external, + ':username' => $username + )); + } + } if (isset($_data['sender_acl'])) { // Get sender_acl items set by admin $sender_acl_admin = array_merge( @@ -2116,9 +2253,9 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { unset($sender_acl_domain_admin[$key]); continue; } - // Check if user has mailbox access (if object is email) + // Check if user has alias access (if object is email) if (filter_var($val, FILTER_VALIDATE_EMAIL)) { - if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $val)) { + if (!hasAliasObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $val)) { $_SESSION['return'][] = array( 'type' => 'danger', 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), @@ -2133,15 +2270,20 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { $sender_acl_merged = array_merge($sender_acl_domain_admin, $sender_acl_admin); // If merged array still contains "*", set it as only value !in_array('*', $sender_acl_merged) ?: $sender_acl_merged = array('*'); - $stmt = $pdo->prepare("DELETE FROM `sender_acl` WHERE `logged_in_as` = :username"); + $stmt = $pdo->prepare("DELETE FROM `sender_acl` WHERE `external` = 0 AND `logged_in_as` = :username"); $stmt->execute(array( ':username' => $username )); + $fixed_sender_aliases = mailbox('get', 'sender_acl_handles', $username)['fixed_sender_aliases']; foreach ($sender_acl_merged as $sender_acl) { $domain = ltrim($sender_acl, '@'); if (is_valid_domain_name($domain)) { $sender_acl = '@' . $domain; } + // Don't add if allowed by alias + if (in_array($sender_acl, $fixed_sender_aliases)) { + continue; + } $stmt = $pdo->prepare("INSERT INTO `sender_acl` (`send_as`, `logged_in_as`) VALUES (:sender_acl, :username)"); $stmt->execute(array( @@ -2151,7 +2293,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { } } else { - $stmt = $pdo->prepare("DELETE FROM `sender_acl` WHERE `logged_in_as` = :username"); + $stmt = $pdo->prepare("DELETE FROM `sender_acl` WHERE `external` = 0 AND `logged_in_as` = :username"); $stmt->execute(array( ':username' => $username )); @@ -2306,6 +2448,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { $data['sender_acl_addresses']['rw'] = array(); $data['sender_acl_addresses']['selectable'] = array(); $data['fixed_sender_aliases'] = array(); + $data['external_sender_aliases'] = array(); // Fixed addresses $stmt = $pdo->prepare("SELECT `address` FROM `alias` WHERE `goto` REGEXP :goto AND `address` NOT LIKE '@%'"); $stmt->execute(array(':goto' => '(^|,)'.$_data.'($|,)')); @@ -2323,9 +2466,18 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { $data['fixed_sender_aliases'][] = $row['alias_domain_alias']; } } + // External addresses + $stmt = $pdo->prepare("SELECT `send_as` as `send_as_external` FROM `sender_acl` WHERE `logged_in_as` = :logged_in_as AND `external` = '1'"); + $stmt->execute(array(':logged_in_as' => $_data)); + $exernal_rows = $stmt->fetchAll(PDO::FETCH_ASSOC); + while ($row = array_shift($exernal_rows)) { + if (!empty($row['send_as_external'])) { + $data['external_sender_aliases'][] = $row['send_as_external']; + } + } // 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 '@%' OR `send_as` = '*')"); + $stmt = $pdo->prepare("SELECT REPLACE(`send_as`, '@', '') AS `send_as` FROM `sender_acl` WHERE `logged_in_as` = :logged_in_as AND `external` = '0' AND (`send_as` LIKE '@%' OR `send_as` = '*')"); $stmt->execute(array(':logged_in_as' => $_data)); $domain_rows = $stmt->fetchAll(PDO::FETCH_ASSOC); while ($domain_row = array_shift($domain_rows)) { @@ -2344,15 +2496,15 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { $data['sender_acl_domains']['rw'][] = $domain_row['send_as']; } } - $stmt = $pdo->prepare("SELECT `send_as` FROM `sender_acl` WHERE `logged_in_as` = :logged_in_as AND (`send_as` NOT LIKE '@%' AND `send_as` != '*')"); + $stmt = $pdo->prepare("SELECT `send_as` FROM `sender_acl` WHERE `logged_in_as` = :logged_in_as AND `external` = '0' AND (`send_as` NOT LIKE '@%' AND `send_as` != '*')"); $stmt->execute(array(':logged_in_as' => $_data)); $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'])) { + if (filter_var($address_row['send_as'], FILTER_VALIDATE_EMAIL) && !hasAliasObjectAccess($_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'])) { + if (filter_var($address_row['send_as'], FILTER_VALIDATE_EMAIL) && hasAliasObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $address_row['send_as'])) { $data['sender_acl_addresses']['rw'][] = $address_row['send_as']; continue; } @@ -2361,12 +2513,14 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { WHERE `domain` NOT IN ( SELECT REPLACE(`send_as`, '@', '') FROM `sender_acl` WHERE `logged_in_as` = :logged_in_as1 + AND `external` = '0' AND `send_as` LIKE '@%') UNION SELECT '*' FROM `domain` WHERE '*' NOT IN ( SELECT `send_as` FROM `sender_acl` WHERE `logged_in_as` = :logged_in_as2 + AND `external` = '0' )"); $stmt->execute(array( ':logged_in_as1' => $_data, @@ -2388,6 +2542,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { AND `address` NOT IN ( SELECT `send_as` FROM `sender_acl` WHERE `logged_in_as` = :logged_in_as + AND `external` = '0' AND `send_as` NOT LIKE '@%')"); $stmt->execute(array( ':logged_in_as' => $_data, @@ -2395,7 +2550,11 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { )); $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'])) { + // Aliases are not selectable + if (in_array($row['address'], $data['fixed_sender_aliases'])) { + continue; + } + if (filter_var($row['address'], FILTER_VALIDATE_EMAIL) && hasAliasObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $row['address'])) { $data['sender_acl_addresses']['selectable'][] = $row['address']; } } @@ -2852,7 +3011,13 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { ':aliasdomain' => $_data, )); $row = $stmt->fetch(PDO::FETCH_ASSOC); + $stmt = $pdo->prepare("SELECT `backupmx` FROM `domain` WHERE `domain` = :target_domain"); + $stmt->execute(array( + ':target_domain' => $row['target_domain'] + )); + $row_parent = $stmt->fetch(PDO::FETCH_ASSOC); $aliasdomaindata['alias_domain'] = $row['alias_domain']; + $aliasdomaindata['parent_is_backupmx'] = $row_parent['backupmx']; $aliasdomaindata['target_domain'] = $row['target_domain']; $aliasdomaindata['active'] = $row['active']; $aliasdomaindata['rl'] = $rl; @@ -2904,6 +3069,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { `description`, `aliases`, `mailboxes`, + `defquota`, `maxquota`, `quota`, `relayhost`, @@ -2935,6 +3101,10 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { if ($domaindata['max_new_mailbox_quota'] > ($row['maxquota'] * 1048576)) { $domaindata['max_new_mailbox_quota'] = ($row['maxquota'] * 1048576); } + $domaindata['def_new_mailbox_quota'] = $domaindata['max_new_mailbox_quota']; + if ($domaindata['def_new_mailbox_quota'] > ($row['defquota'] * 1048576)) { + $domaindata['def_new_mailbox_quota'] = ($row['defquota'] * 1048576); + } $domaindata['quota_used_in_domain'] = $MailboxDataDomain['in_use']; $domaindata['mboxes_in_domain'] = $MailboxDataDomain['count']; $domaindata['mboxes_left'] = $row['mailboxes'] - $MailboxDataDomain['count']; @@ -2942,6 +3112,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { $domaindata['description'] = $row['description']; $domaindata['max_num_aliases_for_domain'] = $row['aliases']; $domaindata['max_num_mboxes_for_domain'] = $row['mailboxes']; + $domaindata['def_quota_for_mbox'] = $row['defquota'] * 1048576; $domaindata['max_quota_for_mbox'] = $row['maxquota'] * 1048576; $domaindata['max_quota_for_domain'] = $row['quota'] * 1048576; $domaindata['relayhost'] = $row['relayhost']; @@ -3006,7 +3177,14 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { $mailboxdata['max_new_quota'] = ($DomainQuota['maxquota'] * 1048576); } $mailboxdata['username'] = $row['username']; - $mailboxdata['rl'] = $rl; + if (!empty($rl)) { + $mailboxdata['rl'] = $rl; + $mailboxdata['rl_scope'] = 'mailbox'; + } + else { + $mailboxdata['rl'] = ratelimit('get', 'domain', $row['domain']); + $mailboxdata['rl_scope'] = 'domain'; + } $mailboxdata['is_relayed'] = $row['backupmx']; $mailboxdata['name'] = $row['name']; $mailboxdata['active'] = $row['active']; @@ -3016,10 +3194,13 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { $mailboxdata['quota'] = $row['quota']; $mailboxdata['attributes'] = json_decode($row['attributes'], true); $mailboxdata['quota_used'] = intval($row['bytes']); - $mailboxdata['percent_in_use'] = round((intval($row['bytes']) / intval($row['quota'])) * 100); + $mailboxdata['percent_in_use'] = ($row['quota'] == 0) ? '- ' : round((intval($row['bytes']) / intval($row['quota'])) * 100); $mailboxdata['messages'] = $row['messages']; $mailboxdata['spam_aliases'] = $SpamaliasUsage['sa_count']; - if ($mailboxdata['percent_in_use'] >= 90) { + if ($mailboxdata['percent_in_use'] === '- ') { + $mailboxdata['percent_class'] = "info"; + } + elseif ($mailboxdata['percent_in_use'] >= 90) { $mailboxdata['percent_class'] = "danger"; } elseif ($mailboxdata['percent_in_use'] >= 75) { @@ -3317,7 +3498,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { $_SESSION['return'][] = array( 'type' => 'danger', 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), - 'msg' => 'domain_not_empty' + 'msg' => array('domain_not_empty', $domain) ); continue; } @@ -3411,6 +3592,10 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { $stmt->execute(array( ':id' => $id )); + $stmt = $pdo->prepare("DELETE FROM `sender_acl` WHERE `send_as` = :alias_address"); + $stmt->execute(array( + ':alias_address' => $alias_data['address'] + )); $_SESSION['return'][] = array( 'type' => 'success', 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), @@ -3525,7 +3710,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { } if (strtolower(getenv('SKIP_SOLR')) == 'n') { $curl = curl_init(); - curl_setopt($curl, CURLOPT_URL, 'http://solr:8983/solr/dovecot/update?commit=true'); + curl_setopt($curl, CURLOPT_URL, 'http://solr:8983/solr/dovecot-fts/update?commit=true'); curl_setopt($curl, CURLOPT_HTTPHEADER,array('Content-Type: text/xml')); curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1); curl_setopt($curl, CURLOPT_POST, 1); @@ -3587,7 +3772,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { $stmt->execute(array( ':username' => $username )); - $stmt = $pdo->prepare("DELETE FROM `sogo_acl` WHERE `c_object` LIKE '%/" . $username . "/%' OR `c_uid` = :username"); + $stmt = $pdo->prepare("DELETE FROM `sogo_acl` WHERE `c_object` LIKE '%/" . str_replace('%', '\%', $username) . "/%' OR `c_uid` = :username"); $stmt->execute(array( ':username' => $username )); @@ -3714,7 +3899,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { } break; } - if ($_action != 'get' && in_array($_type, array('domain', 'alias', 'alias_domain', 'mailbox'))) { + if ($_action != 'get' && in_array($_type, array('domain', 'alias', 'alias_domain', 'mailbox', 'resource'))) { update_sogo_static_view(); } } diff --git a/data/web/inc/functions.quarantine.inc.php b/data/web/inc/functions.quarantine.inc.php index 34dbf39a..430047e0 100644 --- a/data/web/inc/functions.quarantine.inc.php +++ b/data/web/inc/functions.quarantine.inc.php @@ -296,13 +296,18 @@ function quarantine($_action, $_data = null) { $release_format = 'raw'; } $max_size = $_data['max_size']; + $max_age = intval($_data['max_age']); $subject = $_data['subject']; $sender = $_data['sender']; $html = $_data['html_tmpl']; + if ($max_age <= 0) { + $max_age = 365; + } $exclude_domains = (array)$_data['exclude_domains']; try { $redis->Set('Q_RETENTION_SIZE', intval($retention_size)); $redis->Set('Q_MAX_SIZE', intval($max_size)); + $redis->Set('Q_MAX_AGE', $max_age); $redis->Set('Q_EXCLUDE_DOMAINS', json_encode($exclude_domains)); $redis->Set('Q_RELEASE_FORMAT', $release_format); $redis->Set('Q_SENDER', $sender); @@ -614,7 +619,7 @@ function quarantine($_action, $_data = null) { break; case 'get': if ($_SESSION['mailcow_cc_role'] == "user") { - $stmt = $pdo->prepare('SELECT `id`, `qid`, `subject`, LOCATE("VIRUS_FOUND", `symbols`) AS `virus_flag`, `rcpt`, `sender`, UNIX_TIMESTAMP(`created`) AS `created` FROM `quarantine` WHERE `rcpt` = :mbox'); + $stmt = $pdo->prepare('SELECT `id`, `qid`, `subject`, LOCATE("VIRUS_FOUND", `symbols`) AS `virus_flag`, `score`, `rcpt`, `sender`, UNIX_TIMESTAMP(`created`) AS `created` FROM `quarantine` WHERE `rcpt` = :mbox'); $stmt->execute(array(':mbox' => $_SESSION['mailcow_cc_username'])); $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); while($row = array_shift($rows)) { @@ -622,7 +627,7 @@ function quarantine($_action, $_data = null) { } } elseif ($_SESSION['mailcow_cc_role'] == "admin") { - $stmt = $pdo->query('SELECT `id`, `qid`, `subject`, LOCATE("VIRUS_FOUND", `symbols`) AS `virus_flag`, `rcpt`, `sender`, UNIX_TIMESTAMP(`created`) AS `created` FROM `quarantine`'); + $stmt = $pdo->query('SELECT `id`, `qid`, `subject`, LOCATE("VIRUS_FOUND", `symbols`) AS `virus_flag`, `score`, `rcpt`, `sender`, UNIX_TIMESTAMP(`created`) AS `created` FROM `quarantine`'); $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); while($row = array_shift($rows)) { $q_meta[] = $row; @@ -631,7 +636,7 @@ function quarantine($_action, $_data = null) { else { $domains = array_merge(mailbox('get', 'domains'), mailbox('get', 'alias_domains')); foreach ($domains as $domain) { - $stmt = $pdo->prepare('SELECT `id`, `qid`, `subject`, LOCATE("VIRUS_FOUND", `symbols`) AS `virus_flag`, `rcpt`, `sender`, UNIX_TIMESTAMP(`created`) AS `created` FROM `quarantine` WHERE `rcpt` REGEXP :domain'); + $stmt = $pdo->prepare('SELECT `id`, `qid`, `subject`, LOCATE("VIRUS_FOUND", `symbols`) AS `virus_flag`, `score`, `rcpt`, `sender`, UNIX_TIMESTAMP(`created`) AS `created` FROM `quarantine` WHERE `rcpt` REGEXP :domain'); $stmt->execute(array(':domain' => '@' . $domain . '$')); $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); while($row = array_shift($rows)) { @@ -647,6 +652,7 @@ function quarantine($_action, $_data = null) { $settings['exclude_domains'] = json_decode($redis->Get('Q_EXCLUDE_DOMAINS'), true); } $settings['max_size'] = $redis->Get('Q_MAX_SIZE'); + $settings['max_age'] = $redis->Get('Q_MAX_AGE'); $settings['retention_size'] = $redis->Get('Q_RETENTION_SIZE'); $settings['release_format'] = $redis->Get('Q_RELEASE_FORMAT'); $settings['subject'] = $redis->Get('Q_SUBJ'); diff --git a/data/web/inc/functions.transports.inc.php b/data/web/inc/functions.transports.inc.php index e45fab78..f87a7c01 100644 --- a/data/web/inc/functions.transports.inc.php +++ b/data/web/inc/functions.transports.inc.php @@ -186,21 +186,14 @@ function transport($_action, $_data = null) { ); return false; } - $destination = trim($_data['destination']); + $destinations = array_map('trim', preg_split( "/( |,|;|\n)/", $_data['destination'])); + $active = intval($_data['active']); + $lookup_mx = intval($_data['lookup_mx']); $nexthop = trim($_data['nexthop']); preg_match('/\[(.+)\].*/', $nexthop, $next_hop_matches); $next_hop_clean = (isset($next_hop_matches[1])) ? $next_hop_matches[1] : $nexthop; $username = str_replace(':', '\:', trim($_data['username'])); $password = str_replace(':', '\:', trim($_data['password'])); - // ".domain" is a valid destination, "..domain" is not - if (empty($destination) || (is_valid_domain_name(preg_replace('/^' . preg_quote('.', '/') . '/', '', $destination)) === false && $destination != '*')) { - $_SESSION['return'][] = array( - 'type' => 'danger', - 'log' => array(__FUNCTION__, $_action, $_data_log), - 'msg' => 'invalid_destination' - ); - return false; - } if (empty($nexthop)) { $_SESSION['return'][] = array( 'type' => 'danger', @@ -223,8 +216,35 @@ function transport($_action, $_data = null) { ); return false; } + foreach ($destinations as $d_ix => &$dest) { + if (empty($dest)) { + unset($destinations[$d_ix]); + continue; + } + if ($transport_data['destination'] == $dest) { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $_action, $_data_log), + 'msg' => array('transport_dest_exists', $dest) + ); + unset($destinations[$d_ix]); + continue; + } + // ".domain" is a valid destination, "..domain" is not + if (empty($dest) || (is_valid_domain_name(preg_replace('/^' . preg_quote('.', '/') . '/', '', $dest)) === false && $dest != '*' && filter_var($dest, FILTER_VALIDATE_EMAIL) === false)) { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $_action, $_data_log), + 'msg' => array('invalid_destination', $dest) + ); + unset($destinations[$d_ix]); + continue; + } + } } } + $destinations = array_values($destinations); + if (empty($destinations)) { return false; } if (isset($next_hop_matches[1])) { if (in_array($next_hop_clean, $existing_nh)) { $_SESSION['return'][] = array( @@ -247,34 +267,27 @@ function transport($_action, $_data = null) { } } } - try { - $stmt = $pdo->prepare("INSERT INTO `transports` (`nexthop`, `destination`, `username` ,`password`, `active`) - VALUES (:nexthop, :destination, :username, :password, :active)"); + foreach ($destinations as $insert_dest) { + $stmt = $pdo->prepare("INSERT INTO `transports` (`nexthop`, `destination`, `username` , `password`, `lookup_mx`, `active`) + VALUES (:nexthop, :destination, :username, :password, :lookup_mx, :active)"); $stmt->execute(array( ':nexthop' => $nexthop, - ':destination' => $destination, + ':destination' => $insert_dest, ':username' => $username, ':password' => str_replace(':', '\:', $password), - ':active' => '1' - )); - $stmt = $pdo->prepare("UPDATE `transports` SET - `username` = :username, - `password` = :password - WHERE `nexthop` = :nexthop"); - $stmt->execute(array( - ':nexthop' => $nexthop, - ':username' => $username, - ':password' => $password + ':lookup_mx' => $lookup_mx, + ':active' => $active )); } - catch (PDOException $e) { - $_SESSION['return'][] = array( - 'type' => 'danger', - 'log' => array(__FUNCTION__, $_action, $_data_log), - 'msg' => array('mysql_error', $e) - ); - return false; - } + $stmt = $pdo->prepare("UPDATE `transports` SET + `username` = :username, + `password` = :password + WHERE `nexthop` = :nexthop"); + $stmt->execute(array( + ':nexthop' => $nexthop, + ':username' => $username, + ':password' => $password + )); $_SESSION['return'][] = array( 'type' => 'success', 'log' => array(__FUNCTION__, $_action, $_data_log), @@ -298,7 +311,8 @@ function transport($_action, $_data = null) { $nexthop = (!empty($_data['nexthop'])) ? trim($_data['nexthop']) : $is_now['nexthop']; $username = (isset($_data['username'])) ? trim($_data['username']) : $is_now['username']; $password = (isset($_data['password'])) ? trim($_data['password']) : $is_now['password']; - $active = (isset($_data['active'])) ? intval($_data['active']) : $is_now['active_int']; + $lookup_mx = (isset($_data['lookup_mx']) && $_data['lookup_mx'] != '') ? intval($_data['lookup_mx']) : $is_now['lookup_mx_int']; + $active = (isset($_data['active']) && $_data['active'] != '') ? intval($_data['active']) : $is_now['active_int']; } else { $_SESSION['return'][] = array( @@ -319,6 +333,14 @@ function transport($_action, $_data = null) { } $existing_nh[] = $transport_data['nexthop']; preg_match('/\[(.+)\].*/', $transport_data['nexthop'], $existing_clean_nh[]); + if ($transport_data['destination'] == $destination) { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $_action, $_data_log), + 'msg' => 'transport_dest_exists' + ); + return false; + } } } if (isset($next_hop_matches[1])) { @@ -352,6 +374,7 @@ function transport($_action, $_data = null) { `nexthop` = :nexthop, `username` = :username, `password` = :password, + `lookup_mx` = :lookup_mx, `active` = :active WHERE `id` = :id"); $stmt->execute(array( @@ -360,6 +383,7 @@ function transport($_action, $_data = null) { ':nexthop' => $nexthop, ':username' => $username, ':password' => $password, + ':lookup_mx' => $lookup_mx, ':active' => $active )); $stmt = $pdo->prepare("UPDATE `transports` SET @@ -437,8 +461,10 @@ function transport($_action, $_data = null) { `username`, `password`, `active` AS `active_int`, + `lookup_mx` AS `lookup_mx_int`, CONCAT(LEFT(`password`, 3), '...') AS `password_short`, - CASE `active` WHEN 1 THEN '".$lang['mailbox']['yes']."' ELSE '".$lang['mailbox']['no']."' END AS `active` + CASE `active` WHEN 1 THEN '".$lang['mailbox']['yes']."' ELSE '".$lang['mailbox']['no']."' END AS `active`, + CASE `lookup_mx` WHEN 1 THEN '".$lang['mailbox']['yes']."' ELSE '".$lang['mailbox']['no']."' END AS `lookup_mx` FROM `transports` WHERE `id` = :id"); $stmt->execute(array(':id' => $_data)); diff --git a/data/web/inc/header.inc.php b/data/web/inc/header.inc.php index 47e53dc5..4b223140 100644 --- a/data/web/inc/header.inc.php +++ b/data/web/inc/header.inc.php @@ -3,7 +3,7 @@ <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> - <meta name="viewport" content="width=device-width, initial-scale=1"> + <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0"> <meta name="theme-color" content="#F5D76E"/> <meta http-equiv="Referrer-Policy" content="same-origin"> <title><?=$UI_TEXTS['title_name'];?></title> @@ -26,6 +26,9 @@ if (preg_match("/debug/i", $_SERVER['REQUEST_URI'])) { $css_minifier->add('/web/css/site/debug.css'); } + if ($_SERVER['REQUEST_URI'] == '/') { + $css_minifier->add('/web/css/site/index.css'); + } ?> <style><?=$css_minifier->minify();?></style> <?php if (strtolower(trim($DEFAULT_THEME)) != "lumen"): ?> diff --git a/data/web/inc/init_db.inc.php b/data/web/inc/init_db.inc.php index a66d2ab3..019c1afb 100644 --- a/data/web/inc/init_db.inc.php +++ b/data/web/inc/init_db.inc.php @@ -3,12 +3,12 @@ function init_db_schema() { try { global $pdo; - $db_version = "27012019_1217"; + $db_version = "01092019_1240"; $stmt = $pdo->query("SHOW TABLES LIKE 'versions'"); $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); if ($num_results != 0) { - $stmt = $pdo->query("SELECT `version` FROM `versions`"); + $stmt = $pdo->query("SELECT `version` FROM `versions` WHERE `application` = 'db_schema'"); if ($stmt->fetch(PDO::FETCH_ASSOC)['version'] == $db_version) { return true; } @@ -21,10 +21,16 @@ function init_db_schema() { AND active = '1' AND address NOT LIKE '@%' GROUP BY goto;", + // Unused at the moment - we cannot allow to show a foreign mailbox as sender address in SOGo, as SOGo does not like this + // We need to create delegation in SOGo AND set a sender_acl in mailcow to allow to send as user X "grouped_sender_acl" => "CREATE VIEW grouped_sender_acl (username, send_as_acl) AS SELECT logged_in_as, IFNULL(GROUP_CONCAT(send_as SEPARATOR ' '), '') AS send_as_acl FROM sender_acl WHERE send_as NOT LIKE '@%' GROUP BY logged_in_as;", + "grouped_sender_acl_external" => "CREATE VIEW grouped_sender_acl_external (username, send_as_acl) AS + SELECT logged_in_as, IFNULL(GROUP_CONCAT(send_as SEPARATOR ' '), '') AS send_as_acl FROM sender_acl + WHERE send_as NOT LIKE '@%' AND external = '1' + GROUP BY logged_in_as;", "grouped_domain_alias_address" => "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 @@ -78,6 +84,7 @@ function init_db_schema() { // TODO -> use TEXT and check if SOGo login breaks on empty aliases "aliases" => "TEXT NOT NULL", "ad_aliases" => "VARCHAR(6144) NOT NULL DEFAULT ''", + "ext_acl" => "VARCHAR(6144) NOT NULL DEFAULT ''", "kind" => "VARCHAR(100) NOT NULL DEFAULT ''", "multiple_bookings" => "INT NOT NULL DEFAULT -1" ), @@ -116,6 +123,7 @@ function init_db_schema() { "nexthop" => "VARCHAR(255) NOT NULL", "username" => "VARCHAR(255) NOT NULL", "password" => "VARCHAR(255) NOT NULL", + "lookup_mx" => "TINYINT(1) NOT NULL DEFAULT '1'", "active" => "TINYINT(1) NOT NULL DEFAULT '1'" ), "keys" => array( @@ -173,7 +181,8 @@ function init_db_schema() { "cols" => array( "id" => "INT NOT NULL AUTO_INCREMENT", "logged_in_as" => "VARCHAR(255) NOT NULL", - "send_as" => "VARCHAR(255) NOT NULL" + "send_as" => "VARCHAR(255) NOT NULL", + "external" => "TINYINT(1) NOT NULL DEFAULT '0'" ), "keys" => array( "primary" => array( @@ -189,7 +198,8 @@ function init_db_schema() { "description" => "VARCHAR(255)", "aliases" => "INT(10) NOT NULL DEFAULT '0'", "mailboxes" => "INT(10) NOT NULL DEFAULT '0'", - "maxquota" => "BIGINT(20) NOT NULL DEFAULT '0'", + "defquota" => "BIGINT(20) NOT NULL DEFAULT '3072'", + "maxquota" => "BIGINT(20) NOT NULL DEFAULT '102400'", "quota" => "BIGINT(20) NOT NULL DEFAULT '102400'", "relayhost" => "VARCHAR(255) NOT NULL DEFAULT '0'", "backupmx" => "TINYINT(1) NOT NULL DEFAULT '0'", @@ -464,6 +474,8 @@ function init_db_schema() { "filters" => "TINYINT(1) NOT NULL DEFAULT '1'", "ratelimit" => "TINYINT(1) NOT NULL DEFAULT '1'", "spam_policy" => "TINYINT(1) NOT NULL DEFAULT '1'", + "extend_sender_acl" => "TINYINT(1) NOT NULL DEFAULT '0'", + "unlimited_quota" => "TINYINT(1) NOT NULL DEFAULT '0'", "alias_domains" => "TINYINT(1) NOT NULL DEFAULT '0'", ), "keys" => array( @@ -501,7 +513,7 @@ function init_db_schema() { "timeout2" => "SMALLINT NOT NULL DEFAULT '600'", "subscribeall" => "TINYINT(1) NOT NULL DEFAULT '1'", "is_running" => "TINYINT(1) NOT NULL DEFAULT '0'", - "returned_text" => "MEDIUMTEXT", + "returned_text" => "LONGTEXT", "last_run" => "TIMESTAMP NULL DEFAULT NULL", "created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)", "modified" => "DATETIME ON UPDATE CURRENT_TIMESTAMP", diff --git a/data/web/inc/lib/composer.json b/data/web/inc/lib/composer.json index e072a0dd..07dfe4ba 100644 --- a/data/web/inc/lib/composer.json +++ b/data/web/inc/lib/composer.json @@ -3,7 +3,7 @@ "robthree/twofactorauth": "^1.6", "yubico/u2flib-server": "^1.0", "phpmailer/phpmailer": "^5.2", - "php-mime-mail-parser/php-mime-mail-parser": "^2.9", + "php-mime-mail-parser/php-mime-mail-parser": "^5.1", "soundasleep/html2text": "^0.5.0", "ddeboer/imap": "^1.5", "matthiasmullie/minify": "^1.3" diff --git a/data/web/inc/lib/vendor/php-mime-mail-parser/php-mime-mail-parser/README.md b/data/web/inc/lib/vendor/php-mime-mail-parser/php-mime-mail-parser/README.md index 0b0502fc..acd661e6 100644 --- a/data/web/inc/lib/vendor/php-mime-mail-parser/php-mime-mail-parser/README.md +++ b/data/web/inc/lib/vendor/php-mime-mail-parser/php-mime-mail-parser/README.md @@ -1,6 +1,9 @@ # php-mime-mail-parser -A fully tested mailparse extension wrapper for PHP 5.4+ +A fully tested email parser for PHP 7.1+ (mailparse extension wrapper). + +It's the most effective php email parser around in terms of performance, foreign character encoding, attachment handling, and ease of use. +Internet Message Format RFC [822](https://tools.ietf.org/html/rfc822), [2822](https://tools.ietf.org/html/rfc2822), [5322](https://tools.ietf.org/html/rfc5322). [](https://github.com/php-mime-mail-parser/php-mime-mail-parser/releases) [](https://packagist.org/packages/php-mime-mail-parser/php-mime-mail-parser) @@ -10,6 +13,7 @@ A fully tested mailparse extension wrapper for PHP 5.4+ This extension can be used to... * Parse and read email from Postfix + * For reading messages (Filename extension: eml) * Create webmail * Store email information such a subject, HTML body, attachments, and etc. into a database @@ -19,7 +23,7 @@ Yes. All known issues have been reproduced, fixed and tested. We use Travis CI to help ensure code quality. You can see real-time statistics below: -[](https://travis-ci.org/php-mime-mail-parser/php-mime-mail-parser) +[](https://travis-ci.com/php-mime-mail-parser/php-mime-mail-parser) [](https://coveralls.io/r/php-mime-mail-parser/php-mime-mail-parser) [](https://scrutinizer-ci.com/g/php-mime-mail-parser/php-mime-mail-parser) @@ -35,105 +39,185 @@ To install the latest version of PHP MIME Mail Parser, run the command below: The following versions of PHP are supported: -* PHP 5.4 -* PHP 5.5 -* PHP 5.6 -* PHP 7 -* HHVM +* PHP 7.1 +* PHP 7.2 +* PHP 7.3 +Previous Versions: + +| PHP Compatibility | Version | +| ------------- | ------------- | +| HHVM | php-mime-mail-parser 2.11.1 | +| PHP 5.4 | php-mime-mail-parser 2.11.1 | +| PHP 5.5 | php-mime-mail-parser 2.11.1 | +| PHP 5.6 | php-mime-mail-parser 3.0.4 | +| PHP 7.0 | php-mime-mail-parser 3.0.4 | + +Make sure you have the mailparse extension (http://php.net/manual/en/book.mailparse.php) properly installed. The command line `php -m | grep mailparse` need to return "mailparse". + + +### Install mailparse extension + +#### Ubuntu, Debian & derivatives +``` +sudo apt install php-cli php-mailparse +``` + +#### Others platforms ``` sudo apt install php-cli php-pear php-dev php-mbstring +pecl install mailparse ``` -Make sure you have the mailparse extension (http://php.net/manual/en/book.mailparse.php) properly installed. The command line `php -m | grep mailparse` need to return "mailparse" else install it: -* PHP version > 7.0: mailparse -* PHP version < 7.0: mailparse 2.1.6 +#### From source -Follow this steps to install mailparse: - -* Compile in the temp folder the extension mailparse or mailparse-2.1.6 (workaround because pecl install doesn't work yet) +AAAAMMDD should be `php-config --extension-dir` ``` -cd -pecl download mailparse -tar -xvf mailparse-3.0.2.tgz -cd mailparse-3.0.2/ +git clone https://github.com/php/pecl-mail-mailparse.git +cd pecl-mail-mailparse phpize ./configure sed -i 's/#if\s!HAVE_MBSTRING/#ifndef MBFL_MBFILTER_H/' ./mailparse.c make -sudo mv modules/mailparse.so /usr/lib/php/20160303/ -``` -* Add the extension mailparse and activate it -``` +sudo mv modules/mailparse.so /usr/lib/php/AAAAMMDD/ echo "extension=mailparse.so" | sudo tee /etc/php/7.1/mods-available/mailparse.ini sudo phpenmod mailparse ``` -On Windows, you need to download mailparse DLL from http://pecl.php.net/package/mailparse and add the line "extension=php_mailparse.dll" to php.ini accordingly. +#### Windows +You need to download mailparse DLL from http://pecl.php.net/package/mailparse and add the line "extension=php_mailparse.dll" to php.ini accordingly. ## How do I use it? +### Loading an email + +You can load an email with 4 differents ways. You only need to use one of the following four. + ```php -<?php -// Include the library first require_once __DIR__.'/vendor/autoload.php'; -$path = 'path/to/mail.txt'; -$Parser = new PhpMimeMailParser\Parser(); +$path = 'path/to/email.eml'; +$parser = new PhpMimeMailParser\Parser(); -// There are four methods available to indicate which mime mail to parse. -// You only need to use one of the following four: +// 1. Specify a file path (string) +$parser->setPath($path); -// 1. Specify a file path to the mime mail. -$Parser->setPath($path); +// 2. Specify the raw mime mail text (string) +$parser->setText(file_get_contents($path)); -// 2. Specify a php file resource (stream) to the mime mail. -$Parser->setStream(fopen($path, "r")); +// 3. Specify a php file resource (stream) +$parser->setStream(fopen($path, "r")); -// 3. Specify the raw mime mail text. -$Parser->setText(file_get_contents($path)); - -// 4. Specify a stream to work with mail server -$Parser->setStream(fopen("php://stdin", "r")); - -// Once we've indicated where to find the mail, we can parse out the data -$to = $Parser->getHeader('to'); // "test" <test@example.com>, "test2" <test2@example.com> -$addressesTo = $Parser->getAddresses('to'); //Return an array : [["display"=>"test", "address"=>"test@example.com", false],["display"=>"test2", "address"=>"test2@example.com", false]] - -$from = $Parser->getHeader('from'); // "test" <test@example.com> -$addressesFrom = $Parser->getAddresses('from'); //Return an array : [["display"=>"test", "address"=>"test@example.com", "is_group"=>false]] - -$subject = $Parser->getHeader('subject'); - -$text = $Parser->getMessageBody('text'); - -$html = $Parser->getMessageBody('html'); -$htmlEmbedded = $Parser->getMessageBody('htmlEmbedded'); //HTML Body included data - -$stringHeaders = $Parser->getHeadersRaw(); // Get all headers as a string, no charset conversion -$arrayHeaders = $Parser->getHeaders(); // Get all headers as an array, with charset conversion - -// Pass in a writeable path to save attachments -$attach_dir = '/path/to/save/attachments/'; // Be sure to include the trailing slash -$include_inline = true; // Optional argument to include inline attachments (default: true) -$Parser->saveAttachments($attach_dir [,$include_inline]); - -// Get an array of Attachment items from $Parser -$attachments = $Parser->getAttachments([$include_inline]); - -// Loop through all the Attachments -if (count($attachments) > 0) { - foreach ($attachments as $attachment) { - echo 'Filename : '.$attachment->getFilename().'<br />'; // logo.jpg - echo 'Filesize : '.filesize($attach_dir.$attachment->getFilename()).'<br />'; // 1000 - echo 'Filetype : '.$attachment->getContentType().'<br />'; // image/jpeg - echo 'MIME part string : '.$attachment->getMimePartStr().'<br />'; // (the whole MIME part of the attachment) - } -} - -?> +// 4. Specify a stream to work with mail server (stream) +$parser->setStream(fopen("php://stdin", "r")); ``` +### Get the metadata of the message + +Get the sender and the receiver: + +```php +$rawHeaderTo = $parser->getHeader('to'); +// return "test" <test@example.com>, "test2" <test2@example.com> + +$arrayHeaderTo = $parser->getAddresses('to'); +// return [["display"=>"test", "address"=>"test@example.com", false]] + +$rawHeaderFrom = $parser->getHeader('from'); +// return "test" <test@example.com> + +$arrayHeaderFrom = $parser->getAddresses('from'); +// return [["display"=>"test", "address"=>"test@example.com", "is_group"=>false]] +``` + +Get the subject: + +```php +$subject = $parser->getHeader('subject'); +``` + +Get other headers: + +```php +$stringHeaders = $parser->getHeadersRaw(); +// return all headers as a string, no charset conversion + +$arrayHeaders = $parser->getHeaders(); +// return all headers as an array, with charset conversion +``` + +### Get the body of the message + +```php +$text = $parser->getMessageBody('text'); +// return the text version + +$html = $parser->getMessageBody('html'); +// return the html version + +$htmlEmbedded = $parser->getMessageBody('htmlEmbedded'); +// return the html version with the embedded contents like images + +``` + +### Get attachments + +Save all attachments in a directory + +```php +$parser->saveAttachments('/path/to/save/attachments/'); +// return all attachments saved in the directory (include inline attachments) + +$parser->saveAttachments('/path/to/save/attachments/', false); +// return all attachments saved in the directory (exclude inline attachments) + +// Save all attachments with the strategy ATTACHMENT_DUPLICATE_SUFFIX (default) +$parser->saveAttachments('/path/to/save/attachments/', false, Parser::ATTACHMENT_DUPLICATE_SUFFIX); +// return all attachments saved in the directory: logo.jpg, logo_1.jpg, ..., logo_100.jpg, YY34UFHBJ.jpg + +// Save all attachments with the strategy ATTACHMENT_RANDOM_FILENAME +$parser->saveAttachments('/path/to/save/attachments/', false, Parser::ATTACHMENT_RANDOM_FILENAME); +// return all attachments saved in the directory: YY34UFHBJ.jpg and F98DBZ9FZF.jpg + +// Save all attachments with the strategy ATTACHMENT_DUPLICATE_THROW +$parser->saveAttachments('/path/to/save/attachments/', false, Parser::ATTACHMENT_DUPLICATE_THROW); +// return an exception when there is attachments duplicate. + +``` + +Get all attachments + +```php +$attachments = $parser->getAttachments(); +// return an array of all attachments (include inline attachments) + +$attachments = $parser->getAttachments(false); +// return an array of all attachments (exclude inline attachments) +``` + + +Loop through all the Attachments +```php +foreach ($attachments as $attachment) { + echo 'Filename : '.$attachment->getFilename().'<br />'; + // return logo.jpg + + echo 'Filesize : '.filesize($attach_dir.$attachment->getFilename()).'<br />'; + // return 1000 + + echo 'Filetype : '.$attachment->getContentType().'<br />'; + // return image/jpeg + + echo 'MIME part string : '.$attachment->getMimePartStr().'<br />'; + // return the whole MIME part of the attachment + + $attachment->save('/path/to/save/myattachment/', Parser::ATTACHMENT_DUPLICATE_SUFFIX); + // return the path and the filename saved (same strategy available than saveAttachments) +} +``` + +## Postfix configuration to manage email from a mail server + Next you need to forward emails to this script above. For that I'm using [Postfix](http://www.postfix.org/) like a mail server, you need to configure /etc/postfix/master.cf Add this line at the end of the file (specify myhook to send all emails to the script test.php) @@ -150,6 +234,8 @@ smtp inet n - - - - smtpd The php script must use the fourth method to work with this configuration. +And finally the easiest way is to use my SaaS https://mailcare.io + ## Can I contribute? @@ -162,6 +248,6 @@ Feel free to contribute! If you report an issue, please provide the raw email that triggered it. This helps us reproduce the issue and fix it more quickly. -### License +## License The php-mime-mail-parser/php-mime-mail-parser is open-sourced software licensed under the [MIT license](http://opensource.org/licenses/MIT) diff --git a/data/web/inc/lib/vendor/php-mime-mail-parser/php-mime-mail-parser/composer.json b/data/web/inc/lib/vendor/php-mime-mail-parser/php-mime-mail-parser/composer.json index 320d08d2..a797183e 100644 --- a/data/web/inc/lib/vendor/php-mime-mail-parser/php-mime-mail-parser/composer.json +++ b/data/web/inc/lib/vendor/php-mime-mail-parser/php-mime-mail-parser/composer.json @@ -1,8 +1,8 @@ { "name": "php-mime-mail-parser/php-mime-mail-parser", "type": "library", - "description": "Fully Tested Mailparse Extension Wrapper for PHP 5.4+", - "keywords": ["mime", "mail", "mailparse", "MimeMailParser"], + "description": "A fully tested email parser for PHP 7.1+ (mailparse extension wrapper).", + "keywords": ["mime", "mail", "mailparse", "MimeMailParser", "parser", "php"], "homepage": "https://github.com/php-mime-mail-parser/php-mime-mail-parser", "license": "MIT", "authors": [ @@ -42,14 +42,14 @@ "url":"https://github.com/php-mime-mail-parser/php-mime-mail-parser.git" }, "require": { - "php": "^5.4.0 || ^7.0", + "php": "^7.1", "ext-mailparse": "*" }, "require-dev": { - "phpunit/phpunit": "^4.0 || ^5.0", - "phpunit/php-token-stream": "^1.3.0", - "satooshi/php-coveralls": "0.*", - "squizlabs/PHP_CodeSniffer": "2.*" + "phpunit/phpunit": "^7.0", + "phpunit/php-token-stream": "^3.0", + "php-coveralls/php-coveralls": "^2.1", + "squizlabs/php_codesniffer": "^3.4" }, "replace": { "exorus/php-mime-mail-parser": "*", diff --git a/data/web/inc/lib/vendor/php-mime-mail-parser/php-mime-mail-parser/mailparse-stubs.php b/data/web/inc/lib/vendor/php-mime-mail-parser/php-mime-mail-parser/mailparse-stubs.php index 743033ef..e2b5e0e2 100644 --- a/data/web/inc/lib/vendor/php-mime-mail-parser/php-mime-mail-parser/mailparse-stubs.php +++ b/data/web/inc/lib/vendor/php-mime-mail-parser/php-mime-mail-parser/mailparse-stubs.php @@ -135,7 +135,7 @@ function mailparse_msg_create() * @param resource $mimemail A valid MIME resource allocated by * mailparse_msg_create or mailparse_msg_parse_file * - * @return bool + * @return boolean|null */ function mailparse_msg_free($mimemail) { @@ -149,7 +149,7 @@ function mailparse_msg_free($mimemail) * @param resource $mimemail A valid MIME resource * @param string $data * - * @return bool + * @return boolean|null */ function mailparse_msg_parse($mimemail, $data) { @@ -199,7 +199,7 @@ function mailparse_determine_best_xfer_encoding($fp) * @param string $encoding One of the character encodings supported by the mbstring * module * - * @return bool + * @return boolean|null */ function mailparse_stream_encode($sourcefp, $destfp, $encoding) { diff --git a/data/web/inc/lib/vendor/php-mime-mail-parser/php-mime-mail-parser/src/Attachment.php b/data/web/inc/lib/vendor/php-mime-mail-parser/php-mime-mail-parser/src/Attachment.php index 52012622..091eba6a 100644 --- a/data/web/inc/lib/vendor/php-mime-mail-parser/php-mime-mail-parser/src/Attachment.php +++ b/data/web/inc/lib/vendor/php-mime-mail-parser/php-mime-mail-parser/src/Attachment.php @@ -50,6 +50,11 @@ class Attachment */ protected $mimePartStr; + /** + * @var integer $maxDuplicateNumber + */ + public $maxDuplicateNumber = 100; + /** * Attachment constructor. * @@ -133,13 +138,44 @@ class Attachment /** * Get a handle to the stream * - * @return stream + * @return resource */ public function getStream() { return $this->stream; } + /** + * Rename a file if it already exists at its destination. + * Renaming is done by adding a duplicate number to the file name. E.g. existingFileName_1.ext. + * After a max duplicate number, renaming the file will switch over to generating a random suffix. + * + * @param string $fileName Complete path to the file. + * @return string The suffixed file name. + */ + protected function suffixFileName(string $fileName): string + { + $pathInfo = pathinfo($fileName); + $dirname = $pathInfo['dirname'].DIRECTORY_SEPARATOR; + $filename = $pathInfo['filename']; + $extension = empty($pathInfo['extension']) ? '' : '.'.$pathInfo['extension']; + + $i = 0; + do { + $i++; + + if ($i > $this->maxDuplicateNumber) { + $duplicateExtension = uniqid(); + } else { + $duplicateExtension = $i; + } + + $resultName = $dirname.$filename."_$duplicateExtension".$extension; + } while (file_exists($resultName)); + + return $resultName; + } + /** * Read the contents a few bytes at a time until completed * Once read to completion, it always returns false @@ -180,4 +216,57 @@ class Attachment { return $this->mimePartStr; } + + /** + * Save the attachment individually + * + * @param string $attach_dir + * @param string $filenameStrategy + * + * @return string + */ + public function save( + $attach_dir, + $filenameStrategy = Parser::ATTACHMENT_DUPLICATE_SUFFIX + ) { + $attach_dir = rtrim($attach_dir, DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR; + if (!is_dir($attach_dir)) { + mkdir($attach_dir); + } + + // Determine filename + switch ($filenameStrategy) { + case Parser::ATTACHMENT_RANDOM_FILENAME: + $attachment_path = tempnam($attach_dir, ''); + break; + case Parser::ATTACHMENT_DUPLICATE_THROW: + case Parser::ATTACHMENT_DUPLICATE_SUFFIX: + $attachment_path = $attach_dir.$this->getFilename(); + break; + default: + throw new Exception('Invalid filename strategy argument provided.'); + } + + // Handle duplicate filename + if (file_exists($attachment_path)) { + switch ($filenameStrategy) { + case Parser::ATTACHMENT_DUPLICATE_THROW: + throw new Exception('Could not create file for attachment: duplicate filename.'); + case Parser::ATTACHMENT_DUPLICATE_SUFFIX: + $attachment_path = $this->suffixFileName($attachment_path); + break; + } + } + + /** @var resource $fp */ + if ($fp = fopen($attachment_path, 'w')) { + while ($bytes = $this->read()) { + fwrite($fp, $bytes); + } + fclose($fp); + return realpath($attachment_path); + } else { + throw new Exception('Could not write attachments. Your directory may be unwritable by PHP.'); + } + } } diff --git a/data/web/inc/lib/vendor/php-mime-mail-parser/php-mime-mail-parser/src/Charset.php b/data/web/inc/lib/vendor/php-mime-mail-parser/php-mime-mail-parser/src/Charset.php index 04315c1e..9768edea 100644 --- a/data/web/inc/lib/vendor/php-mime-mail-parser/php-mime-mail-parser/src/Charset.php +++ b/data/web/inc/lib/vendor/php-mime-mail-parser/php-mime-mail-parser/src/Charset.php @@ -12,33 +12,33 @@ class Charset implements CharsetManager 'us-ascii' => 'us-ascii', 'ansi_x3.4-1968' => 'us-ascii', '646' => 'us-ascii', - 'iso-8859-1' => 'ISO-8859-1', - 'iso-8859-2' => 'ISO-8859-2', - 'iso-8859-3' => 'ISO-8859-3', - 'iso-8859-4' => 'ISO-8859-4', - 'iso-8859-5' => 'ISO-8859-5', - 'iso-8859-6' => 'ISO-8859-6', - 'iso-8859-6-i' => 'ISO-8859-6-I', - 'iso-8859-6-e' => 'ISO-8859-6-E', - 'iso-8859-7' => 'ISO-8859-7', - 'iso-8859-8' => 'ISO-8859-8', - 'iso-8859-8-i' => 'ISO-8859-8', - 'iso-8859-8-e' => 'ISO-8859-8-E', - 'iso-8859-9' => 'ISO-8859-9', - 'iso-8859-10' => 'ISO-8859-10', - 'iso-8859-11' => 'ISO-8859-11', - 'iso-8859-13' => 'ISO-8859-13', - 'iso-8859-14' => 'ISO-8859-14', - 'iso-8859-15' => 'ISO-8859-15', - 'iso-8859-16' => 'ISO-8859-16', - 'iso-ir-111' => 'ISO-IR-111', - 'iso-2022-cn' => 'ISO-2022-CN', - 'iso-2022-cn-ext' => 'ISO-2022-CN', - 'iso-2022-kr' => 'ISO-2022-KR', - 'iso-2022-jp' => 'ISO-2022-JP', - 'utf-16be' => 'UTF-16BE', - 'utf-16le' => 'UTF-16LE', - 'utf-16' => 'UTF-16', + 'iso-8859-1' => 'iso-8859-1', + 'iso-8859-2' => 'iso-8859-2', + 'iso-8859-3' => 'iso-8859-3', + 'iso-8859-4' => 'iso-8859-4', + 'iso-8859-5' => 'iso-8859-5', + 'iso-8859-6' => 'iso-8859-6', + 'iso-8859-6-i' => 'iso-8859-6-i', + 'iso-8859-6-e' => 'iso-8859-6-e', + 'iso-8859-7' => 'iso-8859-7', + 'iso-8859-8' => 'iso-8859-8', + 'iso-8859-8-i' => 'iso-8859-8', + 'iso-8859-8-e' => 'iso-8859-8-e', + 'iso-8859-9' => 'iso-8859-9', + 'iso-8859-10' => 'iso-8859-10', + 'iso-8859-11' => 'iso-8859-11', + 'iso-8859-13' => 'iso-8859-13', + 'iso-8859-14' => 'iso-8859-14', + 'iso-8859-15' => 'iso-8859-15', + 'iso-8859-16' => 'iso-8859-16', + 'iso-ir-111' => 'iso-ir-111', + 'iso-2022-cn' => 'iso-2022-cn', + 'iso-2022-cn-ext' => 'iso-2022-cn', + 'iso-2022-kr' => 'iso-2022-kr', + 'iso-2022-jp' => 'iso-2022-jp', + 'utf-16be' => 'utf-16be', + 'utf-16le' => 'utf-16le', + 'utf-16' => 'utf-16', 'windows-1250' => 'windows-1250', 'windows-1251' => 'windows-1251', 'windows-1252' => 'windows-1252', @@ -48,172 +48,172 @@ class Charset implements CharsetManager 'windows-1256' => 'windows-1256', 'windows-1257' => 'windows-1257', 'windows-1258' => 'windows-1258', - 'ibm866' => 'IBM866', - 'ibm850' => 'IBM850', - 'ibm852' => 'IBM852', - 'ibm855' => 'IBM855', - 'ibm857' => 'IBM857', - 'ibm862' => 'IBM862', - 'ibm864' => 'IBM864', - 'utf-8' => 'UTF-8', - 'utf-7' => 'UTF-7', - 'shift_jis' => 'Shift_JIS', - 'big5' => 'Big5', - 'euc-jp' => 'EUC-JP', - 'euc-kr' => 'EUC-KR', - 'gb2312' => 'GB2312', + 'ibm866' => 'ibm866', + 'ibm850' => 'ibm850', + 'ibm852' => 'ibm852', + 'ibm855' => 'ibm855', + 'ibm857' => 'ibm857', + 'ibm862' => 'ibm862', + 'ibm864' => 'ibm864', + 'utf-8' => 'utf-8', + 'utf-7' => 'utf-7', + 'shift_jis' => 'shift_jis', + 'big5' => 'big5', + 'euc-jp' => 'euc-jp', + 'euc-kr' => 'euc-kr', + 'gb2312' => 'gb2312', 'gb18030' => 'gb18030', - 'viscii' => 'VISCII', - 'koi8-r' => 'KOI8-R', - 'koi8_r' => 'KOI8-R', - 'cskoi8r' => 'KOI8-R', - 'koi' => 'KOI8-R', - 'koi8' => 'KOI8-R', - 'koi8-u' => 'KOI8-U', - 'tis-620' => 'TIS-620', - 't.61-8bit' => 'T.61-8bit', - 'hz-gb-2312' => 'HZ-GB-2312', - 'big5-hkscs' => 'Big5-HKSCS', + 'viscii' => 'viscii', + 'koi8-r' => 'koi8-r', + 'koi8_r' => 'koi8-r', + 'cskoi8r' => 'koi8-r', + 'koi' => 'koi8-r', + 'koi8' => 'koi8-r', + 'koi8-u' => 'koi8-u', + 'tis-620' => 'tis-620', + 't.61-8bit' => 't.61-8bit', + 'hz-gb-2312' => 'hz-gb-2312', + 'big5-hkscs' => 'big5-hkscs', 'gbk' => 'gbk', 'cns11643' => 'x-euc-tw', 'x-imap4-modified-utf7' => 'x-imap4-modified-utf7', 'x-euc-tw' => 'x-euc-tw', - 'x-mac-ce' => 'x-mac-ce', - 'x-mac-turkish' => 'x-mac-turkish', - 'x-mac-greek' => 'x-mac-greek', - 'x-mac-icelandic' => 'x-mac-icelandic', - 'x-mac-croatian' => 'x-mac-croatian', - 'x-mac-romanian' => 'x-mac-romanian', - 'x-mac-cyrillic' => 'x-mac-cyrillic', - 'x-mac-ukrainian' => 'x-mac-cyrillic', - 'x-mac-hebrew' => 'x-mac-hebrew', - 'x-mac-arabic' => 'x-mac-arabic', - 'x-mac-farsi' => 'x-mac-farsi', - 'x-mac-devanagari' => 'x-mac-devanagari', - 'x-mac-gujarati' => 'x-mac-gujarati', - 'x-mac-gurmukhi' => 'x-mac-gurmukhi', + 'x-mac-ce' => 'macce', + 'x-mac-turkish' => 'macturkish', + 'x-mac-greek' => 'macgreek', + 'x-mac-icelandic' => 'macicelandic', + 'x-mac-croatian' => 'maccroatian', + 'x-mac-romanian' => 'macromanian', + 'x-mac-cyrillic' => 'maccyrillic', + 'x-mac-ukrainian' => 'macukrainian', + 'x-mac-hebrew' => 'machebrew', + 'x-mac-arabic' => 'macarabic', + 'x-mac-farsi' => 'macfarsi', + 'x-mac-devanagari' => 'macdevanagari', + 'x-mac-gujarati' => 'macgujarati', + 'x-mac-gurmukhi' => 'macgurmukhi', 'armscii-8' => 'armscii-8', 'x-viet-tcvn5712' => 'x-viet-tcvn5712', 'x-viet-vps' => 'x-viet-vps', - 'iso-10646-ucs-2' => 'UTF-16BE', - 'x-iso-10646-ucs-2-be' => 'UTF-16BE', - 'x-iso-10646-ucs-2-le' => 'UTF-16LE', + 'iso-10646-ucs-2' => 'utf-16be', + 'x-iso-10646-ucs-2-be' => 'utf-16be', + 'x-iso-10646-ucs-2-le' => 'utf-16le', 'x-user-defined' => 'x-user-defined', 'x-johab' => 'x-johab', - 'latin1' => 'ISO-8859-1', - 'iso_8859-1' => 'ISO-8859-1', - 'iso8859-1' => 'ISO-8859-1', - 'iso8859-2' => 'ISO-8859-2', - 'iso8859-3' => 'ISO-8859-3', - 'iso8859-4' => 'ISO-8859-4', - 'iso8859-5' => 'ISO-8859-5', - 'iso8859-6' => 'ISO-8859-6', - 'iso8859-7' => 'ISO-8859-7', - 'iso8859-8' => 'ISO-8859-8', - 'iso8859-9' => 'ISO-8859-9', - 'iso8859-10' => 'ISO-8859-10', - 'iso8859-11' => 'ISO-8859-11', - 'iso8859-13' => 'ISO-8859-13', - 'iso8859-14' => 'ISO-8859-14', - 'iso8859-15' => 'ISO-8859-15', - 'iso_8859-1:1987' => 'ISO-8859-1', - 'iso-ir-100' => 'ISO-8859-1', - 'l1' => 'ISO-8859-1', - 'ibm819' => 'ISO-8859-1', - 'cp819' => 'ISO-8859-1', - 'csisolatin1' => 'ISO-8859-1', - 'latin2' => 'ISO-8859-2', - 'iso_8859-2' => 'ISO-8859-2', - 'iso_8859-2:1987' => 'ISO-8859-2', - 'iso-ir-101' => 'ISO-8859-2', - 'l2' => 'ISO-8859-2', - 'csisolatin2' => 'ISO-8859-2', - 'latin3' => 'ISO-8859-3', - 'iso_8859-3' => 'ISO-8859-3', - 'iso_8859-3:1988' => 'ISO-8859-3', - 'iso-ir-109' => 'ISO-8859-3', - 'l3' => 'ISO-8859-3', - 'csisolatin3' => 'ISO-8859-3', - 'latin4' => 'ISO-8859-4', - 'iso_8859-4' => 'ISO-8859-4', - 'iso_8859-4:1988' => 'ISO-8859-4', - 'iso-ir-110' => 'ISO-8859-4', - 'l4' => 'ISO-8859-4', - 'csisolatin4' => 'ISO-8859-4', - 'cyrillic' => 'ISO-8859-5', - 'iso_8859-5' => 'ISO-8859-5', - 'iso_8859-5:1988' => 'ISO-8859-5', - 'iso-ir-144' => 'ISO-8859-5', - 'csisolatincyrillic' => 'ISO-8859-5', - 'arabic' => 'ISO-8859-6', - 'iso_8859-6' => 'ISO-8859-6', - 'iso_8859-6:1987' => 'ISO-8859-6', - 'iso-ir-127' => 'ISO-8859-6', - 'ecma-114' => 'ISO-8859-6', - 'asmo-708' => 'ISO-8859-6', - 'csisolatinarabic' => 'ISO-8859-6', - 'csiso88596i' => 'ISO-8859-6-I', - 'csiso88596e' => 'ISO-8859-6-E', - 'greek' => 'ISO-8859-7', - 'greek8' => 'ISO-8859-7', - 'sun_eu_greek' => 'ISO-8859-7', - 'iso_8859-7' => 'ISO-8859-7', - 'iso_8859-7:1987' => 'ISO-8859-7', - 'iso-ir-126' => 'ISO-8859-7', - 'elot_928' => 'ISO-8859-7', - 'ecma-118' => 'ISO-8859-7', - 'csisolatingreek' => 'ISO-8859-7', - 'hebrew' => 'ISO-8859-8', - 'iso_8859-8' => 'ISO-8859-8', - 'visual' => 'ISO-8859-8', - 'iso_8859-8:1988' => 'ISO-8859-8', - 'iso-ir-138' => 'ISO-8859-8', - 'csisolatinhebrew' => 'ISO-8859-8', - 'csiso88598i' => 'ISO-8859-8', - 'iso-8859-8i' => 'ISO-8859-8', - 'logical' => 'ISO-8859-8', - 'csiso88598e' => 'ISO-8859-8-E', - 'latin5' => 'ISO-8859-9', - 'iso_8859-9' => 'ISO-8859-9', - 'iso_8859-9:1989' => 'ISO-8859-9', - 'iso-ir-148' => 'ISO-8859-9', - 'l5' => 'ISO-8859-9', - 'csisolatin5' => 'ISO-8859-9', - 'unicode-1-1-utf-8' => 'UTF-8', - 'utf8' => 'UTF-8', - 'x-sjis' => 'Shift_JIS', - 'shift-jis' => 'Shift_JIS', - 'ms_kanji' => 'Shift_JIS', - 'csshiftjis' => 'Shift_JIS', - 'windows-31j' => 'Shift_JIS', - 'cp932' => 'Shift_JIS', - 'sjis' => 'Shift_JIS', - 'cseucpkdfmtjapanese' => 'EUC-JP', - 'x-euc-jp' => 'EUC-JP', - 'csiso2022jp' => 'ISO-2022-JP', - 'iso-2022-jp-2' => 'ISO-2022-JP', - 'csiso2022jp2' => 'ISO-2022-JP', - 'csbig5' => 'Big5', - 'cn-big5' => 'Big5', - 'x-x-big5' => 'Big5', - 'zh_tw-big5' => 'Big5', - 'cseuckr' => 'EUC-KR', - 'ks_c_5601-1987' => 'EUC-KR', - 'iso-ir-149' => 'EUC-KR', - 'ks_c_5601-1989' => 'EUC-KR', - 'ksc_5601' => 'EUC-KR', - 'ksc5601' => 'EUC-KR', - 'korean' => 'EUC-KR', - 'csksc56011987' => 'EUC-KR', - '5601' => 'EUC-KR', - 'windows-949' => 'EUC-KR', - 'gb_2312-80' => 'GB2312', - 'iso-ir-58' => 'GB2312', - 'chinese' => 'GB2312', - 'csiso58gb231280' => 'GB2312', - 'csgb2312' => 'GB2312', - 'zh_cn.euc' => 'GB2312', - 'gb_2312' => 'GB2312', + 'latin1' => 'iso-8859-1', + 'iso_8859-1' => 'iso-8859-1', + 'iso8859-1' => 'iso-8859-1', + 'iso8859-2' => 'iso-8859-2', + 'iso8859-3' => 'iso-8859-3', + 'iso8859-4' => 'iso-8859-4', + 'iso8859-5' => 'iso-8859-5', + 'iso8859-6' => 'iso-8859-6', + 'iso8859-7' => 'iso-8859-7', + 'iso8859-8' => 'iso-8859-8', + 'iso8859-9' => 'iso-8859-9', + 'iso8859-10' => 'iso-8859-10', + 'iso8859-11' => 'iso-8859-11', + 'iso8859-13' => 'iso-8859-13', + 'iso8859-14' => 'iso-8859-14', + 'iso8859-15' => 'iso-8859-15', + 'iso_8859-1:1987' => 'iso-8859-1', + 'iso-ir-100' => 'iso-8859-1', + 'l1' => 'iso-8859-1', + 'ibm819' => 'iso-8859-1', + 'cp819' => 'iso-8859-1', + 'csisolatin1' => 'iso-8859-1', + 'latin2' => 'iso-8859-2', + 'iso_8859-2' => 'iso-8859-2', + 'iso_8859-2:1987' => 'iso-8859-2', + 'iso-ir-101' => 'iso-8859-2', + 'l2' => 'iso-8859-2', + 'csisolatin2' => 'iso-8859-2', + 'latin3' => 'iso-8859-3', + 'iso_8859-3' => 'iso-8859-3', + 'iso_8859-3:1988' => 'iso-8859-3', + 'iso-ir-109' => 'iso-8859-3', + 'l3' => 'iso-8859-3', + 'csisolatin3' => 'iso-8859-3', + 'latin4' => 'iso-8859-4', + 'iso_8859-4' => 'iso-8859-4', + 'iso_8859-4:1988' => 'iso-8859-4', + 'iso-ir-110' => 'iso-8859-4', + 'l4' => 'iso-8859-4', + 'csisolatin4' => 'iso-8859-4', + 'cyrillic' => 'iso-8859-5', + 'iso_8859-5' => 'iso-8859-5', + 'iso_8859-5:1988' => 'iso-8859-5', + 'iso-ir-144' => 'iso-8859-5', + 'csisolatincyrillic' => 'iso-8859-5', + 'arabic' => 'iso-8859-6', + 'iso_8859-6' => 'iso-8859-6', + 'iso_8859-6:1987' => 'iso-8859-6', + 'iso-ir-127' => 'iso-8859-6', + 'ecma-114' => 'iso-8859-6', + 'asmo-708' => 'iso-8859-6', + 'csisolatinarabic' => 'iso-8859-6', + 'csiso88596i' => 'iso-8859-6-i', + 'csiso88596e' => 'iso-8859-6-e', + 'greek' => 'iso-8859-7', + 'greek8' => 'iso-8859-7', + 'sun_eu_greek' => 'iso-8859-7', + 'iso_8859-7' => 'iso-8859-7', + 'iso_8859-7:1987' => 'iso-8859-7', + 'iso-ir-126' => 'iso-8859-7', + 'elot_928' => 'iso-8859-7', + 'ecma-118' => 'iso-8859-7', + 'csisolatingreek' => 'iso-8859-7', + 'hebrew' => 'iso-8859-8', + 'iso_8859-8' => 'iso-8859-8', + 'visual' => 'iso-8859-8', + 'iso_8859-8:1988' => 'iso-8859-8', + 'iso-ir-138' => 'iso-8859-8', + 'csisolatinhebrew' => 'iso-8859-8', + 'csiso88598i' => 'iso-8859-8', + 'iso-8859-8i' => 'iso-8859-8', + 'logical' => 'iso-8859-8', + 'csiso88598e' => 'iso-8859-8-e', + 'latin5' => 'iso-8859-9', + 'iso_8859-9' => 'iso-8859-9', + 'iso_8859-9:1989' => 'iso-8859-9', + 'iso-ir-148' => 'iso-8859-9', + 'l5' => 'iso-8859-9', + 'csisolatin5' => 'iso-8859-9', + 'unicode-1-1-utf-8' => 'utf-8', + 'utf8' => 'utf-8', + 'x-sjis' => 'shift_jis', + 'shift-jis' => 'shift_jis', + 'ms_kanji' => 'shift_jis', + 'csshiftjis' => 'shift_jis', + 'windows-31j' => 'shift_jis', + 'cp932' => 'shift_jis', + 'sjis' => 'shift_jis', + 'cseucpkdfmtjapanese' => 'euc-jp', + 'x-euc-jp' => 'euc-jp', + 'csiso2022jp' => 'iso-2022-jp', + 'iso-2022-jp-2' => 'iso-2022-jp', + 'csiso2022jp2' => 'iso-2022-jp', + 'csbig5' => 'big5', + 'cn-big5' => 'big5', + 'x-x-big5' => 'big5', + 'zh_tw-big5' => 'big5', + 'cseuckr' => 'euc-kr', + 'ks_c_5601-1987' => 'euc-kr', + 'iso-ir-149' => 'euc-kr', + 'ks_c_5601-1989' => 'euc-kr', + 'ksc_5601' => 'euc-kr', + 'ksc5601' => 'euc-kr', + 'korean' => 'euc-kr', + 'csksc56011987' => 'euc-kr', + '5601' => 'euc-kr', + 'windows-949' => 'euc-kr', + 'gb_2312-80' => 'gb2312', + 'iso-ir-58' => 'gb2312', + 'chinese' => 'gb2312', + 'csiso58gb231280' => 'gb2312', + 'csgb2312' => 'gb2312', + 'zh_cn.euc' => 'gb2312', + 'gb_2312' => 'gb2312', 'x-cp1250' => 'windows-1250', 'x-cp1251' => 'windows-1251', 'x-cp1252' => 'windows-1252', @@ -230,72 +230,72 @@ class Charset implements CharsetManager 'x-mac-roman' => 'macintosh', 'mac' => 'macintosh', 'csmacintosh' => 'macintosh', - 'cp866' => 'IBM866', - 'cp-866' => 'IBM866', - '866' => 'IBM866', - 'csibm866' => 'IBM866', - 'cp850' => 'IBM850', - '850' => 'IBM850', - 'csibm850' => 'IBM850', - 'cp852' => 'IBM852', - '852' => 'IBM852', - 'csibm852' => 'IBM852', - 'cp855' => 'IBM855', - '855' => 'IBM855', - 'csibm855' => 'IBM855', - 'cp857' => 'IBM857', - '857' => 'IBM857', - 'csibm857' => 'IBM857', - 'cp862' => 'IBM862', - '862' => 'IBM862', - 'csibm862' => 'IBM862', - 'cp864' => 'IBM864', - '864' => 'IBM864', - 'csibm864' => 'IBM864', - 'ibm-864' => 'IBM864', - 't.61' => 'T.61-8bit', - 'iso-ir-103' => 'T.61-8bit', - 'csiso103t618bit' => 'T.61-8bit', - 'x-unicode-2-0-utf-7' => 'UTF-7', - 'unicode-2-0-utf-7' => 'UTF-7', - 'unicode-1-1-utf-7' => 'UTF-7', - 'csunicode11utf7' => 'UTF-7', - 'csunicode' => 'UTF-16BE', - 'csunicode11' => 'UTF-16BE', - 'iso-10646-ucs-basic' => 'UTF-16BE', - 'csunicodeascii' => 'UTF-16BE', - 'iso-10646-unicode-latin1' => 'UTF-16BE', - 'csunicodelatin1' => 'UTF-16BE', - 'iso-10646' => 'UTF-16BE', - 'iso-10646-j-1' => 'UTF-16BE', - 'latin6' => 'ISO-8859-10', - 'iso-ir-157' => 'ISO-8859-10', - 'l6' => 'ISO-8859-10', - 'csisolatin6' => 'ISO-8859-10', - 'iso_8859-15' => 'ISO-8859-15', - 'csisolatin9' => 'ISO-8859-15', - 'l9' => 'ISO-8859-15', - 'ecma-cyrillic' => 'ISO-IR-111', - 'csiso111ecmacyrillic' => 'ISO-IR-111', - 'csiso2022kr' => 'ISO-2022-KR', - 'csviscii' => 'VISCII', + 'cp866' => 'ibm866', + 'cp-866' => 'ibm866', + '866' => 'ibm866', + 'csibm866' => 'ibm866', + 'cp850' => 'ibm850', + '850' => 'ibm850', + 'csibm850' => 'ibm850', + 'cp852' => 'ibm852', + '852' => 'ibm852', + 'csibm852' => 'ibm852', + 'cp855' => 'ibm855', + '855' => 'ibm855', + 'csibm855' => 'ibm855', + 'cp857' => 'ibm857', + '857' => 'ibm857', + 'csibm857' => 'ibm857', + 'cp862' => 'ibm862', + '862' => 'ibm862', + 'csibm862' => 'ibm862', + 'cp864' => 'ibm864', + '864' => 'ibm864', + 'csibm864' => 'ibm864', + 'ibm-864' => 'ibm864', + 't.61' => 't.61-8bit', + 'iso-ir-103' => 't.61-8bit', + 'csiso103t618bit' => 't.61-8bit', + 'x-unicode-2-0-utf-7' => 'utf-7', + 'unicode-2-0-utf-7' => 'utf-7', + 'unicode-1-1-utf-7' => 'utf-7', + 'csunicode11utf7' => 'utf-7', + 'csunicode' => 'utf-16be', + 'csunicode11' => 'utf-16be', + 'iso-10646-ucs-basic' => 'utf-16be', + 'csunicodeascii' => 'utf-16be', + 'iso-10646-unicode-latin1' => 'utf-16be', + 'csunicodelatin1' => 'utf-16be', + 'iso-10646' => 'utf-16be', + 'iso-10646-j-1' => 'utf-16be', + 'latin6' => 'iso-8859-10', + 'iso-ir-157' => 'iso-8859-10', + 'l6' => 'iso-8859-10', + 'csisolatin6' => 'iso-8859-10', + 'iso_8859-15' => 'iso-8859-15', + 'csisolatin9' => 'iso-8859-15', + 'l9' => 'iso-8859-15', + 'ecma-cyrillic' => 'iso-ir-111', + 'csiso111ecmacyrillic' => 'iso-ir-111', + 'csiso2022kr' => 'iso-2022-kr', + 'csviscii' => 'viscii', 'zh_tw-euc' => 'x-euc-tw', - 'iso88591' => 'ISO-8859-1', - 'iso88592' => 'ISO-8859-2', - 'iso88593' => 'ISO-8859-3', - 'iso88594' => 'ISO-8859-4', - 'iso88595' => 'ISO-8859-5', - 'iso88596' => 'ISO-8859-6', - 'iso88597' => 'ISO-8859-7', - 'iso88598' => 'ISO-8859-8', - 'iso88599' => 'ISO-8859-9', - 'iso885910' => 'ISO-8859-10', - 'iso885911' => 'ISO-8859-11', - 'iso885912' => 'ISO-8859-12', - 'iso885913' => 'ISO-8859-13', - 'iso885914' => 'ISO-8859-14', - 'iso885915' => 'ISO-8859-15', - 'tis620' => 'TIS-620', + 'iso88591' => 'iso-8859-1', + 'iso88592' => 'iso-8859-2', + 'iso88593' => 'iso-8859-3', + 'iso88594' => 'iso-8859-4', + 'iso88595' => 'iso-8859-5', + 'iso88596' => 'iso-8859-6', + 'iso88597' => 'iso-8859-7', + 'iso88598' => 'iso-8859-8', + 'iso88599' => 'iso-8859-9', + 'iso885910' => 'iso-8859-10', + 'iso885911' => 'iso-8859-11', + 'iso885912' => 'iso-8859-12', + 'iso885913' => 'iso-8859-13', + 'iso885914' => 'iso-8859-14', + 'iso885915' => 'iso-8859-15', + 'tis620' => 'tis-620', 'cp1250' => 'windows-1250', 'cp1251' => 'windows-1251', 'cp1252' => 'windows-1252', @@ -315,11 +315,23 @@ class Charset implements CharsetManager */ public function decodeCharset($encodedString, $charset) { - if (strtolower($charset) == 'utf-8' || strtolower($charset) == 'us-ascii') { + $charset = $this->getCharsetAlias($charset); + + if ($charset == 'utf-8' || $charset == 'us-ascii') { return $encodedString; - } else { - return iconv($this->getCharsetAlias($charset), 'UTF-8//TRANSLIT//IGNORE', $encodedString); } + + if (function_exists('mb_convert_encoding')) { + if ($charset == 'iso-2022-jp') { + return mb_convert_encoding($encodedString, 'utf-8', 'iso-2022-jp-ms'); + } + + if (array_search($charset, array_map('strtolower', mb_list_encodings()))) { + return mb_convert_encoding($encodedString, 'utf-8', $charset); + } + } + + return iconv($charset, 'utf-8//translit//ignore', $encodedString); } /** @@ -331,8 +343,8 @@ class Charset implements CharsetManager if (array_key_exists($charset, $this->charsetAlias)) { return $this->charsetAlias[$charset]; - } else { - return null; } + + return 'us-ascii'; } } diff --git a/data/web/inc/lib/vendor/php-mime-mail-parser/php-mime-mail-parser/src/Middleware.php b/data/web/inc/lib/vendor/php-mime-mail-parser/php-mime-mail-parser/src/Middleware.php index c32fc065..19eb90a3 100644 --- a/data/web/inc/lib/vendor/php-mime-mail-parser/php-mime-mail-parser/src/Middleware.php +++ b/data/web/inc/lib/vendor/php-mime-mail-parser/php-mime-mail-parser/src/Middleware.php @@ -7,6 +7,8 @@ namespace PhpMimeMailParser; */ class Middleware implements Contracts\Middleware { + protected $parser; + /** * Create a middleware using a callable $fn * diff --git a/data/web/inc/lib/vendor/php-mime-mail-parser/php-mime-mail-parser/src/MiddlewareStack.php b/data/web/inc/lib/vendor/php-mime-mail-parser/php-mime-mail-parser/src/MiddlewareStack.php index 967783a4..3ef6da93 100644 --- a/data/web/inc/lib/vendor/php-mime-mail-parser/php-mime-mail-parser/src/MiddlewareStack.php +++ b/data/web/inc/lib/vendor/php-mime-mail-parser/php-mime-mail-parser/src/MiddlewareStack.php @@ -2,7 +2,7 @@ namespace PhpMimeMailParser; -use PhpMimeMailParser\Contracts\MiddleWare; +use PhpMimeMailParser\Contracts\MiddleWare as MiddleWareContracts; /** * A stack of middleware chained together by (MiddlewareStack $next) @@ -29,7 +29,7 @@ class MiddlewareStack * * @param Middleware $middleware */ - public function __construct(Middleware $middleware = null) + public function __construct(MiddleWareContracts $middleware = null) { $this->middleware = $middleware; } @@ -40,7 +40,7 @@ class MiddlewareStack * @param Middleware $middleware * @return MiddlewareStack Immutable MiddlewareStack */ - public function add(Middleware $middleware) + public function add(MiddleWareContracts $middleware) { $stack = new static($middleware); $stack->next = $this; diff --git a/data/web/inc/lib/vendor/php-mime-mail-parser/php-mime-mail-parser/src/Parser.php b/data/web/inc/lib/vendor/php-mime-mail-parser/php-mime-mail-parser/src/Parser.php index 3a641d48..8311663a 100644 --- a/data/web/inc/lib/vendor/php-mime-mail-parser/php-mime-mail-parser/src/Parser.php +++ b/data/web/inc/lib/vendor/php-mime-mail-parser/php-mime-mail-parser/src/Parser.php @@ -110,6 +110,15 @@ class Parser */ public function setPath($path) { + if (is_writable($path)) { + $file = fopen($path, 'a+'); + fseek($file, -1, SEEK_END); + if (fread($file, 1) != "\n") { + fwrite($file, PHP_EOL); + } + fclose($file); + } + // should parse message incrementally from file $this->resource = mailparse_msg_parse_file($path); $this->stream = fopen($path, 'r'); @@ -142,6 +151,11 @@ class Parser while (!feof($stream)) { fwrite($tmp_fp, fread($stream, 2028)); } + + if (fread($tmp_fp, 1) != "\n") { + fwrite($tmp_fp, PHP_EOL); + } + fseek($tmp_fp, 0); $this->stream = &$tmp_fp; } else { @@ -170,9 +184,14 @@ class Parser */ public function setText($data) { - if (!$data) { + if (empty($data)) { throw new Exception('You must not call MimeMailParser::setText with an empty string parameter'); } + + if (substr($data, -1) != "\n") { + $data = $data.PHP_EOL; + } + $this->resource = mailparse_msg_create(); // does not parse incrementally, fast memory hog might explode mailparse_msg_parse($this->resource, $data); @@ -205,7 +224,7 @@ class Parser * * @param string $name Header name (case-insensitive) * - * @return string + * @return string|bool * @throws Exception */ public function getRawHeader($name) @@ -214,7 +233,7 @@ class Parser if (isset($this->parts[1])) { $headers = $this->getPart('headers', $this->parts[1]); - return (isset($headers[$name])) ? $headers[$name] : false; + return isset($headers[$name]) ? $headers[$name] : false; } else { throw new Exception( 'setPath() or setText() or setStream() must be called before retrieving email headers.' @@ -227,7 +246,7 @@ class Parser * * @param string $name Header name (case-insensitive) * - * @return string + * @return string|bool */ public function getHeader($name) { @@ -559,50 +578,10 @@ class Parser $filenameStrategy = self::ATTACHMENT_DUPLICATE_SUFFIX ) { $attachments = $this->getAttachments($include_inline); - if (empty($attachments)) { - return false; - } - - if (!is_dir($attach_dir)) { - mkdir($attach_dir); - } $attachments_paths = []; foreach ($attachments as $attachment) { - // Determine filename - switch ($filenameStrategy) { - case self::ATTACHMENT_RANDOM_FILENAME: - $attachment_path = tempnam($attach_dir, ''); - break; - case self::ATTACHMENT_DUPLICATE_THROW: - case self::ATTACHMENT_DUPLICATE_SUFFIX: - $attachment_path = $attach_dir.$attachment->getFilename(); - break; - default: - throw new Exception('Invalid filename strategy argument provided.'); - } - - // Handle duplicate filename - if (file_exists($attachment_path)) { - switch ($filenameStrategy) { - case self::ATTACHMENT_DUPLICATE_THROW: - throw new Exception('Could not create file for attachment: duplicate filename.'); - case self::ATTACHMENT_DUPLICATE_SUFFIX: - $attachment_path = tempnam($attach_dir, $attachment->getFilename()); - break; - } - } - - /** @var resource $fp */ - if ($fp = fopen($attachment_path, 'w')) { - while ($bytes = $attachment->read()) { - fwrite($fp, $bytes); - } - fclose($fp); - $attachments_paths[] = realpath($attachment_path); - } else { - throw new Exception('Could not write attachments. Your directory may be unwritable by PHP.'); - } + $attachments_paths[] = $attachment->save($attach_dir, $filenameStrategy); } return $attachments_paths; diff --git a/data/web/inc/prerequisites.inc.php b/data/web/inc/prerequisites.inc.php index 66db8662..556c895b 100644 --- a/data/web/inc/prerequisites.inc.php +++ b/data/web/inc/prerequisites.inc.php @@ -36,11 +36,20 @@ foreach ($css_dir as $css_file) { // U2F API + T/HOTP API $u2f = new u2flib_server\U2F('https://' . $_SERVER['HTTP_HOST']); -$tfa = new RobThree\Auth\TwoFactorAuth($OTP_LABEL); +$qrprovider = new RobThree\Auth\Providers\Qr\QRServerProvider(); +$tfa = new RobThree\Auth\TwoFactorAuth($OTP_LABEL, 6, 30, 'sha1', $qrprovider); // Redis $redis = new Redis(); -$redis->connect('redis-mailcow', 6379); +try { + $redis->connect('redis-mailcow', 6379); +} +catch (Exception $e) { +?> +<center style='font-family:sans-serif;'>Connection to Redis failed.<br /><br />The following error was reported:<br/><?=$e->getMessage();?></center> +<?php +exit; +} // PDO // Calculate offset @@ -182,4 +191,3 @@ if (isset($_SESSION['mailcow_cc_role'])) { acl('to_session'); } $UI_TEXTS = customize('get', 'ui_texts'); - diff --git a/data/web/inc/sessions.inc.php b/data/web/inc/sessions.inc.php index 8c47abe5..a94d438c 100644 --- a/data/web/inc/sessions.inc.php +++ b/data/web/inc/sessions.inc.php @@ -21,7 +21,7 @@ elseif (isset($_SERVER['HTTPS'])) { else { $IS_HTTPS = false; } -// session_set_cookie_params($SESSION_LIFETIME, '/', '', $IS_HTTPS, true); + if (session_status() !== PHP_SESSION_ACTIVE) { session_start(); } @@ -35,6 +35,13 @@ if (!isset($_SESSION['SESS_REMOTE_UA'])) { $_SESSION['SESS_REMOTE_UA'] = $_SERVER['HTTP_USER_AGENT']; } +// Keep session active +if (isset($_SESSION['LAST_ACTIVITY']) && (time() - $_SESSION['LAST_ACTIVITY'] > $SESSION_LIFETIME)) { + session_unset(); + session_destroy(); +} +$_SESSION['LAST_ACTIVITY'] = time(); + // API if (!empty($_SERVER['HTTP_X_API_KEY'])) { $stmt = $pdo->prepare("SELECT `allow_from` FROM `api` WHERE `api_key` = :api_key AND `active` = '1';"); @@ -72,8 +79,24 @@ if (!empty($_SERVER['HTTP_X_API_KEY'])) { die(); } } -// Update session cookie -// setcookie(session_name() ,session_id(), time() + $SESSION_LIFETIME); + +// Handle logouts +if (isset($_POST["logout"])) { + if (isset($_SESSION["dual-login"])) { + $_SESSION["mailcow_cc_username"] = $_SESSION["dual-login"]["username"]; + $_SESSION["mailcow_cc_role"] = $_SESSION["dual-login"]["role"]; + unset($_SESSION["dual-login"]); + header("Location: /mailbox"); + exit(); + } + else { + session_regenerate_id(true); + session_unset(); + session_destroy(); + session_write_close(); + header("Location: /"); + } +} // Check session function session_check() { @@ -106,21 +129,3 @@ if (isset($_SESSION['mailcow_cc_role']) && session_check() === false) { $_POST = array(); $_FILES = array(); } - -// Handle logouts -if (isset($_POST["logout"])) { - if (isset($_SESSION["dual-login"])) { - $_SESSION["mailcow_cc_username"] = $_SESSION["dual-login"]["username"]; - $_SESSION["mailcow_cc_role"] = $_SESSION["dual-login"]["role"]; - unset($_SESSION["dual-login"]); - header("Location: /mailbox"); - exit(); - } - else { - session_regenerate_id(true); - session_unset(); - session_destroy(); - session_write_close(); - header("Location: /"); - } -} diff --git a/data/web/inc/spf.inc.php b/data/web/inc/spf.inc.php index 18dd2893..ddb9d48c 100644 --- a/data/web/inc/spf.inc.php +++ b/data/web/inc/spf.inc.php @@ -1,11 +1,11 @@ <?php error_reporting(0); -function get_spf_allowed_hosts($domain) +function get_spf_allowed_hosts($check_domain) { $hosts = array(); - $records = dns_get_record($domain, DNS_TXT); + $records = dns_get_record($check_domain, DNS_TXT); foreach ($records as $record) { $txt = explode(' ', $record['entries'][0]); @@ -47,7 +47,7 @@ function get_spf_allowed_hosts($domain) } $new_hosts = array(); - if ($mech == 'include') // handle an inclusion + if ($mech == 'include' && $check_domain != $domain) // handle an inclusion { $new_hosts = get_spf_allowed_hosts($domain); } @@ -134,4 +134,4 @@ function get_outgoing_hosts_best_guess($domain) // fall back to the A record to get the host name for this domain return get_a_hosts($domain); } -?> \ No newline at end of file +?> diff --git a/data/web/inc/triggers.inc.php b/data/web/inc/triggers.inc.php index 9b622613..ce943be1 100644 --- a/data/web/inc/triggers.inc.php +++ b/data/web/inc/triggers.inc.php @@ -90,8 +90,11 @@ if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == "admi if (isset($_POST["reset_main_logo"])) { customize('delete', 'main_logo'); } - // API cannot be controlled by API - if (isset($_POST["admin_api"])) { + // API and license cannot be controlled by API + if (isset($_POST["license_validate_now"])) { + license('verify'); + } + if (isset($_POST["admin_api"])) { admin_api('edit', $_POST); } if (isset($_POST["admin_api_regen_key"])) { diff --git a/data/web/inc/vars.inc.php b/data/web/inc/vars.inc.php index 918a7ba9..a86377ec 100644 --- a/data/web/inc/vars.inc.php +++ b/data/web/inc/vars.inc.php @@ -100,7 +100,7 @@ $SHOW_DKIM_PRIV_KEYS = false; // mailcow Apps - buttons on login screen $MAILCOW_APPS = array( array( - 'name' => 'SOGo', + 'name' => 'Webmail', 'link' => '/SOGo/', ) ); @@ -141,11 +141,11 @@ $MAILBOX_DEFAULT_ATTRIBUTES['tls_enforce_out'] = false; // Force password change on next login (only allows login to mailcow UI) $MAILBOX_DEFAULT_ATTRIBUTES['force_pw_update'] = false; -// Force password change on next login (only allows login to mailcow UI) +// Enable SOGo access (set to false to disable access by default) $MAILBOX_DEFAULT_ATTRIBUTES['sogo_access'] = true; // Send notification when quarantine is not empty (never, hourly, daily, weekly) -$MAILBOX_DEFAULT_ATTRIBUTES['quarantine_notification'] = 'never'; +$MAILBOX_DEFAULT_ATTRIBUTES['quarantine_notification'] = 'hourly'; // Default mailbox format, should not be changed unless you know exactly, what you do, keep the trailing ":" // Check dovecot.conf for further changes (e.g. shared namespace) diff --git a/data/web/js/build/000-jquery-1.12.4.min.js b/data/web/js/build/000-jquery-1.12.4.min.js deleted file mode 100644 index e8364758..00000000 --- a/data/web/js/build/000-jquery-1.12.4.min.js +++ /dev/null @@ -1,5 +0,0 @@ -/*! jQuery v1.12.4 | (c) jQuery Foundation | jquery.org/license */ -!function(a,b){"object"==typeof module&&"object"==typeof module.exports?module.exports=a.document?b(a,!0):function(a){if(!a.document)throw new Error("jQuery requires a window with a document");return b(a)}:b(a)}("undefined"!=typeof window?window:this,function(a,b){var c=[],d=a.document,e=c.slice,f=c.concat,g=c.push,h=c.indexOf,i={},j=i.toString,k=i.hasOwnProperty,l={},m="1.12.4",n=function(a,b){return new n.fn.init(a,b)},o=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,p=/^-ms-/,q=/-([\da-z])/gi,r=function(a,b){return b.toUpperCase()};n.fn=n.prototype={jquery:m,constructor:n,selector:"",length:0,toArray:function(){return e.call(this)},get:function(a){return null!=a?0>a?this[a+this.length]:this[a]:e.call(this)},pushStack:function(a){var b=n.merge(this.constructor(),a);return b.prevObject=this,b.context=this.context,b},each:function(a){return n.each(this,a)},map:function(a){return this.pushStack(n.map(this,function(b,c){return a.call(b,c,b)}))},slice:function(){return this.pushStack(e.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(a){var b=this.length,c=+a+(0>a?b:0);return this.pushStack(c>=0&&b>c?[this[c]]:[])},end:function(){return this.prevObject||this.constructor()},push:g,sort:c.sort,splice:c.splice},n.extend=n.fn.extend=function(){var a,b,c,d,e,f,g=arguments[0]||{},h=1,i=arguments.length,j=!1;for("boolean"==typeof g&&(j=g,g=arguments[h]||{},h++),"object"==typeof g||n.isFunction(g)||(g={}),h===i&&(g=this,h--);i>h;h++)if(null!=(e=arguments[h]))for(d in e)a=g[d],c=e[d],g!==c&&(j&&c&&(n.isPlainObject(c)||(b=n.isArray(c)))?(b?(b=!1,f=a&&n.isArray(a)?a:[]):f=a&&n.isPlainObject(a)?a:{},g[d]=n.extend(j,f,c)):void 0!==c&&(g[d]=c));return g},n.extend({expando:"jQuery"+(m+Math.random()).replace(/\D/g,""),isReady:!0,error:function(a){throw new Error(a)},noop:function(){},isFunction:function(a){return"function"===n.type(a)},isArray:Array.isArray||function(a){return"array"===n.type(a)},isWindow:function(a){return null!=a&&a==a.window},isNumeric:function(a){var b=a&&a.toString();return!n.isArray(a)&&b-parseFloat(b)+1>=0},isEmptyObject:function(a){var b;for(b in a)return!1;return!0},isPlainObject:function(a){var b;if(!a||"object"!==n.type(a)||a.nodeType||n.isWindow(a))return!1;try{if(a.constructor&&!k.call(a,"constructor")&&!k.call(a.constructor.prototype,"isPrototypeOf"))return!1}catch(c){return!1}if(!l.ownFirst)for(b in a)return k.call(a,b);for(b in a);return void 0===b||k.call(a,b)},type:function(a){return null==a?a+"":"object"==typeof a||"function"==typeof a?i[j.call(a)]||"object":typeof a},globalEval:function(b){b&&n.trim(b)&&(a.execScript||function(b){a.eval.call(a,b)})(b)},camelCase:function(a){return a.replace(p,"ms-").replace(q,r)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toLowerCase()===b.toLowerCase()},each:function(a,b){var c,d=0;if(s(a)){for(c=a.length;c>d;d++)if(b.call(a[d],d,a[d])===!1)break}else for(d in a)if(b.call(a[d],d,a[d])===!1)break;return a},trim:function(a){return null==a?"":(a+"").replace(o,"")},makeArray:function(a,b){var c=b||[];return null!=a&&(s(Object(a))?n.merge(c,"string"==typeof a?[a]:a):g.call(c,a)),c},inArray:function(a,b,c){var d;if(b){if(h)return h.call(b,a,c);for(d=b.length,c=c?0>c?Math.max(0,d+c):c:0;d>c;c++)if(c in b&&b[c]===a)return c}return-1},merge:function(a,b){var c=+b.length,d=0,e=a.length;while(c>d)a[e++]=b[d++];if(c!==c)while(void 0!==b[d])a[e++]=b[d++];return a.length=e,a},grep:function(a,b,c){for(var d,e=[],f=0,g=a.length,h=!c;g>f;f++)d=!b(a[f],f),d!==h&&e.push(a[f]);return e},map:function(a,b,c){var d,e,g=0,h=[];if(s(a))for(d=a.length;d>g;g++)e=b(a[g],g,c),null!=e&&h.push(e);else for(g in a)e=b(a[g],g,c),null!=e&&h.push(e);return f.apply([],h)},guid:1,proxy:function(a,b){var c,d,f;return"string"==typeof b&&(f=a[b],b=a,a=f),n.isFunction(a)?(c=e.call(arguments,2),d=function(){return a.apply(b||this,c.concat(e.call(arguments)))},d.guid=a.guid=a.guid||n.guid++,d):void 0},now:function(){return+new Date},support:l}),"function"==typeof Symbol&&(n.fn[Symbol.iterator]=c[Symbol.iterator]),n.each("Boolean Number String Function Array Date RegExp Object Error Symbol".split(" "),function(a,b){i["[object "+b+"]"]=b.toLowerCase()});function s(a){var b=!!a&&"length"in a&&a.length,c=n.type(a);return"function"===c||n.isWindow(a)?!1:"array"===c||0===b||"number"==typeof b&&b>0&&b-1 in a}var t=function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u="sizzle"+1*new Date,v=a.document,w=0,x=0,y=ga(),z=ga(),A=ga(),B=function(a,b){return a===b&&(l=!0),0},C=1<<31,D={}.hasOwnProperty,E=[],F=E.pop,G=E.push,H=E.push,I=E.slice,J=function(a,b){for(var c=0,d=a.length;d>c;c++)if(a[c]===b)return c;return-1},K="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",L="[\\x20\\t\\r\\n\\f]",M="(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",N="\\["+L+"*("+M+")(?:"+L+"*([*^$|!~]?=)"+L+"*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|("+M+"))|)"+L+"*\\]",O=":("+M+")(?:\\((('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|((?:\\\\.|[^\\\\()[\\]]|"+N+")*)|.*)\\)|)",P=new RegExp(L+"+","g"),Q=new RegExp("^"+L+"+|((?:^|[^\\\\])(?:\\\\.)*)"+L+"+$","g"),R=new RegExp("^"+L+"*,"+L+"*"),S=new RegExp("^"+L+"*([>+~]|"+L+")"+L+"*"),T=new RegExp("="+L+"*([^\\]'\"]*?)"+L+"*\\]","g"),U=new RegExp(O),V=new RegExp("^"+M+"$"),W={ID:new RegExp("^#("+M+")"),CLASS:new RegExp("^\\.("+M+")"),TAG:new RegExp("^("+M+"|[*])"),ATTR:new RegExp("^"+N),PSEUDO:new RegExp("^"+O),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+L+"*(even|odd|(([+-]|)(\\d*)n|)"+L+"*(?:([+-]|)"+L+"*(\\d+)|))"+L+"*\\)|)","i"),bool:new RegExp("^(?:"+K+")$","i"),needsContext:new RegExp("^"+L+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+L+"*((?:-\\d)?\\d*)"+L+"*\\)|)(?=[^-]|$)","i")},X=/^(?:input|select|textarea|button)$/i,Y=/^h\d$/i,Z=/^[^{]+\{\s*\[native \w/,$=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,_=/[+~]/,aa=/'|\\/g,ba=new RegExp("\\\\([\\da-f]{1,6}"+L+"?|("+L+")|.)","ig"),ca=function(a,b,c){var d="0x"+b-65536;return d!==d||c?b:0>d?String.fromCharCode(d+65536):String.fromCharCode(d>>10|55296,1023&d|56320)},da=function(){m()};try{H.apply(E=I.call(v.childNodes),v.childNodes),E[v.childNodes.length].nodeType}catch(ea){H={apply:E.length?function(a,b){G.apply(a,I.call(b))}:function(a,b){var c=a.length,d=0;while(a[c++]=b[d++]);a.length=c-1}}}function fa(a,b,d,e){var f,h,j,k,l,o,r,s,w=b&&b.ownerDocument,x=b?b.nodeType:9;if(d=d||[],"string"!=typeof a||!a||1!==x&&9!==x&&11!==x)return d;if(!e&&((b?b.ownerDocument||b:v)!==n&&m(b),b=b||n,p)){if(11!==x&&(o=$.exec(a)))if(f=o[1]){if(9===x){if(!(j=b.getElementById(f)))return d;if(j.id===f)return d.push(j),d}else if(w&&(j=w.getElementById(f))&&t(b,j)&&j.id===f)return d.push(j),d}else{if(o[2])return H.apply(d,b.getElementsByTagName(a)),d;if((f=o[3])&&c.getElementsByClassName&&b.getElementsByClassName)return H.apply(d,b.getElementsByClassName(f)),d}if(c.qsa&&!A[a+" "]&&(!q||!q.test(a))){if(1!==x)w=b,s=a;else if("object"!==b.nodeName.toLowerCase()){(k=b.getAttribute("id"))?k=k.replace(aa,"\\$&"):b.setAttribute("id",k=u),r=g(a),h=r.length,l=V.test(k)?"#"+k:"[id='"+k+"']";while(h--)r[h]=l+" "+qa(r[h]);s=r.join(","),w=_.test(a)&&oa(b.parentNode)||b}if(s)try{return H.apply(d,w.querySelectorAll(s)),d}catch(y){}finally{k===u&&b.removeAttribute("id")}}}return i(a.replace(Q,"$1"),b,d,e)}function ga(){var a=[];function b(c,e){return a.push(c+" ")>d.cacheLength&&delete b[a.shift()],b[c+" "]=e}return b}function ha(a){return a[u]=!0,a}function ia(a){var b=n.createElement("div");try{return!!a(b)}catch(c){return!1}finally{b.parentNode&&b.parentNode.removeChild(b),b=null}}function ja(a,b){var c=a.split("|"),e=c.length;while(e--)d.attrHandle[c[e]]=b}function ka(a,b){var c=b&&a,d=c&&1===a.nodeType&&1===b.nodeType&&(~b.sourceIndex||C)-(~a.sourceIndex||C);if(d)return d;if(c)while(c=c.nextSibling)if(c===b)return-1;return a?1:-1}function la(a){return function(b){var c=b.nodeName.toLowerCase();return"input"===c&&b.type===a}}function ma(a){return function(b){var c=b.nodeName.toLowerCase();return("input"===c||"button"===c)&&b.type===a}}function na(a){return ha(function(b){return b=+b,ha(function(c,d){var e,f=a([],c.length,b),g=f.length;while(g--)c[e=f[g]]&&(c[e]=!(d[e]=c[e]))})})}function oa(a){return a&&"undefined"!=typeof a.getElementsByTagName&&a}c=fa.support={},f=fa.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return b?"HTML"!==b.nodeName:!1},m=fa.setDocument=function(a){var b,e,g=a?a.ownerDocument||a:v;return g!==n&&9===g.nodeType&&g.documentElement?(n=g,o=n.documentElement,p=!f(n),(e=n.defaultView)&&e.top!==e&&(e.addEventListener?e.addEventListener("unload",da,!1):e.attachEvent&&e.attachEvent("onunload",da)),c.attributes=ia(function(a){return a.className="i",!a.getAttribute("className")}),c.getElementsByTagName=ia(function(a){return a.appendChild(n.createComment("")),!a.getElementsByTagName("*").length}),c.getElementsByClassName=Z.test(n.getElementsByClassName),c.getById=ia(function(a){return o.appendChild(a).id=u,!n.getElementsByName||!n.getElementsByName(u).length}),c.getById?(d.find.ID=function(a,b){if("undefined"!=typeof b.getElementById&&p){var c=b.getElementById(a);return c?[c]:[]}},d.filter.ID=function(a){var b=a.replace(ba,ca);return function(a){return a.getAttribute("id")===b}}):(delete d.find.ID,d.filter.ID=function(a){var b=a.replace(ba,ca);return function(a){var c="undefined"!=typeof a.getAttributeNode&&a.getAttributeNode("id");return c&&c.value===b}}),d.find.TAG=c.getElementsByTagName?function(a,b){return"undefined"!=typeof b.getElementsByTagName?b.getElementsByTagName(a):c.qsa?b.querySelectorAll(a):void 0}:function(a,b){var c,d=[],e=0,f=b.getElementsByTagName(a);if("*"===a){while(c=f[e++])1===c.nodeType&&d.push(c);return d}return f},d.find.CLASS=c.getElementsByClassName&&function(a,b){return"undefined"!=typeof b.getElementsByClassName&&p?b.getElementsByClassName(a):void 0},r=[],q=[],(c.qsa=Z.test(n.querySelectorAll))&&(ia(function(a){o.appendChild(a).innerHTML="<a id='"+u+"'></a><select id='"+u+"-\r\\' msallowcapture=''><option selected=''></option></select>",a.querySelectorAll("[msallowcapture^='']").length&&q.push("[*^$]="+L+"*(?:''|\"\")"),a.querySelectorAll("[selected]").length||q.push("\\["+L+"*(?:value|"+K+")"),a.querySelectorAll("[id~="+u+"-]").length||q.push("~="),a.querySelectorAll(":checked").length||q.push(":checked"),a.querySelectorAll("a#"+u+"+*").length||q.push(".#.+[+~]")}),ia(function(a){var b=n.createElement("input");b.setAttribute("type","hidden"),a.appendChild(b).setAttribute("name","D"),a.querySelectorAll("[name=d]").length&&q.push("name"+L+"*[*^$|!~]?="),a.querySelectorAll(":enabled").length||q.push(":enabled",":disabled"),a.querySelectorAll("*,:x"),q.push(",.*:")})),(c.matchesSelector=Z.test(s=o.matches||o.webkitMatchesSelector||o.mozMatchesSelector||o.oMatchesSelector||o.msMatchesSelector))&&ia(function(a){c.disconnectedMatch=s.call(a,"div"),s.call(a,"[s!='']:x"),r.push("!=",O)}),q=q.length&&new RegExp(q.join("|")),r=r.length&&new RegExp(r.join("|")),b=Z.test(o.compareDocumentPosition),t=b||Z.test(o.contains)?function(a,b){var c=9===a.nodeType?a.documentElement:a,d=b&&b.parentNode;return a===d||!(!d||1!==d.nodeType||!(c.contains?c.contains(d):a.compareDocumentPosition&&16&a.compareDocumentPosition(d)))}:function(a,b){if(b)while(b=b.parentNode)if(b===a)return!0;return!1},B=b?function(a,b){if(a===b)return l=!0,0;var d=!a.compareDocumentPosition-!b.compareDocumentPosition;return d?d:(d=(a.ownerDocument||a)===(b.ownerDocument||b)?a.compareDocumentPosition(b):1,1&d||!c.sortDetached&&b.compareDocumentPosition(a)===d?a===n||a.ownerDocument===v&&t(v,a)?-1:b===n||b.ownerDocument===v&&t(v,b)?1:k?J(k,a)-J(k,b):0:4&d?-1:1)}:function(a,b){if(a===b)return l=!0,0;var c,d=0,e=a.parentNode,f=b.parentNode,g=[a],h=[b];if(!e||!f)return a===n?-1:b===n?1:e?-1:f?1:k?J(k,a)-J(k,b):0;if(e===f)return ka(a,b);c=a;while(c=c.parentNode)g.unshift(c);c=b;while(c=c.parentNode)h.unshift(c);while(g[d]===h[d])d++;return d?ka(g[d],h[d]):g[d]===v?-1:h[d]===v?1:0},n):n},fa.matches=function(a,b){return fa(a,null,null,b)},fa.matchesSelector=function(a,b){if((a.ownerDocument||a)!==n&&m(a),b=b.replace(T,"='$1']"),c.matchesSelector&&p&&!A[b+" "]&&(!r||!r.test(b))&&(!q||!q.test(b)))try{var d=s.call(a,b);if(d||c.disconnectedMatch||a.document&&11!==a.document.nodeType)return d}catch(e){}return fa(b,n,null,[a]).length>0},fa.contains=function(a,b){return(a.ownerDocument||a)!==n&&m(a),t(a,b)},fa.attr=function(a,b){(a.ownerDocument||a)!==n&&m(a);var e=d.attrHandle[b.toLowerCase()],f=e&&D.call(d.attrHandle,b.toLowerCase())?e(a,b,!p):void 0;return void 0!==f?f:c.attributes||!p?a.getAttribute(b):(f=a.getAttributeNode(b))&&f.specified?f.value:null},fa.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)},fa.uniqueSort=function(a){var b,d=[],e=0,f=0;if(l=!c.detectDuplicates,k=!c.sortStable&&a.slice(0),a.sort(B),l){while(b=a[f++])b===a[f]&&(e=d.push(f));while(e--)a.splice(d[e],1)}return k=null,a},e=fa.getText=function(a){var b,c="",d=0,f=a.nodeType;if(f){if(1===f||9===f||11===f){if("string"==typeof a.textContent)return a.textContent;for(a=a.firstChild;a;a=a.nextSibling)c+=e(a)}else if(3===f||4===f)return a.nodeValue}else while(b=a[d++])c+=e(b);return c},d=fa.selectors={cacheLength:50,createPseudo:ha,match:W,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(ba,ca),a[3]=(a[3]||a[4]||a[5]||"").replace(ba,ca),"~="===a[2]&&(a[3]=" "+a[3]+" "),a.slice(0,4)},CHILD:function(a){return a[1]=a[1].toLowerCase(),"nth"===a[1].slice(0,3)?(a[3]||fa.error(a[0]),a[4]=+(a[4]?a[5]+(a[6]||1):2*("even"===a[3]||"odd"===a[3])),a[5]=+(a[7]+a[8]||"odd"===a[3])):a[3]&&fa.error(a[0]),a},PSEUDO:function(a){var b,c=!a[6]&&a[2];return W.CHILD.test(a[0])?null:(a[3]?a[2]=a[4]||a[5]||"":c&&U.test(c)&&(b=g(c,!0))&&(b=c.indexOf(")",c.length-b)-c.length)&&(a[0]=a[0].slice(0,b),a[2]=c.slice(0,b)),a.slice(0,3))}},filter:{TAG:function(a){var b=a.replace(ba,ca).toLowerCase();return"*"===a?function(){return!0}:function(a){return a.nodeName&&a.nodeName.toLowerCase()===b}},CLASS:function(a){var b=y[a+" "];return b||(b=new RegExp("(^|"+L+")"+a+"("+L+"|$)"))&&y(a,function(a){return b.test("string"==typeof a.className&&a.className||"undefined"!=typeof a.getAttribute&&a.getAttribute("class")||"")})},ATTR:function(a,b,c){return function(d){var e=fa.attr(d,a);return null==e?"!="===b:b?(e+="","="===b?e===c:"!="===b?e!==c:"^="===b?c&&0===e.indexOf(c):"*="===b?c&&e.indexOf(c)>-1:"$="===b?c&&e.slice(-c.length)===c:"~="===b?(" "+e.replace(P," ")+" ").indexOf(c)>-1:"|="===b?e===c||e.slice(0,c.length+1)===c+"-":!1):!0}},CHILD:function(a,b,c,d,e){var f="nth"!==a.slice(0,3),g="last"!==a.slice(-4),h="of-type"===b;return 1===d&&0===e?function(a){return!!a.parentNode}:function(b,c,i){var j,k,l,m,n,o,p=f!==g?"nextSibling":"previousSibling",q=b.parentNode,r=h&&b.nodeName.toLowerCase(),s=!i&&!h,t=!1;if(q){if(f){while(p){m=b;while(m=m[p])if(h?m.nodeName.toLowerCase()===r:1===m.nodeType)return!1;o=p="only"===a&&!o&&"nextSibling"}return!0}if(o=[g?q.firstChild:q.lastChild],g&&s){m=q,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n&&j[2],m=n&&q.childNodes[n];while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if(1===m.nodeType&&++t&&m===b){k[a]=[w,n,t];break}}else if(s&&(m=b,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n),t===!1)while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if((h?m.nodeName.toLowerCase()===r:1===m.nodeType)&&++t&&(s&&(l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),k[a]=[w,t]),m===b))break;return t-=e,t===d||t%d===0&&t/d>=0}}},PSEUDO:function(a,b){var c,e=d.pseudos[a]||d.setFilters[a.toLowerCase()]||fa.error("unsupported pseudo: "+a);return e[u]?e(b):e.length>1?(c=[a,a,"",b],d.setFilters.hasOwnProperty(a.toLowerCase())?ha(function(a,c){var d,f=e(a,b),g=f.length;while(g--)d=J(a,f[g]),a[d]=!(c[d]=f[g])}):function(a){return e(a,0,c)}):e}},pseudos:{not:ha(function(a){var b=[],c=[],d=h(a.replace(Q,"$1"));return d[u]?ha(function(a,b,c,e){var f,g=d(a,null,e,[]),h=a.length;while(h--)(f=g[h])&&(a[h]=!(b[h]=f))}):function(a,e,f){return b[0]=a,d(b,null,f,c),b[0]=null,!c.pop()}}),has:ha(function(a){return function(b){return fa(a,b).length>0}}),contains:ha(function(a){return a=a.replace(ba,ca),function(b){return(b.textContent||b.innerText||e(b)).indexOf(a)>-1}}),lang:ha(function(a){return V.test(a||"")||fa.error("unsupported lang: "+a),a=a.replace(ba,ca).toLowerCase(),function(b){var c;do if(c=p?b.lang:b.getAttribute("xml:lang")||b.getAttribute("lang"))return c=c.toLowerCase(),c===a||0===c.indexOf(a+"-");while((b=b.parentNode)&&1===b.nodeType);return!1}}),target:function(b){var c=a.location&&a.location.hash;return c&&c.slice(1)===b.id},root:function(a){return a===o},focus:function(a){return a===n.activeElement&&(!n.hasFocus||n.hasFocus())&&!!(a.type||a.href||~a.tabIndex)},enabled:function(a){return a.disabled===!1},disabled:function(a){return a.disabled===!0},checked:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&!!a.checked||"option"===b&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},empty:function(a){for(a=a.firstChild;a;a=a.nextSibling)if(a.nodeType<6)return!1;return!0},parent:function(a){return!d.pseudos.empty(a)},header:function(a){return Y.test(a.nodeName)},input:function(a){return X.test(a.nodeName)},button:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&"button"===a.type||"button"===b},text:function(a){var b;return"input"===a.nodeName.toLowerCase()&&"text"===a.type&&(null==(b=a.getAttribute("type"))||"text"===b.toLowerCase())},first:na(function(){return[0]}),last:na(function(a,b){return[b-1]}),eq:na(function(a,b,c){return[0>c?c+b:c]}),even:na(function(a,b){for(var c=0;b>c;c+=2)a.push(c);return a}),odd:na(function(a,b){for(var c=1;b>c;c+=2)a.push(c);return a}),lt:na(function(a,b,c){for(var d=0>c?c+b:c;--d>=0;)a.push(d);return a}),gt:na(function(a,b,c){for(var d=0>c?c+b:c;++d<b;)a.push(d);return a})}},d.pseudos.nth=d.pseudos.eq;for(b in{radio:!0,checkbox:!0,file:!0,password:!0,image:!0})d.pseudos[b]=la(b);for(b in{submit:!0,reset:!0})d.pseudos[b]=ma(b);function pa(){}pa.prototype=d.filters=d.pseudos,d.setFilters=new pa,g=fa.tokenize=function(a,b){var c,e,f,g,h,i,j,k=z[a+" "];if(k)return b?0:k.slice(0);h=a,i=[],j=d.preFilter;while(h){c&&!(e=R.exec(h))||(e&&(h=h.slice(e[0].length)||h),i.push(f=[])),c=!1,(e=S.exec(h))&&(c=e.shift(),f.push({value:c,type:e[0].replace(Q," ")}),h=h.slice(c.length));for(g in d.filter)!(e=W[g].exec(h))||j[g]&&!(e=j[g](e))||(c=e.shift(),f.push({value:c,type:g,matches:e}),h=h.slice(c.length));if(!c)break}return b?h.length:h?fa.error(a):z(a,i).slice(0)};function qa(a){for(var b=0,c=a.length,d="";c>b;b++)d+=a[b].value;return d}function ra(a,b,c){var d=b.dir,e=c&&"parentNode"===d,f=x++;return b.first?function(b,c,f){while(b=b[d])if(1===b.nodeType||e)return a(b,c,f)}:function(b,c,g){var h,i,j,k=[w,f];if(g){while(b=b[d])if((1===b.nodeType||e)&&a(b,c,g))return!0}else while(b=b[d])if(1===b.nodeType||e){if(j=b[u]||(b[u]={}),i=j[b.uniqueID]||(j[b.uniqueID]={}),(h=i[d])&&h[0]===w&&h[1]===f)return k[2]=h[2];if(i[d]=k,k[2]=a(b,c,g))return!0}}}function sa(a){return a.length>1?function(b,c,d){var e=a.length;while(e--)if(!a[e](b,c,d))return!1;return!0}:a[0]}function ta(a,b,c){for(var d=0,e=b.length;e>d;d++)fa(a,b[d],c);return c}function ua(a,b,c,d,e){for(var f,g=[],h=0,i=a.length,j=null!=b;i>h;h++)(f=a[h])&&(c&&!c(f,d,e)||(g.push(f),j&&b.push(h)));return g}function va(a,b,c,d,e,f){return d&&!d[u]&&(d=va(d)),e&&!e[u]&&(e=va(e,f)),ha(function(f,g,h,i){var j,k,l,m=[],n=[],o=g.length,p=f||ta(b||"*",h.nodeType?[h]:h,[]),q=!a||!f&&b?p:ua(p,m,a,h,i),r=c?e||(f?a:o||d)?[]:g:q;if(c&&c(q,r,h,i),d){j=ua(r,n),d(j,[],h,i),k=j.length;while(k--)(l=j[k])&&(r[n[k]]=!(q[n[k]]=l))}if(f){if(e||a){if(e){j=[],k=r.length;while(k--)(l=r[k])&&j.push(q[k]=l);e(null,r=[],j,i)}k=r.length;while(k--)(l=r[k])&&(j=e?J(f,l):m[k])>-1&&(f[j]=!(g[j]=l))}}else r=ua(r===g?r.splice(o,r.length):r),e?e(null,g,r,i):H.apply(g,r)})}function wa(a){for(var b,c,e,f=a.length,g=d.relative[a[0].type],h=g||d.relative[" "],i=g?1:0,k=ra(function(a){return a===b},h,!0),l=ra(function(a){return J(b,a)>-1},h,!0),m=[function(a,c,d){var e=!g&&(d||c!==j)||((b=c).nodeType?k(a,c,d):l(a,c,d));return b=null,e}];f>i;i++)if(c=d.relative[a[i].type])m=[ra(sa(m),c)];else{if(c=d.filter[a[i].type].apply(null,a[i].matches),c[u]){for(e=++i;f>e;e++)if(d.relative[a[e].type])break;return va(i>1&&sa(m),i>1&&qa(a.slice(0,i-1).concat({value:" "===a[i-2].type?"*":""})).replace(Q,"$1"),c,e>i&&wa(a.slice(i,e)),f>e&&wa(a=a.slice(e)),f>e&&qa(a))}m.push(c)}return sa(m)}function xa(a,b){var c=b.length>0,e=a.length>0,f=function(f,g,h,i,k){var l,o,q,r=0,s="0",t=f&&[],u=[],v=j,x=f||e&&d.find.TAG("*",k),y=w+=null==v?1:Math.random()||.1,z=x.length;for(k&&(j=g===n||g||k);s!==z&&null!=(l=x[s]);s++){if(e&&l){o=0,g||l.ownerDocument===n||(m(l),h=!p);while(q=a[o++])if(q(l,g||n,h)){i.push(l);break}k&&(w=y)}c&&((l=!q&&l)&&r--,f&&t.push(l))}if(r+=s,c&&s!==r){o=0;while(q=b[o++])q(t,u,g,h);if(f){if(r>0)while(s--)t[s]||u[s]||(u[s]=F.call(i));u=ua(u)}H.apply(i,u),k&&!f&&u.length>0&&r+b.length>1&&fa.uniqueSort(i)}return k&&(w=y,j=v),t};return c?ha(f):f}return h=fa.compile=function(a,b){var c,d=[],e=[],f=A[a+" "];if(!f){b||(b=g(a)),c=b.length;while(c--)f=wa(b[c]),f[u]?d.push(f):e.push(f);f=A(a,xa(e,d)),f.selector=a}return f},i=fa.select=function(a,b,e,f){var i,j,k,l,m,n="function"==typeof a&&a,o=!f&&g(a=n.selector||a);if(e=e||[],1===o.length){if(j=o[0]=o[0].slice(0),j.length>2&&"ID"===(k=j[0]).type&&c.getById&&9===b.nodeType&&p&&d.relative[j[1].type]){if(b=(d.find.ID(k.matches[0].replace(ba,ca),b)||[])[0],!b)return e;n&&(b=b.parentNode),a=a.slice(j.shift().value.length)}i=W.needsContext.test(a)?0:j.length;while(i--){if(k=j[i],d.relative[l=k.type])break;if((m=d.find[l])&&(f=m(k.matches[0].replace(ba,ca),_.test(j[0].type)&&oa(b.parentNode)||b))){if(j.splice(i,1),a=f.length&&qa(j),!a)return H.apply(e,f),e;break}}}return(n||h(a,o))(f,b,!p,e,!b||_.test(a)&&oa(b.parentNode)||b),e},c.sortStable=u.split("").sort(B).join("")===u,c.detectDuplicates=!!l,m(),c.sortDetached=ia(function(a){return 1&a.compareDocumentPosition(n.createElement("div"))}),ia(function(a){return a.innerHTML="<a href='#'></a>","#"===a.firstChild.getAttribute("href")})||ja("type|href|height|width",function(a,b,c){return c?void 0:a.getAttribute(b,"type"===b.toLowerCase()?1:2)}),c.attributes&&ia(function(a){return a.innerHTML="<input/>",a.firstChild.setAttribute("value",""),""===a.firstChild.getAttribute("value")})||ja("value",function(a,b,c){return c||"input"!==a.nodeName.toLowerCase()?void 0:a.defaultValue}),ia(function(a){return null==a.getAttribute("disabled")})||ja(K,function(a,b,c){var d;return c?void 0:a[b]===!0?b.toLowerCase():(d=a.getAttributeNode(b))&&d.specified?d.value:null}),fa}(a);n.find=t,n.expr=t.selectors,n.expr[":"]=n.expr.pseudos,n.uniqueSort=n.unique=t.uniqueSort,n.text=t.getText,n.isXMLDoc=t.isXML,n.contains=t.contains;var u=function(a,b,c){var d=[],e=void 0!==c;while((a=a[b])&&9!==a.nodeType)if(1===a.nodeType){if(e&&n(a).is(c))break;d.push(a)}return d},v=function(a,b){for(var c=[];a;a=a.nextSibling)1===a.nodeType&&a!==b&&c.push(a);return c},w=n.expr.match.needsContext,x=/^<([\w-]+)\s*\/?>(?:<\/\1>|)$/,y=/^.[^:#\[\.,]*$/;function z(a,b,c){if(n.isFunction(b))return n.grep(a,function(a,d){return!!b.call(a,d,a)!==c});if(b.nodeType)return n.grep(a,function(a){return a===b!==c});if("string"==typeof b){if(y.test(b))return n.filter(b,a,c);b=n.filter(b,a)}return n.grep(a,function(a){return n.inArray(a,b)>-1!==c})}n.filter=function(a,b,c){var d=b[0];return c&&(a=":not("+a+")"),1===b.length&&1===d.nodeType?n.find.matchesSelector(d,a)?[d]:[]:n.find.matches(a,n.grep(b,function(a){return 1===a.nodeType}))},n.fn.extend({find:function(a){var b,c=[],d=this,e=d.length;if("string"!=typeof a)return this.pushStack(n(a).filter(function(){for(b=0;e>b;b++)if(n.contains(d[b],this))return!0}));for(b=0;e>b;b++)n.find(a,d[b],c);return c=this.pushStack(e>1?n.unique(c):c),c.selector=this.selector?this.selector+" "+a:a,c},filter:function(a){return this.pushStack(z(this,a||[],!1))},not:function(a){return this.pushStack(z(this,a||[],!0))},is:function(a){return!!z(this,"string"==typeof a&&w.test(a)?n(a):a||[],!1).length}});var A,B=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/,C=n.fn.init=function(a,b,c){var e,f;if(!a)return this;if(c=c||A,"string"==typeof a){if(e="<"===a.charAt(0)&&">"===a.charAt(a.length-1)&&a.length>=3?[null,a,null]:B.exec(a),!e||!e[1]&&b)return!b||b.jquery?(b||c).find(a):this.constructor(b).find(a);if(e[1]){if(b=b instanceof n?b[0]:b,n.merge(this,n.parseHTML(e[1],b&&b.nodeType?b.ownerDocument||b:d,!0)),x.test(e[1])&&n.isPlainObject(b))for(e in b)n.isFunction(this[e])?this[e](b[e]):this.attr(e,b[e]);return this}if(f=d.getElementById(e[2]),f&&f.parentNode){if(f.id!==e[2])return A.find(a);this.length=1,this[0]=f}return this.context=d,this.selector=a,this}return a.nodeType?(this.context=this[0]=a,this.length=1,this):n.isFunction(a)?"undefined"!=typeof c.ready?c.ready(a):a(n):(void 0!==a.selector&&(this.selector=a.selector,this.context=a.context),n.makeArray(a,this))};C.prototype=n.fn,A=n(d);var D=/^(?:parents|prev(?:Until|All))/,E={children:!0,contents:!0,next:!0,prev:!0};n.fn.extend({has:function(a){var b,c=n(a,this),d=c.length;return this.filter(function(){for(b=0;d>b;b++)if(n.contains(this,c[b]))return!0})},closest:function(a,b){for(var c,d=0,e=this.length,f=[],g=w.test(a)||"string"!=typeof a?n(a,b||this.context):0;e>d;d++)for(c=this[d];c&&c!==b;c=c.parentNode)if(c.nodeType<11&&(g?g.index(c)>-1:1===c.nodeType&&n.find.matchesSelector(c,a))){f.push(c);break}return this.pushStack(f.length>1?n.uniqueSort(f):f)},index:function(a){return a?"string"==typeof a?n.inArray(this[0],n(a)):n.inArray(a.jquery?a[0]:a,this):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(a,b){return this.pushStack(n.uniqueSort(n.merge(this.get(),n(a,b))))},addBack:function(a){return this.add(null==a?this.prevObject:this.prevObject.filter(a))}});function F(a,b){do a=a[b];while(a&&1!==a.nodeType);return a}n.each({parent:function(a){var b=a.parentNode;return b&&11!==b.nodeType?b:null},parents:function(a){return u(a,"parentNode")},parentsUntil:function(a,b,c){return u(a,"parentNode",c)},next:function(a){return F(a,"nextSibling")},prev:function(a){return F(a,"previousSibling")},nextAll:function(a){return u(a,"nextSibling")},prevAll:function(a){return u(a,"previousSibling")},nextUntil:function(a,b,c){return u(a,"nextSibling",c)},prevUntil:function(a,b,c){return u(a,"previousSibling",c)},siblings:function(a){return v((a.parentNode||{}).firstChild,a)},children:function(a){return v(a.firstChild)},contents:function(a){return n.nodeName(a,"iframe")?a.contentDocument||a.contentWindow.document:n.merge([],a.childNodes)}},function(a,b){n.fn[a]=function(c,d){var e=n.map(this,b,c);return"Until"!==a.slice(-5)&&(d=c),d&&"string"==typeof d&&(e=n.filter(d,e)),this.length>1&&(E[a]||(e=n.uniqueSort(e)),D.test(a)&&(e=e.reverse())),this.pushStack(e)}});var G=/\S+/g;function H(a){var b={};return n.each(a.match(G)||[],function(a,c){b[c]=!0}),b}n.Callbacks=function(a){a="string"==typeof a?H(a):n.extend({},a);var b,c,d,e,f=[],g=[],h=-1,i=function(){for(e=a.once,d=b=!0;g.length;h=-1){c=g.shift();while(++h<f.length)f[h].apply(c[0],c[1])===!1&&a.stopOnFalse&&(h=f.length,c=!1)}a.memory||(c=!1),b=!1,e&&(f=c?[]:"")},j={add:function(){return f&&(c&&!b&&(h=f.length-1,g.push(c)),function d(b){n.each(b,function(b,c){n.isFunction(c)?a.unique&&j.has(c)||f.push(c):c&&c.length&&"string"!==n.type(c)&&d(c)})}(arguments),c&&!b&&i()),this},remove:function(){return n.each(arguments,function(a,b){var c;while((c=n.inArray(b,f,c))>-1)f.splice(c,1),h>=c&&h--}),this},has:function(a){return a?n.inArray(a,f)>-1:f.length>0},empty:function(){return f&&(f=[]),this},disable:function(){return e=g=[],f=c="",this},disabled:function(){return!f},lock:function(){return e=!0,c||j.disable(),this},locked:function(){return!!e},fireWith:function(a,c){return e||(c=c||[],c=[a,c.slice?c.slice():c],g.push(c),b||i()),this},fire:function(){return j.fireWith(this,arguments),this},fired:function(){return!!d}};return j},n.extend({Deferred:function(a){var b=[["resolve","done",n.Callbacks("once memory"),"resolved"],["reject","fail",n.Callbacks("once memory"),"rejected"],["notify","progress",n.Callbacks("memory")]],c="pending",d={state:function(){return c},always:function(){return e.done(arguments).fail(arguments),this},then:function(){var a=arguments;return n.Deferred(function(c){n.each(b,function(b,f){var g=n.isFunction(a[b])&&a[b];e[f[1]](function(){var a=g&&g.apply(this,arguments);a&&n.isFunction(a.promise)?a.promise().progress(c.notify).done(c.resolve).fail(c.reject):c[f[0]+"With"](this===d?c.promise():this,g?[a]:arguments)})}),a=null}).promise()},promise:function(a){return null!=a?n.extend(a,d):d}},e={};return d.pipe=d.then,n.each(b,function(a,f){var g=f[2],h=f[3];d[f[1]]=g.add,h&&g.add(function(){c=h},b[1^a][2].disable,b[2][2].lock),e[f[0]]=function(){return e[f[0]+"With"](this===e?d:this,arguments),this},e[f[0]+"With"]=g.fireWith}),d.promise(e),a&&a.call(e,e),e},when:function(a){var b=0,c=e.call(arguments),d=c.length,f=1!==d||a&&n.isFunction(a.promise)?d:0,g=1===f?a:n.Deferred(),h=function(a,b,c){return function(d){b[a]=this,c[a]=arguments.length>1?e.call(arguments):d,c===i?g.notifyWith(b,c):--f||g.resolveWith(b,c)}},i,j,k;if(d>1)for(i=new Array(d),j=new Array(d),k=new Array(d);d>b;b++)c[b]&&n.isFunction(c[b].promise)?c[b].promise().progress(h(b,j,i)).done(h(b,k,c)).fail(g.reject):--f;return f||g.resolveWith(k,c),g.promise()}});var I;n.fn.ready=function(a){return n.ready.promise().done(a),this},n.extend({isReady:!1,readyWait:1,holdReady:function(a){a?n.readyWait++:n.ready(!0)},ready:function(a){(a===!0?--n.readyWait:n.isReady)||(n.isReady=!0,a!==!0&&--n.readyWait>0||(I.resolveWith(d,[n]),n.fn.triggerHandler&&(n(d).triggerHandler("ready"),n(d).off("ready"))))}});function J(){d.addEventListener?(d.removeEventListener("DOMContentLoaded",K),a.removeEventListener("load",K)):(d.detachEvent("onreadystatechange",K),a.detachEvent("onload",K))}function K(){(d.addEventListener||"load"===a.event.type||"complete"===d.readyState)&&(J(),n.ready())}n.ready.promise=function(b){if(!I)if(I=n.Deferred(),"complete"===d.readyState||"loading"!==d.readyState&&!d.documentElement.doScroll)a.setTimeout(n.ready);else if(d.addEventListener)d.addEventListener("DOMContentLoaded",K),a.addEventListener("load",K);else{d.attachEvent("onreadystatechange",K),a.attachEvent("onload",K);var c=!1;try{c=null==a.frameElement&&d.documentElement}catch(e){}c&&c.doScroll&&!function f(){if(!n.isReady){try{c.doScroll("left")}catch(b){return a.setTimeout(f,50)}J(),n.ready()}}()}return I.promise(b)},n.ready.promise();var L;for(L in n(l))break;l.ownFirst="0"===L,l.inlineBlockNeedsLayout=!1,n(function(){var a,b,c,e;c=d.getElementsByTagName("body")[0],c&&c.style&&(b=d.createElement("div"),e=d.createElement("div"),e.style.cssText="position:absolute;border:0;width:0;height:0;top:0;left:-9999px",c.appendChild(e).appendChild(b),"undefined"!=typeof b.style.zoom&&(b.style.cssText="display:inline;margin:0;border:0;padding:1px;width:1px;zoom:1",l.inlineBlockNeedsLayout=a=3===b.offsetWidth,a&&(c.style.zoom=1)),c.removeChild(e))}),function(){var a=d.createElement("div");l.deleteExpando=!0;try{delete a.test}catch(b){l.deleteExpando=!1}a=null}();var M=function(a){var b=n.noData[(a.nodeName+" ").toLowerCase()],c=+a.nodeType||1;return 1!==c&&9!==c?!1:!b||b!==!0&&a.getAttribute("classid")===b},N=/^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,O=/([A-Z])/g;function P(a,b,c){if(void 0===c&&1===a.nodeType){var d="data-"+b.replace(O,"-$1").toLowerCase();if(c=a.getAttribute(d),"string"==typeof c){try{c="true"===c?!0:"false"===c?!1:"null"===c?null:+c+""===c?+c:N.test(c)?n.parseJSON(c):c}catch(e){}n.data(a,b,c)}else c=void 0; -}return c}function Q(a){var b;for(b in a)if(("data"!==b||!n.isEmptyObject(a[b]))&&"toJSON"!==b)return!1;return!0}function R(a,b,d,e){if(M(a)){var f,g,h=n.expando,i=a.nodeType,j=i?n.cache:a,k=i?a[h]:a[h]&&h;if(k&&j[k]&&(e||j[k].data)||void 0!==d||"string"!=typeof b)return k||(k=i?a[h]=c.pop()||n.guid++:h),j[k]||(j[k]=i?{}:{toJSON:n.noop}),"object"!=typeof b&&"function"!=typeof b||(e?j[k]=n.extend(j[k],b):j[k].data=n.extend(j[k].data,b)),g=j[k],e||(g.data||(g.data={}),g=g.data),void 0!==d&&(g[n.camelCase(b)]=d),"string"==typeof b?(f=g[b],null==f&&(f=g[n.camelCase(b)])):f=g,f}}function S(a,b,c){if(M(a)){var d,e,f=a.nodeType,g=f?n.cache:a,h=f?a[n.expando]:n.expando;if(g[h]){if(b&&(d=c?g[h]:g[h].data)){n.isArray(b)?b=b.concat(n.map(b,n.camelCase)):b in d?b=[b]:(b=n.camelCase(b),b=b in d?[b]:b.split(" ")),e=b.length;while(e--)delete d[b[e]];if(c?!Q(d):!n.isEmptyObject(d))return}(c||(delete g[h].data,Q(g[h])))&&(f?n.cleanData([a],!0):l.deleteExpando||g!=g.window?delete g[h]:g[h]=void 0)}}}n.extend({cache:{},noData:{"applet ":!0,"embed ":!0,"object ":"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"},hasData:function(a){return a=a.nodeType?n.cache[a[n.expando]]:a[n.expando],!!a&&!Q(a)},data:function(a,b,c){return R(a,b,c)},removeData:function(a,b){return S(a,b)},_data:function(a,b,c){return R(a,b,c,!0)},_removeData:function(a,b){return S(a,b,!0)}}),n.fn.extend({data:function(a,b){var c,d,e,f=this[0],g=f&&f.attributes;if(void 0===a){if(this.length&&(e=n.data(f),1===f.nodeType&&!n._data(f,"parsedAttrs"))){c=g.length;while(c--)g[c]&&(d=g[c].name,0===d.indexOf("data-")&&(d=n.camelCase(d.slice(5)),P(f,d,e[d])));n._data(f,"parsedAttrs",!0)}return e}return"object"==typeof a?this.each(function(){n.data(this,a)}):arguments.length>1?this.each(function(){n.data(this,a,b)}):f?P(f,a,n.data(f,a)):void 0},removeData:function(a){return this.each(function(){n.removeData(this,a)})}}),n.extend({queue:function(a,b,c){var d;return a?(b=(b||"fx")+"queue",d=n._data(a,b),c&&(!d||n.isArray(c)?d=n._data(a,b,n.makeArray(c)):d.push(c)),d||[]):void 0},dequeue:function(a,b){b=b||"fx";var c=n.queue(a,b),d=c.length,e=c.shift(),f=n._queueHooks(a,b),g=function(){n.dequeue(a,b)};"inprogress"===e&&(e=c.shift(),d--),e&&("fx"===b&&c.unshift("inprogress"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return n._data(a,c)||n._data(a,c,{empty:n.Callbacks("once memory").add(function(){n._removeData(a,b+"queue"),n._removeData(a,c)})})}}),n.fn.extend({queue:function(a,b){var c=2;return"string"!=typeof a&&(b=a,a="fx",c--),arguments.length<c?n.queue(this[0],a):void 0===b?this:this.each(function(){var c=n.queue(this,a,b);n._queueHooks(this,a),"fx"===a&&"inprogress"!==c[0]&&n.dequeue(this,a)})},dequeue:function(a){return this.each(function(){n.dequeue(this,a)})},clearQueue:function(a){return this.queue(a||"fx",[])},promise:function(a,b){var c,d=1,e=n.Deferred(),f=this,g=this.length,h=function(){--d||e.resolveWith(f,[f])};"string"!=typeof a&&(b=a,a=void 0),a=a||"fx";while(g--)c=n._data(f[g],a+"queueHooks"),c&&c.empty&&(d++,c.empty.add(h));return h(),e.promise(b)}}),function(){var a;l.shrinkWrapBlocks=function(){if(null!=a)return a;a=!1;var b,c,e;return c=d.getElementsByTagName("body")[0],c&&c.style?(b=d.createElement("div"),e=d.createElement("div"),e.style.cssText="position:absolute;border:0;width:0;height:0;top:0;left:-9999px",c.appendChild(e).appendChild(b),"undefined"!=typeof b.style.zoom&&(b.style.cssText="-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;display:block;margin:0;border:0;padding:1px;width:1px;zoom:1",b.appendChild(d.createElement("div")).style.width="5px",a=3!==b.offsetWidth),c.removeChild(e),a):void 0}}();var T=/[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/.source,U=new RegExp("^(?:([+-])=|)("+T+")([a-z%]*)$","i"),V=["Top","Right","Bottom","Left"],W=function(a,b){return a=b||a,"none"===n.css(a,"display")||!n.contains(a.ownerDocument,a)};function X(a,b,c,d){var e,f=1,g=20,h=d?function(){return d.cur()}:function(){return n.css(a,b,"")},i=h(),j=c&&c[3]||(n.cssNumber[b]?"":"px"),k=(n.cssNumber[b]||"px"!==j&&+i)&&U.exec(n.css(a,b));if(k&&k[3]!==j){j=j||k[3],c=c||[],k=+i||1;do f=f||".5",k/=f,n.style(a,b,k+j);while(f!==(f=h()/i)&&1!==f&&--g)}return c&&(k=+k||+i||0,e=c[1]?k+(c[1]+1)*c[2]:+c[2],d&&(d.unit=j,d.start=k,d.end=e)),e}var Y=function(a,b,c,d,e,f,g){var h=0,i=a.length,j=null==c;if("object"===n.type(c)){e=!0;for(h in c)Y(a,b,h,c[h],!0,f,g)}else if(void 0!==d&&(e=!0,n.isFunction(d)||(g=!0),j&&(g?(b.call(a,d),b=null):(j=b,b=function(a,b,c){return j.call(n(a),c)})),b))for(;i>h;h++)b(a[h],c,g?d:d.call(a[h],h,b(a[h],c)));return e?a:j?b.call(a):i?b(a[0],c):f},Z=/^(?:checkbox|radio)$/i,$=/<([\w:-]+)/,_=/^$|\/(?:java|ecma)script/i,aa=/^\s+/,ba="abbr|article|aside|audio|bdi|canvas|data|datalist|details|dialog|figcaption|figure|footer|header|hgroup|main|mark|meter|nav|output|picture|progress|section|summary|template|time|video";function ca(a){var b=ba.split("|"),c=a.createDocumentFragment();if(c.createElement)while(b.length)c.createElement(b.pop());return c}!function(){var a=d.createElement("div"),b=d.createDocumentFragment(),c=d.createElement("input");a.innerHTML=" <link/><table></table><a href='/a'>a</a><input type='checkbox'/>",l.leadingWhitespace=3===a.firstChild.nodeType,l.tbody=!a.getElementsByTagName("tbody").length,l.htmlSerialize=!!a.getElementsByTagName("link").length,l.html5Clone="<:nav></:nav>"!==d.createElement("nav").cloneNode(!0).outerHTML,c.type="checkbox",c.checked=!0,b.appendChild(c),l.appendChecked=c.checked,a.innerHTML="<textarea>x</textarea>",l.noCloneChecked=!!a.cloneNode(!0).lastChild.defaultValue,b.appendChild(a),c=d.createElement("input"),c.setAttribute("type","radio"),c.setAttribute("checked","checked"),c.setAttribute("name","t"),a.appendChild(c),l.checkClone=a.cloneNode(!0).cloneNode(!0).lastChild.checked,l.noCloneEvent=!!a.addEventListener,a[n.expando]=1,l.attributes=!a.getAttribute(n.expando)}();var da={option:[1,"<select multiple='multiple'>","</select>"],legend:[1,"<fieldset>","</fieldset>"],area:[1,"<map>","</map>"],param:[1,"<object>","</object>"],thead:[1,"<table>","</table>"],tr:[2,"<table><tbody>","</tbody></table>"],col:[2,"<table><tbody></tbody><colgroup>","</colgroup></table>"],td:[3,"<table><tbody><tr>","</tr></tbody></table>"],_default:l.htmlSerialize?[0,"",""]:[1,"X<div>","</div>"]};da.optgroup=da.option,da.tbody=da.tfoot=da.colgroup=da.caption=da.thead,da.th=da.td;function ea(a,b){var c,d,e=0,f="undefined"!=typeof a.getElementsByTagName?a.getElementsByTagName(b||"*"):"undefined"!=typeof a.querySelectorAll?a.querySelectorAll(b||"*"):void 0;if(!f)for(f=[],c=a.childNodes||a;null!=(d=c[e]);e++)!b||n.nodeName(d,b)?f.push(d):n.merge(f,ea(d,b));return void 0===b||b&&n.nodeName(a,b)?n.merge([a],f):f}function fa(a,b){for(var c,d=0;null!=(c=a[d]);d++)n._data(c,"globalEval",!b||n._data(b[d],"globalEval"))}var ga=/<|&#?\w+;/,ha=/<tbody/i;function ia(a){Z.test(a.type)&&(a.defaultChecked=a.checked)}function ja(a,b,c,d,e){for(var f,g,h,i,j,k,m,o=a.length,p=ca(b),q=[],r=0;o>r;r++)if(g=a[r],g||0===g)if("object"===n.type(g))n.merge(q,g.nodeType?[g]:g);else if(ga.test(g)){i=i||p.appendChild(b.createElement("div")),j=($.exec(g)||["",""])[1].toLowerCase(),m=da[j]||da._default,i.innerHTML=m[1]+n.htmlPrefilter(g)+m[2],f=m[0];while(f--)i=i.lastChild;if(!l.leadingWhitespace&&aa.test(g)&&q.push(b.createTextNode(aa.exec(g)[0])),!l.tbody){g="table"!==j||ha.test(g)?"<table>"!==m[1]||ha.test(g)?0:i:i.firstChild,f=g&&g.childNodes.length;while(f--)n.nodeName(k=g.childNodes[f],"tbody")&&!k.childNodes.length&&g.removeChild(k)}n.merge(q,i.childNodes),i.textContent="";while(i.firstChild)i.removeChild(i.firstChild);i=p.lastChild}else q.push(b.createTextNode(g));i&&p.removeChild(i),l.appendChecked||n.grep(ea(q,"input"),ia),r=0;while(g=q[r++])if(d&&n.inArray(g,d)>-1)e&&e.push(g);else if(h=n.contains(g.ownerDocument,g),i=ea(p.appendChild(g),"script"),h&&fa(i),c){f=0;while(g=i[f++])_.test(g.type||"")&&c.push(g)}return i=null,p}!function(){var b,c,e=d.createElement("div");for(b in{submit:!0,change:!0,focusin:!0})c="on"+b,(l[b]=c in a)||(e.setAttribute(c,"t"),l[b]=e.attributes[c].expando===!1);e=null}();var ka=/^(?:input|select|textarea)$/i,la=/^key/,ma=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,na=/^(?:focusinfocus|focusoutblur)$/,oa=/^([^.]*)(?:\.(.+)|)/;function pa(){return!0}function qa(){return!1}function ra(){try{return d.activeElement}catch(a){}}function sa(a,b,c,d,e,f){var g,h;if("object"==typeof b){"string"!=typeof c&&(d=d||c,c=void 0);for(h in b)sa(a,h,c,d,b[h],f);return a}if(null==d&&null==e?(e=c,d=c=void 0):null==e&&("string"==typeof c?(e=d,d=void 0):(e=d,d=c,c=void 0)),e===!1)e=qa;else if(!e)return a;return 1===f&&(g=e,e=function(a){return n().off(a),g.apply(this,arguments)},e.guid=g.guid||(g.guid=n.guid++)),a.each(function(){n.event.add(this,b,e,d,c)})}n.event={global:{},add:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,o,p,q,r=n._data(a);if(r){c.handler&&(i=c,c=i.handler,e=i.selector),c.guid||(c.guid=n.guid++),(g=r.events)||(g=r.events={}),(k=r.handle)||(k=r.handle=function(a){return"undefined"==typeof n||a&&n.event.triggered===a.type?void 0:n.event.dispatch.apply(k.elem,arguments)},k.elem=a),b=(b||"").match(G)||[""],h=b.length;while(h--)f=oa.exec(b[h])||[],o=q=f[1],p=(f[2]||"").split(".").sort(),o&&(j=n.event.special[o]||{},o=(e?j.delegateType:j.bindType)||o,j=n.event.special[o]||{},l=n.extend({type:o,origType:q,data:d,handler:c,guid:c.guid,selector:e,needsContext:e&&n.expr.match.needsContext.test(e),namespace:p.join(".")},i),(m=g[o])||(m=g[o]=[],m.delegateCount=0,j.setup&&j.setup.call(a,d,p,k)!==!1||(a.addEventListener?a.addEventListener(o,k,!1):a.attachEvent&&a.attachEvent("on"+o,k))),j.add&&(j.add.call(a,l),l.handler.guid||(l.handler.guid=c.guid)),e?m.splice(m.delegateCount++,0,l):m.push(l),n.event.global[o]=!0);a=null}},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,o,p,q,r=n.hasData(a)&&n._data(a);if(r&&(k=r.events)){b=(b||"").match(G)||[""],j=b.length;while(j--)if(h=oa.exec(b[j])||[],o=q=h[1],p=(h[2]||"").split(".").sort(),o){l=n.event.special[o]||{},o=(d?l.delegateType:l.bindType)||o,m=k[o]||[],h=h[2]&&new RegExp("(^|\\.)"+p.join("\\.(?:.*\\.|)")+"(\\.|$)"),i=f=m.length;while(f--)g=m[f],!e&&q!==g.origType||c&&c.guid!==g.guid||h&&!h.test(g.namespace)||d&&d!==g.selector&&("**"!==d||!g.selector)||(m.splice(f,1),g.selector&&m.delegateCount--,l.remove&&l.remove.call(a,g));i&&!m.length&&(l.teardown&&l.teardown.call(a,p,r.handle)!==!1||n.removeEvent(a,o,r.handle),delete k[o])}else for(o in k)n.event.remove(a,o+b[j],c,d,!0);n.isEmptyObject(k)&&(delete r.handle,n._removeData(a,"events"))}},trigger:function(b,c,e,f){var g,h,i,j,l,m,o,p=[e||d],q=k.call(b,"type")?b.type:b,r=k.call(b,"namespace")?b.namespace.split("."):[];if(i=m=e=e||d,3!==e.nodeType&&8!==e.nodeType&&!na.test(q+n.event.triggered)&&(q.indexOf(".")>-1&&(r=q.split("."),q=r.shift(),r.sort()),h=q.indexOf(":")<0&&"on"+q,b=b[n.expando]?b:new n.Event(q,"object"==typeof b&&b),b.isTrigger=f?2:3,b.namespace=r.join("."),b.rnamespace=b.namespace?new RegExp("(^|\\.)"+r.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,b.result=void 0,b.target||(b.target=e),c=null==c?[b]:n.makeArray(c,[b]),l=n.event.special[q]||{},f||!l.trigger||l.trigger.apply(e,c)!==!1)){if(!f&&!l.noBubble&&!n.isWindow(e)){for(j=l.delegateType||q,na.test(j+q)||(i=i.parentNode);i;i=i.parentNode)p.push(i),m=i;m===(e.ownerDocument||d)&&p.push(m.defaultView||m.parentWindow||a)}o=0;while((i=p[o++])&&!b.isPropagationStopped())b.type=o>1?j:l.bindType||q,g=(n._data(i,"events")||{})[b.type]&&n._data(i,"handle"),g&&g.apply(i,c),g=h&&i[h],g&&g.apply&&M(i)&&(b.result=g.apply(i,c),b.result===!1&&b.preventDefault());if(b.type=q,!f&&!b.isDefaultPrevented()&&(!l._default||l._default.apply(p.pop(),c)===!1)&&M(e)&&h&&e[q]&&!n.isWindow(e)){m=e[h],m&&(e[h]=null),n.event.triggered=q;try{e[q]()}catch(s){}n.event.triggered=void 0,m&&(e[h]=m)}return b.result}},dispatch:function(a){a=n.event.fix(a);var b,c,d,f,g,h=[],i=e.call(arguments),j=(n._data(this,"events")||{})[a.type]||[],k=n.event.special[a.type]||{};if(i[0]=a,a.delegateTarget=this,!k.preDispatch||k.preDispatch.call(this,a)!==!1){h=n.event.handlers.call(this,a,j),b=0;while((f=h[b++])&&!a.isPropagationStopped()){a.currentTarget=f.elem,c=0;while((g=f.handlers[c++])&&!a.isImmediatePropagationStopped())a.rnamespace&&!a.rnamespace.test(g.namespace)||(a.handleObj=g,a.data=g.data,d=((n.event.special[g.origType]||{}).handle||g.handler).apply(f.elem,i),void 0!==d&&(a.result=d)===!1&&(a.preventDefault(),a.stopPropagation()))}return k.postDispatch&&k.postDispatch.call(this,a),a.result}},handlers:function(a,b){var c,d,e,f,g=[],h=b.delegateCount,i=a.target;if(h&&i.nodeType&&("click"!==a.type||isNaN(a.button)||a.button<1))for(;i!=this;i=i.parentNode||this)if(1===i.nodeType&&(i.disabled!==!0||"click"!==a.type)){for(d=[],c=0;h>c;c++)f=b[c],e=f.selector+" ",void 0===d[e]&&(d[e]=f.needsContext?n(e,this).index(i)>-1:n.find(e,this,null,[i]).length),d[e]&&d.push(f);d.length&&g.push({elem:i,handlers:d})}return h<b.length&&g.push({elem:this,handlers:b.slice(h)}),g},fix:function(a){if(a[n.expando])return a;var b,c,e,f=a.type,g=a,h=this.fixHooks[f];h||(this.fixHooks[f]=h=ma.test(f)?this.mouseHooks:la.test(f)?this.keyHooks:{}),e=h.props?this.props.concat(h.props):this.props,a=new n.Event(g),b=e.length;while(b--)c=e[b],a[c]=g[c];return a.target||(a.target=g.srcElement||d),3===a.target.nodeType&&(a.target=a.target.parentNode),a.metaKey=!!a.metaKey,h.filter?h.filter(a,g):a},props:"altKey bubbles cancelable ctrlKey currentTarget detail eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "),fixHooks:{},keyHooks:{props:"char charCode key keyCode".split(" "),filter:function(a,b){return null==a.which&&(a.which=null!=b.charCode?b.charCode:b.keyCode),a}},mouseHooks:{props:"button buttons clientX clientY fromElement offsetX offsetY pageX pageY screenX screenY toElement".split(" "),filter:function(a,b){var c,e,f,g=b.button,h=b.fromElement;return null==a.pageX&&null!=b.clientX&&(e=a.target.ownerDocument||d,f=e.documentElement,c=e.body,a.pageX=b.clientX+(f&&f.scrollLeft||c&&c.scrollLeft||0)-(f&&f.clientLeft||c&&c.clientLeft||0),a.pageY=b.clientY+(f&&f.scrollTop||c&&c.scrollTop||0)-(f&&f.clientTop||c&&c.clientTop||0)),!a.relatedTarget&&h&&(a.relatedTarget=h===a.target?b.toElement:h),a.which||void 0===g||(a.which=1&g?1:2&g?3:4&g?2:0),a}},special:{load:{noBubble:!0},focus:{trigger:function(){if(this!==ra()&&this.focus)try{return this.focus(),!1}catch(a){}},delegateType:"focusin"},blur:{trigger:function(){return this===ra()&&this.blur?(this.blur(),!1):void 0},delegateType:"focusout"},click:{trigger:function(){return n.nodeName(this,"input")&&"checkbox"===this.type&&this.click?(this.click(),!1):void 0},_default:function(a){return n.nodeName(a.target,"a")}},beforeunload:{postDispatch:function(a){void 0!==a.result&&a.originalEvent&&(a.originalEvent.returnValue=a.result)}}},simulate:function(a,b,c){var d=n.extend(new n.Event,c,{type:a,isSimulated:!0});n.event.trigger(d,null,b),d.isDefaultPrevented()&&c.preventDefault()}},n.removeEvent=d.removeEventListener?function(a,b,c){a.removeEventListener&&a.removeEventListener(b,c)}:function(a,b,c){var d="on"+b;a.detachEvent&&("undefined"==typeof a[d]&&(a[d]=null),a.detachEvent(d,c))},n.Event=function(a,b){return this instanceof n.Event?(a&&a.type?(this.originalEvent=a,this.type=a.type,this.isDefaultPrevented=a.defaultPrevented||void 0===a.defaultPrevented&&a.returnValue===!1?pa:qa):this.type=a,b&&n.extend(this,b),this.timeStamp=a&&a.timeStamp||n.now(),void(this[n.expando]=!0)):new n.Event(a,b)},n.Event.prototype={constructor:n.Event,isDefaultPrevented:qa,isPropagationStopped:qa,isImmediatePropagationStopped:qa,preventDefault:function(){var a=this.originalEvent;this.isDefaultPrevented=pa,a&&(a.preventDefault?a.preventDefault():a.returnValue=!1)},stopPropagation:function(){var a=this.originalEvent;this.isPropagationStopped=pa,a&&!this.isSimulated&&(a.stopPropagation&&a.stopPropagation(),a.cancelBubble=!0)},stopImmediatePropagation:function(){var a=this.originalEvent;this.isImmediatePropagationStopped=pa,a&&a.stopImmediatePropagation&&a.stopImmediatePropagation(),this.stopPropagation()}},n.each({mouseenter:"mouseover",mouseleave:"mouseout",pointerenter:"pointerover",pointerleave:"pointerout"},function(a,b){n.event.special[a]={delegateType:b,bindType:b,handle:function(a){var c,d=this,e=a.relatedTarget,f=a.handleObj;return e&&(e===d||n.contains(d,e))||(a.type=f.origType,c=f.handler.apply(this,arguments),a.type=b),c}}}),l.submit||(n.event.special.submit={setup:function(){return n.nodeName(this,"form")?!1:void n.event.add(this,"click._submit keypress._submit",function(a){var b=a.target,c=n.nodeName(b,"input")||n.nodeName(b,"button")?n.prop(b,"form"):void 0;c&&!n._data(c,"submit")&&(n.event.add(c,"submit._submit",function(a){a._submitBubble=!0}),n._data(c,"submit",!0))})},postDispatch:function(a){a._submitBubble&&(delete a._submitBubble,this.parentNode&&!a.isTrigger&&n.event.simulate("submit",this.parentNode,a))},teardown:function(){return n.nodeName(this,"form")?!1:void n.event.remove(this,"._submit")}}),l.change||(n.event.special.change={setup:function(){return ka.test(this.nodeName)?("checkbox"!==this.type&&"radio"!==this.type||(n.event.add(this,"propertychange._change",function(a){"checked"===a.originalEvent.propertyName&&(this._justChanged=!0)}),n.event.add(this,"click._change",function(a){this._justChanged&&!a.isTrigger&&(this._justChanged=!1),n.event.simulate("change",this,a)})),!1):void n.event.add(this,"beforeactivate._change",function(a){var b=a.target;ka.test(b.nodeName)&&!n._data(b,"change")&&(n.event.add(b,"change._change",function(a){!this.parentNode||a.isSimulated||a.isTrigger||n.event.simulate("change",this.parentNode,a)}),n._data(b,"change",!0))})},handle:function(a){var b=a.target;return this!==b||a.isSimulated||a.isTrigger||"radio"!==b.type&&"checkbox"!==b.type?a.handleObj.handler.apply(this,arguments):void 0},teardown:function(){return n.event.remove(this,"._change"),!ka.test(this.nodeName)}}),l.focusin||n.each({focus:"focusin",blur:"focusout"},function(a,b){var c=function(a){n.event.simulate(b,a.target,n.event.fix(a))};n.event.special[b]={setup:function(){var d=this.ownerDocument||this,e=n._data(d,b);e||d.addEventListener(a,c,!0),n._data(d,b,(e||0)+1)},teardown:function(){var d=this.ownerDocument||this,e=n._data(d,b)-1;e?n._data(d,b,e):(d.removeEventListener(a,c,!0),n._removeData(d,b))}}}),n.fn.extend({on:function(a,b,c,d){return sa(this,a,b,c,d)},one:function(a,b,c,d){return sa(this,a,b,c,d,1)},off:function(a,b,c){var d,e;if(a&&a.preventDefault&&a.handleObj)return d=a.handleObj,n(a.delegateTarget).off(d.namespace?d.origType+"."+d.namespace:d.origType,d.selector,d.handler),this;if("object"==typeof a){for(e in a)this.off(e,b,a[e]);return this}return b!==!1&&"function"!=typeof b||(c=b,b=void 0),c===!1&&(c=qa),this.each(function(){n.event.remove(this,a,c,b)})},trigger:function(a,b){return this.each(function(){n.event.trigger(a,b,this)})},triggerHandler:function(a,b){var c=this[0];return c?n.event.trigger(a,b,c,!0):void 0}});var ta=/ jQuery\d+="(?:null|\d+)"/g,ua=new RegExp("<(?:"+ba+")[\\s/>]","i"),va=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:-]+)[^>]*)\/>/gi,wa=/<script|<style|<link/i,xa=/checked\s*(?:[^=]|=\s*.checked.)/i,ya=/^true\/(.*)/,za=/^\s*<!(?:\[CDATA\[|--)|(?:\]\]|--)>\s*$/g,Aa=ca(d),Ba=Aa.appendChild(d.createElement("div"));function Ca(a,b){return n.nodeName(a,"table")&&n.nodeName(11!==b.nodeType?b:b.firstChild,"tr")?a.getElementsByTagName("tbody")[0]||a.appendChild(a.ownerDocument.createElement("tbody")):a}function Da(a){return a.type=(null!==n.find.attr(a,"type"))+"/"+a.type,a}function Ea(a){var b=ya.exec(a.type);return b?a.type=b[1]:a.removeAttribute("type"),a}function Fa(a,b){if(1===b.nodeType&&n.hasData(a)){var c,d,e,f=n._data(a),g=n._data(b,f),h=f.events;if(h){delete g.handle,g.events={};for(c in h)for(d=0,e=h[c].length;e>d;d++)n.event.add(b,c,h[c][d])}g.data&&(g.data=n.extend({},g.data))}}function Ga(a,b){var c,d,e;if(1===b.nodeType){if(c=b.nodeName.toLowerCase(),!l.noCloneEvent&&b[n.expando]){e=n._data(b);for(d in e.events)n.removeEvent(b,d,e.handle);b.removeAttribute(n.expando)}"script"===c&&b.text!==a.text?(Da(b).text=a.text,Ea(b)):"object"===c?(b.parentNode&&(b.outerHTML=a.outerHTML),l.html5Clone&&a.innerHTML&&!n.trim(b.innerHTML)&&(b.innerHTML=a.innerHTML)):"input"===c&&Z.test(a.type)?(b.defaultChecked=b.checked=a.checked,b.value!==a.value&&(b.value=a.value)):"option"===c?b.defaultSelected=b.selected=a.defaultSelected:"input"!==c&&"textarea"!==c||(b.defaultValue=a.defaultValue)}}function Ha(a,b,c,d){b=f.apply([],b);var e,g,h,i,j,k,m=0,o=a.length,p=o-1,q=b[0],r=n.isFunction(q);if(r||o>1&&"string"==typeof q&&!l.checkClone&&xa.test(q))return a.each(function(e){var f=a.eq(e);r&&(b[0]=q.call(this,e,f.html())),Ha(f,b,c,d)});if(o&&(k=ja(b,a[0].ownerDocument,!1,a,d),e=k.firstChild,1===k.childNodes.length&&(k=e),e||d)){for(i=n.map(ea(k,"script"),Da),h=i.length;o>m;m++)g=k,m!==p&&(g=n.clone(g,!0,!0),h&&n.merge(i,ea(g,"script"))),c.call(a[m],g,m);if(h)for(j=i[i.length-1].ownerDocument,n.map(i,Ea),m=0;h>m;m++)g=i[m],_.test(g.type||"")&&!n._data(g,"globalEval")&&n.contains(j,g)&&(g.src?n._evalUrl&&n._evalUrl(g.src):n.globalEval((g.text||g.textContent||g.innerHTML||"").replace(za,"")));k=e=null}return a}function Ia(a,b,c){for(var d,e=b?n.filter(b,a):a,f=0;null!=(d=e[f]);f++)c||1!==d.nodeType||n.cleanData(ea(d)),d.parentNode&&(c&&n.contains(d.ownerDocument,d)&&fa(ea(d,"script")),d.parentNode.removeChild(d));return a}n.extend({htmlPrefilter:function(a){return a.replace(va,"<$1></$2>")},clone:function(a,b,c){var d,e,f,g,h,i=n.contains(a.ownerDocument,a);if(l.html5Clone||n.isXMLDoc(a)||!ua.test("<"+a.nodeName+">")?f=a.cloneNode(!0):(Ba.innerHTML=a.outerHTML,Ba.removeChild(f=Ba.firstChild)),!(l.noCloneEvent&&l.noCloneChecked||1!==a.nodeType&&11!==a.nodeType||n.isXMLDoc(a)))for(d=ea(f),h=ea(a),g=0;null!=(e=h[g]);++g)d[g]&&Ga(e,d[g]);if(b)if(c)for(h=h||ea(a),d=d||ea(f),g=0;null!=(e=h[g]);g++)Fa(e,d[g]);else Fa(a,f);return d=ea(f,"script"),d.length>0&&fa(d,!i&&ea(a,"script")),d=h=e=null,f},cleanData:function(a,b){for(var d,e,f,g,h=0,i=n.expando,j=n.cache,k=l.attributes,m=n.event.special;null!=(d=a[h]);h++)if((b||M(d))&&(f=d[i],g=f&&j[f])){if(g.events)for(e in g.events)m[e]?n.event.remove(d,e):n.removeEvent(d,e,g.handle);j[f]&&(delete j[f],k||"undefined"==typeof d.removeAttribute?d[i]=void 0:d.removeAttribute(i),c.push(f))}}}),n.fn.extend({domManip:Ha,detach:function(a){return Ia(this,a,!0)},remove:function(a){return Ia(this,a)},text:function(a){return Y(this,function(a){return void 0===a?n.text(this):this.empty().append((this[0]&&this[0].ownerDocument||d).createTextNode(a))},null,a,arguments.length)},append:function(){return Ha(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=Ca(this,a);b.appendChild(a)}})},prepend:function(){return Ha(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=Ca(this,a);b.insertBefore(a,b.firstChild)}})},before:function(){return Ha(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this)})},after:function(){return Ha(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this.nextSibling)})},empty:function(){for(var a,b=0;null!=(a=this[b]);b++){1===a.nodeType&&n.cleanData(ea(a,!1));while(a.firstChild)a.removeChild(a.firstChild);a.options&&n.nodeName(a,"select")&&(a.options.length=0)}return this},clone:function(a,b){return a=null==a?!1:a,b=null==b?a:b,this.map(function(){return n.clone(this,a,b)})},html:function(a){return Y(this,function(a){var b=this[0]||{},c=0,d=this.length;if(void 0===a)return 1===b.nodeType?b.innerHTML.replace(ta,""):void 0;if("string"==typeof a&&!wa.test(a)&&(l.htmlSerialize||!ua.test(a))&&(l.leadingWhitespace||!aa.test(a))&&!da[($.exec(a)||["",""])[1].toLowerCase()]){a=n.htmlPrefilter(a);try{for(;d>c;c++)b=this[c]||{},1===b.nodeType&&(n.cleanData(ea(b,!1)),b.innerHTML=a);b=0}catch(e){}}b&&this.empty().append(a)},null,a,arguments.length)},replaceWith:function(){var a=[];return Ha(this,arguments,function(b){var c=this.parentNode;n.inArray(this,a)<0&&(n.cleanData(ea(this)),c&&c.replaceChild(b,this))},a)}}),n.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){n.fn[a]=function(a){for(var c,d=0,e=[],f=n(a),h=f.length-1;h>=d;d++)c=d===h?this:this.clone(!0),n(f[d])[b](c),g.apply(e,c.get());return this.pushStack(e)}});var Ja,Ka={HTML:"block",BODY:"block"};function La(a,b){var c=n(b.createElement(a)).appendTo(b.body),d=n.css(c[0],"display");return c.detach(),d}function Ma(a){var b=d,c=Ka[a];return c||(c=La(a,b),"none"!==c&&c||(Ja=(Ja||n("<iframe frameborder='0' width='0' height='0'/>")).appendTo(b.documentElement),b=(Ja[0].contentWindow||Ja[0].contentDocument).document,b.write(),b.close(),c=La(a,b),Ja.detach()),Ka[a]=c),c}var Na=/^margin/,Oa=new RegExp("^("+T+")(?!px)[a-z%]+$","i"),Pa=function(a,b,c,d){var e,f,g={};for(f in b)g[f]=a.style[f],a.style[f]=b[f];e=c.apply(a,d||[]);for(f in b)a.style[f]=g[f];return e},Qa=d.documentElement;!function(){var b,c,e,f,g,h,i=d.createElement("div"),j=d.createElement("div");if(j.style){j.style.cssText="float:left;opacity:.5",l.opacity="0.5"===j.style.opacity,l.cssFloat=!!j.style.cssFloat,j.style.backgroundClip="content-box",j.cloneNode(!0).style.backgroundClip="",l.clearCloneStyle="content-box"===j.style.backgroundClip,i=d.createElement("div"),i.style.cssText="border:0;width:8px;height:0;top:0;left:-9999px;padding:0;margin-top:1px;position:absolute",j.innerHTML="",i.appendChild(j),l.boxSizing=""===j.style.boxSizing||""===j.style.MozBoxSizing||""===j.style.WebkitBoxSizing,n.extend(l,{reliableHiddenOffsets:function(){return null==b&&k(),f},boxSizingReliable:function(){return null==b&&k(),e},pixelMarginRight:function(){return null==b&&k(),c},pixelPosition:function(){return null==b&&k(),b},reliableMarginRight:function(){return null==b&&k(),g},reliableMarginLeft:function(){return null==b&&k(),h}});function k(){var k,l,m=d.documentElement;m.appendChild(i),j.style.cssText="-webkit-box-sizing:border-box;box-sizing:border-box;position:relative;display:block;margin:auto;border:1px;padding:1px;top:1%;width:50%",b=e=h=!1,c=g=!0,a.getComputedStyle&&(l=a.getComputedStyle(j),b="1%"!==(l||{}).top,h="2px"===(l||{}).marginLeft,e="4px"===(l||{width:"4px"}).width,j.style.marginRight="50%",c="4px"===(l||{marginRight:"4px"}).marginRight,k=j.appendChild(d.createElement("div")),k.style.cssText=j.style.cssText="-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;display:block;margin:0;border:0;padding:0",k.style.marginRight=k.style.width="0",j.style.width="1px",g=!parseFloat((a.getComputedStyle(k)||{}).marginRight),j.removeChild(k)),j.style.display="none",f=0===j.getClientRects().length,f&&(j.style.display="",j.innerHTML="<table><tr><td></td><td>t</td></tr></table>",j.childNodes[0].style.borderCollapse="separate",k=j.getElementsByTagName("td"),k[0].style.cssText="margin:0;border:0;padding:0;display:none",f=0===k[0].offsetHeight,f&&(k[0].style.display="",k[1].style.display="none",f=0===k[0].offsetHeight)),m.removeChild(i)}}}();var Ra,Sa,Ta=/^(top|right|bottom|left)$/;a.getComputedStyle?(Ra=function(b){var c=b.ownerDocument.defaultView;return c&&c.opener||(c=a),c.getComputedStyle(b)},Sa=function(a,b,c){var d,e,f,g,h=a.style;return c=c||Ra(a),g=c?c.getPropertyValue(b)||c[b]:void 0,""!==g&&void 0!==g||n.contains(a.ownerDocument,a)||(g=n.style(a,b)),c&&!l.pixelMarginRight()&&Oa.test(g)&&Na.test(b)&&(d=h.width,e=h.minWidth,f=h.maxWidth,h.minWidth=h.maxWidth=h.width=g,g=c.width,h.width=d,h.minWidth=e,h.maxWidth=f),void 0===g?g:g+""}):Qa.currentStyle&&(Ra=function(a){return a.currentStyle},Sa=function(a,b,c){var d,e,f,g,h=a.style;return c=c||Ra(a),g=c?c[b]:void 0,null==g&&h&&h[b]&&(g=h[b]),Oa.test(g)&&!Ta.test(b)&&(d=h.left,e=a.runtimeStyle,f=e&&e.left,f&&(e.left=a.currentStyle.left),h.left="fontSize"===b?"1em":g,g=h.pixelLeft+"px",h.left=d,f&&(e.left=f)),void 0===g?g:g+""||"auto"});function Ua(a,b){return{get:function(){return a()?void delete this.get:(this.get=b).apply(this,arguments)}}}var Va=/alpha\([^)]*\)/i,Wa=/opacity\s*=\s*([^)]*)/i,Xa=/^(none|table(?!-c[ea]).+)/,Ya=new RegExp("^("+T+")(.*)$","i"),Za={position:"absolute",visibility:"hidden",display:"block"},$a={letterSpacing:"0",fontWeight:"400"},_a=["Webkit","O","Moz","ms"],ab=d.createElement("div").style;function bb(a){if(a in ab)return a;var b=a.charAt(0).toUpperCase()+a.slice(1),c=_a.length;while(c--)if(a=_a[c]+b,a in ab)return a}function cb(a,b){for(var c,d,e,f=[],g=0,h=a.length;h>g;g++)d=a[g],d.style&&(f[g]=n._data(d,"olddisplay"),c=d.style.display,b?(f[g]||"none"!==c||(d.style.display=""),""===d.style.display&&W(d)&&(f[g]=n._data(d,"olddisplay",Ma(d.nodeName)))):(e=W(d),(c&&"none"!==c||!e)&&n._data(d,"olddisplay",e?c:n.css(d,"display"))));for(g=0;h>g;g++)d=a[g],d.style&&(b&&"none"!==d.style.display&&""!==d.style.display||(d.style.display=b?f[g]||"":"none"));return a}function db(a,b,c){var d=Ya.exec(b);return d?Math.max(0,d[1]-(c||0))+(d[2]||"px"):b}function eb(a,b,c,d,e){for(var f=c===(d?"border":"content")?4:"width"===b?1:0,g=0;4>f;f+=2)"margin"===c&&(g+=n.css(a,c+V[f],!0,e)),d?("content"===c&&(g-=n.css(a,"padding"+V[f],!0,e)),"margin"!==c&&(g-=n.css(a,"border"+V[f]+"Width",!0,e))):(g+=n.css(a,"padding"+V[f],!0,e),"padding"!==c&&(g+=n.css(a,"border"+V[f]+"Width",!0,e)));return g}function fb(a,b,c){var d=!0,e="width"===b?a.offsetWidth:a.offsetHeight,f=Ra(a),g=l.boxSizing&&"border-box"===n.css(a,"boxSizing",!1,f);if(0>=e||null==e){if(e=Sa(a,b,f),(0>e||null==e)&&(e=a.style[b]),Oa.test(e))return e;d=g&&(l.boxSizingReliable()||e===a.style[b]),e=parseFloat(e)||0}return e+eb(a,b,c||(g?"border":"content"),d,f)+"px"}n.extend({cssHooks:{opacity:{get:function(a,b){if(b){var c=Sa(a,"opacity");return""===c?"1":c}}}},cssNumber:{animationIterationCount:!0,columnCount:!0,fillOpacity:!0,flexGrow:!0,flexShrink:!0,fontWeight:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{"float":l.cssFloat?"cssFloat":"styleFloat"},style:function(a,b,c,d){if(a&&3!==a.nodeType&&8!==a.nodeType&&a.style){var e,f,g,h=n.camelCase(b),i=a.style;if(b=n.cssProps[h]||(n.cssProps[h]=bb(h)||h),g=n.cssHooks[b]||n.cssHooks[h],void 0===c)return g&&"get"in g&&void 0!==(e=g.get(a,!1,d))?e:i[b];if(f=typeof c,"string"===f&&(e=U.exec(c))&&e[1]&&(c=X(a,b,e),f="number"),null!=c&&c===c&&("number"===f&&(c+=e&&e[3]||(n.cssNumber[h]?"":"px")),l.clearCloneStyle||""!==c||0!==b.indexOf("background")||(i[b]="inherit"),!(g&&"set"in g&&void 0===(c=g.set(a,c,d)))))try{i[b]=c}catch(j){}}},css:function(a,b,c,d){var e,f,g,h=n.camelCase(b);return b=n.cssProps[h]||(n.cssProps[h]=bb(h)||h),g=n.cssHooks[b]||n.cssHooks[h],g&&"get"in g&&(f=g.get(a,!0,c)),void 0===f&&(f=Sa(a,b,d)),"normal"===f&&b in $a&&(f=$a[b]),""===c||c?(e=parseFloat(f),c===!0||isFinite(e)?e||0:f):f}}),n.each(["height","width"],function(a,b){n.cssHooks[b]={get:function(a,c,d){return c?Xa.test(n.css(a,"display"))&&0===a.offsetWidth?Pa(a,Za,function(){return fb(a,b,d)}):fb(a,b,d):void 0},set:function(a,c,d){var e=d&&Ra(a);return db(a,c,d?eb(a,b,d,l.boxSizing&&"border-box"===n.css(a,"boxSizing",!1,e),e):0)}}}),l.opacity||(n.cssHooks.opacity={get:function(a,b){return Wa.test((b&&a.currentStyle?a.currentStyle.filter:a.style.filter)||"")?.01*parseFloat(RegExp.$1)+"":b?"1":""},set:function(a,b){var c=a.style,d=a.currentStyle,e=n.isNumeric(b)?"alpha(opacity="+100*b+")":"",f=d&&d.filter||c.filter||"";c.zoom=1,(b>=1||""===b)&&""===n.trim(f.replace(Va,""))&&c.removeAttribute&&(c.removeAttribute("filter"),""===b||d&&!d.filter)||(c.filter=Va.test(f)?f.replace(Va,e):f+" "+e)}}),n.cssHooks.marginRight=Ua(l.reliableMarginRight,function(a,b){return b?Pa(a,{display:"inline-block"},Sa,[a,"marginRight"]):void 0}),n.cssHooks.marginLeft=Ua(l.reliableMarginLeft,function(a,b){return b?(parseFloat(Sa(a,"marginLeft"))||(n.contains(a.ownerDocument,a)?a.getBoundingClientRect().left-Pa(a,{ -marginLeft:0},function(){return a.getBoundingClientRect().left}):0))+"px":void 0}),n.each({margin:"",padding:"",border:"Width"},function(a,b){n.cssHooks[a+b]={expand:function(c){for(var d=0,e={},f="string"==typeof c?c.split(" "):[c];4>d;d++)e[a+V[d]+b]=f[d]||f[d-2]||f[0];return e}},Na.test(a)||(n.cssHooks[a+b].set=db)}),n.fn.extend({css:function(a,b){return Y(this,function(a,b,c){var d,e,f={},g=0;if(n.isArray(b)){for(d=Ra(a),e=b.length;e>g;g++)f[b[g]]=n.css(a,b[g],!1,d);return f}return void 0!==c?n.style(a,b,c):n.css(a,b)},a,b,arguments.length>1)},show:function(){return cb(this,!0)},hide:function(){return cb(this)},toggle:function(a){return"boolean"==typeof a?a?this.show():this.hide():this.each(function(){W(this)?n(this).show():n(this).hide()})}});function gb(a,b,c,d,e){return new gb.prototype.init(a,b,c,d,e)}n.Tween=gb,gb.prototype={constructor:gb,init:function(a,b,c,d,e,f){this.elem=a,this.prop=c,this.easing=e||n.easing._default,this.options=b,this.start=this.now=this.cur(),this.end=d,this.unit=f||(n.cssNumber[c]?"":"px")},cur:function(){var a=gb.propHooks[this.prop];return a&&a.get?a.get(this):gb.propHooks._default.get(this)},run:function(a){var b,c=gb.propHooks[this.prop];return this.options.duration?this.pos=b=n.easing[this.easing](a,this.options.duration*a,0,1,this.options.duration):this.pos=b=a,this.now=(this.end-this.start)*b+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),c&&c.set?c.set(this):gb.propHooks._default.set(this),this}},gb.prototype.init.prototype=gb.prototype,gb.propHooks={_default:{get:function(a){var b;return 1!==a.elem.nodeType||null!=a.elem[a.prop]&&null==a.elem.style[a.prop]?a.elem[a.prop]:(b=n.css(a.elem,a.prop,""),b&&"auto"!==b?b:0)},set:function(a){n.fx.step[a.prop]?n.fx.step[a.prop](a):1!==a.elem.nodeType||null==a.elem.style[n.cssProps[a.prop]]&&!n.cssHooks[a.prop]?a.elem[a.prop]=a.now:n.style(a.elem,a.prop,a.now+a.unit)}}},gb.propHooks.scrollTop=gb.propHooks.scrollLeft={set:function(a){a.elem.nodeType&&a.elem.parentNode&&(a.elem[a.prop]=a.now)}},n.easing={linear:function(a){return a},swing:function(a){return.5-Math.cos(a*Math.PI)/2},_default:"swing"},n.fx=gb.prototype.init,n.fx.step={};var hb,ib,jb=/^(?:toggle|show|hide)$/,kb=/queueHooks$/;function lb(){return a.setTimeout(function(){hb=void 0}),hb=n.now()}function mb(a,b){var c,d={height:a},e=0;for(b=b?1:0;4>e;e+=2-b)c=V[e],d["margin"+c]=d["padding"+c]=a;return b&&(d.opacity=d.width=a),d}function nb(a,b,c){for(var d,e=(qb.tweeners[b]||[]).concat(qb.tweeners["*"]),f=0,g=e.length;g>f;f++)if(d=e[f].call(c,b,a))return d}function ob(a,b,c){var d,e,f,g,h,i,j,k,m=this,o={},p=a.style,q=a.nodeType&&W(a),r=n._data(a,"fxshow");c.queue||(h=n._queueHooks(a,"fx"),null==h.unqueued&&(h.unqueued=0,i=h.empty.fire,h.empty.fire=function(){h.unqueued||i()}),h.unqueued++,m.always(function(){m.always(function(){h.unqueued--,n.queue(a,"fx").length||h.empty.fire()})})),1===a.nodeType&&("height"in b||"width"in b)&&(c.overflow=[p.overflow,p.overflowX,p.overflowY],j=n.css(a,"display"),k="none"===j?n._data(a,"olddisplay")||Ma(a.nodeName):j,"inline"===k&&"none"===n.css(a,"float")&&(l.inlineBlockNeedsLayout&&"inline"!==Ma(a.nodeName)?p.zoom=1:p.display="inline-block")),c.overflow&&(p.overflow="hidden",l.shrinkWrapBlocks()||m.always(function(){p.overflow=c.overflow[0],p.overflowX=c.overflow[1],p.overflowY=c.overflow[2]}));for(d in b)if(e=b[d],jb.exec(e)){if(delete b[d],f=f||"toggle"===e,e===(q?"hide":"show")){if("show"!==e||!r||void 0===r[d])continue;q=!0}o[d]=r&&r[d]||n.style(a,d)}else j=void 0;if(n.isEmptyObject(o))"inline"===("none"===j?Ma(a.nodeName):j)&&(p.display=j);else{r?"hidden"in r&&(q=r.hidden):r=n._data(a,"fxshow",{}),f&&(r.hidden=!q),q?n(a).show():m.done(function(){n(a).hide()}),m.done(function(){var b;n._removeData(a,"fxshow");for(b in o)n.style(a,b,o[b])});for(d in o)g=nb(q?r[d]:0,d,m),d in r||(r[d]=g.start,q&&(g.end=g.start,g.start="width"===d||"height"===d?1:0))}}function pb(a,b){var c,d,e,f,g;for(c in a)if(d=n.camelCase(c),e=b[d],f=a[c],n.isArray(f)&&(e=f[1],f=a[c]=f[0]),c!==d&&(a[d]=f,delete a[c]),g=n.cssHooks[d],g&&"expand"in g){f=g.expand(f),delete a[d];for(c in f)c in a||(a[c]=f[c],b[c]=e)}else b[d]=e}function qb(a,b,c){var d,e,f=0,g=qb.prefilters.length,h=n.Deferred().always(function(){delete i.elem}),i=function(){if(e)return!1;for(var b=hb||lb(),c=Math.max(0,j.startTime+j.duration-b),d=c/j.duration||0,f=1-d,g=0,i=j.tweens.length;i>g;g++)j.tweens[g].run(f);return h.notifyWith(a,[j,f,c]),1>f&&i?c:(h.resolveWith(a,[j]),!1)},j=h.promise({elem:a,props:n.extend({},b),opts:n.extend(!0,{specialEasing:{},easing:n.easing._default},c),originalProperties:b,originalOptions:c,startTime:hb||lb(),duration:c.duration,tweens:[],createTween:function(b,c){var d=n.Tween(a,j.opts,b,c,j.opts.specialEasing[b]||j.opts.easing);return j.tweens.push(d),d},stop:function(b){var c=0,d=b?j.tweens.length:0;if(e)return this;for(e=!0;d>c;c++)j.tweens[c].run(1);return b?(h.notifyWith(a,[j,1,0]),h.resolveWith(a,[j,b])):h.rejectWith(a,[j,b]),this}}),k=j.props;for(pb(k,j.opts.specialEasing);g>f;f++)if(d=qb.prefilters[f].call(j,a,k,j.opts))return n.isFunction(d.stop)&&(n._queueHooks(j.elem,j.opts.queue).stop=n.proxy(d.stop,d)),d;return n.map(k,nb,j),n.isFunction(j.opts.start)&&j.opts.start.call(a,j),n.fx.timer(n.extend(i,{elem:a,anim:j,queue:j.opts.queue})),j.progress(j.opts.progress).done(j.opts.done,j.opts.complete).fail(j.opts.fail).always(j.opts.always)}n.Animation=n.extend(qb,{tweeners:{"*":[function(a,b){var c=this.createTween(a,b);return X(c.elem,a,U.exec(b),c),c}]},tweener:function(a,b){n.isFunction(a)?(b=a,a=["*"]):a=a.match(G);for(var c,d=0,e=a.length;e>d;d++)c=a[d],qb.tweeners[c]=qb.tweeners[c]||[],qb.tweeners[c].unshift(b)},prefilters:[ob],prefilter:function(a,b){b?qb.prefilters.unshift(a):qb.prefilters.push(a)}}),n.speed=function(a,b,c){var d=a&&"object"==typeof a?n.extend({},a):{complete:c||!c&&b||n.isFunction(a)&&a,duration:a,easing:c&&b||b&&!n.isFunction(b)&&b};return d.duration=n.fx.off?0:"number"==typeof d.duration?d.duration:d.duration in n.fx.speeds?n.fx.speeds[d.duration]:n.fx.speeds._default,null!=d.queue&&d.queue!==!0||(d.queue="fx"),d.old=d.complete,d.complete=function(){n.isFunction(d.old)&&d.old.call(this),d.queue&&n.dequeue(this,d.queue)},d},n.fn.extend({fadeTo:function(a,b,c,d){return this.filter(W).css("opacity",0).show().end().animate({opacity:b},a,c,d)},animate:function(a,b,c,d){var e=n.isEmptyObject(a),f=n.speed(b,c,d),g=function(){var b=qb(this,n.extend({},a),f);(e||n._data(this,"finish"))&&b.stop(!0)};return g.finish=g,e||f.queue===!1?this.each(g):this.queue(f.queue,g)},stop:function(a,b,c){var d=function(a){var b=a.stop;delete a.stop,b(c)};return"string"!=typeof a&&(c=b,b=a,a=void 0),b&&a!==!1&&this.queue(a||"fx",[]),this.each(function(){var b=!0,e=null!=a&&a+"queueHooks",f=n.timers,g=n._data(this);if(e)g[e]&&g[e].stop&&d(g[e]);else for(e in g)g[e]&&g[e].stop&&kb.test(e)&&d(g[e]);for(e=f.length;e--;)f[e].elem!==this||null!=a&&f[e].queue!==a||(f[e].anim.stop(c),b=!1,f.splice(e,1));!b&&c||n.dequeue(this,a)})},finish:function(a){return a!==!1&&(a=a||"fx"),this.each(function(){var b,c=n._data(this),d=c[a+"queue"],e=c[a+"queueHooks"],f=n.timers,g=d?d.length:0;for(c.finish=!0,n.queue(this,a,[]),e&&e.stop&&e.stop.call(this,!0),b=f.length;b--;)f[b].elem===this&&f[b].queue===a&&(f[b].anim.stop(!0),f.splice(b,1));for(b=0;g>b;b++)d[b]&&d[b].finish&&d[b].finish.call(this);delete c.finish})}}),n.each(["toggle","show","hide"],function(a,b){var c=n.fn[b];n.fn[b]=function(a,d,e){return null==a||"boolean"==typeof a?c.apply(this,arguments):this.animate(mb(b,!0),a,d,e)}}),n.each({slideDown:mb("show"),slideUp:mb("hide"),slideToggle:mb("toggle"),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"},fadeToggle:{opacity:"toggle"}},function(a,b){n.fn[a]=function(a,c,d){return this.animate(b,a,c,d)}}),n.timers=[],n.fx.tick=function(){var a,b=n.timers,c=0;for(hb=n.now();c<b.length;c++)a=b[c],a()||b[c]!==a||b.splice(c--,1);b.length||n.fx.stop(),hb=void 0},n.fx.timer=function(a){n.timers.push(a),a()?n.fx.start():n.timers.pop()},n.fx.interval=13,n.fx.start=function(){ib||(ib=a.setInterval(n.fx.tick,n.fx.interval))},n.fx.stop=function(){a.clearInterval(ib),ib=null},n.fx.speeds={slow:600,fast:200,_default:400},n.fn.delay=function(b,c){return b=n.fx?n.fx.speeds[b]||b:b,c=c||"fx",this.queue(c,function(c,d){var e=a.setTimeout(c,b);d.stop=function(){a.clearTimeout(e)}})},function(){var a,b=d.createElement("input"),c=d.createElement("div"),e=d.createElement("select"),f=e.appendChild(d.createElement("option"));c=d.createElement("div"),c.setAttribute("className","t"),c.innerHTML=" <link/><table></table><a href='/a'>a</a><input type='checkbox'/>",a=c.getElementsByTagName("a")[0],b.setAttribute("type","checkbox"),c.appendChild(b),a=c.getElementsByTagName("a")[0],a.style.cssText="top:1px",l.getSetAttribute="t"!==c.className,l.style=/top/.test(a.getAttribute("style")),l.hrefNormalized="/a"===a.getAttribute("href"),l.checkOn=!!b.value,l.optSelected=f.selected,l.enctype=!!d.createElement("form").enctype,e.disabled=!0,l.optDisabled=!f.disabled,b=d.createElement("input"),b.setAttribute("value",""),l.input=""===b.getAttribute("value"),b.value="t",b.setAttribute("type","radio"),l.radioValue="t"===b.value}();var rb=/\r/g,sb=/[\x20\t\r\n\f]+/g;n.fn.extend({val:function(a){var b,c,d,e=this[0];{if(arguments.length)return d=n.isFunction(a),this.each(function(c){var e;1===this.nodeType&&(e=d?a.call(this,c,n(this).val()):a,null==e?e="":"number"==typeof e?e+="":n.isArray(e)&&(e=n.map(e,function(a){return null==a?"":a+""})),b=n.valHooks[this.type]||n.valHooks[this.nodeName.toLowerCase()],b&&"set"in b&&void 0!==b.set(this,e,"value")||(this.value=e))});if(e)return b=n.valHooks[e.type]||n.valHooks[e.nodeName.toLowerCase()],b&&"get"in b&&void 0!==(c=b.get(e,"value"))?c:(c=e.value,"string"==typeof c?c.replace(rb,""):null==c?"":c)}}}),n.extend({valHooks:{option:{get:function(a){var b=n.find.attr(a,"value");return null!=b?b:n.trim(n.text(a)).replace(sb," ")}},select:{get:function(a){for(var b,c,d=a.options,e=a.selectedIndex,f="select-one"===a.type||0>e,g=f?null:[],h=f?e+1:d.length,i=0>e?h:f?e:0;h>i;i++)if(c=d[i],(c.selected||i===e)&&(l.optDisabled?!c.disabled:null===c.getAttribute("disabled"))&&(!c.parentNode.disabled||!n.nodeName(c.parentNode,"optgroup"))){if(b=n(c).val(),f)return b;g.push(b)}return g},set:function(a,b){var c,d,e=a.options,f=n.makeArray(b),g=e.length;while(g--)if(d=e[g],n.inArray(n.valHooks.option.get(d),f)>-1)try{d.selected=c=!0}catch(h){d.scrollHeight}else d.selected=!1;return c||(a.selectedIndex=-1),e}}}}),n.each(["radio","checkbox"],function(){n.valHooks[this]={set:function(a,b){return n.isArray(b)?a.checked=n.inArray(n(a).val(),b)>-1:void 0}},l.checkOn||(n.valHooks[this].get=function(a){return null===a.getAttribute("value")?"on":a.value})});var tb,ub,vb=n.expr.attrHandle,wb=/^(?:checked|selected)$/i,xb=l.getSetAttribute,yb=l.input;n.fn.extend({attr:function(a,b){return Y(this,n.attr,a,b,arguments.length>1)},removeAttr:function(a){return this.each(function(){n.removeAttr(this,a)})}}),n.extend({attr:function(a,b,c){var d,e,f=a.nodeType;if(3!==f&&8!==f&&2!==f)return"undefined"==typeof a.getAttribute?n.prop(a,b,c):(1===f&&n.isXMLDoc(a)||(b=b.toLowerCase(),e=n.attrHooks[b]||(n.expr.match.bool.test(b)?ub:tb)),void 0!==c?null===c?void n.removeAttr(a,b):e&&"set"in e&&void 0!==(d=e.set(a,c,b))?d:(a.setAttribute(b,c+""),c):e&&"get"in e&&null!==(d=e.get(a,b))?d:(d=n.find.attr(a,b),null==d?void 0:d))},attrHooks:{type:{set:function(a,b){if(!l.radioValue&&"radio"===b&&n.nodeName(a,"input")){var c=a.value;return a.setAttribute("type",b),c&&(a.value=c),b}}}},removeAttr:function(a,b){var c,d,e=0,f=b&&b.match(G);if(f&&1===a.nodeType)while(c=f[e++])d=n.propFix[c]||c,n.expr.match.bool.test(c)?yb&&xb||!wb.test(c)?a[d]=!1:a[n.camelCase("default-"+c)]=a[d]=!1:n.attr(a,c,""),a.removeAttribute(xb?c:d)}}),ub={set:function(a,b,c){return b===!1?n.removeAttr(a,c):yb&&xb||!wb.test(c)?a.setAttribute(!xb&&n.propFix[c]||c,c):a[n.camelCase("default-"+c)]=a[c]=!0,c}},n.each(n.expr.match.bool.source.match(/\w+/g),function(a,b){var c=vb[b]||n.find.attr;yb&&xb||!wb.test(b)?vb[b]=function(a,b,d){var e,f;return d||(f=vb[b],vb[b]=e,e=null!=c(a,b,d)?b.toLowerCase():null,vb[b]=f),e}:vb[b]=function(a,b,c){return c?void 0:a[n.camelCase("default-"+b)]?b.toLowerCase():null}}),yb&&xb||(n.attrHooks.value={set:function(a,b,c){return n.nodeName(a,"input")?void(a.defaultValue=b):tb&&tb.set(a,b,c)}}),xb||(tb={set:function(a,b,c){var d=a.getAttributeNode(c);return d||a.setAttributeNode(d=a.ownerDocument.createAttribute(c)),d.value=b+="","value"===c||b===a.getAttribute(c)?b:void 0}},vb.id=vb.name=vb.coords=function(a,b,c){var d;return c?void 0:(d=a.getAttributeNode(b))&&""!==d.value?d.value:null},n.valHooks.button={get:function(a,b){var c=a.getAttributeNode(b);return c&&c.specified?c.value:void 0},set:tb.set},n.attrHooks.contenteditable={set:function(a,b,c){tb.set(a,""===b?!1:b,c)}},n.each(["width","height"],function(a,b){n.attrHooks[b]={set:function(a,c){return""===c?(a.setAttribute(b,"auto"),c):void 0}}})),l.style||(n.attrHooks.style={get:function(a){return a.style.cssText||void 0},set:function(a,b){return a.style.cssText=b+""}});var zb=/^(?:input|select|textarea|button|object)$/i,Ab=/^(?:a|area)$/i;n.fn.extend({prop:function(a,b){return Y(this,n.prop,a,b,arguments.length>1)},removeProp:function(a){return a=n.propFix[a]||a,this.each(function(){try{this[a]=void 0,delete this[a]}catch(b){}})}}),n.extend({prop:function(a,b,c){var d,e,f=a.nodeType;if(3!==f&&8!==f&&2!==f)return 1===f&&n.isXMLDoc(a)||(b=n.propFix[b]||b,e=n.propHooks[b]),void 0!==c?e&&"set"in e&&void 0!==(d=e.set(a,c,b))?d:a[b]=c:e&&"get"in e&&null!==(d=e.get(a,b))?d:a[b]},propHooks:{tabIndex:{get:function(a){var b=n.find.attr(a,"tabindex");return b?parseInt(b,10):zb.test(a.nodeName)||Ab.test(a.nodeName)&&a.href?0:-1}}},propFix:{"for":"htmlFor","class":"className"}}),l.hrefNormalized||n.each(["href","src"],function(a,b){n.propHooks[b]={get:function(a){return a.getAttribute(b,4)}}}),l.optSelected||(n.propHooks.selected={get:function(a){var b=a.parentNode;return b&&(b.selectedIndex,b.parentNode&&b.parentNode.selectedIndex),null},set:function(a){var b=a.parentNode;b&&(b.selectedIndex,b.parentNode&&b.parentNode.selectedIndex)}}),n.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){n.propFix[this.toLowerCase()]=this}),l.enctype||(n.propFix.enctype="encoding");var Bb=/[\t\r\n\f]/g;function Cb(a){return n.attr(a,"class")||""}n.fn.extend({addClass:function(a){var b,c,d,e,f,g,h,i=0;if(n.isFunction(a))return this.each(function(b){n(this).addClass(a.call(this,b,Cb(this)))});if("string"==typeof a&&a){b=a.match(G)||[];while(c=this[i++])if(e=Cb(c),d=1===c.nodeType&&(" "+e+" ").replace(Bb," ")){g=0;while(f=b[g++])d.indexOf(" "+f+" ")<0&&(d+=f+" ");h=n.trim(d),e!==h&&n.attr(c,"class",h)}}return this},removeClass:function(a){var b,c,d,e,f,g,h,i=0;if(n.isFunction(a))return this.each(function(b){n(this).removeClass(a.call(this,b,Cb(this)))});if(!arguments.length)return this.attr("class","");if("string"==typeof a&&a){b=a.match(G)||[];while(c=this[i++])if(e=Cb(c),d=1===c.nodeType&&(" "+e+" ").replace(Bb," ")){g=0;while(f=b[g++])while(d.indexOf(" "+f+" ")>-1)d=d.replace(" "+f+" "," ");h=n.trim(d),e!==h&&n.attr(c,"class",h)}}return this},toggleClass:function(a,b){var c=typeof a;return"boolean"==typeof b&&"string"===c?b?this.addClass(a):this.removeClass(a):n.isFunction(a)?this.each(function(c){n(this).toggleClass(a.call(this,c,Cb(this),b),b)}):this.each(function(){var b,d,e,f;if("string"===c){d=0,e=n(this),f=a.match(G)||[];while(b=f[d++])e.hasClass(b)?e.removeClass(b):e.addClass(b)}else void 0!==a&&"boolean"!==c||(b=Cb(this),b&&n._data(this,"__className__",b),n.attr(this,"class",b||a===!1?"":n._data(this,"__className__")||""))})},hasClass:function(a){var b,c,d=0;b=" "+a+" ";while(c=this[d++])if(1===c.nodeType&&(" "+Cb(c)+" ").replace(Bb," ").indexOf(b)>-1)return!0;return!1}}),n.each("blur focus focusin focusout load resize scroll unload click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup error contextmenu".split(" "),function(a,b){n.fn[b]=function(a,c){return arguments.length>0?this.on(b,null,a,c):this.trigger(b)}}),n.fn.extend({hover:function(a,b){return this.mouseenter(a).mouseleave(b||a)}});var Db=a.location,Eb=n.now(),Fb=/\?/,Gb=/(,)|(\[|{)|(}|])|"(?:[^"\\\r\n]|\\["\\\/bfnrt]|\\u[\da-fA-F]{4})*"\s*:?|true|false|null|-?(?!0\d)\d+(?:\.\d+|)(?:[eE][+-]?\d+|)/g;n.parseJSON=function(b){if(a.JSON&&a.JSON.parse)return a.JSON.parse(b+"");var c,d=null,e=n.trim(b+"");return e&&!n.trim(e.replace(Gb,function(a,b,e,f){return c&&b&&(d=0),0===d?a:(c=e||b,d+=!f-!e,"")}))?Function("return "+e)():n.error("Invalid JSON: "+b)},n.parseXML=function(b){var c,d;if(!b||"string"!=typeof b)return null;try{a.DOMParser?(d=new a.DOMParser,c=d.parseFromString(b,"text/xml")):(c=new a.ActiveXObject("Microsoft.XMLDOM"),c.async="false",c.loadXML(b))}catch(e){c=void 0}return c&&c.documentElement&&!c.getElementsByTagName("parsererror").length||n.error("Invalid XML: "+b),c};var Hb=/#.*$/,Ib=/([?&])_=[^&]*/,Jb=/^(.*?):[ \t]*([^\r\n]*)\r?$/gm,Kb=/^(?:about|app|app-storage|.+-extension|file|res|widget):$/,Lb=/^(?:GET|HEAD)$/,Mb=/^\/\//,Nb=/^([\w.+-]+:)(?:\/\/(?:[^\/?#]*@|)([^\/?#:]*)(?::(\d+)|)|)/,Ob={},Pb={},Qb="*/".concat("*"),Rb=Db.href,Sb=Nb.exec(Rb.toLowerCase())||[];function Tb(a){return function(b,c){"string"!=typeof b&&(c=b,b="*");var d,e=0,f=b.toLowerCase().match(G)||[];if(n.isFunction(c))while(d=f[e++])"+"===d.charAt(0)?(d=d.slice(1)||"*",(a[d]=a[d]||[]).unshift(c)):(a[d]=a[d]||[]).push(c)}}function Ub(a,b,c,d){var e={},f=a===Pb;function g(h){var i;return e[h]=!0,n.each(a[h]||[],function(a,h){var j=h(b,c,d);return"string"!=typeof j||f||e[j]?f?!(i=j):void 0:(b.dataTypes.unshift(j),g(j),!1)}),i}return g(b.dataTypes[0])||!e["*"]&&g("*")}function Vb(a,b){var c,d,e=n.ajaxSettings.flatOptions||{};for(d in b)void 0!==b[d]&&((e[d]?a:c||(c={}))[d]=b[d]);return c&&n.extend(!0,a,c),a}function Wb(a,b,c){var d,e,f,g,h=a.contents,i=a.dataTypes;while("*"===i[0])i.shift(),void 0===e&&(e=a.mimeType||b.getResponseHeader("Content-Type"));if(e)for(g in h)if(h[g]&&h[g].test(e)){i.unshift(g);break}if(i[0]in c)f=i[0];else{for(g in c){if(!i[0]||a.converters[g+" "+i[0]]){f=g;break}d||(d=g)}f=f||d}return f?(f!==i[0]&&i.unshift(f),c[f]):void 0}function Xb(a,b,c,d){var e,f,g,h,i,j={},k=a.dataTypes.slice();if(k[1])for(g in a.converters)j[g.toLowerCase()]=a.converters[g];f=k.shift();while(f)if(a.responseFields[f]&&(c[a.responseFields[f]]=b),!i&&d&&a.dataFilter&&(b=a.dataFilter(b,a.dataType)),i=f,f=k.shift())if("*"===f)f=i;else if("*"!==i&&i!==f){if(g=j[i+" "+f]||j["* "+f],!g)for(e in j)if(h=e.split(" "),h[1]===f&&(g=j[i+" "+h[0]]||j["* "+h[0]])){g===!0?g=j[e]:j[e]!==!0&&(f=h[0],k.unshift(h[1]));break}if(g!==!0)if(g&&a["throws"])b=g(b);else try{b=g(b)}catch(l){return{state:"parsererror",error:g?l:"No conversion from "+i+" to "+f}}}return{state:"success",data:b}}n.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:Rb,type:"GET",isLocal:Kb.test(Sb[1]),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":Qb,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/\bxml\b/,html:/\bhtml/,json:/\bjson\b/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":n.parseJSON,"text xml":n.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(a,b){return b?Vb(Vb(a,n.ajaxSettings),b):Vb(n.ajaxSettings,a)},ajaxPrefilter:Tb(Ob),ajaxTransport:Tb(Pb),ajax:function(b,c){"object"==typeof b&&(c=b,b=void 0),c=c||{};var d,e,f,g,h,i,j,k,l=n.ajaxSetup({},c),m=l.context||l,o=l.context&&(m.nodeType||m.jquery)?n(m):n.event,p=n.Deferred(),q=n.Callbacks("once memory"),r=l.statusCode||{},s={},t={},u=0,v="canceled",w={readyState:0,getResponseHeader:function(a){var b;if(2===u){if(!k){k={};while(b=Jb.exec(g))k[b[1].toLowerCase()]=b[2]}b=k[a.toLowerCase()]}return null==b?null:b},getAllResponseHeaders:function(){return 2===u?g:null},setRequestHeader:function(a,b){var c=a.toLowerCase();return u||(a=t[c]=t[c]||a,s[a]=b),this},overrideMimeType:function(a){return u||(l.mimeType=a),this},statusCode:function(a){var b;if(a)if(2>u)for(b in a)r[b]=[r[b],a[b]];else w.always(a[w.status]);return this},abort:function(a){var b=a||v;return j&&j.abort(b),y(0,b),this}};if(p.promise(w).complete=q.add,w.success=w.done,w.error=w.fail,l.url=((b||l.url||Rb)+"").replace(Hb,"").replace(Mb,Sb[1]+"//"),l.type=c.method||c.type||l.method||l.type,l.dataTypes=n.trim(l.dataType||"*").toLowerCase().match(G)||[""],null==l.crossDomain&&(d=Nb.exec(l.url.toLowerCase()),l.crossDomain=!(!d||d[1]===Sb[1]&&d[2]===Sb[2]&&(d[3]||("http:"===d[1]?"80":"443"))===(Sb[3]||("http:"===Sb[1]?"80":"443")))),l.data&&l.processData&&"string"!=typeof l.data&&(l.data=n.param(l.data,l.traditional)),Ub(Ob,l,c,w),2===u)return w;i=n.event&&l.global,i&&0===n.active++&&n.event.trigger("ajaxStart"),l.type=l.type.toUpperCase(),l.hasContent=!Lb.test(l.type),f=l.url,l.hasContent||(l.data&&(f=l.url+=(Fb.test(f)?"&":"?")+l.data,delete l.data),l.cache===!1&&(l.url=Ib.test(f)?f.replace(Ib,"$1_="+Eb++):f+(Fb.test(f)?"&":"?")+"_="+Eb++)),l.ifModified&&(n.lastModified[f]&&w.setRequestHeader("If-Modified-Since",n.lastModified[f]),n.etag[f]&&w.setRequestHeader("If-None-Match",n.etag[f])),(l.data&&l.hasContent&&l.contentType!==!1||c.contentType)&&w.setRequestHeader("Content-Type",l.contentType),w.setRequestHeader("Accept",l.dataTypes[0]&&l.accepts[l.dataTypes[0]]?l.accepts[l.dataTypes[0]]+("*"!==l.dataTypes[0]?", "+Qb+"; q=0.01":""):l.accepts["*"]);for(e in l.headers)w.setRequestHeader(e,l.headers[e]);if(l.beforeSend&&(l.beforeSend.call(m,w,l)===!1||2===u))return w.abort();v="abort";for(e in{success:1,error:1,complete:1})w[e](l[e]);if(j=Ub(Pb,l,c,w)){if(w.readyState=1,i&&o.trigger("ajaxSend",[w,l]),2===u)return w;l.async&&l.timeout>0&&(h=a.setTimeout(function(){w.abort("timeout")},l.timeout));try{u=1,j.send(s,y)}catch(x){if(!(2>u))throw x;y(-1,x)}}else y(-1,"No Transport");function y(b,c,d,e){var k,s,t,v,x,y=c;2!==u&&(u=2,h&&a.clearTimeout(h),j=void 0,g=e||"",w.readyState=b>0?4:0,k=b>=200&&300>b||304===b,d&&(v=Wb(l,w,d)),v=Xb(l,v,w,k),k?(l.ifModified&&(x=w.getResponseHeader("Last-Modified"),x&&(n.lastModified[f]=x),x=w.getResponseHeader("etag"),x&&(n.etag[f]=x)),204===b||"HEAD"===l.type?y="nocontent":304===b?y="notmodified":(y=v.state,s=v.data,t=v.error,k=!t)):(t=y,!b&&y||(y="error",0>b&&(b=0))),w.status=b,w.statusText=(c||y)+"",k?p.resolveWith(m,[s,y,w]):p.rejectWith(m,[w,y,t]),w.statusCode(r),r=void 0,i&&o.trigger(k?"ajaxSuccess":"ajaxError",[w,l,k?s:t]),q.fireWith(m,[w,y]),i&&(o.trigger("ajaxComplete",[w,l]),--n.active||n.event.trigger("ajaxStop")))}return w},getJSON:function(a,b,c){return n.get(a,b,c,"json")},getScript:function(a,b){return n.get(a,void 0,b,"script")}}),n.each(["get","post"],function(a,b){n[b]=function(a,c,d,e){return n.isFunction(c)&&(e=e||d,d=c,c=void 0),n.ajax(n.extend({url:a,type:b,dataType:e,data:c,success:d},n.isPlainObject(a)&&a))}}),n._evalUrl=function(a){return n.ajax({url:a,type:"GET",dataType:"script",cache:!0,async:!1,global:!1,"throws":!0})},n.fn.extend({wrapAll:function(a){if(n.isFunction(a))return this.each(function(b){n(this).wrapAll(a.call(this,b))});if(this[0]){var b=n(a,this[0].ownerDocument).eq(0).clone(!0);this[0].parentNode&&b.insertBefore(this[0]),b.map(function(){var a=this;while(a.firstChild&&1===a.firstChild.nodeType)a=a.firstChild;return a}).append(this)}return this},wrapInner:function(a){return n.isFunction(a)?this.each(function(b){n(this).wrapInner(a.call(this,b))}):this.each(function(){var b=n(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){var b=n.isFunction(a);return this.each(function(c){n(this).wrapAll(b?a.call(this,c):a)})},unwrap:function(){return this.parent().each(function(){n.nodeName(this,"body")||n(this).replaceWith(this.childNodes)}).end()}});function Yb(a){return a.style&&a.style.display||n.css(a,"display")}function Zb(a){if(!n.contains(a.ownerDocument||d,a))return!0;while(a&&1===a.nodeType){if("none"===Yb(a)||"hidden"===a.type)return!0;a=a.parentNode}return!1}n.expr.filters.hidden=function(a){return l.reliableHiddenOffsets()?a.offsetWidth<=0&&a.offsetHeight<=0&&!a.getClientRects().length:Zb(a)},n.expr.filters.visible=function(a){return!n.expr.filters.hidden(a)};var $b=/%20/g,_b=/\[\]$/,ac=/\r?\n/g,bc=/^(?:submit|button|image|reset|file)$/i,cc=/^(?:input|select|textarea|keygen)/i;function dc(a,b,c,d){var e;if(n.isArray(b))n.each(b,function(b,e){c||_b.test(a)?d(a,e):dc(a+"["+("object"==typeof e&&null!=e?b:"")+"]",e,c,d)});else if(c||"object"!==n.type(b))d(a,b);else for(e in b)dc(a+"["+e+"]",b[e],c,d)}n.param=function(a,b){var c,d=[],e=function(a,b){b=n.isFunction(b)?b():null==b?"":b,d[d.length]=encodeURIComponent(a)+"="+encodeURIComponent(b)};if(void 0===b&&(b=n.ajaxSettings&&n.ajaxSettings.traditional),n.isArray(a)||a.jquery&&!n.isPlainObject(a))n.each(a,function(){e(this.name,this.value)});else for(c in a)dc(c,a[c],b,e);return d.join("&").replace($b,"+")},n.fn.extend({serialize:function(){return n.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var a=n.prop(this,"elements");return a?n.makeArray(a):this}).filter(function(){var a=this.type;return this.name&&!n(this).is(":disabled")&&cc.test(this.nodeName)&&!bc.test(a)&&(this.checked||!Z.test(a))}).map(function(a,b){var c=n(this).val();return null==c?null:n.isArray(c)?n.map(c,function(a){return{name:b.name,value:a.replace(ac,"\r\n")}}):{name:b.name,value:c.replace(ac,"\r\n")}}).get()}}),n.ajaxSettings.xhr=void 0!==a.ActiveXObject?function(){return this.isLocal?ic():d.documentMode>8?hc():/^(get|post|head|put|delete|options)$/i.test(this.type)&&hc()||ic()}:hc;var ec=0,fc={},gc=n.ajaxSettings.xhr();a.attachEvent&&a.attachEvent("onunload",function(){for(var a in fc)fc[a](void 0,!0)}),l.cors=!!gc&&"withCredentials"in gc,gc=l.ajax=!!gc,gc&&n.ajaxTransport(function(b){if(!b.crossDomain||l.cors){var c;return{send:function(d,e){var f,g=b.xhr(),h=++ec;if(g.open(b.type,b.url,b.async,b.username,b.password),b.xhrFields)for(f in b.xhrFields)g[f]=b.xhrFields[f];b.mimeType&&g.overrideMimeType&&g.overrideMimeType(b.mimeType),b.crossDomain||d["X-Requested-With"]||(d["X-Requested-With"]="XMLHttpRequest");for(f in d)void 0!==d[f]&&g.setRequestHeader(f,d[f]+"");g.send(b.hasContent&&b.data||null),c=function(a,d){var f,i,j;if(c&&(d||4===g.readyState))if(delete fc[h],c=void 0,g.onreadystatechange=n.noop,d)4!==g.readyState&&g.abort();else{j={},f=g.status,"string"==typeof g.responseText&&(j.text=g.responseText);try{i=g.statusText}catch(k){i=""}f||!b.isLocal||b.crossDomain?1223===f&&(f=204):f=j.text?200:404}j&&e(f,i,j,g.getAllResponseHeaders())},b.async?4===g.readyState?a.setTimeout(c):g.onreadystatechange=fc[h]=c:c()},abort:function(){c&&c(void 0,!0)}}}});function hc(){try{return new a.XMLHttpRequest}catch(b){}}function ic(){try{return new a.ActiveXObject("Microsoft.XMLHTTP")}catch(b){}}n.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/\b(?:java|ecma)script\b/},converters:{"text script":function(a){return n.globalEval(a),a}}}),n.ajaxPrefilter("script",function(a){void 0===a.cache&&(a.cache=!1),a.crossDomain&&(a.type="GET",a.global=!1)}),n.ajaxTransport("script",function(a){if(a.crossDomain){var b,c=d.head||n("head")[0]||d.documentElement;return{send:function(e,f){b=d.createElement("script"),b.async=!0,a.scriptCharset&&(b.charset=a.scriptCharset),b.src=a.url,b.onload=b.onreadystatechange=function(a,c){(c||!b.readyState||/loaded|complete/.test(b.readyState))&&(b.onload=b.onreadystatechange=null,b.parentNode&&b.parentNode.removeChild(b),b=null,c||f(200,"success"))},c.insertBefore(b,c.firstChild)},abort:function(){b&&b.onload(void 0,!0)}}}});var jc=[],kc=/(=)\?(?=&|$)|\?\?/;n.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var a=jc.pop()||n.expando+"_"+Eb++;return this[a]=!0,a}}),n.ajaxPrefilter("json jsonp",function(b,c,d){var e,f,g,h=b.jsonp!==!1&&(kc.test(b.url)?"url":"string"==typeof b.data&&0===(b.contentType||"").indexOf("application/x-www-form-urlencoded")&&kc.test(b.data)&&"data");return h||"jsonp"===b.dataTypes[0]?(e=b.jsonpCallback=n.isFunction(b.jsonpCallback)?b.jsonpCallback():b.jsonpCallback,h?b[h]=b[h].replace(kc,"$1"+e):b.jsonp!==!1&&(b.url+=(Fb.test(b.url)?"&":"?")+b.jsonp+"="+e),b.converters["script json"]=function(){return g||n.error(e+" was not called"),g[0]},b.dataTypes[0]="json",f=a[e],a[e]=function(){g=arguments},d.always(function(){void 0===f?n(a).removeProp(e):a[e]=f,b[e]&&(b.jsonpCallback=c.jsonpCallback,jc.push(e)),g&&n.isFunction(f)&&f(g[0]),g=f=void 0}),"script"):void 0}),n.parseHTML=function(a,b,c){if(!a||"string"!=typeof a)return null;"boolean"==typeof b&&(c=b,b=!1),b=b||d;var e=x.exec(a),f=!c&&[];return e?[b.createElement(e[1])]:(e=ja([a],b,f),f&&f.length&&n(f).remove(),n.merge([],e.childNodes))};var lc=n.fn.load;n.fn.load=function(a,b,c){if("string"!=typeof a&&lc)return lc.apply(this,arguments);var d,e,f,g=this,h=a.indexOf(" ");return h>-1&&(d=n.trim(a.slice(h,a.length)),a=a.slice(0,h)),n.isFunction(b)?(c=b,b=void 0):b&&"object"==typeof b&&(e="POST"),g.length>0&&n.ajax({url:a,type:e||"GET",dataType:"html",data:b}).done(function(a){f=arguments,g.html(d?n("<div>").append(n.parseHTML(a)).find(d):a)}).always(c&&function(a,b){g.each(function(){c.apply(this,f||[a.responseText,b,a])})}),this},n.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(a,b){n.fn[b]=function(a){return this.on(b,a)}}),n.expr.filters.animated=function(a){return n.grep(n.timers,function(b){return a===b.elem}).length};function mc(a){return n.isWindow(a)?a:9===a.nodeType?a.defaultView||a.parentWindow:!1}n.offset={setOffset:function(a,b,c){var d,e,f,g,h,i,j,k=n.css(a,"position"),l=n(a),m={};"static"===k&&(a.style.position="relative"),h=l.offset(),f=n.css(a,"top"),i=n.css(a,"left"),j=("absolute"===k||"fixed"===k)&&n.inArray("auto",[f,i])>-1,j?(d=l.position(),g=d.top,e=d.left):(g=parseFloat(f)||0,e=parseFloat(i)||0),n.isFunction(b)&&(b=b.call(a,c,n.extend({},h))),null!=b.top&&(m.top=b.top-h.top+g),null!=b.left&&(m.left=b.left-h.left+e),"using"in b?b.using.call(a,m):l.css(m)}},n.fn.extend({offset:function(a){if(arguments.length)return void 0===a?this:this.each(function(b){n.offset.setOffset(this,a,b)});var b,c,d={top:0,left:0},e=this[0],f=e&&e.ownerDocument;if(f)return b=f.documentElement,n.contains(b,e)?("undefined"!=typeof e.getBoundingClientRect&&(d=e.getBoundingClientRect()),c=mc(f),{top:d.top+(c.pageYOffset||b.scrollTop)-(b.clientTop||0),left:d.left+(c.pageXOffset||b.scrollLeft)-(b.clientLeft||0)}):d},position:function(){if(this[0]){var a,b,c={top:0,left:0},d=this[0];return"fixed"===n.css(d,"position")?b=d.getBoundingClientRect():(a=this.offsetParent(),b=this.offset(),n.nodeName(a[0],"html")||(c=a.offset()),c.top+=n.css(a[0],"borderTopWidth",!0),c.left+=n.css(a[0],"borderLeftWidth",!0)),{top:b.top-c.top-n.css(d,"marginTop",!0),left:b.left-c.left-n.css(d,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var a=this.offsetParent;while(a&&!n.nodeName(a,"html")&&"static"===n.css(a,"position"))a=a.offsetParent;return a||Qa})}}),n.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(a,b){var c=/Y/.test(b);n.fn[a]=function(d){return Y(this,function(a,d,e){var f=mc(a);return void 0===e?f?b in f?f[b]:f.document.documentElement[d]:a[d]:void(f?f.scrollTo(c?n(f).scrollLeft():e,c?e:n(f).scrollTop()):a[d]=e)},a,d,arguments.length,null)}}),n.each(["top","left"],function(a,b){n.cssHooks[b]=Ua(l.pixelPosition,function(a,c){return c?(c=Sa(a,b),Oa.test(c)?n(a).position()[b]+"px":c):void 0})}),n.each({Height:"height",Width:"width"},function(a,b){n.each({ -padding:"inner"+a,content:b,"":"outer"+a},function(c,d){n.fn[d]=function(d,e){var f=arguments.length&&(c||"boolean"!=typeof d),g=c||(d===!0||e===!0?"margin":"border");return Y(this,function(b,c,d){var e;return n.isWindow(b)?b.document.documentElement["client"+a]:9===b.nodeType?(e=b.documentElement,Math.max(b.body["scroll"+a],e["scroll"+a],b.body["offset"+a],e["offset"+a],e["client"+a])):void 0===d?n.css(b,c,g):n.style(b,c,d,g)},b,f?d:void 0,f,null)}})}),n.fn.extend({bind:function(a,b,c){return this.on(a,null,b,c)},unbind:function(a,b){return this.off(a,null,b)},delegate:function(a,b,c,d){return this.on(b,a,c,d)},undelegate:function(a,b,c){return 1===arguments.length?this.off(a,"**"):this.off(b,a||"**",c)}}),n.fn.size=function(){return this.length},n.fn.andSelf=n.fn.addBack,"function"==typeof define&&define.amd&&define("jquery",[],function(){return n});var nc=a.jQuery,oc=a.$;return n.noConflict=function(b){return a.$===n&&(a.$=oc),b&&a.jQuery===n&&(a.jQuery=nc),n},b||(a.jQuery=a.$=n),n}); diff --git a/data/web/js/build/000-jquery.min.js b/data/web/js/build/000-jquery.min.js new file mode 100644 index 00000000..d788cb4f --- /dev/null +++ b/data/web/js/build/000-jquery.min.js @@ -0,0 +1,4 @@ +/*! jQuery v3.4.1 | (c) JS Foundation and other contributors | jquery.org/license */ +!function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(C,e){"use strict";var t=[],E=C.document,r=Object.getPrototypeOf,s=t.slice,g=t.concat,u=t.push,i=t.indexOf,n={},o=n.toString,v=n.hasOwnProperty,a=v.toString,l=a.call(Object),y={},m=function(e){return"function"==typeof e&&"number"!=typeof e.nodeType},x=function(e){return null!=e&&e===e.window},c={type:!0,src:!0,nonce:!0,noModule:!0};function b(e,t,n){var r,i,o=(n=n||E).createElement("script");if(o.text=e,t)for(r in c)(i=t[r]||t.getAttribute&&t.getAttribute(r))&&o.setAttribute(r,i);n.head.appendChild(o).parentNode.removeChild(o)}function w(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?n[o.call(e)]||"object":typeof e}var f="3.4.1",k=function(e,t){return new k.fn.init(e,t)},p=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g;function d(e){var t=!!e&&"length"in e&&e.length,n=w(e);return!m(e)&&!x(e)&&("array"===n||0===t||"number"==typeof t&&0<t&&t-1 in e)}k.fn=k.prototype={jquery:f,constructor:k,length:0,toArray:function(){return s.call(this)},get:function(e){return null==e?s.call(this):e<0?this[e+this.length]:this[e]},pushStack:function(e){var t=k.merge(this.constructor(),e);return t.prevObject=this,t},each:function(e){return k.each(this,e)},map:function(n){return this.pushStack(k.map(this,function(e,t){return n.call(e,t,e)}))},slice:function(){return this.pushStack(s.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(e){var t=this.length,n=+e+(e<0?t:0);return this.pushStack(0<=n&&n<t?[this[n]]:[])},end:function(){return this.prevObject||this.constructor()},push:u,sort:t.sort,splice:t.splice},k.extend=k.fn.extend=function(){var e,t,n,r,i,o,a=arguments[0]||{},s=1,u=arguments.length,l=!1;for("boolean"==typeof a&&(l=a,a=arguments[s]||{},s++),"object"==typeof a||m(a)||(a={}),s===u&&(a=this,s--);s<u;s++)if(null!=(e=arguments[s]))for(t in e)r=e[t],"__proto__"!==t&&a!==r&&(l&&r&&(k.isPlainObject(r)||(i=Array.isArray(r)))?(n=a[t],o=i&&!Array.isArray(n)?[]:i||k.isPlainObject(n)?n:{},i=!1,a[t]=k.extend(l,o,r)):void 0!==r&&(a[t]=r));return a},k.extend({expando:"jQuery"+(f+Math.random()).replace(/\D/g,""),isReady:!0,error:function(e){throw new Error(e)},noop:function(){},isPlainObject:function(e){var t,n;return!(!e||"[object Object]"!==o.call(e))&&(!(t=r(e))||"function"==typeof(n=v.call(t,"constructor")&&t.constructor)&&a.call(n)===l)},isEmptyObject:function(e){var t;for(t in e)return!1;return!0},globalEval:function(e,t){b(e,{nonce:t&&t.nonce})},each:function(e,t){var n,r=0;if(d(e)){for(n=e.length;r<n;r++)if(!1===t.call(e[r],r,e[r]))break}else for(r in e)if(!1===t.call(e[r],r,e[r]))break;return e},trim:function(e){return null==e?"":(e+"").replace(p,"")},makeArray:function(e,t){var n=t||[];return null!=e&&(d(Object(e))?k.merge(n,"string"==typeof e?[e]:e):u.call(n,e)),n},inArray:function(e,t,n){return null==t?-1:i.call(t,e,n)},merge:function(e,t){for(var n=+t.length,r=0,i=e.length;r<n;r++)e[i++]=t[r];return e.length=i,e},grep:function(e,t,n){for(var r=[],i=0,o=e.length,a=!n;i<o;i++)!t(e[i],i)!==a&&r.push(e[i]);return r},map:function(e,t,n){var r,i,o=0,a=[];if(d(e))for(r=e.length;o<r;o++)null!=(i=t(e[o],o,n))&&a.push(i);else for(o in e)null!=(i=t(e[o],o,n))&&a.push(i);return g.apply([],a)},guid:1,support:y}),"function"==typeof Symbol&&(k.fn[Symbol.iterator]=t[Symbol.iterator]),k.each("Boolean Number String Function Array Date RegExp Object Error Symbol".split(" "),function(e,t){n["[object "+t+"]"]=t.toLowerCase()});var h=function(n){var e,d,b,o,i,h,f,g,w,u,l,T,C,a,E,v,s,c,y,k="sizzle"+1*new Date,m=n.document,S=0,r=0,p=ue(),x=ue(),N=ue(),A=ue(),D=function(e,t){return e===t&&(l=!0),0},j={}.hasOwnProperty,t=[],q=t.pop,L=t.push,H=t.push,O=t.slice,P=function(e,t){for(var n=0,r=e.length;n<r;n++)if(e[n]===t)return n;return-1},R="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",M="[\\x20\\t\\r\\n\\f]",I="(?:\\\\.|[\\w-]|[^\0-\\xa0])+",W="\\["+M+"*("+I+")(?:"+M+"*([*^$|!~]?=)"+M+"*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|("+I+"))|)"+M+"*\\]",$=":("+I+")(?:\\((('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|((?:\\\\.|[^\\\\()[\\]]|"+W+")*)|.*)\\)|)",F=new RegExp(M+"+","g"),B=new RegExp("^"+M+"+|((?:^|[^\\\\])(?:\\\\.)*)"+M+"+$","g"),_=new RegExp("^"+M+"*,"+M+"*"),z=new RegExp("^"+M+"*([>+~]|"+M+")"+M+"*"),U=new RegExp(M+"|>"),X=new RegExp($),V=new RegExp("^"+I+"$"),G={ID:new RegExp("^#("+I+")"),CLASS:new RegExp("^\\.("+I+")"),TAG:new RegExp("^("+I+"|[*])"),ATTR:new RegExp("^"+W),PSEUDO:new RegExp("^"+$),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+R+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Y=/HTML$/i,Q=/^(?:input|select|textarea|button)$/i,J=/^h\d$/i,K=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ee=/[+~]/,te=new RegExp("\\\\([\\da-f]{1,6}"+M+"?|("+M+")|.)","ig"),ne=function(e,t,n){var r="0x"+t-65536;return r!=r||n?t:r<0?String.fromCharCode(r+65536):String.fromCharCode(r>>10|55296,1023&r|56320)},re=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ie=function(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},oe=function(){T()},ae=be(function(e){return!0===e.disabled&&"fieldset"===e.nodeName.toLowerCase()},{dir:"parentNode",next:"legend"});try{H.apply(t=O.call(m.childNodes),m.childNodes),t[m.childNodes.length].nodeType}catch(e){H={apply:t.length?function(e,t){L.apply(e,O.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function se(t,e,n,r){var i,o,a,s,u,l,c,f=e&&e.ownerDocument,p=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==p&&9!==p&&11!==p)return n;if(!r&&((e?e.ownerDocument||e:m)!==C&&T(e),e=e||C,E)){if(11!==p&&(u=Z.exec(t)))if(i=u[1]){if(9===p){if(!(a=e.getElementById(i)))return n;if(a.id===i)return n.push(a),n}else if(f&&(a=f.getElementById(i))&&y(e,a)&&a.id===i)return n.push(a),n}else{if(u[2])return H.apply(n,e.getElementsByTagName(t)),n;if((i=u[3])&&d.getElementsByClassName&&e.getElementsByClassName)return H.apply(n,e.getElementsByClassName(i)),n}if(d.qsa&&!A[t+" "]&&(!v||!v.test(t))&&(1!==p||"object"!==e.nodeName.toLowerCase())){if(c=t,f=e,1===p&&U.test(t)){(s=e.getAttribute("id"))?s=s.replace(re,ie):e.setAttribute("id",s=k),o=(l=h(t)).length;while(o--)l[o]="#"+s+" "+xe(l[o]);c=l.join(","),f=ee.test(t)&&ye(e.parentNode)||e}try{return H.apply(n,f.querySelectorAll(c)),n}catch(e){A(t,!0)}finally{s===k&&e.removeAttribute("id")}}}return g(t.replace(B,"$1"),e,n,r)}function ue(){var r=[];return function e(t,n){return r.push(t+" ")>b.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function le(e){return e[k]=!0,e}function ce(e){var t=C.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function fe(e,t){var n=e.split("|"),r=n.length;while(r--)b.attrHandle[n[r]]=t}function pe(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function de(t){return function(e){return"input"===e.nodeName.toLowerCase()&&e.type===t}}function he(n){return function(e){var t=e.nodeName.toLowerCase();return("input"===t||"button"===t)&&e.type===n}}function ge(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&ae(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function ve(a){return le(function(o){return o=+o,le(function(e,t){var n,r=a([],e.length,o),i=r.length;while(i--)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function ye(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}for(e in d=se.support={},i=se.isXML=function(e){var t=e.namespaceURI,n=(e.ownerDocument||e).documentElement;return!Y.test(t||n&&n.nodeName||"HTML")},T=se.setDocument=function(e){var t,n,r=e?e.ownerDocument||e:m;return r!==C&&9===r.nodeType&&r.documentElement&&(a=(C=r).documentElement,E=!i(C),m!==C&&(n=C.defaultView)&&n.top!==n&&(n.addEventListener?n.addEventListener("unload",oe,!1):n.attachEvent&&n.attachEvent("onunload",oe)),d.attributes=ce(function(e){return e.className="i",!e.getAttribute("className")}),d.getElementsByTagName=ce(function(e){return e.appendChild(C.createComment("")),!e.getElementsByTagName("*").length}),d.getElementsByClassName=K.test(C.getElementsByClassName),d.getById=ce(function(e){return a.appendChild(e).id=k,!C.getElementsByName||!C.getElementsByName(k).length}),d.getById?(b.filter.ID=function(e){var t=e.replace(te,ne);return function(e){return e.getAttribute("id")===t}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n=t.getElementById(e);return n?[n]:[]}}):(b.filter.ID=function(e){var n=e.replace(te,ne);return function(e){var t="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return t&&t.value===n}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),b.find.TAG=d.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):d.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},b.find.CLASS=d.getElementsByClassName&&function(e,t){if("undefined"!=typeof t.getElementsByClassName&&E)return t.getElementsByClassName(e)},s=[],v=[],(d.qsa=K.test(C.querySelectorAll))&&(ce(function(e){a.appendChild(e).innerHTML="<a id='"+k+"'></a><select id='"+k+"-\r\\' msallowcapture=''><option selected=''></option></select>",e.querySelectorAll("[msallowcapture^='']").length&&v.push("[*^$]="+M+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||v.push("\\["+M+"*(?:value|"+R+")"),e.querySelectorAll("[id~="+k+"-]").length||v.push("~="),e.querySelectorAll(":checked").length||v.push(":checked"),e.querySelectorAll("a#"+k+"+*").length||v.push(".#.+[+~]")}),ce(function(e){e.innerHTML="<a href='' disabled='disabled'></a><select disabled='disabled'><option/></select>";var t=C.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&v.push("name"+M+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&v.push(":enabled",":disabled"),a.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&v.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),v.push(",.*:")})),(d.matchesSelector=K.test(c=a.matches||a.webkitMatchesSelector||a.mozMatchesSelector||a.oMatchesSelector||a.msMatchesSelector))&&ce(function(e){d.disconnectedMatch=c.call(e,"*"),c.call(e,"[s!='']:x"),s.push("!=",$)}),v=v.length&&new RegExp(v.join("|")),s=s.length&&new RegExp(s.join("|")),t=K.test(a.compareDocumentPosition),y=t||K.test(a.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},D=t?function(e,t){if(e===t)return l=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)===(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!d.sortDetached&&t.compareDocumentPosition(e)===n?e===C||e.ownerDocument===m&&y(m,e)?-1:t===C||t.ownerDocument===m&&y(m,t)?1:u?P(u,e)-P(u,t):0:4&n?-1:1)}:function(e,t){if(e===t)return l=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e===C?-1:t===C?1:i?-1:o?1:u?P(u,e)-P(u,t):0;if(i===o)return pe(e,t);n=e;while(n=n.parentNode)a.unshift(n);n=t;while(n=n.parentNode)s.unshift(n);while(a[r]===s[r])r++;return r?pe(a[r],s[r]):a[r]===m?-1:s[r]===m?1:0}),C},se.matches=function(e,t){return se(e,null,null,t)},se.matchesSelector=function(e,t){if((e.ownerDocument||e)!==C&&T(e),d.matchesSelector&&E&&!A[t+" "]&&(!s||!s.test(t))&&(!v||!v.test(t)))try{var n=c.call(e,t);if(n||d.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){A(t,!0)}return 0<se(t,C,null,[e]).length},se.contains=function(e,t){return(e.ownerDocument||e)!==C&&T(e),y(e,t)},se.attr=function(e,t){(e.ownerDocument||e)!==C&&T(e);var n=b.attrHandle[t.toLowerCase()],r=n&&j.call(b.attrHandle,t.toLowerCase())?n(e,t,!E):void 0;return void 0!==r?r:d.attributes||!E?e.getAttribute(t):(r=e.getAttributeNode(t))&&r.specified?r.value:null},se.escape=function(e){return(e+"").replace(re,ie)},se.error=function(e){throw new Error("Syntax error, unrecognized expression: "+e)},se.uniqueSort=function(e){var t,n=[],r=0,i=0;if(l=!d.detectDuplicates,u=!d.sortStable&&e.slice(0),e.sort(D),l){while(t=e[i++])t===e[i]&&(r=n.push(i));while(r--)e.splice(n[r],1)}return u=null,e},o=se.getText=function(e){var t,n="",r=0,i=e.nodeType;if(i){if(1===i||9===i||11===i){if("string"==typeof e.textContent)return e.textContent;for(e=e.firstChild;e;e=e.nextSibling)n+=o(e)}else if(3===i||4===i)return e.nodeValue}else while(t=e[r++])n+=o(t);return n},(b=se.selectors={cacheLength:50,createPseudo:le,match:G,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(te,ne),e[3]=(e[3]||e[4]||e[5]||"").replace(te,ne),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||se.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&se.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return G.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&X.test(n)&&(t=h(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(te,ne).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=p[e+" "];return t||(t=new RegExp("(^|"+M+")"+e+"("+M+"|$)"))&&p(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(n,r,i){return function(e){var t=se.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===i:"!="===r?t!==i:"^="===r?i&&0===t.indexOf(i):"*="===r?i&&-1<t.indexOf(i):"$="===r?i&&t.slice(-i.length)===i:"~="===r?-1<(" "+t.replace(F," ")+" ").indexOf(i):"|="===r&&(t===i||t.slice(0,i.length+1)===i+"-"))}},CHILD:function(h,e,t,g,v){var y="nth"!==h.slice(0,3),m="last"!==h.slice(-4),x="of-type"===e;return 1===g&&0===v?function(e){return!!e.parentNode}:function(e,t,n){var r,i,o,a,s,u,l=y!==m?"nextSibling":"previousSibling",c=e.parentNode,f=x&&e.nodeName.toLowerCase(),p=!n&&!x,d=!1;if(c){if(y){while(l){a=e;while(a=a[l])if(x?a.nodeName.toLowerCase()===f:1===a.nodeType)return!1;u=l="only"===h&&!u&&"nextSibling"}return!0}if(u=[m?c.firstChild:c.lastChild],m&&p){d=(s=(r=(i=(o=(a=c)[k]||(a[k]={}))[a.uniqueID]||(o[a.uniqueID]={}))[h]||[])[0]===S&&r[1])&&r[2],a=s&&c.childNodes[s];while(a=++s&&a&&a[l]||(d=s=0)||u.pop())if(1===a.nodeType&&++d&&a===e){i[h]=[S,s,d];break}}else if(p&&(d=s=(r=(i=(o=(a=e)[k]||(a[k]={}))[a.uniqueID]||(o[a.uniqueID]={}))[h]||[])[0]===S&&r[1]),!1===d)while(a=++s&&a&&a[l]||(d=s=0)||u.pop())if((x?a.nodeName.toLowerCase()===f:1===a.nodeType)&&++d&&(p&&((i=(o=a[k]||(a[k]={}))[a.uniqueID]||(o[a.uniqueID]={}))[h]=[S,d]),a===e))break;return(d-=v)===g||d%g==0&&0<=d/g}}},PSEUDO:function(e,o){var t,a=b.pseudos[e]||b.setFilters[e.toLowerCase()]||se.error("unsupported pseudo: "+e);return a[k]?a(o):1<a.length?(t=[e,e,"",o],b.setFilters.hasOwnProperty(e.toLowerCase())?le(function(e,t){var n,r=a(e,o),i=r.length;while(i--)e[n=P(e,r[i])]=!(t[n]=r[i])}):function(e){return a(e,0,t)}):a}},pseudos:{not:le(function(e){var r=[],i=[],s=f(e.replace(B,"$1"));return s[k]?le(function(e,t,n,r){var i,o=s(e,null,r,[]),a=e.length;while(a--)(i=o[a])&&(e[a]=!(t[a]=i))}):function(e,t,n){return r[0]=e,s(r,null,n,i),r[0]=null,!i.pop()}}),has:le(function(t){return function(e){return 0<se(t,e).length}}),contains:le(function(t){return t=t.replace(te,ne),function(e){return-1<(e.textContent||o(e)).indexOf(t)}}),lang:le(function(n){return V.test(n||"")||se.error("unsupported lang: "+n),n=n.replace(te,ne).toLowerCase(),function(e){var t;do{if(t=E?e.lang:e.getAttribute("xml:lang")||e.getAttribute("lang"))return(t=t.toLowerCase())===n||0===t.indexOf(n+"-")}while((e=e.parentNode)&&1===e.nodeType);return!1}}),target:function(e){var t=n.location&&n.location.hash;return t&&t.slice(1)===e.id},root:function(e){return e===a},focus:function(e){return e===C.activeElement&&(!C.hasFocus||C.hasFocus())&&!!(e.type||e.href||~e.tabIndex)},enabled:ge(!1),disabled:ge(!0),checked:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&!!e.checked||"option"===t&&!!e.selected},selected:function(e){return e.parentNode&&e.parentNode.selectedIndex,!0===e.selected},empty:function(e){for(e=e.firstChild;e;e=e.nextSibling)if(e.nodeType<6)return!1;return!0},parent:function(e){return!b.pseudos.empty(e)},header:function(e){return J.test(e.nodeName)},input:function(e){return Q.test(e.nodeName)},button:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&"button"===e.type||"button"===t},text:function(e){var t;return"input"===e.nodeName.toLowerCase()&&"text"===e.type&&(null==(t=e.getAttribute("type"))||"text"===t.toLowerCase())},first:ve(function(){return[0]}),last:ve(function(e,t){return[t-1]}),eq:ve(function(e,t,n){return[n<0?n+t:n]}),even:ve(function(e,t){for(var n=0;n<t;n+=2)e.push(n);return e}),odd:ve(function(e,t){for(var n=1;n<t;n+=2)e.push(n);return e}),lt:ve(function(e,t,n){for(var r=n<0?n+t:t<n?t:n;0<=--r;)e.push(r);return e}),gt:ve(function(e,t,n){for(var r=n<0?n+t:n;++r<t;)e.push(r);return e})}}).pseudos.nth=b.pseudos.eq,{radio:!0,checkbox:!0,file:!0,password:!0,image:!0})b.pseudos[e]=de(e);for(e in{submit:!0,reset:!0})b.pseudos[e]=he(e);function me(){}function xe(e){for(var t=0,n=e.length,r="";t<n;t++)r+=e[t].value;return r}function be(s,e,t){var u=e.dir,l=e.next,c=l||u,f=t&&"parentNode"===c,p=r++;return e.first?function(e,t,n){while(e=e[u])if(1===e.nodeType||f)return s(e,t,n);return!1}:function(e,t,n){var r,i,o,a=[S,p];if(n){while(e=e[u])if((1===e.nodeType||f)&&s(e,t,n))return!0}else while(e=e[u])if(1===e.nodeType||f)if(i=(o=e[k]||(e[k]={}))[e.uniqueID]||(o[e.uniqueID]={}),l&&l===e.nodeName.toLowerCase())e=e[u]||e;else{if((r=i[c])&&r[0]===S&&r[1]===p)return a[2]=r[2];if((i[c]=a)[2]=s(e,t,n))return!0}return!1}}function we(i){return 1<i.length?function(e,t,n){var r=i.length;while(r--)if(!i[r](e,t,n))return!1;return!0}:i[0]}function Te(e,t,n,r,i){for(var o,a=[],s=0,u=e.length,l=null!=t;s<u;s++)(o=e[s])&&(n&&!n(o,r,i)||(a.push(o),l&&t.push(s)));return a}function Ce(d,h,g,v,y,e){return v&&!v[k]&&(v=Ce(v)),y&&!y[k]&&(y=Ce(y,e)),le(function(e,t,n,r){var i,o,a,s=[],u=[],l=t.length,c=e||function(e,t,n){for(var r=0,i=t.length;r<i;r++)se(e,t[r],n);return n}(h||"*",n.nodeType?[n]:n,[]),f=!d||!e&&h?c:Te(c,s,d,n,r),p=g?y||(e?d:l||v)?[]:t:f;if(g&&g(f,p,n,r),v){i=Te(p,u),v(i,[],n,r),o=i.length;while(o--)(a=i[o])&&(p[u[o]]=!(f[u[o]]=a))}if(e){if(y||d){if(y){i=[],o=p.length;while(o--)(a=p[o])&&i.push(f[o]=a);y(null,p=[],i,r)}o=p.length;while(o--)(a=p[o])&&-1<(i=y?P(e,a):s[o])&&(e[i]=!(t[i]=a))}}else p=Te(p===t?p.splice(l,p.length):p),y?y(null,t,p,r):H.apply(t,p)})}function Ee(e){for(var i,t,n,r=e.length,o=b.relative[e[0].type],a=o||b.relative[" "],s=o?1:0,u=be(function(e){return e===i},a,!0),l=be(function(e){return-1<P(i,e)},a,!0),c=[function(e,t,n){var r=!o&&(n||t!==w)||((i=t).nodeType?u(e,t,n):l(e,t,n));return i=null,r}];s<r;s++)if(t=b.relative[e[s].type])c=[be(we(c),t)];else{if((t=b.filter[e[s].type].apply(null,e[s].matches))[k]){for(n=++s;n<r;n++)if(b.relative[e[n].type])break;return Ce(1<s&&we(c),1<s&&xe(e.slice(0,s-1).concat({value:" "===e[s-2].type?"*":""})).replace(B,"$1"),t,s<n&&Ee(e.slice(s,n)),n<r&&Ee(e=e.slice(n)),n<r&&xe(e))}c.push(t)}return we(c)}return me.prototype=b.filters=b.pseudos,b.setFilters=new me,h=se.tokenize=function(e,t){var n,r,i,o,a,s,u,l=x[e+" "];if(l)return t?0:l.slice(0);a=e,s=[],u=b.preFilter;while(a){for(o in n&&!(r=_.exec(a))||(r&&(a=a.slice(r[0].length)||a),s.push(i=[])),n=!1,(r=z.exec(a))&&(n=r.shift(),i.push({value:n,type:r[0].replace(B," ")}),a=a.slice(n.length)),b.filter)!(r=G[o].exec(a))||u[o]&&!(r=u[o](r))||(n=r.shift(),i.push({value:n,type:o,matches:r}),a=a.slice(n.length));if(!n)break}return t?a.length:a?se.error(e):x(e,s).slice(0)},f=se.compile=function(e,t){var n,v,y,m,x,r,i=[],o=[],a=N[e+" "];if(!a){t||(t=h(e)),n=t.length;while(n--)(a=Ee(t[n]))[k]?i.push(a):o.push(a);(a=N(e,(v=o,m=0<(y=i).length,x=0<v.length,r=function(e,t,n,r,i){var o,a,s,u=0,l="0",c=e&&[],f=[],p=w,d=e||x&&b.find.TAG("*",i),h=S+=null==p?1:Math.random()||.1,g=d.length;for(i&&(w=t===C||t||i);l!==g&&null!=(o=d[l]);l++){if(x&&o){a=0,t||o.ownerDocument===C||(T(o),n=!E);while(s=v[a++])if(s(o,t||C,n)){r.push(o);break}i&&(S=h)}m&&((o=!s&&o)&&u--,e&&c.push(o))}if(u+=l,m&&l!==u){a=0;while(s=y[a++])s(c,f,t,n);if(e){if(0<u)while(l--)c[l]||f[l]||(f[l]=q.call(r));f=Te(f)}H.apply(r,f),i&&!e&&0<f.length&&1<u+y.length&&se.uniqueSort(r)}return i&&(S=h,w=p),c},m?le(r):r))).selector=e}return a},g=se.select=function(e,t,n,r){var i,o,a,s,u,l="function"==typeof e&&e,c=!r&&h(e=l.selector||e);if(n=n||[],1===c.length){if(2<(o=c[0]=c[0].slice(0)).length&&"ID"===(a=o[0]).type&&9===t.nodeType&&E&&b.relative[o[1].type]){if(!(t=(b.find.ID(a.matches[0].replace(te,ne),t)||[])[0]))return n;l&&(t=t.parentNode),e=e.slice(o.shift().value.length)}i=G.needsContext.test(e)?0:o.length;while(i--){if(a=o[i],b.relative[s=a.type])break;if((u=b.find[s])&&(r=u(a.matches[0].replace(te,ne),ee.test(o[0].type)&&ye(t.parentNode)||t))){if(o.splice(i,1),!(e=r.length&&xe(o)))return H.apply(n,r),n;break}}}return(l||f(e,c))(r,t,!E,n,!t||ee.test(e)&&ye(t.parentNode)||t),n},d.sortStable=k.split("").sort(D).join("")===k,d.detectDuplicates=!!l,T(),d.sortDetached=ce(function(e){return 1&e.compareDocumentPosition(C.createElement("fieldset"))}),ce(function(e){return e.innerHTML="<a href='#'></a>","#"===e.firstChild.getAttribute("href")})||fe("type|href|height|width",function(e,t,n){if(!n)return e.getAttribute(t,"type"===t.toLowerCase()?1:2)}),d.attributes&&ce(function(e){return e.innerHTML="<input/>",e.firstChild.setAttribute("value",""),""===e.firstChild.getAttribute("value")})||fe("value",function(e,t,n){if(!n&&"input"===e.nodeName.toLowerCase())return e.defaultValue}),ce(function(e){return null==e.getAttribute("disabled")})||fe(R,function(e,t,n){var r;if(!n)return!0===e[t]?t.toLowerCase():(r=e.getAttributeNode(t))&&r.specified?r.value:null}),se}(C);k.find=h,k.expr=h.selectors,k.expr[":"]=k.expr.pseudos,k.uniqueSort=k.unique=h.uniqueSort,k.text=h.getText,k.isXMLDoc=h.isXML,k.contains=h.contains,k.escapeSelector=h.escape;var T=function(e,t,n){var r=[],i=void 0!==n;while((e=e[t])&&9!==e.nodeType)if(1===e.nodeType){if(i&&k(e).is(n))break;r.push(e)}return r},S=function(e,t){for(var n=[];e;e=e.nextSibling)1===e.nodeType&&e!==t&&n.push(e);return n},N=k.expr.match.needsContext;function A(e,t){return e.nodeName&&e.nodeName.toLowerCase()===t.toLowerCase()}var D=/^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function j(e,n,r){return m(n)?k.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?k.grep(e,function(e){return e===n!==r}):"string"!=typeof n?k.grep(e,function(e){return-1<i.call(n,e)!==r}):k.filter(n,e,r)}k.filter=function(e,t,n){var r=t[0];return n&&(e=":not("+e+")"),1===t.length&&1===r.nodeType?k.find.matchesSelector(r,e)?[r]:[]:k.find.matches(e,k.grep(t,function(e){return 1===e.nodeType}))},k.fn.extend({find:function(e){var t,n,r=this.length,i=this;if("string"!=typeof e)return this.pushStack(k(e).filter(function(){for(t=0;t<r;t++)if(k.contains(i[t],this))return!0}));for(n=this.pushStack([]),t=0;t<r;t++)k.find(e,i[t],n);return 1<r?k.uniqueSort(n):n},filter:function(e){return this.pushStack(j(this,e||[],!1))},not:function(e){return this.pushStack(j(this,e||[],!0))},is:function(e){return!!j(this,"string"==typeof e&&N.test(e)?k(e):e||[],!1).length}});var q,L=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]+))$/;(k.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||q,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:L.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof k?t[0]:t,k.merge(this,k.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:E,!0)),D.test(r[1])&&k.isPlainObject(t))for(r in t)m(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=E.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):m(e)?void 0!==n.ready?n.ready(e):e(k):k.makeArray(e,this)}).prototype=k.fn,q=k(E);var H=/^(?:parents|prev(?:Until|All))/,O={children:!0,contents:!0,next:!0,prev:!0};function P(e,t){while((e=e[t])&&1!==e.nodeType);return e}k.fn.extend({has:function(e){var t=k(e,this),n=t.length;return this.filter(function(){for(var e=0;e<n;e++)if(k.contains(this,t[e]))return!0})},closest:function(e,t){var n,r=0,i=this.length,o=[],a="string"!=typeof e&&k(e);if(!N.test(e))for(;r<i;r++)for(n=this[r];n&&n!==t;n=n.parentNode)if(n.nodeType<11&&(a?-1<a.index(n):1===n.nodeType&&k.find.matchesSelector(n,e))){o.push(n);break}return this.pushStack(1<o.length?k.uniqueSort(o):o)},index:function(e){return e?"string"==typeof e?i.call(k(e),this[0]):i.call(this,e.jquery?e[0]:e):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(e,t){return this.pushStack(k.uniqueSort(k.merge(this.get(),k(e,t))))},addBack:function(e){return this.add(null==e?this.prevObject:this.prevObject.filter(e))}}),k.each({parent:function(e){var t=e.parentNode;return t&&11!==t.nodeType?t:null},parents:function(e){return T(e,"parentNode")},parentsUntil:function(e,t,n){return T(e,"parentNode",n)},next:function(e){return P(e,"nextSibling")},prev:function(e){return P(e,"previousSibling")},nextAll:function(e){return T(e,"nextSibling")},prevAll:function(e){return T(e,"previousSibling")},nextUntil:function(e,t,n){return T(e,"nextSibling",n)},prevUntil:function(e,t,n){return T(e,"previousSibling",n)},siblings:function(e){return S((e.parentNode||{}).firstChild,e)},children:function(e){return S(e.firstChild)},contents:function(e){return"undefined"!=typeof e.contentDocument?e.contentDocument:(A(e,"template")&&(e=e.content||e),k.merge([],e.childNodes))}},function(r,i){k.fn[r]=function(e,t){var n=k.map(this,i,e);return"Until"!==r.slice(-5)&&(t=e),t&&"string"==typeof t&&(n=k.filter(t,n)),1<this.length&&(O[r]||k.uniqueSort(n),H.test(r)&&n.reverse()),this.pushStack(n)}});var R=/[^\x20\t\r\n\f]+/g;function M(e){return e}function I(e){throw e}function W(e,t,n,r){var i;try{e&&m(i=e.promise)?i.call(e).done(t).fail(n):e&&m(i=e.then)?i.call(e,t,n):t.apply(void 0,[e].slice(r))}catch(e){n.apply(void 0,[e])}}k.Callbacks=function(r){var e,n;r="string"==typeof r?(e=r,n={},k.each(e.match(R)||[],function(e,t){n[t]=!0}),n):k.extend({},r);var i,t,o,a,s=[],u=[],l=-1,c=function(){for(a=a||r.once,o=i=!0;u.length;l=-1){t=u.shift();while(++l<s.length)!1===s[l].apply(t[0],t[1])&&r.stopOnFalse&&(l=s.length,t=!1)}r.memory||(t=!1),i=!1,a&&(s=t?[]:"")},f={add:function(){return s&&(t&&!i&&(l=s.length-1,u.push(t)),function n(e){k.each(e,function(e,t){m(t)?r.unique&&f.has(t)||s.push(t):t&&t.length&&"string"!==w(t)&&n(t)})}(arguments),t&&!i&&c()),this},remove:function(){return k.each(arguments,function(e,t){var n;while(-1<(n=k.inArray(t,s,n)))s.splice(n,1),n<=l&&l--}),this},has:function(e){return e?-1<k.inArray(e,s):0<s.length},empty:function(){return s&&(s=[]),this},disable:function(){return a=u=[],s=t="",this},disabled:function(){return!s},lock:function(){return a=u=[],t||i||(s=t=""),this},locked:function(){return!!a},fireWith:function(e,t){return a||(t=[e,(t=t||[]).slice?t.slice():t],u.push(t),i||c()),this},fire:function(){return f.fireWith(this,arguments),this},fired:function(){return!!o}};return f},k.extend({Deferred:function(e){var o=[["notify","progress",k.Callbacks("memory"),k.Callbacks("memory"),2],["resolve","done",k.Callbacks("once memory"),k.Callbacks("once memory"),0,"resolved"],["reject","fail",k.Callbacks("once memory"),k.Callbacks("once memory"),1,"rejected"]],i="pending",a={state:function(){return i},always:function(){return s.done(arguments).fail(arguments),this},"catch":function(e){return a.then(null,e)},pipe:function(){var i=arguments;return k.Deferred(function(r){k.each(o,function(e,t){var n=m(i[t[4]])&&i[t[4]];s[t[1]](function(){var e=n&&n.apply(this,arguments);e&&m(e.promise)?e.promise().progress(r.notify).done(r.resolve).fail(r.reject):r[t[0]+"With"](this,n?[e]:arguments)})}),i=null}).promise()},then:function(t,n,r){var u=0;function l(i,o,a,s){return function(){var n=this,r=arguments,e=function(){var e,t;if(!(i<u)){if((e=a.apply(n,r))===o.promise())throw new TypeError("Thenable self-resolution");t=e&&("object"==typeof e||"function"==typeof e)&&e.then,m(t)?s?t.call(e,l(u,o,M,s),l(u,o,I,s)):(u++,t.call(e,l(u,o,M,s),l(u,o,I,s),l(u,o,M,o.notifyWith))):(a!==M&&(n=void 0,r=[e]),(s||o.resolveWith)(n,r))}},t=s?e:function(){try{e()}catch(e){k.Deferred.exceptionHook&&k.Deferred.exceptionHook(e,t.stackTrace),u<=i+1&&(a!==I&&(n=void 0,r=[e]),o.rejectWith(n,r))}};i?t():(k.Deferred.getStackHook&&(t.stackTrace=k.Deferred.getStackHook()),C.setTimeout(t))}}return k.Deferred(function(e){o[0][3].add(l(0,e,m(r)?r:M,e.notifyWith)),o[1][3].add(l(0,e,m(t)?t:M)),o[2][3].add(l(0,e,m(n)?n:I))}).promise()},promise:function(e){return null!=e?k.extend(e,a):a}},s={};return k.each(o,function(e,t){var n=t[2],r=t[5];a[t[1]]=n.add,r&&n.add(function(){i=r},o[3-e][2].disable,o[3-e][3].disable,o[0][2].lock,o[0][3].lock),n.add(t[3].fire),s[t[0]]=function(){return s[t[0]+"With"](this===s?void 0:this,arguments),this},s[t[0]+"With"]=n.fireWith}),a.promise(s),e&&e.call(s,s),s},when:function(e){var n=arguments.length,t=n,r=Array(t),i=s.call(arguments),o=k.Deferred(),a=function(t){return function(e){r[t]=this,i[t]=1<arguments.length?s.call(arguments):e,--n||o.resolveWith(r,i)}};if(n<=1&&(W(e,o.done(a(t)).resolve,o.reject,!n),"pending"===o.state()||m(i[t]&&i[t].then)))return o.then();while(t--)W(i[t],a(t),o.reject);return o.promise()}});var $=/^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/;k.Deferred.exceptionHook=function(e,t){C.console&&C.console.warn&&e&&$.test(e.name)&&C.console.warn("jQuery.Deferred exception: "+e.message,e.stack,t)},k.readyException=function(e){C.setTimeout(function(){throw e})};var F=k.Deferred();function B(){E.removeEventListener("DOMContentLoaded",B),C.removeEventListener("load",B),k.ready()}k.fn.ready=function(e){return F.then(e)["catch"](function(e){k.readyException(e)}),this},k.extend({isReady:!1,readyWait:1,ready:function(e){(!0===e?--k.readyWait:k.isReady)||(k.isReady=!0)!==e&&0<--k.readyWait||F.resolveWith(E,[k])}}),k.ready.then=F.then,"complete"===E.readyState||"loading"!==E.readyState&&!E.documentElement.doScroll?C.setTimeout(k.ready):(E.addEventListener("DOMContentLoaded",B),C.addEventListener("load",B));var _=function(e,t,n,r,i,o,a){var s=0,u=e.length,l=null==n;if("object"===w(n))for(s in i=!0,n)_(e,t,s,n[s],!0,o,a);else if(void 0!==r&&(i=!0,m(r)||(a=!0),l&&(a?(t.call(e,r),t=null):(l=t,t=function(e,t,n){return l.call(k(e),n)})),t))for(;s<u;s++)t(e[s],n,a?r:r.call(e[s],s,t(e[s],n)));return i?e:l?t.call(e):u?t(e[0],n):o},z=/^-ms-/,U=/-([a-z])/g;function X(e,t){return t.toUpperCase()}function V(e){return e.replace(z,"ms-").replace(U,X)}var G=function(e){return 1===e.nodeType||9===e.nodeType||!+e.nodeType};function Y(){this.expando=k.expando+Y.uid++}Y.uid=1,Y.prototype={cache:function(e){var t=e[this.expando];return t||(t={},G(e)&&(e.nodeType?e[this.expando]=t:Object.defineProperty(e,this.expando,{value:t,configurable:!0}))),t},set:function(e,t,n){var r,i=this.cache(e);if("string"==typeof t)i[V(t)]=n;else for(r in t)i[V(r)]=t[r];return i},get:function(e,t){return void 0===t?this.cache(e):e[this.expando]&&e[this.expando][V(t)]},access:function(e,t,n){return void 0===t||t&&"string"==typeof t&&void 0===n?this.get(e,t):(this.set(e,t,n),void 0!==n?n:t)},remove:function(e,t){var n,r=e[this.expando];if(void 0!==r){if(void 0!==t){n=(t=Array.isArray(t)?t.map(V):(t=V(t))in r?[t]:t.match(R)||[]).length;while(n--)delete r[t[n]]}(void 0===t||k.isEmptyObject(r))&&(e.nodeType?e[this.expando]=void 0:delete e[this.expando])}},hasData:function(e){var t=e[this.expando];return void 0!==t&&!k.isEmptyObject(t)}};var Q=new Y,J=new Y,K=/^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,Z=/[A-Z]/g;function ee(e,t,n){var r,i;if(void 0===n&&1===e.nodeType)if(r="data-"+t.replace(Z,"-$&").toLowerCase(),"string"==typeof(n=e.getAttribute(r))){try{n="true"===(i=n)||"false"!==i&&("null"===i?null:i===+i+""?+i:K.test(i)?JSON.parse(i):i)}catch(e){}J.set(e,t,n)}else n=void 0;return n}k.extend({hasData:function(e){return J.hasData(e)||Q.hasData(e)},data:function(e,t,n){return J.access(e,t,n)},removeData:function(e,t){J.remove(e,t)},_data:function(e,t,n){return Q.access(e,t,n)},_removeData:function(e,t){Q.remove(e,t)}}),k.fn.extend({data:function(n,e){var t,r,i,o=this[0],a=o&&o.attributes;if(void 0===n){if(this.length&&(i=J.get(o),1===o.nodeType&&!Q.get(o,"hasDataAttrs"))){t=a.length;while(t--)a[t]&&0===(r=a[t].name).indexOf("data-")&&(r=V(r.slice(5)),ee(o,r,i[r]));Q.set(o,"hasDataAttrs",!0)}return i}return"object"==typeof n?this.each(function(){J.set(this,n)}):_(this,function(e){var t;if(o&&void 0===e)return void 0!==(t=J.get(o,n))?t:void 0!==(t=ee(o,n))?t:void 0;this.each(function(){J.set(this,n,e)})},null,e,1<arguments.length,null,!0)},removeData:function(e){return this.each(function(){J.remove(this,e)})}}),k.extend({queue:function(e,t,n){var r;if(e)return t=(t||"fx")+"queue",r=Q.get(e,t),n&&(!r||Array.isArray(n)?r=Q.access(e,t,k.makeArray(n)):r.push(n)),r||[]},dequeue:function(e,t){t=t||"fx";var n=k.queue(e,t),r=n.length,i=n.shift(),o=k._queueHooks(e,t);"inprogress"===i&&(i=n.shift(),r--),i&&("fx"===t&&n.unshift("inprogress"),delete o.stop,i.call(e,function(){k.dequeue(e,t)},o)),!r&&o&&o.empty.fire()},_queueHooks:function(e,t){var n=t+"queueHooks";return Q.get(e,n)||Q.access(e,n,{empty:k.Callbacks("once memory").add(function(){Q.remove(e,[t+"queue",n])})})}}),k.fn.extend({queue:function(t,n){var e=2;return"string"!=typeof t&&(n=t,t="fx",e--),arguments.length<e?k.queue(this[0],t):void 0===n?this:this.each(function(){var e=k.queue(this,t,n);k._queueHooks(this,t),"fx"===t&&"inprogress"!==e[0]&&k.dequeue(this,t)})},dequeue:function(e){return this.each(function(){k.dequeue(this,e)})},clearQueue:function(e){return this.queue(e||"fx",[])},promise:function(e,t){var n,r=1,i=k.Deferred(),o=this,a=this.length,s=function(){--r||i.resolveWith(o,[o])};"string"!=typeof e&&(t=e,e=void 0),e=e||"fx";while(a--)(n=Q.get(o[a],e+"queueHooks"))&&n.empty&&(r++,n.empty.add(s));return s(),i.promise(t)}});var te=/[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/.source,ne=new RegExp("^(?:([+-])=|)("+te+")([a-z%]*)$","i"),re=["Top","Right","Bottom","Left"],ie=E.documentElement,oe=function(e){return k.contains(e.ownerDocument,e)},ae={composed:!0};ie.getRootNode&&(oe=function(e){return k.contains(e.ownerDocument,e)||e.getRootNode(ae)===e.ownerDocument});var se=function(e,t){return"none"===(e=t||e).style.display||""===e.style.display&&oe(e)&&"none"===k.css(e,"display")},ue=function(e,t,n,r){var i,o,a={};for(o in t)a[o]=e.style[o],e.style[o]=t[o];for(o in i=n.apply(e,r||[]),t)e.style[o]=a[o];return i};function le(e,t,n,r){var i,o,a=20,s=r?function(){return r.cur()}:function(){return k.css(e,t,"")},u=s(),l=n&&n[3]||(k.cssNumber[t]?"":"px"),c=e.nodeType&&(k.cssNumber[t]||"px"!==l&&+u)&&ne.exec(k.css(e,t));if(c&&c[3]!==l){u/=2,l=l||c[3],c=+u||1;while(a--)k.style(e,t,c+l),(1-o)*(1-(o=s()/u||.5))<=0&&(a=0),c/=o;c*=2,k.style(e,t,c+l),n=n||[]}return n&&(c=+c||+u||0,i=n[1]?c+(n[1]+1)*n[2]:+n[2],r&&(r.unit=l,r.start=c,r.end=i)),i}var ce={};function fe(e,t){for(var n,r,i,o,a,s,u,l=[],c=0,f=e.length;c<f;c++)(r=e[c]).style&&(n=r.style.display,t?("none"===n&&(l[c]=Q.get(r,"display")||null,l[c]||(r.style.display="")),""===r.style.display&&se(r)&&(l[c]=(u=a=o=void 0,a=(i=r).ownerDocument,s=i.nodeName,(u=ce[s])||(o=a.body.appendChild(a.createElement(s)),u=k.css(o,"display"),o.parentNode.removeChild(o),"none"===u&&(u="block"),ce[s]=u)))):"none"!==n&&(l[c]="none",Q.set(r,"display",n)));for(c=0;c<f;c++)null!=l[c]&&(e[c].style.display=l[c]);return e}k.fn.extend({show:function(){return fe(this,!0)},hide:function(){return fe(this)},toggle:function(e){return"boolean"==typeof e?e?this.show():this.hide():this.each(function(){se(this)?k(this).show():k(this).hide()})}});var pe=/^(?:checkbox|radio)$/i,de=/<([a-z][^\/\0>\x20\t\r\n\f]*)/i,he=/^$|^module$|\/(?:java|ecma)script/i,ge={option:[1,"<select multiple='multiple'>","</select>"],thead:[1,"<table>","</table>"],col:[2,"<table><colgroup>","</colgroup></table>"],tr:[2,"<table><tbody>","</tbody></table>"],td:[3,"<table><tbody><tr>","</tr></tbody></table>"],_default:[0,"",""]};function ve(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&A(e,t)?k.merge([e],n):n}function ye(e,t){for(var n=0,r=e.length;n<r;n++)Q.set(e[n],"globalEval",!t||Q.get(t[n],"globalEval"))}ge.optgroup=ge.option,ge.tbody=ge.tfoot=ge.colgroup=ge.caption=ge.thead,ge.th=ge.td;var me,xe,be=/<|&#?\w+;/;function we(e,t,n,r,i){for(var o,a,s,u,l,c,f=t.createDocumentFragment(),p=[],d=0,h=e.length;d<h;d++)if((o=e[d])||0===o)if("object"===w(o))k.merge(p,o.nodeType?[o]:o);else if(be.test(o)){a=a||f.appendChild(t.createElement("div")),s=(de.exec(o)||["",""])[1].toLowerCase(),u=ge[s]||ge._default,a.innerHTML=u[1]+k.htmlPrefilter(o)+u[2],c=u[0];while(c--)a=a.lastChild;k.merge(p,a.childNodes),(a=f.firstChild).textContent=""}else p.push(t.createTextNode(o));f.textContent="",d=0;while(o=p[d++])if(r&&-1<k.inArray(o,r))i&&i.push(o);else if(l=oe(o),a=ve(f.appendChild(o),"script"),l&&ye(a),n){c=0;while(o=a[c++])he.test(o.type||"")&&n.push(o)}return f}me=E.createDocumentFragment().appendChild(E.createElement("div")),(xe=E.createElement("input")).setAttribute("type","radio"),xe.setAttribute("checked","checked"),xe.setAttribute("name","t"),me.appendChild(xe),y.checkClone=me.cloneNode(!0).cloneNode(!0).lastChild.checked,me.innerHTML="<textarea>x</textarea>",y.noCloneChecked=!!me.cloneNode(!0).lastChild.defaultValue;var Te=/^key/,Ce=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,Ee=/^([^.]*)(?:\.(.+)|)/;function ke(){return!0}function Se(){return!1}function Ne(e,t){return e===function(){try{return E.activeElement}catch(e){}}()==("focus"===t)}function Ae(e,t,n,r,i,o){var a,s;if("object"==typeof t){for(s in"string"!=typeof n&&(r=r||n,n=void 0),t)Ae(e,s,n,r,t[s],o);return e}if(null==r&&null==i?(i=n,r=n=void 0):null==i&&("string"==typeof n?(i=r,r=void 0):(i=r,r=n,n=void 0)),!1===i)i=Se;else if(!i)return e;return 1===o&&(a=i,(i=function(e){return k().off(e),a.apply(this,arguments)}).guid=a.guid||(a.guid=k.guid++)),e.each(function(){k.event.add(this,t,i,r,n)})}function De(e,i,o){o?(Q.set(e,i,!1),k.event.add(e,i,{namespace:!1,handler:function(e){var t,n,r=Q.get(this,i);if(1&e.isTrigger&&this[i]){if(r.length)(k.event.special[i]||{}).delegateType&&e.stopPropagation();else if(r=s.call(arguments),Q.set(this,i,r),t=o(this,i),this[i](),r!==(n=Q.get(this,i))||t?Q.set(this,i,!1):n={},r!==n)return e.stopImmediatePropagation(),e.preventDefault(),n.value}else r.length&&(Q.set(this,i,{value:k.event.trigger(k.extend(r[0],k.Event.prototype),r.slice(1),this)}),e.stopImmediatePropagation())}})):void 0===Q.get(e,i)&&k.event.add(e,i,ke)}k.event={global:{},add:function(t,e,n,r,i){var o,a,s,u,l,c,f,p,d,h,g,v=Q.get(t);if(v){n.handler&&(n=(o=n).handler,i=o.selector),i&&k.find.matchesSelector(ie,i),n.guid||(n.guid=k.guid++),(u=v.events)||(u=v.events={}),(a=v.handle)||(a=v.handle=function(e){return"undefined"!=typeof k&&k.event.triggered!==e.type?k.event.dispatch.apply(t,arguments):void 0}),l=(e=(e||"").match(R)||[""]).length;while(l--)d=g=(s=Ee.exec(e[l])||[])[1],h=(s[2]||"").split(".").sort(),d&&(f=k.event.special[d]||{},d=(i?f.delegateType:f.bindType)||d,f=k.event.special[d]||{},c=k.extend({type:d,origType:g,data:r,handler:n,guid:n.guid,selector:i,needsContext:i&&k.expr.match.needsContext.test(i),namespace:h.join(".")},o),(p=u[d])||((p=u[d]=[]).delegateCount=0,f.setup&&!1!==f.setup.call(t,r,h,a)||t.addEventListener&&t.addEventListener(d,a)),f.add&&(f.add.call(t,c),c.handler.guid||(c.handler.guid=n.guid)),i?p.splice(p.delegateCount++,0,c):p.push(c),k.event.global[d]=!0)}},remove:function(e,t,n,r,i){var o,a,s,u,l,c,f,p,d,h,g,v=Q.hasData(e)&&Q.get(e);if(v&&(u=v.events)){l=(t=(t||"").match(R)||[""]).length;while(l--)if(d=g=(s=Ee.exec(t[l])||[])[1],h=(s[2]||"").split(".").sort(),d){f=k.event.special[d]||{},p=u[d=(r?f.delegateType:f.bindType)||d]||[],s=s[2]&&new RegExp("(^|\\.)"+h.join("\\.(?:.*\\.|)")+"(\\.|$)"),a=o=p.length;while(o--)c=p[o],!i&&g!==c.origType||n&&n.guid!==c.guid||s&&!s.test(c.namespace)||r&&r!==c.selector&&("**"!==r||!c.selector)||(p.splice(o,1),c.selector&&p.delegateCount--,f.remove&&f.remove.call(e,c));a&&!p.length&&(f.teardown&&!1!==f.teardown.call(e,h,v.handle)||k.removeEvent(e,d,v.handle),delete u[d])}else for(d in u)k.event.remove(e,d+t[l],n,r,!0);k.isEmptyObject(u)&&Q.remove(e,"handle events")}},dispatch:function(e){var t,n,r,i,o,a,s=k.event.fix(e),u=new Array(arguments.length),l=(Q.get(this,"events")||{})[s.type]||[],c=k.event.special[s.type]||{};for(u[0]=s,t=1;t<arguments.length;t++)u[t]=arguments[t];if(s.delegateTarget=this,!c.preDispatch||!1!==c.preDispatch.call(this,s)){a=k.event.handlers.call(this,s,l),t=0;while((i=a[t++])&&!s.isPropagationStopped()){s.currentTarget=i.elem,n=0;while((o=i.handlers[n++])&&!s.isImmediatePropagationStopped())s.rnamespace&&!1!==o.namespace&&!s.rnamespace.test(o.namespace)||(s.handleObj=o,s.data=o.data,void 0!==(r=((k.event.special[o.origType]||{}).handle||o.handler).apply(i.elem,u))&&!1===(s.result=r)&&(s.preventDefault(),s.stopPropagation()))}return c.postDispatch&&c.postDispatch.call(this,s),s.result}},handlers:function(e,t){var n,r,i,o,a,s=[],u=t.delegateCount,l=e.target;if(u&&l.nodeType&&!("click"===e.type&&1<=e.button))for(;l!==this;l=l.parentNode||this)if(1===l.nodeType&&("click"!==e.type||!0!==l.disabled)){for(o=[],a={},n=0;n<u;n++)void 0===a[i=(r=t[n]).selector+" "]&&(a[i]=r.needsContext?-1<k(i,this).index(l):k.find(i,this,null,[l]).length),a[i]&&o.push(r);o.length&&s.push({elem:l,handlers:o})}return l=this,u<t.length&&s.push({elem:l,handlers:t.slice(u)}),s},addProp:function(t,e){Object.defineProperty(k.Event.prototype,t,{enumerable:!0,configurable:!0,get:m(e)?function(){if(this.originalEvent)return e(this.originalEvent)}:function(){if(this.originalEvent)return this.originalEvent[t]},set:function(e){Object.defineProperty(this,t,{enumerable:!0,configurable:!0,writable:!0,value:e})}})},fix:function(e){return e[k.expando]?e:new k.Event(e)},special:{load:{noBubble:!0},click:{setup:function(e){var t=this||e;return pe.test(t.type)&&t.click&&A(t,"input")&&De(t,"click",ke),!1},trigger:function(e){var t=this||e;return pe.test(t.type)&&t.click&&A(t,"input")&&De(t,"click"),!0},_default:function(e){var t=e.target;return pe.test(t.type)&&t.click&&A(t,"input")&&Q.get(t,"click")||A(t,"a")}},beforeunload:{postDispatch:function(e){void 0!==e.result&&e.originalEvent&&(e.originalEvent.returnValue=e.result)}}}},k.removeEvent=function(e,t,n){e.removeEventListener&&e.removeEventListener(t,n)},k.Event=function(e,t){if(!(this instanceof k.Event))return new k.Event(e,t);e&&e.type?(this.originalEvent=e,this.type=e.type,this.isDefaultPrevented=e.defaultPrevented||void 0===e.defaultPrevented&&!1===e.returnValue?ke:Se,this.target=e.target&&3===e.target.nodeType?e.target.parentNode:e.target,this.currentTarget=e.currentTarget,this.relatedTarget=e.relatedTarget):this.type=e,t&&k.extend(this,t),this.timeStamp=e&&e.timeStamp||Date.now(),this[k.expando]=!0},k.Event.prototype={constructor:k.Event,isDefaultPrevented:Se,isPropagationStopped:Se,isImmediatePropagationStopped:Se,isSimulated:!1,preventDefault:function(){var e=this.originalEvent;this.isDefaultPrevented=ke,e&&!this.isSimulated&&e.preventDefault()},stopPropagation:function(){var e=this.originalEvent;this.isPropagationStopped=ke,e&&!this.isSimulated&&e.stopPropagation()},stopImmediatePropagation:function(){var e=this.originalEvent;this.isImmediatePropagationStopped=ke,e&&!this.isSimulated&&e.stopImmediatePropagation(),this.stopPropagation()}},k.each({altKey:!0,bubbles:!0,cancelable:!0,changedTouches:!0,ctrlKey:!0,detail:!0,eventPhase:!0,metaKey:!0,pageX:!0,pageY:!0,shiftKey:!0,view:!0,"char":!0,code:!0,charCode:!0,key:!0,keyCode:!0,button:!0,buttons:!0,clientX:!0,clientY:!0,offsetX:!0,offsetY:!0,pointerId:!0,pointerType:!0,screenX:!0,screenY:!0,targetTouches:!0,toElement:!0,touches:!0,which:function(e){var t=e.button;return null==e.which&&Te.test(e.type)?null!=e.charCode?e.charCode:e.keyCode:!e.which&&void 0!==t&&Ce.test(e.type)?1&t?1:2&t?3:4&t?2:0:e.which}},k.event.addProp),k.each({focus:"focusin",blur:"focusout"},function(e,t){k.event.special[e]={setup:function(){return De(this,e,Ne),!1},trigger:function(){return De(this,e),!0},delegateType:t}}),k.each({mouseenter:"mouseover",mouseleave:"mouseout",pointerenter:"pointerover",pointerleave:"pointerout"},function(e,i){k.event.special[e]={delegateType:i,bindType:i,handle:function(e){var t,n=e.relatedTarget,r=e.handleObj;return n&&(n===this||k.contains(this,n))||(e.type=r.origType,t=r.handler.apply(this,arguments),e.type=i),t}}}),k.fn.extend({on:function(e,t,n,r){return Ae(this,e,t,n,r)},one:function(e,t,n,r){return Ae(this,e,t,n,r,1)},off:function(e,t,n){var r,i;if(e&&e.preventDefault&&e.handleObj)return r=e.handleObj,k(e.delegateTarget).off(r.namespace?r.origType+"."+r.namespace:r.origType,r.selector,r.handler),this;if("object"==typeof e){for(i in e)this.off(i,t,e[i]);return this}return!1!==t&&"function"!=typeof t||(n=t,t=void 0),!1===n&&(n=Se),this.each(function(){k.event.remove(this,e,n,t)})}});var je=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([a-z][^\/\0>\x20\t\r\n\f]*)[^>]*)\/>/gi,qe=/<script|<style|<link/i,Le=/checked\s*(?:[^=]|=\s*.checked.)/i,He=/^\s*<!(?:\[CDATA\[|--)|(?:\]\]|--)>\s*$/g;function Oe(e,t){return A(e,"table")&&A(11!==t.nodeType?t:t.firstChild,"tr")&&k(e).children("tbody")[0]||e}function Pe(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function Re(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Me(e,t){var n,r,i,o,a,s,u,l;if(1===t.nodeType){if(Q.hasData(e)&&(o=Q.access(e),a=Q.set(t,o),l=o.events))for(i in delete a.handle,a.events={},l)for(n=0,r=l[i].length;n<r;n++)k.event.add(t,i,l[i][n]);J.hasData(e)&&(s=J.access(e),u=k.extend({},s),J.set(t,u))}}function Ie(n,r,i,o){r=g.apply([],r);var e,t,a,s,u,l,c=0,f=n.length,p=f-1,d=r[0],h=m(d);if(h||1<f&&"string"==typeof d&&!y.checkClone&&Le.test(d))return n.each(function(e){var t=n.eq(e);h&&(r[0]=d.call(this,e,t.html())),Ie(t,r,i,o)});if(f&&(t=(e=we(r,n[0].ownerDocument,!1,n,o)).firstChild,1===e.childNodes.length&&(e=t),t||o)){for(s=(a=k.map(ve(e,"script"),Pe)).length;c<f;c++)u=e,c!==p&&(u=k.clone(u,!0,!0),s&&k.merge(a,ve(u,"script"))),i.call(n[c],u,c);if(s)for(l=a[a.length-1].ownerDocument,k.map(a,Re),c=0;c<s;c++)u=a[c],he.test(u.type||"")&&!Q.access(u,"globalEval")&&k.contains(l,u)&&(u.src&&"module"!==(u.type||"").toLowerCase()?k._evalUrl&&!u.noModule&&k._evalUrl(u.src,{nonce:u.nonce||u.getAttribute("nonce")}):b(u.textContent.replace(He,""),u,l))}return n}function We(e,t,n){for(var r,i=t?k.filter(t,e):e,o=0;null!=(r=i[o]);o++)n||1!==r.nodeType||k.cleanData(ve(r)),r.parentNode&&(n&&oe(r)&&ye(ve(r,"script")),r.parentNode.removeChild(r));return e}k.extend({htmlPrefilter:function(e){return e.replace(je,"<$1></$2>")},clone:function(e,t,n){var r,i,o,a,s,u,l,c=e.cloneNode(!0),f=oe(e);if(!(y.noCloneChecked||1!==e.nodeType&&11!==e.nodeType||k.isXMLDoc(e)))for(a=ve(c),r=0,i=(o=ve(e)).length;r<i;r++)s=o[r],u=a[r],void 0,"input"===(l=u.nodeName.toLowerCase())&&pe.test(s.type)?u.checked=s.checked:"input"!==l&&"textarea"!==l||(u.defaultValue=s.defaultValue);if(t)if(n)for(o=o||ve(e),a=a||ve(c),r=0,i=o.length;r<i;r++)Me(o[r],a[r]);else Me(e,c);return 0<(a=ve(c,"script")).length&&ye(a,!f&&ve(e,"script")),c},cleanData:function(e){for(var t,n,r,i=k.event.special,o=0;void 0!==(n=e[o]);o++)if(G(n)){if(t=n[Q.expando]){if(t.events)for(r in t.events)i[r]?k.event.remove(n,r):k.removeEvent(n,r,t.handle);n[Q.expando]=void 0}n[J.expando]&&(n[J.expando]=void 0)}}}),k.fn.extend({detach:function(e){return We(this,e,!0)},remove:function(e){return We(this,e)},text:function(e){return _(this,function(e){return void 0===e?k.text(this):this.empty().each(function(){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||(this.textContent=e)})},null,e,arguments.length)},append:function(){return Ie(this,arguments,function(e){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||Oe(this,e).appendChild(e)})},prepend:function(){return Ie(this,arguments,function(e){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var t=Oe(this,e);t.insertBefore(e,t.firstChild)}})},before:function(){return Ie(this,arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this)})},after:function(){return Ie(this,arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this.nextSibling)})},empty:function(){for(var e,t=0;null!=(e=this[t]);t++)1===e.nodeType&&(k.cleanData(ve(e,!1)),e.textContent="");return this},clone:function(e,t){return e=null!=e&&e,t=null==t?e:t,this.map(function(){return k.clone(this,e,t)})},html:function(e){return _(this,function(e){var t=this[0]||{},n=0,r=this.length;if(void 0===e&&1===t.nodeType)return t.innerHTML;if("string"==typeof e&&!qe.test(e)&&!ge[(de.exec(e)||["",""])[1].toLowerCase()]){e=k.htmlPrefilter(e);try{for(;n<r;n++)1===(t=this[n]||{}).nodeType&&(k.cleanData(ve(t,!1)),t.innerHTML=e);t=0}catch(e){}}t&&this.empty().append(e)},null,e,arguments.length)},replaceWith:function(){var n=[];return Ie(this,arguments,function(e){var t=this.parentNode;k.inArray(this,n)<0&&(k.cleanData(ve(this)),t&&t.replaceChild(e,this))},n)}}),k.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(e,a){k.fn[e]=function(e){for(var t,n=[],r=k(e),i=r.length-1,o=0;o<=i;o++)t=o===i?this:this.clone(!0),k(r[o])[a](t),u.apply(n,t.get());return this.pushStack(n)}});var $e=new RegExp("^("+te+")(?!px)[a-z%]+$","i"),Fe=function(e){var t=e.ownerDocument.defaultView;return t&&t.opener||(t=C),t.getComputedStyle(e)},Be=new RegExp(re.join("|"),"i");function _e(e,t,n){var r,i,o,a,s=e.style;return(n=n||Fe(e))&&(""!==(a=n.getPropertyValue(t)||n[t])||oe(e)||(a=k.style(e,t)),!y.pixelBoxStyles()&&$e.test(a)&&Be.test(t)&&(r=s.width,i=s.minWidth,o=s.maxWidth,s.minWidth=s.maxWidth=s.width=a,a=n.width,s.width=r,s.minWidth=i,s.maxWidth=o)),void 0!==a?a+"":a}function ze(e,t){return{get:function(){if(!e())return(this.get=t).apply(this,arguments);delete this.get}}}!function(){function e(){if(u){s.style.cssText="position:absolute;left:-11111px;width:60px;margin-top:1px;padding:0;border:0",u.style.cssText="position:relative;display:block;box-sizing:border-box;overflow:scroll;margin:auto;border:1px;padding:1px;width:60%;top:1%",ie.appendChild(s).appendChild(u);var e=C.getComputedStyle(u);n="1%"!==e.top,a=12===t(e.marginLeft),u.style.right="60%",o=36===t(e.right),r=36===t(e.width),u.style.position="absolute",i=12===t(u.offsetWidth/3),ie.removeChild(s),u=null}}function t(e){return Math.round(parseFloat(e))}var n,r,i,o,a,s=E.createElement("div"),u=E.createElement("div");u.style&&(u.style.backgroundClip="content-box",u.cloneNode(!0).style.backgroundClip="",y.clearCloneStyle="content-box"===u.style.backgroundClip,k.extend(y,{boxSizingReliable:function(){return e(),r},pixelBoxStyles:function(){return e(),o},pixelPosition:function(){return e(),n},reliableMarginLeft:function(){return e(),a},scrollboxSize:function(){return e(),i}}))}();var Ue=["Webkit","Moz","ms"],Xe=E.createElement("div").style,Ve={};function Ge(e){var t=k.cssProps[e]||Ve[e];return t||(e in Xe?e:Ve[e]=function(e){var t=e[0].toUpperCase()+e.slice(1),n=Ue.length;while(n--)if((e=Ue[n]+t)in Xe)return e}(e)||e)}var Ye=/^(none|table(?!-c[ea]).+)/,Qe=/^--/,Je={position:"absolute",visibility:"hidden",display:"block"},Ke={letterSpacing:"0",fontWeight:"400"};function Ze(e,t,n){var r=ne.exec(t);return r?Math.max(0,r[2]-(n||0))+(r[3]||"px"):t}function et(e,t,n,r,i,o){var a="width"===t?1:0,s=0,u=0;if(n===(r?"border":"content"))return 0;for(;a<4;a+=2)"margin"===n&&(u+=k.css(e,n+re[a],!0,i)),r?("content"===n&&(u-=k.css(e,"padding"+re[a],!0,i)),"margin"!==n&&(u-=k.css(e,"border"+re[a]+"Width",!0,i))):(u+=k.css(e,"padding"+re[a],!0,i),"padding"!==n?u+=k.css(e,"border"+re[a]+"Width",!0,i):s+=k.css(e,"border"+re[a]+"Width",!0,i));return!r&&0<=o&&(u+=Math.max(0,Math.ceil(e["offset"+t[0].toUpperCase()+t.slice(1)]-o-u-s-.5))||0),u}function tt(e,t,n){var r=Fe(e),i=(!y.boxSizingReliable()||n)&&"border-box"===k.css(e,"boxSizing",!1,r),o=i,a=_e(e,t,r),s="offset"+t[0].toUpperCase()+t.slice(1);if($e.test(a)){if(!n)return a;a="auto"}return(!y.boxSizingReliable()&&i||"auto"===a||!parseFloat(a)&&"inline"===k.css(e,"display",!1,r))&&e.getClientRects().length&&(i="border-box"===k.css(e,"boxSizing",!1,r),(o=s in e)&&(a=e[s])),(a=parseFloat(a)||0)+et(e,t,n||(i?"border":"content"),o,r,a)+"px"}function nt(e,t,n,r,i){return new nt.prototype.init(e,t,n,r,i)}k.extend({cssHooks:{opacity:{get:function(e,t){if(t){var n=_e(e,"opacity");return""===n?"1":n}}}},cssNumber:{animationIterationCount:!0,columnCount:!0,fillOpacity:!0,flexGrow:!0,flexShrink:!0,fontWeight:!0,gridArea:!0,gridColumn:!0,gridColumnEnd:!0,gridColumnStart:!0,gridRow:!0,gridRowEnd:!0,gridRowStart:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{},style:function(e,t,n,r){if(e&&3!==e.nodeType&&8!==e.nodeType&&e.style){var i,o,a,s=V(t),u=Qe.test(t),l=e.style;if(u||(t=Ge(s)),a=k.cssHooks[t]||k.cssHooks[s],void 0===n)return a&&"get"in a&&void 0!==(i=a.get(e,!1,r))?i:l[t];"string"===(o=typeof n)&&(i=ne.exec(n))&&i[1]&&(n=le(e,t,i),o="number"),null!=n&&n==n&&("number"!==o||u||(n+=i&&i[3]||(k.cssNumber[s]?"":"px")),y.clearCloneStyle||""!==n||0!==t.indexOf("background")||(l[t]="inherit"),a&&"set"in a&&void 0===(n=a.set(e,n,r))||(u?l.setProperty(t,n):l[t]=n))}},css:function(e,t,n,r){var i,o,a,s=V(t);return Qe.test(t)||(t=Ge(s)),(a=k.cssHooks[t]||k.cssHooks[s])&&"get"in a&&(i=a.get(e,!0,n)),void 0===i&&(i=_e(e,t,r)),"normal"===i&&t in Ke&&(i=Ke[t]),""===n||n?(o=parseFloat(i),!0===n||isFinite(o)?o||0:i):i}}),k.each(["height","width"],function(e,u){k.cssHooks[u]={get:function(e,t,n){if(t)return!Ye.test(k.css(e,"display"))||e.getClientRects().length&&e.getBoundingClientRect().width?tt(e,u,n):ue(e,Je,function(){return tt(e,u,n)})},set:function(e,t,n){var r,i=Fe(e),o=!y.scrollboxSize()&&"absolute"===i.position,a=(o||n)&&"border-box"===k.css(e,"boxSizing",!1,i),s=n?et(e,u,n,a,i):0;return a&&o&&(s-=Math.ceil(e["offset"+u[0].toUpperCase()+u.slice(1)]-parseFloat(i[u])-et(e,u,"border",!1,i)-.5)),s&&(r=ne.exec(t))&&"px"!==(r[3]||"px")&&(e.style[u]=t,t=k.css(e,u)),Ze(0,t,s)}}}),k.cssHooks.marginLeft=ze(y.reliableMarginLeft,function(e,t){if(t)return(parseFloat(_e(e,"marginLeft"))||e.getBoundingClientRect().left-ue(e,{marginLeft:0},function(){return e.getBoundingClientRect().left}))+"px"}),k.each({margin:"",padding:"",border:"Width"},function(i,o){k.cssHooks[i+o]={expand:function(e){for(var t=0,n={},r="string"==typeof e?e.split(" "):[e];t<4;t++)n[i+re[t]+o]=r[t]||r[t-2]||r[0];return n}},"margin"!==i&&(k.cssHooks[i+o].set=Ze)}),k.fn.extend({css:function(e,t){return _(this,function(e,t,n){var r,i,o={},a=0;if(Array.isArray(t)){for(r=Fe(e),i=t.length;a<i;a++)o[t[a]]=k.css(e,t[a],!1,r);return o}return void 0!==n?k.style(e,t,n):k.css(e,t)},e,t,1<arguments.length)}}),((k.Tween=nt).prototype={constructor:nt,init:function(e,t,n,r,i,o){this.elem=e,this.prop=n,this.easing=i||k.easing._default,this.options=t,this.start=this.now=this.cur(),this.end=r,this.unit=o||(k.cssNumber[n]?"":"px")},cur:function(){var e=nt.propHooks[this.prop];return e&&e.get?e.get(this):nt.propHooks._default.get(this)},run:function(e){var t,n=nt.propHooks[this.prop];return this.options.duration?this.pos=t=k.easing[this.easing](e,this.options.duration*e,0,1,this.options.duration):this.pos=t=e,this.now=(this.end-this.start)*t+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),n&&n.set?n.set(this):nt.propHooks._default.set(this),this}}).init.prototype=nt.prototype,(nt.propHooks={_default:{get:function(e){var t;return 1!==e.elem.nodeType||null!=e.elem[e.prop]&&null==e.elem.style[e.prop]?e.elem[e.prop]:(t=k.css(e.elem,e.prop,""))&&"auto"!==t?t:0},set:function(e){k.fx.step[e.prop]?k.fx.step[e.prop](e):1!==e.elem.nodeType||!k.cssHooks[e.prop]&&null==e.elem.style[Ge(e.prop)]?e.elem[e.prop]=e.now:k.style(e.elem,e.prop,e.now+e.unit)}}}).scrollTop=nt.propHooks.scrollLeft={set:function(e){e.elem.nodeType&&e.elem.parentNode&&(e.elem[e.prop]=e.now)}},k.easing={linear:function(e){return e},swing:function(e){return.5-Math.cos(e*Math.PI)/2},_default:"swing"},k.fx=nt.prototype.init,k.fx.step={};var rt,it,ot,at,st=/^(?:toggle|show|hide)$/,ut=/queueHooks$/;function lt(){it&&(!1===E.hidden&&C.requestAnimationFrame?C.requestAnimationFrame(lt):C.setTimeout(lt,k.fx.interval),k.fx.tick())}function ct(){return C.setTimeout(function(){rt=void 0}),rt=Date.now()}function ft(e,t){var n,r=0,i={height:e};for(t=t?1:0;r<4;r+=2-t)i["margin"+(n=re[r])]=i["padding"+n]=e;return t&&(i.opacity=i.width=e),i}function pt(e,t,n){for(var r,i=(dt.tweeners[t]||[]).concat(dt.tweeners["*"]),o=0,a=i.length;o<a;o++)if(r=i[o].call(n,t,e))return r}function dt(o,e,t){var n,a,r=0,i=dt.prefilters.length,s=k.Deferred().always(function(){delete u.elem}),u=function(){if(a)return!1;for(var e=rt||ct(),t=Math.max(0,l.startTime+l.duration-e),n=1-(t/l.duration||0),r=0,i=l.tweens.length;r<i;r++)l.tweens[r].run(n);return s.notifyWith(o,[l,n,t]),n<1&&i?t:(i||s.notifyWith(o,[l,1,0]),s.resolveWith(o,[l]),!1)},l=s.promise({elem:o,props:k.extend({},e),opts:k.extend(!0,{specialEasing:{},easing:k.easing._default},t),originalProperties:e,originalOptions:t,startTime:rt||ct(),duration:t.duration,tweens:[],createTween:function(e,t){var n=k.Tween(o,l.opts,e,t,l.opts.specialEasing[e]||l.opts.easing);return l.tweens.push(n),n},stop:function(e){var t=0,n=e?l.tweens.length:0;if(a)return this;for(a=!0;t<n;t++)l.tweens[t].run(1);return e?(s.notifyWith(o,[l,1,0]),s.resolveWith(o,[l,e])):s.rejectWith(o,[l,e]),this}}),c=l.props;for(!function(e,t){var n,r,i,o,a;for(n in e)if(i=t[r=V(n)],o=e[n],Array.isArray(o)&&(i=o[1],o=e[n]=o[0]),n!==r&&(e[r]=o,delete e[n]),(a=k.cssHooks[r])&&"expand"in a)for(n in o=a.expand(o),delete e[r],o)n in e||(e[n]=o[n],t[n]=i);else t[r]=i}(c,l.opts.specialEasing);r<i;r++)if(n=dt.prefilters[r].call(l,o,c,l.opts))return m(n.stop)&&(k._queueHooks(l.elem,l.opts.queue).stop=n.stop.bind(n)),n;return k.map(c,pt,l),m(l.opts.start)&&l.opts.start.call(o,l),l.progress(l.opts.progress).done(l.opts.done,l.opts.complete).fail(l.opts.fail).always(l.opts.always),k.fx.timer(k.extend(u,{elem:o,anim:l,queue:l.opts.queue})),l}k.Animation=k.extend(dt,{tweeners:{"*":[function(e,t){var n=this.createTween(e,t);return le(n.elem,e,ne.exec(t),n),n}]},tweener:function(e,t){m(e)?(t=e,e=["*"]):e=e.match(R);for(var n,r=0,i=e.length;r<i;r++)n=e[r],dt.tweeners[n]=dt.tweeners[n]||[],dt.tweeners[n].unshift(t)},prefilters:[function(e,t,n){var r,i,o,a,s,u,l,c,f="width"in t||"height"in t,p=this,d={},h=e.style,g=e.nodeType&&se(e),v=Q.get(e,"fxshow");for(r in n.queue||(null==(a=k._queueHooks(e,"fx")).unqueued&&(a.unqueued=0,s=a.empty.fire,a.empty.fire=function(){a.unqueued||s()}),a.unqueued++,p.always(function(){p.always(function(){a.unqueued--,k.queue(e,"fx").length||a.empty.fire()})})),t)if(i=t[r],st.test(i)){if(delete t[r],o=o||"toggle"===i,i===(g?"hide":"show")){if("show"!==i||!v||void 0===v[r])continue;g=!0}d[r]=v&&v[r]||k.style(e,r)}if((u=!k.isEmptyObject(t))||!k.isEmptyObject(d))for(r in f&&1===e.nodeType&&(n.overflow=[h.overflow,h.overflowX,h.overflowY],null==(l=v&&v.display)&&(l=Q.get(e,"display")),"none"===(c=k.css(e,"display"))&&(l?c=l:(fe([e],!0),l=e.style.display||l,c=k.css(e,"display"),fe([e]))),("inline"===c||"inline-block"===c&&null!=l)&&"none"===k.css(e,"float")&&(u||(p.done(function(){h.display=l}),null==l&&(c=h.display,l="none"===c?"":c)),h.display="inline-block")),n.overflow&&(h.overflow="hidden",p.always(function(){h.overflow=n.overflow[0],h.overflowX=n.overflow[1],h.overflowY=n.overflow[2]})),u=!1,d)u||(v?"hidden"in v&&(g=v.hidden):v=Q.access(e,"fxshow",{display:l}),o&&(v.hidden=!g),g&&fe([e],!0),p.done(function(){for(r in g||fe([e]),Q.remove(e,"fxshow"),d)k.style(e,r,d[r])})),u=pt(g?v[r]:0,r,p),r in v||(v[r]=u.start,g&&(u.end=u.start,u.start=0))}],prefilter:function(e,t){t?dt.prefilters.unshift(e):dt.prefilters.push(e)}}),k.speed=function(e,t,n){var r=e&&"object"==typeof e?k.extend({},e):{complete:n||!n&&t||m(e)&&e,duration:e,easing:n&&t||t&&!m(t)&&t};return k.fx.off?r.duration=0:"number"!=typeof r.duration&&(r.duration in k.fx.speeds?r.duration=k.fx.speeds[r.duration]:r.duration=k.fx.speeds._default),null!=r.queue&&!0!==r.queue||(r.queue="fx"),r.old=r.complete,r.complete=function(){m(r.old)&&r.old.call(this),r.queue&&k.dequeue(this,r.queue)},r},k.fn.extend({fadeTo:function(e,t,n,r){return this.filter(se).css("opacity",0).show().end().animate({opacity:t},e,n,r)},animate:function(t,e,n,r){var i=k.isEmptyObject(t),o=k.speed(e,n,r),a=function(){var e=dt(this,k.extend({},t),o);(i||Q.get(this,"finish"))&&e.stop(!0)};return a.finish=a,i||!1===o.queue?this.each(a):this.queue(o.queue,a)},stop:function(i,e,o){var a=function(e){var t=e.stop;delete e.stop,t(o)};return"string"!=typeof i&&(o=e,e=i,i=void 0),e&&!1!==i&&this.queue(i||"fx",[]),this.each(function(){var e=!0,t=null!=i&&i+"queueHooks",n=k.timers,r=Q.get(this);if(t)r[t]&&r[t].stop&&a(r[t]);else for(t in r)r[t]&&r[t].stop&&ut.test(t)&&a(r[t]);for(t=n.length;t--;)n[t].elem!==this||null!=i&&n[t].queue!==i||(n[t].anim.stop(o),e=!1,n.splice(t,1));!e&&o||k.dequeue(this,i)})},finish:function(a){return!1!==a&&(a=a||"fx"),this.each(function(){var e,t=Q.get(this),n=t[a+"queue"],r=t[a+"queueHooks"],i=k.timers,o=n?n.length:0;for(t.finish=!0,k.queue(this,a,[]),r&&r.stop&&r.stop.call(this,!0),e=i.length;e--;)i[e].elem===this&&i[e].queue===a&&(i[e].anim.stop(!0),i.splice(e,1));for(e=0;e<o;e++)n[e]&&n[e].finish&&n[e].finish.call(this);delete t.finish})}}),k.each(["toggle","show","hide"],function(e,r){var i=k.fn[r];k.fn[r]=function(e,t,n){return null==e||"boolean"==typeof e?i.apply(this,arguments):this.animate(ft(r,!0),e,t,n)}}),k.each({slideDown:ft("show"),slideUp:ft("hide"),slideToggle:ft("toggle"),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"},fadeToggle:{opacity:"toggle"}},function(e,r){k.fn[e]=function(e,t,n){return this.animate(r,e,t,n)}}),k.timers=[],k.fx.tick=function(){var e,t=0,n=k.timers;for(rt=Date.now();t<n.length;t++)(e=n[t])()||n[t]!==e||n.splice(t--,1);n.length||k.fx.stop(),rt=void 0},k.fx.timer=function(e){k.timers.push(e),k.fx.start()},k.fx.interval=13,k.fx.start=function(){it||(it=!0,lt())},k.fx.stop=function(){it=null},k.fx.speeds={slow:600,fast:200,_default:400},k.fn.delay=function(r,e){return r=k.fx&&k.fx.speeds[r]||r,e=e||"fx",this.queue(e,function(e,t){var n=C.setTimeout(e,r);t.stop=function(){C.clearTimeout(n)}})},ot=E.createElement("input"),at=E.createElement("select").appendChild(E.createElement("option")),ot.type="checkbox",y.checkOn=""!==ot.value,y.optSelected=at.selected,(ot=E.createElement("input")).value="t",ot.type="radio",y.radioValue="t"===ot.value;var ht,gt=k.expr.attrHandle;k.fn.extend({attr:function(e,t){return _(this,k.attr,e,t,1<arguments.length)},removeAttr:function(e){return this.each(function(){k.removeAttr(this,e)})}}),k.extend({attr:function(e,t,n){var r,i,o=e.nodeType;if(3!==o&&8!==o&&2!==o)return"undefined"==typeof e.getAttribute?k.prop(e,t,n):(1===o&&k.isXMLDoc(e)||(i=k.attrHooks[t.toLowerCase()]||(k.expr.match.bool.test(t)?ht:void 0)),void 0!==n?null===n?void k.removeAttr(e,t):i&&"set"in i&&void 0!==(r=i.set(e,n,t))?r:(e.setAttribute(t,n+""),n):i&&"get"in i&&null!==(r=i.get(e,t))?r:null==(r=k.find.attr(e,t))?void 0:r)},attrHooks:{type:{set:function(e,t){if(!y.radioValue&&"radio"===t&&A(e,"input")){var n=e.value;return e.setAttribute("type",t),n&&(e.value=n),t}}}},removeAttr:function(e,t){var n,r=0,i=t&&t.match(R);if(i&&1===e.nodeType)while(n=i[r++])e.removeAttribute(n)}}),ht={set:function(e,t,n){return!1===t?k.removeAttr(e,n):e.setAttribute(n,n),n}},k.each(k.expr.match.bool.source.match(/\w+/g),function(e,t){var a=gt[t]||k.find.attr;gt[t]=function(e,t,n){var r,i,o=t.toLowerCase();return n||(i=gt[o],gt[o]=r,r=null!=a(e,t,n)?o:null,gt[o]=i),r}});var vt=/^(?:input|select|textarea|button)$/i,yt=/^(?:a|area)$/i;function mt(e){return(e.match(R)||[]).join(" ")}function xt(e){return e.getAttribute&&e.getAttribute("class")||""}function bt(e){return Array.isArray(e)?e:"string"==typeof e&&e.match(R)||[]}k.fn.extend({prop:function(e,t){return _(this,k.prop,e,t,1<arguments.length)},removeProp:function(e){return this.each(function(){delete this[k.propFix[e]||e]})}}),k.extend({prop:function(e,t,n){var r,i,o=e.nodeType;if(3!==o&&8!==o&&2!==o)return 1===o&&k.isXMLDoc(e)||(t=k.propFix[t]||t,i=k.propHooks[t]),void 0!==n?i&&"set"in i&&void 0!==(r=i.set(e,n,t))?r:e[t]=n:i&&"get"in i&&null!==(r=i.get(e,t))?r:e[t]},propHooks:{tabIndex:{get:function(e){var t=k.find.attr(e,"tabindex");return t?parseInt(t,10):vt.test(e.nodeName)||yt.test(e.nodeName)&&e.href?0:-1}}},propFix:{"for":"htmlFor","class":"className"}}),y.optSelected||(k.propHooks.selected={get:function(e){var t=e.parentNode;return t&&t.parentNode&&t.parentNode.selectedIndex,null},set:function(e){var t=e.parentNode;t&&(t.selectedIndex,t.parentNode&&t.parentNode.selectedIndex)}}),k.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){k.propFix[this.toLowerCase()]=this}),k.fn.extend({addClass:function(t){var e,n,r,i,o,a,s,u=0;if(m(t))return this.each(function(e){k(this).addClass(t.call(this,e,xt(this)))});if((e=bt(t)).length)while(n=this[u++])if(i=xt(n),r=1===n.nodeType&&" "+mt(i)+" "){a=0;while(o=e[a++])r.indexOf(" "+o+" ")<0&&(r+=o+" ");i!==(s=mt(r))&&n.setAttribute("class",s)}return this},removeClass:function(t){var e,n,r,i,o,a,s,u=0;if(m(t))return this.each(function(e){k(this).removeClass(t.call(this,e,xt(this)))});if(!arguments.length)return this.attr("class","");if((e=bt(t)).length)while(n=this[u++])if(i=xt(n),r=1===n.nodeType&&" "+mt(i)+" "){a=0;while(o=e[a++])while(-1<r.indexOf(" "+o+" "))r=r.replace(" "+o+" "," ");i!==(s=mt(r))&&n.setAttribute("class",s)}return this},toggleClass:function(i,t){var o=typeof i,a="string"===o||Array.isArray(i);return"boolean"==typeof t&&a?t?this.addClass(i):this.removeClass(i):m(i)?this.each(function(e){k(this).toggleClass(i.call(this,e,xt(this),t),t)}):this.each(function(){var e,t,n,r;if(a){t=0,n=k(this),r=bt(i);while(e=r[t++])n.hasClass(e)?n.removeClass(e):n.addClass(e)}else void 0!==i&&"boolean"!==o||((e=xt(this))&&Q.set(this,"__className__",e),this.setAttribute&&this.setAttribute("class",e||!1===i?"":Q.get(this,"__className__")||""))})},hasClass:function(e){var t,n,r=0;t=" "+e+" ";while(n=this[r++])if(1===n.nodeType&&-1<(" "+mt(xt(n))+" ").indexOf(t))return!0;return!1}});var wt=/\r/g;k.fn.extend({val:function(n){var r,e,i,t=this[0];return arguments.length?(i=m(n),this.each(function(e){var t;1===this.nodeType&&(null==(t=i?n.call(this,e,k(this).val()):n)?t="":"number"==typeof t?t+="":Array.isArray(t)&&(t=k.map(t,function(e){return null==e?"":e+""})),(r=k.valHooks[this.type]||k.valHooks[this.nodeName.toLowerCase()])&&"set"in r&&void 0!==r.set(this,t,"value")||(this.value=t))})):t?(r=k.valHooks[t.type]||k.valHooks[t.nodeName.toLowerCase()])&&"get"in r&&void 0!==(e=r.get(t,"value"))?e:"string"==typeof(e=t.value)?e.replace(wt,""):null==e?"":e:void 0}}),k.extend({valHooks:{option:{get:function(e){var t=k.find.attr(e,"value");return null!=t?t:mt(k.text(e))}},select:{get:function(e){var t,n,r,i=e.options,o=e.selectedIndex,a="select-one"===e.type,s=a?null:[],u=a?o+1:i.length;for(r=o<0?u:a?o:0;r<u;r++)if(((n=i[r]).selected||r===o)&&!n.disabled&&(!n.parentNode.disabled||!A(n.parentNode,"optgroup"))){if(t=k(n).val(),a)return t;s.push(t)}return s},set:function(e,t){var n,r,i=e.options,o=k.makeArray(t),a=i.length;while(a--)((r=i[a]).selected=-1<k.inArray(k.valHooks.option.get(r),o))&&(n=!0);return n||(e.selectedIndex=-1),o}}}}),k.each(["radio","checkbox"],function(){k.valHooks[this]={set:function(e,t){if(Array.isArray(t))return e.checked=-1<k.inArray(k(e).val(),t)}},y.checkOn||(k.valHooks[this].get=function(e){return null===e.getAttribute("value")?"on":e.value})}),y.focusin="onfocusin"in C;var Tt=/^(?:focusinfocus|focusoutblur)$/,Ct=function(e){e.stopPropagation()};k.extend(k.event,{trigger:function(e,t,n,r){var i,o,a,s,u,l,c,f,p=[n||E],d=v.call(e,"type")?e.type:e,h=v.call(e,"namespace")?e.namespace.split("."):[];if(o=f=a=n=n||E,3!==n.nodeType&&8!==n.nodeType&&!Tt.test(d+k.event.triggered)&&(-1<d.indexOf(".")&&(d=(h=d.split(".")).shift(),h.sort()),u=d.indexOf(":")<0&&"on"+d,(e=e[k.expando]?e:new k.Event(d,"object"==typeof e&&e)).isTrigger=r?2:3,e.namespace=h.join("."),e.rnamespace=e.namespace?new RegExp("(^|\\.)"+h.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,e.result=void 0,e.target||(e.target=n),t=null==t?[e]:k.makeArray(t,[e]),c=k.event.special[d]||{},r||!c.trigger||!1!==c.trigger.apply(n,t))){if(!r&&!c.noBubble&&!x(n)){for(s=c.delegateType||d,Tt.test(s+d)||(o=o.parentNode);o;o=o.parentNode)p.push(o),a=o;a===(n.ownerDocument||E)&&p.push(a.defaultView||a.parentWindow||C)}i=0;while((o=p[i++])&&!e.isPropagationStopped())f=o,e.type=1<i?s:c.bindType||d,(l=(Q.get(o,"events")||{})[e.type]&&Q.get(o,"handle"))&&l.apply(o,t),(l=u&&o[u])&&l.apply&&G(o)&&(e.result=l.apply(o,t),!1===e.result&&e.preventDefault());return e.type=d,r||e.isDefaultPrevented()||c._default&&!1!==c._default.apply(p.pop(),t)||!G(n)||u&&m(n[d])&&!x(n)&&((a=n[u])&&(n[u]=null),k.event.triggered=d,e.isPropagationStopped()&&f.addEventListener(d,Ct),n[d](),e.isPropagationStopped()&&f.removeEventListener(d,Ct),k.event.triggered=void 0,a&&(n[u]=a)),e.result}},simulate:function(e,t,n){var r=k.extend(new k.Event,n,{type:e,isSimulated:!0});k.event.trigger(r,null,t)}}),k.fn.extend({trigger:function(e,t){return this.each(function(){k.event.trigger(e,t,this)})},triggerHandler:function(e,t){var n=this[0];if(n)return k.event.trigger(e,t,n,!0)}}),y.focusin||k.each({focus:"focusin",blur:"focusout"},function(n,r){var i=function(e){k.event.simulate(r,e.target,k.event.fix(e))};k.event.special[r]={setup:function(){var e=this.ownerDocument||this,t=Q.access(e,r);t||e.addEventListener(n,i,!0),Q.access(e,r,(t||0)+1)},teardown:function(){var e=this.ownerDocument||this,t=Q.access(e,r)-1;t?Q.access(e,r,t):(e.removeEventListener(n,i,!0),Q.remove(e,r))}}});var Et=C.location,kt=Date.now(),St=/\?/;k.parseXML=function(e){var t;if(!e||"string"!=typeof e)return null;try{t=(new C.DOMParser).parseFromString(e,"text/xml")}catch(e){t=void 0}return t&&!t.getElementsByTagName("parsererror").length||k.error("Invalid XML: "+e),t};var Nt=/\[\]$/,At=/\r?\n/g,Dt=/^(?:submit|button|image|reset|file)$/i,jt=/^(?:input|select|textarea|keygen)/i;function qt(n,e,r,i){var t;if(Array.isArray(e))k.each(e,function(e,t){r||Nt.test(n)?i(n,t):qt(n+"["+("object"==typeof t&&null!=t?e:"")+"]",t,r,i)});else if(r||"object"!==w(e))i(n,e);else for(t in e)qt(n+"["+t+"]",e[t],r,i)}k.param=function(e,t){var n,r=[],i=function(e,t){var n=m(t)?t():t;r[r.length]=encodeURIComponent(e)+"="+encodeURIComponent(null==n?"":n)};if(null==e)return"";if(Array.isArray(e)||e.jquery&&!k.isPlainObject(e))k.each(e,function(){i(this.name,this.value)});else for(n in e)qt(n,e[n],t,i);return r.join("&")},k.fn.extend({serialize:function(){return k.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var e=k.prop(this,"elements");return e?k.makeArray(e):this}).filter(function(){var e=this.type;return this.name&&!k(this).is(":disabled")&&jt.test(this.nodeName)&&!Dt.test(e)&&(this.checked||!pe.test(e))}).map(function(e,t){var n=k(this).val();return null==n?null:Array.isArray(n)?k.map(n,function(e){return{name:t.name,value:e.replace(At,"\r\n")}}):{name:t.name,value:n.replace(At,"\r\n")}}).get()}});var Lt=/%20/g,Ht=/#.*$/,Ot=/([?&])_=[^&]*/,Pt=/^(.*?):[ \t]*([^\r\n]*)$/gm,Rt=/^(?:GET|HEAD)$/,Mt=/^\/\//,It={},Wt={},$t="*/".concat("*"),Ft=E.createElement("a");function Bt(o){return function(e,t){"string"!=typeof e&&(t=e,e="*");var n,r=0,i=e.toLowerCase().match(R)||[];if(m(t))while(n=i[r++])"+"===n[0]?(n=n.slice(1)||"*",(o[n]=o[n]||[]).unshift(t)):(o[n]=o[n]||[]).push(t)}}function _t(t,i,o,a){var s={},u=t===Wt;function l(e){var r;return s[e]=!0,k.each(t[e]||[],function(e,t){var n=t(i,o,a);return"string"!=typeof n||u||s[n]?u?!(r=n):void 0:(i.dataTypes.unshift(n),l(n),!1)}),r}return l(i.dataTypes[0])||!s["*"]&&l("*")}function zt(e,t){var n,r,i=k.ajaxSettings.flatOptions||{};for(n in t)void 0!==t[n]&&((i[n]?e:r||(r={}))[n]=t[n]);return r&&k.extend(!0,e,r),e}Ft.href=Et.href,k.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:Et.href,type:"GET",isLocal:/^(?:about|app|app-storage|.+-extension|file|res|widget):$/.test(Et.protocol),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":$t,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/\bxml\b/,html:/\bhtml/,json:/\bjson\b/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":JSON.parse,"text xml":k.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(e,t){return t?zt(zt(e,k.ajaxSettings),t):zt(k.ajaxSettings,e)},ajaxPrefilter:Bt(It),ajaxTransport:Bt(Wt),ajax:function(e,t){"object"==typeof e&&(t=e,e=void 0),t=t||{};var c,f,p,n,d,r,h,g,i,o,v=k.ajaxSetup({},t),y=v.context||v,m=v.context&&(y.nodeType||y.jquery)?k(y):k.event,x=k.Deferred(),b=k.Callbacks("once memory"),w=v.statusCode||{},a={},s={},u="canceled",T={readyState:0,getResponseHeader:function(e){var t;if(h){if(!n){n={};while(t=Pt.exec(p))n[t[1].toLowerCase()+" "]=(n[t[1].toLowerCase()+" "]||[]).concat(t[2])}t=n[e.toLowerCase()+" "]}return null==t?null:t.join(", ")},getAllResponseHeaders:function(){return h?p:null},setRequestHeader:function(e,t){return null==h&&(e=s[e.toLowerCase()]=s[e.toLowerCase()]||e,a[e]=t),this},overrideMimeType:function(e){return null==h&&(v.mimeType=e),this},statusCode:function(e){var t;if(e)if(h)T.always(e[T.status]);else for(t in e)w[t]=[w[t],e[t]];return this},abort:function(e){var t=e||u;return c&&c.abort(t),l(0,t),this}};if(x.promise(T),v.url=((e||v.url||Et.href)+"").replace(Mt,Et.protocol+"//"),v.type=t.method||t.type||v.method||v.type,v.dataTypes=(v.dataType||"*").toLowerCase().match(R)||[""],null==v.crossDomain){r=E.createElement("a");try{r.href=v.url,r.href=r.href,v.crossDomain=Ft.protocol+"//"+Ft.host!=r.protocol+"//"+r.host}catch(e){v.crossDomain=!0}}if(v.data&&v.processData&&"string"!=typeof v.data&&(v.data=k.param(v.data,v.traditional)),_t(It,v,t,T),h)return T;for(i in(g=k.event&&v.global)&&0==k.active++&&k.event.trigger("ajaxStart"),v.type=v.type.toUpperCase(),v.hasContent=!Rt.test(v.type),f=v.url.replace(Ht,""),v.hasContent?v.data&&v.processData&&0===(v.contentType||"").indexOf("application/x-www-form-urlencoded")&&(v.data=v.data.replace(Lt,"+")):(o=v.url.slice(f.length),v.data&&(v.processData||"string"==typeof v.data)&&(f+=(St.test(f)?"&":"?")+v.data,delete v.data),!1===v.cache&&(f=f.replace(Ot,"$1"),o=(St.test(f)?"&":"?")+"_="+kt+++o),v.url=f+o),v.ifModified&&(k.lastModified[f]&&T.setRequestHeader("If-Modified-Since",k.lastModified[f]),k.etag[f]&&T.setRequestHeader("If-None-Match",k.etag[f])),(v.data&&v.hasContent&&!1!==v.contentType||t.contentType)&&T.setRequestHeader("Content-Type",v.contentType),T.setRequestHeader("Accept",v.dataTypes[0]&&v.accepts[v.dataTypes[0]]?v.accepts[v.dataTypes[0]]+("*"!==v.dataTypes[0]?", "+$t+"; q=0.01":""):v.accepts["*"]),v.headers)T.setRequestHeader(i,v.headers[i]);if(v.beforeSend&&(!1===v.beforeSend.call(y,T,v)||h))return T.abort();if(u="abort",b.add(v.complete),T.done(v.success),T.fail(v.error),c=_t(Wt,v,t,T)){if(T.readyState=1,g&&m.trigger("ajaxSend",[T,v]),h)return T;v.async&&0<v.timeout&&(d=C.setTimeout(function(){T.abort("timeout")},v.timeout));try{h=!1,c.send(a,l)}catch(e){if(h)throw e;l(-1,e)}}else l(-1,"No Transport");function l(e,t,n,r){var i,o,a,s,u,l=t;h||(h=!0,d&&C.clearTimeout(d),c=void 0,p=r||"",T.readyState=0<e?4:0,i=200<=e&&e<300||304===e,n&&(s=function(e,t,n){var r,i,o,a,s=e.contents,u=e.dataTypes;while("*"===u[0])u.shift(),void 0===r&&(r=e.mimeType||t.getResponseHeader("Content-Type"));if(r)for(i in s)if(s[i]&&s[i].test(r)){u.unshift(i);break}if(u[0]in n)o=u[0];else{for(i in n){if(!u[0]||e.converters[i+" "+u[0]]){o=i;break}a||(a=i)}o=o||a}if(o)return o!==u[0]&&u.unshift(o),n[o]}(v,T,n)),s=function(e,t,n,r){var i,o,a,s,u,l={},c=e.dataTypes.slice();if(c[1])for(a in e.converters)l[a.toLowerCase()]=e.converters[a];o=c.shift();while(o)if(e.responseFields[o]&&(n[e.responseFields[o]]=t),!u&&r&&e.dataFilter&&(t=e.dataFilter(t,e.dataType)),u=o,o=c.shift())if("*"===o)o=u;else if("*"!==u&&u!==o){if(!(a=l[u+" "+o]||l["* "+o]))for(i in l)if((s=i.split(" "))[1]===o&&(a=l[u+" "+s[0]]||l["* "+s[0]])){!0===a?a=l[i]:!0!==l[i]&&(o=s[0],c.unshift(s[1]));break}if(!0!==a)if(a&&e["throws"])t=a(t);else try{t=a(t)}catch(e){return{state:"parsererror",error:a?e:"No conversion from "+u+" to "+o}}}return{state:"success",data:t}}(v,s,T,i),i?(v.ifModified&&((u=T.getResponseHeader("Last-Modified"))&&(k.lastModified[f]=u),(u=T.getResponseHeader("etag"))&&(k.etag[f]=u)),204===e||"HEAD"===v.type?l="nocontent":304===e?l="notmodified":(l=s.state,o=s.data,i=!(a=s.error))):(a=l,!e&&l||(l="error",e<0&&(e=0))),T.status=e,T.statusText=(t||l)+"",i?x.resolveWith(y,[o,l,T]):x.rejectWith(y,[T,l,a]),T.statusCode(w),w=void 0,g&&m.trigger(i?"ajaxSuccess":"ajaxError",[T,v,i?o:a]),b.fireWith(y,[T,l]),g&&(m.trigger("ajaxComplete",[T,v]),--k.active||k.event.trigger("ajaxStop")))}return T},getJSON:function(e,t,n){return k.get(e,t,n,"json")},getScript:function(e,t){return k.get(e,void 0,t,"script")}}),k.each(["get","post"],function(e,i){k[i]=function(e,t,n,r){return m(t)&&(r=r||n,n=t,t=void 0),k.ajax(k.extend({url:e,type:i,dataType:r,data:t,success:n},k.isPlainObject(e)&&e))}}),k._evalUrl=function(e,t){return k.ajax({url:e,type:"GET",dataType:"script",cache:!0,async:!1,global:!1,converters:{"text script":function(){}},dataFilter:function(e){k.globalEval(e,t)}})},k.fn.extend({wrapAll:function(e){var t;return this[0]&&(m(e)&&(e=e.call(this[0])),t=k(e,this[0].ownerDocument).eq(0).clone(!0),this[0].parentNode&&t.insertBefore(this[0]),t.map(function(){var e=this;while(e.firstElementChild)e=e.firstElementChild;return e}).append(this)),this},wrapInner:function(n){return m(n)?this.each(function(e){k(this).wrapInner(n.call(this,e))}):this.each(function(){var e=k(this),t=e.contents();t.length?t.wrapAll(n):e.append(n)})},wrap:function(t){var n=m(t);return this.each(function(e){k(this).wrapAll(n?t.call(this,e):t)})},unwrap:function(e){return this.parent(e).not("body").each(function(){k(this).replaceWith(this.childNodes)}),this}}),k.expr.pseudos.hidden=function(e){return!k.expr.pseudos.visible(e)},k.expr.pseudos.visible=function(e){return!!(e.offsetWidth||e.offsetHeight||e.getClientRects().length)},k.ajaxSettings.xhr=function(){try{return new C.XMLHttpRequest}catch(e){}};var Ut={0:200,1223:204},Xt=k.ajaxSettings.xhr();y.cors=!!Xt&&"withCredentials"in Xt,y.ajax=Xt=!!Xt,k.ajaxTransport(function(i){var o,a;if(y.cors||Xt&&!i.crossDomain)return{send:function(e,t){var n,r=i.xhr();if(r.open(i.type,i.url,i.async,i.username,i.password),i.xhrFields)for(n in i.xhrFields)r[n]=i.xhrFields[n];for(n in i.mimeType&&r.overrideMimeType&&r.overrideMimeType(i.mimeType),i.crossDomain||e["X-Requested-With"]||(e["X-Requested-With"]="XMLHttpRequest"),e)r.setRequestHeader(n,e[n]);o=function(e){return function(){o&&(o=a=r.onload=r.onerror=r.onabort=r.ontimeout=r.onreadystatechange=null,"abort"===e?r.abort():"error"===e?"number"!=typeof r.status?t(0,"error"):t(r.status,r.statusText):t(Ut[r.status]||r.status,r.statusText,"text"!==(r.responseType||"text")||"string"!=typeof r.responseText?{binary:r.response}:{text:r.responseText},r.getAllResponseHeaders()))}},r.onload=o(),a=r.onerror=r.ontimeout=o("error"),void 0!==r.onabort?r.onabort=a:r.onreadystatechange=function(){4===r.readyState&&C.setTimeout(function(){o&&a()})},o=o("abort");try{r.send(i.hasContent&&i.data||null)}catch(e){if(o)throw e}},abort:function(){o&&o()}}}),k.ajaxPrefilter(function(e){e.crossDomain&&(e.contents.script=!1)}),k.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/\b(?:java|ecma)script\b/},converters:{"text script":function(e){return k.globalEval(e),e}}}),k.ajaxPrefilter("script",function(e){void 0===e.cache&&(e.cache=!1),e.crossDomain&&(e.type="GET")}),k.ajaxTransport("script",function(n){var r,i;if(n.crossDomain||n.scriptAttrs)return{send:function(e,t){r=k("<script>").attr(n.scriptAttrs||{}).prop({charset:n.scriptCharset,src:n.url}).on("load error",i=function(e){r.remove(),i=null,e&&t("error"===e.type?404:200,e.type)}),E.head.appendChild(r[0])},abort:function(){i&&i()}}});var Vt,Gt=[],Yt=/(=)\?(?=&|$)|\?\?/;k.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=Gt.pop()||k.expando+"_"+kt++;return this[e]=!0,e}}),k.ajaxPrefilter("json jsonp",function(e,t,n){var r,i,o,a=!1!==e.jsonp&&(Yt.test(e.url)?"url":"string"==typeof e.data&&0===(e.contentType||"").indexOf("application/x-www-form-urlencoded")&&Yt.test(e.data)&&"data");if(a||"jsonp"===e.dataTypes[0])return r=e.jsonpCallback=m(e.jsonpCallback)?e.jsonpCallback():e.jsonpCallback,a?e[a]=e[a].replace(Yt,"$1"+r):!1!==e.jsonp&&(e.url+=(St.test(e.url)?"&":"?")+e.jsonp+"="+r),e.converters["script json"]=function(){return o||k.error(r+" was not called"),o[0]},e.dataTypes[0]="json",i=C[r],C[r]=function(){o=arguments},n.always(function(){void 0===i?k(C).removeProp(r):C[r]=i,e[r]&&(e.jsonpCallback=t.jsonpCallback,Gt.push(r)),o&&m(i)&&i(o[0]),o=i=void 0}),"script"}),y.createHTMLDocument=((Vt=E.implementation.createHTMLDocument("").body).innerHTML="<form></form><form></form>",2===Vt.childNodes.length),k.parseHTML=function(e,t,n){return"string"!=typeof e?[]:("boolean"==typeof t&&(n=t,t=!1),t||(y.createHTMLDocument?((r=(t=E.implementation.createHTMLDocument("")).createElement("base")).href=E.location.href,t.head.appendChild(r)):t=E),o=!n&&[],(i=D.exec(e))?[t.createElement(i[1])]:(i=we([e],t,o),o&&o.length&&k(o).remove(),k.merge([],i.childNodes)));var r,i,o},k.fn.load=function(e,t,n){var r,i,o,a=this,s=e.indexOf(" ");return-1<s&&(r=mt(e.slice(s)),e=e.slice(0,s)),m(t)?(n=t,t=void 0):t&&"object"==typeof t&&(i="POST"),0<a.length&&k.ajax({url:e,type:i||"GET",dataType:"html",data:t}).done(function(e){o=arguments,a.html(r?k("<div>").append(k.parseHTML(e)).find(r):e)}).always(n&&function(e,t){a.each(function(){n.apply(this,o||[e.responseText,t,e])})}),this},k.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){k.fn[t]=function(e){return this.on(t,e)}}),k.expr.pseudos.animated=function(t){return k.grep(k.timers,function(e){return t===e.elem}).length},k.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l=k.css(e,"position"),c=k(e),f={};"static"===l&&(e.style.position="relative"),s=c.offset(),o=k.css(e,"top"),u=k.css(e,"left"),("absolute"===l||"fixed"===l)&&-1<(o+u).indexOf("auto")?(a=(r=c.position()).top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),m(t)&&(t=t.call(e,n,k.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):c.css(f)}},k.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){k.offset.setOffset(this,t,e)});var e,n,r=this[0];return r?r.getClientRects().length?(e=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:e.top+n.pageYOffset,left:e.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],i={top:0,left:0};if("fixed"===k.css(r,"position"))t=r.getBoundingClientRect();else{t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;while(e&&(e===n.body||e===n.documentElement)&&"static"===k.css(e,"position"))e=e.parentNode;e&&e!==r&&1===e.nodeType&&((i=k(e).offset()).top+=k.css(e,"borderTopWidth",!0),i.left+=k.css(e,"borderLeftWidth",!0))}return{top:t.top-i.top-k.css(r,"marginTop",!0),left:t.left-i.left-k.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent;while(e&&"static"===k.css(e,"position"))e=e.offsetParent;return e||ie})}}),k.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(t,i){var o="pageYOffset"===i;k.fn[t]=function(e){return _(this,function(e,t,n){var r;if(x(e)?r=e:9===e.nodeType&&(r=e.defaultView),void 0===n)return r?r[i]:e[t];r?r.scrollTo(o?r.pageXOffset:n,o?n:r.pageYOffset):e[t]=n},t,e,arguments.length)}}),k.each(["top","left"],function(e,n){k.cssHooks[n]=ze(y.pixelPosition,function(e,t){if(t)return t=_e(e,n),$e.test(t)?k(e).position()[n]+"px":t})}),k.each({Height:"height",Width:"width"},function(a,s){k.each({padding:"inner"+a,content:s,"":"outer"+a},function(r,o){k.fn[o]=function(e,t){var n=arguments.length&&(r||"boolean"!=typeof e),i=r||(!0===e||!0===t?"margin":"border");return _(this,function(e,t,n){var r;return x(e)?0===o.indexOf("outer")?e["inner"+a]:e.document.documentElement["client"+a]:9===e.nodeType?(r=e.documentElement,Math.max(e.body["scroll"+a],r["scroll"+a],e.body["offset"+a],r["offset"+a],r["client"+a])):void 0===n?k.css(e,t,i):k.style(e,t,n,i)},s,n?e:void 0,n)}})}),k.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(e,n){k.fn[n]=function(e,t){return 0<arguments.length?this.on(n,null,e,t):this.trigger(n)}}),k.fn.extend({hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)}}),k.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)}}),k.proxy=function(e,t){var n,r,i;if("string"==typeof t&&(n=e[t],t=e,e=n),m(e))return r=s.call(arguments,2),(i=function(){return e.apply(t||this,r.concat(s.call(arguments)))}).guid=e.guid=e.guid||k.guid++,i},k.holdReady=function(e){e?k.readyWait++:k.ready(!0)},k.isArray=Array.isArray,k.parseJSON=JSON.parse,k.nodeName=A,k.isFunction=m,k.isWindow=x,k.camelCase=V,k.type=w,k.now=Date.now,k.isNumeric=function(e){var t=k.type(e);return("number"===t||"string"===t)&&!isNaN(e-parseFloat(e))},"function"==typeof define&&define.amd&&define("jquery",[],function(){return k});var Qt=C.jQuery,Jt=C.$;return k.noConflict=function(e){return C.$===k&&(C.$=Jt),e&&C.jQuery===k&&(C.jQuery=Qt),k},e||(C.jQuery=C.$=k),k}); +/*! jQuery Migrate v3.1.0 | (c) OpenJS Foundation and other contributors | jquery.org/license */ +"undefined"==typeof jQuery.migrateMute&&(jQuery.migrateMute=!0),function(t){"function"==typeof define&&define.amd?define(["jquery"],function(e){return t(e,window)}):"object"==typeof module&&module.exports?module.exports=t(require("jquery"),window):t(jQuery,window)}(function(s,n){"use strict";function e(e){return 0<=function(e,t){for(var r=/^(\d+)\.(\d+)\.(\d+)/,n=r.exec(e)||[],o=r.exec(t)||[],i=1;i<=3;i++){if(+n[i]>+o[i])return 1;if(+n[i]<+o[i])return-1}return 0}(s.fn.jquery,e)}s.migrateVersion="3.1.0",n.console&&n.console.log&&(s&&e("3.0.0")||n.console.log("JQMIGRATE: jQuery 3.0.0+ REQUIRED"),s.migrateWarnings&&n.console.log("JQMIGRATE: Migrate plugin loaded multiple times"),n.console.log("JQMIGRATE: Migrate is installed"+(s.migrateMute?"":" with logging active")+", version "+s.migrateVersion));var r={};function u(e){var t=n.console;r[e]||(r[e]=!0,s.migrateWarnings.push(e),t&&t.warn&&!s.migrateMute&&(t.warn("JQMIGRATE: "+e),s.migrateTrace&&t.trace&&t.trace()))}function t(e,t,r,n){Object.defineProperty(e,t,{configurable:!0,enumerable:!0,get:function(){return u(n),r},set:function(e){u(n),r=e}})}function o(e,t,r,n){e[t]=function(){return u(n),r.apply(this,arguments)}}s.migrateWarnings=[],void 0===s.migrateTrace&&(s.migrateTrace=!0),s.migrateReset=function(){r={},s.migrateWarnings.length=0},"BackCompat"===n.document.compatMode&&u("jQuery is not compatible with Quirks Mode");var i,a=s.fn.init,c=s.isNumeric,d=s.find,l=/\[(\s*[-\w]+\s*)([~|^$*]?=)\s*([-\w#]*?#[-\w#]*)\s*\]/,p=/\[(\s*[-\w]+\s*)([~|^$*]?=)\s*([-\w#]*?#[-\w#]*)\s*\]/g;for(i in s.fn.init=function(e){var t=Array.prototype.slice.call(arguments);return"string"==typeof e&&"#"===e&&(u("jQuery( '#' ) is not a valid selector"),t[0]=[]),a.apply(this,t)},s.fn.init.prototype=s.fn,s.find=function(t){var r=Array.prototype.slice.call(arguments);if("string"==typeof t&&l.test(t))try{n.document.querySelector(t)}catch(e){t=t.replace(p,function(e,t,r,n){return"["+t+r+'"'+n+'"]'});try{n.document.querySelector(t),u("Attribute selector with '#' must be quoted: "+r[0]),r[0]=t}catch(e){u("Attribute selector with '#' was not fixed: "+r[0])}}return d.apply(this,r)},d)Object.prototype.hasOwnProperty.call(d,i)&&(s.find[i]=d[i]);s.fn.size=function(){return u("jQuery.fn.size() is deprecated and removed; use the .length property"),this.length},s.parseJSON=function(){return u("jQuery.parseJSON is deprecated; use JSON.parse"),JSON.parse.apply(null,arguments)},s.isNumeric=function(e){var t,r,n=c(e),o=(r=(t=e)&&t.toString(),!s.isArray(t)&&0<=r-parseFloat(r)+1);return n!==o&&u("jQuery.isNumeric() should not be called on constructed objects"),o},e("3.3.0")&&o(s,"isWindow",function(e){return null!=e&&e===e.window},"jQuery.isWindow() is deprecated"),o(s,"holdReady",s.holdReady,"jQuery.holdReady is deprecated"),o(s,"unique",s.uniqueSort,"jQuery.unique is deprecated; use jQuery.uniqueSort"),t(s.expr,"filters",s.expr.pseudos,"jQuery.expr.filters is deprecated; use jQuery.expr.pseudos"),t(s.expr,":",s.expr.pseudos,"jQuery.expr[':'] is deprecated; use jQuery.expr.pseudos"),e("3.2.0")&&o(s,"nodeName",s.nodeName,"jQuery.nodeName is deprecated");var f=s.ajax;s.ajax=function(){var e=f.apply(this,arguments);return e.promise&&(o(e,"success",e.done,"jQXHR.success is deprecated and removed"),o(e,"error",e.fail,"jQXHR.error is deprecated and removed"),o(e,"complete",e.always,"jQXHR.complete is deprecated and removed")),e};var y=s.fn.removeAttr,m=s.fn.toggleClass,h=/\S+/g;s.fn.removeAttr=function(e){var r=this;return s.each(e.match(h),function(e,t){s.expr.match.bool.test(t)&&(u("jQuery.fn.removeAttr no longer sets boolean properties: "+t),r.prop(t,!1))}),y.apply(this,arguments)};var g=!(s.fn.toggleClass=function(t){return void 0!==t&&"boolean"!=typeof t?m.apply(this,arguments):(u("jQuery.fn.toggleClass( boolean ) is deprecated"),this.each(function(){var e=this.getAttribute&&this.getAttribute("class")||"";e&&s.data(this,"__className__",e),this.setAttribute&&this.setAttribute("class",e||!1===t?"":s.data(this,"__className__")||"")}))});s.swap&&s.each(["height","width","reliableMarginRight"],function(e,t){var r=s.cssHooks[t]&&s.cssHooks[t].get;r&&(s.cssHooks[t].get=function(){var e;return g=!0,e=r.apply(this,arguments),g=!1,e})}),s.swap=function(e,t,r,n){var o,i,a={};for(i in g||u("jQuery.swap() is undocumented and deprecated"),t)a[i]=e.style[i],e.style[i]=t[i];for(i in o=r.apply(e,n||[]),t)e.style[i]=a[i];return o};var v=s.data;s.data=function(e,t,r){var n;if(t&&"object"==typeof t&&2===arguments.length){n=s.hasData(e)&&v.call(this,e);var o={};for(var i in t)i!==s.camelCase(i)?(u("jQuery.data() always sets/gets camelCased names: "+i),n[i]=t[i]):o[i]=t[i];return v.call(this,e,o),t}return t&&"string"==typeof t&&t!==s.camelCase(t)&&(n=s.hasData(e)&&v.call(this,e))&&t in n?(u("jQuery.data() always sets/gets camelCased names: "+t),2<arguments.length&&(n[t]=r),n[t]):v.apply(this,arguments)};function j(e){return e}var Q=s.Tween.prototype.run;s.Tween.prototype.run=function(){1<s.easing[this.easing].length&&(u("'jQuery.easing."+this.easing.toString()+"' should use only one argument"),s.easing[this.easing]=j),Q.apply(this,arguments)};var w=s.fx.interval||13,b="jQuery.fx.interval is deprecated";n.requestAnimationFrame&&Object.defineProperty(s.fx,"interval",{configurable:!0,enumerable:!0,get:function(){return n.document.hidden||u(b),w},set:function(e){u(b),w=e}});var x=s.fn.load,k=s.event.add,A=s.event.fix;s.event.props=[],s.event.fixHooks={},t(s.event.props,"concat",s.event.props.concat,"jQuery.event.props.concat() is deprecated and removed"),s.event.fix=function(e){var t,r=e.type,n=this.fixHooks[r],o=s.event.props;if(o.length){u("jQuery.event.props are deprecated and removed: "+o.join());while(o.length)s.event.addProp(o.pop())}if(n&&!n._migrated_&&(n._migrated_=!0,u("jQuery.event.fixHooks are deprecated and removed: "+r),(o=n.props)&&o.length))while(o.length)s.event.addProp(o.pop());return t=A.call(this,e),n&&n.filter?n.filter(t,e):t},s.event.add=function(e,t){return e===n&&"load"===t&&"complete"===n.document.readyState&&u("jQuery(window).on('load'...) called after load event occurred"),k.apply(this,arguments)},s.each(["load","unload","error"],function(e,t){s.fn[t]=function(){var e=Array.prototype.slice.call(arguments,0);return"load"===t&&"string"==typeof e[0]?x.apply(this,e):(u("jQuery.fn."+t+"() is deprecated"),e.splice(0,0,t),arguments.length?this.on.apply(this,e):(this.triggerHandler.apply(this,e),this))}}),s.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(e,r){s.fn[r]=function(e,t){return u("jQuery.fn."+r+"() event shorthand is deprecated"),0<arguments.length?this.on(r,null,e,t):this.trigger(r)}}),s(function(){s(n.document).triggerHandler("ready")}),s.event.special.ready={setup:function(){this===n.document&&u("'ready' event is deprecated")}},s.fn.extend({bind:function(e,t,r){return u("jQuery.fn.bind() is deprecated"),this.on(e,null,t,r)},unbind:function(e,t){return u("jQuery.fn.unbind() is deprecated"),this.off(e,null,t)},delegate:function(e,t,r,n){return u("jQuery.fn.delegate() is deprecated"),this.on(t,e,r,n)},undelegate:function(e,t,r){return u("jQuery.fn.undelegate() is deprecated"),1===arguments.length?this.off(e,"**"):this.off(t,e||"**",r)},hover:function(e,t){return u("jQuery.fn.hover() is deprecated"),this.on("mouseenter",e).on("mouseleave",t||e)}});var S=s.fn.offset;s.fn.offset=function(){var e,t=this[0],r={top:0,left:0};return t&&t.nodeType?(e=(t.ownerDocument||n.document).documentElement,s.contains(e,t)?S.apply(this,arguments):(u("jQuery.fn.offset() requires an element connected to a document"),r)):(u("jQuery.fn.offset() requires a valid DOM element"),r)};var q=s.param;s.param=function(e,t){var r=s.ajaxSettings&&s.ajaxSettings.traditional;return void 0===t&&r&&(u("jQuery.param() no longer uses jQuery.ajaxSettings.traditional"),t=r),q.call(this,e,t)};var C=s.fn.andSelf||s.fn.addBack;s.fn.andSelf=function(){return u("jQuery.fn.andSelf() is deprecated and removed, use jQuery.fn.addBack()"),C.apply(this,arguments)};var M=s.Deferred,R=[["resolve","done",s.Callbacks("once memory"),s.Callbacks("once memory"),"resolved"],["reject","fail",s.Callbacks("once memory"),s.Callbacks("once memory"),"rejected"],["notify","progress",s.Callbacks("memory"),s.Callbacks("memory")]];return s.Deferred=function(e){var i=M(),a=i.promise();return i.pipe=a.pipe=function(){var o=arguments;return u("deferred.pipe() is deprecated"),s.Deferred(function(n){s.each(R,function(e,t){var r=s.isFunction(o[e])&&o[e];i[t[1]](function(){var e=r&&r.apply(this,arguments);e&&s.isFunction(e.promise)?e.promise().done(n.resolve).fail(n.reject).progress(n.notify):n[t[0]+"With"](this===a?n.promise():this,r?[e]:arguments)})}),o=null}).promise()},e&&e.call(i,i),i},s.Deferred.exceptionHook=M.exceptionHook,s}); \ No newline at end of file diff --git a/data/web/js/build/003-bootstrap-slider.min.js b/data/web/js/build/003-bootstrap-slider.min.js index 1ae4e4f2..633988c8 100644 --- a/data/web/js/build/003-bootstrap-slider.min.js +++ b/data/web/js/build/003-bootstrap-slider.min.js @@ -1,5 +1,5 @@ -/*! ======================================================= - VERSION 10.6.0 -========================================================= */ -"use strict";var _typeof="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(a){return typeof a}:function(a){return a&&"function"==typeof Symbol&&a.constructor===Symbol&&a!==Symbol.prototype?"symbol":typeof a},windowIsDefined="object"===("undefined"==typeof window?"undefined":_typeof(window));!function(a){if("function"==typeof define&&define.amd)define(["jquery"],a);else if("object"===("undefined"==typeof module?"undefined":_typeof(module))&&module.exports){var b;try{b=require("jquery")}catch(c){b=null}module.exports=a(b)}else window&&(window.Slider=a(window.jQuery))}(function(a){var b="slider",c="bootstrapSlider";windowIsDefined&&!window.console&&(window.console={}),windowIsDefined&&!window.console.log&&(window.console.log=function(){}),windowIsDefined&&!window.console.warn&&(window.console.warn=function(){});var d;return function(a){function b(){}function c(a){function c(b){b.prototype.option||(b.prototype.option=function(b){a.isPlainObject(b)&&(this.options=a.extend(!0,this.options,b))})}function e(b,c){a.fn[b]=function(e){if("string"==typeof e){for(var g=d.call(arguments,1),h=0,i=this.length;i>h;h++){var j=this[h],k=a.data(j,b);if(k)if(a.isFunction(k[e])&&"_"!==e.charAt(0)){var l=k[e].apply(k,g);if(void 0!==l&&l!==k)return l}else f("no such method '"+e+"' for "+b+" instance");else f("cannot call methods on "+b+" prior to initialization; attempted to call '"+e+"'")}return this}var m=this.map(function(){var d=a.data(this,b);return d?(d.option(e),d._init()):(d=new c(this,e),a.data(this,b,d)),a(this)});return!m||m.length>1?m:m[0]}}if(a){var f="undefined"==typeof console?b:function(a){console.error(a)};return a.bridget=function(a,b){c(b),e(a,b)},a.bridget}}var d=Array.prototype.slice;c(a)}(a),function(a){function e(b,c){function d(a,b){var c="data-slider-"+b.replace(/_/g,"-"),d=a.getAttribute(c);try{return JSON.parse(d)}catch(e){return d}}this._state={value:null,enabled:null,offset:null,size:null,percentage:null,inDrag:!1,over:!1,tickIndex:null},this.ticksCallbackMap={},this.handleCallbackMap={},"string"==typeof b?this.element=document.querySelector(b):b instanceof HTMLElement&&(this.element=b),c=c?c:{};for(var e=Object.keys(this.defaultOptions),f=c.hasOwnProperty("min"),g=c.hasOwnProperty("max"),i=0;i<e.length;i++){var j=e[i],k=c[j];k="undefined"!=typeof k?k:d(this.element,j),k=null!==k?k:this.defaultOptions[j],this.options||(this.options={}),this.options[j]=k}if(this.ticksAreValid=Array.isArray(this.options.ticks)&&this.options.ticks.length>0,this.ticksAreValid||(this.options.lock_to_ticks=!1),"auto"===this.options.rtl){var l=window.getComputedStyle(this.element);null!=l?this.options.rtl="rtl"===l.direction:this.options.rtl="rtl"===this.element.style.direction}"vertical"!==this.options.orientation||"top"!==this.options.tooltip_position&&"bottom"!==this.options.tooltip_position?"horizontal"!==this.options.orientation||"left"!==this.options.tooltip_position&&"right"!==this.options.tooltip_position||(this.options.tooltip_position="top"):this.options.rtl?this.options.tooltip_position="left":this.options.tooltip_position="right";var m,n,o,p,q,r=this.element.style.width,s=!1,t=this.element.parentNode;if(this.sliderElem)s=!0;else{this.sliderElem=document.createElement("div"),this.sliderElem.className="slider";var u=document.createElement("div");u.className="slider-track",n=document.createElement("div"),n.className="slider-track-low",m=document.createElement("div"),m.className="slider-selection",o=document.createElement("div"),o.className="slider-track-high",p=document.createElement("div"),p.className="slider-handle min-slider-handle",p.setAttribute("role","slider"),p.setAttribute("aria-valuemin",this.options.min),p.setAttribute("aria-valuemax",this.options.max),q=document.createElement("div"),q.className="slider-handle max-slider-handle",q.setAttribute("role","slider"),q.setAttribute("aria-valuemin",this.options.min),q.setAttribute("aria-valuemax",this.options.max),u.appendChild(n),u.appendChild(m),u.appendChild(o),this.rangeHighlightElements=[];var v=this.options.rangeHighlights;if(Array.isArray(v)&&v.length>0)for(var w=0;w<v.length;w++){var x=document.createElement("div"),y=v[w]["class"]||"";x.className="slider-rangeHighlight slider-selection "+y,this.rangeHighlightElements.push(x),u.appendChild(x)}var z=Array.isArray(this.options.labelledby);if(z&&this.options.labelledby[0]&&p.setAttribute("aria-labelledby",this.options.labelledby[0]),z&&this.options.labelledby[1]&&q.setAttribute("aria-labelledby",this.options.labelledby[1]),!z&&this.options.labelledby&&(p.setAttribute("aria-labelledby",this.options.labelledby),q.setAttribute("aria-labelledby",this.options.labelledby)),this.ticks=[],Array.isArray(this.options.ticks)&&this.options.ticks.length>0){for(this.ticksContainer=document.createElement("div"),this.ticksContainer.className="slider-tick-container",i=0;i<this.options.ticks.length;i++){var A=document.createElement("div");if(A.className="slider-tick",this.options.ticks_tooltip){var B=this._addTickListener(),C=B.addMouseEnter(this,A,i),D=B.addMouseLeave(this,A);this.ticksCallbackMap[i]={mouseEnter:C,mouseLeave:D}}this.ticks.push(A),this.ticksContainer.appendChild(A)}m.className+=" tick-slider-selection"}if(this.tickLabels=[],Array.isArray(this.options.ticks_labels)&&this.options.ticks_labels.length>0)for(this.tickLabelContainer=document.createElement("div"),this.tickLabelContainer.className="slider-tick-label-container",i=0;i<this.options.ticks_labels.length;i++){var E=document.createElement("div"),F=0===this.options.ticks_positions.length,G=this.options.reversed&&F?this.options.ticks_labels.length-(i+1):i;E.className="slider-tick-label",E.innerHTML=this.options.ticks_labels[G],this.tickLabels.push(E),this.tickLabelContainer.appendChild(E)}var H=function(a){var b=document.createElement("div");b.className="tooltip-arrow";var c=document.createElement("div");c.className="tooltip-inner",a.appendChild(b),a.appendChild(c)},I=document.createElement("div");I.className="tooltip tooltip-main",I.setAttribute("role","presentation"),H(I);var J=document.createElement("div");J.className="tooltip tooltip-min",J.setAttribute("role","presentation"),H(J);var K=document.createElement("div");K.className="tooltip tooltip-max",K.setAttribute("role","presentation"),H(K),this.sliderElem.appendChild(u),this.sliderElem.appendChild(I),this.sliderElem.appendChild(J),this.sliderElem.appendChild(K),this.tickLabelContainer&&this.sliderElem.appendChild(this.tickLabelContainer),this.ticksContainer&&this.sliderElem.appendChild(this.ticksContainer),this.sliderElem.appendChild(p),this.sliderElem.appendChild(q),t.insertBefore(this.sliderElem,this.element),this.element.style.display="none"}if(a&&(this.$element=a(this.element),this.$sliderElem=a(this.sliderElem)),this.eventToCallbackMap={},this.sliderElem.id=this.options.id,this.touchCapable="ontouchstart"in window||window.DocumentTouch&&document instanceof window.DocumentTouch,this.touchX=0,this.touchY=0,this.tooltip=this.sliderElem.querySelector(".tooltip-main"),this.tooltipInner=this.tooltip.querySelector(".tooltip-inner"),this.tooltip_min=this.sliderElem.querySelector(".tooltip-min"),this.tooltipInner_min=this.tooltip_min.querySelector(".tooltip-inner"),this.tooltip_max=this.sliderElem.querySelector(".tooltip-max"),this.tooltipInner_max=this.tooltip_max.querySelector(".tooltip-inner"),h[this.options.scale]&&(this.options.scale=h[this.options.scale]),s===!0&&(this._removeClass(this.sliderElem,"slider-horizontal"),this._removeClass(this.sliderElem,"slider-vertical"),this._removeClass(this.sliderElem,"slider-rtl"),this._removeClass(this.tooltip,"hide"),this._removeClass(this.tooltip_min,"hide"),this._removeClass(this.tooltip_max,"hide"),["left","right","top","width","height"].forEach(function(a){this._removeProperty(this.trackLow,a),this._removeProperty(this.trackSelection,a),this._removeProperty(this.trackHigh,a)},this),[this.handle1,this.handle2].forEach(function(a){this._removeProperty(a,"left"),this._removeProperty(a,"right"),this._removeProperty(a,"top")},this),[this.tooltip,this.tooltip_min,this.tooltip_max].forEach(function(a){this._removeProperty(a,"left"),this._removeProperty(a,"right"),this._removeProperty(a,"top"),this._removeClass(a,"right"),this._removeClass(a,"left"),this._removeClass(a,"top")},this)),"vertical"===this.options.orientation?(this._addClass(this.sliderElem,"slider-vertical"),this.stylePos="top",this.mousePos="pageY",this.sizePos="offsetHeight"):(this._addClass(this.sliderElem,"slider-horizontal"),this.sliderElem.style.width=r,this.options.orientation="horizontal",this.options.rtl?this.stylePos="right":this.stylePos="left",this.mousePos="clientX",this.sizePos="offsetWidth"),this.options.rtl&&this._addClass(this.sliderElem,"slider-rtl"),this._setTooltipPosition(),Array.isArray(this.options.ticks)&&this.options.ticks.length>0&&(g||(this.options.max=Math.max.apply(Math,this.options.ticks)),f||(this.options.min=Math.min.apply(Math,this.options.ticks))),Array.isArray(this.options.value)?(this.options.range=!0,this._state.value=this.options.value):this.options.range?this._state.value=[this.options.value,this.options.max]:this._state.value=this.options.value,this.trackLow=n||this.trackLow,this.trackSelection=m||this.trackSelection,this.trackHigh=o||this.trackHigh,"none"===this.options.selection?(this._addClass(this.trackLow,"hide"),this._addClass(this.trackSelection,"hide"),this._addClass(this.trackHigh,"hide")):("after"===this.options.selection||"before"===this.options.selection)&&(this._removeClass(this.trackLow,"hide"),this._removeClass(this.trackSelection,"hide"),this._removeClass(this.trackHigh,"hide")),this.handle1=p||this.handle1,this.handle2=q||this.handle2,s===!0)for(this._removeClass(this.handle1,"round triangle"),this._removeClass(this.handle2,"round triangle hide"),i=0;i<this.ticks.length;i++)this._removeClass(this.ticks[i],"round triangle hide");var L=["round","triangle","custom"],M=-1!==L.indexOf(this.options.handle);if(M)for(this._addClass(this.handle1,this.options.handle),this._addClass(this.handle2,this.options.handle),i=0;i<this.ticks.length;i++)this._addClass(this.ticks[i],this.options.handle);if(this._state.offset=this._offset(this.sliderElem),this._state.size=this.sliderElem[this.sizePos],this.setValue(this._state.value),this.handle1Keydown=this._keydown.bind(this,0),this.handle1.addEventListener("keydown",this.handle1Keydown,!1),this.handle2Keydown=this._keydown.bind(this,1),this.handle2.addEventListener("keydown",this.handle2Keydown,!1),this.mousedown=this._mousedown.bind(this),this.touchstart=this._touchstart.bind(this),this.touchmove=this._touchmove.bind(this),this.touchCapable){var N=!1;try{var O=Object.defineProperty({},"passive",{get:function(){N=!0}});window.addEventListener("test",null,O)}catch(P){}var Q=N?{passive:!0}:!1;this.sliderElem.addEventListener("touchstart",this.touchstart,Q),this.sliderElem.addEventListener("touchmove",this.touchmove,Q)}if(this.sliderElem.addEventListener("mousedown",this.mousedown,!1),this.resize=this._resize.bind(this),window.addEventListener("resize",this.resize,!1),"hide"===this.options.tooltip)this._addClass(this.tooltip,"hide"),this._addClass(this.tooltip_min,"hide"),this._addClass(this.tooltip_max,"hide");else if("always"===this.options.tooltip)this._showTooltip(),this._alwaysShowTooltip=!0;else{if(this.showTooltip=this._showTooltip.bind(this),this.hideTooltip=this._hideTooltip.bind(this),this.options.ticks_tooltip){var R=this._addTickListener(),S=R.addMouseEnter(this,this.handle1),T=R.addMouseLeave(this,this.handle1);this.handleCallbackMap.handle1={mouseEnter:S,mouseLeave:T},S=R.addMouseEnter(this,this.handle2),T=R.addMouseLeave(this,this.handle2),this.handleCallbackMap.handle2={mouseEnter:S,mouseLeave:T}}else this.sliderElem.addEventListener("mouseenter",this.showTooltip,!1),this.sliderElem.addEventListener("mouseleave",this.hideTooltip,!1);this.handle1.addEventListener("focus",this.showTooltip,!1),this.handle1.addEventListener("blur",this.hideTooltip,!1),this.handle2.addEventListener("focus",this.showTooltip,!1),this.handle2.addEventListener("blur",this.hideTooltip,!1)}this.options.enabled?this.enable():this.disable()}var f=void 0,g={formatInvalidInputErrorMsg:function(a){return"Invalid input value '"+a+"' passed in"},callingContextNotSliderInstance:"Calling context element does not have instance of Slider bound to it. Check your code to make sure the JQuery object returned from the call to the slider() initializer is calling the method"},h={linear:{getValue:function(a,b){return a<b.min?b.min:a>b.max?b.max:a},toValue:function(a){var b=a/100*(this.options.max-this.options.min),c=!0;if(this.options.ticks_positions.length>0){for(var d,e,f,g=0,i=1;i<this.options.ticks_positions.length;i++)if(a<=this.options.ticks_positions[i]){d=this.options.ticks[i-1],f=this.options.ticks_positions[i-1],e=this.options.ticks[i],g=this.options.ticks_positions[i];break}var j=(a-f)/(g-f);b=d+j*(e-d),c=!1}var k=c?this.options.min:0,l=k+Math.round(b/this.options.step)*this.options.step;return h.linear.getValue(l,this.options)},toPercentage:function(a){if(this.options.max===this.options.min)return 0;if(this.options.ticks_positions.length>0){for(var b,c,d,e=0,f=0;f<this.options.ticks.length;f++)if(a<=this.options.ticks[f]){b=f>0?this.options.ticks[f-1]:0,d=f>0?this.options.ticks_positions[f-1]:0,c=this.options.ticks[f],e=this.options.ticks_positions[f];break}if(f>0){var g=(a-b)/(c-b);return d+g*(e-d)}}return 100*(a-this.options.min)/(this.options.max-this.options.min)}},logarithmic:{toValue:function(a){var b=1-this.options.min,c=Math.log(this.options.min+b),d=Math.log(this.options.max+b),e=Math.exp(c+(d-c)*a/100)-b;return Math.round(e)===d?d:(e=this.options.min+Math.round((e-this.options.min)/this.options.step)*this.options.step,h.linear.getValue(e,this.options))},toPercentage:function(a){if(this.options.max===this.options.min)return 0;var b=1-this.options.min,c=Math.log(this.options.max+b),d=Math.log(this.options.min+b),e=Math.log(a+b);return 100*(e-d)/(c-d)}}};d=function(a,b){return e.call(this,a,b),this},d.prototype={_init:function(){},constructor:d,defaultOptions:{id:"",min:0,max:10,step:1,precision:0,orientation:"horizontal",value:5,range:!1,selection:"before",tooltip:"show",tooltip_split:!1,lock_to_ticks:!1,handle:"round",reversed:!1,rtl:"auto",enabled:!0,formatter:function(a){return Array.isArray(a)?a[0]+" : "+a[1]:a},natural_arrow_keys:!1,ticks:[],ticks_positions:[],ticks_labels:[],ticks_snap_bounds:0,ticks_tooltip:!1,scale:"linear",focus:!1,tooltip_position:null,labelledby:null,rangeHighlights:[]},getElement:function(){return this.sliderElem},getValue:function(){return this.options.range?this._state.value:this._state.value[0]},setValue:function(a,b,c){a||(a=0);var d=this.getValue();this._state.value=this._validateInputValue(a);var e=this._applyPrecision.bind(this);this.options.range?(this._state.value[0]=e(this._state.value[0]),this._state.value[1]=e(this._state.value[1]),this.ticksAreValid&&this.options.lock_to_ticks&&(this._state.value[0]=this.options.ticks[this._getClosestTickIndex(this._state.value[0])],this._state.value[1]=this.options.ticks[this._getClosestTickIndex(this._state.value[1])]),this._state.value[0]=Math.max(this.options.min,Math.min(this.options.max,this._state.value[0])),this._state.value[1]=Math.max(this.options.min,Math.min(this.options.max,this._state.value[1]))):(this._state.value=e(this._state.value),this.ticksAreValid&&this.options.lock_to_ticks&&(this._state.value=this.options.ticks[this._getClosestTickIndex(this._state.value)]),this._state.value=[Math.max(this.options.min,Math.min(this.options.max,this._state.value))],this._addClass(this.handle2,"hide"),"after"===this.options.selection?this._state.value[1]=this.options.max:this._state.value[1]=this.options.min),this._setTickIndex(),this.options.max>this.options.min?this._state.percentage=[this._toPercentage(this._state.value[0]),this._toPercentage(this._state.value[1]),100*this.options.step/(this.options.max-this.options.min)]:this._state.percentage=[0,0,100],this._layout();var f=this.options.range?this._state.value:this._state.value[0];this._setDataVal(f),b===!0&&this._trigger("slide",f);var g=!1;return g=Array.isArray(f)?d[0]!==f[0]||d[1]!==f[1]:d!==f,g&&c===!0&&this._trigger("change",{oldValue:d,newValue:f}),this},destroy:function(){this._removeSliderEventHandlers(),this.sliderElem.parentNode.removeChild(this.sliderElem),this.element.style.display="",this._cleanUpEventCallbacksMap(),this.element.removeAttribute("data"),a&&(this._unbindJQueryEventHandlers(),f===b&&this.$element.removeData(f),this.$element.removeData(c))},disable:function(){return this._state.enabled=!1,this.handle1.removeAttribute("tabindex"),this.handle2.removeAttribute("tabindex"),this._addClass(this.sliderElem,"slider-disabled"),this._trigger("slideDisabled"),this},enable:function(){return this._state.enabled=!0,this.handle1.setAttribute("tabindex",0),this.handle2.setAttribute("tabindex",0),this._removeClass(this.sliderElem,"slider-disabled"),this._trigger("slideEnabled"),this},toggle:function(){return this._state.enabled?this.disable():this.enable(),this},isEnabled:function(){return this._state.enabled},on:function(a,b){return this._bindNonQueryEventHandler(a,b),this},off:function(b,c){a?(this.$element.off(b,c),this.$sliderElem.off(b,c)):this._unbindNonQueryEventHandler(b,c)},getAttribute:function(a){return a?this.options[a]:this.options},setAttribute:function(a,b){return this.options[a]=b,this},refresh:function(d){var g=this.getValue();return this._removeSliderEventHandlers(),e.call(this,this.element,this.options),d&&d.useCurrentValue===!0&&this.setValue(g),a&&(f===b?(a.data(this.element,b,this),a.data(this.element,c,this)):a.data(this.element,c,this)),this},relayout:function(){return this._resize(),this},_removeTooltipListener:function(a){this.handle1.removeEventListener(a,this.showTooltip,!1),this.handle2.removeEventListener(a,this.showTooltip,!1)},_removeSliderEventHandlers:function(){if(this.handle1.removeEventListener("keydown",this.handle1Keydown,!1),this.handle2.removeEventListener("keydown",this.handle2Keydown,!1),this.options.ticks_tooltip){for(var a=this.ticksContainer.getElementsByClassName("slider-tick"),b=0;b<a.length;b++)a[b].removeEventListener("mouseenter",this.ticksCallbackMap[b].mouseEnter,!1),a[b].removeEventListener("mouseleave",this.ticksCallbackMap[b].mouseLeave,!1);this.handleCallbackMap.handle1&&this.handleCallbackMap.handle2&&(this.handle1.removeEventListener("mouseenter",this.handleCallbackMap.handle1.mouseEnter,!1),this.handle2.removeEventListener("mouseenter",this.handleCallbackMap.handle2.mouseEnter,!1),this.handle1.removeEventListener("mouseleave",this.handleCallbackMap.handle1.mouseLeave,!1),this.handle2.removeEventListener("mouseleave",this.handleCallbackMap.handle2.mouseLeave,!1))}this.handleCallbackMap=null,this.ticksCallbackMap=null,this.showTooltip&&this._removeTooltipListener("focus"),this.hideTooltip&&this._removeTooltipListener("blur"),this.showTooltip&&this.sliderElem.removeEventListener("mouseenter",this.showTooltip,!1),this.hideTooltip&&this.sliderElem.removeEventListener("mouseleave",this.hideTooltip,!1),this.sliderElem.removeEventListener("touchstart",this.touchstart,!1),this.sliderElem.removeEventListener("touchmove",this.touchmove,!1),this.sliderElem.removeEventListener("mousedown",this.mousedown,!1),window.removeEventListener("resize",this.resize,!1)},_bindNonQueryEventHandler:function(a,b){void 0===this.eventToCallbackMap[a]&&(this.eventToCallbackMap[a]=[]),this.eventToCallbackMap[a].push(b)},_unbindNonQueryEventHandler:function(a,b){var c=this.eventToCallbackMap[a];if(void 0!==c)for(var d=0;d<c.length;d++)if(c[d]===b){c.splice(d,1);break}},_cleanUpEventCallbacksMap:function(){for(var a=Object.keys(this.eventToCallbackMap),b=0;b<a.length;b++){var c=a[b];delete this.eventToCallbackMap[c]}},_showTooltip:function(){this.options.tooltip_split===!1?(this._addClass(this.tooltip,"in"),this.tooltip_min.style.display="none",this.tooltip_max.style.display="none"):(this._addClass(this.tooltip_min,"in"),this._addClass(this.tooltip_max,"in"),this.tooltip.style.display="none"),this._state.over=!0},_hideTooltip:function(){this._state.inDrag===!1&&this._alwaysShowTooltip!==!0&&(this._removeClass(this.tooltip,"in"),this._removeClass(this.tooltip_min,"in"),this._removeClass(this.tooltip_max,"in")),this._state.over=!1},_setToolTipOnMouseOver:function(a){function b(a,b){return b?[100-a.percentage[0],c.options.range?100-a.percentage[1]:a.percentage[1]]:[a.percentage[0],a.percentage[1]]}var c=this,d=this.options.formatter(a?a.value[0]:this._state.value[0]),e=a?b(a,this.options.reversed):b(this._state,this.options.reversed);this._setText(this.tooltipInner,d),this.tooltip.style[this.stylePos]=e[0]+"%"},_copyState:function(){return{value:[this._state.value[0],this._state.value[1]],enabled:this._state.enabled,offset:this._state.offset,size:this._state.size,percentage:[this._state.percentage[0],this._state.percentage[1],this._state.percentage[2]],inDrag:this._state.inDrag,over:this._state.over,dragged:this._state.dragged,keyCtrl:this._state.keyCtrl}},_addTickListener:function(){return{addMouseEnter:function(a,b,c){var d=function(){var d=a._copyState(),e=b===a.handle1?d.value[0]:d.value[1],f=void 0;void 0!==c?(e=a.options.ticks[c],f=a.options.ticks_positions.length>0&&a.options.ticks_positions[c]||a._toPercentage(a.options.ticks[c])):f=a._toPercentage(e),d.value[0]=e,d.percentage[0]=f,a._setToolTipOnMouseOver(d),a._showTooltip()};return b.addEventListener("mouseenter",d,!1),d},addMouseLeave:function(a,b){var c=function(){a._hideTooltip()};return b.addEventListener("mouseleave",c,!1),c}}},_layout:function(){var a,b;if(a=this.options.reversed?[100-this._state.percentage[0],this.options.range?100-this._state.percentage[1]:this._state.percentage[1]]:[this._state.percentage[0],this._state.percentage[1]],this.handle1.style[this.stylePos]=a[0]+"%",this.handle1.setAttribute("aria-valuenow",this._state.value[0]),b=this.options.formatter(this._state.value[0]),isNaN(b)?this.handle1.setAttribute("aria-valuetext",b):this.handle1.removeAttribute("aria-valuetext"),this.handle2.style[this.stylePos]=a[1]+"%",this.handle2.setAttribute("aria-valuenow",this._state.value[1]),b=this.options.formatter(this._state.value[1]),isNaN(b)?this.handle2.setAttribute("aria-valuetext",b):this.handle2.removeAttribute("aria-valuetext"),this.rangeHighlightElements.length>0&&Array.isArray(this.options.rangeHighlights)&&this.options.rangeHighlights.length>0)for(var c=0;c<this.options.rangeHighlights.length;c++){var d=this._toPercentage(this.options.rangeHighlights[c].start),e=this._toPercentage(this.options.rangeHighlights[c].end);if(this.options.reversed){var f=100-e;e=100-d,d=f}var g=this._createHighlightRange(d,e);g?"vertical"===this.options.orientation?(this.rangeHighlightElements[c].style.top=g.start+"%",this.rangeHighlightElements[c].style.height=g.size+"%"):(this.options.rtl?this.rangeHighlightElements[c].style.right=g.start+"%":this.rangeHighlightElements[c].style.left=g.start+"%",this.rangeHighlightElements[c].style.width=g.size+"%"):this.rangeHighlightElements[c].style.display="none"}if(Array.isArray(this.options.ticks)&&this.options.ticks.length>0){var h,i="vertical"===this.options.orientation?"height":"width";h="vertical"===this.options.orientation?"marginTop":this.options.rtl?"marginRight":"marginLeft";var j=this._state.size/(this.options.ticks.length-1);if(this.tickLabelContainer){var k=0;if(0===this.options.ticks_positions.length)"vertical"!==this.options.orientation&&(this.tickLabelContainer.style[h]=-j/2+"px"),k=this.tickLabelContainer.offsetHeight;else for(l=0;l<this.tickLabelContainer.childNodes.length;l++)this.tickLabelContainer.childNodes[l].offsetHeight>k&&(k=this.tickLabelContainer.childNodes[l].offsetHeight);"horizontal"===this.options.orientation&&(this.sliderElem.style.marginBottom=k+"px")}for(var l=0;l<this.options.ticks.length;l++){var m=this.options.ticks_positions[l]||this._toPercentage(this.options.ticks[l]);this.options.reversed&&(m=100-m),this.ticks[l].style[this.stylePos]=m+"%",this._removeClass(this.ticks[l],"in-selection"),this.options.range?m>=a[0]&&m<=a[1]&&this._addClass(this.ticks[l],"in-selection"):"after"===this.options.selection&&m>=a[0]?this._addClass(this.ticks[l],"in-selection"):"before"===this.options.selection&&m<=a[0]&&this._addClass(this.ticks[l],"in-selection"),this.tickLabels[l]&&(this.tickLabels[l].style[i]=j+"px","vertical"!==this.options.orientation&&void 0!==this.options.ticks_positions[l]?(this.tickLabels[l].style.position="absolute",this.tickLabels[l].style[this.stylePos]=m+"%",this.tickLabels[l].style[h]=-j/2+"px"):"vertical"===this.options.orientation&&(this.options.rtl?this.tickLabels[l].style.marginRight=this.sliderElem.offsetWidth+"px":this.tickLabels[l].style.marginLeft=this.sliderElem.offsetWidth+"px",this.tickLabelContainer.style[h]=this.sliderElem.offsetWidth/2*-1+"px"),this._removeClass(this.tickLabels[l],"label-in-selection label-is-selection"),this.options.range?m>=a[0]&&m<=a[1]&&(this._addClass(this.tickLabels[l],"label-in-selection"),(m===a[0]||a[1])&&this._addClass(this.tickLabels[l],"label-is-selection")):("after"===this.options.selection&&m>=a[0]?this._addClass(this.tickLabels[l],"label-in-selection"):"before"===this.options.selection&&m<=a[0]&&this._addClass(this.tickLabels[l],"label-in-selection"),m===a[0]&&this._addClass(this.tickLabels[l],"label-is-selection")))}}var n;if(this.options.range){n=this.options.formatter(this._state.value),this._setText(this.tooltipInner,n),this.tooltip.style[this.stylePos]=(a[1]+a[0])/2+"%";var o=this.options.formatter(this._state.value[0]);this._setText(this.tooltipInner_min,o);var p=this.options.formatter(this._state.value[1]);this._setText(this.tooltipInner_max,p),this.tooltip_min.style[this.stylePos]=a[0]+"%",this.tooltip_max.style[this.stylePos]=a[1]+"%"}else n=this.options.formatter(this._state.value[0]),this._setText(this.tooltipInner,n),this.tooltip.style[this.stylePos]=a[0]+"%";if("vertical"===this.options.orientation)this.trackLow.style.top="0",this.trackLow.style.height=Math.min(a[0],a[1])+"%",this.trackSelection.style.top=Math.min(a[0],a[1])+"%",this.trackSelection.style.height=Math.abs(a[0]-a[1])+"%",this.trackHigh.style.bottom="0",this.trackHigh.style.height=100-Math.min(a[0],a[1])-Math.abs(a[0]-a[1])+"%";else{"right"===this.stylePos?this.trackLow.style.right="0":this.trackLow.style.left="0",this.trackLow.style.width=Math.min(a[0],a[1])+"%","right"===this.stylePos?this.trackSelection.style.right=Math.min(a[0],a[1])+"%":this.trackSelection.style.left=Math.min(a[0],a[1])+"%",this.trackSelection.style.width=Math.abs(a[0]-a[1])+"%","right"===this.stylePos?this.trackHigh.style.left="0":this.trackHigh.style.right="0",this.trackHigh.style.width=100-Math.min(a[0],a[1])-Math.abs(a[0]-a[1])+"%";var q=this.tooltip_min.getBoundingClientRect(),r=this.tooltip_max.getBoundingClientRect();"bottom"===this.options.tooltip_position?q.right>r.left?(this._removeClass(this.tooltip_max,"bottom"),this._addClass(this.tooltip_max,"top"),this.tooltip_max.style.top="",this.tooltip_max.style.bottom="22px"):(this._removeClass(this.tooltip_max,"top"),this._addClass(this.tooltip_max,"bottom"),this.tooltip_max.style.top=this.tooltip_min.style.top,this.tooltip_max.style.bottom=""):q.right>r.left?(this._removeClass(this.tooltip_max,"top"),this._addClass(this.tooltip_max,"bottom"),this.tooltip_max.style.top="18px"):(this._removeClass(this.tooltip_max,"bottom"),this._addClass(this.tooltip_max,"top"),this.tooltip_max.style.top=this.tooltip_min.style.top)}},_createHighlightRange:function(a,b){return this._isHighlightRange(a,b)?a>b?{start:b,size:a-b}:{start:a,size:b-a}:null},_isHighlightRange:function(a,b){return a>=0&&100>=a&&b>=0&&100>=b?!0:!1},_resize:function(a){this._state.offset=this._offset(this.sliderElem),this._state.size=this.sliderElem[this.sizePos],this._layout()},_removeProperty:function(a,b){a.style.removeProperty?a.style.removeProperty(b):a.style.removeAttribute(b)},_mousedown:function(a){if(!this._state.enabled)return!1;a.preventDefault&&a.preventDefault(),this._state.offset=this._offset(this.sliderElem),this._state.size=this.sliderElem[this.sizePos];var b=this._getPercentage(a);if(this.options.range){var c=Math.abs(this._state.percentage[0]-b),d=Math.abs(this._state.percentage[1]-b);this._state.dragged=d>c?0:1,this._adjustPercentageForRangeSliders(b)}else this._state.dragged=0;this._state.percentage[this._state.dragged]=b,this.touchCapable&&(document.removeEventListener("touchmove",this.mousemove,!1),document.removeEventListener("touchend",this.mouseup,!1)),this.mousemove&&document.removeEventListener("mousemove",this.mousemove,!1),this.mouseup&&document.removeEventListener("mouseup",this.mouseup,!1),this.mousemove=this._mousemove.bind(this),this.mouseup=this._mouseup.bind(this),this.touchCapable&&(document.addEventListener("touchmove",this.mousemove,!1),document.addEventListener("touchend",this.mouseup,!1)),document.addEventListener("mousemove",this.mousemove,!1),document.addEventListener("mouseup",this.mouseup,!1),this._state.inDrag=!0;var e=this._calculateValue();return this._trigger("slideStart",e),this.setValue(e,!1,!0),a.returnValue=!1,this.options.focus&&this._triggerFocusOnHandle(this._state.dragged),!0},_touchstart:function(a){if(void 0===a.changedTouches)return void this._mousedown(a);var b=a.changedTouches[0];this.touchX=b.pageX,this.touchY=b.pageY},_triggerFocusOnHandle:function(a){0===a&&this.handle1.focus(),1===a&&this.handle2.focus()},_keydown:function(a,b){if(!this._state.enabled)return!1;var c;switch(b.keyCode){case 37:case 40:c=-1;break;case 39:case 38:c=1}if(c){if(this.options.natural_arrow_keys){var d="horizontal"===this.options.orientation,e="vertical"===this.options.orientation,f=this.options.rtl,g=this.options.reversed;d?f?g||(c=-c):g&&(c=-c):e&&(g||(c=-c))}var h;if(this.ticksAreValid&&this.options.lock_to_ticks){var i=void 0;i=this.options.ticks.indexOf(this._state.value[a]),-1===i&&(i=0,window.console.warn("(lock_to_ticks) _keydown: index should not be -1")),i+=c,i=Math.max(0,Math.min(this.options.ticks.length-1,i)),h=this.options.ticks[i]}else h=this._state.value[a]+c*this.options.step;var j=this._toPercentage(h);if(this._state.keyCtrl=a,this.options.range){this._adjustPercentageForRangeSliders(j);var k=this._state.keyCtrl?this._state.value[0]:h,l=this._state.keyCtrl?h:this._state.value[1];h=[Math.max(this.options.min,Math.min(this.options.max,k)),Math.max(this.options.min,Math.min(this.options.max,l))]}else h=Math.max(this.options.min,Math.min(this.options.max,h));return this._trigger("slideStart",h),this.setValue(h,!0,!0),this._trigger("slideStop",h),this._pauseEvent(b),delete this._state.keyCtrl,!1}},_pauseEvent:function(a){a.stopPropagation&&a.stopPropagation(),a.preventDefault&&a.preventDefault(),a.cancelBubble=!0,a.returnValue=!1},_mousemove:function(a){if(!this._state.enabled)return!1;var b=this._getPercentage(a);this._adjustPercentageForRangeSliders(b),this._state.percentage[this._state.dragged]=b;var c=this._calculateValue(!0);return this.setValue(c,!0,!0),!1},_touchmove:function(a){if(void 0!==a.changedTouches){var b=a.changedTouches[0],c=b.pageX-this.touchX,d=b.pageY-this.touchY;this._state.inDrag||("vertical"===this.options.orientation&&5>=c&&c>=-5&&(d>=15||-15>=d)?this._mousedown(a):5>=d&&d>=-5&&(c>=15||-15>=c)&&this._mousedown(a))}},_adjustPercentageForRangeSliders:function(a){if(this.options.range){var b=this._getNumDigitsAfterDecimalPlace(a);b=b?b-1:0;var c=this._applyToFixedAndParseFloat(a,b);0===this._state.dragged&&this._applyToFixedAndParseFloat(this._state.percentage[1],b)<c?(this._state.percentage[0]=this._state.percentage[1],this._state.dragged=1):1===this._state.dragged&&this._applyToFixedAndParseFloat(this._state.percentage[0],b)>c?(this._state.percentage[1]=this._state.percentage[0],this._state.dragged=0):0===this._state.keyCtrl&&this._toPercentage(this._state.value[1])<a?(this._state.percentage[0]=this._state.percentage[1], -this._state.keyCtrl=1,this.handle2.focus()):1===this._state.keyCtrl&&this._toPercentage(this._state.value[0])>a&&(this._state.percentage[1]=this._state.percentage[0],this._state.keyCtrl=0,this.handle1.focus())}},_mouseup:function(a){if(!this._state.enabled)return!1;var b=this._getPercentage(a);this._adjustPercentageForRangeSliders(b),this._state.percentage[this._state.dragged]=b,this.touchCapable&&(document.removeEventListener("touchmove",this.mousemove,!1),document.removeEventListener("touchend",this.mouseup,!1)),document.removeEventListener("mousemove",this.mousemove,!1),document.removeEventListener("mouseup",this.mouseup,!1),this._state.inDrag=!1,this._state.over===!1&&this._hideTooltip();var c=this._calculateValue(!0);return this.setValue(c,!1,!0),this._trigger("slideStop",c),this._state.dragged=null,!1},_setValues:function(a,b){var c=0===a?0:100;this._state.percentage[a]!==c&&(b.data[a]=this._toValue(this._state.percentage[a]),b.data[a]=this._applyPrecision(b.data[a]))},_calculateValue:function(a){var b={};return this.options.range?(b.data=[this.options.min,this.options.max],this._setValues(0,b),this._setValues(1,b),a&&(b.data[0]=this._snapToClosestTick(b.data[0]),b.data[1]=this._snapToClosestTick(b.data[1]))):(b.data=this._toValue(this._state.percentage[0]),b.data=parseFloat(b.data),b.data=this._applyPrecision(b.data),a&&(b.data=this._snapToClosestTick(b.data))),b.data},_snapToClosestTick:function(a){for(var b=[a,1/0],c=0;c<this.options.ticks.length;c++){var d=Math.abs(this.options.ticks[c]-a);d<=b[1]&&(b=[this.options.ticks[c],d])}return b[1]<=this.options.ticks_snap_bounds?b[0]:a},_applyPrecision:function(a){var b=this.options.precision||this._getNumDigitsAfterDecimalPlace(this.options.step);return this._applyToFixedAndParseFloat(a,b)},_getNumDigitsAfterDecimalPlace:function(a){var b=(""+a).match(/(?:\.(\d+))?(?:[eE]([+-]?\d+))?$/);return b?Math.max(0,(b[1]?b[1].length:0)-(b[2]?+b[2]:0)):0},_applyToFixedAndParseFloat:function(a,b){var c=a.toFixed(b);return parseFloat(c)},_getPercentage:function(a){!this.touchCapable||"touchstart"!==a.type&&"touchmove"!==a.type||(a=a.touches[0]);var b=a[this.mousePos],c=this._state.offset[this.stylePos],d=b-c;"right"===this.stylePos&&(d=-d);var e=d/this._state.size*100;return e=Math.round(e/this._state.percentage[2])*this._state.percentage[2],this.options.reversed&&(e=100-e),Math.max(0,Math.min(100,e))},_validateInputValue:function(a){if(isNaN(+a)){if(Array.isArray(a))return this._validateArray(a),a;throw new Error(g.formatInvalidInputErrorMsg(a))}return+a},_validateArray:function(a){for(var b=0;b<a.length;b++){var c=a[b];if("number"!=typeof c)throw new Error(g.formatInvalidInputErrorMsg(c))}},_setDataVal:function(a){this.element.setAttribute("data-value",a),this.element.setAttribute("value",a),this.element.value=a},_trigger:function(b,c){c=c||0===c?c:void 0;var d=this.eventToCallbackMap[b];if(d&&d.length)for(var e=0;e<d.length;e++){var f=d[e];f(c)}a&&this._triggerJQueryEvent(b,c)},_triggerJQueryEvent:function(a,b){var c={type:a,value:b};this.$element.trigger(c),this.$sliderElem.trigger(c)},_unbindJQueryEventHandlers:function(){this.$element.off(),this.$sliderElem.off()},_setText:function(a,b){"undefined"!=typeof a.textContent?a.textContent=b:"undefined"!=typeof a.innerText&&(a.innerText=b)},_removeClass:function(a,b){for(var c=b.split(" "),d=a.className,e=0;e<c.length;e++){var f=c[e],g=new RegExp("(?:\\s|^)"+f+"(?:\\s|$)");d=d.replace(g," ")}a.className=d.trim()},_addClass:function(a,b){for(var c=b.split(" "),d=a.className,e=0;e<c.length;e++){var f=c[e],g=new RegExp("(?:\\s|^)"+f+"(?:\\s|$)"),h=g.test(d);h||(d+=" "+f)}a.className=d.trim()},_offsetLeft:function(a){return a.getBoundingClientRect().left},_offsetRight:function(a){return a.getBoundingClientRect().right},_offsetTop:function(a){for(var b=a.offsetTop;(a=a.offsetParent)&&!isNaN(a.offsetTop);)b+=a.offsetTop,"BODY"!==a.tagName&&(b-=a.scrollTop);return b},_offset:function(a){return{left:this._offsetLeft(a),right:this._offsetRight(a),top:this._offsetTop(a)}},_css:function(b,c,d){if(a)a.style(b,c,d);else{var e=c.replace(/^-ms-/,"ms-").replace(/-([\da-z])/gi,function(a,b){return b.toUpperCase()});b.style[e]=d}},_toValue:function(a){return this.options.scale.toValue.apply(this,[a])},_toPercentage:function(a){return this.options.scale.toPercentage.apply(this,[a])},_setTooltipPosition:function(){var a=[this.tooltip,this.tooltip_min,this.tooltip_max];if("vertical"===this.options.orientation){var b;b=this.options.tooltip_position?this.options.tooltip_position:this.options.rtl?"left":"right";var c="left"===b?"right":"left";a.forEach(function(a){this._addClass(a,b),a.style[c]="100%"}.bind(this))}else"bottom"===this.options.tooltip_position?a.forEach(function(a){this._addClass(a,"bottom"),a.style.top="22px"}.bind(this)):a.forEach(function(a){this._addClass(a,"top"),a.style.top=-this.tooltip.outerHeight-14+"px"}.bind(this))},_getClosestTickIndex:function(a){for(var b=Math.abs(a-this.options.ticks[0]),c=0,d=0;d<this.options.ticks.length;++d){var e=Math.abs(a-this.options.ticks[d]);b>e&&(b=e,c=d)}return c},_setTickIndex:function(){this.ticksAreValid&&(this._state.tickIndex=[this.options.ticks.indexOf(this._state.value[0]),this.options.ticks.indexOf(this._state.value[1])])}},a&&a.fn&&(a.fn.slider?(windowIsDefined&&window.console.warn("bootstrap-slider.js - WARNING: $.fn.slider namespace is already bound. Use the $.fn.bootstrapSlider namespace instead."),f=c):(a.bridget(b,d),f=b),a.bridget(c,d),a(function(){a("input[data-provide=slider]")[f]()}))}(a),d}); \ No newline at end of file +/*! ======================================================= + VERSION 10.6.1 +========================================================= */ +"use strict";var _typeof="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(a){return typeof a}:function(a){return a&&"function"==typeof Symbol&&a.constructor===Symbol&&a!==Symbol.prototype?"symbol":typeof a},windowIsDefined="object"===("undefined"==typeof window?"undefined":_typeof(window));!function(a){if("function"==typeof define&&define.amd)define(["jquery"],a);else if("object"===("undefined"==typeof module?"undefined":_typeof(module))&&module.exports){var b;try{b=require("jquery")}catch(c){b=null}module.exports=a(b)}else window&&(window.Slider=a(window.jQuery))}(function(a){var b="slider",c="bootstrapSlider";windowIsDefined&&!window.console&&(window.console={}),windowIsDefined&&!window.console.log&&(window.console.log=function(){}),windowIsDefined&&!window.console.warn&&(window.console.warn=function(){});var d;return function(a){function b(){}function c(a){function c(b){b.prototype.option||(b.prototype.option=function(b){a.isPlainObject(b)&&(this.options=a.extend(!0,this.options,b))})}function e(b,c){a.fn[b]=function(e){if("string"==typeof e){for(var g=d.call(arguments,1),h=0,i=this.length;i>h;h++){var j=this[h],k=a.data(j,b);if(k)if(a.isFunction(k[e])&&"_"!==e.charAt(0)){var l=k[e].apply(k,g);if(void 0!==l&&l!==k)return l}else f("no such method '"+e+"' for "+b+" instance");else f("cannot call methods on "+b+" prior to initialization; attempted to call '"+e+"'")}return this}var m=this.map(function(){var d=a.data(this,b);return d?(d.option(e),d._init()):(d=new c(this,e),a.data(this,b,d)),a(this)});return!m||m.length>1?m:m[0]}}if(a){var f="undefined"==typeof console?b:function(a){console.error(a)};return a.bridget=function(a,b){c(b),e(a,b)},a.bridget}}var d=Array.prototype.slice;c(a)}(a),function(a){function e(b,c){function d(a,b){var c="data-slider-"+b.replace(/_/g,"-"),d=a.getAttribute(c);try{return JSON.parse(d)}catch(e){return d}}this._state={value:null,enabled:null,offset:null,size:null,percentage:null,inDrag:!1,over:!1,tickIndex:null},this.ticksCallbackMap={},this.handleCallbackMap={},"string"==typeof b?this.element=document.querySelector(b):b instanceof HTMLElement&&(this.element=b),c=c?c:{};for(var e=Object.keys(this.defaultOptions),f=c.hasOwnProperty("min"),g=c.hasOwnProperty("max"),i=0;i<e.length;i++){var j=e[i],k=c[j];k="undefined"!=typeof k?k:d(this.element,j),k=null!==k?k:this.defaultOptions[j],this.options||(this.options={}),this.options[j]=k}if(this.ticksAreValid=Array.isArray(this.options.ticks)&&this.options.ticks.length>0,this.ticksAreValid||(this.options.lock_to_ticks=!1),"auto"===this.options.rtl){var l=window.getComputedStyle(this.element);null!=l?this.options.rtl="rtl"===l.direction:this.options.rtl="rtl"===this.element.style.direction}"vertical"!==this.options.orientation||"top"!==this.options.tooltip_position&&"bottom"!==this.options.tooltip_position?"horizontal"!==this.options.orientation||"left"!==this.options.tooltip_position&&"right"!==this.options.tooltip_position||(this.options.tooltip_position="top"):this.options.rtl?this.options.tooltip_position="left":this.options.tooltip_position="right";var m,n,o,p,q,r=this.element.style.width,s=!1,t=this.element.parentNode;if(this.sliderElem)s=!0;else{this.sliderElem=document.createElement("div"),this.sliderElem.className="slider";var u=document.createElement("div");u.className="slider-track",n=document.createElement("div"),n.className="slider-track-low",m=document.createElement("div"),m.className="slider-selection",o=document.createElement("div"),o.className="slider-track-high",p=document.createElement("div"),p.className="slider-handle min-slider-handle",p.setAttribute("role","slider"),p.setAttribute("aria-valuemin",this.options.min),p.setAttribute("aria-valuemax",this.options.max),q=document.createElement("div"),q.className="slider-handle max-slider-handle",q.setAttribute("role","slider"),q.setAttribute("aria-valuemin",this.options.min),q.setAttribute("aria-valuemax",this.options.max),u.appendChild(n),u.appendChild(m),u.appendChild(o),this.rangeHighlightElements=[];var v=this.options.rangeHighlights;if(Array.isArray(v)&&v.length>0)for(var w=0;w<v.length;w++){var x=document.createElement("div"),y=v[w]["class"]||"";x.className="slider-rangeHighlight slider-selection "+y,this.rangeHighlightElements.push(x),u.appendChild(x)}var z=Array.isArray(this.options.labelledby);if(z&&this.options.labelledby[0]&&p.setAttribute("aria-labelledby",this.options.labelledby[0]),z&&this.options.labelledby[1]&&q.setAttribute("aria-labelledby",this.options.labelledby[1]),!z&&this.options.labelledby&&(p.setAttribute("aria-labelledby",this.options.labelledby),q.setAttribute("aria-labelledby",this.options.labelledby)),this.ticks=[],Array.isArray(this.options.ticks)&&this.options.ticks.length>0){for(this.ticksContainer=document.createElement("div"),this.ticksContainer.className="slider-tick-container",i=0;i<this.options.ticks.length;i++){var A=document.createElement("div");if(A.className="slider-tick",this.options.ticks_tooltip){var B=this._addTickListener(),C=B.addMouseEnter(this,A,i),D=B.addMouseLeave(this,A);this.ticksCallbackMap[i]={mouseEnter:C,mouseLeave:D}}this.ticks.push(A),this.ticksContainer.appendChild(A)}m.className+=" tick-slider-selection"}if(this.tickLabels=[],Array.isArray(this.options.ticks_labels)&&this.options.ticks_labels.length>0)for(this.tickLabelContainer=document.createElement("div"),this.tickLabelContainer.className="slider-tick-label-container",i=0;i<this.options.ticks_labels.length;i++){var E=document.createElement("div"),F=0===this.options.ticks_positions.length,G=this.options.reversed&&F?this.options.ticks_labels.length-(i+1):i;E.className="slider-tick-label",E.innerHTML=this.options.ticks_labels[G],this.tickLabels.push(E),this.tickLabelContainer.appendChild(E)}var H=function(a){var b=document.createElement("div");b.className="tooltip-arrow";var c=document.createElement("div");c.className="tooltip-inner",a.appendChild(b),a.appendChild(c)},I=document.createElement("div");I.className="tooltip tooltip-main",I.setAttribute("role","presentation"),H(I);var J=document.createElement("div");J.className="tooltip tooltip-min",J.setAttribute("role","presentation"),H(J);var K=document.createElement("div");K.className="tooltip tooltip-max",K.setAttribute("role","presentation"),H(K),this.sliderElem.appendChild(u),this.sliderElem.appendChild(I),this.sliderElem.appendChild(J),this.sliderElem.appendChild(K),this.tickLabelContainer&&this.sliderElem.appendChild(this.tickLabelContainer),this.ticksContainer&&this.sliderElem.appendChild(this.ticksContainer),this.sliderElem.appendChild(p),this.sliderElem.appendChild(q),t.insertBefore(this.sliderElem,this.element),this.element.style.display="none"}if(a&&(this.$element=a(this.element),this.$sliderElem=a(this.sliderElem)),this.eventToCallbackMap={},this.sliderElem.id=this.options.id,this.touchCapable="ontouchstart"in window||window.DocumentTouch&&document instanceof window.DocumentTouch,this.touchX=0,this.touchY=0,this.tooltip=this.sliderElem.querySelector(".tooltip-main"),this.tooltipInner=this.tooltip.querySelector(".tooltip-inner"),this.tooltip_min=this.sliderElem.querySelector(".tooltip-min"),this.tooltipInner_min=this.tooltip_min.querySelector(".tooltip-inner"),this.tooltip_max=this.sliderElem.querySelector(".tooltip-max"),this.tooltipInner_max=this.tooltip_max.querySelector(".tooltip-inner"),h[this.options.scale]&&(this.options.scale=h[this.options.scale]),s===!0&&(this._removeClass(this.sliderElem,"slider-horizontal"),this._removeClass(this.sliderElem,"slider-vertical"),this._removeClass(this.sliderElem,"slider-rtl"),this._removeClass(this.tooltip,"hide"),this._removeClass(this.tooltip_min,"hide"),this._removeClass(this.tooltip_max,"hide"),["left","right","top","width","height"].forEach(function(a){this._removeProperty(this.trackLow,a),this._removeProperty(this.trackSelection,a),this._removeProperty(this.trackHigh,a)},this),[this.handle1,this.handle2].forEach(function(a){this._removeProperty(a,"left"),this._removeProperty(a,"right"),this._removeProperty(a,"top")},this),[this.tooltip,this.tooltip_min,this.tooltip_max].forEach(function(a){this._removeProperty(a,"left"),this._removeProperty(a,"right"),this._removeProperty(a,"top"),this._removeClass(a,"right"),this._removeClass(a,"left"),this._removeClass(a,"top")},this)),"vertical"===this.options.orientation?(this._addClass(this.sliderElem,"slider-vertical"),this.stylePos="top",this.mousePos="pageY",this.sizePos="offsetHeight"):(this._addClass(this.sliderElem,"slider-horizontal"),this.sliderElem.style.width=r,this.options.orientation="horizontal",this.options.rtl?this.stylePos="right":this.stylePos="left",this.mousePos="clientX",this.sizePos="offsetWidth"),this.options.rtl&&this._addClass(this.sliderElem,"slider-rtl"),this._setTooltipPosition(),Array.isArray(this.options.ticks)&&this.options.ticks.length>0&&(g||(this.options.max=Math.max.apply(Math,this.options.ticks)),f||(this.options.min=Math.min.apply(Math,this.options.ticks))),Array.isArray(this.options.value)?(this.options.range=!0,this._state.value=this.options.value):this.options.range?this._state.value=[this.options.value,this.options.max]:this._state.value=this.options.value,this.trackLow=n||this.trackLow,this.trackSelection=m||this.trackSelection,this.trackHigh=o||this.trackHigh,"none"===this.options.selection?(this._addClass(this.trackLow,"hide"),this._addClass(this.trackSelection,"hide"),this._addClass(this.trackHigh,"hide")):("after"===this.options.selection||"before"===this.options.selection)&&(this._removeClass(this.trackLow,"hide"),this._removeClass(this.trackSelection,"hide"),this._removeClass(this.trackHigh,"hide")),this.handle1=p||this.handle1,this.handle2=q||this.handle2,s===!0)for(this._removeClass(this.handle1,"round triangle"),this._removeClass(this.handle2,"round triangle hide"),i=0;i<this.ticks.length;i++)this._removeClass(this.ticks[i],"round triangle hide");var L=["round","triangle","custom"],M=-1!==L.indexOf(this.options.handle);if(M)for(this._addClass(this.handle1,this.options.handle),this._addClass(this.handle2,this.options.handle),i=0;i<this.ticks.length;i++)this._addClass(this.ticks[i],this.options.handle);if(this._state.offset=this._offset(this.sliderElem),this._state.size=this.sliderElem[this.sizePos],this.setValue(this._state.value),this.handle1Keydown=this._keydown.bind(this,0),this.handle1.addEventListener("keydown",this.handle1Keydown,!1),this.handle2Keydown=this._keydown.bind(this,1),this.handle2.addEventListener("keydown",this.handle2Keydown,!1),this.mousedown=this._mousedown.bind(this),this.touchstart=this._touchstart.bind(this),this.touchmove=this._touchmove.bind(this),this.touchCapable&&(this.sliderElem.addEventListener("touchstart",this.touchstart,!1),this.sliderElem.addEventListener("touchmove",this.touchmove,!1)),this.sliderElem.addEventListener("mousedown",this.mousedown,!1),this.resize=this._resize.bind(this),window.addEventListener("resize",this.resize,!1),"hide"===this.options.tooltip)this._addClass(this.tooltip,"hide"),this._addClass(this.tooltip_min,"hide"),this._addClass(this.tooltip_max,"hide");else if("always"===this.options.tooltip)this._showTooltip(),this._alwaysShowTooltip=!0;else{if(this.showTooltip=this._showTooltip.bind(this),this.hideTooltip=this._hideTooltip.bind(this),this.options.ticks_tooltip){var N=this._addTickListener(),O=N.addMouseEnter(this,this.handle1),P=N.addMouseLeave(this,this.handle1);this.handleCallbackMap.handle1={mouseEnter:O,mouseLeave:P},O=N.addMouseEnter(this,this.handle2),P=N.addMouseLeave(this,this.handle2),this.handleCallbackMap.handle2={mouseEnter:O,mouseLeave:P}}else this.sliderElem.addEventListener("mouseenter",this.showTooltip,!1),this.sliderElem.addEventListener("mouseleave",this.hideTooltip,!1),this.touchCapable&&(this.sliderElem.addEventListener("touchstart",this.showTooltip,!1),this.sliderElem.addEventListener("touchmove",this.showTooltip,!1),this.sliderElem.addEventListener("touchend",this.hideTooltip,!1));this.handle1.addEventListener("focus",this.showTooltip,!1),this.handle1.addEventListener("blur",this.hideTooltip,!1),this.handle2.addEventListener("focus",this.showTooltip,!1),this.handle2.addEventListener("blur",this.hideTooltip,!1),this.touchCapable&&(this.handle1.addEventListener("touchstart",this.showTooltip,!1),this.handle1.addEventListener("touchmove",this.showTooltip,!1),this.handle1.addEventListener("touchend",this.hideTooltip,!1),this.handle2.addEventListener("touchstart",this.showTooltip,!1),this.handle2.addEventListener("touchmove",this.showTooltip,!1),this.handle2.addEventListener("touchend",this.hideTooltip,!1))}this.options.enabled?this.enable():this.disable()}var f=void 0,g={formatInvalidInputErrorMsg:function(a){return"Invalid input value '"+a+"' passed in"},callingContextNotSliderInstance:"Calling context element does not have instance of Slider bound to it. Check your code to make sure the JQuery object returned from the call to the slider() initializer is calling the method"},h={linear:{getValue:function(a,b){return a<b.min?b.min:a>b.max?b.max:a},toValue:function(a){var b=a/100*(this.options.max-this.options.min),c=!0;if(this.options.ticks_positions.length>0){for(var d,e,f,g=0,i=1;i<this.options.ticks_positions.length;i++)if(a<=this.options.ticks_positions[i]){d=this.options.ticks[i-1],f=this.options.ticks_positions[i-1],e=this.options.ticks[i],g=this.options.ticks_positions[i];break}var j=(a-f)/(g-f);b=d+j*(e-d),c=!1}var k=c?this.options.min:0,l=k+Math.round(b/this.options.step)*this.options.step;return h.linear.getValue(l,this.options)},toPercentage:function(a){if(this.options.max===this.options.min)return 0;if(this.options.ticks_positions.length>0){for(var b,c,d,e=0,f=0;f<this.options.ticks.length;f++)if(a<=this.options.ticks[f]){b=f>0?this.options.ticks[f-1]:0,d=f>0?this.options.ticks_positions[f-1]:0,c=this.options.ticks[f],e=this.options.ticks_positions[f];break}if(f>0){var g=(a-b)/(c-b);return d+g*(e-d)}}return 100*(a-this.options.min)/(this.options.max-this.options.min)}},logarithmic:{toValue:function(a){var b=1-this.options.min,c=Math.log(this.options.min+b),d=Math.log(this.options.max+b),e=Math.exp(c+(d-c)*a/100)-b;return Math.round(e)===d?d:(e=this.options.min+Math.round((e-this.options.min)/this.options.step)*this.options.step,h.linear.getValue(e,this.options))},toPercentage:function(a){if(this.options.max===this.options.min)return 0;var b=1-this.options.min,c=Math.log(this.options.max+b),d=Math.log(this.options.min+b),e=Math.log(a+b);return 100*(e-d)/(c-d)}}};d=function(a,b){return e.call(this,a,b),this},d.prototype={_init:function(){},constructor:d,defaultOptions:{id:"",min:0,max:10,step:1,precision:0,orientation:"horizontal",value:5,range:!1,selection:"before",tooltip:"show",tooltip_split:!1,lock_to_ticks:!1,handle:"round",reversed:!1,rtl:"auto",enabled:!0,formatter:function(a){return Array.isArray(a)?a[0]+" : "+a[1]:a},natural_arrow_keys:!1,ticks:[],ticks_positions:[],ticks_labels:[],ticks_snap_bounds:0,ticks_tooltip:!1,scale:"linear",focus:!1,tooltip_position:null,labelledby:null,rangeHighlights:[]},getElement:function(){return this.sliderElem},getValue:function(){return this.options.range?this._state.value:this._state.value[0]},setValue:function(a,b,c){a||(a=0);var d=this.getValue();this._state.value=this._validateInputValue(a);var e=this._applyPrecision.bind(this);this.options.range?(this._state.value[0]=e(this._state.value[0]),this._state.value[1]=e(this._state.value[1]),this.ticksAreValid&&this.options.lock_to_ticks&&(this._state.value[0]=this.options.ticks[this._getClosestTickIndex(this._state.value[0])],this._state.value[1]=this.options.ticks[this._getClosestTickIndex(this._state.value[1])]),this._state.value[0]=Math.max(this.options.min,Math.min(this.options.max,this._state.value[0])),this._state.value[1]=Math.max(this.options.min,Math.min(this.options.max,this._state.value[1]))):(this._state.value=e(this._state.value),this.ticksAreValid&&this.options.lock_to_ticks&&(this._state.value=this.options.ticks[this._getClosestTickIndex(this._state.value)]),this._state.value=[Math.max(this.options.min,Math.min(this.options.max,this._state.value))],this._addClass(this.handle2,"hide"),"after"===this.options.selection?this._state.value[1]=this.options.max:this._state.value[1]=this.options.min),this._setTickIndex(),this.options.max>this.options.min?this._state.percentage=[this._toPercentage(this._state.value[0]),this._toPercentage(this._state.value[1]),100*this.options.step/(this.options.max-this.options.min)]:this._state.percentage=[0,0,100],this._layout();var f=this.options.range?this._state.value:this._state.value[0];this._setDataVal(f),b===!0&&this._trigger("slide",f);var g=!1;return g=Array.isArray(f)?d[0]!==f[0]||d[1]!==f[1]:d!==f,g&&c===!0&&this._trigger("change",{oldValue:d,newValue:f}),this},destroy:function(){this._removeSliderEventHandlers(),this.sliderElem.parentNode.removeChild(this.sliderElem),this.element.style.display="",this._cleanUpEventCallbacksMap(),this.element.removeAttribute("data"),a&&(this._unbindJQueryEventHandlers(),f===b&&this.$element.removeData(f),this.$element.removeData(c))},disable:function(){return this._state.enabled=!1,this.handle1.removeAttribute("tabindex"),this.handle2.removeAttribute("tabindex"),this._addClass(this.sliderElem,"slider-disabled"),this._trigger("slideDisabled"),this},enable:function(){return this._state.enabled=!0,this.handle1.setAttribute("tabindex",0),this.handle2.setAttribute("tabindex",0),this._removeClass(this.sliderElem,"slider-disabled"),this._trigger("slideEnabled"),this},toggle:function(){return this._state.enabled?this.disable():this.enable(),this},isEnabled:function(){return this._state.enabled},on:function(a,b){return this._bindNonQueryEventHandler(a,b),this},off:function(b,c){a?(this.$element.off(b,c),this.$sliderElem.off(b,c)):this._unbindNonQueryEventHandler(b,c)},getAttribute:function(a){return a?this.options[a]:this.options},setAttribute:function(a,b){return this.options[a]=b,this},refresh:function(d){var g=this.getValue();return this._removeSliderEventHandlers(),e.call(this,this.element,this.options),d&&d.useCurrentValue===!0&&this.setValue(g),a&&(f===b?(a.data(this.element,b,this),a.data(this.element,c,this)):a.data(this.element,c,this)),this},relayout:function(){return this._resize(),this},_removeTooltipListener:function(a,b){this.handle1.removeEventListener(a,b,!1),this.handle2.removeEventListener(a,b,!1)},_removeSliderEventHandlers:function(){if(this.handle1.removeEventListener("keydown",this.handle1Keydown,!1),this.handle2.removeEventListener("keydown",this.handle2Keydown,!1),this.options.ticks_tooltip){for(var a=this.ticksContainer.getElementsByClassName("slider-tick"),b=0;b<a.length;b++)a[b].removeEventListener("mouseenter",this.ticksCallbackMap[b].mouseEnter,!1),a[b].removeEventListener("mouseleave",this.ticksCallbackMap[b].mouseLeave,!1);this.handleCallbackMap.handle1&&this.handleCallbackMap.handle2&&(this.handle1.removeEventListener("mouseenter",this.handleCallbackMap.handle1.mouseEnter,!1),this.handle2.removeEventListener("mouseenter",this.handleCallbackMap.handle2.mouseEnter,!1),this.handle1.removeEventListener("mouseleave",this.handleCallbackMap.handle1.mouseLeave,!1),this.handle2.removeEventListener("mouseleave",this.handleCallbackMap.handle2.mouseLeave,!1))}this.handleCallbackMap=null,this.ticksCallbackMap=null,this.showTooltip&&this._removeTooltipListener("focus",this.showTooltip),this.hideTooltip&&this._removeTooltipListener("blur",this.hideTooltip),this.showTooltip&&this.sliderElem.removeEventListener("mouseenter",this.showTooltip,!1),this.hideTooltip&&this.sliderElem.removeEventListener("mouseleave",this.hideTooltip,!1),this.sliderElem.removeEventListener("mousedown",this.mousedown,!1),this.touchCapable&&(this.showTooltip&&(this.handle1.removeEventListener("touchstart",this.showTooltip,!1),this.handle1.removeEventListener("touchmove",this.showTooltip,!1),this.handle2.removeEventListener("touchstart",this.showTooltip,!1),this.handle2.removeEventListener("touchmove",this.showTooltip,!1)),this.hideTooltip&&(this.handle1.removeEventListener("touchend",this.hideTooltip,!1),this.handle2.removeEventListener("touchend",this.hideTooltip,!1)),this.showTooltip&&(this.sliderElem.removeEventListener("touchstart",this.showTooltip,!1),this.sliderElem.removeEventListener("touchmove",this.showTooltip,!1)),this.hideTooltip&&this.sliderElem.removeEventListener("touchend",this.hideTooltip,!1),this.sliderElem.removeEventListener("touchstart",this.touchstart,!1),this.sliderElem.removeEventListener("touchmove",this.touchmove,!1)),window.removeEventListener("resize",this.resize,!1)},_bindNonQueryEventHandler:function(a,b){void 0===this.eventToCallbackMap[a]&&(this.eventToCallbackMap[a]=[]),this.eventToCallbackMap[a].push(b)},_unbindNonQueryEventHandler:function(a,b){var c=this.eventToCallbackMap[a];if(void 0!==c)for(var d=0;d<c.length;d++)if(c[d]===b){c.splice(d,1);break}},_cleanUpEventCallbacksMap:function(){for(var a=Object.keys(this.eventToCallbackMap),b=0;b<a.length;b++){var c=a[b];delete this.eventToCallbackMap[c]}},_showTooltip:function(){this.options.tooltip_split===!1?(this._addClass(this.tooltip,"in"),this.tooltip_min.style.display="none",this.tooltip_max.style.display="none"):(this._addClass(this.tooltip_min,"in"),this._addClass(this.tooltip_max,"in"),this.tooltip.style.display="none"),this._state.over=!0},_hideTooltip:function(){this._state.inDrag===!1&&this._alwaysShowTooltip!==!0&&(this._removeClass(this.tooltip,"in"),this._removeClass(this.tooltip_min,"in"),this._removeClass(this.tooltip_max,"in")),this._state.over=!1},_setToolTipOnMouseOver:function(a){function b(a,b){return b?[100-a.percentage[0],c.options.range?100-a.percentage[1]:a.percentage[1]]:[a.percentage[0],a.percentage[1]]}var c=this,d=this.options.formatter(a?a.value[0]:this._state.value[0]),e=a?b(a,this.options.reversed):b(this._state,this.options.reversed);this._setText(this.tooltipInner,d),this.tooltip.style[this.stylePos]=e[0]+"%"},_copyState:function(){return{value:[this._state.value[0],this._state.value[1]],enabled:this._state.enabled,offset:this._state.offset,size:this._state.size,percentage:[this._state.percentage[0],this._state.percentage[1],this._state.percentage[2]],inDrag:this._state.inDrag,over:this._state.over,dragged:this._state.dragged,keyCtrl:this._state.keyCtrl}},_addTickListener:function(){return{addMouseEnter:function(a,b,c){var d=function(){var d=a._copyState(),e=b===a.handle1?d.value[0]:d.value[1],f=void 0;void 0!==c?(e=a.options.ticks[c],f=a.options.ticks_positions.length>0&&a.options.ticks_positions[c]||a._toPercentage(a.options.ticks[c])):f=a._toPercentage(e),d.value[0]=e,d.percentage[0]=f,a._setToolTipOnMouseOver(d),a._showTooltip()};return b.addEventListener("mouseenter",d,!1),d},addMouseLeave:function(a,b){var c=function(){a._hideTooltip()};return b.addEventListener("mouseleave",c,!1),c}}},_layout:function(){var a,b;if(a=this.options.reversed?[100-this._state.percentage[0],this.options.range?100-this._state.percentage[1]:this._state.percentage[1]]:[this._state.percentage[0],this._state.percentage[1]],this.handle1.style[this.stylePos]=a[0]+"%",this.handle1.setAttribute("aria-valuenow",this._state.value[0]),b=this.options.formatter(this._state.value[0]),isNaN(b)?this.handle1.setAttribute("aria-valuetext",b):this.handle1.removeAttribute("aria-valuetext"),this.handle2.style[this.stylePos]=a[1]+"%",this.handle2.setAttribute("aria-valuenow",this._state.value[1]),b=this.options.formatter(this._state.value[1]),isNaN(b)?this.handle2.setAttribute("aria-valuetext",b):this.handle2.removeAttribute("aria-valuetext"),this.rangeHighlightElements.length>0&&Array.isArray(this.options.rangeHighlights)&&this.options.rangeHighlights.length>0)for(var c=0;c<this.options.rangeHighlights.length;c++){var d=this._toPercentage(this.options.rangeHighlights[c].start),e=this._toPercentage(this.options.rangeHighlights[c].end);if(this.options.reversed){var f=100-e;e=100-d,d=f}var g=this._createHighlightRange(d,e);g?"vertical"===this.options.orientation?(this.rangeHighlightElements[c].style.top=g.start+"%",this.rangeHighlightElements[c].style.height=g.size+"%"):(this.options.rtl?this.rangeHighlightElements[c].style.right=g.start+"%":this.rangeHighlightElements[c].style.left=g.start+"%",this.rangeHighlightElements[c].style.width=g.size+"%"):this.rangeHighlightElements[c].style.display="none"}if(Array.isArray(this.options.ticks)&&this.options.ticks.length>0){var h,i="vertical"===this.options.orientation?"height":"width";h="vertical"===this.options.orientation?"marginTop":this.options.rtl?"marginRight":"marginLeft";var j=this._state.size/(this.options.ticks.length-1);if(this.tickLabelContainer){var k=0;if(0===this.options.ticks_positions.length)"vertical"!==this.options.orientation&&(this.tickLabelContainer.style[h]=-j/2+"px"),k=this.tickLabelContainer.offsetHeight;else for(l=0;l<this.tickLabelContainer.childNodes.length;l++)this.tickLabelContainer.childNodes[l].offsetHeight>k&&(k=this.tickLabelContainer.childNodes[l].offsetHeight);"horizontal"===this.options.orientation&&(this.sliderElem.style.marginBottom=k+"px")}for(var l=0;l<this.options.ticks.length;l++){var m=this.options.ticks_positions[l]||this._toPercentage(this.options.ticks[l]);this.options.reversed&&(m=100-m),this.ticks[l].style[this.stylePos]=m+"%",this._removeClass(this.ticks[l],"in-selection"),this.options.range?m>=a[0]&&m<=a[1]&&this._addClass(this.ticks[l],"in-selection"):"after"===this.options.selection&&m>=a[0]?this._addClass(this.ticks[l],"in-selection"):"before"===this.options.selection&&m<=a[0]&&this._addClass(this.ticks[l],"in-selection"),this.tickLabels[l]&&(this.tickLabels[l].style[i]=j+"px","vertical"!==this.options.orientation&&void 0!==this.options.ticks_positions[l]?(this.tickLabels[l].style.position="absolute",this.tickLabels[l].style[this.stylePos]=m+"%",this.tickLabels[l].style[h]=-j/2+"px"):"vertical"===this.options.orientation&&(this.options.rtl?this.tickLabels[l].style.marginRight=this.sliderElem.offsetWidth+"px":this.tickLabels[l].style.marginLeft=this.sliderElem.offsetWidth+"px",this.tickLabelContainer.style[h]=this.sliderElem.offsetWidth/2*-1+"px"),this._removeClass(this.tickLabels[l],"label-in-selection label-is-selection"),this.options.range?m>=a[0]&&m<=a[1]&&(this._addClass(this.tickLabels[l],"label-in-selection"),(m===a[0]||a[1])&&this._addClass(this.tickLabels[l],"label-is-selection")):("after"===this.options.selection&&m>=a[0]?this._addClass(this.tickLabels[l],"label-in-selection"):"before"===this.options.selection&&m<=a[0]&&this._addClass(this.tickLabels[l],"label-in-selection"),m===a[0]&&this._addClass(this.tickLabels[l],"label-is-selection")))}}var n;if(this.options.range){n=this.options.formatter(this._state.value),this._setText(this.tooltipInner,n),this.tooltip.style[this.stylePos]=(a[1]+a[0])/2+"%";var o=this.options.formatter(this._state.value[0]);this._setText(this.tooltipInner_min,o);var p=this.options.formatter(this._state.value[1]);this._setText(this.tooltipInner_max,p),this.tooltip_min.style[this.stylePos]=a[0]+"%",this.tooltip_max.style[this.stylePos]=a[1]+"%"}else n=this.options.formatter(this._state.value[0]),this._setText(this.tooltipInner,n),this.tooltip.style[this.stylePos]=a[0]+"%";if("vertical"===this.options.orientation)this.trackLow.style.top="0",this.trackLow.style.height=Math.min(a[0],a[1])+"%",this.trackSelection.style.top=Math.min(a[0],a[1])+"%",this.trackSelection.style.height=Math.abs(a[0]-a[1])+"%",this.trackHigh.style.bottom="0",this.trackHigh.style.height=100-Math.min(a[0],a[1])-Math.abs(a[0]-a[1])+"%";else{"right"===this.stylePos?this.trackLow.style.right="0":this.trackLow.style.left="0",this.trackLow.style.width=Math.min(a[0],a[1])+"%","right"===this.stylePos?this.trackSelection.style.right=Math.min(a[0],a[1])+"%":this.trackSelection.style.left=Math.min(a[0],a[1])+"%",this.trackSelection.style.width=Math.abs(a[0]-a[1])+"%","right"===this.stylePos?this.trackHigh.style.left="0":this.trackHigh.style.right="0",this.trackHigh.style.width=100-Math.min(a[0],a[1])-Math.abs(a[0]-a[1])+"%";var q=this.tooltip_min.getBoundingClientRect(),r=this.tooltip_max.getBoundingClientRect();"bottom"===this.options.tooltip_position?q.right>r.left?(this._removeClass(this.tooltip_max,"bottom"),this._addClass(this.tooltip_max,"top"),this.tooltip_max.style.top="",this.tooltip_max.style.bottom="22px"):(this._removeClass(this.tooltip_max,"top"),this._addClass(this.tooltip_max,"bottom"),this.tooltip_max.style.top=this.tooltip_min.style.top,this.tooltip_max.style.bottom=""):q.right>r.left?(this._removeClass(this.tooltip_max,"top"),this._addClass(this.tooltip_max,"bottom"),this.tooltip_max.style.top="18px"):(this._removeClass(this.tooltip_max,"bottom"),this._addClass(this.tooltip_max,"top"),this.tooltip_max.style.top=this.tooltip_min.style.top)}},_createHighlightRange:function(a,b){return this._isHighlightRange(a,b)?a>b?{start:b,size:a-b}:{start:a,size:b-a}:null},_isHighlightRange:function(a,b){return a>=0&&100>=a&&b>=0&&100>=b?!0:!1},_resize:function(a){this._state.offset=this._offset(this.sliderElem),this._state.size=this.sliderElem[this.sizePos],this._layout()},_removeProperty:function(a,b){a.style.removeProperty?a.style.removeProperty(b):a.style.removeAttribute(b)},_mousedown:function(a){if(!this._state.enabled)return!1;a.preventDefault&&a.preventDefault(),this._state.offset=this._offset(this.sliderElem),this._state.size=this.sliderElem[this.sizePos];var b=this._getPercentage(a);if(this.options.range){var c=Math.abs(this._state.percentage[0]-b),d=Math.abs(this._state.percentage[1]-b);this._state.dragged=d>c?0:1,this._adjustPercentageForRangeSliders(b)}else this._state.dragged=0;this._state.percentage[this._state.dragged]=b,this.touchCapable&&(document.removeEventListener("touchmove",this.mousemove,!1),document.removeEventListener("touchend",this.mouseup,!1)),this.mousemove&&document.removeEventListener("mousemove",this.mousemove,!1),this.mouseup&&document.removeEventListener("mouseup",this.mouseup,!1),this.mousemove=this._mousemove.bind(this),this.mouseup=this._mouseup.bind(this),this.touchCapable&&(document.addEventListener("touchmove",this.mousemove,!1),document.addEventListener("touchend",this.mouseup,!1)),document.addEventListener("mousemove",this.mousemove,!1),document.addEventListener("mouseup",this.mouseup,!1),this._state.inDrag=!0;var e=this._calculateValue();return this._trigger("slideStart",e),this.setValue(e,!1,!0),a.returnValue=!1,this.options.focus&&this._triggerFocusOnHandle(this._state.dragged),!0},_touchstart:function(a){this._mousedown(a)},_triggerFocusOnHandle:function(a){0===a&&this.handle1.focus(),1===a&&this.handle2.focus()},_keydown:function(a,b){if(!this._state.enabled)return!1;var c;switch(b.keyCode){case 37:case 40:c=-1;break;case 39:case 38:c=1}if(c){if(this.options.natural_arrow_keys){var d="horizontal"===this.options.orientation,e="vertical"===this.options.orientation,f=this.options.rtl,g=this.options.reversed;d?f?g||(c=-c):g&&(c=-c):e&&(g||(c=-c))}var h;if(this.ticksAreValid&&this.options.lock_to_ticks){var i=void 0;i=this.options.ticks.indexOf(this._state.value[a]),-1===i&&(i=0,window.console.warn("(lock_to_ticks) _keydown: index should not be -1")),i+=c,i=Math.max(0,Math.min(this.options.ticks.length-1,i)),h=this.options.ticks[i]}else h=this._state.value[a]+c*this.options.step;var j=this._toPercentage(h);if(this._state.keyCtrl=a,this.options.range){this._adjustPercentageForRangeSliders(j);var k=this._state.keyCtrl?this._state.value[0]:h,l=this._state.keyCtrl?h:this._state.value[1];h=[Math.max(this.options.min,Math.min(this.options.max,k)),Math.max(this.options.min,Math.min(this.options.max,l))]}else h=Math.max(this.options.min,Math.min(this.options.max,h));return this._trigger("slideStart",h),this.setValue(h,!0,!0),this._trigger("slideStop",h),this._pauseEvent(b),delete this._state.keyCtrl,!1}},_pauseEvent:function(a){a.stopPropagation&&a.stopPropagation(),a.preventDefault&&a.preventDefault(),a.cancelBubble=!0,a.returnValue=!1},_mousemove:function(a){if(!this._state.enabled)return!1; +var b=this._getPercentage(a);this._adjustPercentageForRangeSliders(b),this._state.percentage[this._state.dragged]=b;var c=this._calculateValue(!0);return this.setValue(c,!0,!0),!1},_touchmove:function(a){void 0!==a.changedTouches&&a.preventDefault&&a.preventDefault()},_adjustPercentageForRangeSliders:function(a){if(this.options.range){var b=this._getNumDigitsAfterDecimalPlace(a);b=b?b-1:0;var c=this._applyToFixedAndParseFloat(a,b);0===this._state.dragged&&this._applyToFixedAndParseFloat(this._state.percentage[1],b)<c?(this._state.percentage[0]=this._state.percentage[1],this._state.dragged=1):1===this._state.dragged&&this._applyToFixedAndParseFloat(this._state.percentage[0],b)>c?(this._state.percentage[1]=this._state.percentage[0],this._state.dragged=0):0===this._state.keyCtrl&&this._toPercentage(this._state.value[1])<a?(this._state.percentage[0]=this._state.percentage[1],this._state.keyCtrl=1,this.handle2.focus()):1===this._state.keyCtrl&&this._toPercentage(this._state.value[0])>a&&(this._state.percentage[1]=this._state.percentage[0],this._state.keyCtrl=0,this.handle1.focus())}},_mouseup:function(a){if(!this._state.enabled)return!1;var b=this._getPercentage(a);this._adjustPercentageForRangeSliders(b),this._state.percentage[this._state.dragged]=b,this.touchCapable&&(document.removeEventListener("touchmove",this.mousemove,!1),document.removeEventListener("touchend",this.mouseup,!1)),document.removeEventListener("mousemove",this.mousemove,!1),document.removeEventListener("mouseup",this.mouseup,!1),this._state.inDrag=!1,this._state.over===!1&&this._hideTooltip();var c=this._calculateValue(!0);return this.setValue(c,!1,!0),this._trigger("slideStop",c),this._state.dragged=null,!1},_setValues:function(a,b){var c=0===a?0:100;this._state.percentage[a]!==c&&(b.data[a]=this._toValue(this._state.percentage[a]),b.data[a]=this._applyPrecision(b.data[a]))},_calculateValue:function(a){var b={};return this.options.range?(b.data=[this.options.min,this.options.max],this._setValues(0,b),this._setValues(1,b),a&&(b.data[0]=this._snapToClosestTick(b.data[0]),b.data[1]=this._snapToClosestTick(b.data[1]))):(b.data=this._toValue(this._state.percentage[0]),b.data=parseFloat(b.data),b.data=this._applyPrecision(b.data),a&&(b.data=this._snapToClosestTick(b.data))),b.data},_snapToClosestTick:function(a){for(var b=[a,1/0],c=0;c<this.options.ticks.length;c++){var d=Math.abs(this.options.ticks[c]-a);d<=b[1]&&(b=[this.options.ticks[c],d])}return b[1]<=this.options.ticks_snap_bounds?b[0]:a},_applyPrecision:function(a){var b=this.options.precision||this._getNumDigitsAfterDecimalPlace(this.options.step);return this._applyToFixedAndParseFloat(a,b)},_getNumDigitsAfterDecimalPlace:function(a){var b=(""+a).match(/(?:\.(\d+))?(?:[eE]([+-]?\d+))?$/);return b?Math.max(0,(b[1]?b[1].length:0)-(b[2]?+b[2]:0)):0},_applyToFixedAndParseFloat:function(a,b){var c=a.toFixed(b);return parseFloat(c)},_getPercentage:function(a){!this.touchCapable||"touchstart"!==a.type&&"touchmove"!==a.type&&"touchend"!==a.type||(a=a.changedTouches[0]);var b=a[this.mousePos],c=this._state.offset[this.stylePos],d=b-c;"right"===this.stylePos&&(d=-d);var e=d/this._state.size*100;return e=Math.round(e/this._state.percentage[2])*this._state.percentage[2],this.options.reversed&&(e=100-e),Math.max(0,Math.min(100,e))},_validateInputValue:function(a){if(isNaN(+a)){if(Array.isArray(a))return this._validateArray(a),a;throw new Error(g.formatInvalidInputErrorMsg(a))}return+a},_validateArray:function(a){for(var b=0;b<a.length;b++){var c=a[b];if("number"!=typeof c)throw new Error(g.formatInvalidInputErrorMsg(c))}},_setDataVal:function(a){this.element.setAttribute("data-value",a),this.element.setAttribute("value",a),this.element.value=a},_trigger:function(b,c){c=c||0===c?c:void 0;var d=this.eventToCallbackMap[b];if(d&&d.length)for(var e=0;e<d.length;e++){var f=d[e];f(c)}a&&this._triggerJQueryEvent(b,c)},_triggerJQueryEvent:function(a,b){var c={type:a,value:b};this.$element.trigger(c),this.$sliderElem.trigger(c)},_unbindJQueryEventHandlers:function(){this.$element.off(),this.$sliderElem.off()},_setText:function(a,b){"undefined"!=typeof a.textContent?a.textContent=b:"undefined"!=typeof a.innerText&&(a.innerText=b)},_removeClass:function(a,b){for(var c=b.split(" "),d=a.className,e=0;e<c.length;e++){var f=c[e],g=new RegExp("(?:\\s|^)"+f+"(?:\\s|$)");d=d.replace(g," ")}a.className=d.trim()},_addClass:function(a,b){for(var c=b.split(" "),d=a.className,e=0;e<c.length;e++){var f=c[e],g=new RegExp("(?:\\s|^)"+f+"(?:\\s|$)"),h=g.test(d);h||(d+=" "+f)}a.className=d.trim()},_offsetLeft:function(a){return a.getBoundingClientRect().left},_offsetRight:function(a){return a.getBoundingClientRect().right},_offsetTop:function(a){for(var b=a.offsetTop;(a=a.offsetParent)&&!isNaN(a.offsetTop);)b+=a.offsetTop,"BODY"!==a.tagName&&(b-=a.scrollTop);return b},_offset:function(a){return{left:this._offsetLeft(a),right:this._offsetRight(a),top:this._offsetTop(a)}},_css:function(b,c,d){if(a)a.style(b,c,d);else{var e=c.replace(/^-ms-/,"ms-").replace(/-([\da-z])/gi,function(a,b){return b.toUpperCase()});b.style[e]=d}},_toValue:function(a){return this.options.scale.toValue.apply(this,[a])},_toPercentage:function(a){return this.options.scale.toPercentage.apply(this,[a])},_setTooltipPosition:function(){var a=[this.tooltip,this.tooltip_min,this.tooltip_max];if("vertical"===this.options.orientation){var b;b=this.options.tooltip_position?this.options.tooltip_position:this.options.rtl?"left":"right";var c="left"===b?"right":"left";a.forEach(function(a){this._addClass(a,b),a.style[c]="100%"}.bind(this))}else"bottom"===this.options.tooltip_position?a.forEach(function(a){this._addClass(a,"bottom"),a.style.top="22px"}.bind(this)):a.forEach(function(a){this._addClass(a,"top"),a.style.top=-this.tooltip.outerHeight-14+"px"}.bind(this))},_getClosestTickIndex:function(a){for(var b=Math.abs(a-this.options.ticks[0]),c=0,d=0;d<this.options.ticks.length;++d){var e=Math.abs(a-this.options.ticks[d]);b>e&&(b=e,c=d)}return c},_setTickIndex:function(){this.ticksAreValid&&(this._state.tickIndex=[this.options.ticks.indexOf(this._state.value[0]),this.options.ticks.indexOf(this._state.value[1])])}},a&&a.fn&&(a.fn.slider?(windowIsDefined&&window.console.warn("bootstrap-slider.js - WARNING: $.fn.slider namespace is already bound. Use the $.fn.bootstrapSlider namespace instead."),f=c):(a.bridget(b,d),f=b),a.bridget(c,d),a(function(){a("input[data-provide=slider]")[f]()}))}(a),d}); \ No newline at end of file diff --git a/data/web/js/build/014-mailcow.js b/data/web/js/build/014-mailcow.js index cf3a3a72..66ff61ad 100644 --- a/data/web/js/build/014-mailcow.js +++ b/data/web/js/build/014-mailcow.js @@ -113,11 +113,17 @@ $(document).ready(function() { if ($(this).is("a")) { $(this).removeAttr("data-toggle"); $(this).removeAttr("data-target"); + $(this).removeAttr("data-action"); + $(this).click(function(event) { + event.preventDefault(); + }); } if ($(this).hasClass('btn-group')) { $(this).find('a').each(function(){ $(this).removeClass('dropdown-toggle') .removeAttr('data-toggle') + .removeAttr('data-target') + .removeAttr('data-action') .removeAttr('id') .attr("disabled", true); $(this).click(function(event) { @@ -140,14 +146,18 @@ $(document).ready(function() { $(this).find('button').each(function() { $(this).attr("disabled", true); }); + } else if ($(this).hasClass('form-group')) { + $(this).find('input').each(function() { + $(this).attr("disabled", true); + }); } else if ($(this).hasClass('btn')) { $(this).attr("disabled", true); - } else if ($(this).attr('data-provide', 'slider')) { + } else if ($(this).attr('data-provide') == 'slider') { $(this).slider("disable"); } $(this).data("toggle", "tooltip"); $(this).attr("title", lang_acl.prohibited); - $(this).tooltip(); + $(this).tooltip(); }); // disable submit after submitting form (not API driven buttons) @@ -195,4 +205,4 @@ $(document).ready(function() { }) }); }) -}); \ No newline at end of file +}); diff --git a/data/web/js/site/admin.js b/data/web/js/site/admin.js index cb124dd3..15e512ce 100644 --- a/data/web/js/site/admin.js +++ b/data/web/js/site/admin.js @@ -74,7 +74,8 @@ jQuery(function($){ "paging": {"enabled": true,"limit": 5,"size": log_pagination_size}, "state": {"enabled": true}, "filtering": {"enabled": true,"delay": 1,"position": "left","connectors": false,"placeholder": lang.filter_table}, - "sorting": {"enabled": true} + "sorting": {"enabled": true}, + "toggleSelector": "table tbody span.footable-toggle" }); } function draw_admins() { @@ -101,7 +102,8 @@ jQuery(function($){ "paging": {"enabled": true,"limit": 5,"size": log_pagination_size}, "filtering": {"enabled": false}, "state": {"enabled": true}, - "sorting": {"enabled": true} + "sorting": {"enabled": true}, + "toggleSelector": "table tbody span.footable-toggle" }); } function draw_fwd_hosts() { @@ -126,7 +128,8 @@ jQuery(function($){ }), "empty": lang.empty, "paging": {"enabled": true,"limit": 5,"size": log_pagination_size}, - "sorting": {"enabled": true} + "sorting": {"enabled": true}, + "toggleSelector": "table tbody span.footable-toggle" }); } function draw_relayhosts() { @@ -153,7 +156,8 @@ jQuery(function($){ }), "empty": lang.empty, "paging": {"enabled": true,"limit": 5,"size": log_pagination_size}, - "sorting": {"enabled": true} + "sorting": {"enabled": true}, + "toggleSelector": "table tbody span.footable-toggle" }); } function draw_transport_maps() { @@ -180,7 +184,8 @@ jQuery(function($){ }), "empty": lang.empty, "paging": {"enabled": true,"limit": 5,"size": log_pagination_size}, - "sorting": {"enabled": true} + "sorting": {"enabled": true}, + "toggleSelector": "table tbody span.footable-toggle" }); } function draw_queue() { @@ -210,6 +215,7 @@ jQuery(function($){ "empty": lang.empty, "paging": {"enabled": true,"limit": 5,"size": log_pagination_size}, "sorting": {"enabled": true}, + "toggleSelector": "table tbody span.footable-toggle", "on": { "ready.ft.table": function(e, ft){ table_admin_ready(ft, 'queuetable'); @@ -274,7 +280,7 @@ jQuery(function($){ }); } else if (table == 'adminstable') { $.each(data, function (i, item) { - if (admin_username == item.username) { + if (admin_username.toLowerCase() == item.username.toLowerCase()) { item.usr = '→ ' + item.username; } else { item.usr = item.username; diff --git a/data/web/js/site/debug.js b/data/web/js/site/debug.js index 979e21c4..10441874 100644 --- a/data/web/js/site/debug.js +++ b/data/web/js/site/debug.js @@ -482,6 +482,7 @@ jQuery(function($){ } item.symbols[key].str = str; }); + item.subject = escapeHtml(item.subject); item.symbols = Object.keys(item.symbols). map(function(key) { return item.symbols[key]; @@ -524,6 +525,11 @@ jQuery(function($){ }); } else if (table == 'autodiscover_log') { $.each(data, function (i, item) { + if (item.ua == null) { + item.ua = 'unknown'; + } else { + item.ua = escapeHtml(item.ua); + } item.ua = '<span style="font-size:small">' + item.ua + '</span>'; if (item.service == "activesync") { item.service = '<span class="label label-info">ActiveSync</span>'; @@ -532,7 +538,7 @@ jQuery(function($){ item.service = '<span class="label label-success">IMAP, SMTP, Cal-/CardDAV</span>'; } else { - item.service = '<span class="label label-danger">' + item.service + '</span>'; + item.service = '<span class="label label-danger">' + escapeHtml(item.service) + '</span>'; } }); } else if (table == 'watchdog') { @@ -558,6 +564,7 @@ jQuery(function($){ $.each(data, function (i, item) { if (item === null) { return true; } item.user = escapeHtml(item.user); + item.call = escapeHtml(item.call); item.task = '<code>' + item.task + '</code>'; item.type = '<span class="label label-' + item.type + '">' + item.type + '</span>'; }); @@ -569,6 +576,7 @@ jQuery(function($){ } else { item.message = escapeHtml(item.message); } + item.call = escapeHtml(item.call); var danger_class = ["emerg", "alert", "crit", "err"]; var warning_class = ["warning", "warn"]; var info_class = ["notice", "info", "debug"]; @@ -588,6 +596,7 @@ jQuery(function($){ } else if (item.method == 'POST') { item.method = '<span class="label label-warning">' + item.method + '</span>'; } + item.data = escapeHtml(item.data); }); } else if (table == 'rllog') { $.each(data, function (i, item) { diff --git a/data/web/js/site/index.js b/data/web/js/site/index.js index 568bbb8a..02de2068 100644 --- a/data/web/js/site/index.js +++ b/data/web/js/site/index.js @@ -1,3 +1,4 @@ $(document).ready(function() { $('nav').hide(); + localStorage.clear(); }); \ No newline at end of file diff --git a/data/web/js/site/mailbox.js b/data/web/js/site/mailbox.js index 00a815e6..5d18fc64 100644 --- a/data/web/js/site/mailbox.js +++ b/data/web/js/site/mailbox.js @@ -1,990 +1,1106 @@ -$(document).ready(function() { - acl_data = JSON.parse(acl); - FooTable.domainFilter = FooTable.Filtering.extend({ - construct: function(instance){ - this._super(instance); - var domain_list = []; - $.ajax({ - dataType: 'json', - url: '/api/v1/get/domain/all', - jsonp: false, - async: true, - error: function () { - domain_list.push('Cannot read domain list'); - }, - success: function (data) { - $.each(data, function (i, item) { - domain_list.push(item.domain_name); - }); - } - }); - this.domains = domain_list; - this.def = 'All Domains'; - this.$domain = null; - }, - $create: function(){ - this._super(); - var self = this, - $form_grp = $('<div/>', {'class': 'form-group'}) - .append($('<label/>', {'class': 'sr-only', text: 'Domain'})) - .prependTo(self.$form); - self.$domain = $('<select/>', { 'class': 'aform-control' }) - .on('change', {self: self}, self._onDomainDropdownChanged) - .append($('<option/>', {text: self.def})) - .appendTo($form_grp); - - $.each(self.domains, function(i, domain){ - self.$domain.append($('<option/>').text(domain)); - }); - }, - _onDomainDropdownChanged: function(e){ - var self = e.data.self, - selected = $(this).val(); - if (selected !== self.def){ - self.addFilter('domain', selected, ['domain']); - } else { - self.removeFilter('domain'); - } - self.filter(); - }, - draw: function(){ - this._super(); - var domain = this.find('domain'); - if (domain instanceof FooTable.Filter){ - this.$domain.val(domain.query.val()); - } else { - this.$domain.val(this.def); - } - $(this.$domain).closest("select").selectpicker(); - } - }); - // Auto-fill domain quota when adding new domain - auto_fill_quota = function(domain) { - $.get("/api/v1/get/domain/" + domain, function(data){ - var result = $.parseJSON(JSON.stringify(data)); - max_new_mailbox_quota = ( result.max_new_mailbox_quota / 1048576); - if (max_new_mailbox_quota != '0') { - $("#quotaBadge").html('max. ' + max_new_mailbox_quota + ' MiB'); - $('#addInputQuota').attr({"disabled": false, "value": "", "type": "number", "max": max_new_mailbox_quota}); - $('#addInputQuota').val(max_new_mailbox_quota); - } - else { - $("#quotaBadge").html('max. ' + max_new_mailbox_quota + ' MiB'); - $('#addInputQuota').attr({"disabled": true, "value": "", "type": "text", "value": "n/a"}); - $('#addInputQuota').val(max_new_mailbox_quota); - } - }); - } - $('#addSelectDomain').on('change', function() { - auto_fill_quota($('#addSelectDomain').val()); - }); - auto_fill_quota($('#addSelectDomain').val()); - $(".generate_password").click(function( event ) { - event.preventDefault(); - $('[data-hibp]').trigger('input'); - var random_passwd = Math.random().toString(36).slice(-8) - $(this).closest("form").find("input[name='password']").prop('type', 'text'); - $(this).closest("form").find("input[name='password2']").prop('type', 'text'); - $(this).closest("form").find("input[name='password']").val(random_passwd); - $(this).closest("form").find("input[name='password2']").val(random_passwd); - }); - $(".goto_checkbox").click(function( event ) { - $("form[data-id='add_alias'] .goto_checkbox").not(this).prop('checked', false); - if ($("form[data-id='add_alias'] .goto_checkbox:checked").length > 0) { - $('#textarea_alias_goto').prop('disabled', true); - } - else { - $("#textarea_alias_goto").removeAttr('disabled'); - } - }); - $('#addAliasModal').on('show.bs.modal', function(e) { - if ($("form[data-id='add_alias'] .goto_checkbox:checked").length > 0) { - $('#textarea_alias_goto').prop('disabled', true); - } - else { - $("#textarea_alias_goto").removeAttr('disabled'); - } - }); - // Log modal - $('#syncjobLogModal').on('show.bs.modal', function(e) { - var syncjob_id = $(e.relatedTarget).data('syncjob-id'); - $.ajax({ - url: '/inc/ajax/syncjob_logs.php', - data: { id: syncjob_id }, - dataType: 'text', - success: function(data){ - $(e.currentTarget).find('#logText').text(data); - }, - error: function(xhr, status, error) { - $(e.currentTarget).find('#logText').text(xhr.responseText); - } - }); - }); - // Log modal - $('#dnsInfoModal').on('show.bs.modal', function(e) { - var domain = $(e.relatedTarget).data('domain'); - $('.dns-modal-body').html('<center><span style="font-size:18pt;margin:50px" class="glyphicon glyphicon-refresh glyphicon-spin"></span></center>'); - $.ajax({ - url: '/inc/ajax/dns_diagnostics.php', - data: { domain: domain }, - dataType: 'text', - success: function(data){ - $('.dns-modal-body').html(data); - }, - error: function(xhr, status, error) { - $('.dns-modal-body').html(xhr.responseText); - } - }); - }); - // Sieve data modal - $('#sieveDataModal').on('show.bs.modal', function(e) { - var sieveScript = $(e.relatedTarget).data('sieve-script'); - $(e.currentTarget).find('#sieveDataText').html('<pre style="font-size:14px;line-height:1.1">' + sieveScript + '</pre>'); - }); - // Disable submit button on script change - $('#script_data').on('keyup', function() { - $('#add_filter_btns > #add_sieve_script').attr({"disabled": true}); - $('#validation_msg').html('-'); - }); - // Validate script data - $("#validate_sieve").click(function( event ) { - event.preventDefault(); - var script = $('#script_data').val(); - $.ajax({ - dataType: 'jsonp', - url: "/inc/ajax/sieve_validation.php", - type: "get", - data: { script: script }, - complete: function(data) { - var response = (data.responseText); - response_obj = JSON.parse(response); - if (response_obj.type == "success") { - $('#add_filter_btns > #add_sieve_script').attr({"disabled": false}); - } - mailcow_alert_box(response_obj.msg, response_obj.type); - }, - }); - }); - // $(document).on('DOMNodeInserted', '#prefilter_table', function () { - // $("#active-script").closest('td').css('background-color','#b0f0a0'); - // $("#inactive-script").closest('td').css('background-color','#b0f0a0'); - // }); - - - -}); -jQuery(function($){ - // http://stackoverflow.com/questions/24816/escaping-html-strings-with-jquery - var entityMap={"&":"&","<":"<",">":">",'"':""","'":"'","/":"/","`":"`","=":"="}; - function escapeHtml(n){return String(n).replace(/[&<>"'`=\/]/g,function(n){return entityMap[n]})} - // http://stackoverflow.com/questions/46155/validate-email-address-in-javascript - function validateEmail(email) { - var re = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; - return re.test(email); - } - function humanFileSize(i){if(Math.abs(i)<1024)return i+" B";var B=["KiB","MiB","GiB","TiB","PiB","EiB","ZiB","YiB"],e=-1;do{i/=1024,++e}while(Math.abs(i)>=1024&&e<B.length-1);return i.toFixed(1)+" "+B[e]} - $(".refresh_table").on('click', function(e) { - e.preventDefault(); - var table_name = $(this).data('table'); - $('#' + table_name).find("tr.footable-empty").remove(); - draw_table = $(this).data('draw'); - eval(draw_table + '()'); - }); - function table_mailbox_ready(ft, name) { - if(is_dual) { - $('.login_as').data("toggle", "tooltip") - .attr("disabled", true) - .removeAttr("href") - .attr("title", "Dual login cannot be used twice") - .tooltip(); - } - heading = ft.$el.parents('.tab-pane').find('.panel-heading') - var ft_paging = ft.use(FooTable.Paging) - $(heading).children('.table-lines').text(function(){ - return ft_paging.totalRows; - }) - } - function draw_domain_table() { - ft_domain_table = FooTable.init('#domain_table', { - "columns": [ - {"name":"chkbox","title":"","style":{"maxWidth":"60px","width":"60px"},"filterable": false,"sortable": false,"type":"html"}, - {"sorted": true,"name":"domain_name","title":lang.domain,"style":{"width":"250px"}}, - {"name":"aliases","title":lang.aliases,"breakpoints":"xs sm"}, - {"name":"mailboxes","title":lang.mailboxes}, - {"name":"quota","style":{"whiteSpace":"nowrap"},"title":lang.domain_quota,"formatter": function(value){ - res = value.split("/"); - return humanFileSize(res[0]) + " / " + humanFileSize(res[1]); - }, - "sortValue": function(value){ - res = value.split("/"); - return Number(res[0]); - }, - }, - {"name":"max_quota_for_mbox","title":lang.mailbox_quota,"breakpoints":"xs sm","style":{"width":"125px"}}, - {"name":"rl","title":"RL","breakpoints":"xs sm md","style":{"maxWidth":"100px","width":"100px"}}, - {"name":"backupmx","filterable": false,"style":{"maxWidth":"120px","width":"120px"},"title":lang.backup_mx,"breakpoints":"xs sm md"}, - {"name":"active","filterable": false,"style":{"maxWidth":"80px","width":"80px"},"title":lang.active}, - {"name":"action","filterable": false,"sortable": false,"style":{"text-align":"right","maxWidth":"240px","width":"240px"},"type":"html","title":lang.action,"breakpoints":"xs sm md"} - ], - "rows": $.ajax({ - dataType: 'json', - url: '/api/v1/get/domain/all', - jsonp: false, - error: function (data) { - console.log('Cannot draw domain table'); - }, - success: function (data) { - $.each(data, function (i, item) { - item.aliases = item.aliases_in_domain + " / " + item.max_num_aliases_for_domain; - item.mailboxes = item.mboxes_in_domain + " / " + item.max_num_mboxes_for_domain; - item.quota = item.quota_used_in_domain + "/" + item.max_quota_for_domain; - if (!item.rl) { - item.rl = '∞'; - } else { - item.rl = $.map(item.rl, function(e){ - return e; - }).join('/1'); - } - item.max_quota_for_mbox = humanFileSize(item.max_quota_for_mbox); - item.chkbox = '<input type="checkbox" data-id="domain" name="multi_select" value="' + encodeURIComponent(item.domain_name) + '" />'; - item.action = '<div class="btn-group">'; - if (role == "admin") { - item.action += '<a href="/edit/domain/' + encodeURIComponent(item.domain_name) + '" class="btn btn-xs btn-default"><span class="glyphicon glyphicon-pencil"></span> ' + lang.edit + '</a>' + - '<a href="#" data-action="delete_selected" data-id="single-domain" data-api-url="delete/domain" data-item="' + encodeURIComponent(item.domain_name) + '" class="btn btn-xs btn-danger"><span class="glyphicon glyphicon-trash"></span> ' + lang.remove + '</a>'; - } - else { - item.action += '<a href="/edit/domain/' + encodeURIComponent(item.domain_name) + '" class="btn btn-xs btn-default"><span class="glyphicon glyphicon-pencil"></span> ' + lang.edit + '</a>'; - } - item.action += '<a href="#dnsInfoModal" class="btn btn-xs btn-info" data-toggle="modal" data-domain="' + encodeURIComponent(item.domain_name) + '"><span class="glyphicon glyphicon-question-sign"></span> DNS</a></div>'; - }); - } - }), - "empty": lang.empty, - "paging": { - "enabled": true, - "limit": 5, - "size": pagination_size - }, - "state": { - "enabled": true - }, - "filtering": { - "enabled": true, - "delay": 100, - "position": "left", - "connectors": false, - "placeholder": lang.filter_table - }, - "sorting": { - "enabled": true - }, - "on": { - "ready.ft.table": function(e, ft){ - table_mailbox_ready(ft, 'domain_table'); - } - } - }); - } - function draw_mailbox_table() { - ft_mailbox_table = FooTable.init('#mailbox_table', { - "columns": [ - {"name":"chkbox","title":"","style":{"maxWidth":"60px","width":"60px"},"filterable": false,"sortable": false,"type":"html"}, - {"sorted": true,"name":"username","style":{"word-break":"break-all","min-width":"120px"},"title":lang.username}, - {"name":"name","title":lang.fname,"style":{"word-break":"break-all","min-width":"120px"},"breakpoints":"xs sm"}, - {"name":"domain","title":lang.domain,"breakpoints":"xs sm"}, - {"name":"quota","style":{"whiteSpace":"nowrap"},"title":lang.domain_quota,"formatter": function(value){ - res = value.split("/"); - return humanFileSize(res[0]) + " / " + humanFileSize(res[1]); - }, - "sortValue": function(value){ - res = value.split("/"); - return Number(res[0]); - }, - }, - {"name":"spam_aliases","filterable": false,"title":lang.spam_aliases,"breakpoints":"xs sm md"}, - {"name":"tls_enforce_in","filterable": false,"title":lang.tls_enforce_in,"breakpoints":"all"}, - {"name":"tls_enforce_out","filterable": false,"title":lang.tls_enforce_out,"breakpoints":"all"}, - {"name":"quarantine_notification","filterable": false,"title":lang.quarantine_notification,"breakpoints":"all"}, - {"name":"in_use","filterable": false,"type":"html","title":lang.in_use,"sortValue": function(value){ - return Number($(value).find(".progress-bar").attr('aria-valuenow')); - }, - }, - {"name":"messages","filterable": false,"title":lang.msg_num,"breakpoints":"xs sm md"}, - {"name":"rl","title":"RL","breakpoints":"xs sm md","style":{"width":"125px"}}, - {"name":"active","filterable": false,"title":lang.active}, - {"name":"action","filterable": false,"sortable": false,"style":{"min-width":"250px","text-align":"right"},"type":"html","title":lang.action,"breakpoints":"xs sm md"} - ], - "empty": lang.empty, - "rows": $.ajax({ - dataType: 'json', - url: '/api/v1/get/mailbox/all', - jsonp: false, - error: function () { - console.log('Cannot draw mailbox table'); - }, - success: function (data) { - $.each(data, function (i, item) { - item.quota = item.quota_used + "/" + item.quota; - item.max_quota_for_mbox = humanFileSize(item.max_quota_for_mbox); - if (!item.rl) { - item.rl = '∞'; - } else { - item.rl = $.map(item.rl, function(e){ - return e; - }).join('/1'); - } - item.chkbox = '<input type="checkbox" data-id="mailbox" name="multi_select" value="' + encodeURIComponent(item.username) + '" />'; - item.tls_enforce_in = '<span class="text-' + (item.attributes.tls_enforce_in == 1 ? 'success' : 'danger') + ' glyphicon glyphicon-lock"></span>'; - item.tls_enforce_out = '<span class="text-' + (item.attributes.tls_enforce_out == 1 ? 'success' : 'danger') + ' glyphicon glyphicon-lock"></span>'; - if (item.attributes.quarantine_notification === 'never') { - item.quarantine_notification = lang.never; - } else if (item.attributes.quarantine_notification === 'hourly') { - item.quarantine_notification = lang.hourly; - } else if (item.attributes.quarantine_notification === 'daily') { - item.quarantine_notification = lang.daily; - } else if (item.attributes.quarantine_notification === 'weekly') { - item.quarantine_notification = lang.weekly; - } - if (acl_data.login_as === 1) { - item.action = '<div class="btn-group">' + - '<a href="/edit/mailbox/' + encodeURIComponent(item.username) + '" class="btn btn-xs btn-default"><span class="glyphicon glyphicon-pencil"></span> ' + lang.edit + '</a>' + - '<a href="#" data-action="delete_selected" data-id="single-mailbox" data-api-url="delete/mailbox" data-item="' + encodeURIComponent(item.username) + '" class="btn btn-xs btn-danger"><span class="glyphicon glyphicon-trash"></span> ' + lang.remove + '</a>' + - '<a href="/index.php?duallogin=' + encodeURIComponent(item.username) + '" class="login_as btn btn-xs btn-success"><span class="glyphicon glyphicon-user"></span> Login</a>' + - '</div>'; - } - else { - item.action = '<div class="btn-group">' + - '<a href="/edit/mailbox/' + encodeURIComponent(item.username) + '" class="btn btn-xs btn-default"><span class="glyphicon glyphicon-pencil"></span> ' + lang.edit + '</a>' + - '<a href="#" data-action="delete_selected" data-id="single-mailbox" data-api-url="delete/mailbox" data-item="' + encodeURIComponent(item.username) + '" class="btn btn-xs btn-danger"><span class="glyphicon glyphicon-trash"></span> ' + lang.remove + '</a>' + - '</div>'; - } - item.in_use = '<div class="progress">' + - '<div class="progress-bar progress-bar-' + item.percent_class + ' role="progressbar" aria-valuenow="' + item.percent_in_use + '" aria-valuemin="0" aria-valuemax="100" ' + - 'style="min-width:2em;width:' + item.percent_in_use + '%">' + item.percent_in_use + '%' + '</div></div>'; - item.username = escapeHtml(item.username); - }); - } - }), - "paging": { - "enabled": true, - "limit": 5, - "size": pagination_size - }, - "state": { - "enabled": true - }, - "filtering": { - "enabled": true, - "delay": 100, - "position": "left", - "connectors": false, - //"container": "#tab-mailboxes.panel", - "placeholder": lang.filter_table - }, - "components": { - "filtering": FooTable.domainFilter - }, - "sorting": { - "enabled": true - }, - "on": { - "ready.ft.table": function(e, ft){ - table_mailbox_ready(ft, 'mailbox_table'); - } - } - }); - } - function draw_resource_table() { - ft_resource_table = FooTable.init('#resource_table', { - "columns": [ - {"name":"chkbox","title":"","style":{"maxWidth":"60px","width":"60px"},"filterable": false,"sortable": false,"type":"html"}, - {"sorted": true,"name":"description","title":lang.description,"style":{"width":"250px"}}, - {"name":"kind","title":lang.kind}, - {"name":"domain","title":lang.domain,"breakpoints":"xs sm"}, - {"name":"multiple_bookings","filterable": false,"style":{"maxWidth":"150px","width":"140px"},"title":lang.multiple_bookings,"breakpoints":"xs sm"}, - {"name":"active","filterable": false,"style":{"maxWidth":"80px","width":"80px"},"title":lang.active}, - {"name":"action","filterable": false,"sortable": false,"style":{"text-align":"right","maxWidth":"180px","width":"180px"},"type":"html","title":lang.action,"breakpoints":"xs sm"} - ], - "empty": lang.empty, - "rows": $.ajax({ - dataType: 'json', - url: '/api/v1/get/resource/all', - jsonp: false, - error: function () { - console.log('Cannot draw resource table'); - }, - success: function (data) { - $.each(data, function (i, item) { - if (item.multiple_bookings == '0') { - item.multiple_bookings = '<span id="active-script" class="label label-success">' + lang.booking_0_short + '</span>'; - } else if (item.multiple_bookings == '-1') { - item.multiple_bookings = '<span id="active-script" class="label label-warning">' + lang.booking_lt0_short + '</span>'; - } else { - item.multiple_bookings = '<span id="active-script" class="label label-danger">' + lang.booking_custom_short + ' (' + item.multiple_bookings + ')</span>'; - } - item.action = '<div class="btn-group">' + - '<a href="/edit/resource/' + encodeURIComponent(item.name) + '" class="btn btn-xs btn-default"><span class="glyphicon glyphicon-pencil"></span> ' + lang.edit + '</a>' + - '<a href="#" data-action="delete_selected" data-id="single-resource" data-api-url="delete/resource" data-item="' + item.name + '" class="btn btn-xs btn-danger"><span class="glyphicon glyphicon-trash"></span> ' + lang.remove + '</a>' + - '</div>'; - item.chkbox = '<input type="checkbox" data-id="resource" name="multi_select" value="' + encodeURIComponent(item.name) + '" />'; - item.name = escapeHtml(item.name); - }); - } - }), - "paging": { - "enabled": true, - "limit": 5, - "size": pagination_size - }, - "state": { - "enabled": true - }, - "filtering": { - "enabled": true, - "delay": 100, - "position": "left", - "connectors": false, - "placeholder": lang.filter_table - }, - "components": { - "filtering": FooTable.domainFilter - }, - "sorting": { - "enabled": true - }, - "on": { - "ready.ft.table": function(e, ft){ - table_mailbox_ready(ft, 'resource_table'); - } - } - }); - } - function draw_bcc_table() { - ft_bcc_table = FooTable.init('#bcc_table', { - "columns": [ - {"name":"chkbox","title":"","style":{"maxWidth":"60px","width":"60px"},"filterable": false,"sortable": false,"type":"html"}, - {"sorted": true,"name":"id","title":"ID","style":{"maxWidth":"60px","width":"60px","text-align":"center"}}, - {"name":"type","title":lang.bcc_type}, - {"name":"local_dest","title":lang.bcc_local_dest}, - {"name":"bcc_dest","title":lang.bcc_destinations}, - {"name":"domain","title":lang.domain,"breakpoints":"xs sm"}, - {"name":"active","filterable": false,"style":{"maxWidth":"80px","width":"80px"},"title":lang.active}, - {"name":"action","filterable": false,"sortable": false,"style":{"text-align":"right","maxWidth":"180px","width":"180px"},"type":"html","title":lang.action,"breakpoints":"xs sm"} - ], - "empty": lang.empty, - "rows": $.ajax({ - dataType: 'json', - url: '/api/v1/get/bcc/all', - jsonp: false, - error: function () { - console.log('Cannot draw bcc table'); - }, - success: function (data) { - $.each(data, function (i, item) { - item.action = '<div class="btn-group">' + - '<a href="/edit/bcc/' + item.id + '" class="btn btn-xs btn-default"><span class="glyphicon glyphicon-pencil"></span> ' + lang.edit + '</a>' + - '<a href="#" data-action="delete_selected" data-id="single-bcc" data-api-url="delete/bcc" data-item="' + item.id + '" class="btn btn-xs btn-danger"><span class="glyphicon glyphicon-trash"></span> ' + lang.remove + '</a>' + - '</div>'; - item.chkbox = '<input type="checkbox" data-id="bcc" name="multi_select" value="' + item.id + '" />'; - item.local_dest = escapeHtml(item.local_dest); - item.bcc_dest = escapeHtml(item.bcc_dest); - if (item.type == 'sender') { - item.type = '<span id="active-script" class="label label-success">Sender</span>'; - } else { - item.type = '<span id="inactive-script" class="label label-warning">Recipient</span>'; - } - }); - } - }), - "paging": { - "enabled": true, - "limit": 5, - "size": pagination_size - }, - "state": { - "enabled": true - }, - "filtering": { - "enabled": true, - "delay": 100, - "position": "left", - "connectors": false, - "placeholder": lang.filter_table - }, - "sorting": { - "enabled": true - }, - "on": { - "ready.ft.table": function(e, ft){ - table_mailbox_ready(ft, 'bcc_table'); - } - } - }); - } - function draw_recipient_map_table() { - ft_recipient_map_table = FooTable.init('#recipient_map_table', { - "columns": [ - {"name":"chkbox","title":"","style":{"maxWidth":"60px","width":"60px"},"filterable": false,"sortable": false,"type":"html"}, - {"sorted": true,"name":"id","title":"ID","style":{"maxWidth":"60px","width":"60px","text-align":"center"}}, - {"name":"recipient_map_old","title":lang.recipient_map_old}, - {"name":"recipient_map_new","title":lang.recipient_map_new}, - {"name":"active","filterable": false,"style":{"maxWidth":"80px","width":"80px"},"title":lang.active}, - {"name":"action","filterable": false,"sortable": false,"style":{"text-align":"right","maxWidth":"180px","width":"180px"},"type":"html","title":(role == "admin" ? lang.action : ""),"breakpoints":"xs sm"} - ], - "empty": lang.empty, - "rows": $.ajax({ - dataType: 'json', - url: '/api/v1/get/recipient_map/all', - jsonp: false, - error: function () { - console.log('Cannot draw recipient map table'); - }, - success: function (data) { - if (role == "admin") { - $.each(data, function (i, item) { - item.recipient_map_old = escapeHtml(item.recipient_map_old); - item.recipient_map_new = escapeHtml(item.recipient_map_new); - item.action = '<div class="btn-group">' + - '<a href="/edit/recipient_map/' + item.id + '" class="btn btn-xs btn-default"><span class="glyphicon glyphicon-pencil"></span> ' + lang.edit + '</a>' + - '<a href="#" data-action="delete_selected" data-id="single-recipient_map" data-api-url="delete/recipient_map" data-item="' + item.id + '" class="btn btn-xs btn-danger"><span class="glyphicon glyphicon-trash"></span> ' + lang.remove + '</a>' + - '</div>'; - item.chkbox = '<input type="checkbox" data-id="recipient_map" name="multi_select" value="' + item.id + '" />'; - }); - } - } - }), - "paging": { - "enabled": true, - "limit": 5, - "size": pagination_size - }, - "state": { - "enabled": true - }, - "filtering": { - "enabled": true, - "delay": 100, - "position": "left", - "connectors": false, - "placeholder": lang.filter_table - }, - "sorting": { - "enabled": true - }, - "on": { - "ready.ft.table": function(e, ft){ - table_mailbox_ready(ft, 'recipient_map_table'); - } - } - }); - } - function draw_tls_policy_table() { - ft_tls_policy_table = FooTable.init('#tls_policy_table', { - "columns": [ - {"name":"chkbox","title":"","style":{"maxWidth":"60px","width":"60px"},"filterable": false,"sortable": false,"type":"html"}, - {"sorted": true,"name":"id","title":"ID","style":{"maxWidth":"60px","width":"60px","text-align":"center"}}, - {"name":"dest","title":lang.tls_map_dest}, - {"name":"policy","title":lang.tls_map_policy}, - {"name":"parameters","title":lang.tls_map_parameters}, - {"name":"active","filterable": false,"style":{"maxWidth":"80px","width":"80px"},"title":lang.active}, - {"name":"action","filterable": false,"sortable": false,"style":{"text-align":"right","maxWidth":"180px","width":"180px"},"type":"html","title":(role == "admin" ? lang.action : ""),"breakpoints":"xs sm"} - ], - "empty": lang.empty, - "rows": $.ajax({ - dataType: 'json', - url: '/api/v1/get/tls-policy-map/all', - jsonp: false, - error: function () { - console.log('Cannot draw tls policy map table'); - }, - success: function (data) { - if (role == "admin") { - $.each(data, function (i, item) { - item.dest = escapeHtml(item.dest); - item.policy = '<b>' + escapeHtml(item.policy) + '</b>'; - if (item.parameters == '') { - item.parameters = '<code>-</code>'; - } else { - item.parameters = '<code>' + escapeHtml(item.parameters) + '</code>'; - } - item.action = '<div class="btn-group">' + - '<a href="/edit/tls_policy_map/' + item.id + '" class="btn btn-xs btn-default"><span class="glyphicon glyphicon-pencil"></span> ' + lang.edit + '</a>' + - '<a href="#" data-action="delete_selected" data-id="single-tls-policy-map" data-api-url="delete/tls-policy-map" data-item="' + item.id + '" class="btn btn-xs btn-danger"><span class="glyphicon glyphicon-trash"></span> ' + lang.remove + '</a>' + - '</div>'; - item.chkbox = '<input type="checkbox" data-id="tls-policy-map" name="multi_select" value="' + item.id + '" />'; - }); - } - } - }), - "paging": { - "enabled": true, - "limit": 5, - "size": pagination_size - }, - "state": { - "enabled": true - }, - "filtering": { - "enabled": true, - "delay": 100, - "position": "left", - "connectors": false, - "placeholder": lang.filter_table - }, - "sorting": { - "enabled": true - }, - "on": { - "ready.ft.table": function(e, ft){ - table_mailbox_ready(ft, 'tls_policy_table'); - } - } - }); - } - function draw_transport_maps_table() { - ft_transport_maps_table = FooTable.init('#transport_maps_table', { - "columns": [ - {"name":"chkbox","title":"","style":{"maxWidth":"60px","width":"60px"},"filterable": false,"sortable": false,"type":"html"}, - {"sorted": true,"name":"id","title":"ID","style":{"maxWidth":"60px","width":"60px","text-align":"center"}}, - {"name":"dest","title":lang.tls_map_dest}, - {"name":"parameters","title":lang.tls_map_parameters}, - {"name":"active","filterable": false,"style":{"maxWidth":"80px","width":"80px"},"title":lang.active}, - {"name":"action","filterable": false,"sortable": false,"style":{"text-align":"right","maxWidth":"180px","width":"180px"},"type":"html","title":(role == "admin" ? lang.action : ""),"breakpoints":"xs sm"} - ], - "empty": lang.empty, - "rows": $.ajax({ - dataType: 'json', - url: '/api/v1/get/transport-map/all', - jsonp: false, - error: function () { - console.log('Cannot draw transport map table'); - }, - success: function (data) { - if (role == "admin") { - $.each(data, function (i, item) { - item.dest = escapeHtml(item.dest); - if (item.parameters == '') { - item.parameters = '<code>-</code>'; - } else { - item.parameters = '<code>' + escapeHtml(item.parameters) + '</code>'; - } - item.action = '<div class="btn-group">' + - '<a href="/edit/transport_map/' + item.id + '" class="btn btn-xs btn-default"><span class="glyphicon glyphicon-pencil"></span> ' + lang.edit + '</a>' + - '<a href="#" data-action="delete_selected" data-id="single-transport-map" data-api-url="delete/transport-map" data-item="' + item.id + '" class="btn btn-xs btn-danger"><span class="glyphicon glyphicon-trash"></span> ' + lang.remove + '</a>' + - '</div>'; - item.chkbox = '<input type="checkbox" data-id="transport-map" name="multi_select" value="' + item.id + '" />'; - }); - } - } - }), - "paging": { - "enabled": true, - "limit": 5, - "size": pagination_size - }, - "state": { - "enabled": true - }, - "filtering": { - "enabled": true, - "delay": 100, - "position": "left", - "connectors": false, - "placeholder": lang.filter_table - }, - "sorting": { - "enabled": true - }, - "on": { - "ready.ft.table": function(e, ft){ - table_mailbox_ready(ft, 'transport_maps_table'); - } - } - }); - } - function draw_alias_table() { - ft_alias_table = FooTable.init('#alias_table', { - "columns": [ - {"name":"chkbox","title":"","style":{"maxWidth":"60px","width":"60px"},"filterable": false,"sortable": false,"type":"html"}, - {"name":"id","title":"ID","style":{"maxWidth":"60px","width":"60px","text-align":"center"}}, - {"sorted": true,"name":"address","title":lang.alias,"style":{"width":"250px"}}, - {"name":"goto","title":lang.target_address}, - {"name":"domain","title":lang.domain,"breakpoints":"xs sm"}, - {"name":"public_comment","title":lang.public_comment,"breakpoints":"all"}, - {"name":"private_comment","title":lang.private_comment,"breakpoints":"all"}, - {"name":"active","filterable": false,"style":{"maxWidth":"50px","width":"70px"},"title":lang.active}, - {"name":"action","filterable": false,"sortable": false,"style":{"text-align":"right","maxWidth":"180px","width":"180px"},"type":"html","title":lang.action,"breakpoints":"xs sm"} - ], - "empty": lang.empty, - "rows": $.ajax({ - dataType: 'json', - url: '/api/v1/get/alias/all', - jsonp: false, - error: function () { - console.log('Cannot draw alias table'); - }, - success: function (data) { - $.each(data, function (i, item) { - item.action = '<div class="btn-group">' + - '<a href="/edit/alias/' + encodeURIComponent(item.id) + '" class="btn btn-xs btn-default"><span class="glyphicon glyphicon-pencil"></span> ' + lang.edit + '</a>' + - '<a href="#" data-action="delete_selected" data-id="single-alias" data-api-url="delete/alias" data-item="' + encodeURIComponent(item.id) + '" class="btn btn-xs btn-danger"><span class="glyphicon glyphicon-trash"></span> ' + lang.remove + '</a>' + - '</div>'; - item.chkbox = '<input type="checkbox" data-id="alias" name="multi_select" value="' + encodeURIComponent(item.id) + '" />'; - item.goto = escapeHtml(item.goto.replace(/,/g, " ")); - item.public_comment = escapeHtml(item.public_comment); - item.private_comment = escapeHtml(item.private_comment); - if (item.is_catch_all == 1) { - item.address = '<div class="label label-default">Catch-All</div> ' + escapeHtml(item.address); - } - else { - item.address = escapeHtml(item.address); - } - if (item.goto == "null@localhost") { - item.goto = '⤷ <span style="font-size:12px" class="glyphicon glyphicon-trash" aria-hidden="true"></span>'; - } - else if (item.goto == "spam@localhost") { - item.goto = '<span class="label label-danger">Learn as spam</span>'; - } - else if (item.goto == "ham@localhost") { - item.goto = '<span class="label label-success">Learn as ham</span>'; - } - if (item.in_primary_domain !== "") { - item.domain = "↳ " + item.domain + " (" + item.in_primary_domain + ")"; - } - }); - } - }), - "paging": { - "enabled": true, - "limit": 5, - "size": pagination_size - }, - "state": { - "enabled": true - }, - "filtering": { - "enabled": true, - "delay": 100, - "position": "left", - "connectors": false, - "placeholder": lang.filter_table - }, - "components": { - "filtering": FooTable.domainFilter - }, - "sorting": { - "enabled": true - }, - "on": { - "ready.ft.table": function(e, ft){ - table_mailbox_ready(ft, 'alias_table'); - } - } - }); - } - - function draw_aliasdomain_table() { - ft_aliasdomain_table = FooTable.init('#aliasdomain_table', { - "columns": [ - {"name":"chkbox","title":"","style":{"maxWidth":"60px","width":"60px"},"filterable": false,"sortable": false,"type":"html"}, - {"sorted": true,"name":"alias_domain","title":lang.alias,"style":{"width":"250px"}}, - {"name":"target_domain","title":lang.target_domain}, - {"name":"active","filterable": false,"style":{"maxWidth":"50px","width":"70px"},"title":lang.active}, - {"name":"action","filterable": false,"sortable": false,"style":{"text-align":"right","maxWidth":"250px","width":"250px"},"type":"html","title":lang.action,"breakpoints":"xs sm"} - ], - "empty": lang.empty, - "rows": $.ajax({ - dataType: 'json', - url: '/api/v1/get/alias-domain/all', - jsonp: false, - error: function () { - console.log('Cannot draw alias domain table'); - }, - success: function (data) { - $.each(data, function (i, item) { - item.action = '<div class="btn-group">' + - '<a href="/edit/aliasdomain/' + encodeURIComponent(item.alias_domain) + '" class="btn btn-xs btn-default"><span class="glyphicon glyphicon-pencil"></span> ' + lang.edit + '</a>' + - '<a href="#" data-action="delete_selected" data-id="single-alias-domain" data-api-url="delete/alias-domain" data-item="' + encodeURIComponent(item.alias_domain) + '" class="btn btn-xs btn-danger"><span class="glyphicon glyphicon-trash"></span> ' + lang.remove + '</a>' + - '<a href="#dnsInfoModal" class="btn btn-xs btn-info" data-toggle="modal" data-domain="' + encodeURIComponent(item.alias_domain) + '"><span class="glyphicon glyphicon-question-sign"></span> DNS</a></div>' + - '</div>'; - item.chkbox = '<input type="checkbox" data-id="alias-domain" name="multi_select" value="' + encodeURIComponent(item.alias_domain) + '" />'; - }); - } - }), - "paging": { - "enabled": true, - "limit": 5, - "size": pagination_size - }, - "state": { - "enabled": true - }, - "filtering": { - "enabled": true, - "delay": 100, - "position": "left", - "connectors": false, - "placeholder": lang.filter_table - }, - "sorting": { - "enabled": true - }, - "on": { - "ready.ft.table": function(e, ft){ - table_mailbox_ready(ft, 'aliasdomain_table'); - } - } - }); - } - - function draw_sync_job_table() { - ft_syncjob_table = FooTable.init('#sync_job_table', { - "columns": [ - {"name":"chkbox","title":"","style":{"maxWidth":"60px","width":"60px","text-align":"center"},"filterable": false,"sortable": false,"type":"html"}, - {"sorted": true,"name":"id","title":"ID","style":{"maxWidth":"60px","width":"60px","text-align":"center"}}, - {"name":"user2","title":lang.owner}, - {"name":"server_w_port","title":"Server","breakpoints":"xs","style":{"word-break":"break-all"}}, - {"name":"exclude","title":lang.excludes,"breakpoints":"all"}, - {"name":"mins_interval","title":lang.mins_interval,"breakpoints":"all"}, - {"name":"last_run","title":lang.last_run,"breakpoints":"sm"}, - {"name":"log","title":"Log"}, - {"name":"active","filterable": false,"style":{"maxWidth":"70px","width":"70px"},"title":lang.active}, - {"name":"is_running","filterable": false,"style":{"maxWidth":"120px","width":"100px"},"title":lang.status}, - {"name":"action","filterable": false,"sortable": false,"style":{"text-align":"right","maxWidth":"180px","width":"180px"},"type":"html","title":lang.action,"breakpoints":"xs sm"} - ], - "empty": lang.empty, - "rows": $.ajax({ - dataType: 'json', - url: '/api/v1/get/syncjobs/all/no_log', - jsonp: false, - error: function () { - console.log('Cannot draw sync job table'); - }, - success: function (data) { - $.each(data, function (i, item) { - item.log = '<a href="#syncjobLogModal" data-toggle="modal" data-syncjob-id="' + encodeURIComponent(item.id) + '">Open logs</a>' - item.user2 = escapeHtml(item.user2); - if (!item.exclude > 0) { - item.exclude = '-'; - } else { - item.exclude = '<code>' + item.exclude + '</code>'; - } - item.server_w_port = escapeHtml(item.user1) + '@' + item.host1 + ':' + item.port1; - item.action = '<div class="btn-group">' + - '<a href="/edit/syncjob/' + item.id + '" class="btn btn-xs btn-default"><span class="glyphicon glyphicon-pencil"></span> ' + lang.edit + '</a>' + - '<a href="#" data-action="delete_selected" data-id="single-syncjob" data-api-url="delete/syncjob" data-item="' + item.id + '" class="btn btn-xs btn-danger"><span class="glyphicon glyphicon-trash"></span> ' + lang.remove + '</a>' + - '</div>'; - item.chkbox = '<input type="checkbox" data-id="syncjob" name="multi_select" value="' + item.id + '" />'; - if (item.is_running == 1) { - item.is_running = '<span id="active-script" class="label label-success">' + lang.running + '</span>'; - } else { - item.is_running = '<span id="inactive-script" class="label label-warning">' + lang.waiting + '</span>'; - } - if (!item.last_run > 0) { - item.last_run = lang.waiting; - } - }); - } - }), - "paging": { - "enabled": true, - "limit": 5, - "size": pagination_size - }, - "state": { - "enabled": true - }, - "filtering": { - "enabled": true, - "delay": 100, - "position": "left", - "connectors": false, - "placeholder": lang.filter_table - }, - "sorting": { - "enabled": true - }, - "on": { - "ready.ft.table": function(e, ft){ - table_mailbox_ready(ft, 'sync_job_table'); - } - } - }); - } - - function draw_filter_table() { - ft_filter_table = FooTable.init('#filter_table', { - "columns": [ - {"name":"chkbox","title":"","style":{"maxWidth":"60px","width":"60px","text-align":"center"},"filterable": false,"sortable": false,"type":"html"}, - {"name":"id","title":"ID","style":{"maxWidth":"60px","width":"60px","text-align":"center"}}, - {"name":"active","style":{"maxWidth":"80px","width":"80px"},"title":lang.active}, - {"name":"filter_type","style":{"maxWidth":"80px","width":"80px"},"title":"Type"}, - {"sorted": true,"name":"username","title":lang.owner,"style":{"maxWidth":"550px","width":"350px"}}, - {"name":"script_desc","title":lang.description,"breakpoints":"xs"}, - {"name":"script_data","title":"Script","breakpoints":"all"}, - {"name":"action","filterable": false,"sortable": false,"style":{"text-align":"right","maxWidth":"180px","width":"180px"},"type":"html","title":lang.action,"breakpoints":"xs sm"} - ], - "empty": lang.empty, - "rows": $.ajax({ - dataType: 'json', - url: '/api/v1/get/filters/all', - jsonp: false, - error: function () { - console.log('Cannot draw filter table'); - }, - success: function (data) { - $.each(data, function (i, item) { - if (item.active_int == 1) { - item.active = '<span id="active-script" class="label label-success">' + lang.active + '</span>'; - } else { - item.active = '<span id="inactive-script" class="label label-warning">' + lang.inactive + '</span>'; - } - item.script_data = '<pre style="margin:0px">' + escapeHtml(item.script_data) + '</pre>' - item.filter_type = '<div class="label label-default">' + item.filter_type.charAt(0).toUpperCase() + item.filter_type.slice(1).toLowerCase() + '</div>' - item.action = '<div class="btn-group">' + - '<a href="/edit/filter/' + item.id + '" class="btn btn-xs btn-default"><span class="glyphicon glyphicon-pencil"></span> ' + lang.edit + '</a>' + - '<a href="#" data-action="delete_selected" data-id="single-filter" data-api-url="delete/filter" data-item="' + encodeURIComponent(item.id) + '" class="btn btn-xs btn-danger"><span class="glyphicon glyphicon-trash"></span> ' + lang.remove + '</a>' + - '</div>'; - item.chkbox = '<input type="checkbox" data-id="filter_item" name="multi_select" value="' + item.id + '" />' - }); - } - }), - "paging": { - "enabled": true, - "limit": 5, - "size": pagination_size - }, - "state": { - "enabled": true - }, - "filtering": { - "enabled": true, - "delay": 100, - "position": "left", - "connectors": false, - "placeholder": lang.filter_table - }, - "sorting": { - "enabled": true - }, - "on": { - "ready.ft.table": function(e, ft){ - table_mailbox_ready(ft, 'filter_table'); - } - } - }); - }; - - draw_domain_table(); - draw_mailbox_table(); - draw_resource_table(); - draw_alias_table(); - draw_aliasdomain_table(); - draw_sync_job_table(); - draw_filter_table(); - draw_bcc_table(); - draw_recipient_map_table(); - draw_tls_policy_table(); - draw_transport_maps_table(); - -}); +$(document).ready(function() { + acl_data = JSON.parse(acl); + FooTable.domainFilter = FooTable.Filtering.extend({ + construct: function(instance){ + this._super(instance); + this.def = 'All Domains'; + this.$domain = null; + }, + $create: function(){ + this._super(); + var self = this; + var domains = []; + + $.each(self.ft.rows.all, function(i, row){ + if((row.val().domain != null) && ($.inArray(row.val().domain, domains) === -1)) domains.push(row.val().domain); + }); + + $form_grp = $('<div/>', {'class': 'form-group'}) + .append($('<label/>', {'class': 'sr-only', text: 'Domain'})) + .prependTo(self.$form); + self.$domain = $('<select/>', { 'class': 'aform-control' }) + .on('change', {self: self}, self._onDomainDropdownChanged) + .append($('<option/>', {text: self.def})) + .appendTo($form_grp); + + $.each(domains, function(i, domain){ + self.$domain.append($('<option/>').text(domain)); + }); + }, + _onDomainDropdownChanged: function(e){ + var self = e.data.self, + selected = $(this).val(); + if (selected !== self.def){ + self.addFilter('domain', selected, ['domain']); + } else { + self.removeFilter('domain'); + } + self.filter(); + }, + draw: function(){ + this._super(); + var domain = this.find('domain'); + if (domain instanceof FooTable.Filter){ + this.$domain.val(domain.query.val()); + } else { + this.$domain.val(this.def); + } + $(this.$domain).closest("select").selectpicker(); + } + }); + // Auto-fill domain quota when adding new domain + auto_fill_quota = function(domain) { + $.get("/api/v1/get/domain/" + domain, function(data){ + var result = $.parseJSON(JSON.stringify(data)); + def_new_mailbox_quota = ( result.def_new_mailbox_quota / 1048576); + max_new_mailbox_quota = ( result.max_new_mailbox_quota / 1048576); + if (max_new_mailbox_quota != '0') { + $("#quotaBadge").html('max. ' + max_new_mailbox_quota + ' MiB'); + $('#addInputQuota').attr({"disabled": false, "value": "", "type": "number", "max": max_new_mailbox_quota}); + $('#addInputQuota').val(def_new_mailbox_quota); + } + else { + $("#quotaBadge").html('max. ' + max_new_mailbox_quota + ' MiB'); + $('#addInputQuota').attr({"disabled": true, "value": "", "type": "text", "value": "n/a"}); + $('#addInputQuota').val(max_new_mailbox_quota); + } + }); + } + $('#addSelectDomain').on('change', function() { + auto_fill_quota($('#addSelectDomain').val()); + }); + auto_fill_quota($('#addSelectDomain').val()); + $(".generate_password").click(function( event ) { + event.preventDefault(); + $('[data-hibp]').trigger('input'); + var random_passwd = GPW.pronounceable(8) + $(this).closest("form").find("input[name='password']").prop('type', 'text'); + $(this).closest("form").find("input[name='password2']").prop('type', 'text'); + $(this).closest("form").find("input[name='password']").val(random_passwd); + $(this).closest("form").find("input[name='password2']").val(random_passwd); + }); + $(".goto_checkbox").click(function( event ) { + $("form[data-id='add_alias'] .goto_checkbox").not(this).prop('checked', false); + if ($("form[data-id='add_alias'] .goto_checkbox:checked").length > 0) { + $('#textarea_alias_goto').prop('disabled', true); + } + else { + $("#textarea_alias_goto").removeAttr('disabled'); + } + }); + $('#addAliasModal').on('show.bs.modal', function(e) { + if ($("form[data-id='add_alias'] .goto_checkbox:checked").length > 0) { + $('#textarea_alias_goto').prop('disabled', true); + } + else { + $("#textarea_alias_goto").removeAttr('disabled'); + } + }); + // Log modal + $('#syncjobLogModal').on('show.bs.modal', function(e) { + var syncjob_id = $(e.relatedTarget).data('syncjob-id'); + $.ajax({ + url: '/inc/ajax/syncjob_logs.php', + data: { id: syncjob_id }, + dataType: 'text', + success: function(data){ + $(e.currentTarget).find('#logText').text(data); + }, + error: function(xhr, status, error) { + $(e.currentTarget).find('#logText').text(xhr.responseText); + } + }); + }); + // Log modal + $('#dnsInfoModal').on('show.bs.modal', function(e) { + var domain = $(e.relatedTarget).data('domain'); + $('.dns-modal-body').html('<center><span style="font-size:18pt;margin:50px" class="glyphicon glyphicon-refresh glyphicon-spin"></span></center>'); + $.ajax({ + url: '/inc/ajax/dns_diagnostics.php', + data: { domain: domain }, + dataType: 'text', + success: function(data){ + $('.dns-modal-body').html(data); + }, + error: function(xhr, status, error) { + $('.dns-modal-body').html(xhr.responseText); + } + }); + }); + // Sieve data modal + $('#sieveDataModal').on('show.bs.modal', function(e) { + var sieveScript = $(e.relatedTarget).data('sieve-script'); + $(e.currentTarget).find('#sieveDataText').html('<pre style="font-size:14px;line-height:1.1">' + sieveScript + '</pre>'); + }); + // Disable submit button on script change + $('#script_data').on('keyup', function() { + $('#add_filter_btns > #add_sieve_script').attr({"disabled": true}); + $('#validation_msg').html('-'); + }); + // Validate script data + $("#validate_sieve").click(function( event ) { + event.preventDefault(); + var script = $('#script_data').val(); + $.ajax({ + dataType: 'jsonp', + url: "/inc/ajax/sieve_validation.php", + type: "get", + data: { script: script }, + complete: function(data) { + var response = (data.responseText); + response_obj = JSON.parse(response); + if (response_obj.type == "success") { + $('#add_filter_btns > #add_sieve_script').attr({"disabled": false}); + } + mailcow_alert_box(response_obj.msg, response_obj.type); + }, + }); + }); + // $(document).on('DOMNodeInserted', '#prefilter_table', function () { + // $("#active-script").closest('td').css('background-color','#b0f0a0'); + // $("#inactive-script").closest('td').css('background-color','#b0f0a0'); + // }); + $('#addResourceModal').on('shown.bs.modal', function() { + $("#multiple_bookings").val($("#multiple_bookings_select").val()); + if ($("#multiple_bookings").val() == "custom") { + $("#multiple_bookings_custom_div").show(); + $("#multiple_bookings").val($("#multiple_bookings_custom").val()); + } + }) + $("#multiple_bookings_select").change(function() { + $("#multiple_bookings").val($("#multiple_bookings_select").val()); + if ($("#multiple_bookings").val() == "custom") { + $("#multiple_bookings_custom_div").show(); + } + else { + $("#multiple_bookings_custom_div").hide(); + } + }); + $("#multiple_bookings_custom").bind ("change keypress keyup blur", function () { + $("#multiple_bookings").val($("#multiple_bookings_custom").val()); + }); + + +}); +jQuery(function($){ + // http://stackoverflow.com/questions/24816/escaping-html-strings-with-jquery + var entityMap={"&":"&","<":"<",">":">",'"':""","'":"'","/":"/","`":"`","=":"="}; + function escapeHtml(n){return String(n).replace(/[&<>"'`=\/]/g,function(n){return entityMap[n]})} + // http://stackoverflow.com/questions/46155/validate-email-address-in-javascript + function validateEmail(email) { + var re = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; + return re.test(email); + } + function humanFileSize(i){if(Math.abs(i)<1024)return i+" B";var B=["KiB","MiB","GiB","TiB","PiB","EiB","ZiB","YiB"],e=-1;do{i/=1024,++e}while(Math.abs(i)>=1024&&e<B.length-1);return i.toFixed(1)+" "+B[e]} + $(".refresh_table").on('click', function(e) { + e.preventDefault(); + var table_name = $(this).data('table'); + $('#' + table_name).find("tr.footable-empty").remove(); + draw_table = $(this).data('draw'); + eval(draw_table + '()'); + }); + function table_mailbox_ready(ft, name) { + if(is_dual) { + $('.login_as').data("toggle", "tooltip") + .attr("disabled", true) + .removeAttr("href") + .attr("title", "Dual login cannot be used twice") + .tooltip(); + } + $('.refresh_table').prop("disabled", false); + heading = ft.$el.parents('.tab-pane').find('.panel-heading') + var ft_paging = ft.use(FooTable.Paging) + $(heading).children('.table-lines').text(function(){ + return ft_paging.totalRows; + }) + } + function draw_domain_table() { + ft_domain_table = FooTable.init('#domain_table', { + "columns": [ + {"name":"chkbox","title":"","style":{"maxWidth":"60px","width":"60px"},"filterable": false,"sortable": false,"type":"html"}, + {"sorted": true,"name":"domain_name","title":lang.domain,"style":{"width":"250px"}}, + {"name":"aliases","title":lang.aliases,"breakpoints":"xs sm"}, + {"name":"mailboxes","title":lang.mailboxes}, + {"name":"quota","style":{"whiteSpace":"nowrap"},"title":lang.domain_quota,"formatter": function(value){ + res = value.split("/"); + return humanFileSize(res[0]) + " / " + humanFileSize(res[1]); + }, + "sortValue": function(value){ + res = value.split("/"); + return Number(res[0]); + }, + }, + {"name":"def_quota_for_mbox","title":lang.mailbox_defquota,"breakpoints":"xs sm md","style":{"width":"125px"}}, + {"name":"max_quota_for_mbox","title":lang.mailbox_quota,"breakpoints":"xs sm","style":{"width":"125px"}}, + {"name":"rl","title":"RL","breakpoints":"xs sm md lg","style":{"maxWidth":"100px","width":"100px"}}, + {"name":"backupmx","filterable": false,"style":{"maxWidth":"120px","width":"120px"},"title":lang.backup_mx,"breakpoints":"xs sm md lg"}, + {"name":"active","filterable": false,"style":{"maxWidth":"80px","width":"80px"},"title":lang.active}, + {"name":"action","filterable": false,"sortable": false,"style":{"text-align":"right","maxWidth":"240px","width":"240px"},"type":"html","title":lang.action,"breakpoints":"xs sm md"} + ], + "rows": $.ajax({ + dataType: 'json', + url: '/api/v1/get/domain/all', + jsonp: false, + error: function (data) { + console.log('Cannot draw domain table'); + }, + success: function (data) { + $.each(data, function (i, item) { + item.aliases = item.aliases_in_domain + " / " + item.max_num_aliases_for_domain; + item.mailboxes = item.mboxes_in_domain + " / " + item.max_num_mboxes_for_domain; + item.quota = item.quota_used_in_domain + "/" + item.max_quota_for_domain; + if (!item.rl) { + item.rl = '∞'; + } else { + item.rl = $.map(item.rl, function(e){ + return e; + }).join('/1'); + } + item.def_quota_for_mbox = humanFileSize(item.def_quota_for_mbox); + item.max_quota_for_mbox = humanFileSize(item.max_quota_for_mbox); + item.chkbox = '<input type="checkbox" data-id="domain" name="multi_select" value="' + encodeURIComponent(item.domain_name) + '" />'; + item.action = '<div class="btn-group">'; + if (role == "admin") { + item.action += '<a href="/edit/domain/' + encodeURIComponent(item.domain_name) + '" class="btn btn-xs btn-default"><span class="glyphicon glyphicon-pencil"></span> ' + lang.edit + '</a>' + + '<a href="#" data-action="delete_selected" data-id="single-domain" data-api-url="delete/domain" data-item="' + encodeURIComponent(item.domain_name) + '" class="btn btn-xs btn-danger"><span class="glyphicon glyphicon-trash"></span> ' + lang.remove + '</a>'; + } + else { + item.action += '<a href="/edit/domain/' + encodeURIComponent(item.domain_name) + '" class="btn btn-xs btn-default"><span class="glyphicon glyphicon-pencil"></span> ' + lang.edit + '</a>'; + } + item.action += '<a href="#dnsInfoModal" class="btn btn-xs btn-info" data-toggle="modal" data-domain="' + encodeURIComponent(item.domain_name) + '"><span class="glyphicon glyphicon-question-sign"></span> DNS</a></div>'; + }); + } + }), + "empty": lang.empty, + "paging": { + "enabled": true, + "limit": 5, + "size": pagination_size + }, + "state": { + "enabled": true + }, + "filtering": { + "enabled": true, + "delay": 1200, + "position": "left", + "connectors": false, + "placeholder": lang.filter_table + }, + "sorting": { + "enabled": true + }, + "on": { + "destroy.ft.table": function(e, ft){ + $('.refresh_table').attr('disabled', 'true'); + }, + "ready.ft.table": function(e, ft){ + table_mailbox_ready(ft, 'domain_table'); + }, + "after.ft.filtering": function(e, ft){ + table_mailbox_ready(ft, 'domain_table'); + } + }, + "toggleSelector": "table tbody span.footable-toggle" + }); + } + function draw_mailbox_table() { + ft_mailbox_table = FooTable.init('#mailbox_table', { + "columns": [ + {"name":"chkbox","title":"","style":{"maxWidth":"60px","width":"60px"},"filterable": false,"sortable": false,"type":"html"}, + {"sorted": true,"name":"username","style":{"word-break":"break-all","min-width":"120px"},"title":lang.username}, + {"name":"name","title":lang.fname,"style":{"word-break":"break-all","min-width":"120px"},"breakpoints":"xs sm md lg"}, + {"name":"domain","title":lang.domain,"breakpoints":"xs sm md lg"}, + {"name":"quota","style":{"whiteSpace":"nowrap"},"title":lang.domain_quota,"formatter": function(value){ + res = value.split("/"); + var of_q = (res[1] == 0 ? "∞" : humanFileSize(res[1])); + return humanFileSize(res[0]) + " / " + of_q; + }, + "sortValue": function(value){ + res = value.split("/"); + return Number(res[0]); + }, + }, + {"name":"spam_aliases","filterable": false,"title":lang.spam_aliases,"breakpoints":"all"}, + {"name":"tls_enforce_in","filterable": false,"title":lang.tls_enforce_in,"breakpoints":"all"}, + {"name":"tls_enforce_out","filterable": false,"title":lang.tls_enforce_out,"breakpoints":"all"}, + {"name":"quarantine_notification","filterable": false,"title":lang.quarantine_notification,"breakpoints":"all"}, + {"name":"in_use","filterable": false,"type":"html","title":lang.in_use,"sortValue": function(value){ + return Number($(value).find(".progress-bar").attr('aria-valuenow')); + }, + }, + {"name":"messages","filterable": false,"title":lang.msg_num,"breakpoints":"xs sm md"}, + {"name":"rl","title":"RL","breakpoints":"all","style":{"width":"125px"}}, + {"name":"active","filterable": false,"title":lang.active}, + {"name":"action","filterable": false,"sortable": false,"style":{"min-width":"290px","text-align":"right"},"type":"html","title":lang.action,"breakpoints":"xs sm md"} + ], + "empty": lang.empty, + "rows": $.ajax({ + dataType: 'json', + url: '/api/v1/get/mailbox/all', + jsonp: false, + error: function () { + console.log('Cannot draw mailbox table'); + }, + success: function (data) { + $.each(data, function (i, item) { + item.quota = item.quota_used + "/" + item.quota; + item.max_quota_for_mbox = humanFileSize(item.max_quota_for_mbox); + if (!item.rl) { + item.rl = '∞'; + } else { + item.rl = $.map(item.rl, function(e){ + return e; + }).join('/1'); + if (item.rl_scope === 'domain') { + item.rl = '↪ ' + item.rl + ' (via ' + item.domain + ')'; + } + } + item.chkbox = '<input type="checkbox" data-id="mailbox" name="multi_select" value="' + encodeURIComponent(item.username) + '" />'; + item.tls_enforce_in = '<span class="text-' + (item.attributes.tls_enforce_in == 1 ? 'success' : 'danger') + ' glyphicon glyphicon-lock"></span>'; + item.tls_enforce_out = '<span class="text-' + (item.attributes.tls_enforce_out == 1 ? 'success' : 'danger') + ' glyphicon glyphicon-lock"></span>'; + if (item.attributes.quarantine_notification === 'never') { + item.quarantine_notification = lang.never; + } else if (item.attributes.quarantine_notification === 'hourly') { + item.quarantine_notification = lang.hourly; + } else if (item.attributes.quarantine_notification === 'daily') { + item.quarantine_notification = lang.daily; + } else if (item.attributes.quarantine_notification === 'weekly') { + item.quarantine_notification = lang.weekly; + } + if (acl_data.login_as === 1) { + item.action = '<div class="btn-group">' + + '<a href="/edit/mailbox/' + encodeURIComponent(item.username) + '" class="btn btn-xs btn-default"><span class="glyphicon glyphicon-pencil"></span> ' + lang.edit + '</a>' + + '<a href="#" data-action="delete_selected" data-id="single-mailbox" data-api-url="delete/mailbox" data-item="' + encodeURIComponent(item.username) + '" class="btn btn-xs btn-danger"><span class="glyphicon glyphicon-trash"></span> ' + lang.remove + '</a>' + + '<a href="/index.php?duallogin=' + encodeURIComponent(item.username) + '" class="login_as btn btn-xs btn-success"><span class="glyphicon glyphicon-user"></span> Login</a>'; + if (ALLOW_ADMIN_EMAIL_LOGIN) { + item.action += '<a href="/sogo-auth.php?login=' + encodeURIComponent(item.username) + '" class="login_as btn btn-xs btn-primary" target="_blank"><span class="glyphicon glyphicon-envelope"></span> SOGo</a>'; + } + item.action += '</div>'; + } + else { + item.action = '<div class="btn-group">' + + '<a href="/edit/mailbox/' + encodeURIComponent(item.username) + '" class="btn btn-xs btn-default"><span class="glyphicon glyphicon-pencil"></span> ' + lang.edit + '</a>' + + '<a href="#" data-action="delete_selected" data-id="single-mailbox" data-api-url="delete/mailbox" data-item="' + encodeURIComponent(item.username) + '" class="btn btn-xs btn-danger"><span class="glyphicon glyphicon-trash"></span> ' + lang.remove + '</a>' + + '</div>'; + } + item.in_use = '<div class="progress">' + + '<div class="progress-bar progress-bar-' + item.percent_class + ' role="progressbar" aria-valuenow="' + item.percent_in_use + '" aria-valuemin="0" aria-valuemax="100" ' + + 'style="min-width:2em;width:' + item.percent_in_use + '%">' + item.percent_in_use + '%' + '</div></div>'; + item.username = escapeHtml(item.username); + }); + } + }), + "paging": { + "enabled": true, + "limit": 5, + "size": pagination_size + }, + "state": { + "enabled": true + }, + "filtering": { + "enabled": true, + "delay": 1200, + "position": "left", + "connectors": false, + //"container": "#tab-mailboxes.panel", + "placeholder": lang.filter_table + }, + "components": { + "filtering": FooTable.domainFilter + }, + "sorting": { + "enabled": true + }, + "on": { + "destroy.ft.table": function(e, ft){ + $('.refresh_table').attr('disabled', 'true'); + }, + "ready.ft.table": function(e, ft){ + table_mailbox_ready(ft, 'mailbox_table'); + }, + "after.ft.filtering": function(e, ft){ + table_mailbox_ready(ft, 'mailbox_table'); + } + }, + "toggleSelector": "table tbody span.footable-toggle" + }); + } + function draw_resource_table() { + ft_resource_table = FooTable.init('#resource_table', { + "columns": [ + {"name":"chkbox","title":"","style":{"maxWidth":"60px","width":"60px"},"filterable": false,"sortable": false,"type":"html"}, + {"sorted": true,"name":"description","title":lang.description,"style":{"width":"250px"}}, + {"name":"name","title":lang.alias}, + {"name":"kind","title":lang.kind}, + {"name":"domain","title":lang.domain,"breakpoints":"xs sm"}, + {"name":"multiple_bookings","filterable": false,"style":{"maxWidth":"150px","width":"140px"},"title":lang.multiple_bookings,"breakpoints":"xs sm"}, + {"name":"active","filterable": false,"style":{"maxWidth":"80px","width":"80px"},"title":lang.active}, + {"name":"action","filterable": false,"sortable": false,"style":{"text-align":"right","maxWidth":"180px","width":"180px"},"type":"html","title":lang.action,"breakpoints":"xs sm"} + ], + "empty": lang.empty, + "rows": $.ajax({ + dataType: 'json', + url: '/api/v1/get/resource/all', + jsonp: false, + error: function () { + console.log('Cannot draw resource table'); + }, + success: function (data) { + $.each(data, function (i, item) { + if (item.multiple_bookings == '0') { + item.multiple_bookings = '<span id="active-script" class="label label-success">' + lang.booking_0_short + '</span>'; + } else if (item.multiple_bookings == '-1') { + item.multiple_bookings = '<span id="active-script" class="label label-warning">' + lang.booking_lt0_short + '</span>'; + } else { + item.multiple_bookings = '<span id="active-script" class="label label-danger">' + lang.booking_custom_short + ' (' + item.multiple_bookings + ')</span>'; + } + item.action = '<div class="btn-group">' + + '<a href="/edit/resource/' + encodeURIComponent(item.name) + '" class="btn btn-xs btn-default"><span class="glyphicon glyphicon-pencil"></span> ' + lang.edit + '</a>' + + '<a href="#" data-action="delete_selected" data-id="single-resource" data-api-url="delete/resource" data-item="' + item.name + '" class="btn btn-xs btn-danger"><span class="glyphicon glyphicon-trash"></span> ' + lang.remove + '</a>' + + '</div>'; + item.chkbox = '<input type="checkbox" data-id="resource" name="multi_select" value="' + encodeURIComponent(item.name) + '" />'; + item.name = escapeHtml(item.name); + }); + } + }), + "paging": { + "enabled": true, + "limit": 5, + "size": pagination_size + }, + "state": { + "enabled": true + }, + "filtering": { + "enabled": true, + "delay": 1200, + "position": "left", + "connectors": false, + "placeholder": lang.filter_table + }, + "components": { + "filtering": FooTable.domainFilter + }, + "sorting": { + "enabled": true + }, + "on": { + "destroy.ft.table": function(e, ft){ + $('.refresh_table').attr('disabled', 'true'); + }, + "ready.ft.table": function(e, ft){ + table_mailbox_ready(ft, 'resource_table'); + }, + "after.ft.filtering": function(e, ft){ + table_mailbox_ready(ft, 'resource_table'); + } + }, + "toggleSelector": "table tbody span.footable-toggle" + }); + } + function draw_bcc_table() { + ft_bcc_table = FooTable.init('#bcc_table', { + "columns": [ + {"name":"chkbox","title":"","style":{"maxWidth":"60px","width":"60px"},"filterable": false,"sortable": false,"type":"html"}, + {"sorted": true,"name":"id","title":"ID","style":{"maxWidth":"60px","width":"60px","text-align":"center"}}, + {"name":"type","title":lang.bcc_type}, + {"name":"local_dest","title":lang.bcc_local_dest}, + {"name":"bcc_dest","title":lang.bcc_destinations}, + {"name":"domain","title":lang.domain,"breakpoints":"xs sm"}, + {"name":"active","filterable": false,"style":{"maxWidth":"80px","width":"80px"},"title":lang.active}, + {"name":"action","filterable": false,"sortable": false,"style":{"text-align":"right","maxWidth":"180px","width":"180px"},"type":"html","title":lang.action,"breakpoints":"xs sm"} + ], + "empty": lang.empty, + "rows": $.ajax({ + dataType: 'json', + url: '/api/v1/get/bcc/all', + jsonp: false, + error: function () { + console.log('Cannot draw bcc table'); + }, + success: function (data) { + $.each(data, function (i, item) { + item.action = '<div class="btn-group">' + + '<a href="/edit/bcc/' + item.id + '" class="btn btn-xs btn-default"><span class="glyphicon glyphicon-pencil"></span> ' + lang.edit + '</a>' + + '<a href="#" data-action="delete_selected" data-id="single-bcc" data-api-url="delete/bcc" data-item="' + item.id + '" class="btn btn-xs btn-danger"><span class="glyphicon glyphicon-trash"></span> ' + lang.remove + '</a>' + + '</div>'; + item.chkbox = '<input type="checkbox" data-id="bcc" name="multi_select" value="' + item.id + '" />'; + item.local_dest = escapeHtml(item.local_dest); + item.bcc_dest = escapeHtml(item.bcc_dest); + if (item.type == 'sender') { + item.type = '<span id="active-script" class="label label-success">Sender</span>'; + } else { + item.type = '<span id="inactive-script" class="label label-warning">Recipient</span>'; + } + }); + } + }), + "paging": { + "enabled": true, + "limit": 5, + "size": pagination_size + }, + "state": { + "enabled": true + }, + "filtering": { + "enabled": true, + "delay": 1200, + "position": "left", + "connectors": false, + "placeholder": lang.filter_table + }, + "sorting": { + "enabled": true + }, + "on": { + "destroy.ft.table": function(e, ft){ + $('.refresh_table').attr('disabled', 'true'); + }, + "ready.ft.table": function(e, ft){ + table_mailbox_ready(ft, 'bcc_table'); + }, + "after.ft.filtering": function(e, ft){ + table_mailbox_ready(ft, 'bcc_table'); + } + }, + "toggleSelector": "table tbody span.footable-toggle" + }); + } + function draw_recipient_map_table() { + ft_recipient_map_table = FooTable.init('#recipient_map_table', { + "columns": [ + {"name":"chkbox","title":"","style":{"maxWidth":"60px","width":"60px"},"filterable": false,"sortable": false,"type":"html"}, + {"sorted": true,"name":"id","title":"ID","style":{"maxWidth":"60px","width":"60px","text-align":"center"}}, + {"name":"recipient_map_old","title":lang.recipient_map_old}, + {"name":"recipient_map_new","title":lang.recipient_map_new}, + {"name":"active","filterable": false,"style":{"maxWidth":"80px","width":"80px"},"title":lang.active}, + {"name":"action","filterable": false,"sortable": false,"style":{"text-align":"right","maxWidth":"180px","width":"180px"},"type":"html","title":(role == "admin" ? lang.action : ""),"breakpoints":"xs sm"} + ], + "empty": lang.empty, + "rows": $.ajax({ + dataType: 'json', + url: '/api/v1/get/recipient_map/all', + jsonp: false, + error: function () { + console.log('Cannot draw recipient map table'); + }, + success: function (data) { + if (role == "admin") { + $.each(data, function (i, item) { + item.recipient_map_old = escapeHtml(item.recipient_map_old); + item.recipient_map_new = escapeHtml(item.recipient_map_new); + item.action = '<div class="btn-group">' + + '<a href="/edit/recipient_map/' + item.id + '" class="btn btn-xs btn-default"><span class="glyphicon glyphicon-pencil"></span> ' + lang.edit + '</a>' + + '<a href="#" data-action="delete_selected" data-id="single-recipient_map" data-api-url="delete/recipient_map" data-item="' + item.id + '" class="btn btn-xs btn-danger"><span class="glyphicon glyphicon-trash"></span> ' + lang.remove + '</a>' + + '</div>'; + item.chkbox = '<input type="checkbox" data-id="recipient_map" name="multi_select" value="' + item.id + '" />'; + }); + } + } + }), + "paging": { + "enabled": true, + "limit": 5, + "size": pagination_size + }, + "state": { + "enabled": true + }, + "filtering": { + "enabled": true, + "delay": 1200, + "position": "left", + "connectors": false, + "placeholder": lang.filter_table + }, + "sorting": { + "enabled": true + }, + "on": { + "destroy.ft.table": function(e, ft){ + $('.refresh_table').attr('disabled', 'true'); + }, + "ready.ft.table": function(e, ft){ + table_mailbox_ready(ft, 'recipient_map_table'); + }, + "after.ft.filtering": function(e, ft){ + table_mailbox_ready(ft, 'recipient_map_table'); + } + }, + "toggleSelector": "table tbody span.footable-toggle" + }); + } + function draw_tls_policy_table() { + ft_tls_policy_table = FooTable.init('#tls_policy_table', { + "columns": [ + {"name":"chkbox","title":"","style":{"maxWidth":"60px","width":"60px"},"filterable": false,"sortable": false,"type":"html"}, + {"sorted": true,"name":"id","title":"ID","style":{"maxWidth":"60px","width":"60px","text-align":"center"}}, + {"name":"dest","title":lang.tls_map_dest}, + {"name":"policy","title":lang.tls_map_policy}, + {"name":"parameters","title":lang.tls_map_parameters}, + {"name":"active","filterable": false,"style":{"maxWidth":"80px","width":"80px"},"title":lang.active}, + {"name":"action","filterable": false,"sortable": false,"style":{"text-align":"right","maxWidth":"180px","width":"180px"},"type":"html","title":(role == "admin" ? lang.action : ""),"breakpoints":"xs sm"} + ], + "empty": lang.empty, + "rows": $.ajax({ + dataType: 'json', + url: '/api/v1/get/tls-policy-map/all', + jsonp: false, + error: function () { + console.log('Cannot draw tls policy map table'); + }, + success: function (data) { + if (role == "admin") { + $.each(data, function (i, item) { + item.dest = escapeHtml(item.dest); + item.policy = '<b>' + escapeHtml(item.policy) + '</b>'; + if (item.parameters == '') { + item.parameters = '<code>-</code>'; + } else { + item.parameters = '<code>' + escapeHtml(item.parameters) + '</code>'; + } + item.action = '<div class="btn-group">' + + '<a href="/edit/tls_policy_map/' + item.id + '" class="btn btn-xs btn-default"><span class="glyphicon glyphicon-pencil"></span> ' + lang.edit + '</a>' + + '<a href="#" data-action="delete_selected" data-id="single-tls-policy-map" data-api-url="delete/tls-policy-map" data-item="' + item.id + '" class="btn btn-xs btn-danger"><span class="glyphicon glyphicon-trash"></span> ' + lang.remove + '</a>' + + '</div>'; + item.chkbox = '<input type="checkbox" data-id="tls-policy-map" name="multi_select" value="' + item.id + '" />'; + }); + } + } + }), + "paging": { + "enabled": true, + "limit": 5, + "size": pagination_size + }, + "state": { + "enabled": true + }, + "filtering": { + "enabled": true, + "delay": 1200, + "position": "left", + "connectors": false, + "placeholder": lang.filter_table + }, + "sorting": { + "enabled": true + }, + "on": { + "destroy.ft.table": function(e, ft){ + $('.refresh_table').attr('disabled', 'true'); + }, + "ready.ft.table": function(e, ft){ + table_mailbox_ready(ft, 'tls_policy_table'); + }, + "after.ft.filtering": function(e, ft){ + table_mailbox_ready(ft, 'tls_policy_table'); + } + }, + "toggleSelector": "table tbody span.footable-toggle" + }); + } + function draw_transport_maps_table() { + ft_transport_maps_table = FooTable.init('#transport_maps_table', { + "columns": [ + {"name":"chkbox","title":"","style":{"maxWidth":"60px","width":"60px"},"filterable": false,"sortable": false,"type":"html"}, + {"sorted": true,"name":"id","title":"ID","style":{"maxWidth":"60px","width":"60px","text-align":"center"}}, + {"name":"dest","title":lang.tls_map_dest}, + {"name":"parameters","title":lang.tls_map_parameters}, + {"name":"active","filterable": false,"style":{"maxWidth":"80px","width":"80px"},"title":lang.active}, + {"name":"action","filterable": false,"sortable": false,"style":{"text-align":"right","maxWidth":"180px","width":"180px"},"type":"html","title":(role == "admin" ? lang.action : ""),"breakpoints":"xs sm"} + ], + "empty": lang.empty, + "rows": $.ajax({ + dataType: 'json', + url: '/api/v1/get/transport-map/all', + jsonp: false, + error: function () { + console.log('Cannot draw transport map table'); + }, + success: function (data) { + if (role == "admin") { + $.each(data, function (i, item) { + item.dest = escapeHtml(item.dest); + if (item.parameters == '') { + item.parameters = '<code>-</code>'; + } else { + item.parameters = '<code>' + escapeHtml(item.parameters) + '</code>'; + } + item.action = '<div class="btn-group">' + + '<a href="/edit/transport_map/' + item.id + '" class="btn btn-xs btn-default"><span class="glyphicon glyphicon-pencil"></span> ' + lang.edit + '</a>' + + '<a href="#" data-action="delete_selected" data-id="single-transport-map" data-api-url="delete/transport-map" data-item="' + item.id + '" class="btn btn-xs btn-danger"><span class="glyphicon glyphicon-trash"></span> ' + lang.remove + '</a>' + + '</div>'; + item.chkbox = '<input type="checkbox" data-id="transport-map" name="multi_select" value="' + item.id + '" />'; + }); + } + } + }), + "paging": { + "enabled": true, + "limit": 5, + "size": pagination_size + }, + "state": { + "enabled": true + }, + "filtering": { + "enabled": true, + "delay": 1200, + "position": "left", + "connectors": false, + "placeholder": lang.filter_table + }, + "sorting": { + "enabled": true + }, + "on": { + "destroy.ft.table": function(e, ft){ + $('.refresh_table').attr('disabled', 'true'); + }, + "ready.ft.table": function(e, ft){ + table_mailbox_ready(ft, 'transport_maps_table'); + }, + "after.ft.filtering": function(e, ft){ + table_mailbox_ready(ft, 'transport_maps_table'); + } + }, + "toggleSelector": "table tbody span.footable-toggle" + }); + } + function draw_alias_table() { + ft_alias_table = FooTable.init('#alias_table', { + "columns": [ + {"name":"chkbox","title":"","style":{"maxWidth":"60px","width":"60px"},"filterable": false,"sortable": false,"type":"html"}, + {"name":"id","title":"ID","style":{"maxWidth":"60px","width":"60px","text-align":"center"}}, + {"sorted": true,"name":"address","title":lang.alias,"style":{"width":"250px"}}, + {"name":"goto","title":lang.target_address}, + {"name":"domain","title":lang.domain,"breakpoints":"xs sm"}, + {"name":"public_comment","title":lang.public_comment,"breakpoints":"all"}, + {"name":"private_comment","title":lang.private_comment,"breakpoints":"all"}, + {"name":"active","filterable": false,"style":{"maxWidth":"50px","width":"70px"},"title":lang.active}, + {"name":"action","filterable": false,"sortable": false,"style":{"text-align":"right","maxWidth":"180px","width":"180px"},"type":"html","title":lang.action,"breakpoints":"xs sm"} + ], + "empty": lang.empty, + "rows": $.ajax({ + dataType: 'json', + url: '/api/v1/get/alias/all', + jsonp: false, + error: function () { + console.log('Cannot draw alias table'); + }, + success: function (data) { + $.each(data, function (i, item) { + item.action = '<div class="btn-group">' + + '<a href="/edit/alias/' + encodeURIComponent(item.id) + '" class="btn btn-xs btn-default"><span class="glyphicon glyphicon-pencil"></span> ' + lang.edit + '</a>' + + '<a href="#" data-action="delete_selected" data-id="single-alias" data-api-url="delete/alias" data-item="' + encodeURIComponent(item.id) + '" class="btn btn-xs btn-danger"><span class="glyphicon glyphicon-trash"></span> ' + lang.remove + '</a>' + + '</div>'; + item.chkbox = '<input type="checkbox" data-id="alias" name="multi_select" value="' + encodeURIComponent(item.id) + '" />'; + item.goto = escapeHtml(item.goto.replace(/,/g, " ")); + if (item.public_comment !== null) { + item.public_comment = escapeHtml(item.public_comment); + } + else { + item.public_comment = '-'; + } + if (item.private_comment !== null) { + item.private_comment = escapeHtml(item.private_comment); + } + else { + item.private_comment = '-'; + } + if (item.is_catch_all == 1) { + item.address = '<div class="label label-default">Catch-All</div> ' + escapeHtml(item.address); + } + else { + item.address = escapeHtml(item.address); + } + if (item.goto == "null@localhost") { + item.goto = '⤷ <span style="font-size:12px" class="glyphicon glyphicon-trash" aria-hidden="true"></span>'; + } + else if (item.goto == "spam@localhost") { + item.goto = '<span class="label label-danger">Learn as spam</span>'; + } + else if (item.goto == "ham@localhost") { + item.goto = '<span class="label label-success">Learn as ham</span>'; + } + if (item.in_primary_domain !== "") { + item.domain = "↳ " + item.domain + " (" + item.in_primary_domain + ")"; + } + }); + } + }), + "paging": { + "enabled": true, + "limit": 5, + "size": pagination_size + }, + "state": { + "enabled": true + }, + "filtering": { + "enabled": true, + "delay": 1200, + "position": "left", + "connectors": false, + "placeholder": lang.filter_table + }, + "components": { + "filtering": FooTable.domainFilter + }, + "sorting": { + "enabled": true + }, + "on": { + "destroy.ft.table": function(e, ft){ + $('.refresh_table').attr('disabled', 'true'); + }, + "ready.ft.table": function(e, ft){ + table_mailbox_ready(ft, 'alias_table'); + }, + "after.ft.filtering": function(e, ft){ + table_mailbox_ready(ft, 'alias_table'); + } + }, + "toggleSelector": "table tbody span.footable-toggle" + }); + } + + function draw_aliasdomain_table() { + ft_aliasdomain_table = FooTable.init('#aliasdomain_table', { + "columns": [ + {"name":"chkbox","title":"","style":{"maxWidth":"60px","width":"60px"},"filterable": false,"sortable": false,"type":"html"}, + {"sorted": true,"name":"alias_domain","title":lang.alias,"style":{"width":"250px"}}, + {"name":"target_domain","title":lang.target_domain,"type":"html"}, + {"name":"active","filterable": false,"style":{"maxWidth":"50px","width":"70px"},"title":lang.active}, + {"name":"action","filterable": false,"sortable": false,"style":{"text-align":"right","maxWidth":"250px","width":"250px"},"type":"html","title":lang.action,"breakpoints":"xs sm"} + ], + "empty": lang.empty, + "rows": $.ajax({ + dataType: 'json', + url: '/api/v1/get/alias-domain/all', + jsonp: false, + error: function () { + console.log('Cannot draw alias domain table'); + }, + success: function (data) { + $.each(data, function (i, item) { + item.action = '<div class="btn-group">' + + '<a href="/edit/aliasdomain/' + encodeURIComponent(item.alias_domain) + '" class="btn btn-xs btn-default"><span class="glyphicon glyphicon-pencil"></span> ' + lang.edit + '</a>' + + '<a href="#" data-action="delete_selected" data-id="single-alias-domain" data-api-url="delete/alias-domain" data-item="' + encodeURIComponent(item.alias_domain) + '" class="btn btn-xs btn-danger"><span class="glyphicon glyphicon-trash"></span> ' + lang.remove + '</a>' + + '<a href="#dnsInfoModal" class="btn btn-xs btn-info" data-toggle="modal" data-domain="' + encodeURIComponent(item.alias_domain) + '"><span class="glyphicon glyphicon-question-sign"></span> DNS</a></div>' + + '</div>'; + item.chkbox = '<input type="checkbox" data-id="alias-domain" name="multi_select" value="' + encodeURIComponent(item.alias_domain) + '" />'; + if(item.parent_is_backupmx == '1') { + item.target_domain = '<span><a href="/edit/domain/' + item.target_domain + '">' + item.target_domain + '</a> <div class="label label-warning">' + lang.alias_domain_backupmx + '</div></span>'; + } else { + item.target_domain = '<span><a href="/edit/domain/' + item.target_domain + '">' + item.target_domain + '</a></span>'; + } + }); + } + }), + "paging": { + "enabled": true, + "limit": 5, + "size": pagination_size + }, + "state": { + "enabled": true + }, + "filtering": { + "enabled": true, + "delay": 1200, + "position": "left", + "connectors": false, + "placeholder": lang.filter_table + }, + "sorting": { + "enabled": true + }, + "on": { + "destroy.ft.table": function(e, ft){ + $('.refresh_table').attr('disabled', 'true'); + }, + "ready.ft.table": function(e, ft){ + table_mailbox_ready(ft, 'aliasdomain_table'); + }, + "after.ft.filtering": function(e, ft){ + table_mailbox_ready(ft, 'aliasdomain_table'); + } + }, + "toggleSelector": "table tbody span.footable-toggle" + }); + } + + function draw_sync_job_table() { + ft_syncjob_table = FooTable.init('#sync_job_table', { + "columns": [ + {"name":"chkbox","title":"","style":{"maxWidth":"60px","width":"60px","text-align":"center"},"filterable": false,"sortable": false,"type":"html"}, + {"sorted": true,"name":"id","title":"ID","style":{"maxWidth":"60px","width":"60px","text-align":"center"}}, + {"name":"user2","title":lang.owner}, + {"name":"server_w_port","title":"Server","breakpoints":"xs sm md","style":{"word-break":"break-all"}}, + {"name":"exclude","title":lang.excludes,"breakpoints":"all"}, + {"name":"mins_interval","title":lang.mins_interval,"breakpoints":"all"}, + {"name":"last_run","title":lang.last_run,"breakpoints":"xs sm md"}, + {"name":"log","title":"Log"}, + {"name":"active","filterable": false,"style":{"maxWidth":"70px","width":"70px"},"title":lang.active}, + {"name":"is_running","filterable": false,"style":{"maxWidth":"120px","width":"100px"},"title":lang.status}, + {"name":"action","filterable": false,"sortable": false,"style":{"text-align":"right","maxWidth":"180px","width":"180px"},"type":"html","title":lang.action,"breakpoints":"xs sm"} + ], + "empty": lang.empty, + "rows": $.ajax({ + dataType: 'json', + url: '/api/v1/get/syncjobs/all/no_log', + jsonp: false, + error: function () { + console.log('Cannot draw sync job table'); + }, + success: function (data) { + $.each(data, function (i, item) { + item.log = '<a href="#syncjobLogModal" data-toggle="modal" data-syncjob-id="' + encodeURIComponent(item.id) + '">Open logs</a>' + item.user2 = escapeHtml(item.user2); + if (!item.exclude > 0) { + item.exclude = '-'; + } else { + item.exclude = '<code>' + item.exclude + '</code>'; + } + item.server_w_port = escapeHtml(item.user1) + '@' + item.host1 + ':' + item.port1; + item.action = '<div class="btn-group">' + + '<a href="/edit/syncjob/' + item.id + '" class="btn btn-xs btn-default"><span class="glyphicon glyphicon-pencil"></span> ' + lang.edit + '</a>' + + '<a href="#" data-action="delete_selected" data-id="single-syncjob" data-api-url="delete/syncjob" data-item="' + item.id + '" class="btn btn-xs btn-danger"><span class="glyphicon glyphicon-trash"></span> ' + lang.remove + '</a>' + + '</div>'; + item.chkbox = '<input type="checkbox" data-id="syncjob" name="multi_select" value="' + item.id + '" />'; + if (item.is_running == 1) { + item.is_running = '<span id="active-script" class="label label-success">' + lang.running + '</span>'; + } else { + item.is_running = '<span id="inactive-script" class="label label-warning">' + lang.waiting + '</span>'; + } + if (!item.last_run > 0) { + item.last_run = lang.waiting; + } + }); + } + }), + "paging": { + "enabled": true, + "limit": 5, + "size": pagination_size + }, + "state": { + "enabled": true + }, + "filtering": { + "enabled": true, + "delay": 1200, + "position": "left", + "connectors": false, + "placeholder": lang.filter_table + }, + "sorting": { + "enabled": true + }, + "on": { + "destroy.ft.table": function(e, ft){ + $('.refresh_table').attr('disabled', 'true'); + }, + "ready.ft.table": function(e, ft){ + table_mailbox_ready(ft, 'sync_job_table'); + }, + "after.ft.filtering": function(e, ft){ + table_mailbox_ready(ft, 'sync_job_table'); + } + }, + "toggleSelector": "table tbody span.footable-toggle" + }); + } + + function draw_filter_table() { + ft_filter_table = FooTable.init('#filter_table', { + "columns": [ + {"name":"chkbox","title":"","style":{"maxWidth":"60px","width":"60px","text-align":"center"},"filterable": false,"sortable": false,"type":"html"}, + {"name":"id","title":"ID","style":{"maxWidth":"60px","width":"60px","text-align":"center"}}, + {"name":"active","style":{"maxWidth":"80px","width":"80px"},"title":lang.active}, + {"name":"filter_type","style":{"maxWidth":"80px","width":"80px"},"title":"Type"}, + {"sorted": true,"name":"username","title":lang.owner,"style":{"maxWidth":"550px","width":"350px"}}, + {"name":"script_desc","title":lang.description,"breakpoints":"xs"}, + {"name":"script_data","title":"Script","breakpoints":"all"}, + {"name":"action","filterable": false,"sortable": false,"style":{"text-align":"right","maxWidth":"180px","width":"180px"},"type":"html","title":lang.action,"breakpoints":"xs sm"} + ], + "empty": lang.empty, + "rows": $.ajax({ + dataType: 'json', + url: '/api/v1/get/filters/all', + jsonp: false, + error: function () { + console.log('Cannot draw filter table'); + }, + success: function (data) { + $.each(data, function (i, item) { + if (item.active_int == 1) { + item.active = '<span id="active-script" class="label label-success">' + lang.active + '</span>'; + } else { + item.active = '<span id="inactive-script" class="label label-warning">' + lang.inactive + '</span>'; + } + item.script_data = '<pre style="margin:0px">' + escapeHtml(item.script_data) + '</pre>' + item.filter_type = '<div class="label label-default">' + item.filter_type.charAt(0).toUpperCase() + item.filter_type.slice(1).toLowerCase() + '</div>' + item.action = '<div class="btn-group">' + + '<a href="/edit/filter/' + item.id + '" class="btn btn-xs btn-default"><span class="glyphicon glyphicon-pencil"></span> ' + lang.edit + '</a>' + + '<a href="#" data-action="delete_selected" data-id="single-filter" data-api-url="delete/filter" data-item="' + encodeURIComponent(item.id) + '" class="btn btn-xs btn-danger"><span class="glyphicon glyphicon-trash"></span> ' + lang.remove + '</a>' + + '</div>'; + item.chkbox = '<input type="checkbox" data-id="filter_item" name="multi_select" value="' + item.id + '" />' + }); + } + }), + "paging": { + "enabled": true, + "limit": 5, + "size": pagination_size + }, + "state": { + "enabled": true + }, + "filtering": { + "enabled": true, + "delay": 1200, + "position": "left", + "connectors": false, + "placeholder": lang.filter_table + }, + "sorting": { + "enabled": true + }, + "on": { + "destroy.ft.table": function(e, ft){ + $('.refresh_table').attr('disabled', 'true'); + }, + "ready.ft.table": function(e, ft){ + table_mailbox_ready(ft, 'filter_table'); + }, + "after.ft.filtering": function(e, ft){ + table_mailbox_ready(ft, 'filter_table'); + } + }, + "toggleSelector": "table tbody span.footable-toggle" + }); + }; + + $('body').on('click', 'span.footable-toggle', function () { + event.stopPropagation(); + }) + + draw_domain_table(); + draw_mailbox_table(); + draw_resource_table(); + draw_alias_table(); + draw_aliasdomain_table(); + draw_sync_job_table(); + draw_filter_table(); + draw_bcc_table(); + draw_recipient_map_table(); + draw_tls_policy_table(); + draw_transport_maps_table(); + +}); \ No newline at end of file diff --git a/data/web/js/site/pwgen.js b/data/web/js/site/pwgen.js new file mode 100644 index 00000000..00afb474 --- /dev/null +++ b/data/web/js/site/pwgen.js @@ -0,0 +1,759 @@ +/* GPW - Generate pronounceable passwords + This program uses statistics on the frequency of three-letter sequences + in English to generate passwords. The statistics are + generated from your dictionary by the program load_trigram. + + See www.multicians.org/thvv/gpw.html for history and info. + Tom Van Vleck + + THVV 06/01/94 Coded + THVV 04/14/96 converted to Java + THVV 07/30/97 fixed for Netscape 4.0 + THVV 11/27/09 ported to Javascript + */ + +var GPW = { + +/** + * var pw = GPW.pronounceable(10); + */ + +pronounceable : function (pwl) { + var output = ""; + var c1, c2, c3; + var sum = 0; + var nchar; + var ranno; + var pwnum; + var pik; + + var _alphabet = "abcdefghijklmnopqrstuvwxyz"; + +// letter frequencies +var _trigram = [[ /* [26][26][26] */ +/* A A */ [2,0,3,0,0,0,1,0,0,0,0,1,1,1,0,0,0,3,2,0,0,0,0,0,0,0], +/* A B */ [37,25,2,5,38,0,0,2,46,1,0,304,0,2,49,0,0,24,24,0,19,0,0,0,14,0], +/* A C */ [26,1,64,2,107,0,1,94,67,0,173,13,5,1,35,1,13,32,3,114,23,0,0,0,45,0], +/* A D */ [35,7,3,43,116,6,3,8,75,14,1,16,25,3,44,3,1,35,20,1,10,25,9,0,18,0], +/* A E */ [2,0,2,1,0,1,3,0,0,0,0,10,0,2,3,0,0,12,6,0,2,0,0,0,0,0], +/* A F */ [5,0,0,0,14,50,2,0,3,0,2,5,0,2,7,0,0,5,1,39,1,0,0,0,1,0], +/* A G */ [30,1,0,1,182,0,42,5,30,0,0,7,9,42,51,3,0,24,3,0,21,0,3,0,3,0], +/* A H */ [12,0,0,0,20,0,0,0,3,0,0,5,4,2,13,0,0,2,0,0,1,0,0,0,0,0], +/* A I */ [2,0,10,26,2,1,10,0,2,1,2,87,13,144,0,2,0,93,30,23,0,3,1,0,0,0], +/* A J */ [4,0,0,0,3,0,0,0,0,0,0,0,0,0,4,0,0,0,0,0,0,0,0,0,0,0], +/* A K */ [11,0,1,1,98,1,0,1,15,0,0,3,0,0,5,1,0,3,0,1,2,0,3,0,8,0], +/* A L */ [78,20,34,45,124,21,24,5,109,0,28,237,31,3,53,23,0,7,16,69,29,26,5,0,26,2], +/* A M */ [70,57,1,1,98,3,0,1,68,0,0,3,38,2,43,69,0,3,14,3,12,0,2,0,14,0], +/* A N */ [114,6,156,359,103,8,146,12,141,2,57,4,0,89,61,1,4,1,124,443,29,6,1,3,28,9], +/* A O */ [0,0,1,0,0,0,0,0,0,0,0,3,1,0,0,0,0,3,2,2,2,0,0,0,0,0], +/* A P */ [29,3,0,1,59,1,0,86,25,0,1,14,1,1,37,94,0,9,22,30,8,0,0,0,9,0], +/* A Q */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,14,0,0,0,0,0], +/* A R */ [124,64,101,233,115,12,47,5,188,3,61,55,68,34,46,25,6,94,48,189,5,22,5,1,172,2], +/* A S */ [19,3,32,0,71,0,1,81,49,0,22,3,19,2,19,34,4,0,152,211,12,0,1,0,17,1], +/* A T */ [50,3,41,2,863,4,0,144,352,0,5,14,6,3,144,0,0,60,13,106,57,1,5,0,8,5], +/* A U */ [0,5,23,35,5,5,38,1,0,1,3,33,4,23,0,4,1,35,52,56,0,1,0,7,0,1], +/* A V */ [35,0,0,1,108,0,0,0,49,0,0,1,0,0,19,0,0,0,0,0,3,1,0,0,6,0], +/* A W */ [30,10,0,4,3,6,2,2,2,0,10,13,4,15,3,0,0,6,3,5,0,0,0,0,2,0], +/* A X */ [3,0,0,0,4,0,0,0,22,0,0,1,0,0,7,2,0,0,1,1,0,0,3,0,3,0], +/* A Y */ [11,8,1,5,16,5,1,2,2,0,0,10,7,4,13,1,0,3,5,7,3,0,5,0,0,0], +/* A Z */ [10,0,0,1,22,0,0,0,10,0,0,0,0,0,7,0,0,0,0,2,2,0,0,0,4,11]], +/* B A */ [[0,17,74,11,1,2,19,4,8,0,10,68,7,73,1,7,0,110,54,55,9,1,3,1,12,1], +/* B B */ [7,0,0,0,16,0,0,0,10,0,0,24,0,0,9,0,0,2,3,0,2,0,0,0,14,0], +/* B C */ [2,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0], +/* B D */ [2,0,0,0,2,0,0,0,2,0,0,0,0,0,3,0,0,1,0,0,3,0,0,0,0,0], +/* B E */ [51,1,14,34,18,11,16,7,9,0,1,85,5,48,2,2,2,199,36,41,0,4,5,1,6,2], +/* B F */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0], +/* B G */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* B H */ [0,0,0,0,1,0,0,0,0,0,0,0,0,0,5,0,0,0,0,0,1,0,0,0,0,0], +/* B I */ [34,8,22,21,8,3,9,1,0,3,1,50,7,45,16,4,2,29,22,59,4,4,0,0,0,3], +/* B J */ [0,0,0,0,6,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0], +/* B K */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* B L */ [57,0,0,0,519,0,0,0,35,0,0,0,0,0,47,0,0,0,0,0,32,1,0,0,3,0], +/* B M */ [0,0,0,0,1,0,0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0], +/* B N */ [1,0,0,0,2,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0], +/* B O */ [62,7,4,21,3,2,9,3,8,1,1,46,8,63,58,2,0,55,15,20,46,6,17,10,19,0], +/* B P */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,1,0,0,0,0,0,0,0,0], +/* B Q */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* B R */ [110,0,0,0,77,0,0,0,100,0,0,0,0,0,78,0,0,0,0,0,28,0,0,0,10,0], +/* B S */ [0,0,6,0,16,0,0,0,7,0,0,0,0,0,12,0,0,0,0,27,2,0,0,0,0,0], +/* B T */ [1,0,0,0,3,1,0,0,0,0,0,4,0,0,1,0,0,3,0,0,0,0,0,0,0,0], +/* B U */ [0,3,21,16,3,5,14,0,12,1,2,52,7,20,2,0,1,104,44,54,0,0,0,3,1,5], +/* B V */ [0,0,0,0,3,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* B W */ [0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* B X */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* B Y */ [1,0,0,0,3,0,1,2,0,0,0,4,0,0,0,3,0,6,8,3,0,0,2,0,0,2], +/* B Z */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]], +/* C A */ [[1,47,17,33,1,3,4,5,7,1,3,120,40,120,1,59,1,171,60,150,19,20,1,0,5,0], +/* C B */ [0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,1,1,0,0,0,0,0,0,0], +/* C C */ [23,0,0,0,22,0,0,5,13,0,0,13,0,0,26,0,0,7,0,0,27,0,0,0,0,0], +/* C D */ [1,0,1,0,1,0,0,0,0,0,0,0,0,0,8,0,0,0,0,0,0,0,0,0,0,0], +/* C E */ [23,6,4,17,6,6,1,2,13,0,0,50,12,109,7,43,0,76,63,22,1,0,4,0,2,1], +/* C F */ [2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* C G */ [0,0,0,0,1,0,0,0,2,0,0,0,0,0,2,0,0,4,1,0,1,0,0,0,0,0], +/* C H */ [165,10,2,3,176,4,3,1,141,0,0,26,20,16,102,1,0,63,8,10,44,0,13,0,20,0], +/* C I */ [76,15,8,33,24,16,3,0,0,0,0,38,5,45,50,28,0,29,38,71,6,8,0,0,0,0], +/* C J */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* C K */ [17,16,2,3,90,4,1,7,20,1,1,45,8,8,12,9,0,3,32,6,6,0,13,0,22,0], +/* C L */ [95,0,0,0,84,0,0,0,50,0,0,0,0,0,54,0,0,0,0,0,34,0,0,0,3,0], +/* C M */ [1,0,0,0,2,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0], +/* C N */ [2,0,0,0,1,0,0,0,4,0,0,0,0,0,1,0,0,0,0,0,1,0,0,0,0,0], +/* C O */ [33,16,40,22,14,10,11,12,9,1,1,101,218,421,24,56,2,129,37,40,86,22,25,4,4,2], +/* C P */ [1,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0], +/* C Q */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,13,0,0,0,0,0], +/* C R */ [101,0,0,0,112,0,0,0,75,0,0,0,0,0,88,0,0,0,0,1,41,0,0,0,25,0], +/* C S */ [0,0,0,0,0,0,0,0,3,0,0,0,0,1,2,0,0,0,1,2,0,0,0,0,0,0], +/* C T */ [44,0,0,0,12,2,0,0,113,0,0,0,2,0,94,0,0,46,0,0,42,0,1,0,3,0], +/* C U */ [3,12,2,6,6,6,0,0,8,0,0,102,42,10,9,15,0,72,51,41,1,0,0,0,0,0], +/* C V */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* C W */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* C X */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* C Y */ [5,1,20,0,0,0,1,0,0,0,0,3,0,2,2,4,0,3,2,9,0,0,0,0,0,0], +/* C Z */ [2,0,0,0,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]], +/* D A */ [[0,7,16,7,1,2,13,6,18,0,3,54,23,59,0,10,0,31,6,40,8,13,3,0,32,3], +/* D B */ [9,0,0,0,7,0,0,0,3,0,0,2,0,0,8,0,0,1,0,0,8,0,0,0,2,0], +/* D C */ [5,0,0,0,0,0,0,2,0,0,0,2,0,0,3,0,0,0,0,0,2,0,0,0,0,0], +/* D D */ [8,0,0,0,30,0,0,3,19,0,0,38,0,0,4,0,0,4,0,0,1,0,0,0,16,0], +/* D E */ [34,37,82,14,17,41,11,4,5,2,0,88,62,170,14,40,4,183,99,39,6,20,16,6,1,2], +/* D F */ [6,0,0,0,0,0,0,0,6,0,0,2,0,0,5,0,0,2,0,0,4,0,0,0,0,0], +/* D G */ [4,0,0,0,73,0,0,0,2,0,1,1,1,0,0,0,0,1,0,0,2,0,1,0,3,0], +/* D H */ [8,0,0,0,9,0,0,0,4,0,0,0,0,0,10,0,0,0,0,0,0,0,0,0,0,0], +/* D I */ [100,10,104,12,33,26,31,1,1,0,1,22,22,65,57,15,0,20,138,53,20,31,1,6,0,1], +/* D J */ [4,0,0,0,2,0,0,0,0,0,0,0,0,0,4,0,0,0,0,0,7,0,0,0,0,0], +/* D K */ [0,0,0,0,1,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* D L */ [9,0,0,0,79,0,0,0,12,0,0,0,0,0,7,0,0,0,0,0,1,0,0,0,3,0], +/* D M */ [13,0,0,0,3,0,0,0,21,0,0,0,0,0,11,0,0,0,0,0,1,0,0,0,0,0], +/* D N */ [7,0,0,0,9,0,0,0,3,0,0,0,0,0,1,0,0,0,0,6,0,0,0,0,0,0], +/* D O */ [1,5,21,10,6,3,20,1,3,0,0,30,38,54,17,7,0,39,11,10,30,5,54,5,1,3], +/* D P */ [6,0,0,0,1,0,0,1,3,0,0,1,0,0,7,0,0,1,0,0,0,0,0,0,0,0], +/* D Q */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,0,0,0,0,0], +/* D R */ [74,0,0,0,47,0,0,0,53,0,0,0,0,0,80,0,0,0,0,0,22,0,0,0,8,0], +/* D S */ [1,0,3,0,10,0,0,9,5,0,1,3,10,0,16,8,0,0,0,31,1,0,2,0,0,0], +/* D T */ [3,0,0,0,1,0,0,6,1,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0], +/* D U */ [10,7,52,2,5,3,4,0,2,0,1,33,14,15,5,11,1,19,15,8,1,0,0,0,0,1], +/* D V */ [3,0,0,0,13,0,0,0,7,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0], +/* D W */ [19,0,0,0,10,0,0,0,19,0,0,0,0,0,8,0,0,2,0,0,0,0,0,0,2,0], +/* D X */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* D Y */ [4,2,1,2,3,1,2,0,1,0,1,4,4,12,0,0,0,0,8,1,0,0,1,0,0,0], +/* D Z */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0]], +/* E A */ [[0,39,34,110,0,12,13,3,0,0,50,68,38,71,0,13,1,117,80,112,28,19,7,0,0,1], +/* E B */ [32,5,0,0,31,0,0,0,8,0,0,6,0,0,28,0,0,32,2,3,29,0,0,0,4,0], +/* E C */ [33,0,9,2,51,0,0,39,49,0,47,26,0,0,59,0,0,35,2,206,42,0,0,0,2,0], +/* E D */ [29,7,1,16,45,5,22,3,88,0,0,8,9,4,24,2,0,27,8,4,27,0,7,0,13,0], +/* E E */ [2,4,13,63,1,6,1,4,10,0,19,23,13,66,1,42,0,43,9,34,1,4,6,0,0,8], +/* E F */ [14,0,1,2,36,33,0,0,22,0,0,15,0,0,24,0,0,14,1,13,35,0,0,0,5,0], +/* E G */ [48,1,0,0,36,1,15,2,38,0,0,7,4,4,26,0,0,38,0,0,19,0,0,0,4,0], +/* E H */ [14,0,0,0,24,0,0,0,6,0,0,0,1,0,18,0,0,4,0,0,4,0,0,0,3,0], +/* E I */ [8,0,5,13,2,1,42,0,1,1,2,13,7,59,1,1,0,10,25,22,0,7,0,0,0,2], +/* E J */ [4,0,0,0,4,0,0,0,0,0,0,0,0,0,3,0,0,0,0,0,3,0,0,0,0,0], +/* E K */ [2,1,0,1,6,0,0,0,4,0,0,0,0,1,1,0,0,0,2,3,0,0,0,0,1,0], +/* E L */ [76,7,6,57,131,19,7,3,125,0,4,238,22,1,48,15,0,4,27,26,17,19,2,0,7,0], +/* E M */ [87,53,1,0,84,0,0,0,102,0,0,3,8,8,56,64,0,0,4,0,19,0,1,0,8,0], +/* E N */ [78,17,68,159,128,8,35,14,96,2,2,4,5,54,57,3,2,9,127,624,33,10,8,0,11,16], +/* E O */ [0,0,8,10,0,6,7,1,2,0,0,23,10,38,0,16,0,14,6,4,41,3,2,2,0,1], +/* E P */ [26,1,1,0,27,0,0,32,45,0,0,21,1,0,35,9,0,35,10,65,13,0,2,0,3,0], +/* E Q */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,59,0,0,0,0,0], +/* E R */ [217,57,66,22,190,41,70,13,200,3,14,40,134,117,113,42,2,123,167,135,23,58,22,1,123,1], +/* E S */ [17,7,74,6,58,1,3,25,82,0,3,6,17,5,34,52,7,0,222,278,18,2,1,0,6,0], +/* E T */ [78,3,19,0,129,4,0,93,105,0,1,3,2,2,50,1,0,73,5,113,17,0,4,0,32,4], +/* E U */ [0,4,7,6,1,0,4,0,0,0,2,3,17,4,0,15,0,46,20,18,0,2,1,0,0,0], +/* E V */ [29,0,0,0,121,0,0,0,56,0,0,0,0,0,26,0,0,2,1,0,2,2,0,0,3,1], +/* E W */ [33,4,3,4,16,2,0,5,24,0,0,3,3,3,23,2,0,3,15,4,0,0,1,0,2,0], +/* E X */ [29,0,43,0,20,0,0,14,21,0,0,0,0,0,15,78,1,0,0,72,12,0,0,1,2,0], +/* E Y */ [7,3,1,4,25,2,0,2,0,0,1,4,6,4,4,1,0,2,3,0,0,1,4,0,0,0], +/* E Z */ [1,0,0,0,9,0,0,0,1,0,0,0,0,0,4,0,0,1,0,0,1,1,0,0,2,3]], +/* F A */ [[1,10,39,5,2,1,1,3,18,0,2,35,10,27,0,0,0,36,13,18,10,0,2,3,4,1], +/* F B */ [2,0,0,0,1,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* F C */ [1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* F D */ [1,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0], +/* F E */ [18,5,24,6,12,0,2,0,6,0,1,25,6,18,2,0,0,114,17,15,4,2,2,0,1,0], +/* F F */ [10,2,0,0,51,0,0,2,45,0,0,21,4,0,13,0,0,9,7,0,7,0,0,0,8,0], +/* F G */ [1,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* F H */ [2,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* F I */ [9,9,58,18,42,7,11,0,0,0,0,29,2,53,0,0,0,40,41,18,0,2,0,10,0,3], +/* F J */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0], +/* F K */ [2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* F L */ [64,0,0,0,50,0,0,0,21,0,0,0,0,0,60,0,0,0,0,0,42,0,0,0,15,0], +/* F M */ [6,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* F N */ [0,0,0,0,0,0,0,0,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* F O */ [5,1,8,2,1,0,7,0,6,0,0,34,1,8,32,2,0,165,5,0,25,1,2,7,1,0], +/* F P */ [0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* F Q */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* F R */ [64,0,0,0,66,0,0,0,35,0,0,0,0,0,35,0,0,0,0,0,11,0,0,0,3,0], +/* F S */ [1,0,0,0,2,0,0,2,0,0,1,0,0,0,1,1,0,0,0,2,0,0,0,0,0,0], +/* F T */ [1,1,1,0,19,0,0,3,1,0,0,0,1,0,3,0,0,1,9,0,0,0,4,0,8,0], +/* F U */ [0,0,4,2,1,0,9,0,0,2,0,119,7,24,0,0,0,28,31,6,0,0,0,0,0,2], +/* F V */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* F W */ [1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* F X */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* F Y */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* F Z */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]], +/* G A */ [[0,20,5,11,3,2,11,3,13,0,0,68,24,60,1,5,0,63,23,68,15,8,5,0,2,5], +/* G B */ [4,0,0,0,1,0,0,0,3,0,0,0,0,0,5,0,0,0,0,0,0,0,0,0,0,0], +/* G C */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* G D */ [2,0,0,0,1,0,0,0,0,0,0,0,0,0,3,0,0,0,0,0,0,0,0,0,0,0], +/* G E */ [23,3,2,4,12,1,1,3,4,0,0,32,8,141,39,4,0,96,29,33,1,1,4,0,5,0], +/* G F */ [0,0,0,0,1,0,0,0,3,0,0,0,0,0,0,0,0,1,0,0,3,0,0,0,0,0], +/* G G */ [8,0,0,0,20,0,0,1,60,0,0,24,0,0,3,1,0,6,4,0,0,0,0,0,12,0], +/* G H */ [18,4,1,1,12,2,1,1,2,0,1,4,0,3,12,1,0,1,3,153,2,0,3,0,1,0], +/* G I */ [23,21,16,6,7,2,9,0,0,0,0,24,7,103,17,1,0,10,26,19,3,10,0,0,0,1], +/* G J */ [1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* G K */ [0,0,0,0,0,0,0,0,2,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0], +/* G L */ [49,0,0,0,73,0,0,0,25,0,0,0,0,0,38,0,0,0,0,0,13,0,0,0,17,0], +/* G M */ [23,0,0,0,12,0,0,0,1,0,0,0,0,0,0,0,0,0,0,1,3,0,0,0,1,0], +/* G N */ [26,1,0,0,28,0,0,0,20,0,0,0,0,0,26,2,0,0,0,1,7,0,0,0,0,0], +/* G O */ [6,4,3,16,6,1,10,1,5,0,0,22,1,49,20,3,0,34,12,23,16,7,5,0,1,0], +/* G P */ [0,0,0,0,1,0,0,0,3,0,0,2,0,0,2,0,0,0,0,0,0,0,0,0,0,0], +/* G Q */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* G R */ [216,0,0,0,97,0,0,0,43,0,0,0,0,0,50,0,0,0,0,0,14,0,0,0,3,0], +/* G S */ [2,2,0,0,0,0,0,2,2,0,1,1,0,0,2,1,0,0,0,18,0,0,1,0,0,0], +/* G T */ [2,0,0,0,0,0,0,8,3,0,0,0,0,0,17,0,0,1,0,0,0,0,0,0,0,0], +/* G U */ [28,1,1,0,49,1,1,0,41,0,0,26,15,24,2,0,0,14,22,6,0,0,0,0,3,1], +/* G V */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* G W */ [5,0,0,0,2,0,0,0,0,0,0,0,0,0,1,0,0,1,0,0,0,0,0,0,1,0], +/* G X */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* G Y */ [1,0,0,0,0,0,0,0,0,0,0,0,7,3,0,6,0,5,0,0,0,0,0,0,0,0], +/* G Z */ [2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]], +/* H A */ [[2,26,15,20,6,8,22,3,31,0,11,90,66,171,3,25,0,142,30,49,20,11,20,0,13,8], +/* H B */ [4,0,0,0,3,0,0,0,1,0,0,2,0,0,12,0,0,2,0,0,4,0,0,0,1,0], +/* H C */ [1,0,0,0,0,0,0,1,0,0,0,0,0,0,2,0,0,1,0,0,0,0,0,0,0,0], +/* H D */ [2,0,0,0,0,0,0,0,1,0,0,0,0,0,2,0,0,4,0,0,0,0,0,0,0,0], +/* H E */ [123,5,22,33,37,5,3,0,27,0,0,87,65,86,17,7,1,311,57,42,11,11,14,8,11,2], +/* H F */ [2,0,0,0,0,0,0,0,3,0,0,0,0,0,2,0,0,0,0,0,10,0,0,0,0,0], +/* H G */ [1,0,0,0,1,0,0,0,0,0,0,0,0,0,3,0,0,0,0,0,0,0,0,0,0,0], +/* H H */ [1,0,0,0,2,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0], +/* H I */ [22,22,56,15,23,6,19,0,0,1,1,73,20,79,17,41,0,36,53,39,3,11,0,0,0,6], +/* H J */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* H K */ [0,0,0,0,1,0,0,0,1,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0], +/* H L */ [5,0,0,0,11,0,0,0,8,0,0,0,0,0,22,0,0,1,0,0,1,0,0,0,1,0], +/* H M */ [21,0,0,0,15,0,0,0,6,0,0,0,1,0,7,0,0,0,2,0,1,0,0,0,0,0], +/* H N */ [3,0,0,0,8,0,0,0,9,0,0,0,0,1,3,0,0,0,4,0,2,0,0,0,0,0], +/* H O */ [13,18,13,25,17,5,13,0,7,1,4,101,62,62,44,29,0,130,45,33,81,8,28,0,6,2], +/* H P */ [3,0,0,0,0,0,0,0,2,0,0,1,0,0,1,0,0,0,0,0,1,0,0,0,0,0], +/* H Q */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0], +/* H R */ [20,0,0,0,23,0,0,0,40,0,0,1,0,0,72,0,0,0,0,0,13,0,0,0,3,0], +/* H S */ [3,0,1,0,0,0,0,2,1,0,0,0,0,0,3,0,0,0,0,5,0,0,0,0,0,0], +/* H T */ [3,0,2,1,21,9,1,7,5,0,0,1,4,3,4,1,0,2,7,1,1,0,3,0,6,0], +/* H U */ [3,13,7,6,3,5,12,1,0,0,0,7,37,26,0,3,0,37,24,15,0,0,0,2,2,1], +/* H V */ [0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* H W */ [17,0,0,0,5,0,0,2,5,0,0,0,0,0,9,0,0,0,0,0,0,0,0,0,0,0], +/* H X */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* H Y */ [5,1,1,39,1,0,3,0,1,0,0,13,9,0,0,25,0,9,29,9,0,0,0,1,0,0], +/* H Z */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]], +/* I A */ [[0,33,20,8,1,0,17,5,1,0,2,169,20,230,0,3,0,30,13,91,0,1,1,2,0,1], +/* I B */ [11,19,0,0,38,0,0,0,22,0,0,131,1,2,10,0,0,20,1,0,23,0,0,0,2,0], +/* I C */ [161,0,3,0,113,0,0,62,113,0,142,15,0,4,46,0,0,12,5,53,42,0,0,0,7,0], +/* I D */ [51,2,0,31,232,0,30,0,46,1,0,5,1,8,10,1,0,1,10,5,11,0,7,0,9,0], +/* I E */ [0,1,17,6,1,16,11,1,0,0,1,52,4,70,0,1,0,66,18,50,7,17,6,0,0,2], +/* I F */ [7,0,0,0,31,45,0,0,27,0,0,9,0,1,10,0,0,2,0,24,10,0,0,0,71,0], +/* I G */ [48,0,0,0,41,0,30,147,30,0,0,4,15,57,20,1,0,23,3,1,15,0,1,0,2,2], +/* I H */ [1,0,0,0,2,0,0,0,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* I I */ [1,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* I J */ [3,0,0,0,2,0,0,0,1,0,0,0,0,0,2,0,0,0,0,0,1,0,0,0,0,0], +/* I K */ [6,0,0,0,17,0,0,0,3,0,1,0,0,0,3,0,0,0,0,1,2,0,0,0,1,0], +/* I L */ [60,10,6,36,106,6,5,7,90,0,13,253,14,0,24,1,0,1,10,31,6,6,5,0,10,0], +/* I M */ [76,26,0,0,94,1,0,1,53,0,0,1,38,1,30,133,0,1,8,0,17,0,0,0,2,0], +/* I N */ [212,12,143,168,396,83,435,26,94,8,43,9,6,44,70,3,10,2,139,205,35,46,4,4,15,1], +/* I O */ [2,2,20,10,1,0,9,0,0,0,0,28,12,604,0,8,0,25,13,24,139,3,2,3,0,1], +/* I P */ [20,5,0,0,26,2,0,16,16,1,0,33,6,0,13,39,0,5,19,28,5,0,1,0,1,0], +/* I Q */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,36,0,0,0,0,0], +/* I R */ [41,2,39,24,106,7,9,0,19,0,11,20,24,1,24,8,0,39,11,31,3,5,8,0,10,0], +/* I S */ [35,5,71,4,110,4,2,189,56,1,13,12,93,5,55,33,3,6,85,271,4,1,1,0,8,0], +/* I T */ [136,1,34,1,184,5,0,77,158,0,1,4,6,5,70,1,0,31,2,105,72,0,1,0,142,19], +/* I U */ [0,0,1,0,0,0,0,0,0,0,0,1,121,1,0,0,0,1,19,0,0,0,0,0,0,0], +/* I V */ [57,0,0,0,292,0,0,0,37,0,0,0,0,0,12,0,0,1,0,0,3,0,0,0,2,0], +/* I W */ [3,0,0,0,0,0,0,0,2,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0], +/* I X */ [1,0,0,0,2,1,1,0,3,0,0,0,0,0,4,0,0,0,0,9,1,0,0,0,1,0], +/* I Y */ [1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* I Z */ [9,0,0,0,13,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,1,1,0,0,0,16]], +/* J A */ [[0,2,32,1,1,0,3,3,2,0,3,1,8,17,0,2,0,5,2,0,2,3,2,1,1,2], +/* J B */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* J C */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* J D */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* J E */ [4,0,24,1,1,3,0,1,0,2,0,2,0,6,2,0,0,11,9,5,0,0,6,0,0,0], +/* J F */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* J G */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* J H */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* J I */ [0,1,0,0,0,1,4,0,0,0,0,2,4,3,0,0,0,0,0,4,0,1,0,0,0,0], +/* J J */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* J K */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* J L */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* J M */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* J N */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* J O */ [4,2,6,0,3,0,3,12,10,0,1,6,0,5,0,0,0,10,10,1,13,4,2,0,7,0], +/* J P */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* J Q */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* J R */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* J S */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* J T */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* J U */ [3,3,0,19,0,0,8,0,2,2,2,8,5,24,0,1,0,15,9,5,0,1,0,2,0,0], +/* J V */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* J W */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* J X */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* J Y */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* J Z */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]], +/* K A */ [[0,3,0,6,1,2,8,2,1,1,1,9,4,13,2,3,0,18,4,17,2,1,2,1,5,2], +/* K B */ [3,0,0,0,3,0,0,0,2,0,0,0,0,0,11,0,0,1,0,0,1,0,0,0,0,0], +/* K C */ [2,0,0,0,0,0,0,1,1,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0], +/* K D */ [3,0,0,0,1,0,0,0,0,0,0,0,0,0,2,0,0,1,0,0,0,0,0,0,0,0], +/* K E */ [4,3,0,7,28,3,3,2,1,0,0,20,5,55,3,3,0,59,18,56,2,1,4,0,27,0], +/* K F */ [1,0,0,0,1,0,0,0,1,0,0,0,0,0,3,0,0,0,0,0,3,0,0,0,0,0], +/* K G */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,1,0,0,0,0,0,0,0,0], +/* K H */ [9,0,0,0,2,0,0,0,0,0,0,0,1,0,8,0,0,1,0,1,0,0,0,0,0,0], +/* K I */ [5,2,3,9,15,1,1,0,0,0,1,10,10,87,2,4,0,11,15,13,0,2,2,0,0,0], +/* K J */ [2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* K K */ [1,0,0,0,1,0,0,0,0,0,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0], +/* K L */ [15,0,0,0,46,0,0,0,13,0,0,0,0,0,3,0,0,0,0,0,1,0,0,0,2,0], +/* K M */ [13,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* K N */ [5,0,0,0,11,0,0,0,10,0,0,0,0,0,24,0,0,0,0,0,8,0,0,0,0,0], +/* K O */ [1,1,2,3,2,4,0,2,1,0,1,3,1,7,1,2,0,6,2,1,7,4,5,2,0,0], +/* K P */ [2,0,0,0,0,0,0,0,4,0,0,4,0,0,5,0,0,0,0,0,0,0,0,0,0,0], +/* K Q */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* K R */ [10,0,0,0,3,0,0,0,3,0,0,0,0,0,6,0,0,0,0,0,5,0,0,0,2,0], +/* K S */ [2,2,1,0,1,0,1,9,5,0,1,0,4,0,8,3,0,0,0,11,4,0,1,0,1,0], +/* K T */ [3,0,0,0,0,0,0,2,3,0,0,0,0,0,5,0,0,2,0,0,0,0,0,0,0,0], +/* K U */ [0,0,0,2,0,0,0,1,0,0,0,5,1,1,0,8,0,2,1,1,0,0,1,0,1,0], +/* K V */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* K W */ [9,0,0,0,4,0,0,1,2,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,0], +/* K X */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* K Y */ [2,0,0,0,1,0,0,1,0,1,0,4,0,0,2,0,0,2,1,0,1,0,3,0,0,0], +/* K Z */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]], +/* L A */ [[1,46,84,43,3,2,46,9,52,0,10,3,64,242,4,23,1,157,92,210,45,21,23,9,42,11], +/* L B */ [12,0,0,0,17,0,0,0,3,0,0,2,0,0,13,0,0,4,0,0,4,0,0,0,2,0], +/* L C */ [9,0,0,0,6,0,0,12,4,0,0,1,1,0,19,0,0,2,0,1,7,0,0,0,2,0], +/* L D */ [2,3,2,0,41,4,0,1,16,0,0,1,2,3,13,1,0,8,9,2,3,0,5,0,3,0], +/* L E */ [94,25,75,44,36,13,55,9,26,1,1,9,55,121,22,22,0,77,84,115,12,29,14,30,75,1], +/* L F */ [9,1,0,0,4,1,1,1,12,0,0,1,0,0,7,0,0,8,1,2,8,0,1,0,0,0], +/* L G */ [16,0,0,0,12,0,0,0,10,0,0,0,0,0,6,0,0,6,0,0,0,0,0,0,0,0], +/* L H */ [7,0,0,0,6,0,0,0,2,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,0], +/* L I */ [82,33,140,26,43,37,73,0,0,1,6,11,46,238,50,40,13,5,90,127,12,36,0,3,0,7], +/* L J */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0], +/* L K */ [7,0,0,0,4,0,0,3,9,0,0,2,0,1,2,0,0,0,3,0,0,0,3,0,8,0], +/* L L */ [128,12,2,4,169,7,2,4,152,1,0,0,7,0,100,2,0,1,10,2,41,0,7,0,53,0], +/* L M */ [27,0,0,2,11,0,0,2,9,0,0,0,1,0,13,0,0,0,4,0,3,0,0,0,3,0], +/* L N */ [0,0,0,0,2,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,3,0,0,0,0,0], +/* L O */ [23,23,65,15,7,4,132,3,32,0,2,7,29,69,50,36,11,74,33,53,66,16,80,1,12,1], +/* L P */ [11,0,0,0,3,1,0,21,5,0,0,0,1,0,6,0,0,3,1,4,0,0,0,0,1,0], +/* L Q */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* L R */ [2,0,0,0,1,0,0,0,0,0,0,0,0,0,5,0,0,0,0,0,2,0,0,0,6,0], +/* L S */ [7,1,0,0,16,0,0,8,23,0,1,0,1,0,20,3,0,0,1,23,0,0,1,0,2,0], +/* L T */ [22,1,0,0,23,0,0,14,34,0,0,0,2,0,23,0,0,9,3,0,8,1,1,0,18,5], +/* L U */ [5,17,26,18,31,5,13,0,5,2,4,8,68,31,15,5,0,21,68,56,0,4,0,13,0,1], +/* L V */ [19,0,0,1,46,0,0,0,9,0,0,0,0,0,3,0,0,0,0,0,0,0,0,0,0,0], +/* L W */ [8,0,0,0,2,0,0,1,2,0,0,0,0,0,9,0,0,0,0,0,0,0,0,0,1,0], +/* L X */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* L Y */ [2,4,12,2,2,2,3,7,2,0,1,3,13,11,2,11,0,2,31,15,1,0,4,0,0,0], +/* L Z */ [2,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]], +/* M A */ [[0,10,59,34,3,0,57,7,31,3,25,104,6,326,2,4,0,144,49,192,10,2,3,11,14,7], +/* M B */ [31,1,0,1,44,0,0,0,32,0,0,31,0,1,27,1,0,32,1,0,21,0,0,0,0,0], +/* M C */ [3,1,17,6,2,2,9,3,5,0,9,3,3,4,2,1,0,0,0,0,0,0,0,0,0,0], +/* M D */ [0,0,0,0,2,0,0,0,0,0,0,0,0,0,1,0,0,1,0,0,0,0,0,0,0,0], +/* M E */ [30,6,8,45,3,2,14,1,4,0,1,51,19,283,10,4,0,125,39,128,0,2,9,3,4,1], +/* M F */ [0,0,0,0,3,0,0,0,3,0,0,2,0,0,4,0,0,0,0,0,4,0,0,0,0,0], +/* M G */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* M H */ [0,0,0,0,3,0,0,0,0,0,0,0,0,0,4,0,0,0,0,0,1,0,0,0,0,0], +/* M I */ [19,0,93,54,8,2,19,0,0,1,2,76,9,194,4,0,1,21,96,109,10,0,0,5,0,1], +/* M J */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* M K */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* M L */ [1,0,0,0,3,0,0,0,6,0,0,0,0,0,3,0,0,0,0,0,0,0,0,0,0,0], +/* M M */ [40,0,0,0,46,0,0,0,33,0,0,0,0,0,32,0,0,0,0,0,17,0,0,0,12,0], +/* M N */ [12,0,0,0,4,0,0,0,10,0,0,0,0,0,3,0,0,0,0,0,1,0,0,0,1,0], +/* M O */ [4,10,13,28,4,1,14,3,11,0,6,47,10,168,16,3,0,107,40,45,56,8,1,1,1,2], +/* M P */ [52,3,0,0,71,1,1,26,18,0,4,71,0,0,50,0,0,41,9,43,19,0,0,0,7,0], +/* M Q */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0], +/* M R */ [2,0,0,0,0,0,0,0,0,0,0,0,0,0,3,0,0,0,1,0,0,0,0,0,0,0], +/* M S */ [0,1,2,1,5,1,0,2,3,0,1,0,2,0,8,2,0,0,1,10,1,0,0,0,2,0], +/* M T */ [0,0,0,0,1,0,0,1,0,0,0,0,0,0,1,0,0,1,0,0,0,0,0,0,0,0], +/* M U */ [0,0,7,11,6,3,6,0,2,0,2,55,11,29,2,1,0,18,53,30,0,0,0,0,0,3], +/* M V */ [0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* M W */ [2,0,0,0,2,0,0,0,1,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0], +/* M X */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* M Y */ [0,0,11,0,5,0,1,0,0,0,0,1,0,2,7,0,0,7,7,4,0,0,0,0,0,0], +/* M Z */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]], +/* N A */ [[2,24,33,23,6,3,30,6,20,0,9,115,29,59,2,31,0,94,28,159,19,10,5,0,1,5], +/* N B */ [5,0,1,0,20,0,0,0,1,0,0,4,0,0,7,0,0,4,1,0,10,0,0,0,0,0], +/* N C */ [25,0,0,0,190,0,0,87,51,0,1,18,0,0,62,0,0,16,0,36,21,0,0,0,8,0], +/* N D */ [75,11,4,1,162,6,3,7,102,1,1,22,10,2,57,9,2,46,30,4,37,0,11,0,20,0], +/* N E */ [34,12,36,12,29,17,16,4,14,0,0,45,16,20,25,8,6,88,80,84,32,12,37,18,45,3], +/* N F */ [15,0,0,0,30,0,0,0,38,0,0,23,0,0,26,0,0,10,0,0,19,0,0,0,0,0], +/* N G */ [22,8,0,3,114,6,0,15,18,0,3,51,5,0,20,2,0,24,24,28,38,0,2,0,9,0], +/* N H */ [18,0,0,0,16,0,0,0,6,0,0,0,0,0,15,0,0,0,0,0,2,0,0,0,3,0], +/* N I */ [90,9,148,14,33,27,35,4,1,0,5,12,25,44,26,21,7,4,87,94,29,11,0,4,0,4], +/* N J */ [2,0,0,0,3,0,0,0,0,0,0,0,0,0,4,0,0,0,0,0,13,0,0,0,0,0], +/* N K */ [6,0,1,0,22,4,1,1,10,0,0,12,2,0,1,1,0,2,2,3,0,0,0,0,9,0], +/* N L */ [9,0,0,0,8,0,0,0,5,0,0,0,0,0,5,0,0,0,0,0,0,0,0,0,1,0], +/* N M */ [8,0,0,0,5,0,0,0,2,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,0], +/* N N */ [39,0,0,0,74,0,0,0,52,0,1,0,0,0,23,0,0,0,1,0,14,0,1,0,25,0], +/* N O */ [4,18,21,10,4,4,15,0,11,0,0,30,60,34,11,11,0,80,32,47,52,18,24,7,2,2], +/* N P */ [0,0,0,0,1,0,0,0,1,0,0,4,0,0,6,0,0,0,0,0,2,0,0,0,0,0], +/* N Q */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,22,0,0,0,0,0], +/* N R */ [3,0,1,0,1,0,0,0,6,0,0,0,0,0,6,0,0,0,0,0,3,0,0,0,6,0], +/* N S */ [26,4,23,2,73,17,3,12,96,0,5,8,13,0,60,25,0,1,3,79,39,4,4,0,5,0], +/* N T */ [143,1,1,1,175,2,2,64,209,0,0,13,3,1,65,1,0,114,3,0,32,0,2,0,21,1], +/* N U */ [12,6,16,6,11,3,6,0,5,0,1,15,35,9,6,3,0,9,25,31,1,0,0,0,0,1], +/* N V */ [15,0,0,0,43,0,0,0,20,0,0,0,0,0,17,0,0,0,0,0,4,0,0,0,1,0], +/* N W */ [12,0,0,0,3,0,0,2,4,0,0,0,0,0,6,0,0,1,0,0,0,0,0,0,0,0], +/* N X */ [0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0], +/* N Y */ [5,3,1,1,0,0,0,1,0,0,0,7,14,0,4,1,1,1,3,1,1,1,2,1,0,0], +/* N Z */ [10,0,0,0,5,0,0,0,5,0,0,0,0,0,1,0,0,0,0,0,0,0,1,0,5,0]], +/* O A */ [[1,0,20,30,0,2,5,2,0,0,9,9,8,18,0,4,1,51,13,44,1,1,0,2,0,0], +/* O B */ [17,24,2,2,28,2,0,1,32,4,0,19,0,1,16,0,0,5,26,3,8,3,1,0,2,0], +/* O C */ [50,0,28,0,38,0,0,47,26,0,129,14,0,0,33,0,0,25,0,34,20,0,0,0,8,0], +/* O D */ [17,3,3,15,59,3,13,4,47,0,1,13,2,1,22,3,0,8,11,0,21,0,8,0,35,0], +/* O E */ [0,6,1,7,0,3,0,1,6,0,1,10,3,13,1,0,1,10,15,6,2,7,0,3,1,0], +/* O F */ [7,0,0,0,4,63,0,0,10,0,0,4,1,0,6,0,0,1,0,15,4,0,0,0,1,0], +/* O G */ [34,2,0,1,44,1,22,3,15,1,0,11,3,11,7,0,0,80,1,2,18,0,1,0,83,0], +/* O H */ [10,0,0,0,8,0,0,0,6,0,0,1,5,9,5,0,0,2,0,0,0,0,0,0,1,0], +/* O I */ [3,1,12,53,1,1,2,0,0,0,1,27,0,51,0,0,0,11,39,8,0,0,0,1,0,0], +/* O J */ [1,0,0,0,5,0,0,0,1,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0], +/* O K */ [5,2,1,0,48,0,0,1,7,0,1,4,0,0,3,1,0,0,5,0,3,0,1,0,6,0], +/* O L */ [71,4,6,83,111,8,5,3,121,0,14,124,16,1,132,6,0,1,18,24,43,16,2,0,46,1], +/* O M */ [89,50,1,0,174,5,0,1,76,0,0,2,64,7,56,125,1,1,4,0,4,0,2,0,22,0], +/* O N */ [129,3,64,82,181,52,86,3,124,10,11,7,3,46,75,1,6,10,107,149,8,38,9,1,54,5], +/* O O */ [0,2,4,92,0,22,4,1,0,0,68,42,42,44,0,19,0,21,21,68,0,3,0,0,0,2], +/* O P */ [28,1,2,0,71,0,2,82,32,1,3,16,1,1,45,29,0,17,14,21,10,0,2,0,19,0], +/* O Q */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,14,0,0,0,0,0], +/* O R */ [122,26,31,96,138,7,34,2,143,0,61,8,85,76,61,59,1,58,46,211,11,4,9,0,116,1], +/* O S */ [31,4,24,0,107,0,3,18,102,0,2,7,9,1,18,42,2,0,63,127,5,1,2,0,8,0], +/* O T */ [45,7,11,0,64,2,1,88,63,0,0,10,3,1,42,4,0,17,7,63,9,0,3,0,11,0], +/* O U */ [3,11,17,13,3,3,62,1,6,0,0,32,1,137,0,11,1,86,445,103,0,7,0,1,0,2], +/* O V */ [26,0,0,0,109,0,0,0,27,0,1,0,0,0,7,0,0,0,0,0,0,0,0,0,2,0], +/* O W */ [18,14,2,13,48,6,0,8,8,0,1,28,7,83,1,8,0,5,13,2,2,0,1,0,4,1], +/* O X */ [2,1,3,0,5,1,1,3,26,0,0,0,0,1,1,0,0,0,0,1,0,1,1,0,14,0], +/* O Y */ [15,1,4,6,3,1,0,0,1,0,0,3,0,1,4,1,0,1,2,1,0,0,0,0,0,0], +/* O Z */ [2,0,0,0,9,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,3,1]], +/* P A */ [[0,8,38,11,1,0,18,0,17,0,2,50,5,73,1,23,1,176,50,101,18,5,7,1,10,2], +/* P B */ [3,0,0,0,3,0,0,0,0,0,0,1,0,0,6,0,0,2,1,0,3,0,0,0,0,0], +/* P C */ [0,0,0,0,0,0,0,1,0,0,0,0,0,0,3,0,0,0,0,0,0,0,0,0,0,0], +/* P D */ [1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,2,0,0,0,0,0,0,0,0], +/* P E */ [51,1,62,34,19,4,8,0,3,1,2,47,2,108,4,10,0,292,22,50,3,1,8,2,2,4], +/* P F */ [0,0,0,0,1,0,0,0,2,0,0,1,0,0,0,0,0,1,0,0,3,0,0,0,0,0], +/* P G */ [2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0], +/* P H */ [56,0,0,2,88,0,0,0,76,0,0,3,0,1,97,0,0,13,1,3,5,0,0,0,79,0], +/* P I */ [21,0,74,25,33,1,19,0,0,0,6,27,3,74,12,11,2,37,27,57,3,2,0,2,0,2], +/* P J */ [1,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* P K */ [0,0,0,0,2,0,0,0,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* P L */ [150,0,0,0,121,0,0,0,59,0,0,0,0,0,33,0,0,0,0,0,29,0,0,0,11,0], +/* P M */ [6,0,0,0,2,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,1,0,0,0,0,0], +/* P N */ [0,0,0,0,4,0,0,0,0,0,0,0,0,0,3,0,0,0,0,0,0,0,0,0,0,0], +/* P O */ [2,1,19,10,12,2,7,0,31,0,12,111,14,55,23,17,0,97,126,52,20,3,13,3,2,0], +/* P P */ [16,0,0,0,48,0,0,1,20,0,0,32,1,0,25,0,0,32,3,0,1,0,0,0,16,0], +/* P Q */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* P R */ [39,0,0,0,166,0,0,0,104,0,0,0,0,0,273,0,0,0,0,0,12,0,0,0,1,0], +/* P S */ [4,1,3,0,17,0,0,5,22,0,1,1,2,0,13,0,0,0,0,14,6,0,1,0,35,0], +/* P T */ [16,0,1,0,9,0,0,3,107,0,0,0,0,0,33,0,0,3,0,0,19,0,0,0,4,0], +/* P U */ [1,8,4,8,3,6,4,0,1,0,1,41,8,22,0,9,0,39,18,28,0,0,0,0,0,1], +/* P V */ [0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* P W */ [3,0,0,0,0,0,0,0,2,0,0,0,0,0,1,0,0,1,0,0,0,0,0,0,0,0], +/* P X */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* P Y */ [1,2,0,0,0,0,3,0,1,0,1,3,0,0,1,0,0,20,0,3,0,0,1,0,0,0], +/* P Z */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]], +/* Q A */ [[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0], +/* Q B */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* Q C */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* Q D */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* Q E */ [0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* Q F */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* Q G */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* Q H */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* Q I */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* Q J */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* Q K */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* Q L */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* Q M */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* Q N */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* Q O */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* Q P */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* Q Q */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* Q R */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* Q S */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* Q T */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* Q U */ [110,0,0,0,100,0,0,0,128,0,0,0,0,0,13,0,0,0,0,0,0,0,0,0,3,0], +/* Q V */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* Q W */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* Q X */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* Q Y */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* Q Z */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]], +/* R A */ [[0,72,130,95,8,35,73,14,85,3,10,121,95,313,2,119,1,26,66,277,19,45,28,2,28,13], +/* R B */ [32,0,0,0,26,0,0,0,35,0,0,4,0,0,44,0,0,3,1,0,9,0,0,0,5,0], +/* R C */ [18,0,2,0,47,0,0,86,25,0,3,11,0,0,13,0,0,1,2,7,38,0,0,0,4,0], +/* R D */ [22,5,1,0,26,1,0,4,42,0,0,4,0,2,17,1,0,5,9,4,3,0,4,0,7,0], +/* R E */ [166,26,106,99,114,52,55,20,25,4,4,60,69,143,20,72,8,11,257,119,14,56,34,7,23,2], +/* R F */ [11,0,0,0,15,1,0,0,9,0,0,7,0,0,8,0,0,4,0,0,12,0,0,0,0,0], +/* R G */ [26,0,0,0,63,0,0,5,25,0,0,11,1,0,18,0,0,2,2,0,13,0,0,0,11,0], +/* R H */ [11,0,0,0,19,0,0,0,5,0,0,0,0,0,18,0,0,0,0,0,2,0,0,0,3,0], +/* R I */ [182,54,210,87,79,38,65,1,0,1,6,49,65,166,82,61,1,0,151,141,29,44,1,6,1,10], +/* R J */ [0,0,0,0,3,0,0,0,0,0,0,0,0,0,3,0,0,0,0,0,2,0,0,0,0,0], +/* R K */ [4,2,0,1,19,0,0,3,9,0,0,6,3,2,5,3,0,1,10,2,0,0,1,0,6,0], +/* R L */ [24,2,0,4,28,0,0,0,36,0,0,0,0,0,14,1,0,0,2,1,2,0,1,0,8,0], +/* R M */ [97,1,2,0,29,2,0,3,65,0,0,2,0,0,39,1,0,0,1,1,10,0,1,0,5,0], +/* R N */ [53,5,0,0,50,4,0,3,29,0,1,0,6,0,16,1,0,0,9,5,7,0,2,0,4,0], +/* R O */ [46,40,79,40,18,22,56,4,32,5,10,76,90,167,84,127,2,14,127,74,127,42,63,17,15,3], +/* R P */ [10,0,0,0,21,0,0,33,10,0,0,5,1,0,25,0,0,12,8,8,5,0,0,0,1,0], +/* R Q */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,10,0,0,0,0,0], +/* R R */ [53,0,0,0,92,0,0,5,85,0,0,0,0,0,47,0,0,0,0,0,14,0,0,0,60,0], +/* R S */ [26,2,2,2,84,1,0,16,44,0,4,2,3,1,43,12,1,0,0,32,14,1,2,0,2,0], +/* R T */ [39,2,2,0,61,5,3,101,99,0,0,11,7,3,32,0,0,17,12,1,27,0,2,0,24,7], +/* R U */ [5,21,30,31,15,6,12,0,18,0,0,10,46,41,1,28,0,3,83,22,0,1,1,1,0,1], +/* R V */ [31,0,0,0,37,0,0,0,28,0,0,0,0,0,5,0,0,0,0,0,1,0,0,0,2,0], +/* R W */ [15,0,0,0,6,0,0,0,12,0,0,0,0,0,15,0,0,0,0,0,0,0,0,0,0,0], +/* R X */ [0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* R Y */ [5,3,3,5,3,0,1,0,0,0,0,10,11,4,12,16,0,0,9,4,0,0,2,0,0,0], +/* R Z */ [2,0,0,0,1,0,0,0,1,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0]], +/* S A */ [[2,44,23,16,1,10,21,4,16,1,7,80,17,89,1,10,0,36,10,43,22,10,13,5,7,0], +/* S B */ [9,0,0,0,4,0,0,0,2,0,0,0,0,0,6,0,0,2,0,0,18,0,0,0,3,0], +/* S C */ [81,0,0,0,65,0,1,78,37,0,0,5,1,0,88,0,0,92,0,0,40,0,0,0,3,0], +/* S D */ [11,0,0,0,0,0,0,0,1,0,0,0,0,0,2,0,0,2,0,0,2,0,0,0,0,0], +/* S E */ [38,14,47,18,33,7,8,3,11,0,1,63,39,101,5,28,14,83,28,41,12,19,15,15,19,1], +/* S F */ [3,0,0,0,7,0,0,0,5,0,0,0,0,0,7,0,0,0,0,0,6,0,0,0,1,0], +/* S G */ [0,0,0,0,2,0,0,0,2,0,0,0,0,0,2,0,0,5,1,0,2,0,0,0,0,0], +/* S H */ [97,9,1,0,79,3,0,0,75,0,1,4,16,3,81,2,0,27,0,1,20,1,6,0,17,0], +/* S I */ [55,56,44,80,28,15,38,0,0,0,2,50,40,78,148,7,1,7,99,89,9,76,0,8,0,3], +/* S J */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0], +/* S K */ [9,0,0,0,24,0,0,0,35,0,0,0,2,0,3,0,0,1,0,0,5,0,0,0,23,0], +/* S L */ [42,0,0,0,35,0,0,0,29,0,0,1,0,0,29,0,0,0,0,0,13,0,0,0,2,0], +/* S M */ [57,0,0,0,30,0,0,0,31,0,0,0,0,0,25,0,0,0,0,0,14,0,0,0,2,0], +/* S N */ [21,0,0,0,12,0,0,0,12,0,0,0,0,0,19,0,0,0,0,4,6,0,0,0,2,0], +/* S O */ [6,4,26,12,6,10,4,1,8,1,0,67,65,190,8,21,0,71,0,11,34,6,3,0,3,1], +/* S P */ [63,1,0,0,116,0,0,41,82,0,0,24,0,0,69,0,0,34,1,0,16,0,0,0,3,0], +/* S Q */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,52,0,0,0,0,0], +/* S R */ [4,0,0,0,1,0,0,0,1,0,0,0,0,0,2,0,0,0,1,0,3,0,0,0,0,0], +/* S S */ [50,3,2,0,77,3,0,4,151,0,0,5,11,1,42,2,0,4,0,4,17,0,13,0,19,0], +/* S T */ [258,6,4,1,291,9,1,11,240,1,0,25,12,2,205,6,0,255,3,0,58,2,7,0,36,0], +/* S U */ [14,38,17,6,7,11,6,0,11,0,0,39,35,37,1,42,0,71,30,4,0,0,0,0,0,4], +/* S V */ [0,0,0,0,5,0,0,0,6,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0], +/* S W */ [37,0,0,0,31,0,0,0,28,0,0,0,0,0,21,0,0,2,0,0,2,0,0,0,0,0], +/* S X */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* S Y */ [0,2,32,1,1,0,1,0,0,0,1,18,19,30,0,2,0,9,5,1,0,0,0,0,0,1], +/* S Z */ [0,0,0,0,0,0,0,0,1,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0]], +/* T A */ [[0,74,44,8,3,9,45,8,68,0,15,130,36,181,1,23,0,128,22,185,13,11,9,13,4,0], +/* T B */ [7,0,0,0,4,0,0,0,4,0,0,0,0,0,6,0,0,3,0,0,3,0,0,0,0,0], +/* T C */ [5,0,0,0,0,0,0,112,0,0,0,2,0,0,5,0,0,1,0,0,1,0,0,0,1,0], +/* T D */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,0,0,1,0,0,0,0,0,0,0,0], +/* T E */ [52,9,29,37,66,9,17,6,16,0,2,65,49,185,18,20,0,588,61,23,9,9,9,16,1,0], +/* T F */ [6,0,0,0,1,0,0,0,5,0,0,1,0,0,6,0,0,1,0,0,24,0,0,0,0,0], +/* T G */ [4,0,0,0,2,0,0,0,0,0,0,0,0,0,2,0,0,1,0,0,1,0,0,0,0,0], +/* T H */ [68,6,1,5,274,8,1,2,62,0,1,9,13,3,90,4,1,61,8,2,31,0,16,0,49,0], +/* T I */ [99,35,342,16,35,45,34,0,0,0,3,67,75,183,419,28,9,18,75,88,9,128,0,0,0,2], +/* T J */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* T K */ [2,0,0,0,1,0,0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0], +/* T L */ [18,0,0,0,102,0,0,0,5,0,0,2,0,0,3,0,0,0,0,0,2,0,0,0,3,0], +/* T M */ [25,0,0,0,8,0,0,0,3,0,0,0,0,0,11,0,0,0,0,0,3,0,0,0,0,0], +/* T N */ [3,0,0,0,9,0,0,0,5,0,0,0,0,0,2,0,0,0,0,4,1,0,0,0,0,0], +/* T O */ [5,6,34,11,8,7,26,0,14,0,9,38,65,238,26,56,0,319,19,16,36,3,36,7,3,2], +/* T P */ [2,0,0,0,1,0,0,0,1,0,0,2,0,0,3,0,0,5,0,0,0,0,0,0,0,0], +/* T Q */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* T R */ [315,0,0,0,98,0,0,0,246,0,0,0,0,0,201,0,0,0,0,0,68,0,1,0,64,0], +/* T S */ [2,2,2,1,10,2,0,3,4,0,1,0,13,0,9,3,0,0,0,8,5,2,5,0,3,0], +/* T T */ [44,0,0,0,154,1,1,2,53,0,1,45,0,0,33,0,0,10,8,0,4,1,0,0,25,0], +/* T U */ [41,14,9,41,8,5,4,0,10,0,0,19,30,29,13,10,0,159,35,22,0,0,0,1,1,0], +/* T V */ [3,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* T W */ [14,0,0,0,12,0,0,1,23,0,0,0,0,0,15,0,0,0,0,0,2,0,0,1,0,0], +/* T X */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* T Y */ [2,1,2,0,0,0,1,0,1,0,0,14,2,0,0,34,0,14,3,0,0,0,2,1,0,0], +/* T Z */ [1,0,0,0,5,0,1,0,2,0,0,1,1,0,1,1,0,1,1,0,0,0,0,0,0,0]], +/* U A */ [[0,4,7,21,0,1,5,1,4,0,5,51,2,26,0,1,0,48,9,37,0,2,4,0,3,0], +/* U B */ [8,18,0,1,20,0,0,2,18,2,0,23,5,0,2,1,0,10,15,8,7,2,0,0,1,0], +/* U C */ [10,0,14,0,23,0,0,31,29,0,55,16,0,0,7,0,0,9,1,47,5,0,0,0,2,0], +/* U D */ [17,1,0,24,67,0,18,0,39,0,0,4,0,0,8,0,0,1,10,0,2,0,2,0,7,1], +/* U E */ [6,9,0,1,5,5,4,1,0,1,0,21,1,33,1,1,0,19,22,15,2,0,0,0,3,6], +/* U F */ [1,0,0,0,0,58,0,0,0,0,0,1,1,0,1,0,0,0,0,3,1,0,0,0,0,0], +/* U G */ [19,1,0,0,21,0,34,80,3,0,0,4,2,2,6,0,0,1,1,0,11,0,0,0,0,0], +/* U H */ [3,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0], +/* U I */ [3,2,14,14,6,0,1,0,0,0,0,32,0,31,1,8,0,19,44,64,1,4,0,2,0,3], +/* U J */ [1,0,0,0,0,0,0,0,2,0,0,0,0,0,1,0,0,0,0,0,3,0,0,0,0,0], +/* U K */ [1,0,0,1,12,0,0,0,3,0,1,0,0,0,1,0,0,2,0,0,0,0,0,0,0,0], +/* U L */ [136,4,11,11,46,14,7,0,35,0,10,67,5,2,23,16,0,1,24,73,16,3,1,0,5,1], +/* U M */ [22,52,3,1,51,5,0,1,32,0,0,2,28,11,8,48,1,0,8,1,6,2,0,0,0,0], +/* U N */ [21,6,73,131,25,5,46,2,55,0,33,4,2,13,4,2,0,2,15,82,1,0,2,0,5,0], +/* U O */ [0,0,0,1,0,0,0,0,3,0,0,2,0,3,0,2,0,16,3,5,29,0,0,0,2,0], +/* U P */ [4,4,1,2,31,1,1,14,10,0,1,13,1,0,8,24,0,13,13,24,2,0,2,0,2,0], +/* U Q */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0,0,0,0], +/* U R */ [75,27,21,17,149,8,60,1,66,2,11,17,11,55,28,15,1,51,43,43,9,15,3,0,28,1], +/* U S */ [31,5,29,2,105,0,1,53,64,0,17,3,0,1,8,12,1,0,34,115,6,0,0,0,4,0], +/* U T */ [45,1,14,1,69,0,1,55,77,0,0,8,3,3,49,0,0,13,7,51,11,0,2,0,6,2], +/* U U */ [0,0,0,0,0,0,0,0,0,0,0,0,3,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* U V */ [0,0,0,0,8,0,0,0,5,0,0,0,0,0,0,0,0,3,0,0,0,0,0,0,0,0], +/* U W */ [2,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* U X */ [0,0,0,0,4,0,0,0,2,0,0,1,0,0,1,0,0,0,0,5,4,0,0,0,0,0], +/* U Y */ [1,0,0,0,1,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,1,1,0,0,0,0], +/* U Z */ [2,0,0,0,4,0,0,0,0,0,0,0,0,0,3,0,0,0,0,0,1,0,0,0,0,12]], +/* V A */ [[0,9,20,8,1,0,14,2,8,1,3,69,2,57,0,1,0,31,18,36,5,0,0,0,0,0], +/* V B */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* V C */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* V D */ [1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* V E */ [6,2,5,4,4,3,6,4,5,0,1,47,4,120,3,1,0,271,46,24,0,0,1,5,10,0], +/* V F */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* V G */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* V H */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* V I */ [37,4,33,23,21,2,8,0,2,0,3,43,0,47,18,0,0,16,65,30,5,16,0,2,0,1], +/* V J */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* V K */ [0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* V L */ [2,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0], +/* V M */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* V N */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* V O */ [0,0,23,0,0,0,3,0,9,0,5,48,2,6,1,0,0,10,4,9,10,1,3,0,6,0], +/* V P */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* V Q */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* V R */ [0,0,0,0,5,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0], +/* V S */ [0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* V T */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* V U */ [0,0,0,0,0,0,0,0,0,0,0,13,0,0,0,0,0,2,2,0,0,0,0,0,0,0], +/* V V */ [0,0,0,0,1,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0], +/* V W */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* V X */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* V Y */ [0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0], +/* V Z */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0]], +/* W A */ [[1,4,7,8,0,3,12,3,18,0,8,53,5,20,0,4,0,100,27,55,1,9,1,4,71,1], +/* W B */ [6,0,0,0,7,0,0,0,1,0,0,0,0,0,10,0,0,3,0,0,1,0,0,0,0,0], +/* W C */ [3,0,0,0,0,0,0,1,0,0,0,0,0,0,1,0,0,0,0,0,1,0,0,0,0,0], +/* W D */ [0,0,0,0,5,0,0,0,1,0,0,0,0,0,4,0,0,3,0,0,1,0,0,0,3,0], +/* W E */ [30,5,1,9,33,0,2,1,19,0,0,51,0,11,0,2,0,36,21,7,0,2,0,0,2,0], +/* W F */ [1,0,0,0,0,0,0,0,3,0,0,3,0,0,4,0,0,0,0,0,3,0,0,0,0,0], +/* W G */ [0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* W H */ [18,0,0,0,47,0,0,0,52,0,0,0,0,0,19,0,0,0,0,0,1,0,0,0,1,0], +/* W I */ [0,0,14,18,5,5,15,0,0,0,0,40,2,83,0,2,0,8,38,47,0,4,0,1,0,2], +/* W J */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* W K */ [0,0,0,0,0,0,0,0,2,0,0,0,0,1,0,0,0,0,0,0,0,0,1,0,1,0], +/* W L */ [3,0,0,0,9,0,0,0,5,0,0,0,0,0,1,0,0,0,1,1,0,0,0,0,3,0], +/* W M */ [8,0,0,0,5,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* W N */ [0,1,1,1,6,1,1,2,3,0,0,0,0,0,0,2,0,1,10,4,1,0,2,0,3,0], +/* W O */ [0,1,0,0,3,1,0,0,0,0,3,10,17,8,54,1,0,121,1,1,3,2,1,0,0,0], +/* W P */ [1,0,0,0,1,0,0,0,1,0,0,1,0,0,5,0,0,0,0,0,1,0,0,0,0,0], +/* W Q */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* W R */ [7,0,0,0,12,0,0,0,25,0,0,0,0,0,10,0,0,0,0,0,0,0,0,0,6,0], +/* W S */ [0,1,1,0,2,0,0,1,1,0,1,2,2,0,5,3,0,1,1,4,1,0,2,0,1,0], +/* W T */ [1,0,0,0,1,0,0,3,1,0,0,0,0,0,3,0,0,0,0,0,1,0,0,0,0,0], +/* W U */ [0,0,0,0,0,0,0,1,0,0,0,1,1,1,0,1,0,2,0,0,0,0,0,0,0,0], +/* W V */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* W W */ [1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0], +/* W X */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* W Y */ [2,0,0,0,5,0,0,0,0,0,0,1,1,4,1,0,0,0,0,0,0,0,0,0,0,0], +/* W Z */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0]], +/* X A */ [[0,0,5,1,0,1,3,0,0,0,0,4,6,6,0,0,0,0,3,6,0,1,0,0,0,0], +/* X B */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0], +/* X C */ [3,0,0,0,11,0,0,3,7,0,0,7,0,0,3,0,0,5,0,0,7,0,0,0,0,0], +/* X D */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* X E */ [0,0,7,1,0,0,2,0,1,0,0,2,6,9,0,0,0,6,1,1,0,0,0,0,1,0], +/* X F */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0], +/* X G */ [0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,1,0,0,0,0,0], +/* X H */ [7,0,0,0,0,0,0,0,4,0,0,0,0,0,4,0,0,0,0,0,2,0,0,0,0,0], +/* X I */ [8,2,12,8,4,2,2,0,0,0,0,2,11,4,8,0,0,0,9,2,0,1,1,0,0,0], +/* X J */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* X K */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* X L */ [0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* X M */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* X N */ [1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* X O */ [0,0,0,1,0,0,3,0,0,0,0,1,1,10,0,1,0,6,1,5,0,0,0,0,0,0], +/* X P */ [8,0,0,0,27,0,0,0,5,0,0,18,0,0,12,0,0,7,0,0,3,0,0,0,0,0], +/* X Q */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0], +/* X R */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* X S */ [0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* X T */ [6,1,0,0,22,0,0,1,7,0,0,0,0,0,7,0,0,31,0,0,9,0,0,0,1,0], +/* X U */ [4,1,0,2,0,0,0,0,0,0,0,3,0,0,0,1,0,6,0,0,0,0,0,0,0,0], +/* X V */ [0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* X W */ [0,0,0,0,2,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0], +/* X X */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0], +/* X Y */ [0,0,0,0,0,0,2,0,0,0,0,6,0,0,0,0,0,2,0,0,0,0,0,0,0,0], +/* X Z */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]], +/* Y A */ [[0,0,5,5,0,0,1,1,0,0,2,11,3,29,1,4,1,20,1,3,0,0,3,0,0,0], +/* Y B */ [4,0,0,4,7,0,0,0,2,0,0,0,0,0,9,0,0,3,0,0,3,0,0,0,0,0], +/* Y C */ [4,0,0,0,18,0,0,31,4,0,0,19,0,0,12,0,0,0,0,0,0,0,0,0,0,0], +/* Y D */ [4,1,0,0,12,0,0,0,2,0,0,0,0,2,1,0,0,37,0,0,0,0,0,0,0,0], +/* Y E */ [11,3,0,1,1,1,1,0,1,0,0,13,1,6,2,1,0,19,7,6,0,1,1,0,0,0], +/* Y F */ [1,0,0,0,1,0,0,0,3,0,0,2,0,0,0,0,0,0,0,0,4,0,0,0,0,0], +/* Y G */ [0,0,0,1,2,0,0,0,2,0,0,1,3,1,8,0,0,3,0,0,1,0,0,0,2,0], +/* Y H */ [0,0,0,0,4,0,0,0,0,0,0,0,0,0,10,0,0,0,0,0,0,0,0,0,1,0], +/* Y I */ [0,0,0,1,1,0,0,0,0,0,0,0,0,9,0,2,0,0,2,0,0,0,0,0,0,0], +/* Y J */ [1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* Y K */ [0,0,0,0,3,0,0,0,0,1,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0], +/* Y L */ [15,0,0,0,22,0,0,0,13,0,1,19,0,0,11,1,0,0,2,0,3,6,0,0,0,0], +/* Y M */ [18,4,1,0,20,0,0,0,5,0,0,0,3,7,11,20,0,0,0,0,2,0,0,0,1,0], +/* Y N */ [14,0,11,3,12,0,3,1,2,0,0,0,0,3,11,0,0,0,0,6,0,0,0,2,1,0], +/* Y O */ [0,0,2,2,0,4,6,0,0,0,5,2,1,18,0,4,0,8,4,5,17,1,1,0,0,1], +/* Y P */ [2,0,0,0,24,0,0,17,5,0,0,2,0,2,21,0,0,5,7,16,3,0,0,0,1,0], +/* Y Q */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0], +/* Y R */ [15,0,0,2,6,1,0,0,21,0,0,0,0,2,29,0,0,2,0,1,4,0,0,0,1,0], +/* Y S */ [3,1,3,0,12,0,0,1,38,0,0,1,2,0,4,3,0,0,6,39,2,0,0,0,0,0], +/* Y T */ [2,0,0,0,16,0,0,16,10,0,0,0,0,0,12,0,0,0,0,2,0,0,0,0,1,0], +/* Y U */ [0,0,3,0,0,0,3,1,0,0,2,1,0,1,0,1,0,0,2,0,0,0,0,0,0,0], +/* Y V */ [1,0,0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* Y W */ [10,0,1,0,3,0,0,2,4,0,0,0,0,0,5,0,0,3,0,0,0,0,0,0,0,0], +/* Y X */ [0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* Y Y */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* Y Z */ [2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0]], +/* Z A */ [[1,3,2,0,0,0,5,1,1,0,1,4,1,11,0,1,0,19,0,0,0,1,0,0,0,1], +/* Z B */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* Z C */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* Z D */ [1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* Z E */ [5,1,2,1,1,0,0,0,1,0,1,7,0,12,0,0,0,13,3,3,1,0,1,0,0,0], +/* Z F */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* Z G */ [0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* Z H */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* Z I */ [1,1,2,0,7,0,5,0,0,0,0,5,4,6,1,1,0,2,1,1,1,0,0,0,0,0], +/* Z J */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* Z K */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* Z L */ [0,0,0,0,16,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,2,0], +/* Z M */ [1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* Z N */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* Z O */ [3,0,0,2,2,0,1,0,7,0,0,0,3,10,5,2,0,5,0,0,1,1,0,0,0,0], +/* Z P */ [1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* Z Q */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* Z R */ [1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0], +/* Z S */ [0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* Z T */ [0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* Z U */ [0,0,1,0,1,0,0,0,0,0,1,0,0,0,0,0,0,4,0,0,0,0,0,0,0,0], +/* Z V */ [0,0,0,0,1,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0], +/* Z W */ [0,0,0,0,1,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0], +/* Z X */ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* Z Y */ [0,1,0,0,0,0,4,0,0,0,0,0,3,0,0,0,0,0,0,0,0,0,0,0,0,0], +/* Z Z */ [7,0,0,0,1,0,0,0,7,0,0,17,0,0,2,0,0,0,0,0,0,0,1,0,5,0]]]; + + // Pick a random starting point. + pik = Math.random(); // random number [0,1] + ranno = pik * 125729.0; + sum = 0; + for (c1=0; c1 < 26; c1++) { + for (c2=0; c2 < 26; c2++) { + for (c3=0; c3 < 26; c3++) { + sum += _trigram[c1][c2][c3]; + if (sum > ranno) { + output += _alphabet.charAt(c1); + output += _alphabet.charAt(c2); + output += _alphabet.charAt(c3); + c1 = 26; // Found start. Break all 3 loops. + c2 = 26; + c3 = 26; + } // if sum + } // for c3 + } // for c2 + } // for c1 + // Now do a random walk. + nchar = 3; + while (nchar < pwl) { + c1 = _alphabet.indexOf(output.charAt(nchar-2)); + c2 = _alphabet.indexOf(output.charAt(nchar-1)); + sum = 0; + for (c3=0; c3 < 26; c3++) + sum += _trigram[c1][c2][c3]; + if (sum == 0) { + //alert("sum was 0, outut="+output); + break; // exit while loop + } + //pik = ran.nextDouble(); + pik = Math.random(); + ranno = pik * sum; + sum = 0; + for (c3=0; c3 < 26; c3++) { + sum += _trigram[c1][c2][c3]; + if (sum > ranno) { + output += _alphabet.charAt(c3); + c3 = 26; // break for loop + } // if sum + } // for c3 + nchar ++; + } // while nchar + + return output + Math.floor(Math.random() * 90 + 10) + } // pronounceable + +} // GPW diff --git a/data/web/js/site/quarantine.js b/data/web/js/site/quarantine.js index 090b5054..0376c893 100644 --- a/data/web/js/site/quarantine.js +++ b/data/web/js/site/quarantine.js @@ -1,11 +1,13 @@ // Base64 functions var Base64={_keyStr:"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",encode:function(r){var t,e,o,a,h,n,c,d="",C=0;for(r=Base64._utf8_encode(r);C<r.length;)a=(t=r.charCodeAt(C++))>>2,h=(3&t)<<4|(e=r.charCodeAt(C++))>>4,n=(15&e)<<2|(o=r.charCodeAt(C++))>>6,c=63&o,isNaN(e)?n=c=64:isNaN(o)&&(c=64),d=d+this._keyStr.charAt(a)+this._keyStr.charAt(h)+this._keyStr.charAt(n)+this._keyStr.charAt(c);return d},decode:function(r){var t,e,o,a,h,n,c="",d=0;for(r=r.replace(/[^A-Za-z0-9\+\/\=]/g,"");d<r.length;)t=this._keyStr.indexOf(r.charAt(d++))<<2|(a=this._keyStr.indexOf(r.charAt(d++)))>>4,e=(15&a)<<4|(h=this._keyStr.indexOf(r.charAt(d++)))>>2,o=(3&h)<<6|(n=this._keyStr.indexOf(r.charAt(d++))),c+=String.fromCharCode(t),64!=h&&(c+=String.fromCharCode(e)),64!=n&&(c+=String.fromCharCode(o));return c=Base64._utf8_decode(c)},_utf8_encode:function(r){r=r.replace(/\r\n/g,"\n");for(var t="",e=0;e<r.length;e++){var o=r.charCodeAt(e);o<128?t+=String.fromCharCode(o):o>127&&o<2048?(t+=String.fromCharCode(o>>6|192),t+=String.fromCharCode(63&o|128)):(t+=String.fromCharCode(o>>12|224),t+=String.fromCharCode(o>>6&63|128),t+=String.fromCharCode(63&o|128))}return t},_utf8_decode:function(r){for(var t="",e=0,o=c1=c2=0;e<r.length;)(o=r.charCodeAt(e))<128?(t+=String.fromCharCode(o),e++):o>191&&o<224?(c2=r.charCodeAt(e+1),t+=String.fromCharCode((31&o)<<6|63&c2),e+=2):(c2=r.charCodeAt(e+1),c3=r.charCodeAt(e+2),t+=String.fromCharCode((15&o)<<12|(63&c2)<<6|63&c3),e+=3);return t}}; + jQuery(function($){ acl_data = JSON.parse(acl); // http://stackoverflow.com/questions/24816/escaping-html-strings-with-jquery var entityMap={"&":"&","<":"<",">":">",'"':""","'":"'","/":"/","`":"`","=":"="}; function escapeHtml(n){return String(n).replace(/[&<>"'`=\/]/g,function(n){return entityMap[n]})} function humanFileSize(i){if(Math.abs(i)<1024)return i+" B";var B=["KiB","MiB","GiB","TiB","PiB","EiB","ZiB","YiB"],e=-1;do{i/=1024,++e}while(Math.abs(i)>=1024&&e<B.length-1);return i.toFixed(1)+" "+B[e]} + function draw_quarantine_table() { ft_quarantinetable = FooTable.init('#quarantinetable', { "columns": [ @@ -15,6 +17,7 @@ jQuery(function($){ {"name":"sender","title":lang.sender}, {"name":"rcpt","title":lang.rcpt, "breakpoints":"xs sm md", "type": "text"}, {"name":"virus","title":lang.danger, "type": "text"}, + {"name":"score","title": lang.spam_score, "type": "text"}, {"name":"subject","title":lang.subj, "type": "text"}, {"name":"created","formatter":function unix_time_format(tm) { var date = new Date(tm ? tm * 1000 : 0); return date.toLocaleString();},"title":lang.received,"style":{"width":"170px"}}, {"name":"action","filterable": false,"sortable": false,"style":{"text-align":"right"},"style":{"width":"220px"},"type":"html","title":lang.action,"breakpoints":"xs sm md"} @@ -33,6 +36,9 @@ jQuery(function($){ } else { item.subject = escapeHtml(item.subject); } + if (item.score === null) { + item.score = '-'; + } if (item.virus_flag > 0) { item.virus = '<span class="dot-danger"></span>'; } else { @@ -56,54 +62,66 @@ jQuery(function($){ "empty": lang.empty, "paging": {"enabled": true,"limit": 5,"size": pagination_size}, "sorting": {"enabled": true}, - "on": { - "ready.ft.table": btn_group_quarantine, - "after.ft.paging": btn_group_quarantine - }, "filtering": {"enabled": true,"position": "left","connectors": false,"placeholder": lang.filter_table}, + "toggleSelector": "table tbody span.footable-toggle" }); } - btn_group_quarantine = function(ev, ft){ - $('.show_qid_info').on('click', function (e) { - e.preventDefault(); - var qitem = $(this).data('item'); - $('#qidDetailModal').modal('show'); - $( "#qid_error" ).hide(); - $.ajax({ - url: '/inc/ajax/qitem_details.php', - data: { id: qitem }, - dataType: 'json', - success: function(data){ - if (typeof data.error !== 'undefined') { - $( "#qid_error" ).text(data.error); - $( "#qid_error" ).show(); - } - $( "li" ).each(function( index ) { - console.log( index + ": " + $( this ).text() ); - }); - $('[data-id="qitems_single"]').each(function( index ) { - $(this).attr("data-item", qitem); - }); - $('#qid_detail_subj').text(data.subject); - $('#qid_detail_text').text(data.text_plain); - $('#qid_detail_text_from_html').text(data.text_html); - if (typeof data.attachments !== 'undefined') { - $( "#qid_detail_atts" ).text(''); - $.each(data.attachments, function( index, value ) { - $( "#qid_detail_atts" ).append( - '<p><a href="/inc/ajax/qitem_details.php?id=' + qitem + '&att=' + index + '" target="_blank">' + value[0] + '</a> (' + value[1] + ')' + - ' - <small><a href="' + value[3] + '" target="_blank">' + lang.check_hash + '</a></small></p>' - ); - }); - } - else { - $( "#qid_detail_atts" ).text('-'); - } + $('body').on('click', '.show_qid_info', function (e) { + e.preventDefault(); + var qitem = $(this).data('item'); + var qError = $("#qid_error"); + + $('#qidDetailModal').modal('show'); + qError.hide(); + + $.ajax({ + url: '/inc/ajax/qitem_details.php', + data: { id: qitem }, + dataType: 'json', + success: function(data){ + if (typeof data.error !== 'undefined') { + qError.text(data.error); + qError.show(); } - }); - }) - } + $('[data-id="qitems_single"]').each(function(index) { + $(this).attr("data-item", qitem); + }); + + $('#qid_detail_subj').text(data.subject); + $('#qid_detail_text').text(data.text_plain); + $('#qid_detail_text_from_html').text(data.text_html); + + $('#qid_detail_recipients').html(''); + if (typeof data.recipients !== 'undefined') { + $.each(data.recipients, function(index, value) { + var elem = $('<span class="mail-address-item"></span>'); + elem.text(value.address + (value.type != 'to' ? (' (' + value.type.toUpperCase() + ')') : '')); + $('#qid_detail_recipients').append(elem); + }); + } + + var qAtts = $("#qid_detail_atts"); + if (typeof data.attachments !== 'undefined') { + qAtts.text(''); + $.each(data.attachments, function(index, value) { + qAtts.append( + '<p><a href="/inc/ajax/qitem_details.php?id=' + qitem + '&att=' + index + '" target="_blank">' + value[0] + '</a> (' + value[1] + ')' + + ' - <small><a href="' + value[3] + '" target="_blank">' + lang.check_hash + '</a></small></p>' + ); + }); + } + else { + qAtts.text('-'); + } + } + }); + }); + + $('body').on('click', 'span.footable-toggle', function () { + event.stopPropagation(); + }) + // Initial table drawings draw_quarantine_table(); }); diff --git a/data/web/js/site/user.js b/data/web/js/site/user.js index 505da772..a976dd85 100644 --- a/data/web/js/site/user.js +++ b/data/web/js/site/user.js @@ -84,7 +84,8 @@ jQuery(function($){ "state": {"enabled": true}, "sorting": { "enabled": true - } + }, + "toggleSelector": "table tbody span.footable-toggle" }); } function draw_sync_job_table() { @@ -151,7 +152,8 @@ jQuery(function($){ "state": {"enabled": true}, "sorting": { "enabled": true - } + }, + "toggleSelector": "table tbody span.footable-toggle" }); } function draw_wl_policy_mailbox_table() { @@ -236,6 +238,11 @@ jQuery(function($){ } }); } + + $('body').on('click', 'span.footable-toggle', function () { + event.stopPropagation(); + }) + draw_sync_job_table(); draw_tla_table(); draw_wl_policy_mailbox_table(); diff --git a/data/web/json_api.php b/data/web/json_api.php index 53ecf520..fea4565b 100644 --- a/data/web/json_api.php +++ b/data/web/json_api.php @@ -53,8 +53,6 @@ function api_log($_data) { } } -api_log($_POST); - if (isset($_SESSION['mailcow_cc_role']) || isset($_SESSION['pending_mailcow_cc_username'])) { if (isset($_GET['query'])) { @@ -64,9 +62,42 @@ if (isset($_SESSION['mailcow_cc_role']) || isset($_SESSION['pending_mailcow_cc_u $object = (isset($query[2])) ? $query[2] : null; $extra = (isset($query[3])) ? $query[3] : null; + // accept json in request body + if($_SERVER['HTTP_CONTENT_TYPE'] === 'application/json') { + $request = file_get_contents('php://input'); + $requestDecoded = json_decode($request, true); + + // check for valid json + if ($action != 'get' && $requestDecoded === null) { + echo json_encode(array( + 'type' => 'error', + 'msg' => 'Request body doesn\'t contain valid json!' + )); + exit; + } + + // add + if ($action == 'add') { + $_POST['attr'] = $request; + } + + // edit + if ($action == 'edit') { + $_POST['attr'] = json_encode($requestDecoded['attr']); + $_POST['items'] = json_encode($requestDecoded['items']); + } + + // delete + if ($action == 'delete') { + $_POST['items'] = $request; + } + + } + api_log($_POST); + $request_incomplete = json_encode(array( - 'type' => 'error', - 'msg' => 'Cannot find attributes in post data' + 'type' => 'error', + 'msg' => 'Cannot find attributes in post data' )); switch ($action) { @@ -788,6 +819,14 @@ if (isset($_SESSION['mailcow_cc_role']) || isset($_SESSION['pending_mailcow_cc_u break; } break; + case "fail2ban": + switch ($object) { + default: + $data = fail2ban('get'); + process_get_return($data); + break; + } + break; case "resource": switch ($object) { case "all": diff --git a/data/web/lang/lang.ca.php b/data/web/lang/lang.ca.php index 91072ecf..63722661 100644 --- a/data/web/lang/lang.ca.php +++ b/data/web/lang/lang.ca.php @@ -498,6 +498,7 @@ $lang['quarantine']['show_item'] = "Mostrar"; $lang['quarantine']['check_hash'] = "Comprovar el hash del fitxer a VT"; $lang['quarantine']['qitem'] = "Element en quarantena"; $lang['quarantine']['subj'] = "Assumpte"; +$lang['quarantine']['recipients'] = "Recipients"; $lang['quarantine']['text_plain_content'] = "Contingut (text/plain)"; $lang['quarantine']['text_from_html_content'] = "Contingut (a partir del HTML)"; $lang['quarantine']['atts'] = "Adjunts"; diff --git a/data/web/lang/lang.cs.php b/data/web/lang/lang.cs.php index ca2a4684..a0910534 100644 --- a/data/web/lang/lang.cs.php +++ b/data/web/lang/lang.cs.php @@ -3,6 +3,7 @@ * Czech language file for mailcow * * Author: radek@fastlinux.eu (www.fastlinux.eu) + * Author: filip@hajny.net * */ @@ -13,9 +14,9 @@ $lang['header']['restart_netfilter'] = 'Restartovat netfilter'; $lang['footer']['restart_container'] = 'Restartovat kontejner'; $lang['footer']['restart_now'] = 'Restartovat nyní'; $lang['footer']['restarting_container'] = 'Restartuje se kontejner, může to chvilku trvat...'; -$lang['footer']['restart_container_info'] = '<b>Důležité:</b> Může trvat dlouho než bude "Graceful restart" dokončen, prosím čekejte...'; +$lang['footer']['restart_container_info'] = '<b>Důležité:</b> Šetrný restart může chvíli trvat, prosím čekejte...'; -$lang['footer']['confirm_delete'] = 'Potvrzení smazání'; +$lang['footer']['confirm_delete'] = 'Potvdit smazání'; $lang['footer']['delete_these_items'] = 'Prosím potvrďte vaše změny objektu id:'; $lang['footer']['delete_now'] = 'Smazat'; $lang['footer']['cancel'] = 'Zrušit'; @@ -23,6 +24,8 @@ $lang['footer']['cancel'] = 'Zrušit'; $lang['footer']['hibp_nok'] = 'Nalezeno! Toto je potenciálně nebezpečné heslo!'; $lang['footer']['hibp_ok'] = 'Nebyla nalezena žádná shoda.'; +$lang['danger']['transport_dest_exists'] = 'Transportní cíl "%s" již existuje'; +$lang['danger']['unlimited_quota_acl'] = "Neomeznou kvótu nepovoluje seznam oprávnění ACL"; $lang['danger']['mysql_error'] = "Chyba MySQL: %s"; $lang['danger']['redis_error'] = "Chyba Redis: %s"; $lang['danger']['unknown_tfa_method'] = "Neznámá TFA metoda"; @@ -33,16 +36,16 @@ $lang['success']['verified_u2f_login'] = "U2F přihlášení ověřeno"; $lang['success']['verified_yotp_login'] = "Yubico OTP přihlášení ověřeno"; $lang['danger']['yotp_verification_failed'] = "Yubico OTP ověření selhalo: %s"; $lang['danger']['ip_list_empty'] = "Seznam povolených IP nesmí být prázdný"; -$lang['danger']['invalid_destination'] = "Formát cíle je špatný"; -$lang['danger']['invalid_nexthop'] = "Formát skoku (Next hop) je špatný"; -$lang['danger']['invalid_nexthop_authenticated'] = "Skok (Next hop) již existuje s rozílným přihlašovacím údajem, nejdříve prosím aktualizujte existující přihlašovací údaje tohoto skoku."; +$lang['danger']['invalid_destination'] = "Formát cíle je neplatný"; +$lang['danger']['invalid_nexthop'] = "Formát skoku (Next hop) je neplatný"; +$lang['danger']['invalid_nexthop_authenticated'] = "Skok (Next hop) již existuje s rozdílnými přihlašovacími údaji, nejdříve prosím aktualizujte existující přihlašovací údaje tohoto skoku."; $lang['danger']['next_hop_interferes'] = "%s koliduje se skokem %s"; $lang['danger']['next_hop_interferes_any'] = "Existující skok koliduje s %s"; -$lang['danger']['rspamd_ui_pw_length'] = "Heslo pro Rspamd UI musí být minimálně 6 znaků dlouhé"; +$lang['danger']['rspamd_ui_pw_length'] = "Heslo pro Rspamd UI musí mít alespoň 6 znaků"; $lang['success']['rspamd_ui_pw_set'] = "Heslo k Rspamd UI nastaveno"; $lang['success']['queue_command_success'] = "Příkaz pro frontu úspěšně dokončen"; $lang['danger']['unknown'] = "Došlo k neznámé chybě"; -$lang['danger']['malformed_username'] = "Vadné jméno uživatele"; +$lang['danger']['malformed_username'] = "Neplatné uživatelské jméno"; $lang['info']['awaiting_tfa_confirmation'] = "Čeká se na potvrzení TFA"; $lang['success']['logged_in_as'] = "Přihlášen jako %s"; $lang['danger']['login_failed'] = "Přihlášení selhalo"; @@ -53,12 +56,12 @@ $lang['danger']['sieve_error'] = "Chyba Sieve parseru: %s"; $lang['danger']['value_missing'] = "Prosím, uveďte všechny hodnoty"; $lang['danger']['filter_type'] = "Špatný typ filtru"; $lang['danger']['domain_cannot_match_hostname'] = "Doména a hostname nesmí být stejné"; -$lang['warning']['domain_added_sogo_failed'] = "Doména byla přidána ale selhal restart SOGo kontejneru, prosím zkontrolujte logy serveru."; -$lang['danger']['rl_timeframe'] = "Omezení časového rámce (Rate limit time frame) je špatné"; -$lang['success']['rl_saved'] = "Omezení provozu (Rate limit) pro objekt %s uloženo"; +$lang['warning']['domain_added_sogo_failed'] = "Doména přidána, ale selhal restart SOGo kontejneru, prosím zkontrolujte logy serveru."; +$lang['danger']['rl_timeframe'] = "Nesprávný časový rámec omezení provozu"; +$lang['success']['rl_saved'] = "Omezení provozu pro objekt %s uloženo"; $lang['success']['acl_saved'] = "ACL pro objekt %s uloženo"; -$lang['success']['deleted_syncjobs'] = "Smazané synchromizační úlohy: %s"; -$lang['success']['deleted_syncjob'] = "Smazaná synchronizační úloha ID %s"; +$lang['success']['deleted_syncjobs'] = "Smazány synchronizační úlohy: %s"; +$lang['success']['deleted_syncjob'] = "Smazán synchronizační úloha ID %s"; $lang['success']['delete_filters'] = "Smazané filtry: %s"; $lang['success']['delete_filter'] = "Smazané filtry ID %s"; $lang['danger']['invalid_bcc_map_type'] = "Špatný typ BCC mapování"; @@ -72,92 +75,94 @@ $lang['danger']['private_key_error'] = "Chyba soukroméhop klíče: %s"; $lang['danger']['map_content_empty'] = "Obsah mapování nesmí být prázdný"; $lang['success']['settings_map_added'] = "Přidána položka mapování nastavení"; $lang['danger']['settings_map_invalid'] = "Položka mapování nastavení ID %s je špatná"; -$lang['danger']['settings_map_removed'] = "Položka mapování nastavení: %s byla smazána"; -$lang['danger']['invalid_host'] = "Zadán neplatný klient (Host): %s"; -$lang['danger']['relayhost_invalid'] = "Nadřazený SMTP server (Relayhost) %s je špatný"; +$lang['danger']['settings_map_removed'] = "Položka mapování nastavení: %s smazána"; +$lang['danger']['invalid_host'] = "Zadán neplatný hostitel: %s"; +$lang['danger']['relayhost_invalid'] = "Nadřazený SMTP server (Relayhost) %s je neplatný"; $lang['success']['saved_settings'] = "Nastavení uložena"; -$lang['success']['db_init_complete'] = "Inicializace databáze byla dokončena"; +$lang['success']['db_init_complete'] = "Inicializace databáze dokončena"; $lang['warning']['session_ua'] = "Token formuláře není platný: User-Agent validation error"; $lang['warning']['session_token'] = "Token formuláře není platný: Token mismatch"; -$lang['danger']['dkim_domain_or_sel_invalid'] = "DKIM nebo selektor doménu je špatný: %s"; -$lang['success']['dkim_removed'] = "DKIM klíč %s byl odebrán"; -$lang['success']['dkim_added'] = "DKIM klíč %s byl uložen"; -$lang['success']['dkim_duplicated'] = "DKIM klíč domény %s byl zkopírován do %s"; -$lang['danger']['access_denied'] = "Přístup byl odepřen nebo jsou neplatné data ve formuláři"; -$lang['danger']['domain_invalid'] = "Název domény je prázdný nebo špatný"; -$lang['danger']['mailbox_quota_exceeds_domain_quota'] = "Max. kvóta přektočila limit pro doménu"; -$lang['danger']['object_is_not_numeric'] = "Hodnota %s není numerická"; +$lang['danger']['dkim_domain_or_sel_invalid'] = "DKIM nebo selektor doménu je neplatný: %s"; +$lang['success']['dkim_removed'] = "DKIM klíč %s odebrán"; +$lang['success']['dkim_added'] = "DKIM klíč %s uložen"; +$lang['success']['dkim_duplicated'] = "DKIM klíč domény %s zkopírován do %s"; +$lang['danger']['access_denied'] = "Přístup odepřen nebo jsou neplatná data ve formuláři"; +$lang['danger']['domain_invalid'] = "Název domény je prázdný nebo neplatný"; +$lang['danger']['mailbox_quota_exceeds_domain_quota'] = "Max. kvóta překročila limit domény"; +$lang['danger']['object_is_not_numeric'] = "Hodnota %s není číslo"; $lang['success']['domain_added'] = "Přidána doména %s"; -$lang['success']['items_deleted'] = "Položka %s byla úspěšně smazána"; -$lang['success']['item_deleted'] = "Položka %s byla úspěšně smazána"; +$lang['success']['items_deleted'] = "Položka %s úspěšně smazána"; +$lang['success']['item_deleted'] = "Položka %s úspěšně smazána"; $lang['danger']['alias_empty'] = "Adresa aliasu nesmí být prázdná"; $lang['danger']['last_key'] = 'Nelze smazat poslední klíč'; $lang['danger']['goto_empty'] = "Cílová adresa nesmí být prázdná"; -$lang['danger']['policy_list_from_exists'] = "Záznam s daným jménem ji6 existuje"; -$lang['danger']['policy_list_from_invalid'] = "Záznam má špatný formát"; -$lang['danger']['alias_invalid'] = "Adresa aliasu %s je špatná"; -$lang['danger']['goto_invalid'] = "Cílová adresa %s není správná"; -$lang['danger']['alias_domain_invalid'] = "Doménový alias %s není správný"; -$lang['danger']['target_domain_invalid'] = "Cílová doména %s není správná"; +$lang['danger']['policy_list_from_exists'] = "Záznam s daným jménem již existuje"; +$lang['danger']['policy_list_from_invalid'] = "Záznam má neplatný formát"; +$lang['danger']['alias_invalid'] = "Adresa aliasu %s je neplatná"; +$lang['danger']['goto_invalid'] = "Cílová adresa %s je neplatná"; +$lang['danger']['alias_domain_invalid'] = "Doménový alias %s je neplatný"; +$lang['danger']['target_domain_invalid'] = "Cílová doména %s je neplatná"; $lang['danger']['object_exists'] = "Objekt %s již existuje"; $lang['danger']['domain_exists'] = "Doména %s již existuje"; -$lang['danger']['alias_goto_identical'] = "Alias a cílová adresa nesmí být stejná"; +$lang['danger']['alias_goto_identical'] = "Alias a cílová adresa nesmějí být stejné"; $lang['danger']['aliasd_targetd_identical'] = "Doménový alias nesmí být stejný jako cílová doména: %s"; $lang['danger']['maxquota_empty'] = 'Max. kvóta poštovní schránky nesmí být 0.'; $lang['success']['alias_added'] = "Byl přidán alias %s"; -$lang['success']['alias_modified'] = "Změny aliasu %s byly uloženy"; -$lang['success']['mailbox_modified'] = "Změny poštovní schránky %s byly uloženy"; -$lang['success']['resource_modified'] = "Změny poštovní schránky %s byly uloženy"; -$lang['success']['object_modified'] = "Změny objektu %s byly uloženy"; -$lang['success']['f2b_modified'] = "Změny Fail2ban parametrů byly uloženy"; +$lang['success']['alias_modified'] = "Změny aliasu %s uloženy"; +$lang['success']['mailbox_modified'] = "Změny poštovní schránky %s uloženy"; +$lang['success']['resource_modified'] = "Změny poštovní schránky %s uloženy"; +$lang['success']['object_modified'] = "Změny objektu %s uloženy"; +$lang['success']['f2b_modified'] = "Změny parametrů Fail2ban uloženy"; $lang['danger']['targetd_not_found'] = "Cílová doména %s nenalezena"; +$lang['danger']['targetd_relay_domain'] = "Cílová doména %s je přesměrovaná"; $lang['success']['aliasd_added'] = "Přidán doménový alias %s"; -$lang['success']['aliasd_modified'] = "Změny doménového aliau %s byly uloženy"; -$lang['success']['domain_modified'] = "Změny domény %s byly uloženy"; -$lang['success']['domain_admin_modified'] = "Změny doménového administrátora %s byly uloženy"; -$lang['success']['domain_admin_added'] = "Doménový administrátor %s byl přidán"; -$lang['success']['admin_added'] = "Administrátor %s byl přidán"; -$lang['success']['admin_modified'] = "Změny administrátora byly uloženy"; -$lang['success']['admin_api_modified'] = "Změna API byla uložena"; -$lang['danger']['username_invalid'] = "Jméno uživatele %s nemůže být použito"; +$lang['success']['aliasd_modified'] = "Změny aliasu domény %s uloženy"; +$lang['success']['domain_modified'] = "Změny domény %s uloženy"; +$lang['success']['domain_admin_modified'] = "Změny správce domény %s uloženy"; +$lang['success']['domain_admin_added'] = "Správce domény %s přidán"; +$lang['success']['admin_added'] = "Správce %s přidán"; +$lang['success']['admin_modified'] = "Změny správce uloženy"; +$lang['success']['admin_api_modified'] = "Změna API uložena"; +$lang['success']['license_modified'] = "Změny licence uloženy"; +$lang['danger']['username_invalid'] = "Uživatelské jméno %s nelze použít"; $lang['danger']['password_mismatch'] = "Potvrzení hesla nesouhlasí"; $lang['danger']['password_complexity'] = "Heslo nesplňuje pravidla"; $lang['danger']['password_empty'] = "Heslo nesmí být prázdné"; $lang['danger']['login_failed'] = "Přihlášení selhalo"; -$lang['danger']['mailbox_invalid'] = "Název poštovní chránky není správný"; -$lang['danger']['description_invalid'] = 'Popis zdroje %s je špatný'; -$lang['danger']['resource_invalid'] = "Název zdroje %s je špatný"; +$lang['danger']['mailbox_invalid'] = "Název poštovní chránky je neplatný"; +$lang['danger']['description_invalid'] = 'Popis zdroje %s je neplatný'; +$lang['danger']['resource_invalid'] = "Název zdroje %s je neplatný"; $lang['danger']['is_alias'] = "%s je již známa jako adresa aliasu"; -$lang['danger']['is_alias_or_mailbox'] = "%s je již známa jako adresa aliasu, poštovní schránky nebo aliasu zděděného z doménového aliasu."; -$lang['danger']['is_spam_alias'] = "%s je již známa jako adresa spam aliasu"; +$lang['danger']['is_alias_or_mailbox'] = "%s je již známa jako adresa aliasu, poštovní schránky nebo aliasu rozvedeného z aliasu domény."; +$lang['danger']['is_spam_alias'] = "%s je již známa jako adresa spamového aliasu"; $lang['danger']['quota_not_0_not_numeric'] = "Kvóta musí být číslo >= 0"; $lang['danger']['domain_not_found'] = 'Doména %s nebyla nalezena'; $lang['danger']['max_mailbox_exceeded'] = "Max. počet poštovních schránek překročen (%d z %d)"; $lang['danger']['max_alias_exceeded'] = 'Překročeno max. množství aliasů'; $lang['danger']['mailbox_quota_exceeded'] = "Kvóta překročila limit domény (max. %d MiB)"; -$lang['danger']['mailbox_quota_left_exceeded'] = "Není dostatek volného místa (zbývá: %d MiB)"; -$lang['success']['mailbox_added'] = "Poštovní schránka %s byla přidána"; -$lang['success']['resource_added'] = "Zdroj %s byl přidán"; -$lang['success']['domain_removed'] = "Doména %s byla odebrána"; -$lang['success']['alias_removed'] = "Alias %s byl odebrán"; -$lang['success']['alias_domain_removed'] = "Doménový alias %s byl odebrán"; -$lang['success']['domain_admin_removed'] = "Doménový administrátor %s byl odebrán"; -$lang['success']['admin_removed'] = "Administrátor %s byl odebrán"; -$lang['success']['mailbox_removed'] = "Poštovní schránka %s byla odebrána"; -$lang['success']['eas_reset'] = "ActiveSync zařízení uživatele %s bylo vyresetováno"; -$lang['success']['sogo_profile_reset'] = "SOGo profil uživatele %s byl resetován"; -$lang['success']['resource_removed'] = "Zdroj %s byl odebrán"; +$lang['danger']['mailbox_quota_left_exceeded'] = "Není dost volného místa (zbývá: %d MiB)"; +$lang['success']['mailbox_added'] = "Poštovní schránka %s přidána"; +$lang['success']['resource_added'] = "Zdroj %s přidán"; +$lang['success']['domain_removed'] = "Doména %s odebrána"; +$lang['success']['alias_removed'] = "Alias %s odebrán"; +$lang['success']['alias_domain_removed'] = "Doménový alias %s odebrán"; +$lang['success']['domain_admin_removed'] = "Správce domény %s odebrán"; +$lang['success']['admin_removed'] = "Správce %s odebrán"; +$lang['success']['mailbox_removed'] = "Poštovní schránka %s odebrána"; +$lang['success']['eas_reset'] = "ActiveSync zařízení uživatele %s vyresetováno"; +$lang['success']['sogo_profile_reset'] = "SOGo profil uživatele %s vyresetován"; +$lang['success']['resource_removed'] = "Zdroj %s odebrán"; $lang['warning']['cannot_delete_self'] = "Nelze smazat právě přihlášeného uživatele"; -$lang['warning']['no_active_admin'] = "Nelze deaktivovat posledního aktivního administrátora"; +$lang['warning']['no_active_admin'] = "Nelze deaktivovat posledního aktivního správce"; $lang['danger']['max_quota_in_use'] = "Kvóta poštovní schránky musí být větší nebo rovna %d MiB"; -$lang['danger']['domain_quota_m_in_use'] = "Kvóta domény muí být větší nebo rovna %s MiB"; -$lang['danger']['mailboxes_in_use'] = "Max. počet poštovních schránek musí být větší nebo rovno %d"; -$lang['danger']['aliases_in_use'] = "Max. počet aliasů musí být větší nebo rovno %d"; -$lang['danger']['sender_acl_invalid'] = "ACL hodnota odesílatele %s je špatná"; +$lang['danger']['domain_quota_m_in_use'] = "Kvóta domény musí být větší nebo rovna %s MiB"; +$lang['danger']['mailboxes_in_use'] = "Max. počet poštovních schránek musí být větší nebo roven %d"; +$lang['danger']['aliases_in_use'] = "Max. počet aliasů musí být větší nebo roven %d"; +$lang['danger']['sender_acl_invalid'] = "Hodnota ACL odesílatele %s je neplatná"; $lang['danger']['domain_not_empty'] = "Nelze odebrat doménu, která není prázdná"; -$lang['danger']['validity_missing'] = 'Přidejte dobu platnosti'; +$lang['danger']['validity_missing'] = 'Zdejte dobu platnosti'; $lang['user']['loading'] = "Načítá se..."; $lang['user']['force_pw_update'] = 'Pro přístup k groupware funkcím <b>musíte změnit heslo</b>.'; $lang['user']['active_sieve'] = "Aktivní filtr"; @@ -169,21 +174,21 @@ $lang['user']['user_change_fn'] = ""; $lang['user']['user_settings'] = 'Uživatelské nastavení'; $lang['user']['mailbox_details'] = 'Podrobnosti poštovní schránky'; $lang['user']['change_password'] = 'Změnit heslo'; -$lang['user']['client_configuration'] = 'Zobrazit průvodce konfigurací pro e-mailové klienty a smartphony'; +$lang['user']['client_configuration'] = 'Zobrazit průvodce nastavením e-mailových klientů a smartphonů'; $lang['user']['new_password'] = 'Nové heslo'; $lang['user']['save_changes'] = 'Uložit změny'; -$lang['user']['password_now'] = 'Současné heslo (pro schválení změny)'; +$lang['user']['password_now'] = 'Současné heslo (pro potvrzení změny)'; $lang['user']['new_password_repeat'] = 'Potvrzení nového hesla (opakujte)'; -$lang['user']['new_password_description'] = 'Požadavek: délka min. 6 znaků, písmena a čísla.'; +$lang['user']['new_password_description'] = 'Požadavek: min. délka 6 znaků, písmena a čísla.'; $lang['user']['spam_aliases'] = 'Dočasné e-mailové aliasy'; $lang['user']['alias'] = 'Alias'; $lang['user']['shared_aliases'] = 'Sdílené aliasy'; -$lang['user']['shared_aliases_desc'] = 'Sdílené aliasy nejsou ovlivněny uživatelským nastavením, jako je spam filtr nebo pravidla šifrování. Odpovídající nastavení mohou provádět pouze správci na úrovni domény.'; +$lang['user']['shared_aliases_desc'] = 'Na sdílené aliasy se neuplatňuje uživatelské nastavení jako filtr spamu nebo pravidla šifrování. Nastavení filtrování spamu může provádět jen správce pro celou doménu.'; $lang['user']['direct_aliases'] = 'Přímé aliasy'; -$lang['user']['direct_aliases_desc'] = 'Přímé aliasy jsou ovlivněny filtrem spamu a nastavením pravidel TLS'; +$lang['user']['direct_aliases_desc'] = 'Na přímé aliasy se uplatňuje filtr spamu a nastavení pravidel TLS'; $lang['user']['is_catch_all'] = 'Catch-all pro doménu/y'; -$lang['user']['aliases_also_send_as'] = 'Také smí odesílat jako uživatel'; -$lang['user']['aliases_send_as_all'] = 'Nekontroluje se přístup odesílatele pro následující doménu(y) a jejich alias domény:'; +$lang['user']['aliases_also_send_as'] = 'Smí odesílat tako jako uživatel'; +$lang['user']['aliases_send_as_all'] = 'Nekontrolovat přístup odesílatele pro následující doménu(y) a jejich aliasy domény:'; $lang['user']['alias_create_random'] = 'Generovat náhodný alias'; $lang['user']['alias_extend_all'] = 'Prodloužit aliasy o 1 hodinu'; $lang['user']['alias_valid_until'] = 'Platný do'; @@ -198,12 +203,12 @@ $lang['user']['hours'] = 'hodin'; $lang['user']['day'] = 'den'; $lang['user']['week'] = 'týden'; $lang['user']['weeks'] = 'týdny'; -$lang['user']['spamfilter'] = 'Spam filtr'; -$lang['admin']['spamfilter'] = 'Spam filtr'; -$lang['user']['spamfilter_wl'] = 'Seznam povolených (Whitelist)'; -$lang['user']['spamfilter_wl_desc'] = 'E-mailové adresy v seznamu povolených (Whitelist) <b>nebudou nikdy klasifikované jako spam</b>. Mohou být použity zástupné znaky (*). Filtr je aplikován pouze na přímé aliasy (aliasy s jednou cílovou poštovní schránkou), s výjimkou aliasů typu catch-all a samotné poštovní schránky.'; -$lang['user']['spamfilter_bl'] = 'Seznam zakázaných (Blacklist)'; -$lang['user']['spamfilter_bl_desc'] = 'E-mailové adresy v seznamu zakázaných (Blacklist) <b>budou vždy klasifikované jako spam a odmítnuté</b>. Mohou být použity zástupné znaky(*). Filtr je aplikován pouze na přímé aliasy (aliasy s jednou cílovou poštovní schránkou), s výjimkou aliasů typu catch-all a samotné poštovní schránky.'; +$lang['user']['spamfilter'] = 'Filtr spamu'; +$lang['admin']['spamfilter'] = 'Filtr spamu'; +$lang['user']['spamfilter_wl'] = 'Seznam povolených adres (whitelist)'; +$lang['user']['spamfilter_wl_desc'] = 'Povolené emailové adresy <b>nebudou nikdy klasifikovány jako spam</b>. Lze použít zástupné znaky (*). Filtr se použije pouze na přímé aliasy (s jednou cílovou poštovní schránkou), s výjimkou aliasů typu catch-all a samotné poštovní schránky.'; +$lang['user']['spamfilter_bl'] = 'Seznam zakázaných adres (blacklist)'; +$lang['user']['spamfilter_bl_desc'] = 'Zakázané emailové adresy <b>budou vždy klasifikovány jako spam a odmítnuté</b>. Lze použít zástupné znaky (*). Filtr se použije pouze na přímé aliasy (s jednou cílovou poštovní schránkou), s výjimkou aliasů typu catch-all a samotné poštovní schránky.'; $lang['user']['spamfilter_behavior'] = 'Hodnocení'; $lang['user']['spamfilter_table_rule'] = 'Pravidlo'; $lang['user']['spamfilter_table_action'] = 'Akce'; @@ -211,7 +216,7 @@ $lang['user']['spamfilter_table_empty'] = 'Žádná data k zobrazení'; $lang['user']['spamfilter_table_remove'] = 'smazat'; $lang['user']['spamfilter_table_add'] = 'Přidat položku'; $lang['user']['spamfilter_green'] = 'Zelená: tato zpráva není spam'; -$lang['user']['spamfilter_yellow'] = 'Žlutá: tato zpráva může být spam, bude označena jako spam a přesunuta do složky nevyžádaná pošta'; +$lang['user']['spamfilter_yellow'] = 'Žlutá: tato zpráva může být spam, bude označena jako spam a přesunuta do složky nevyžádané pošty'; $lang['user']['spamfilter_red'] = 'Červená: Tato zpráva je spam a server ji odmítne'; $lang['user']['spamfilter_default_score'] = 'Výchozí hodnoty:'; $lang['user']['spamfilter_hint'] = 'První hodnota představuje "nízké spam skóre" a druhá "vysoké spam skóre".'; @@ -220,31 +225,31 @@ $lang['user']['waiting'] = "Čekání"; $lang['user']['status'] = "Stav"; $lang['user']['running'] = "Běží"; -$lang['user']['tls_policy_warning'] = '<strong>Upozornění:</strong> Pokud se rozhodnete vynutit šifrovaný přenos pošty, může dojít ke ztrátě e-mailů.<br>Zprávy, které nesplňují tuto politiku, budou poštovním systémem odmítnuty.<br>Tato volba se vztahuje k vaší primární e-mailové adrese (přihlašovací jméno), všem adresám odvozených z doménových aliasů i aliasů majících tuto poštovní chránku jako cíl.'; +$lang['user']['tls_policy_warning'] = '<strong>Varování:</strong> Pokud se rozhodnete vynutit šifrovaný přenos pošty, může dojít ke ztrátě e-mailů.<br>Zprávy, které nesplňují tuto politiku, budou poštovním systémem odmítnuty.<br>Tato volba ovlivňuje vaši primární e-mailovou adresu (přihlašovací jméno), všechny adresy odvozené z doménových aliasů i aliasy, jež mají tuto poštovní chránku jako cíl.'; $lang['user']['tls_policy'] = 'Politika šifrování'; $lang['user']['tls_enforce_in'] = 'Vynutit TLS pro příchozí poštu '; $lang['user']['tls_enforce_out'] = 'Vynutit TLS pro odchozí poštu'; $lang['user']['no_record'] = 'Žádný záznam'; -$lang['user']['tag_handling'] = 'Chování označené (tagged) pošty'; +$lang['user']['tag_handling'] = 'Zacházení s označkovanou poštou'; $lang['user']['tag_in_subfolder'] = 'V podsložce'; $lang['user']['tag_in_subject'] = 'V předmětu'; $lang['user']['tag_in_none'] = 'Nedělat nic'; -$lang['user']['tag_help_explain'] = 'V podsložce: v INBOXU bude vytvořena nová podsložka pojmenovaná po označení (tagu) zprávy ("INBOX / Facebook").<br> -V předmětu: název značky (tagu) bude přidáván k předmětu mailu, například: "[Facebook] Moje zprávy".'; -$lang['user']['tag_help_example'] = 'Příklad e-mailové adresy s označením (tagem): me<b>+Facebook</b>@example.org'; +$lang['user']['tag_help_explain'] = 'V podsložce: v doručené poště bude vytvořena nová podsložka pojmenovaná po značce zprávy ("INBOX / Facebook").<br> +V předmětu: název značky bude přidáván k předmětu mailu, například: "[Facebook] Moje zprávy".'; +$lang['user']['tag_help_example'] = 'Příklad e-mailové adresy se značkou: me<b>+Facebook</b>@example.org'; -$lang['user']['eas_reset'] = 'Smazat mezipaměť (cache) zařízení ActiveSync'; +$lang['user']['eas_reset'] = 'Smazat mezipaměť zařízení ActiveSync'; $lang['user']['eas_reset_now'] = 'Smazat'; -$lang['user']['eas_reset_help'] = 'Obnovení mezipaměti zařízení pomůže obnovit poškozený profil služby ActiveSync.<br><b>Upozornění:</b> Všechna data budou opětovně stažena!'; +$lang['user']['eas_reset_help'] = 'Obnovení mezipaměti zařízení pomůže zpravidla obnovit poškozený profil služby ActiveSync.<br><b>Upozornění:</b> Všechna data budou opětovně stažena!'; $lang['user']['sogo_profile_reset'] = 'Resetovat profil SOGo'; $lang['user']['sogo_profile_reset_now'] = 'Resetovat profil'; $lang['user']['sogo_profile_reset_help'] = 'Tato volba odstraní uživatelský profil SOGo a <b>nenávratně vymaže všechna data</b>.'; $lang['user']['encryption'] = 'Šifrování'; -$lang['user']['username'] = 'Jméno uživatele'; +$lang['user']['username'] = 'Uživatelské jméno'; $lang['user']['last_run'] = 'Naposledy spuštěno'; $lang['user']['excludes'] = 'Vyloučené'; $lang['user']['interval'] = 'Interval'; @@ -254,36 +259,36 @@ $lang['user']['edit'] = 'Upravit'; $lang['user']['remove'] = 'Smazat'; $lang['user']['create_syncjob'] = 'Vytvořit novou synchronizační úlohu'; -$lang['start']['mailcow_apps_detail'] = 'Použijte mailcow aplikace pro přístup k vašim e-mailům, kalendáři, kontaktům a dalším funkcím.'; -$lang['start']['mailcow_panel_detail'] = '<b>Administrátoři domén</b> mohou vytvářet, upravovat nebo mazat mailboxy a aliasy. Dále mohou upravovat parametry domény a zobrazovat další informace o jejich přidělených doménách.<br> -<b>Uživatelé</b> mohou vytvářet časově omezené aliasy (spam aliases), měnit jejich heslo a nastavovat spam filtr.'; -$lang['start']['imap_smtp_server_auth_info'] = 'Použijte prosím vaší celou e-mailovou adresu a způsob ověřování PLAIN.<br> +$lang['start']['mailcow_apps_detail'] = 'Použijte aplikace pro přístup k vašim e-mailům, kalendáři, kontaktům atd.'; +$lang['start']['mailcow_panel_detail'] = '<b>Správci domén</b> mohou vytvářet, upravovat nebo mazat schránky a aliasy, upravovat parametry domén a zobrazovat další informace o svých přidělených doménách.<br> +<b>Uživatelé</b> mohou vytvářet dočasné aliasy (spam aliases), měnit svá hesla a nastavovat filtr spamu.'; +$lang['start']['imap_smtp_server_auth_info'] = 'Použijte vaši celou e-mailovou adresu a zvolte způsob ověřování PLAIN.<br> Vaše přihlašovací údaje budou zašifrovány na straně serveru.'; -$lang['start']['help'] = 'Zobrazit/Skrýt panel nápovědy'; +$lang['start']['help'] = 'Zobrazit/skrýt panel nápovědy'; $lang['header']['mailcow_settings'] = 'Nastavení'; $lang['header']['administration'] = 'Hlavní nastavení'; $lang['header']['mailboxes'] = 'Nastavení pošty'; $lang['header']['user_settings'] = 'Uživatelská nastavení'; $lang['header']['quarantine'] = "Karanténa"; $lang['header']['debug'] = "Systémové informace"; -$lang['quarantine']['disabled_by_config'] = "Funkce karanténa je momentálně zakázaná."; -$lang['mailbox']['tls_policy_maps'] = 'Přetěžování TLS pravidel'; -$lang['mailbox']['tls_policy_maps_long'] = 'Přetěžování odchozích TLS pravidel (TLS policy map overrides).'; -$lang['mailbox']['tls_policy_maps_info'] = 'Tato mapa pravidel přetěžuje odchozí transportní TLS pravidla nezávisle na TLS nastavení uživatele.<br> - Prosím prostudujte <a href="http://www.postfix.org/postconf.5.html#smtp_tls_policy_maps" target="_blank">the "smtp_tls_policy_maps" dokumentaci</a> pro další informace.'; -$lang['mailbox']['tls_enforce_in'] = 'Vynucení TLS pro příchozí'; -$lang['mailbox']['tls_enforce_out'] = 'Vynucení TLS pro odchozí'; +$lang['quarantine']['disabled_by_config'] = "Funkce karanténa je momentálně vypnuta v nastavení systému."; +$lang['mailbox']['tls_policy_maps'] = 'Mapování TLS pravidel'; +$lang['mailbox']['tls_policy_maps_long'] = 'Přepisování pravidel odchozího TLS'; +$lang['mailbox']['tls_policy_maps_info'] = 'Tato mapa přepisuje pravidla odchozího TLS na transportech nezávisle na TLS nastavení uživatele.<br> + Pro více informací prosím prostudujte <a href="http://www.postfix.org/postconf.5.html#smtp_tls_policy_maps" target="_blank">dokumentaci k "smtp_tls_policy_maps"</a>.'; +$lang['mailbox']['tls_enforce_in'] = 'Vynutit TLS pro příchozí'; +$lang['mailbox']['tls_enforce_out'] = 'Vynutit TLS pro odchozí'; $lang['mailbox']['tls_map_dest'] = 'Cíl'; $lang['mailbox']['tls_map_dest_info'] = 'Příklady: example.org, .example.org, mail@example.org, [mail.example.org]:25'; $lang['mailbox']['tls_map_policy'] = 'Pravidlo'; $lang['mailbox']['tls_map_parameters'] = 'Parametry'; $lang['mailbox']['tls_map_parameters_info'] = 'Prázdné nebo parametry, například: protocols=!SSLv2 ciphers=medium exclude=3DES'; -$lang['mailbox']['booking_0'] = 'Vždy zobrazovat jako volný'; -$lang['mailbox']['booking_lt0'] = 'Neomezený, ale při rezervaci je zaneprázdněn (booked)'; -$lang['mailbox']['booking_custom'] = 'Hard-limit na vlastní počet rezervací'; +$lang['mailbox']['booking_0'] = 'Vždy ukazovat jako volný'; +$lang['mailbox']['booking_lt0'] = 'Neomezený, ale po rezervaci se ukazuje jako zabraný'; +$lang['mailbox']['booking_custom'] = 'Pevný limit na určitý počet rezervací'; $lang['mailbox']['booking_0_short'] = 'Vždy volný'; -$lang['mailbox']['booking_lt0_short'] = 'Soft limit'; -$lang['mailbox']['booking_custom_short'] = 'Hard limit'; +$lang['mailbox']['booking_lt0_short'] = 'Volný limit'; +$lang['mailbox']['booking_custom_short'] = 'Pevný limit'; $lang['mailbox']['domain'] = 'Doména'; $lang['mailbox']['spam_aliases'] = 'Dočasný alias'; $lang['mailbox']['multiple_bookings'] = 'Vícenásobné rezervace'; @@ -295,6 +300,7 @@ $lang['mailbox']['domains'] = 'Domény'; $lang['admin']['domain'] = 'Doména'; $lang['admin']['domain_s'] = 'Doména/y'; $lang['mailbox']['mailboxes'] = 'Poštovní schránky'; +$lang['mailbox']['mailbox'] = 'Poštovní schránka'; $lang['mailbox']['resources'] = 'Zdroje'; $lang['mailbox']['mailbox_quota'] = 'Max. velikost poštovní schránky'; $lang['mailbox']['domain_quota'] = 'Kvóta'; @@ -304,7 +310,7 @@ $lang['mailbox']['backup_mx'] = 'Záložní MX'; $lang['mailbox']['domain_aliases'] = 'Doménové aliasy'; $lang['mailbox']['target_domain'] = 'Cílová doména'; $lang['mailbox']['target_address'] = 'Cílová adresa'; -$lang['mailbox']['username'] = 'Jméno uživatele'; +$lang['mailbox']['username'] = 'Uživatelské jméno'; $lang['mailbox']['fname'] = 'Celé jméno'; $lang['mailbox']['filter_table'] = 'Tabulka filtrů'; $lang['mailbox']['yes'] = '✓'; @@ -320,7 +326,7 @@ $lang['mailbox']['add_domain_alias'] = 'Přidat doménový alias'; $lang['mailbox']['add_mailbox'] = 'Přidat poštovní schránku'; $lang['mailbox']['add_resource'] = 'Přidat zdroj'; $lang['mailbox']['add_alias'] = 'Přidat alias'; -$lang['mailbox']['add_domain_record_first'] = 'Prosím vytvořte nejdříve doménu'; +$lang['mailbox']['add_domain_record_first'] = 'Prosím nejdříve vytvořte doménu'; $lang['mailbox']['empty'] = 'Žádné výsledky'; $lang['mailbox']['toggle_all'] = 'Označit vše'; $lang['mailbox']['quick_actions'] = 'Akce'; @@ -332,99 +338,137 @@ $lang['mailbox']['last_run'] = 'Naposledy spuštěno'; $lang['mailbox']['excludes'] = 'Vyloučené'; $lang['mailbox']['last_run_reset'] = 'Plánovat další'; $lang['mailbox']['sieve_info'] = 'Můžete uložit více filtrů pro každého uživatele, ale současně může být aktivní pouze jeden prefilter a jeden postfilter.<br> -Každý filtr bude zpracován v daném pořadí. Ani chyba při vykonávání skriptu nebo snaha o pozdržení nezastaví vykonání dalších skriptů.<br> -Prefilter → Uživatelské skripty → Postfilter → <a href="https://github.com/mailcow/mailcow-dockerized/blob/master/data/conf/dovecot/sieve_after" target="_blank">global sieve postfilter</a>'; -$lang['info']['no_action'] = 'Není použitelná žádná akce'; +Každý filtr bude proveden v daném pořadí. Ani chyba při vykonávání skriptu nebo snaha o pozdržení nezastaví vykonání dalších skriptů.<br> +<a href="https://github.com/mailcow/mailcow-dockerized/blob/master/data/conf/dovecot/global_sieve_before" target="_blank">Global sieve prefilter</a> → Prefilter → Uživatelské skripty → Postfilter → <a href="https://github.com/mailcow/mailcow-dockerized/blob/master/data/conf/dovecot/global_sieve_after" target="_blank">Global sieve postfilter</a>'; +$lang['info']['no_action'] = 'Žádná použitelná akce'; + $lang['edit']['syncjob'] = 'Upravit synchronizační úlohu'; $lang['edit']['client_id'] = 'ID klienta'; $lang['edit']['client_secret'] = 'Client secret'; -$lang['edit']['scope'] = 'Rozsah (Scope)'; -$lang['edit']['grant_types'] = 'Grant types'; -$lang['edit']['redirect_uri'] = 'Redirect/Callback URL'; -$lang['edit']['hostname'] = 'Jméno hostitele (Hostname)'; +$lang['edit']['scope'] = 'Rozsah'; +$lang['edit']['grant_types'] = 'Typy udělení'; +$lang['edit']['redirect_uri'] = 'URL pro přesměrování/callback'; +$lang['edit']['hostname'] = 'Jméno hostitele'; $lang['edit']['encryption'] = 'Šifrování'; $lang['edit']['maxage'] = 'Maximální stáří zpráv ve dnech, které budou staženy ze vzdáleného umístění<br><small>(0 = ignorovat stáří)</small>'; $lang['edit']['maxbytespersecond'] = 'Max. bajtů za sekundu <br><small>(0 = neomezeno)</small>'; -$lang['edit']['automap'] = 'Pokusit se automaticky mapovat složky ("Sent items", "Sent" => "Sent" etc.)'; -$lang['edit']['skipcrossduplicates'] = 'Přeskočit duplicitní zprávy mezi složkami (first come, first serve)'; -$lang['add']['automap'] = 'Pokusit se automaticky mapovat složky ("Sent items", "Sent" => "Sent" etc.)'; -$lang['add']['skipcrossduplicates'] = 'Přeskočit duplicitní zprávy mezi složkami (first come, first serve)'; +$lang['edit']['automap'] = 'Pokusit se automaticky mapovat složky ("Sent items", "Sent" => "Sent" atd.)'; +$lang['edit']['skipcrossduplicates'] = 'Přeskočit duplicitní zprávy ("první přijde, první mele")'; +$lang['add']['automap'] = 'Pokusit se automaticky mapovat složky ("Sent items", "Sent" => "Sent" atd.)'; +$lang['add']['skipcrossduplicates'] = 'Přeskočit duplicitní zprávy ("první přijde, první mele")'; $lang['edit']['subfolder2'] = 'Synchronizace do podsložky v cílovém umístění<br><small>(prázdné = nepoužívat podsložku)</small>'; $lang['edit']['mins_interval'] = 'Interval (min)'; $lang['edit']['exclude'] = 'Vyloučit objekty (regex)'; $lang['edit']['save'] = 'Uložit změny'; -$lang['edit']['username'] = 'Jméno uživatele'; -$lang['edit']['max_mailboxes'] = 'Max. množství poštovních schránek'; +$lang['edit']['username'] = 'Uživatelské jméno'; +$lang['edit']['max_mailboxes'] = 'Max. počet poštovních schránek'; $lang['edit']['title'] = 'Úprava objektu'; $lang['edit']['target_address'] = 'Cílová adresa/y <small>(odělené čárkou)</small>'; $lang['edit']['active'] = 'Aktivní'; -$lang['edit']['force_pw_update'] = 'Vynucení změny hesla při příštím přihlášení'; +$lang['edit']['gal'] = 'Globální seznam adres'; +$lang['add']['gal'] = 'Globální seznam adres'; +$lang['edit']['gal_info'] = 'Globální seznam adres obsahuje všechny objekty v doméně a uživatel jej nemůže měnit. Pokud je vypnut, budou v SOGo chybět informace o obsazenosti! <b>Při změně nutno restartovat SOGo.</b>'; +$lang['add']['gal_info'] = 'Globální seznam adres obsahuje všechny objekty v doméně a uživatel jej nemůže měnit. Pokud je vypnut, budou v SOGo chybět informace o obsazenosti! <b>Při změně nutno restartovat SOGo.</b>'; +$lang['edit']['force_pw_update'] = 'Vynutit změnu hesla při příštím přihlášení'; $lang['edit']['force_pw_update_info'] = 'Uživatel se bude moci přihlásit pouze do administrace účtu.'; -$lang['edit']['sogo_access'] = 'Udělit přístup k Sogo'; +$lang['edit']['sogo_access'] = 'Udělit přístup k SOGo'; $lang['edit']['sogo_access_info'] = 'Toto nastavení neovlivňuje přístup k ostatním službám, ani nezmění existující profil uživatele SOGo.'; $lang['edit']['target_domain'] = 'Cílová doména'; $lang['edit']['password'] = 'Heslo'; $lang['edit']['password_repeat'] = 'Potvrzení nového hesla (opakujte)'; -$lang['edit']['domain_admin'] = 'Upravit doménového administrátora'; +$lang['edit']['domain_admin'] = 'Upravit správce domény'; $lang['edit']['domain'] = 'Úprava domény'; $lang['edit']['edit_alias_domain'] = 'Upravit doménový alias'; $lang['edit']['domains'] = 'Domény'; $lang['edit']['alias'] = 'Upravit alias'; $lang['edit']['mailbox'] = 'Úprava poštovní schránky'; $lang['edit']['description'] = 'Popis'; -$lang['edit']['max_aliases'] = 'Max. množství aliasů'; +$lang['edit']['max_aliases'] = 'Max. počet aliasů'; $lang['edit']['max_quota'] = 'Max. kvóta poštovní schránky (MiB)'; $lang['edit']['domain_quota'] = 'Kvóta domény'; $lang['edit']['backup_mx_options'] = 'Možnosti záložního MX'; -$lang['edit']['relay_domain'] = 'Přesměrování provozu (Relay) domény'; -$lang['edit']['relay_all'] = 'Přesměrování provozu (Relay) pro všechny příjemce'; -$lang['edit']['relay_all_info'] = '<small>Pokud se rozhodnete <b>nepřesměrovat</b> provoz pro všechny příjemce, bude nutné přidat prázdnou ("blind") poštovní schránku pro každého příjemce, který se má přesměrovávat.</small>'; +$lang['edit']['relay_domain'] = 'Přesměrování domény'; +$lang['edit']['relay_all'] = 'Přesměrování všech příjemců'; +$lang['edit']['relay_all_info'] = '<small>Pokud se rozhodnete <b>nepřesměrovat</b> všechny příjemce, musíte přidat prázdnou poštovní schránku pro každého příjemce, který se má přesměrovávat.</small>'; $lang['edit']['full_name'] = 'Celé jméno'; $lang['edit']['quota_mb'] = 'Kvóta (MiB)'; -$lang['edit']['sender_acl'] = 'Povolit odeslání jako'; -$lang['edit']['sender_acl_disabled'] = '↳ <span class="label label-danger">Sender check is disabled</span>'; -$lang['user']['sender_acl_disabled'] = '<span class="label label-danger">Sender check is disabled</span>'; +$lang['edit']['sender_acl'] = 'Povolit odesílání jako'; +$lang['edit']['sender_acl_disabled'] = '↳ <span class="label label-danger">Kontrola odesílatele vypnuta</span>'; +$lang['user']['sender_acl_disabled'] = '<span class="label label-danger">Kontrola odesílatele vypnuta</span>'; $lang['edit']['previous'] = 'Předchozí stránka'; $lang['edit']['unchanged_if_empty'] = 'Pokud se nemění, ponechte prázdné'; -$lang['edit']['dont_check_sender_acl'] = "Disable sender check for domain %s (+ alias domains)"; +$lang['edit']['dont_check_sender_acl'] = "Vypnout kontrolu odesílatele pro doménu %s (+ doménové aliasy)"; $lang['edit']['multiple_bookings'] = 'Vícenásobné rezervace'; $lang['edit']['kind'] = 'Druh'; -$lang['edit']['resource'] = 'Zdroje'; +$lang['edit']['resource'] = 'Zdroj'; $lang['edit']['relayhost'] = 'Přeposílání závislé na odesílateli'; - +$lang['edit']['public_comment'] = 'Veřejný komentář'; +$lang['mailbox']['public_comment'] = 'Veřejný komentář'; +$lang['edit']['private_comment'] = 'Soukromý komentář'; +$lang['mailbox']['private_comment'] = 'Soukromý komentář'; +$lang['edit']['comment_info'] = 'Soukromý komentář se nezobrazí uživateli; veřejný komentář se zobrazí jako nápověda při zastavení se kurzorem v přehledu uživatelů'; +$lang['add']['public_comment'] = 'Veřejný komentář'; +$lang['add']['private_comment'] = 'Soukromý komentář'; +$lang['add']['comment_info'] = 'Soukromý komentář se nezobrazí uživateli; veřejný komentář se zobrazí jako nápověda při zastavení se kurzorem v přehledu uživatelů'; $lang['acl']['spam_alias'] = 'Dočasné aliasy'; $lang['acl']['tls_policy'] = 'Pravidla TLS'; -$lang['acl']['spam_score'] = 'Spam skóre'; +$lang['acl']['spam_score'] = 'Skóre spamu'; $lang['acl']['spam_policy'] = 'Blacklist/Whitelist'; -$lang['acl']['delimiter_action'] = 'Delimiter akce'; +$lang['acl']['delimiter_action'] = 'Oddělovač akce'; $lang['acl']['syncjobs'] = 'Synchronizační úlohy'; $lang['acl']['eas_reset'] = 'Resetování EAS zařízení'; $lang['acl']['sogo_profile_reset'] = 'Resetování profilu SOGo'; $lang['acl']['quarantine'] = 'Karanténa'; -$lang['acl']['login_as'] = 'Přihlásit jako uživatel poštovní schránky'; -$lang['acl']['bcc_maps'] = 'BCC maps'; +$lang['acl']['quarantine_notification'] = 'Upozornění z karantény'; +$lang['acl']['quarantine_attachments'] = 'Přílohy v karanténě'; +$lang['acl']['alias_domains'] = 'Přidat doménové aliasy'; +$lang['acl']['login_as'] = 'Přihlásit se jako uživatel poštovní schránky'; +$lang['acl']['bcc_maps'] = 'BCC mapy'; $lang['acl']['filters'] = 'Filtry'; $lang['acl']['ratelimit'] = 'Omezování provozu'; -$lang['acl']['recipient_maps'] = 'Recipient maps'; +$lang['acl']['recipient_maps'] = 'Mapy příjemců'; +$lang['acl']['unlimited_quota'] = 'Neomezené kvóty pro poštovní schránky'; +$lang['acl']['extend_sender_acl'] = 'Povolit rozšíření ACL odesílatele o externí adresy'; $lang['acl']['prohibited'] = 'Zakázáno z důvodu ACL'; +$lang['edit']['extended_sender_acl'] = 'Externí adresy odesílatele'; +$lang['edit']['extended_sender_acl_info'] = 'Je dobré importovat DKIM klíč domény, pokud existuje.<br> + Nezapoměňte přidat tento server do příslušného záznamu SPF TXT.<br> + Je-li na tomto serveru vytvořena doména nebo doménový alias, který se shoduje s externí adresou, je tato externí adresa smazána.<br> + Použije formát @domain.tld, chcete-li odesílat jako *@domain.tld.'; +$lang['edit']['sender_acl_info'] = 'Má-li uživatel schránky A dovoleno odesílat jako uživatel schránky B, nezobrazuje se adresa odesílatele B v seznamu "Od" v SOGo automaticky.<br> + Uživatel schránky A musí nejdříve v SOGo vytvořit pověření, jež umožní uživateli B vybrat svou adresu jako odesílatele. Tento mechanismus neplatí pro aliasy.'; + +$lang['mailbox']['quarantine_notification'] = 'Upozornění z karantény'; +$lang['mailbox']['never'] = 'Nikdy'; +$lang['mailbox']['hourly'] = 'Každou hodinu'; +$lang['mailbox']['daily'] = 'Každý den'; +$lang['mailbox']['weekly'] = 'Každý týden'; +$lang['user']['quarantine_notification'] = 'Upozornění z karantény'; +$lang['user']['never'] = 'Nikdy'; +$lang['user']['hourly'] = 'Každou hodinu'; +$lang['user']['daily'] = 'Každý den'; +$lang['user']['weekly'] = 'Každý týden'; +$lang['user']['quarantine_notification_info'] = 'Jakmile se upozornění odešle, budou příslušné položky vyznačeny jako "upozorněné" a nebude pro ně odesláno žádné další upozornění.'; + $lang['add']['generate'] = 'generovat'; $lang['add']['syncjob'] = 'Přidat synchronizační úlohu'; -$lang['add']['syncjob_hint'] = 'Upozornění: Heslo bude uloženo v prostém textu!'; -$lang['add']['hostname'] = 'Jméno hostitele (Host)'; +$lang['add']['syncjob_hint'] = 'Upozornění: Heslo bude uloženo jako prostý text!'; +$lang['add']['hostname'] = 'Jméno hostitele'; $lang['add']['destination'] = 'Cíl'; -$lang['add']['nexthop'] = 'Další skok (Next hop)'; -$lang['edit']['nexthop'] = 'Další skok (Next hop)'; +$lang['add']['nexthop'] = 'Další skok'; +$lang['edit']['nexthop'] = 'Další skok'; $lang['add']['port'] = 'Port'; -$lang['add']['username'] = 'Jméno uživatele'; -$lang['add']['enc_method'] = 'Metoda šifrování'; -$lang['add']['mins_interval'] = 'Interval dotazu (Polling interval) (minuty)'; -$lang['add']['exclude'] = 'Vyloučit objekty (regex)'; +$lang['add']['username'] = 'Uživatelské jméno'; +$lang['add']['enc_method'] = 'Způsob šifrování'; +$lang['add']['mins_interval'] = 'Interval kontroly (minuty)'; +$lang['add']['exclude'] = 'Vyloučené objekty (regex)'; $lang['add']['delete2duplicates'] = 'Odstranit duplicity v cílovém místě'; $lang['add']['delete1'] = 'Odstranit ze zdroje po dokončení'; $lang['add']['delete2'] = 'Smazat zprávy v cíli, které nejsou ve zdroji'; $lang['add']['custom_params'] = 'Vlastní parametry'; +$lang['add']['custom_params_hint'] = 'Správně: --param=xy, špatně: --param xy'; $lang['add']['subscribeall'] = 'Odebírat všechny složky'; $lang['add']['timeout1'] = 'Časový limit pro připojení ke vzdálenému hostiteli'; $lang['add']['timeout2'] = 'Časový limit pro připojení k lokálnímu hostiteli'; @@ -435,21 +479,21 @@ $lang['edit']['delete2duplicates'] = 'Odstranit duplicity v cílovém místě'; $lang['edit']['delete1'] = 'Odstranit ze zdroje po dokončení'; $lang['edit']['delete2'] = 'Smazat zprávy v cíli, které nejsou ve zdroji'; -$lang['add']['domain_matches_hostname'] = 'Doména %s se shoduje s hostname'; +$lang['add']['domain_matches_hostname'] = 'Doména %s se shoduje s názvem hostitele'; $lang['add']['domain'] = 'Doména'; $lang['add']['active'] = 'Aktivní'; $lang['add']['multiple_bookings'] = 'Vícenásobné rezervace'; $lang['add']['description'] = 'Popis'; -$lang['add']['max_aliases'] = 'Max. množství aliasů'; -$lang['add']['max_mailboxes'] = 'Max. množství poštovních schránek'; +$lang['add']['max_aliases'] = 'Max. počet aliasů'; +$lang['add']['max_mailboxes'] = 'Max. počet poštovních schránek'; $lang['add']['mailbox_quota_m'] = 'Max. kvóta poštovní schránky (MiB)'; $lang['add']['domain_quota_m'] = 'Celková kvóta domény (MiB)'; $lang['add']['backup_mx_options'] = 'Možnosti záložního MX'; -$lang['add']['relay_all'] = 'Přesměrování provozy (Relay) pro všechny příjemce'; -$lang['add']['relay_domain'] = 'Přesměrování provozu domény'; -$lang['add']['relay_all_info'] = '<small>Pokud se rozhodnete <b>nepřesměrovat</b> provoz pro všechny příjemce, bude nutné přidat prázdnou ("blind") poštovní schránku pro každého příjemce, který se má přesměrovávat.</small>'; +$lang['add']['relay_all'] = 'Přesměrování všech příjemců'; +$lang['add']['relay_domain'] = 'Přesměrování domény'; +$lang['add']['relay_all_info'] = '<small>Pokud se rozhodnete <b>nepřesměrovat</b> všechny příjemce, musíte přidat prázdnou poštovní schránku pro každého příjemce, který se má přesměrovávat.</small>'; $lang['add']['alias_address'] = 'Adresa/y aliasů'; -$lang['add']['alias_address_info'] = '<small>Kompletní email adresa/y nebo @example.com pro zachycení všech zpráv pro doménu (oddělené čárkami). <b>pouze mailcow domény</b>.</small>'; +$lang['add']['alias_address_info'] = '<small>Kompletní emailová adresa/y, nebo @example.com pro zachycení všech zpráv pro doménu (oddělené čárkami). <b>Pouze domény v systému mailcow</b>.</small>'; $lang['add']['alias_domain_info'] = '<small>Platné názvy domén (oddělené čárkami).</small>'; $lang['add']['target_address'] = 'Cílové adresy'; $lang['add']['target_address_info'] = '<small>Kompletní email adresa/y (oddělené čárkami).</small>'; @@ -457,7 +501,7 @@ $lang['add']['alias_domain'] = 'Doménový alias'; $lang['add']['select'] = 'Prosím vyberte...'; $lang['add']['target_domain'] = 'Cílová doména'; $lang['add']['kind'] = 'Druh'; -$lang['add']['mailbox_username'] = 'Jméno uživatele (levá část email adresy)'; +$lang['add']['mailbox_username'] = 'Uživatelské jméno (levá část email adresy)'; $lang['add']['full_name'] = 'Celé jméno'; $lang['add']['quota_mb'] = 'Kvóta (MiB)'; $lang['add']['select_domain'] = 'Nejdříve vyberte doménu'; @@ -475,17 +519,18 @@ $lang['add']['sieve_desc'] = 'Krátký popis'; $lang['edit']['sieve_desc'] = 'Krátký popis'; $lang['add']['sieve_type'] = 'Typ filtru'; $lang['edit']['sieve_type'] = 'Typ filtru'; -$lang['mailbox']['set_prefilter'] = 'Označit jako (prefilter)'; -$lang['mailbox']['set_postfilter'] = 'Označit jako (postfilter)'; +$lang['mailbox']['set_prefilter'] = 'Označit jako pre-filtr'; +$lang['mailbox']['set_postfilter'] = 'Označit jako post-filtr)'; $lang['mailbox']['filters'] = 'Filtry'; $lang['mailbox']['sync_jobs'] = 'Synchronizační úlohy'; $lang['mailbox']['inactive'] = 'Neaktivní'; $lang['edit']['validate_save'] = 'Ověřit a uložit'; -$lang['login']['username'] = 'Jméno uživatele'; + +$lang['login']['username'] = 'Uživatelské jméno'; $lang['login']['password'] = 'Heslo'; $lang['login']['login'] = 'Přihlásit'; -$lang['login']['delayed'] = 'Přihlášení bylo pozdrženo o %s sekund.'; +$lang['login']['delayed'] = 'Přihlášení zpožděno o %s sekund.'; $lang['tfa']['tfa'] = "Dvoufaktorové ověřování"; $lang['tfa']['set_tfa'] = "Nastavení způsobu dvoufaktorového ověřování"; @@ -498,20 +543,20 @@ $lang['tfa']['none'] = "Deaktivovat"; $lang['tfa']['delete_tfa'] = "Zakázat TFA"; $lang['tfa']['disable_tfa'] = "Zakázat TFA do příštího úspěšného přihlášení"; $lang['tfa']['confirm'] = "Potvrdit"; -$lang['tfa']['totp'] = "Časově založené OTP (Google ověřování apod.)"; +$lang['tfa']['totp'] = "Časově založené OTP (Google Authenticator, Authy apod.)"; $lang['tfa']['select'] = "Prosím vyberte..."; -$lang['tfa']['waiting_usb_auth'] = "<i>Probíhá čekání na USB zařízení...</i><br><br>Prosím stiskněte tlačítko na vašem U2F USB zařízení."; -$lang['tfa']['waiting_usb_register'] = "<i>Probíhá čekání na USB zařízení...</i><br><br>Prosím zadejte vaše heslo výše a potvrďte U2F registraci stiskem tlačítka na vašem U2F USB zařízení."; -$lang['tfa']['scan_qr_code'] = "Prosím oscanujte následující kód vaší aplikací na ověřování nebo zadejte kód ručně."; -$lang['tfa']['enter_qr_code'] = "Výš kód TOTP pokud vaše zařízení nemůže scanovat QR kódy"; +$lang['tfa']['waiting_usb_auth'] = "<i>Čeká se na USB zařízení...</i><br><br>Prosím stiskněte tlačítko na svém U2F USB zařízení."; +$lang['tfa']['waiting_usb_register'] = "<i>Čeká se na USB zařízení...</i><br><br>Prosím zadejte své heslo výše a potvrďte U2F registraci stiskem tlačítka na svém U2F USB zařízení."; +$lang['tfa']['scan_qr_code'] = "Prosím načtěte následující kód svou aplikací na ověřování nebo zadejte kód ručně."; +$lang['tfa']['enter_qr_code'] = "Váš kód TOTP, pokud vaše zařízení neumí číst QR kódy"; $lang['tfa']['confirm_totp_token'] = "Prosím potvrďte změny zadáním vygenerovaného tokenu"; $lang['admin']['rspamd-com_settings'] = '<a href="https://rspamd.com/doc/configuration/settings.html#settings-structure" target="_blank">Rspamd dokumentace</a> - - Název nastavení bude automaticky generován, viz níže uvedené předvolby.'; + - Název nastavení bude automaticky vygenerován, viz níže uvedené předvolby.'; -$lang['admin']['queue_manager'] = 'Správce fronty'; $lang['admin']['no_new_rows'] = 'Žádné další řádky nejsou k dispozici'; -$lang['admin']['additional_rows'] = ' řádků přidáno'; // parses to 'n additional rows were added' +$lang['admin']['queue_manager'] = 'Správce fronty'; +$lang['admin']['additional_rows'] = ' dalších řádků přidáno'; // parses to 'n additional rows were added' $lang['admin']['private_key'] = 'Soukromý klíč'; $lang['admin']['import'] = 'Importovat'; $lang['admin']['duplicate'] = 'Duplikovat'; @@ -519,22 +564,22 @@ $lang['admin']['import_private_key'] = 'Importovat soukromý klíč'; $lang['admin']['duplicate_dkim'] = 'Duplikovat DKIM záznam'; $lang['admin']['dkim_from'] = 'Od'; $lang['admin']['dkim_to'] = 'Komu'; -$lang['admin']['dkim_from_title'] = 'Zdrojová doména z které se budou kopírovat data'; -$lang['admin']['dkim_to_title'] = 'Cílová doména/y - budou přepsány'; -$lang['admin']['f2b_parameters'] = 'Fail2ban'; +$lang['admin']['dkim_from_title'] = 'Zdrojová doména, z níž se budou kopírovat data'; +$lang['admin']['dkim_to_title'] = 'Cílová doména/y - bude přepsáno'; +$lang['admin']['f2b_parameters'] = 'Nastavení Fail2ban'; $lang['admin']['f2b_ban_time'] = 'Čas blokování (s)'; $lang['admin']['f2b_max_attempts'] = 'Max. pokusů'; -$lang['admin']['f2b_retry_window'] = 'Retry window (s). Interval během kterého se počítají neúspěšné pokusy o přihlášení'; -$lang['admin']['f2b_netban_ipv4'] = 'Rozsah IPv4 podsítě pro provedení blokování (8-32)'; -$lang['admin']['f2b_netban_ipv6'] = 'Rozsah IPv6 podsítě pro provedení blokování (8-128)'; -$lang['admin']['f2b_whitelist'] = 'Sítě/klienti na Whitelistu'; -$lang['admin']['f2b_blacklist'] = 'Sítě/klienti na Blacklistu'; -$lang['admin']['f2b_list_info'] = 'Sít nebo klienti na blacklistu budou mít vždy větší váhu než položky na whitelistu. Záznamy z blacklistu jsou vytvářeny při startu kontejneru. Záznamy ve whitelistu jsou čteny pokaždé, když je aplikováno blokování.'; +$lang['admin']['f2b_retry_window'] = 'Časový horizont pro maximum pokusů (s)'; +$lang['admin']['f2b_netban_ipv4'] = 'Rozsah IPv4 podsítě k zablokování (8-32)'; +$lang['admin']['f2b_netban_ipv6'] = 'Rozsah IPv6 podsítě k zablokování (8-128)'; +$lang['admin']['f2b_whitelist'] = 'Sítě/hostitelé na whitelistu'; +$lang['admin']['f2b_blacklist'] = 'Sítě/hostitelé na blacklistu'; +$lang['admin']['f2b_list_info'] = 'Síť nebo hostitelé na blacklistu budou mít vždy větší váhu než položky na whitelistu. Záznamy z blacklistu jsou vytvářeny při startu kontejneru.'; $lang['admin']['search_domain_da'] = 'Hledat domény'; -$lang['admin']['r_inactive'] = 'Neaktivní restrikce'; -$lang['admin']['r_active'] = 'Aktivní restrikce'; -$lang['admin']['r_info'] = 'Šedé/zakázané položky v seznamu aktivních omezení nejsou mailcow systému známy jako platná a nelze je přesouvat. Neznámá omezení budou stejně seřazena dle času jejich výskytu. <br>Pro nastavení lze přidat nové záznamy do <code>inc/vars.local.inc.php</code>.'; -$lang['admin']['dkim_key_length'] = 'Délka DKIM klíče (bits)'; +$lang['admin']['r_inactive'] = 'Neaktivní omezení'; +$lang['admin']['r_active'] = 'Aktivní omezení'; +$lang['admin']['r_info'] = 'Šedé/vypnuté položky v seznamu aktivních omezení neuznává mailcow jako platná a nelze je přesouvat. Neznámá omezení budou však budou stejně seřazena tak, jak jdou za sebou. <br>Nové prvky lze přidat do <code>inc/vars.local.inc.php</code> a pak je zde přepínat.'; +$lang['admin']['dkim_key_length'] = 'Délka DKIM klíče (v bitech)'; $lang['admin']['dkim_key_valid'] = 'Klíč je platný'; $lang['admin']['dkim_key_unused'] = 'Klíč nepoužitý'; $lang['admin']['dkim_key_missing'] = 'Klíč chybí'; @@ -552,35 +597,35 @@ $lang['admin']['password_repeat'] = 'Potvrzení nového hesla (opakujte)'; $lang['admin']['active'] = 'Aktivní'; $lang['admin']['inactive'] = 'Neaktivní'; $lang['admin']['action'] = 'Akce'; -$lang['admin']['add_domain_admin'] = 'Přidat doménového administrátora'; -$lang['admin']['add_admin'] = 'Přidat administrátora'; +$lang['admin']['add_domain_admin'] = 'Přidat správce domény'; +$lang['admin']['add_admin'] = 'Přidat správce'; $lang['admin']['add_settings_rule'] = 'Přidat nastavení'; $lang['admin']['rsetting_desc'] = 'Krátký popis'; $lang['admin']['rsetting_content'] = 'Obsah pravidla'; $lang['admin']['rsetting_none'] = 'Žádné pravidlo není k dispozici'; $lang['admin']['rsetting_no_selection'] = 'Prosím vyberte pravidlo'; -$lang['admin']['rsettings_preset_1'] = 'Pro přihlášené uživatele vypnuto vše kromě DKIM a omezování provozu'; -$lang['admin']['rsettings_preset_2'] = 'Poštmistři chtějí spam'; +$lang['admin']['rsettings_preset_1'] = 'Pro přihlášené uživatele vypnout vše kromě DKIM a omezení provozu'; +$lang['admin']['rsettings_preset_2'] = 'Postmasteři chtějí dostávat spam'; $lang['admin']['rsettings_insert_preset'] = 'Vložit příklad nastavení "%s"'; $lang['admin']['rsetting_add_rule'] = 'Přidat pravidlo'; $lang['admin']['queue_ays'] = 'Potvrďte prosím, že chcete odstranit všechny položky z aktuální fronty.'; -$lang['admin']['arrival_time'] = 'Čas zařazení do fronty (čas serveru)'; +$lang['admin']['arrival_time'] = 'Čas zařazení do fronty (čas na serveru)'; $lang['admin']['message_size'] = 'Velikost zprávy'; $lang['admin']['sender'] = 'Odesílatel'; $lang['admin']['recipients'] = 'Příjemci'; -$lang['admin']['admin_domains'] = 'Přidělěné domény'; -$lang['admin']['domain_admins'] = 'Administrátoři domén'; -$lang['admin']['flush_queue'] = 'Zkusit opětovně doručit frontu. (Flush queue)'; +$lang['admin']['admin_domains'] = 'Přidělené domény'; +$lang['admin']['domain_admins'] = 'Správci domén'; +$lang['admin']['flush_queue'] = 'Vyprázdnit frontu (opětovně doručit)'; $lang['admin']['delete_queue'] = 'Smazat vše'; $lang['admin']['queue_deliver_mail'] = 'Doručit'; $lang['admin']['queue_hold_mail'] = "Zadržet"; $lang['admin']['queue_unhold_mail'] = 'Propustit'; -$lang['admin']['username'] = 'Jméno uživatele'; +$lang['admin']['username'] = 'Uživatelské jméno'; $lang['admin']['edit'] = 'Upravit'; $lang['admin']['remove'] = 'Smazat'; $lang['admin']['save'] = 'Uložit změny'; -$lang['admin']['admin'] = 'Administrator'; -$lang['admin']['admin_details'] = 'Administrátoři mailserveru'; +$lang['admin']['admin'] = 'Správce'; +$lang['admin']['admin_details'] = 'Upravit správce'; $lang['admin']['unchanged_if_empty'] = 'Pokud se nemění, ponechte prázdné'; $lang['admin']['yes'] = '✓'; $lang['admin']['no'] = '✕'; @@ -591,81 +636,93 @@ $lang['admin']['empty'] = 'Žádné výsledky'; $lang['admin']['time'] = 'Čas'; $lang['admin']['last_applied'] = 'Naposledy použité'; $lang['admin']['reset_limit'] = 'Odebrat hash'; -$lang['admin']['hash_remove_info'] = 'Odebrání hashe omezování provozu (pokud stále existuje) vyresetuje kompletně jeho čítač.<br> - Každý hash je označen jedinečnou barvou.'; +$lang['admin']['hash_remove_info'] = 'Odebrání hashe omezení provozu (pokud stále existuje) zcela vyresetuje jeho počítadlo.<br> + Každý hash je označen jinou barvou.'; $lang['warning']['hash_not_found'] = 'Hash nenalezen'; -$lang['success']['hash_deleted'] = 'Hash byl smazán'; +$lang['success']['hash_deleted'] = 'Hash smazán'; $lang['admin']['authed_user'] = 'Přihlášený uživatel'; $lang['admin']['priority'] = 'Priorita'; $lang['admin']['message'] = 'Zpráva'; -$lang['admin']['rate_name'] = 'Název (Rate name)'; +$lang['admin']['rate_name'] = 'Název'; $lang['admin']['refresh'] = 'Obnovit'; $lang['admin']['to_top'] = 'Zpět na začátek'; $lang['admin']['in_use_by'] = 'Používáno'; -$lang['admin']['forwarding_hosts'] = 'Přesměrování klientů (Forwarding Hosts)'; -$lang['admin']['forwarding_hosts_hint'] = 'Příchozí zprávy jsou bezpodmínečně akceptovány od všech zde uvedených klientů. Tito klienti nebudou kontrolováni proti DNSBL nebo podrobeni greylistingu. Spam obdržený od těchto klientů nebude nikdy odmítnut, ale příležitostně může být uložen do složky se spamem. Nejčastějším účelem je zadat poštovní servery, pro které jste nastavili pravidlo, které předává příchozí e-maily na váš poštovní server.'; -$lang['admin']['forwarding_hosts_add_hint'] = 'Lze zadat IPv4/IPv6 adresy, sítě ve formátu CIDR, názvy klientů (které budou převedeny na IP adresy) nebo názvy domén (které budou převedeny na IP dotazováním se DNS na SPF záznam nebo pokud neexistuje tak na MX záznam).'; -$lang['admin']['relayhosts_hint'] = 'Nastavte zde přeposílání závislé na odesílateli (sender-dependent transports), pro možnost je vybrat v okně pro nastavení domény.<br> -Služba přeposílání je vždy "smtp:". Individuální uživatelská nastavení odchozích TLS politik jsou také povolena.'; -$lang['admin']['transports_hint'] = 'Položky seznamu přeposílání (transport map) <b>přetěžují</b> položky seznamu přeposílání závislém na odesílateli (sender-dependent transport)</b>.<br> -Individuální uživatelská nastavení odchozích TLS politik jsou ignorována a lze je vynutit přes přetěžování odchozích TLS pravidel (TLS policy map overrides). Služba přeposílání je vždy "smtp:".<br> -Pro zjištění přihlašovacích údajů dalšího skoku "[host]:25" se Postfix <b>vždy</b> dotáže na "nexthop" před hledáním "[nexthop]:25". Toto chování znemožnuje použít "nexthop" a "[nexthop]:25" současně.'; -$lang['admin']['add_relayhost_hint'] = 'Upozornění: přihlašovací údaje (pokud existují) budou uloženy jako prostý text.'; -$lang['admin']['add_transports_hint'] = 'Upozornění: přihlašovací údaje budou uloženy jako prostý text.'; -$lang['admin']['host'] = 'Klient (Host)'; +$lang['admin']['forwarding_hosts'] = 'Přeposílající hostitelé'; +$lang['admin']['forwarding_hosts_hint'] = 'Příchozí zprávy od zde uvedených hostitelů jsou bezpodmínečně přijaty. Tito hostitelé nebudou kontrolováni na DNSBL, ani podrobeni greylistingu. Spam od těchto hostitelů se nikdy neodmítá, ale občas může skončit ve složce se spamem. Nejčastěji se používají pro poštovní servery, jež jsou nastaveny, aby předávaly příchozí e-maily na tento poštovní server mailcow.'; +$lang['admin']['forwarding_hosts_add_hint'] = 'Lze zadat IPv4/IPv6 adresy, sítě ve formátu CIDR, názvy hostitelů (budou převedeny na IP adresy) nebo názvy domén (budou převedeny na IP pomocí SPF záznamů, příp. MX záznamů).'; +$lang['admin']['relayhosts_hint'] = 'Zde definujte transporty závislé na odesílateli, jež pak můžete použít v nastavení domény.<br> +Protokol transportu je vždy "smtp:". Bere se v potaz uživatelské nastavení odchozího TLS.'; +$lang['admin']['transports_hint'] = '→ Položka transportní mapy <b>přebíjí</b> transportní mapu závislou na odesílateli</b>.<br> +→ Uživatelské nastavení odchozího TLS se ignoruje a lze je výhradně vynutit mapováním TLS pravidel.<br> +→ Protokol transportu je vždy "smtp:".<br> +→ Adresy, jež odpovídají výrazu "/localhost$/", se vždy předají přes "local:", takže nejsou zahrnuty do definice cíle "*".<br> +→ Pro stanovení přihlašovacích údajů dalšího skoku, např. "[host]:25", bude Postfix <b>vždy</b> hledat nejdříve "host" a teprve pak "[host]:25". Kvůli tomu nelze použít současně "host" a "[host]:25"'; +$lang['admin']['add_relayhost_hint'] = 'Pozor: jakékoliv přihlašovací údaje budou uloženy jako prostý text.'; +$lang['admin']['add_transports_hint'] = 'Pozor: jakékoliv přihlašovací údaje budou uloženy jako prostý text.'; +$lang['admin']['host'] = 'Hostitel'; $lang['admin']['source'] = 'Zdroj'; -$lang['admin']['add_forwarding_host'] = 'Přidat přesměrování klientů (Forwarding Hosts)'; -$lang['admin']['add_relayhost'] = 'Přidat přeposílání závislé na odesílateli (sender-dependent transport)'; -$lang['admin']['add_transport'] = 'Přidat přeposílání (Transport)'; -$lang['admin']['relayhosts'] = 'Přeposílání závislé na odesílateli (Sender-dependent transports)'; -$lang['admin']['transport_maps'] = 'Přeposílání (Transport Maps)'; +$lang['admin']['add_forwarding_host'] = 'Přidat předávajícího hostitele'; +$lang['admin']['add_relayhost'] = 'Přidat transport závislý na odesílateli'; +$lang['admin']['add_transport'] = 'Přidat transport'; +$lang['admin']['relayhosts'] = 'Transporty závislé na odesílateli'; +$lang['admin']['transport_maps'] = 'Mapy transportů'; $lang['admin']['routing'] = 'Směrování'; -$lang['admin']['credentials_transport_warning'] = '<b>Upozornění</b>: Přidání položky do seznamu přeposílání aktualizuje také přihlašovací údaje všech záznamů s odpovídajícím sloupcem (nexthop).'; +$lang['admin']['credentials_transport_warning'] = '<b>Upozornění</b>: Přidání položky do mapy transportů aktualizuje také přihlašovací údaje všech záznamů s odpovídajícím skokem.'; $lang['admin']['destination'] = 'Cíl'; -$lang['admin']['nexthop'] = 'Další skok (Next hop)'; +$lang['admin']['nexthop'] = 'Další skok'; -$lang['success']['forwarding_host_removed'] = "Přesměrovaný klient %s byl odebrán"; -$lang['success']['forwarding_host_added'] = "Přesměrovaný klient %s byl přidán"; -$lang['success']['relayhost_removed'] = "Položka seznamu přeposílání %s byla odebrána"; -$lang['success']['relayhost_added'] = "Položky seznamu přeposílání %s byla přidána"; +$lang['success']['forwarding_host_removed'] = "Předávající hostitel %s odebrán"; +$lang['success']['forwarding_host_added'] = "Předávající hostitel %s přidán"; +$lang['success']['relayhost_removed'] = "Položka %s odebrána"; +$lang['success']['relayhost_added'] = "Položky %s přidána"; $lang['diagnostics']['dns_records'] = 'DNS záznamy'; -$lang['diagnostics']['dns_records_24hours'] = 'Upozornění: Změnám provedeným v systému DNS může trvat až 24 hodin, než se na této stránce správně zobrazí jejich aktuální stav. Tato stránka je určena pro snadné zjištění, jak nakonfigurovat DNS záznamy a zda jsou všechny vaše záznamy správně uloženy.'; +$lang['diagnostics']['dns_records_24hours'] = 'Upozornění: Změnám v systému DNS může trvat až 24 hodin, než se na této stránce správně zobrazí jejich aktuální stav. Na této stránce můžete snadno zjistit, jak nastavit DNS záznamy a zda jsou všechny záznamy správně uloženy.'; $lang['diagnostics']['dns_records_name'] = 'Název'; $lang['diagnostics']['dns_records_type'] = 'Typ'; $lang['diagnostics']['dns_records_data'] = 'Správný záznam'; $lang['diagnostics']['dns_records_status'] = 'Současný stav'; $lang['diagnostics']['optional'] = 'Tento záznam je volitelný.'; -$lang['diagnostics']['cname_from_a'] = 'Hodnota odvozena z A/AAAA záznamu. Toto je podporováno dokud záznam míří na správný zdroj.'; +$lang['diagnostics']['cname_from_a'] = 'Hodnota odvozena z A/AAAA záznamu. Lze použít, pokud záznam ukazuje na správný zdroj.'; -$lang['admin']['relay_from'] = '"Od:" adresa'; -$lang['admin']['relay_run'] = "Provedení testu"; -$lang['admin']['api_allow_from'] = "Povolení přístupu k API z těchto IP adres (oddělené čárkou nebo novým řádkem)"; +$lang['admin']['relay_from'] = 'Adresa "Od:"'; +$lang['admin']['relay_run'] = "Provést test"; +$lang['admin']['api_allow_from'] = "Povolit přístup k API z těchto IP adres (oddělené čárkou nebo novým řádkem)"; $lang['admin']['api_key'] = "API klíč"; $lang['admin']['activate_api'] = "Zapnout API"; $lang['admin']['regen_api_key'] = "Generovat API klíč"; -$lang['admin']['ban_list_info'] = "Seznam blokovaných IP adres je zobrazen níže: <b>síť (zbývající čas blokování) - [akce]</b>.<br />IP adresy zařazené pro odblokování budou odebrány z aktivního seznamu během několika sekund.<br />Červeně označené položky jsou pernamentně blokované blacklistem."; +$lang['admin']['ban_list_info'] = "Seznam blokovaných IP adres je zobrazen níže: <b>síť (zbývající čas blokování) - [akce]</b>.<br />IP adresy zařazené pro odblokování budou z aktivního seznamu odebrány během několika sekund.<br />Červeně označené položky jsou pernamentní bloky z blacklistu."; $lang['admin']['unban_pending'] = "čeká na odblokování"; $lang['admin']['queue_unban'] = "odblokovat"; $lang['admin']['no_active_bans'] = "Žádná aktivní blokování"; $lang['admin']['quarantine'] = "Karanténa"; -$lang['admin']['quarantine_retention_size'] = "Počet zadržených zpráv na poštovní schránku<br />0 znamená <b>neaktivní</b>!"; -$lang['admin']['quarantine_max_size'] = "Maximální velikost v MiB (větší prvky budou smazány)<br />0 <b>neznamená</b> neomezeno!"; +$lang['admin']['rspamd_settings_map'] = "Nastavení Rspamd"; +$lang['admin']['quota_notifications'] = "Upozornění na kvóty"; +$lang['admin']['quota_notifications_vars'] = "{{percent}} se rovná aktuální kvótě<br>{{username}} je jméno poštovní schránky"; +$lang['admin']['active_rspamd_settings_map'] = "Aktivní nastavení"; +$lang['admin']['quota_notifications_info'] = "Upozornění na kvótu se uživateli odesílají při překročení 80 % a 95 % limitu."; +$lang['admin']['quarantine_retention_size'] = "Počet zadržených zpráv na poštovní schránku<br />0 znamená <b>neaktivní</b>."; +$lang['admin']['quarantine_max_size'] = "Maximální velikost v MiB (větší prvky budou smazány)<br />0 <b>neznamená</b> neomezeno."; $lang['admin']['quarantine_exclude_domains'] = "Vyloučené domény a doménové aliasy"; $lang['admin']['quarantine_release_format'] = "Formát propuštěných položek"; $lang['admin']['quarantine_release_format_raw'] = "Nezměněný originál"; $lang['admin']['quarantine_release_format_att'] = "Jako příloha"; - -$lang['admin']['ui_texts'] = "Úpravy UI textů"; -$lang['admin']['help_text'] = "Přetíží text nápovědy pod přihlašovacím formulářem (HTML povoleno)"; -$lang['admin']['title_name'] = 'Název webu ("mailcow UI" title)'; -$lang['admin']['main_name'] = 'Popis přihlašovacího formuláře ("mailcow UI")'; -$lang['admin']['apps_name'] = 'Popis sekce aplikací ("mailcow Apps")'; +$lang['admin']['quarantine_notification_sender'] = "Odesílatel upozornění"; +$lang['admin']['quarantine_notification_subject'] = "Předmět upozornění"; +$lang['admin']['quarantine_notification_html'] = "Šablona upozornění:<br><small>Ponechte prázdné, aby se obnovila výchozí šablona.</small>"; +$lang['admin']['quota_notification_sender'] = "Odesílatel upozornění"; +$lang['admin']['quota_notification_subject'] = "Předmět upozornění"; +$lang['admin']['quota_notification_html'] = "Šablona upozornění:<br><small>Ponechte prázdné, aby se obnovila výchozí šablona.</small>"; +$lang['admin']['ui_texts'] = "Hlavička a texty UI"; +$lang['admin']['help_text'] = "Přepsat text nápovědy pod přihlašovacím formulářem (HTML povoleno)"; +$lang['admin']['title_name'] = 'Titulek webu ("mailcow UI")'; +$lang['admin']['main_name'] = 'Název webu ("mailcow UI")'; +$lang['admin']['apps_name'] = 'Hlavička aplikací ("mailcow Apps")'; $lang['admin']['customize'] = "Přizpůsobení"; -$lang['admin']['change_logo'] = "Změna loga"; -$lang['admin']['logo_info'] = "Váš obrázek bude zmenšen na výšku 40 pixelů pro horní navigační lištu a na max. šířku 250px pro úvodní stránku."; +$lang['admin']['change_logo'] = "Změnit logo"; +$lang['admin']['logo_info'] = "Obrázek bude zmenšen na výšku 40 pixelů pro horní navigační lištu a na max. šířku 250 pixelů pro úvodní stránku."; $lang['admin']['upload'] = "Nahrát"; $lang['admin']['app_links'] = "Odkazy na aplikace"; $lang['admin']['app_name'] = "Název aplikace"; @@ -677,27 +734,30 @@ $lang['admin']['merged_vars_hint'] = 'Šedé řádky byly přidány z <code>vars $lang['mailbox']['waiting'] = "Čekání"; $lang['mailbox']['status'] = "Stav"; $lang['mailbox']['running'] = "Běží"; +$lang['mailbox']['enable_x'] = "Zapnout"; +$lang['mailbox']['disable_x'] = "Vypnout"; $lang['edit']['spam_score'] = "Nastavte vlastní skóre spamu"; $lang['user']['spam_score_reset'] = "Obnovit výchozí nastavení serveru"; $lang['edit']['spam_policy'] = "Přidat nebo odebrat položky whitelistu/blacklistu"; -$lang['edit']['spam_alias'] = "Vytvořit nebo změnit časově omezené (spam) aliasy"; +$lang['edit']['spam_alias'] = "Vytvořit nebo změnit dočasné aliasy"; -$lang['danger']['img_tmp_missing'] = "Nelze ověřit soubor s obrázkem: Dočasný soubor nenalezen"; +$lang['danger']['comment_too_long'] = "Moc dlouhý komentář, max. 160 znaků"; +$lang['danger']['img_tmp_missing'] = "Nelze ověřit soubor s obrázkem: dočasný soubor nebyl nalezen"; $lang['danger']['img_invalid'] = "Nelze ověřit soubor s obrázkem"; $lang['danger']['invalid_mime_type'] = "Špatný mime typ"; -$lang['success']['upload_success'] = "Soubor byl úspěšně nahrán"; +$lang['success']['upload_success'] = "Soubor úspěšně nahrán"; $lang['success']['app_links'] = "Změny odkazů na aplikace uloženy"; $lang['success']['ui_texts'] = "Změny UI textů uloženy"; $lang['success']['reset_main_logo'] = "Obnovit výchozí logo"; -$lang['success']['items_released'] = "Vybraná položka byla propuštěna"; -$lang['success']['item_released'] = "Položka %s byla propuštěna"; +$lang['success']['items_released'] = "Vybraná položka propuštěna"; +$lang['success']['item_released'] = "Položka %s propuštěna"; $lang['danger']['imagick_exception'] = "Chyba: Výjimka programu Imagick při čtení obrázku"; $lang['quarantine']['quarantine'] = "Karanténa"; $lang['quarantine']['learn_spam_delete'] = "Naučit jako spam a smazat"; $lang['quarantine']['qinfo'] = 'Karanténní systém uloží odmítnutou poštu do databáze a odesílatel <em>nebude</em> informován o nedoručené poště. - <br>"' . $lang['quarantine']['learn_spam_delete'] . '" naučí systém že zpráva je spam pomocí Bayes theoremu a také vypočítá "fuzzy hashes" pro zakázání podobných zpráv v budoucnu. - <br>Upozornění: Učení se vícero zpráv najednou může být v závislosti na vašem systému časově náročné.'; + <br>"' . $lang['quarantine']['learn_spam_delete'] . '" naučí systém, že zpráva je spam, pomocí Bayes teorému a také vypočítá "fuzzy hashes" pro odmítnutí podobných zpráv v budoucnu. + <br>Upozornění: Učení se vícera zpráv najednou může být v závislosti na výkonu systému časově náročné.'; $lang['quarantine']['release'] = "Propustit"; $lang['quarantine']['empty'] = 'Žádné výsledky'; $lang['quarantine']['toggle_all'] = 'Označit vše'; @@ -709,70 +769,89 @@ $lang['quarantine']['rcpt'] = "Příjemce"; $lang['quarantine']['qid'] = "Rspamd QID"; $lang['quarantine']['sender'] = "Odesílatel"; $lang['quarantine']['show_item'] = "Zobrazit položku"; -$lang['quarantine']['check_hash'] = "Hledat hash @ VT souboru"; +$lang['quarantine']['check_hash'] = "Hledat hash na serveru VT"; $lang['quarantine']['qitem'] = "Položka v karanténě"; $lang['quarantine']['subj'] = "Předmět"; +$lang['quarantine']['recipients'] = "Příjemci"; $lang['quarantine']['text_plain_content'] = "Obsah (text/plain)"; $lang['quarantine']['text_from_html_content'] = "Obsah (konvertované html)"; $lang['quarantine']['atts'] = "Přílohy"; +$lang['quarantine']['low_danger'] = "Malé nebezpečí"; +$lang['quarantine']['neutral_danger'] = "Neutrální nebo žádné hodnocení"; +$lang['quarantine']['medium_danger'] = "Střední nebezpečí"; +$lang['quarantine']['high_danger'] = "Vysoké nebezpečí"; +$lang['quarantine']['danger'] = "Nebezpečí"; +$lang['quarantine']['spam_score'] = "Skóre"; +$lang['quarantine']['confirm_delete'] = "Potvrdit smazání prvku."; +$lang['quarantine']['qhandler_success'] = "Požadavek úspěšně přijat. Můžete nyní zavřít okno."; $lang['warning']['fuzzy_learn_error'] = "Chyba při učení Fuzzy hash: %s"; $lang['danger']['spam_learn_error'] = "Chyba při učení spamu: %s"; -$lang['success']['qlearn_spam'] = "Zpráva ID %s byla naučena jako spam a smazána"; +$lang['success']['qlearn_spam'] = "Zpráva ID %s naučena jako spam a smazána"; -$lang['debug']['system_containers'] = 'Systém a docker kontejnery'; +$lang['debug']['system_containers'] = 'Systém a kontejnery'; +$lang['debug']['solr_status'] = 'Stav Solr'; +$lang['debug']['solr_dead'] = 'Solr se spouští, je vypnutý nebo spadl.'; $lang['debug']['logs'] = 'Logy'; -$lang['debug']['log_info'] = '<p><b>Logy v paměti</b> jsou schromažďovány pomocí Redis seznamů a jsou omezeny na LOG_LINES (%d) každou minutu pro zabránění přetěžování serveru. Nejsou navrženy jako trvalé. Všechny aplikace, které logují do paměti zároveň logují i do docker služby podle nastavení logging driveru. Logy v paměti jsou navrženy pro ladění menších problémů s kontejnery.</p> - <p><b>Externí logy</b> jsou schromažďovány pomocí API dané aplikace.</p> - <p><b>Statické logy</b> jsou většinou logy činností, které nejsou zaznamenávány do Docker služby, ale je potřeba aby byly trvalé (vyjímkou jsou API logy).</p>'; +$lang['debug']['log_info'] = '<p><b>Logy v paměti</b> jsou shromažďovány v Redis seznamech a jsou oříznuty na LOG_LINES (%d) každou minutu, aby se nepřetěžoval server. + <br>Logy v paměti nemají být trvalé. Všechny aplikace, které logují do paměti, zároveň logují i do Docker služby podle nastavení logging driveru. + <br>Logy v paměti lze použít pro ladění drobných problémů s kontejnery.</p> + <p><b>Externí logy</b> jsou shromažďovány pomocí API dané aplikace.</p> + <p><b>Statické logy</b> jsou většinou logy činností, které nejsou zaznamenávány do Docker služby, ale přesto je dobré je schraňovat (výjimkou jsou logy API).</p>'; $lang['debug']['in_memory_logs'] = 'Logy v paměti'; $lang['debug']['external_logs'] = 'Externí logy'; $lang['debug']['static_logs'] = 'Statické logy'; +$lang['debug']['solr_uptime'] = 'Doba běhu'; +$lang['debug']['solr_started_at'] = 'Spuštěn'; +$lang['debug']['solr_last_modified'] = 'Naposledy změněn'; +$lang['debug']['solr_size'] = 'Velikost'; +$lang['debug']['solr_docs'] = 'Dokumentace'; $lang['debug']['disk_usage'] = 'Využití disku'; -$lang['debug']['containers_info'] = "Informace o docker kontejnerech"; +$lang['debug']['containers_info'] = "Informace o kontejnerech"; $lang['debug']['restart_container'] = 'Restartovat'; -$lang['quarantine']['release_body'] = "Váš email byla připojen jako soubor eml k této zprávě."; +$lang['quarantine']['release_body'] = "Zpráva připojena jako příloha EML k této zprávě."; $lang['danger']['release_send_failed'] = "Zprávu nelze propustit: %s"; $lang['quarantine']['release_subject'] = "Potenciálně škodlivá položka v karanténě %s"; $lang['mailbox']['bcc_map'] = "BCC mapování"; $lang['mailbox']['bcc_map_type'] = "Typ BCC"; $lang['mailbox']['bcc_type'] = "Typ BCC"; -$lang['mailbox']['bcc_sender_map'] = "Mapování odesílatele (Sender map)"; -$lang['mailbox']['bcc_rcpt_map'] = "Mapování příjemce (Recipient map)"; +$lang['mailbox']['bcc_sender_map'] = "Mapa odesílatelů"; +$lang['mailbox']['bcc_rcpt_map'] = "Mapa příjemců"; $lang['mailbox']['bcc_local_dest'] = "Místní cíl"; $lang['mailbox']['bcc_destinations'] = "BCC cíl"; $lang['mailbox']['bcc_destination'] = "BCC cíl"; $lang['edit']['bcc_dest_format'] = 'BCC cíl musí být jedna platná email adresa.'; $lang['mailbox']['bcc'] = "BCC"; -$lang['mailbox']['bcc_maps'] = "BCC mapování"; -$lang['mailbox']['bcc_to_sender'] = "Přepnout na mapování odesílatele (sender map)"; -$lang['mailbox']['bcc_to_rcpt'] = "Přepnout na mapování příjemce (recipient map)"; -$lang['mailbox']['add_bcc_entry'] = "Přidat BCC mapování"; -$lang['mailbox']['add_tls_policy_map'] = "Přidat mapování TLS pravidel"; -$lang['mailbox']['bcc_info'] = "Mapování BCC se používá pro tiché předávání kopií všech zpráv na jinou adresu. Mapování příjemců (Recipient map) se používá, pokud místní cíl (local destination) působí jako příjemce zprávy. -Mapování odesílatelů (Sender maps) podléhá stejnému principu. Místní cíl nebude informován o neúspěšném doručení."; -$lang['mailbox']['address_rewriting'] = 'Úpravy adresování (Address rewriting)'; -$lang['mailbox']['recipient_maps'] = 'Mapování příjemců (Recipient maps)'; -$lang['mailbox']['recipient_map'] = 'Mapování příjemce (Recipient map)'; -$lang['mailbox']['recipient_map_info'] = 'Mapování příjemců slouží k nahrazení cílové adresy zprávy před doručením.'; -$lang['mailbox']['recipient_map_old_info'] = 'Původní příjemce musí být musí být platná email adresa nebo název domény.'; -$lang['mailbox']['recipient_map_new_info'] = 'Nový příjemce musí být platná email adresa.'; +$lang['mailbox']['bcc_maps'] = "BCC mapy"; +$lang['mailbox']['bcc_to_sender'] = "Přepnout na mapu odesílatelů"; +$lang['mailbox']['bcc_to_rcpt'] = "Přepnout na mapu příjemců"; +$lang['mailbox']['add_bcc_entry'] = "Přidat BCC mapu"; +$lang['mailbox']['add_tls_policy_map'] = "Přidat mapu TLS pravidel"; +$lang['mailbox']['bcc_info'] = "Mapa BCC se používá pro tiché předávání kopií všech zpráv na jinou adresu. Mapa příjemců se používá, pokud je místní cíl příjemcem zprávy.<br/> + Mapa odesílatelů podléhá stejnému principu. Místní cíl nebude informován o neúspěšném doručení."; +$lang['mailbox']['address_rewriting'] = 'Přepisování adres'; +$lang['mailbox']['recipient_maps'] = 'Mapy příjemců'; +$lang['mailbox']['recipient_map'] = 'Mapa příjemce'; +$lang['mailbox']['recipient_map_info'] = 'Mapy příjemců slouží k nahrazení cílové adresy zprávy před doručením.'; +$lang['mailbox']['recipient_map_old_info'] = 'Původní příjemce musí být platná emailová adresa nebo název domény.'; +$lang['mailbox']['recipient_map_new_info'] = 'Cílová adresa mapy příjemce musí být platná emailová adresa.'; $lang['mailbox']['recipient_map_old'] = 'Původní příjemce'; $lang['mailbox']['recipient_map_new'] = 'Nový přijemce'; -$lang['danger']['invalid_recipient_map_new'] = 'Byl specifikován špatný nový příjemce: %s'; -$lang['danger']['invalid_recipient_map_old'] = 'Byl specifikován špatný původní příjemce: %s'; -$lang['danger']['recipient_map_entry_exists'] = 'Položka mapování příjemců "%s" již existuje'; -$lang['success']['recipient_map_entry_saved'] = 'Položka mapování příjemců "%s" byla uložena'; -$lang['success']['recipient_map_entry_deleted'] = 'Mapovaná položka ID %s byla smazána'; -$lang['danger']['tls_policy_map_entry_exists'] = 'Položka mapování TLS pravidel "%s" již existuje'; -$lang['success']['tls_policy_map_entry_saved'] = 'Položka mapování TLS pravidel "%s" byla uložena'; -$lang['success']['tls_policy_map_entry_deleted'] = 'Mapovaná položka TLS pravidel ID %s byla smazána'; -$lang['mailbox']['add_recipient_map_entry'] = 'Přidat mapování příjemce'; -$lang['danger']['tls_policy_map_parameter_invalid'] = "Parametr zaásad je špatný"; +$lang['danger']['invalid_recipient_map_new'] = 'Neplatný nový příjemce: %s'; +$lang['danger']['invalid_recipient_map_old'] = 'Neplatný původní příjemce: %s'; +$lang['danger']['recipient_map_entry_exists'] = 'Položka mapy příjemců "%s" již existuje'; +$lang['success']['recipient_map_entry_saved'] = 'Položka mapy příjemců "%s" uložena'; +$lang['success']['recipient_map_entry_deleted'] = 'Položka mapy ID %s smazána'; +$lang['danger']['tls_policy_map_entry_exists'] = 'Položka mapy TLS pravidel "%s" již existuje'; +$lang['success']['tls_policy_map_entry_saved'] = 'Položka mapy TLS pravidel "%s" uložena'; +$lang['success']['tls_policy_map_entry_deleted'] = 'Položka mapy TLS pravidel ID %s smazána'; +$lang['mailbox']['add_recipient_map_entry'] = 'Přidat mapu příjemce'; +$lang['danger']['tls_policy_map_parameter_invalid'] = "Parametr pravidel TLS je neplatný"; +$lang['danger']['temp_error'] = "Dočasná chyba"; $lang['oauth2']['scope_ask_permission'] = 'Aplikace požádala o následující oprávnění'; $lang['oauth2']['profile'] = 'Profil'; @@ -780,9 +859,9 @@ $lang['oauth2']['profile_desc'] = 'Zobrazení osobních informací: uživatelsk $lang['oauth2']['permit'] = 'Ověření aplikace'; $lang['oauth2']['authorize_app'] = 'Ověření aplikace'; $lang['oauth2']['deny'] = 'Zakázáno'; -$lang['oauth2']['access_denied'] = 'Pro povolení přístupu přes OAuth2 se přihlašte jako vlastník poštovní schránky.'; +$lang['oauth2']['access_denied'] = 'Pro povolení přístupu přes OAuth2 se přihlaste jako vlastník poštovní schránky.'; -$lang['admin']['sys_mails'] = 'Systémová pošta'; +$lang['admin']['sys_mails'] = 'Systémové zprávy'; $lang['admin']['subject'] = 'Předmět'; $lang['admin']['from'] = 'Od'; $lang['admin']['include_exclude'] = 'Zahrnout/Vyloučit'; @@ -793,7 +872,38 @@ $lang['admin']['text'] = 'Text'; $lang['admin']['activate_send'] = 'Povolit tlačítko "Odeslat"'; $lang['admin']['send'] = 'Odeslat'; -$lang['warning']['ip_invalid'] = 'Přeskočeno, vadná IP: %s'; +$lang['warning']['ip_invalid'] = 'Přeskočena neplatná IP: %s'; $lang['danger']['text_empty'] = 'Text nesmí být prázdný'; $lang['danger']['subject_empty'] = 'Předmět nesmí být prázdný'; -$lang['danger']['from_invalid'] = 'Odesílat nesmí být prázdný'; +$lang['danger']['from_invalid'] = 'Odesílatel nesmí být prázdný'; +$lang['danger']['network_host_invalid'] = 'Neplatná síť nebo hostitel: %s'; + +$lang['add']['mailbox_quota_def'] = 'Výchozí kvóta schránky'; +$lang['edit']['mailbox_quota_def'] = 'Výchozí kvóta schránky'; +$lang['danger']['mailbox_defquota_exceeds_mailbox_maxquota'] = 'Výchozí kvóta překračuje maximální kvótu schránky"'; +$lang['danger']['defquota_empty'] = 'Výchozí kvóta schránky nesmí být 0.'; +$lang['mailbox']['mailbox_defquota'] = 'Výchozí velikost schránky'; + +$lang['admin']['api_info'] = 'API je stále ve vývoji.'; + +$lang['admin']['guid_and_license'] = 'GUID a licence'; +$lang['admin']['guid'] = 'GUID - unique instance ID'; +$lang['admin']['license_info'] = 'Vlastnit licenci není povinné, ale pomáhá to v dalšímu vývoji.<br><a href="https://www.servercow.de/mailcow?lang=en#sal" target="_blank" alt="SAL order">Registrujte si svoje GUID zde</a>, nebo si <a href="https://www.servercow.de/mailcow?lang=en#support" target="_blank" alt="Support order">zaplaťte podporu pro svou instalaci mailcow.</a>'; +$lang['admin']['validate_license_now'] = 'Ověřit GUID na licenčním serveru'; + +$lang['admin']['customer_id'] = 'ID zákazníka'; +$lang['admin']['service_id'] = 'ID podpory'; + +$lang['admin']['lookup_mx'] = 'Ověřit cíl proti MX záznamu (.outlook.com bude směrovat všechnu poštu pro MX *.outlook.com přes tento uzel)'; +$lang['edit']['mbox_rl_info'] = 'Toto omezení provozu se vyhodnocuje podle přihlašovacího jména SASL, porovnává se s jakoukoliv adresou "od" použitou přihlášeným uživatelem. Omezení provozu poštovní schránku má prioritu před omezením provozu domény.'; + +$lang['add']['relayhost_wrapped_tls_info'] = '<b>Nepoužívejte</b> prosím porty s aktivním protokolem TLS (většinou port 465).<br> +Používejte porty bez TLS a pak pošlete příkaz STARTTLS. Pravidlo k vynucení užití TLS lze vytvořit pomocí mapy TLS pravidel".'; + +$lang['admin']['transport_dest_format'] = 'Formát: example.org, .example.org, *, box@example.org (vícero položek lze oddělit čárkou)'; + +$lang['mailbox']['alias_domain_backupmx'] = 'Doménový alias není aktivní pro předávanou doménu'; + +$lang['danger']['extra_acl_invalid'] = 'Externí adresa odesílatele "%s" je neplatná'; +$lang['danger']['extra_acl_invalid_domain'] = 'Externí adresa odesílatele "%s" má neplatnou doménu'; + diff --git a/data/web/lang/lang.de.php b/data/web/lang/lang.de.php index 77aab3c6..2ba9b42e 100644 --- a/data/web/lang/lang.de.php +++ b/data/web/lang/lang.de.php @@ -19,6 +19,8 @@ $lang['footer']['cancel'] = 'Abbrechen'; $lang['footer']['hibp_nok'] = 'Übereinstimmung gefunden! Dieses Passwort ist potentiell gefährlich!'; $lang['footer']['hibp_ok'] = 'Keine Übereinstimmung gefunden.'; +$lang['danger']['transport_dest_exists'] = 'Transport Maps Ziel "%s" existiert bereits'; +$lang['danger']['unlimited_quota_acl'] = "Unendliche Quota untersagt durch ACL"; $lang['danger']['mysql_error'] = "MySQL Fehler: %s"; $lang['danger']['redis_error'] = "Redis Fehler: %s"; $lang['danger']['unknown_tfa_method'] = "Unbekannte TFA Methode"; @@ -29,7 +31,7 @@ $lang['success']['verified_u2f_login'] = "U2F Anmeldung verifiziert"; $lang['success']['verified_yotp_login'] = "Yubico OTP Anmeldung verifiziert"; $lang['danger']['yotp_verification_failed'] = "Yubico OTP Verifizierung fehlgeschlagen: %s"; $lang['danger']['ip_list_empty'] = "Liste erlaubter IPs darf nicht leer sein"; -$lang['danger']['invalid_destination'] = "Ziel-Format ist ungültig"; +$lang['danger']['invalid_destination'] = 'Ziel-Format "%s" ist ungültig'; $lang['danger']['invalid_nexthop'] = "Next Hop ist ungültig"; $lang['danger']['invalid_nexthop_authenticated'] = 'Dieser Next Hop existiert bereits mit abweichenden Authentifizierungsdaten. Die bestehenden Authentifizierungsdaten dieses "Next Hops" müssen vorab angepasst werden.'; $lang['danger']['next_hop_interferes'] = "%s verhindert das Hinzufügen von Next Hop %s"; @@ -59,7 +61,7 @@ $lang['success']['delete_filters'] = "Filter gelöscht: %s"; $lang['success']['delete_filter'] = "Filter ID %s wurde gelöscht"; $lang['danger']['invalid_bcc_map_type'] = "Ungültiger BCC Map-Typ"; $lang['danger']['bcc_empty'] = "BCC Ziel darf nicht leer sein"; -$lang['danger']['bcc_must_be_email'] = "BCC Map %s ist keine gültige E-Mail-Adresse"; +$lang['danger']['bcc_must_be_email'] = "BCC Ziel %s ist keine gültige E-Mail-Adresse"; $lang['danger']['bcc_exists'] = "Ein BCC Map Eintrag %s existiert bereits als Typ %s"; $lang['success']['bcc_saved'] = "BCC Map Eintrag wurde gespeichert"; $lang['success']['bcc_edited'] = "BCC Map Eintrag %s wurde editiert"; @@ -106,6 +108,7 @@ $lang['success']['resource_modified'] = "Änderungen an Ressource %s wurden gesp $lang['success']['object_modified'] = "Änderungen an Objekt %s wurden gespeichert"; $lang['success']['f2b_modified'] = "Änderungen an Fail2ban-Parametern wurden gespeichert"; $lang['danger']['targetd_not_found'] = 'Ziel-Domain %s nicht gefunden'; +$lang['danger']['targetd_relay_domain'] = 'Ziel-Domain %s ist eine Relay-Domain'; $lang['success']['aliasd_added'] = 'Alias-Domain %s wurde angelegt'; $lang['success']['aliasd_modified'] = 'Änderungen an Alias-Domain %s wurden gespeichert'; $lang['success']['domain_modified'] = 'Änderungen an Domain %s wurden gespeichert'; @@ -114,6 +117,7 @@ $lang['success']['domain_admin_added'] = 'Domain-Administrator %s wurde angelegt $lang['success']['admin_added'] = 'Administrator %s wurde angelegt'; $lang['success']['admin_modified'] = 'Änderungen am Administrator wurden gespeichert'; $lang['success']['admin_api_modified'] = "Änderungen an API wurden gespeichert"; +$lang['success']['license_modified'] = "Änderungen an Lizenz wurden gespeichert"; $lang['danger']['username_invalid'] = 'Benutzername %s kann nicht verwendet werden'; $lang['danger']['password_mismatch'] = 'Passwort-Wiederholung stimmt nicht überein'; $lang['danger']['password_complexity'] = 'Passwort entspricht nicht den Richtlinien'; @@ -149,7 +153,7 @@ $lang['danger']['domain_quota_m_in_use'] = 'Domain Speicherplatzlimit muss grö $lang['danger']['mailboxes_in_use'] = 'Maximale Anzahl an Mailboxen muss größer oder gleich %d sein'; $lang['danger']['aliases_in_use'] = 'Maximale Anzahl an Aliassen muss größer oder gleich %d sein'; $lang['danger']['sender_acl_invalid'] = 'Sender ACL %s ist ungültig'; -$lang['danger']['domain_not_empty'] = 'Kann nur leere Domains entfernen'; +$lang['danger']['domain_not_empty'] = 'Domain %s ist nicht leer'; $lang['danger']['validity_missing'] = 'Bitte geben Sie eine Gültigkeitsdauer an'; $lang['user']['loading'] = "Lade..."; $lang['user']['force_pw_update'] = 'Das Passwort für diesen Benutzer <b>muss</b> geändert werden, damit die Zugriffssperre auf die Groupwarekomponenten wieder freigeschaltet wird.'; @@ -206,7 +210,7 @@ $lang['user']['spamfilter_behavior'] = 'Bewertung'; $lang['user']['spamfilter_green'] = 'Grün: Die Nachricht ist kein Spam'; $lang['user']['spamfilter_yellow'] = 'Gelb: Die Nachricht ist vielleicht Spam, wird als Spam markiert und in den Junk-Ordner verschoben'; $lang['user']['spamfilter_red'] = 'Rot: Die Nachricht ist eindeutig Spam und wird vom Server abgelehnt'; -$lang['user']['spamfilter_default_score'] = 'Standardwert:'; +$lang['user']['spamfilter_default_score'] = 'Standardwert'; $lang['user']['spamfilter_hint'] = 'Der erste Wert beschreibt den "low spam score", der zweite Wert den "high spam score".'; $lang['user']['spamfilter_table_domain_policy'] = "n.v. (Domainrichtlinie)"; $lang['user']['waiting'] = "Warte auf Ausführung"; @@ -234,7 +238,7 @@ $lang['user']['eas_reset_help'] = 'In vielen Fällen kann ein ActiveSync Profil $lang['user']['sogo_profile_reset'] = 'SOGo Profil zurücksetzen'; $lang['user']['sogo_profile_reset_now'] = 'Profil jetzt zurücksetzen'; -$lang['user']['sogo_profile_reset_help'] = 'Das Profil wird zuzüglich aller Daten <b>unwiederbringlich gelöscht</b>.'; +$lang['user']['sogo_profile_reset_help'] = 'Das Profil wird inklusive <strong>aller</strong> Daten <b>unwiederbringlich gelöscht</b>.'; $lang['user']['encryption'] = 'Verschlüsselung'; $lang['user']['username'] = 'Benutzername'; @@ -323,7 +327,7 @@ $lang['mailbox']['last_run_reset'] = 'Als nächstes ausführen'; $lang['mailbox']['excludes'] = 'Ausschlüsse'; $lang['mailbox']['sieve_info'] = 'Es können mehrere Filter pro Benutzer existieren, aber nur ein Filter eines Typs (Pre-/Postfilter) kann gleichzeitig aktiv sein.<br> Die Ausführung erfolgt in nachstehender Reihenfolge. Ein fehlgeschlagenes Script sowie der Befehl "keep;" stoppen die weitere Verarbeitung <b>nicht</b>.<br> -Prefilter → User scripts → Postfilter → <a href="https://github.com/mailcow/mailcow-dockerized/blob/master/data/conf/dovecot/sieve_after" target="_blank">global sieve postfilter</a>'; +<a href="https://github.com/mailcow/mailcow-dockerized/blob/master/data/conf/dovecot/global_sieve_before" target="_blank">Global sieve prefilter</a> → Prefilter → User scripts → Postfilter → <a href="https://github.com/mailcow/mailcow-dockerized/blob/master/data/conf/dovecot/global_sieve_after" target="_blank">Global sieve postfilter</a>'; $lang['info']['no_action'] = 'Keine Aktion anwendbar'; @@ -341,33 +345,35 @@ $lang['edit']['skipcrossduplicates'] = 'Duplikate auch über Ordner hinweg über $lang['add']['automap'] = 'Ordner automatisch mappen ("Sent items", "Sent" => "Sent" etc.)'; $lang['add']['skipcrossduplicates'] = 'Duplikate auch über Ordner hinweg überspringen ("first come, first serve")'; $lang['edit']['exclude'] = 'Elemente ausschließen (Regex)'; -$lang['edit']['max_mailboxes'] = 'Max. Mailboxanzahl:'; +$lang['edit']['max_mailboxes'] = 'Max. Mailboxanzahl'; $lang['edit']['title'] = 'Objekt bearbeiten'; -$lang['edit']['target_address'] = 'Ziel-Adresse(n) <small>(getrennt durch Komma)</small>:'; +$lang['edit']['target_address'] = 'Ziel-Adresse(n) <small>(getrennt durch Komma)</small>'; $lang['edit']['active'] = 'Aktiv'; +$lang['add']['gal'] = 'Globales Adressbuch'; $lang['edit']['gal'] = 'Globales Adressbuch'; -$lang['edit']['gal_info'] = 'Das Globale Adressbuch enthält alle Objekte einer Domain und kann durch keinen Benutzer editiert werden. <b>Zum Anwenden einer Änderung muss SOGo neugestartet werden.</b>'; +$lang['add']['gal_info'] = 'Das Globale Adressbuch enthält alle Objekte einer Domain und kann durch keinen Benutzer editiert werden. Die Verfügbarkeitsinformation in SOGo ist nur bei eingeschaltetem globalen Adressbuch ersichtlich! <b>Zum Anwenden einer Änderung muss SOGo neugestartet werden.</b>'; +$lang['edit']['gal_info'] = 'Das Globale Adressbuch enthält alle Objekte einer Domain und kann durch keinen Benutzer editiert werden. Die Verfügbarkeitsinformation in SOGo ist nur bei eingeschaltetem globalen Adressbuch ersichtlich <b>Zum Anwenden einer Änderung muss SOGo neugestartet werden.</b>'; $lang['edit']['force_pw_update'] = 'Erzwinge Passwortänderung bei nächstem Login'; $lang['edit']['force_pw_update_info'] = 'Dem Benutzer wird lediglich der Zugang zur mailcow UI ermöglicht.'; $lang['edit']['sogo_access'] = 'SOGo Zugriffsrecht'; $lang['edit']['sogo_access_info'] = 'Zugriff auf SOGo erlauben oder verbieten. Diese Einstellung hat weder Einfluss auf den Zugang sonstiger Dienste noch entfernt sie ein vorhandenes SOGo Benutzerprofil.'; -$lang['edit']['target_domain'] = 'Ziel-Domain:'; -$lang['edit']['password'] = 'Passwort:'; -$lang['edit']['password_repeat'] = 'Passwort (Wiederholung):'; +$lang['edit']['target_domain'] = 'Ziel-Domain'; +$lang['edit']['password'] = 'Passwort'; +$lang['edit']['password_repeat'] = 'Passwort (Wiederholung)'; $lang['edit']['domain_admin'] = 'Domain-Administrator bearbeiten'; $lang['edit']['domain'] = 'Domain bearbeiten'; $lang['edit']['edit_alias_domain'] = 'Alias-Domain bearbeiten'; $lang['edit']['domains'] = 'Domains'; $lang['edit']['alias'] = 'Alias bearbeiten'; $lang['edit']['mailbox'] = 'Mailbox bearbeiten'; -$lang['edit']['description'] = 'Beschreibung:'; -$lang['edit']['max_aliases'] = 'Max. Aliasse:'; -$lang['edit']['max_quota'] = 'Max. Größe per Mailbox (MiB):'; -$lang['edit']['domain_quota'] = 'Domain Speicherplatz gesamt (MiB):'; -$lang['edit']['backup_mx_options'] = 'Backup MX Optionen:'; -$lang['edit']['relay_domain'] = 'Relay Domain'; +$lang['edit']['description'] = 'Beschreibung'; +$lang['edit']['max_aliases'] = 'Max. Aliasse'; +$lang['edit']['max_quota'] = 'Max. Größe per Mailbox (MiB)'; +$lang['edit']['domain_quota'] = 'Domain Speicherplatz gesamt (MiB)'; +$lang['edit']['backup_mx_options'] = 'Backup MX Optionen'; +$lang['edit']['relay_domain'] = 'Diese Domain relayen'; $lang['edit']['relay_all'] = 'Alle Empfänger-Adressen relayen'; -$lang['edit']['relay_all_info'] = '<small>Wenn Sie <b>nicht</b> alle Empfänger-Adressen relayen möchten, müssen Sie eine ("blinde") Mailbox für jede Adresse, die relayt werden soll, erstellen.</small>'; +$lang['edit']['relay_all_info'] = '<small>Wenn <b>nicht</b> alle Empfänger-Adressen relayt werden sollen, müssen "blinde" Mailboxen für jede Adresse, die relayt werden soll, erstellen werden.</small>'; $lang['edit']['full_name'] = 'Voller Name'; $lang['edit']['quota_mb'] = 'Speicherplatz (MiB)'; $lang['edit']['sender_acl'] = 'Darf Nachrichten versenden als'; @@ -397,7 +403,7 @@ $lang['acl']['syncjobs'] = 'Sync Jobs'; $lang['acl']['eas_reset'] = 'EAS-Cache zurücksetzen'; $lang['acl']['sogo_profile_reset'] = 'SOGo Profil zurücksetzen'; $lang['acl']['quarantine'] = 'Quarantäne-Aktionen'; -$lang['acl']['quarantine_notification'] = 'Quarantäne-Benachrichtigung'; +$lang['acl']['quarantine_notification'] = 'Ändern der Quarantäne-Benachrichtigung'; $lang['acl']['quarantine_attachments'] = 'Anhänge aus Quarantäne'; $lang['acl']['alias_domains'] = 'Alias-Domains hinzufügen'; $lang['acl']['login_as'] = 'Einloggen als Mailbox-Benutzer'; @@ -405,8 +411,18 @@ $lang['acl']['bcc_maps'] = 'BCC Maps'; $lang['acl']['filters'] = 'Filter'; $lang['acl']['ratelimit'] = 'Rate limit'; $lang['acl']['recipient_maps'] = 'Empfängerumschreibungen'; +$lang['acl']['unlimited_quota'] = 'Unendliche Quota für Mailboxen'; +$lang['acl']['extend_sender_acl'] = 'Eingabe externer Absenderadressen erlauben'; $lang['acl']['prohibited'] = 'Untersagt durch Richtlinie'; +$lang['edit']['extended_sender_acl'] = 'Externe Absenderadressen'; +$lang['edit']['extended_sender_acl_info'] = 'Der DKIM Domainkey der externen Absenderdomain sollte in diesen Server importiert werden, falls vorhanden.<br> + Wird SPF verwendet, muss diesem Server der Versand gestattet werden.<br> + Wird eine Domain oder Alias-Domain zu diesem Server hinzugefügt, die sich mit der externen Absenderadresse überschneidet, wird der externe Absender hier entfernt.<br> + Ein Eintrag @domain.tld erlaubt den Versand als *@domain.tld'; +$lang['edit']['sender_acl_info'] = 'Wird einem Mailbox-Benutzer A der Versand als Mailbox-Benutzer B gestattet, so erscheint der Absender <b>nicht</b> automatisch in SOGo zur Auswahl.<br> + In SOGo muss zusätzlich eine Delegation eingerichtet werden. Dieses Verhalten trifft nicht auf Alias-Adressen zu.'; + $lang['mailbox']['quarantine_notification'] = 'Quarantäne-Benachrichtigung'; $lang['mailbox']['never'] = 'Niemals'; $lang['mailbox']['hourly'] = 'Stündlich'; @@ -435,6 +451,7 @@ $lang['add']['delete2duplicates'] = 'Lösche Duplikate im Ziel'; $lang['add']['delete1'] = 'Lösche Nachricht nach Übertragung vom Quell-Server'; $lang['add']['delete2'] = 'Lösche Nachrichten von Ziel-Server, die nicht auf Quell-Server vorhanden sind'; $lang['add']['custom_params'] = 'Eigene Parameter'; +$lang['add']['custom_params_hint'] = 'Richtig: --param=xy, falsch: --param xy'; $lang['add']['subscribeall'] = 'Alle synchronisierten Ordner abonnieren'; $lang['add']['timeout1'] = 'Timeout für Verbindung zum Remote-Host'; $lang['add']['timeout2'] = 'Timeout für Verbindung zum lokalen Host'; @@ -593,7 +610,7 @@ $lang['admin']['last_applied'] = 'Zuletzt angewendet'; $lang['admin']['reset_limit'] = 'Hash entfernen'; $lang['admin']['hash_remove_info'] = 'Das Entfernen eines Ratelimit Hashes - sofern noch existent - bewirkt den Reset gezählter Nachrichten dieses Elements.<br> Jeder Hash wird durch eine eindeutige Farbe gekennzeichnet.'; -$lang['warning']['hash_not_found'] = 'Hash nicht gefunden'; +$lang['warning']['hash_not_found'] = 'Hash nicht gefunden. Möglicherweise wurde dieser bereits gelöscht.'; $lang['success']['hash_deleted'] = 'Hash wurde gelöscht'; $lang['admin']['authed_user'] = 'Auth. Benutzer'; $lang['admin']['priority'] = 'Gewichtung'; @@ -607,11 +624,11 @@ $lang['admin']['forwarding_hosts_hint'] = 'Eingehende Nachrichten werden von den $lang['admin']['forwarding_hosts_add_hint'] = 'Sie können entweder IPv4/IPv6-Adressen, Netzwerke in CIDR-Notation, Hostnamen (die zu IP-Adressen aufgelöst werden), oder Domainnamen (die zu IP-Adressen aufgelöst werden, indem ihr SPF-Record abgefragt wird oder, in dessen Abwesenheit, ihre MX-Records) angeben.'; $lang['admin']['relayhosts_hint'] = 'Erstellen Sie senderabhängige Transporte, um diese im Einstellungsdialog einer Domain auszuwählen.<br> Der Transporttyp lautet immer "smtp:". Benutzereinstellungen bezüglich Verschlüsselungsrichtlinie werden beim Transport berücksichtigt.'; -$lang['admin']['transports_hint'] = 'Transport Maps <b>überwiegen</b> senderabhängige Transport Maps und ignorieren die individuellen Einstellungen eines Benutzers bezüglich Verschlüsselungsrichtlinie, da der Absender bei Ermittlung der Transportregel nicht berücksichtigt wird.<br> - Der Transport erfolgt immer via "smtp:".<br> - Ein Eintrag in der TLS Policy Map kann eine Verschlüsselung erzwingen.<br> - Die Authentifizierung wird anhand des Host Parameters ermittelt, hierbei würde bei einem beispielhaften Next Hop "[host]:25" immer zuerst "host" abfragt und <b>erst im Anschluss</b> "[host]:25".<br> - Dieses Verhalten schließt die <b>gleichzeitige Verwendung</b> von Einträgen der Art "host" sowie "[host]:25" aus.'; +$lang['admin']['transports_hint'] = '→ Transport Maps <b>überwiegen</b> senderabhängige Transport Maps.<br> +→ Transport Maps ignorieren Mailbox-Einstellungen für ausgehende Verschlüsselung. Eine serverweite TLS-Richtlinie wird jedoch angewendet.<br> +→ Der Transport erfolgt immer via "smtp:".<br> +→ Adressen, die mit "/localhost$/" übereinstimmen, werden immer via "local:" transportiert, daher sind sie von einer Zieldefinition "*" ausgeschlossen.<br> +→ Die Authentifizierung wird anhand des "Next hop" Parameters ermittelt. Hierbei würde bei einem beispielhaften Wert "[host]:25" immer zuerst "host" abfragt und <b>erst im Anschluss</b> "[host]:25". Dieses Verhalten schließt die <b>gleichzeitige Verwendung</b> von Einträgen der Art "host" sowie "[host]:25" aus.'; $lang['admin']['add_relayhost_hint'] = 'Bitte beachten Sie, dass Anmeldedaten unverschlüsselt gespeichert werden.<br> Angelegte Transporte dieser Art sind <b>senderabhängig</b> und müssen erst einer Domain zugewiesen werden, bevor sie als Transport verwendet werden.<br> Diese Einstellungen entsprechen demach <i>nicht</i> dem "relayhost" Parameter in Postfix.'; @@ -646,6 +663,7 @@ $lang['admin']['active_rspamd_settings_map'] = "Derzeit aktive Settings Map"; $lang['admin']['quota_notifications_info'] = "Quota Benachrichtigungen werden an Mailboxen versendet, die 80 respektive 95 Prozent der zur Verfügung stehenden Quota überschreiten."; $lang['admin']['quarantine_retention_size'] = "Rückhaltungen pro Mailbox:<br><small>0 bedeutet <b>inaktiv</b>.</small>"; $lang['admin']['quarantine_max_size'] = "Maximale Größe in MiB (größere Elemente werden verworfen):<br><small>0 bedeutet <b>nicht</b> unlimitert.</small>"; +$lang['admin']['quarantine_max_age'] = "Maximales Alter in Tagen<br><small>Wert muss größer oder gleich 1 Tag sein.</small>"; $lang['admin']['quarantine_exclude_domains'] = "Domains und Alias-Domains ausschließen"; $lang['admin']['quarantine_notification_sender'] = "Benachrichtigungs-E-Mail Absender"; $lang['admin']['quota_notification_sender'] = "Benachrichtigungs-E-Mail Absender"; @@ -729,6 +747,7 @@ $lang['quarantine']['show_item'] = "Details"; $lang['quarantine']['check_hash'] = "Checksumme auf VirusTotal suchen"; $lang['quarantine']['qitem'] = "Quarantäneeintrag"; $lang['quarantine']['subj'] = "Betreff"; +$lang['quarantine']['recipients'] = "Empfänger"; $lang['quarantine']['text_plain_content'] = "Inhalt (text/plain)"; $lang['quarantine']['text_from_html_content'] = "Inhalt (html, konvertiert)"; $lang['quarantine']['atts'] = "Anhänge"; @@ -737,6 +756,7 @@ $lang['quarantine']['neutral_danger'] = "Neutral/ohne Bewertung"; $lang['quarantine']['medium_danger'] = "Mittlere Gefahr"; $lang['quarantine']['high_danger'] = "Hohe Gefahr"; $lang['quarantine']['danger'] = "Gefahr"; +$lang['quarantine']['spam_score'] = "Bewertung"; $lang['quarantine']['qhandler_success'] = "Aktion wurde an das System übergeben. Sie dürfen dieses Fenster nun schließen."; $lang['warning']['fuzzy_learn_error'] = "Fuzzy Lernfehler: %s"; $lang['danger']['spam_learn_error'] = "Spam Lernfehler: %s"; @@ -746,7 +766,7 @@ $lang['debug']['log_info'] = '<p>mailcow <b>in-memory Logs</b> werden in Redis L <br>In-memory Logs sind vergänglich und nicht zur ständigen Aufbewahrung bestimmt. Alle Anwendungen, die in-memory protokollieren, schreiben ebenso in den Docker Daemon. <br>Das in-memory Protokoll versteht sich als schnelle Übersicht zum Debugging eines Containers, für komplexere Protokolle sollte der Docker Daemon konsultiert werden.</p> <p><b>Externe Logs</b> werden via API externer Applikationen bezogen.</p> - <p><b>Statische Logs</b> sind weitesgehend Aktivitätsprotokolle, die nicht in den Docker Daemon geschrieben werden, jedoch permanent verfügbar sein müssen (ausgeschloßen API Logs).</p>'; + <p><b>Statische Logs</b> sind weitestgehend Aktivitätsprotokolle, die nicht in den Docker Daemon geschrieben werden, jedoch permanent verfügbar sein müssen (ausgeschlossen API Logs).</p>'; $lang['debug']['in_memory_logs'] = 'In-memory Logs'; $lang['debug']['external_logs'] = 'Externe Logs'; @@ -800,6 +820,7 @@ $lang['success']['tls_policy_map_entry_saved'] = 'TLS-Richtlinieneintrag "%s" wu $lang['success']['tls_policy_map_entry_deleted'] = 'TLS-Richtlinie mit der ID %s wurde gelöscht'; $lang['mailbox']['add_tls_policy_map'] = "TLS-Richtlinieneintrag hinzufügen"; $lang['danger']['tls_policy_map_parameter_invalid'] = "Parameter ist ungültig"; +$lang['danger']['temp_error'] = "Temporärer Fehler"; $lang['oauth2']['scope_ask_permission'] = 'Eine Anwendung hat um die folgenden Berechtigungen gebeten'; $lang['oauth2']['profile'] = 'Profil'; @@ -825,3 +846,31 @@ $lang['danger']['text_empty'] = 'Text darf nicht leer sein'; $lang['danger']['subject_empty'] = 'Betreff darf nicht leer sein'; $lang['danger']['from_invalid'] = 'From address must be a valid email address'; $lang['danger']['network_host_invalid'] = 'Netzwerk oder Host ungültig: %s'; + +$lang['add']['mailbox_quota_def'] = 'Standard-Quota einer Mailbox'; +$lang['edit']['mailbox_quota_def'] = 'Standard-Quota einer Mailbox'; +$lang['danger']['mailbox_defquota_exceeds_mailbox_maxquota'] = 'Standard-Quota überschreitet das Limit der maximal erlaubten Größe einer Mailbox'; +$lang['danger']['defquota_empty'] = 'Standard-Quota darf nicht 0 sein'; +$lang['mailbox']['mailbox_defquota'] = 'Standard-Quota'; + +$lang['admin']['api_info'] = 'Das API befindet sich noch in Entwicklung, eine Dokumentation ist ausstehend.'; + +$lang['admin']['guid_and_license'] = 'GUID & Lizenz'; +$lang['admin']['guid'] = 'GUID - Eindeutige Instanz-ID'; +$lang['admin']['license_info'] = 'Eine Lizenz ist nicht erforderlich, hilft jedoch der Entwicklung mailcows.<br><a href="https://www.servercow.de/mailcow#sal" target="_blank" alt="SAL Bestellung">Hier kann die mailcow GUID registriert werden.</a> Alternativ ist <a href="https://www.servercow.de/mailcow#support" target="_blank" alt="SAL Bestellung">die Bestellung von Support-Paketen möglich</a>.'; +$lang['admin']['validate_license_now'] = 'GUID erneut verifizieren'; +$lang['admin']['customer_id'] = 'Kunde'; +$lang['admin']['service_id'] = 'Service'; + +$lang['admin']['lookup_mx'] = 'Ziel gegen MX prüfen (etwa .outlook.com, um alle Ziele mit MX *.outlook.com zu routen)'; +$lang['edit']['mbox_rl_info'] = 'Dieses Limit wird auf den SASL Loginnamen angewendet und betrifft daher alle Absenderadressen, die der eingeloggte Benutzer verwendet. Eub Mailbox Ratelimit überwiegt ein Domain-weites Ratelimit.'; + +$lang['add']['relayhost_wrapped_tls_info'] = 'Bitte <b>keine</b> TLS-wrapped Ports verwenden (etwa SMTPS via Port 465/tcp).<br> +Der Transport wird stattdessen STARTTLS anfordern, um TLS zu verwenden. TLS kann unter "TLS Policy Maps" erzwungen werden.'; + +$lang['admin']['transport_dest_format'] = 'Syntax: example.org, .example.org, *, box@example.org (mehrere Werte getrennt durch Komma einzugeben)'; + +$lang['mailbox']['alias_domain_backupmx'] = 'Alias-Domain für Relay-Domain inaktiv'; + +$lang['danger']['extra_acl_invalid'] = 'Externe Absenderadresse "%s" ist ungültig'; +$lang['danger']['extra_acl_invalid_domain'] = 'Externe Absenderadresse "%s" verwendet eine ungültige Domain'; diff --git a/data/web/lang/lang.en.php b/data/web/lang/lang.en.php index a8a276e0..908cbe4f 100644 --- a/data/web/lang/lang.en.php +++ b/data/web/lang/lang.en.php @@ -13,13 +13,15 @@ $lang['footer']['restarting_container'] = 'Restarting container, this may take a $lang['footer']['restart_container_info'] = '<b>Important:</b> A graceful restart may take a while to complete, please wait for it to finish.'; $lang['footer']['confirm_delete'] = 'Confirm deletion'; -$lang['footer']['delete_these_items'] = 'Please confirm your changes to the following object id:'; +$lang['footer']['delete_these_items'] = 'Please confirm your changes to the following object id'; $lang['footer']['delete_now'] = 'Delete now'; $lang['footer']['cancel'] = 'Cancel'; $lang['footer']['hibp_nok'] = 'Matched! This is a potentially dangerous password!'; $lang['footer']['hibp_ok'] = 'No match found.'; +$lang['danger']['transport_dest_exists'] = 'Transport destination "%s" exists'; +$lang['danger']['unlimited_quota_acl'] = "Unlimited quota prohibited by ACL"; $lang['danger']['mysql_error'] = "MySQL error: %s"; $lang['danger']['redis_error'] = "Redis error: %s"; $lang['danger']['unknown_tfa_method'] = "Unknown TFA method"; @@ -30,15 +32,15 @@ $lang['success']['verified_u2f_login'] = "Verified U2F login"; $lang['success']['verified_yotp_login'] = "Verified Yubico OTP login"; $lang['danger']['yotp_verification_failed'] = "Yubico OTP verification failed: %s"; $lang['danger']['ip_list_empty'] = "List of allowed IPs cannot be empty"; -$lang['danger']['invalid_destination'] = "Destination format is invalid"; +$lang['danger']['invalid_destination'] = 'Destination format "%s" is invalid'; $lang['danger']['invalid_nexthop'] = "Next hop format is invalid"; -$lang['danger']['invalid_nexthop_authenticated'] = "Next hops exists with different credentials, please update the existing credentials for this next hop first."; +$lang['danger']['invalid_nexthop_authenticated'] = "Next hop exists with different credentials, please update the existing credentials for this next hop first."; $lang['danger']['next_hop_interferes'] = "%s interferes with nexthop %s"; $lang['danger']['next_hop_interferes_any'] = "An existing next hop interferes with %s"; $lang['danger']['rspamd_ui_pw_length'] = "Rspamd UI password should be at least 6 chars long"; $lang['success']['rspamd_ui_pw_set'] = "Rspamd UI password successfully set"; $lang['success']['queue_command_success'] = "Queue command completed successfully"; -$lang['danger']['unknown'] = "An unknown error occured"; +$lang['danger']['unknown'] = "An unknown error occurred"; $lang['danger']['malformed_username'] = "Malformed username"; $lang['info']['awaiting_tfa_confirmation'] = "Awaiting TFA confirmation"; $lang['success']['logged_in_as'] = "Logged in as %s"; @@ -60,7 +62,7 @@ $lang['success']['delete_filters'] = "Deleted filters: %s"; $lang['success']['delete_filter'] = "Deleted filters ID %s"; $lang['danger']['invalid_bcc_map_type'] = "Invalid BCC map type"; $lang['danger']['bcc_empty'] = "BCC destination cannot be empty"; -$lang['danger']['bcc_must_be_email'] = "BCC map %s is not a valid email address"; +$lang['danger']['bcc_must_be_email'] = "BCC destination %s is not a valid email address"; $lang['danger']['bcc_exists'] = "A BCC map %s exists for type %s"; $lang['success']['bcc_saved'] = "BCC map entry saved"; $lang['success']['bcc_edited'] = "BCC map entry %s edited"; @@ -110,6 +112,7 @@ $lang['success']['resource_modified'] = "Changes to mailbox %s have been saved"; $lang['success']['object_modified'] = "Changes to object %s have been saved"; $lang['success']['f2b_modified'] = "Changes to Fail2ban parameters have been saved"; $lang['danger']['targetd_not_found'] = "Target domain %s not found"; +$lang['danger']['targetd_relay_domain'] = "Target domain %s is a relay domain"; $lang['success']['aliasd_added'] = "Added alias domain %s"; $lang['success']['aliasd_modified'] = "Changes to alias domain %s have been saved"; $lang['success']['domain_modified'] = "Changes to domain %s have been saved"; @@ -118,6 +121,7 @@ $lang['success']['domain_admin_added'] = "Domain administrator %s has been added $lang['success']['admin_added'] = "Administrator %s has been added"; $lang['success']['admin_modified'] = "Changes to administrator have been saved"; $lang['success']['admin_api_modified'] = "Changes to API have been saved"; +$lang['success']['license_modified'] = "Changes to license have been saved"; $lang['danger']['username_invalid'] = "Username %s cannot be used"; $lang['danger']['password_mismatch'] = "Confirmation password does not match"; $lang['danger']['password_complexity'] = "Password does not meet the policy"; @@ -153,7 +157,7 @@ $lang['danger']['domain_quota_m_in_use'] = "Domain quota must be greater or equa $lang['danger']['mailboxes_in_use'] = "Max. mailboxes must be greater or equal to %d"; $lang['danger']['aliases_in_use'] = "Max. aliases must be greater or equal to %d"; $lang['danger']['sender_acl_invalid'] = "Sender ACL value %s is invalid"; -$lang['danger']['domain_not_empty'] = "Cannot remove non-empty domain"; +$lang['danger']['domain_not_empty'] = "Cannot remove non-empty domain %s"; $lang['danger']['validity_missing'] = 'Please assign a period of validity'; $lang['user']['loading'] = "Loading..."; $lang['user']['force_pw_update'] = 'You <b>must</b> set a new password to be able to access groupware related services.'; @@ -210,7 +214,7 @@ $lang['user']['spamfilter_table_add'] = 'Add item'; $lang['user']['spamfilter_green'] = 'Green: this message is not spam'; $lang['user']['spamfilter_yellow'] = 'Yellow: this message may be spam, will be tagged as spam and moved to your junk folder'; $lang['user']['spamfilter_red'] = 'Red: This message is spam and will be rejected by the server'; -$lang['user']['spamfilter_default_score'] = 'Default values:'; +$lang['user']['spamfilter_default_score'] = 'Default values'; $lang['user']['spamfilter_hint'] = 'The first value describes the "low spam score", the second represents the "high spam score".'; $lang['user']['spamfilter_table_domain_policy'] = "n/a (domain policy)"; $lang['user']['waiting'] = "Waiting"; @@ -331,7 +335,7 @@ $lang['mailbox']['excludes'] = 'Excludes'; $lang['mailbox']['last_run_reset'] = 'Schedule next'; $lang['mailbox']['sieve_info'] = 'You can store multiple filters per user, but only one prefilter and one postfilter can be active at the same time.<br> Each filter will be processed in the described order. Neither a failed script nor an issued "keep;" will stop processing of further scripts.<br> -Prefilter → User scripts → Postfilter → <a href="https://github.com/mailcow/mailcow-dockerized/blob/master/data/conf/dovecot/sieve_after" target="_blank">global sieve postfilter</a>'; +<a href="https://github.com/mailcow/mailcow-dockerized/blob/master/data/conf/dovecot/global_sieve_before" target="_blank">Global sieve prefilter</a> → Prefilter → User scripts → Postfilter → <a href="https://github.com/mailcow/mailcow-dockerized/blob/master/data/conf/dovecot/global_sieve_after" target="_blank">Global sieve postfilter</a>'; $lang['info']['no_action'] = 'No action applicable'; @@ -359,7 +363,9 @@ $lang['edit']['title'] = 'Edit object'; $lang['edit']['target_address'] = 'Goto address/es <small>(comma-separated)</small>'; $lang['edit']['active'] = 'Active'; $lang['edit']['gal'] = 'Global Address List'; -$lang['edit']['gal_info'] = 'The GAL contains all objects of a domain and cannot be edited by any user. <b>Restart SOGo to apply changes.</b>'; +$lang['add']['gal'] = 'Global Address List'; +$lang['edit']['gal_info'] = 'The GAL contains all objects of a domain and cannot be edited by any user. Free/busy information in SOGo is missing, if disabled! <b>Restart SOGo to apply changes.</b>'; +$lang['add']['gal_info'] = 'The GAL contains all objects of a domain and cannot be edited by any user. Free/busy information in SOGo is missing, if disabled! <b>Restart SOGo to apply changes.</b>'; $lang['edit']['force_pw_update'] = 'Force password update at next login'; $lang['edit']['force_pw_update_info'] = 'This user will only be able to login to mailcow UI.'; $lang['edit']['sogo_access'] = 'Grant access to SOGo'; @@ -410,7 +416,7 @@ $lang['acl']['syncjobs'] = 'Sync jobs'; $lang['acl']['eas_reset'] = 'Reset EAS devices'; $lang['acl']['sogo_profile_reset'] = 'Reset SOGo profile'; $lang['acl']['quarantine'] = 'Quarantine actions'; -$lang['acl']['quarantine_notification'] = 'Quarantine notifications'; +$lang['acl']['quarantine_notification'] = 'Change quarantine notifications'; $lang['acl']['quarantine_attachments'] = 'Quarantine attachments'; $lang['acl']['alias_domains'] = 'Add alias domains'; $lang['acl']['login_as'] = 'Login as mailbox user'; @@ -418,8 +424,18 @@ $lang['acl']['bcc_maps'] = 'BCC maps'; $lang['acl']['filters'] = 'Filters'; $lang['acl']['ratelimit'] = 'Rate limit'; $lang['acl']['recipient_maps'] = 'Recipient maps'; +$lang['acl']['unlimited_quota'] = 'Unlimited quota for mailboxes'; +$lang['acl']['extend_sender_acl'] = 'Allow to extend sender ACL by external addresses'; $lang['acl']['prohibited'] = 'Prohibited by ACL'; +$lang['edit']['extended_sender_acl'] = 'External sender addresses'; +$lang['edit']['extended_sender_acl_info'] = 'A DKIM domain key should be imported, if available.<br> + Remember to add this server to the corresponding SPF TXT record.<br> + Whenever a domain or alias domain is added to this server, that overlaps with an external address, the external address is removed.<br> + Use @domain.tld to allow to send as *@domain.tld.'; +$lang['edit']['sender_acl_info'] = 'If mailbox user A is allowed to send as mailbox user B, the sender address is not automatically displayed as selectable "from" field in SOGo.<br> + Mailbox user A needs to create a delegation in SOGo to allow mailbox user b to select their address as sender. This behaviour does not apply to alias addresses.'; + $lang['mailbox']['quarantine_notification'] = 'Quarantine notifications'; $lang['mailbox']['never'] = 'Never'; $lang['mailbox']['hourly'] = 'Hourly'; @@ -448,6 +464,7 @@ $lang['add']['delete2duplicates'] = 'Delete duplicates on destination'; $lang['add']['delete1'] = 'Delete from source when completed'; $lang['add']['delete2'] = 'Delete messages on destination that are not on source'; $lang['add']['custom_params'] = 'Custom parameters'; +$lang['add']['custom_params_hint'] = 'Right: --param=xy, wrong: --param xy'; $lang['add']['subscribeall'] = 'Subscribe all folders'; $lang['add']['timeout1'] = 'Timeout for connection to remote host'; $lang['add']['timeout2'] = 'Timeout for connection to local host'; @@ -522,7 +539,7 @@ $lang['tfa']['none'] = "Deactivate"; $lang['tfa']['delete_tfa'] = "Disable TFA"; $lang['tfa']['disable_tfa'] = "Disable TFA until next successful login"; $lang['tfa']['confirm'] = "Confirm"; -$lang['tfa']['totp'] = "Time-based OTP (Google Authenticator etc.)"; +$lang['tfa']['totp'] = "Time-based OTP (Google Authenticator, Authy, etc.)"; $lang['tfa']['select'] = "Please select"; $lang['tfa']['waiting_usb_auth'] = "<i>Waiting for USB device...</i><br><br>Please tap the button on your U2F USB device now."; $lang['tfa']['waiting_usb_register'] = "<i>Waiting for USB device...</i><br><br>Please enter your password above and confirm your U2F registration by tapping the button on your U2F USB device."; @@ -553,7 +570,7 @@ $lang['admin']['f2b_netban_ipv4'] = 'IPv4 subnet size to apply ban on (8-32)'; $lang['admin']['f2b_netban_ipv6'] = 'IPv6 subnet size to apply ban on (8-128)'; $lang['admin']['f2b_whitelist'] = 'Whitelisted networks/hosts'; $lang['admin']['f2b_blacklist'] = 'Blacklisted networks/hosts'; -$lang['admin']['f2b_list_info'] = 'A blacklisted host or network will always outweigh a whitelist entity. Blacklist records are created at boot-time of the container. Whitelist records are read each time a ban is about to be applied.'; +$lang['admin']['f2b_list_info'] = 'A blacklisted host or network will always outweigh a whitelist entity. <b>List updates will take a few seconds to be applied.</b>'; $lang['admin']['search_domain_da'] = 'Search domains'; $lang['admin']['r_inactive'] = 'Inactive restrictions'; $lang['admin']['r_active'] = 'Active restrictions'; @@ -581,7 +598,7 @@ $lang['admin']['add_admin'] = 'Add administrator'; $lang['admin']['add_settings_rule'] = 'Add settings rule'; $lang['admin']['rsetting_desc'] = 'Short description'; $lang['admin']['rsetting_content'] = 'Rule content'; -$lang['admin']['rsetting_none'] = 'No rule available'; +$lang['admin']['rsetting_none'] = 'No rules available'; $lang['admin']['rsetting_no_selection'] = 'Please select a rule'; $lang['admin']['rsettings_preset_1'] = 'Disable all but DKIM and rate limit for authenticated users'; $lang['admin']['rsettings_preset_2'] = 'Postmasters want spam'; @@ -617,7 +634,7 @@ $lang['admin']['last_applied'] = 'Last applied'; $lang['admin']['reset_limit'] = 'Remove hash'; $lang['admin']['hash_remove_info'] = 'Removing a ratelimit hash (if still existing) will reset its counter completely.<br> Each hash is indicated by an individual color.'; -$lang['warning']['hash_not_found'] = 'Hash not found'; +$lang['warning']['hash_not_found'] = 'Hash not found or already deleted'; $lang['success']['hash_deleted'] = 'Hash deleted'; $lang['admin']['authed_user'] = 'Auth. user'; $lang['admin']['priority'] = 'Priority'; @@ -631,9 +648,11 @@ $lang['admin']['forwarding_hosts_hint'] = 'Incoming messages are unconditionally $lang['admin']['forwarding_hosts_add_hint'] = 'You can either specify IPv4/IPv6 addresses, networks in CIDR notation, host names (which will be resolved to IP addresses), or domain names (which will be resolved to IP addresses by querying SPF records or, in their absence, MX records).'; $lang['admin']['relayhosts_hint'] = 'Define sender-dependent transports to be able to select them in a domains configuration dialog.<br> The transport service is always "smtp:". A users individual outbound TLS policy setting is taken into account.'; -$lang['admin']['transports_hint'] = 'A transport map entry <b>overrules</b> a sender-dependent transport map</b>.<br> -Outbound TLS policy settings per-user are ignored and can only be enfored by TLS policy map entries. The transport service is always "smtp:".<br> -To determine credentials for an exemplary next hop "[host]:25", Postfix <b>always</b> queries for "nexthop" before searching for "[nexthop]:25". This behavior makes it impossible to use "nexthop" and "[nexthop]:25" at the same time.'; +$lang['admin']['transports_hint'] = '→ A transport map entry <b>overrules</b> a sender-dependent transport map</b>.<br> +→ Outbound TLS policy settings per-user are ignored and can only be enforced by TLS policy map entries.<br> +→ The transport service for defined transports is always "smtp:".<br> +→ Addresses matching "/localhost$/" will always be transported via "local:", therefore a "*" destination will not apply to those addresses.<br> +→ To determine credentials for an exemplary next hop "[host]:25", Postfix <b>always</b> queries for "host" before searching for "[host]:25". This behavior makes it impossible to use "host" and "[host]:25" at the same time.'; $lang['admin']['add_relayhost_hint'] = 'Please be aware that authentication data, if any, will be stored as plain text.'; $lang['admin']['add_transports_hint'] = 'Please be aware that authentication data, if any, will be stored as plain text.'; $lang['admin']['host'] = 'Host'; @@ -668,7 +687,7 @@ $lang['admin']['api_allow_from'] = "Allow API access from these IPs (separated b $lang['admin']['api_key'] = "API key"; $lang['admin']['activate_api'] = "Activate API"; $lang['admin']['regen_api_key'] = "Regenerate API key"; -$lang['admin']['ban_list_info'] = "See a list of banned IPs below: <b>network (remaining ban time) - [actions]</b>.<br />IPs queued to be unbanned, will be removed from the active ban list within a few seconds.<br />Red labels indicate active permanent bans by blacklisting."; +$lang['admin']['ban_list_info'] = "See a list of banned IPs below: <b>network (remaining ban time) - [actions]</b>.<br />IPs queued to be unbanned will be removed from the active ban list within a few seconds.<br />Red labels indicate active permanent bans by blacklisting."; $lang['admin']['unban_pending'] = "unban pending"; $lang['admin']['queue_unban'] = "queue unban"; $lang['admin']['no_active_bans'] = "No active bans"; @@ -681,6 +700,7 @@ $lang['admin']['active_rspamd_settings_map'] = "Active settings map"; $lang['admin']['quota_notifications_info'] = "Quota notications are sent to users once when crossing 80% and once when crossing 95% usage."; $lang['admin']['quarantine_retention_size'] = "Retentions per mailbox:<br><small>0 indicates <b>inactive</b>.</small>"; $lang['admin']['quarantine_max_size'] = "Maximum size in MiB (larger elements are discarded):<br><small>0 does <b>not</b> indicate unlimited.</small>"; +$lang['admin']['quarantine_max_age'] = "Maximum age in days<br><small>Value must be equal to or greater than 1 day.</small>"; $lang['admin']['quarantine_exclude_domains'] = "Exclude domains and alias-domains"; $lang['admin']['quarantine_release_format'] = "Format of released items"; $lang['admin']['quarantine_release_format_raw'] = "Unmodified original"; @@ -749,6 +769,7 @@ $lang['quarantine']['show_item'] = "Show item"; $lang['quarantine']['check_hash'] = "Search file hash @ VT"; $lang['quarantine']['qitem'] = "Quarantine item"; $lang['quarantine']['subj'] = "Subject"; +$lang['quarantine']['recipients'] = "Recipients"; $lang['quarantine']['text_plain_content'] = "Content (text/plain)"; $lang['quarantine']['text_from_html_content'] = "Content (converted html)"; $lang['quarantine']['atts'] = "Attachments"; @@ -757,9 +778,9 @@ $lang['quarantine']['neutral_danger'] = "Neutral/no rating"; $lang['quarantine']['medium_danger'] = "Medium danger"; $lang['quarantine']['high_danger'] = "High"; $lang['quarantine']['danger'] = "Danger"; +$lang['quarantine']['spam_score'] = "Score"; $lang['quarantine']['confirm_delete'] = "Confirm the deletion of this element."; $lang['quarantine']['qhandler_success'] = "Request successfully sent to the system. You can now close the window."; - $lang['warning']['fuzzy_learn_error'] = "Fuzzy hash learn error: %s"; $lang['danger']['spam_learn_error'] = "Spam learn error: %s"; $lang['success']['qlearn_spam'] = "Message ID %s was learned as spam and deleted"; @@ -827,6 +848,7 @@ $lang['success']['tls_policy_map_entry_saved'] = 'TLS policy map entry "%s" has $lang['success']['tls_policy_map_entry_deleted'] = 'TLS policy map ID %s has been deleted'; $lang['mailbox']['add_recipient_map_entry'] = 'Add recipient map'; $lang['danger']['tls_policy_map_parameter_invalid'] = "Policy parameter is invalid"; +$lang['danger']['temp_error'] = "Temporary error"; $lang['oauth2']['scope_ask_permission'] = 'An application asked for the following permissions'; $lang['oauth2']['profile'] = 'Profile'; @@ -852,3 +874,33 @@ $lang['danger']['text_empty'] = 'Text must not be empty'; $lang['danger']['subject_empty'] = 'Subject must not be empty'; $lang['danger']['from_invalid'] = 'Sender must not be empty'; $lang['danger']['network_host_invalid'] = 'Invalid network or host: %s'; + +$lang['add']['mailbox_quota_def'] = 'Default mailbox quota'; +$lang['edit']['mailbox_quota_def'] = 'Default mailbox quota'; +$lang['danger']['mailbox_defquota_exceeds_mailbox_maxquota'] = 'Default quota exceeds max quota limit'; +$lang['danger']['defquota_empty'] = 'Default quota per mailbox must not be 0.'; +$lang['mailbox']['mailbox_defquota'] = 'Default mailbox size'; + +$lang['admin']['api_info'] = 'The API is a work in progress.'; + +$lang['admin']['guid_and_license'] = 'GUID & License'; +$lang['admin']['guid'] = 'GUID - unique instance ID'; +$lang['admin']['license_info'] = 'A license is not required but helps further development.<br><a href="https://www.servercow.de/mailcow?lang=en#sal" target="_blank" alt="SAL order">Register your GUID here</a> or <a href="https://www.servercow.de/mailcow?lang=en#support" target="_blank" alt="Support order">buy support for your mailcow installation.</a>'; +$lang['admin']['validate_license_now'] = 'Validate GUID against license server'; + +$lang['admin']['customer_id'] = 'Customer ID'; +$lang['admin']['service_id'] = 'Service ID'; + +$lang['admin']['lookup_mx'] = 'Match destination against MX (.outlook.com to route all mail targeted to a MX *.outlook.com over this hop)'; +$lang['edit']['mbox_rl_info'] = 'This rate limit is applied on the SASL login name, it matches any "from" address used by the logged-in user. A mailbox rate limit overrides a domain-wide rate limit.'; + +$lang['add']['relayhost_wrapped_tls_info'] = 'Please do <b>not</b> use TLS-wrapped ports (mostly used on port 465).<br> +Use any non-wrapped port and issue STARTTLS. A TLS policy to enforce TLS can be created in "TLS policy maps".'; + +$lang['admin']['transport_dest_format'] = 'Syntax: example.org, .example.org, *, box@example.org (multiple values can be comma-separated)'; + +$lang['mailbox']['alias_domain_backupmx'] = 'Alias domain inactive for relay domain'; + +$lang['danger']['extra_acl_invalid'] = 'External sender address "%s" is invalid'; +$lang['danger']['extra_acl_invalid_domain'] = 'External sender "%s" uses an invalid domain'; + diff --git a/data/web/lang/lang.es.php b/data/web/lang/lang.es.php index 0d0e033b..4e3aafa9 100644 --- a/data/web/lang/lang.es.php +++ b/data/web/lang/lang.es.php @@ -3,11 +3,11 @@ * Spanish language file */ -$lang['footer']['loading'] = "Espere Por Favor..."; +$lang['footer']['loading'] = "Espera por favor..."; $lang['header']['restart_sogo'] = 'Reiniciar SOGo'; $lang['footer']['restart_sogo'] = 'Reiniciar SOGo'; $lang['footer']['restart_now'] = 'Reiniciar ahora'; -$lang['footer']['restart_sogo_info'] = 'Algunas tareas, por ejemplo agregar un dominio, requieren que reinicies SOGo para detectar cambios hechos en la UI de mailcow.<br><br><b>Importante:</b> Un reinicio sencillo puede tardar un poco en completarse, por favor espere a que termine.'; +$lang['footer']['restart_sogo_info'] = 'Algunas tareas, por ejemplo agregar un dominio, requieren que se reinicie SOGo para detectar cambios hechos en la UI de mailcow.<br><br><b>Importante:</b> Un reinicio sencillo puede tardar un poco en completarse, por favor espera a que termine.'; $lang['danger']['dkim_domain_or_sel_invalid'] = "Dominio DKIM ó selector inválido"; $lang['success']['dkim_removed'] = "Registro DKIM removido"; $lang['success']['dkim_added'] = "Registro DKIM guardado"; @@ -28,11 +28,11 @@ $lang['danger']['object_exists'] = "El objeto %s ya existe"; $lang['danger']['domain_exists'] = "El dominio %s ya existe"; $lang['danger']['alias_goto_identical'] = "Las direcciones alias y \"goto\" no deben ser idénticas"; $lang['danger']['aliasd_targetd_identical'] = "El dominio alias no debe ser igual al dominio destino"; -$lang['success']['alias_added'] = "Dirección/es alias ha/han sidgo agregada"; +$lang['success']['alias_added'] = "Dirección(es) alias ha/han sidgo agregada(s)"; $lang['success']['alias_modified'] = "Cambios al alias guardados"; $lang['success']['mailbox_modified'] = "Cambios al buzón %s guardados"; $lang['danger']['targetd_not_found'] = "Dominio destino no encontrado"; -$lang['success']['aliasd_added'] = "Agregado dominio alias %s"; +$lang['success']['aliasd_added'] = "Dominio alias %s agregado"; $lang['success']['aliasd_modified'] = "Cambios al dominio alias %s guardados"; $lang['success']['domain_modified'] = "Cambios al dominio %s guardados"; $lang['success']['domain_admin_modified'] = "Cambios al administrador del dominio %s guardados"; @@ -49,15 +49,15 @@ $lang['danger']['fetchmail_source_empty'] = "Por favor define una carpeta fuente $lang['danger']['fetchmail_dest_empty'] = "Por favor define una carpeta destino"; $lang['danger']['is_alias'] = "%s ya está definida como una dirección alias"; $lang['danger']['is_alias_or_mailbox'] = "%s ya está definido como un alias ó como un buzón"; -$lang['danger']['is_spam_alias'] = "%s ya está definida como una dirección alias de correo no deseado"; +$lang['danger']['is_spam_alias'] = "%s ya está definida como un alias de correo no deseado"; $lang['danger']['quota_not_0_not_numeric'] = "Cuota debe ser numérica y >= 0"; -$lang['danger']['domain_not_found'] = "Dominio no encontrado."; +$lang['danger']['domain_not_found'] = "Dominio no encontrado"; $lang['danger']['max_mailbox_exceeded'] = "Máx. de buzones superado (%d de %d)"; $lang['danger']['mailbox_quota_exceeded'] = "Cuota excede el límite de dominio (máx. %d MiB)"; $lang['danger']['mailbox_quota_left_exceeded'] = "No queda espacio suficiente (espacio libre: %d MiB)"; $lang['success']['mailbox_added'] = "Buzón %s agregado"; $lang['success']['domain_removed'] = "Dominio %s removido"; -$lang['success']['alias_removed'] = "Dirección alias %s removida"; +$lang['success']['alias_removed'] = "Alias %s removido"; $lang['success']['alias_domain_removed'] = "Dominio alias %s removido"; $lang['success']['domain_admin_removed'] = "Administrador del dominio %s removido"; $lang['success']['mailbox_removed'] = "Buzón %s removido"; @@ -65,7 +65,7 @@ $lang['danger']['max_quota_in_use'] = "Cuota del buzón debe ser mayor o igual a $lang['danger']['domain_quota_m_in_use'] = "Cuota del dominio debe ser mayor o igual a %d MiB"; $lang['danger']['mailboxes_in_use'] = "Máx. de buzones debe ser mayor o igual a %d"; $lang['danger']['aliases_in_use'] = "Máx. de alias debe ser mayor o igual a %d"; -$lang['danger']['sender_acl_invalid'] = "Valor del remitente ACL inválido"; +$lang['danger']['sender_acl_invalid'] = "Valor %s del ACL del remitente es inválido"; $lang['danger']['domain_not_empty'] = "No se puede eliminar un dominio que no esté vacío"; $lang['danger']['fetchmail_active'] = "Un proceso ya se está ejecutando, por favor espera a que termine."; $lang['danger']['validity_missing'] = 'Por favor asigna un periodo de validez'; @@ -80,9 +80,9 @@ $lang['user']['new_password_repeat'] = 'Confirmación de contraseña (repetir):' $lang['user']['new_password_description'] = 'Requisitos: longitud de 6 caracteres, letras y números.'; $lang['user']['spam_aliases'] = 'Alias de email temporales'; $lang['user']['alias'] = 'Alias'; -$lang['user']['is_catch_all'] = 'Atrapa-Todo para el/los dominio/s'; -$lang['user']['aliases_also_send_as'] = 'También permitido para mandarse como'; -$lang['user']['aliases_send_as_all'] = 'No verifiques el acceso del remitente para los siguientes dominios'; +$lang['user']['is_catch_all'] = 'Atrapa-Todo para el/los dominio(s)'; +$lang['user']['aliases_also_send_as'] = 'También permitido enviar como'; +$lang['user']['aliases_send_as_all'] = 'No verificar permisos del remitente para los siguientes dominios (y sus aliases)'; $lang['user']['alias_create_random'] = 'Generar alias aleatorio'; $lang['user']['alias_extend_all'] = 'Extender alias por 1 hora'; $lang['user']['alias_valid_until'] = 'Válido hasta'; @@ -97,27 +97,40 @@ $lang['user']['week'] = 'Semana'; $lang['user']['weeks'] = 'Semanas'; $lang['user']['spamfilter'] = 'Filtro de spam'; $lang['user']['spamfilter_wl'] = 'Lista blanca'; -$lang['user']['spamfilter_wl_desc'] = 'Direcciones en la lista blanca <b>nunca</b> clasificarán como spam. Probablemente se usará un comodín.'; +$lang['user']['spamfilter_wl_desc'] = 'Direcciones en la lista blanca <b>nunca</b> clasificarán como spam. Se puede usar comodines. Filtro solo aplica a los alias directos (alias con un solo buzón de destino) excluyendo los alias catch-all.'; $lang['user']['spamfilter_bl'] = 'Lista negra'; -$lang['user']['spamfilter_bl_desc'] = 'Direcciones en la lista negra <b>siempre</b> clasificarán como spam. Probablemente se usará un comodín.'; +$lang['user']['spamfilter_bl_desc'] = 'Direcciones en la lista negra <b>siempre</b> clasificarán como spam. Se puede usar comodines. Filtro solo aplica a los alias directos (alias con un solo buzón de destino) excluyendo los alias catch-all.'; $lang['user']['spamfilter_behavior'] = 'Clasificación'; $lang['user']['spamfilter_table_rule'] = 'Regla'; $lang['user']['spamfilter_table_action'] = 'Acción'; $lang['user']['spamfilter_table_empty'] = 'No hay datos para mostrar'; -$lang['user']['spamfilter_table_remove'] = 'eliminar'; +$lang['user']['spamfilter_table_remove'] = 'Eliminar'; $lang['user']['spamfilter_table_add'] = 'Agregar elemento'; $lang['user']['spamfilter_green'] = 'Verde: éste mensaje no es spam'; -$lang['user']['spamfilter_yellow'] = 'Amarillo: éste mensaje puede ser spam, será etiquetado como spam y trasladado a tu carpeta basura'; -$lang['user']['spamfilter_red'] = 'Rojo: Este mensaje es spam y sera rechazado por el servidor'; -$lang['user']['spamfilter_default_score'] = 'Valores por defecto:'; +$lang['user']['spamfilter_yellow'] = 'Amarillo: éste mensaje puede ser spam, será etiquetado como spam y trasladado a tu carpeta de correo no deseado'; +$lang['user']['spamfilter_red'] = 'Rojo: Este mensaje es spam, será rechazado por el servidor y enviado a la quarantena (si esta configurada)'; +$lang['user']['spamfilter_default_score'] = 'Valores predeterminados:'; $lang['user']['spamfilter_hint'] = 'El primer valor representa la "calificación baja de spam", el segundo representa la "calificación alta de spam".'; - -$lang['user']['tls_policy_warning'] = '<strong>Advertencia:</strong> Si decides forzar la transmisión de correo encriptado, puedes perder correos.<br>Mensajes que no satisfagan la política serán rebotados con una falla grave en el sistema de correos .'; -$lang['user']['tls_policy'] = 'Política de encriptación'; +$lang['user']['no_record'] = 'Sin registro'; +$lang['user']['tls_policy_warning'] = '<strong>Advertencia:</strong> Si se forza la transmisión de correo cifrado, es posible que no todos los correos lleguen a su destino.<br>Mensajes que no satisfagan la política, o no sean aceptados por el remitente, serán rebotados por el servidor.<br> Esta opción aplica a la dirección de correo electrónico principal (nombre de inicio de sesión), a todas las direcciones derivadas de dominios de alias, así como a las direcciones de alias <b> con este único buzón </b> como destino.'; +$lang['user']['tls_policy'] = 'Política de cifrado'; $lang['user']['tls_enforce_in'] = 'Aplicar TLS entrante'; $lang['user']['tls_enforce_out'] = 'Aplicar TLS saliente'; -$lang['user']['no_record'] = 'Sin registro'; +$lang['mailbox']['tls_enforce_in'] = 'Aplicar TLS entrante'; +$lang['mailbox']['tls_enforce_out'] = 'Aplicar TLS saliente'; +$lang['mailbox']['tls_map_dest'] = 'Destino'; +$lang['mailbox']['tls_map_dest_info'] = 'Ejemplos: example.org, .example.org, mail@example.org, [mail.example.org]:25'; +$lang['mailbox']['tls_map_policy'] = 'Póliza'; +$lang['mailbox']['tls_map_parameters'] = 'Parametros'; +$lang['mailbox']['tls_map_parameters_info'] = 'Vacío o parametros, por ejemplo: protocols=!SSLv2 ciphers=medium exclude=3DES'; +$lang['user']['shared_aliases'] = 'Alias compartidos'; +$lang['user']['shared_aliases_desc'] = 'Los alias compartidos no se ven afectados por la configuración específica del usuario, como el filtro de correo no deseado o la política de cifrado. Los filtros de spam correspondientes solo pueden ser realizados por un administrador como una política de dominio.'; +$lang['user']['direct_aliases'] = 'Alias directos'; +$lang['user']['direct_aliases_desc'] = 'Los alias directos se ven afectadas por el filtro de correo no deseado y la configuración de la política TLS del usuario.'; +$lang['user']['eas_reset'] = 'Resetear el caché ActiveSync'; +$lang['user']['eas_reset_now'] = 'Resetear ahora'; +$lang['user']['eas_reset_help'] = 'En muchos casos, el restablecimiento de la memoria caché del dispositivo ayudará a recuperar un perfil de ActiveSync dañado.<br> <b>Atención:</b> ¡Todos los elementos se volverán a descargar!'; $lang['user']['tag_handling'] = 'Establecer manejo para el correo etiquetado'; $lang['user']['tag_in_subfolder'] = 'En subcarpeta'; @@ -127,16 +140,29 @@ $lang['user']['tag_help_explain'] = 'En subcarpeta: una nueva subcarpeta llamada En asunto: los nombres de las etiquetas serán añadidos al asunto de los correos, ejemplo: "[Facebook] Mis Noticias".'; $lang['user']['tag_help_example'] = 'Ejemplo de una dirección email etiquetada: mi<b>+Facebook</b>@ejemplo.org'; +$lang['user']['encryption'] = 'Cifrado'; +$lang['user']['username'] = 'Nombre de usuario'; +$lang['user']['last_run'] = 'Última ejecución'; +$lang['user']['excludes'] = 'Excluye'; +$lang['user']['interval'] = 'Intervalo'; +$lang['user']['active'] = 'Activo'; +$lang['user']['action'] = 'Acción'; +$lang['user']['edit'] = 'Editar'; +$lang['user']['remove'] = 'Eliminar'; +$lang['user']['create_syncjob'] = 'Crear nuevo trabajo de sincronización'; + $lang['start']['mailcow_apps_detail'] = 'Utiliza una aplicación de mailcow para acceder a tus correos, calendario, contactos y más.'; -$lang['start']['mailcow_panel_detail'] = '<b>Administradores del dominio</b> crean, modifican o eliminan buzones y alias, cambia dominios y lee información más detallada sobre sus dominios asignados<br> - <b>Usuarios de buzón</b> son capaces de crear alias de tiempo limitado (spam alias), cambiar su contraseña y la configuración del filtro de spam.'; -$lang['start']['imap_smtp_server_auth_info'] = 'Por favor utiliza tu dirección de correo completa y el mecanismo de autenticación PLANO.<br> -Tus datos para iniciar sesión serán encriptados por la encriptación obligatoria del servidor'; +$lang['start']['mailcow_panel_detail'] = '<b>Administradores por dominio</b> pueden crear, modificar o eliminar buzones y alias, modificar los dominios asignados y ver información más detallada sobre los dominios asignados<br> + <b>Usuarios de buzón</b> son capaces de crear alias de tiempo limitado (spam alias), cambiar su contraseña y la configuración del filtro de spam del buzón.'; +$lang['start']['imap_smtp_server_auth_info'] = 'Por favor utiliza tu dirección de correo completa y el mecanismo de autenticación PLAIN.<br> +Tus datos para iniciar sesión serán cifrados por el cifrado obligatorio del servidor'; $lang['start']['help'] = 'Mostrar/Ocultar panel de ayuda'; -$lang['header']['mailcow_settings'] = 'Configuracion'; +$lang['header']['mailcow_settings'] = 'Configuración'; $lang['header']['administration'] = 'Administración'; $lang['header']['mailboxes'] = 'Buzones'; $lang['header']['user_settings'] = 'Configuraciones de usuario'; +$lang['header']['quarantine'] = "Cuarentena"; +$lang['header']['debug'] = "Información del sistema"; $lang['mailbox']['domain'] = 'Dominio'; $lang['mailbox']['alias'] = 'Alias'; $lang['mailbox']['aliases'] = 'Alias'; @@ -146,10 +172,10 @@ $lang['mailbox']['mailbox_quota'] = 'Tamaño máx. de cuota'; $lang['mailbox']['domain_quota'] = 'Cuota'; $lang['mailbox']['active'] = 'Activo'; $lang['mailbox']['action'] = 'Acción'; -$lang['mailbox']['backup_mx'] = 'Respaldar MX'; +$lang['mailbox']['backup_mx'] = 'MX de respaldo'; $lang['mailbox']['domain_aliases'] = 'Alias de dominio'; $lang['mailbox']['target_domain'] = 'Dominio destino'; -$lang['mailbox']['target_address'] = 'Dirección Goto'; +$lang['mailbox']['target_address'] = 'Dirección destino'; $lang['mailbox']['username'] = 'Nombre de usuario'; $lang['mailbox']['fname'] = 'Nombre completo'; $lang['mailbox']['filter_table'] = 'Filtrar tabla'; @@ -162,14 +188,48 @@ $lang['mailbox']['add_domain'] = 'Agregar dominio'; $lang['mailbox']['add_domain_alias'] = 'Agregar alias de dominio'; $lang['mailbox']['add_mailbox'] = 'Agregar buzón'; $lang['mailbox']['add_alias'] = 'Agregar alias'; - +$lang['mailbox']['toggle_all'] = 'Seleccionar todo'; +$lang['mailbox']['add_resource'] = 'Añadir recurso'; +$lang['mailbox']['add_domain_record_first'] = 'Agrega un dominio primero'; +$lang['mailbox']['empty'] = 'Sin resultados'; +$lang['mailbox']['toggle_all'] = 'Selecionar todo'; +$lang['mailbox']['quick_actions'] = 'Acciones'; +$lang['mailbox']['activate'] = 'Activar'; +$lang['mailbox']['resources'] = 'Recursos'; +$lang['mailbox']['multiple_bookings'] = 'Reservas multiples'; +$lang['mailbox']['kind'] = 'Tipo'; +$lang['mailbox']['description'] = 'Descripción'; +$lang['mailbox']['booking_0'] = 'Mostrar siempre disponible'; +$lang['mailbox']['booking_lt0'] = 'Sín limite, pero mostrar ocupado cuando esté reservado'; +$lang['mailbox']['booking_custom'] = 'Límite pre-establecido de reservas'; +$lang['mailbox']['booking_0_short'] = 'Siempre disponible'; +$lang['mailbox']['booking_lt0_short'] = 'Ocupado cuando en uso'; +$lang['mailbox']['booking_custom_short'] = 'Límite estricto'; +$lang['mailbox']['activate'] = 'Activar'; +$lang['mailbox']['deactivate'] = 'Desactivar'; +$lang['mailbox']['quarantine_notification'] = 'Notificaciones de cuarentena'; +$lang['mailbox']['never'] = 'Nunca'; +$lang['mailbox']['hourly'] = 'Cada hora'; +$lang['mailbox']['daily'] = 'Cada día'; +$lang['mailbox']['weekly'] = 'Cada semana'; +$lang['user']['quarantine_notification'] = 'Notificaciones de cuarentena'; +$lang['user']['never'] = 'Nunca'; +$lang['user']['hourly'] = 'Cada hora'; +$lang['user']['daily'] = 'Cada día'; +$lang['user']['weekly'] = 'Cada semana'; +$lang['user']['quarantine_notification_info'] = 'Una vez que se haya enviado una notificación, los elementos se marcarán como "notificados" y no se enviarán más notificaciones para este elemento en particular.'; $lang['info']['no_action'] = 'No hay acción aplicable'; - +$lang['add']['kind'] = 'Tipo'; +$lang['add']['multiple_bookings'] = 'Múltiples reservas'; +$lang['edit']['multiple_bookings'] = 'Múltiples reservas'; +$lang['edit']['kind'] = 'Tipo'; +$lang['edit']['resource'] = 'Recurso'; +$lang['success']['resource_added'] = "Recurso %s añadido"; $lang['edit']['save'] = 'Guardar cambios'; $lang['edit']['max_mailboxes'] = 'Máx. buzones posibles:'; -$lang['edit']['title'] = 'Editas objeto'; -$lang['edit']['target_address'] = 'Dirección/es goto <small>(separadas por coma)</small>:'; +$lang['edit']['title'] = 'Editar objeto'; +$lang['edit']['target_address'] = 'Dirección/es destino <small>(separadas por coma)</small>:'; $lang['edit']['active'] = 'Activo'; $lang['edit']['target_domain'] = 'Dominio destino:'; $lang['edit']['password'] = 'Contraseña:'; @@ -186,16 +246,15 @@ $lang['edit']['max_quota'] = 'Máx. cuota por buzón (MiB):'; $lang['edit']['domain_quota'] = 'Cuota de dominio:'; $lang['edit']['backup_mx_options'] = 'Opciones del respaldo MX:'; $lang['edit']['relay_domain'] = 'Dominio de retransmisión'; -$lang['edit']['relay_all'] = 'Retransmitir todos los recipientes'; -$lang['edit']['relay_all_info'] = '<small>Si eliges <b>no</b> retransmitir a todos los recipientes, necesitas agregar un buzón "blind"("ciego") por cada recipiente que debe ser retransmitido.</small>'; +$lang['edit']['relay_all'] = 'Retransmitir todos los destinatarios'; +$lang['edit']['relay_all_info'] = '<small>Si eliges <b>no</b> retransmitir a todos los destinatarios, necesitas agregar un buzón "ciego" por cada destinatario que debe ser retransmitido.</small>'; $lang['edit']['full_name'] = 'Nombre completo'; $lang['edit']['full_name'] = 'Nombre completo'; $lang['edit']['quota_mb'] = 'Cuota (MiB)'; $lang['edit']['sender_acl'] = 'Permitir envío como:'; $lang['edit']['previous'] = 'Página anterior'; $lang['edit']['unchanged_if_empty'] = 'Si no hay cambios dejalo en blanco'; -$lang['edit']['dont_check_sender_acl'] = 'No verifiques remitente para el dominio %s'; - +$lang['edit']['dont_check_sender_acl'] = 'No verificar remitente para el dominio %s'; $lang['add']['domain'] = 'Dominio'; $lang['add']['active'] = 'Activo'; $lang['add']['description'] = 'Descripción:'; @@ -204,14 +263,14 @@ $lang['add']['max_mailboxes'] = 'Máx. buzones posibles:'; $lang['add']['mailbox_quota_m'] = 'Máx. cuota por buzón (MiB):'; $lang['add']['domain_quota_m'] = 'Cuota total del dominio (MiB):'; $lang['add']['backup_mx_options'] = 'Opciones del respaldo MX:'; -$lang['add']['relay_all'] = 'Retransmitir todos los recipientes'; +$lang['add']['relay_all'] = 'Retransmitir todos los destinatarios'; $lang['add']['relay_domain'] = 'Retransmitir este dominio'; -$lang['add']['relay_all_info'] = '<small>Si eliges <b>no</b> retransmitir a todos los recipientes, necesitas agregar un buzón "blind"("ciego") por cada recipiente que debe ser retransmitido.</small>'; -$lang['add']['alias_address'] = 'Dirección/es alias:'; -$lang['add']['alias_address_info'] = '<small>Dirección/es de correo completa/s ó @ejemplo.com, para atrapar todos los mensajes para un dominio (separado por coma). <b>Dominios mailcow solamente</b>.</small>'; +$lang['add']['relay_all_info'] = '<small>Si eliges <b>no</b> retransmitir a todos los destinatarios, necesitas agregar un buzón "ciego" por cada destinatario que debe ser retransmitido.</small>'; +$lang['add']['alias_address'] = 'Dirección(es) alias:'; +$lang['add']['alias_address_info'] = '<small>Dirección(es) de correo completa(s) ó @dominio.com, para atrapar todos los mensajes para un dominio (separado por coma). <b>Dominios que existan en mailcow solamente</b>.</small>'; $lang['add']['alias_domain_info'] = '<small>Nombres de dominio válidos solamente (separado por coma).</small>'; -$lang['add']['target_address'] = 'Direcciones goto:'; -$lang['add']['target_address_info'] = '<small>Dirección/es de correo completa/s (separado por coma).</small>'; +$lang['add']['target_address'] = 'Direcciones destino:'; +$lang['add']['target_address_info'] = '<small>Dirección(es) de correo completa(s) (separado por coma).</small>'; $lang['add']['alias_domain'] = 'Dominio alias'; $lang['add']['select'] = 'Por favor selecciona...'; $lang['add']['target_domain'] = 'Dominio destino:'; @@ -221,7 +280,7 @@ $lang['add']['quota_mb'] = 'Cuota (MiB):'; $lang['add']['select_domain'] = 'Por favor elige un dominio primero'; $lang['add']['password'] = 'Constraseña:'; $lang['add']['password_repeat'] = 'Confirmación de contraseña (repetir):'; -$lang['add']['restart_sogo_hint'] = '¡Necesitas reiniciar el contenedor del servicio SOGo antes de agregar un nuevo dominio!'; +$lang['add']['restart_sogo_hint'] = '<b>Nota:</b> Necesitarás reiniciar el contenedor del servicio SOGo despues de agregar un nuevo dominio'; $lang['add']['port'] = 'Port'; $lang['login']['username'] = 'Nombre de usuario'; $lang['login']['password'] = 'Contraseña'; @@ -238,9 +297,40 @@ $lang['admin']['search_domain_da'] = 'Buscar dominios'; $lang['admin']['r_inactive'] = 'Restricciones inactivas'; $lang['admin']['r_active'] = 'Restricciones activas'; $lang['admin']['r_info'] = 'Elementos en gris/deshabilitados en la lista de restricciones activas no son reconocidas como restricciones válidas para mailcow y no pueden ser movidas. Restricciones desconocidas serán establecidas en el orden de aparicion de todas maneras. <br>Puedes agregar nuevos elementos en <code>inc/vars.local.inc.php</code> para ser capaz de habilitarlas.'; +$lang['admin']['no_new_rows'] = 'No hay más filas disponibles.'; +$lang['admin']['queue_manager'] = 'Administrador de cola'; +$lang['admin']['additional_rows'] = ' filas adicionales fueron agregadas'; // parses to 'n additional rows were added' +$lang['admin']['private_key'] = 'Llave privada'; +$lang['admin']['import'] = 'Importar'; +$lang['admin']['duplicate'] = 'Duplicar'; +$lang['admin']['import_private_key'] = 'Importar llave privada'; +$lang['admin']['duplicate_dkim'] = 'Duplicar registro DKIM'; +$lang['admin']['dkim_from'] = 'De'; +$lang['admin']['dkim_to'] = 'A'; +$lang['admin']['dkim_from_title'] = 'Dominio de origen para copiar datos desde'; +$lang['admin']['dkim_to_title'] = 'Dominio(s) de destino: (se sobrescribirán)'; +$lang['admin']['f2b_parameters'] = 'Parametros Fail2ban'; +$lang['admin']['f2b_ban_time'] = 'Tiempo de restricción (s)'; +$lang['admin']['f2b_max_attempts'] = 'Max num. de intentos'; +$lang['admin']['f2b_retry_window'] = 'Ventana de tiempo entre reintentos'; +$lang['admin']['f2b_netban_ipv4'] = 'Tamaño de subred IPv4 para aplicar la restricción (8-32)'; +$lang['admin']['f2b_netban_ipv6'] = 'Tamaño de subred IPv6 para aplicar la restricción (8-128)'; +$lang['admin']['f2b_whitelist'] = 'Redes y hosts en lista blanca'; +$lang['admin']['f2b_blacklist'] = 'Redes y hosts en lista negra'; +$lang['admin']['f2b_list_info'] = 'Un host o red en lista negra siempre superará a una entidad de la lista blanca. <b>Las actualizaciones de la lista tardarán unos segundos en aplicarse.</b>'; +$lang['admin']['search_domain_da'] = 'Buscar dominios'; +$lang['admin']['r_inactive'] = 'Restricciones inactivas'; +$lang['admin']['r_active'] = 'Restricciones activas'; +$lang['admin']['r_info'] = 'Los elementos desactivados en la lista de restricciones activas no son reconocidas como restricciones válidas por mailcow y no se pueden mover. <br>Se puede agregar nuevos elementos en <code>inc/vars.local.inc.php</code> para poder activarlos.'; $lang['admin']['dkim_key_length'] = 'Longitud de la llave DKIM (bits)'; +$lang['admin']['dkim_key_valid'] = 'Registro válido'; +$lang['admin']['dkim_key_unused'] = 'Registro sin usar'; +$lang['admin']['dkim_key_missing'] = 'Registro faltante'; $lang['admin']['dkim_add_key'] = 'Agregar registro ARC/DKIM'; $lang['admin']['dkim_keys'] = 'Registros ARC/DKIM'; +$lang['admin']['dkim_private_key'] = 'Private key'; +$lang['admin']['dkim_domains_wo_keys'] = "Select domains with missing keys"; +$lang['admin']['dkim_domains_selector'] = "Selector"; $lang['admin']['add'] = 'Agregar'; $lang['admin']['configuration'] = 'Configuración'; $lang['admin']['password'] = 'Contraseña'; @@ -256,6 +346,439 @@ $lang['admin']['remove'] = 'Eliminar'; $lang['admin']['save'] = 'Guardar cambios'; $lang['admin']['admin'] = 'Administrador'; $lang['admin']['admin_details'] = 'Editar detalles del administrador'; -$lang['admin']['unchanged_if_empty'] = 'Si no hay cambios dejalo en blanco'; +$lang['admin']['unchanged_if_empty'] = 'Si no hay cambios déjalo en blanco'; $lang['admin']['access'] = 'Acceso'; $lang['admin']['no_record'] = 'Sin registro'; + +$lang['edit']['relayhost'] = 'Ruta saliente'; +$lang['admin']['forwarding_hosts'] = 'Hosts de reenvío'; +$lang['admin']['forwarding_hosts_hint'] = 'Los mensajes entrantes son aceptados incondicionalmente de cualquiera de los hosts enumerados aquí. Estos hosts no se comprueban con listas DNSBL o se someten a greylisting. El spam recibido de ellos nunca se rechaza, pero opcionalmente se puede archivar en la carpeta de correo no deseado. El uso más común para esto es especificar los servidores de correo en los que ha configurado una regla que reenvía los correos electrónicos entrantes al servidor mailcow.'; +$lang['admin']['forwarding_hosts_add_hint'] = 'Se puede especificar direcciones IPv4 / IPv6, redes en notación CIDR, nombres de host (que se resolverán en direcciones IP) o dominios (que se resolverán en direcciones IP consultando registros SPF o, en su defecto, registros MX)'; +$lang['admin']['relayhosts_hint'] = 'Define rutas de reenvío dependientes del remitente para poder seleccionarlos en la configuración de dominios.<br> +El servicio de transporte es siempre <em>smtp:</em> y se tiene en cuenta la configuración de política TLS saliente individual de los usuarios.'; +$lang['admin']['transports_hint'] = '→ Una entrada del ruta de transporte <b> anula </b> la regla de host de reenvío dependiente del remitente</b>.<br> +→ La configuración de la política saliente de TLS por usuario se ignora y solo puede ser aplicada por las entradas del mapa de políticas TLS.<br> +→ El servicio de transporte es siempre <em>smtp:</em><br> +→ Las direcciones que coincidan con "/localhost$/" siempre se transportarán a través de "local:", por lo tanto, un destino "*" no se aplicará a esas direcciones.<br> +→ Para determinar las credenciales para el siguiente destino "[host]:25", Postfix <b>siempre</b> busca "host" antes de buscar "[host]:25". Este comportamiento hace que sea imposible utilizar "host" y "[host]:25" al mismo tiempo.'; +$lang['admin']['add_relayhost_hint'] = 'Ten en cuenta que los datos de autenticación, si los hay, se almacenarán sin cifrar.'; +$lang['admin']['add_transports_hint'] = 'Ten en cuenta que los datos de autenticación, si los hay, se almacenarán sin cifrar.'; +$lang['admin']['host'] = 'Host'; +$lang['admin']['source'] = 'Origen'; +$lang['admin']['add_forwarding_host'] = 'Añadir host de reenvío'; +$lang['admin']['add_relayhost'] = 'Agregar ruta de salida de mensajes'; +$lang['admin']['add_transport'] = 'Añadir ruta de transporte'; +$lang['admin']['relayhosts'] = 'Enrutamiento de salida de mensajes'; +$lang['admin']['transport_maps'] = 'Mapa de rutas de transporte'; +$lang['admin']['routing'] = 'Enrutamiento'; +$lang['admin']['credentials_transport_warning'] = '<b>Advertencia</b>: al agregar una nueva entrada de ruta de transporte se actualizarán las credenciales para todas las entradas con una columna de "siguiente destino" coincidente.'; +$lang['admin']['destination'] = 'Destino'; +$lang['admin']['nexthop'] = 'Siguiente destino'; +$lang['admin']['relay_from'] = 'Dirección "De:"'; +$lang['admin']['relay_run'] = "Probar configuración"; +$lang['admin']['api_allow_from'] = "Permitir acceso al API desde estas IPs (separadas por coma o en una nueva línea)"; +$lang['admin']['api_key'] = "Clave del API"; +$lang['admin']['activate_api'] = "Activar API"; +$lang['admin']['regen_api_key'] = "Regenerar clave API"; +$lang['admin']['ban_list_info'] = "La lista de IPs bloqueadas sigue a continuación: <b> red (tiempo de prohibición restante) - [acciones]</b>.<br/> Las IPs en cola para ser desbloquadas se eliminarán de la lista de bloqueos en unos pocos segundos. <br/> Las etiquetas rojas indican bloqueos permanentes permanentes mediante la inclusión en una lista negra."; +$lang['admin']['unban_pending'] = "Desbloqueo pendiente"; +$lang['admin']['queue_unban'] = "Encolar desbloqueo"; +$lang['admin']['no_active_bans'] = "No hay bloqueos pendientes"; +$lang['admin']['quarantine'] = "Cuarentena"; +$lang['admin']['rspamd_settings_map'] = "Reglas de ajustes de rspamd"; +$lang['admin']['quota_notifications'] = "Notificaciones de cuota"; +$lang['admin']['quota_notifications_vars'] = "{{percent}} equivale a la cuota actual del usuario<br>{{username}} es el buzón del usuario"; +$lang['admin']['active_rspamd_settings_map'] = "Reglas de ajustes activos"; +$lang['admin']['quota_notifications_info'] = "Las notificaciones de cuota se envían a los usuarios una vez cuando cruzan el 80% y cuando cruzan el 95% de uso."; +$lang['admin']['quarantine_retention_size'] = "Retenciones por buzón:<br><small>0 indica <b>inactivo</b>.</small>"; +$lang['admin']['quarantine_max_size'] = "Tamaño máximo en MiB (elementos más grandes se eliminan):<br><small>0 <b>no</b> indíca ilimitado.</small>"; +$lang['admin']['quarantine_max_age'] = "Edad máxima del mensaje en días<br><small>Valor debe ser igual o mayor a un día</small>"; +$lang['admin']['quarantine_exclude_domains'] = "Excluir dominios y dominios alias"; +$lang['admin']['quarantine_release_format'] = "Formato de mensajes liberados"; +$lang['admin']['quarantine_release_format_raw'] = "Original sin modificar"; +$lang['admin']['quarantine_release_format_att'] = "Como adjunto"; +$lang['admin']['quarantine_notification_sender'] = "Remitente del email de notificación"; +$lang['admin']['quarantine_notification_subject'] = "Asunto del email de notificación"; +$lang['admin']['quarantine_notification_html'] = "Plantilla del email de notificación:<br><small>Dejar en blanco para usar la planilla predeterminada.</small>"; +$lang['admin']['quota_notification_sender'] = "Remitente del email de notificación"; +$lang['admin']['quota_notification_subject'] = "Asunto del email de notificación"; +$lang['admin']['quota_notification_html'] = "Plantilla del email de notificación:<br><small>Dejar en blanco para usar la planilla predeterminada.</small>"; +$lang['admin']['ui_texts'] = "Etiquetas y textos de UI"; +$lang['admin']['help_text'] = "Campo de texto debajo del formulario de inicio de sesión (se permite HTML)"; +$lang['admin']['title_name'] = '"mailcow UI" (título de la página)'; +$lang['admin']['main_name'] = 'Nombre "mailcow UI"'; +$lang['admin']['apps_name'] = 'Nombre "mailcow Apps"'; +$lang['admin']['queue_ays'] = 'Confirme que desea eliminar todos los elementos de la cola actual.'; +$lang['admin']['arrival_time'] = 'Tiempo de llegada (hora del servidor)'; +$lang['admin']['message_size'] = 'Tamaño del mensaje'; +$lang['admin']['sender'] = 'Remitente'; +$lang['admin']['recipients'] = 'Destinatarios'; +$lang['admin']['admin_domains'] = 'Dominios asignados'; +$lang['admin']['domain_admins'] = 'Administradores por dominio'; +$lang['admin']['flush_queue'] = 'Vaciar la cola'; +$lang['admin']['delete_queue'] = 'Eliminar todos'; +$lang['admin']['queue_deliver_mail'] = 'Entregar'; +$lang['admin']['queue_hold_mail'] = 'Retener'; +$lang['admin']['queue_unhold_mail'] = 'Liberar retención'; +$lang['admin']['sys_mails'] = 'Mails del sistema'; +$lang['admin']['subject'] = 'Asunto'; +$lang['admin']['from'] = 'De'; +$lang['admin']['include_exclude'] = 'Incluir/Excluir'; +$lang['admin']['include_exclude_info'] = 'Por defecto - sin selección - <b>todos los buzones</b> son notificados.'; +$lang['admin']['excludes'] = 'Excluye a estos destinatarios'; +$lang['admin']['includes'] = 'Incluye a estos destinatarios'; +$lang['admin']['text'] = 'Texto'; +$lang['admin']['activate_send'] = 'Activar botón de envío'; +$lang['admin']['send'] = 'Enviar'; + +$lang['quarantine']['quarantine'] = "Cuarentena"; +$lang['quarantine']['learn_spam_delete'] = "Aprender como spam y eliminar"; +$lang['quarantine']['qinfo'] = 'El sistema de cuarentena guardará el correo rechazado en la base de datos, mientras que el remitente <em>no</em> tendrá la impresión de un correo entregado. + <br><b>' . $lang['quarantine']['learn_spam_delete'] . '</b> aprenderá un mensaje como correo no deseado a través del teorema bayesiano y también calculará hashes para rechazar mensajes similares en el futuro. + Ten en cuenta que aprender varios mensajes puede ser lento, dependiendo de las especificaciones del servidor.'; +$lang['quarantine']['release'] = "Liberar"; +$lang['quarantine']['empty'] = 'Sin resultados'; +$lang['quarantine']['toggle_all'] = 'Seleccionar todos'; +$lang['quarantine']['quick_actions'] = 'Acciones'; +$lang['quarantine']['remove'] = 'Remover'; +$lang['quarantine']['received'] = "Recibido"; +$lang['quarantine']['action'] = "Acción"; +$lang['quarantine']['rcpt'] = "Destinatario"; +$lang['quarantine']['qid'] = "Rspamd QID"; +$lang['quarantine']['sender'] = "Remitente"; +$lang['quarantine']['show_item'] = "Mostrar item"; +$lang['quarantine']['check_hash'] = "Buscar hash de archivo @ VT"; +$lang['quarantine']['qitem'] = "Quarantine item"; +$lang['quarantine']['subj'] = "Asunto"; +$lang['quarantine']['recipients'] = "Destinatarios"; +$lang['quarantine']['text_plain_content'] = "Contenido (text/plain)"; +$lang['quarantine']['text_from_html_content'] = "Contenido (html convertido)"; +$lang['quarantine']['atts'] = "Adjuntos"; +$lang['quarantine']['low_danger'] = "Peligro bajo"; +$lang['quarantine']['neutral_danger'] = "Neutral"; +$lang['quarantine']['medium_danger'] = "Peligro medio"; +$lang['quarantine']['high_danger'] = "Peligro alto"; +$lang['quarantine']['danger'] = "Peligro"; +$lang['quarantine']['confirm_delete'] = "Confirmar la eliminación del objeto"; +$lang['quarantine']['qhandler_success'] = "Solicitud enviada con éxito al sistema, puede cerrar la ventana"; + + +$lang['warning']['fuzzy_learn_error'] = "Error aprendiendo hash: %s"; +$lang['danger']['spam_learn_error'] = "Error aprendiendo muestra: %s"; +$lang['success']['qlearn_spam'] = "Message ID %s se aprendió como spam y se eliminó"; + +$lang['debug']['system_containers'] = 'Sistema y Contenedores'; +$lang['debug']['solr_status'] = 'Solr status'; +$lang['debug']['solr_dead'] = 'Solr está empezando, deshabilitado o caído.'; +$lang['debug']['logs'] = 'Logs'; +$lang['debug']['log_info'] = '<p>Los <b>logs en memoria</b> son recopilados en listas de Redis y recortados a LOG_LINES (%d) cada minuto para prevenir sobrecarga en el sistema. + <br>Los logs en memoria no están destinados a ser persistentes. Todas las aplicaciones que logean a la memoria, también logean en el daemon de Docker y, por lo tanto, en el controlador de registro predeterminado. + El log en memoria se debe utilizar para analizar problemas menores con los contenedores.</p> + <p>Los <b>logs externos</b> se recopilan a través de la API de la aplicación dada.</p> + <p>Los <b>logs estáticos</b> son principalmente registros de actividad, que no están registrados en Dockerd pero que aún deben ser persistentes (excepto los registros de API).</p>'; + +$lang['debug']['in_memory_logs'] = 'Logs en memoria'; +$lang['debug']['external_logs'] = 'Logs externos'; +$lang['debug']['static_logs'] = 'Logs estáticos'; +$lang['debug']['solr_uptime'] = 'Uptime'; +$lang['debug']['solr_started_at'] = 'Iniciado el'; +$lang['debug']['solr_last_modified'] = 'Última modificación'; +$lang['debug']['solr_size'] = 'Tamaño'; +$lang['debug']['solr_docs'] = 'Docs'; + +$lang['debug']['disk_usage'] = 'Utilización de disco'; +$lang['debug']['containers_info'] = "Información de los contenedores"; +$lang['debug']['restart_container'] = 'Reiniciar'; + +$lang['quarantine']['release_body'] = "Adjuntamos el mensaje en formato eml a este correo."; +$lang['danger']['release_send_failed'] = "El mensaje no pudo ser liberado: %s"; +$lang['quarantine']['release_subject'] = "Mensaje de cuarentena potencialmente dañino %s"; +$lang['quarantine']['spam_score'] = "Puntaje"; + +$lang['mailbox']['bcc_map'] = "Reglas BCC"; +$lang['mailbox']['bcc_map_type'] = "Tipo de BCC"; +$lang['mailbox']['bcc_type'] = "Tipo de BCC"; +$lang['mailbox']['bcc_sender_map'] = "Remitente"; +$lang['mailbox']['bcc_rcpt_map'] = "Destinatario"; +$lang['mailbox']['bcc_local_dest'] = "Destino local"; +$lang['mailbox']['bcc_destinations'] = "Destino del BCC"; +$lang['mailbox']['bcc_destination'] = "Destino del BCC"; +$lang['edit']['bcc_dest_format'] = 'El destino BCC debe ser una única dirección de correo electrónico válida'; + +$lang['mailbox']['bcc'] = "BCC"; +$lang['mailbox']['bcc_maps'] = "Reglas BCC"; +$lang['mailbox']['bcc_to_sender'] = "Cambiar tipo de regla a 'Remitente'"; +$lang['mailbox']['bcc_to_rcpt'] = "Cambiar tipo de regla a 'Destinatario'"; +$lang['mailbox']['add_bcc_entry'] = "Añadir regla de BCC"; +$lang['mailbox']['add_tls_policy_map'] = "Añadir regla de póliza de TLS"; +$lang['mailbox']['bcc_info'] = "Las reglas BCC se utilizan para enviar silenciosamente copias de todos los mensajes a otra dirección. Se utiliza una regla de destinatario cuando el destino local actúa como destinatario de un correo. Las reglas de remitentes se ajustan al mismo principio.<br/> +El destino local no será informado sobre una entrega fallida al BCC."; +$lang['mailbox']['address_rewriting'] = 'Reescritura de direcciones'; +$lang['mailbox']['recipient_maps'] = 'Reglas de destinatario'; +$lang['mailbox']['recipient_map'] = 'Regla de destinatario'; +$lang['mailbox']['recipient_map_info'] = 'Las reglas de destinatarios se utilizan para reemplazar la dirección de destino en un mensaje antes de que se entregue.'; +$lang['mailbox']['recipient_map_old_info'] = 'El destino original de una regla de destinatario debe ser una dirección de correo electrónico válida o un nombre de dominio.'; +$lang['mailbox']['recipient_map_new_info'] = 'El destino de la regla debe ser una dirección de correo válida.'; +$lang['mailbox']['recipient_map_old'] = 'Destinatario original'; +$lang['mailbox']['recipient_map_new'] = 'Destinatario nuevo'; +$lang['danger']['invalid_recipient_map_new'] = 'Destinatario nuevo no válido: %s'; +$lang['danger']['invalid_recipient_map_old'] = 'Destinatario original no válido: %s'; +$lang['danger']['recipient_map_entry_exists'] = 'Una regla de destinatario "%s" existe'; +$lang['success']['recipient_map_entry_saved'] = 'Regla de destinatario "%s" ha sido guardada'; +$lang['success']['recipient_map_entry_deleted'] = 'Regla de destinatario con ID %s ha sido elimindada'; +$lang['danger']['tls_policy_map_entry_exists'] = 'Regla de póliza de TLS "%s" existe'; +$lang['success']['tls_policy_map_entry_saved'] = 'Regla de póliza de TLS "%s" ha sido guardada'; +$lang['success']['tls_policy_map_entry_deleted'] = 'Regla de póliza de TLS con ID %s ha sido elimindada'; +$lang['mailbox']['add_recipient_map_entry'] = 'Añadir regla de destinatario'; +$lang['danger']['tls_policy_map_parameter_invalid'] = "El parámetro de póliza no es válido."; + +$lang['success']['forwarding_host_removed'] = "Forwarding host %s has been removed"; +$lang['success']['forwarding_host_added'] = "Forwarding host %s has been added"; +$lang['success']['relayhost_removed'] = "Regla %s ha sido eliminada"; +$lang['success']['relayhost_added'] = "Regla %s ha sido añadida"; +$lang['diagnostics']['dns_records'] = 'Récords DNS'; +$lang['diagnostics']['dns_records_24hours'] = '<b>Nota:</b> Los cambios realizados en DNS pueden tardar hasta 24 horas para que su estado actual quede reflejado correctamente en esta página. Esta herramienta es una forma de ver fácilmente cómo configurar los registros DNS y verificar si todos los registros están correctamente en DNS.'; +$lang['diagnostics']['dns_records_name'] = 'Nombre'; +$lang['diagnostics']['dns_records_type'] = 'Tipo'; +$lang['diagnostics']['dns_records_data'] = 'Información correcta'; +$lang['diagnostics']['dns_records_status'] = 'Información actual'; +$lang['diagnostics']['optional'] = 'Este récord es opcional.'; +$lang['diagnostics']['cname_from_a'] = 'Valor derivado del registro A / AAAA. Esto es permitido siempre que el registro apunte al recurso correcto.'; + +//To Translate +$lang['warning']['ip_invalid'] = 'IP inválida omitida: %s'; +$lang['danger']['text_empty'] = 'Texto no puede estar vacío'; +$lang['danger']['subject_empty'] = 'Asunto no puede estar vacío'; +$lang['danger']['from_invalid'] = 'Remitente no puede estar vacío'; +$lang['danger']['network_host_invalid'] = 'Red o host inválido: %s'; + +$lang['add']['mailbox_quota_def'] = 'Cuota de buzón predeterminada'; +$lang['edit']['mailbox_quota_def'] = 'Cuota de buzón predeterminada'; +$lang['danger']['mailbox_defquota_exceeds_mailbox_maxquota'] = 'Cuota predeterminada supera el límite máximo de cuota'; +$lang['danger']['defquota_empty'] = 'La cuota predeterminada por buzón no debe ser 0.'; +$lang['mailbox']['mailbox_defquota'] = 'Tamaño de buzón predeterminado'; +$lang['user']['sync_jobs'] = 'Trabajos de sincronización'; +$lang['acl']['syncjobs'] = 'Trabajos de sincronización'; +$lang['mailbox']['sync_jobs'] = 'Trabajos de sincronización'; +$lang['tfa']['tfa'] = "Autenticación de dos factores"; +$lang['tfa']['set_tfa'] = "Establecer el método de autenticación de dos factores"; +$lang['tfa']['yubi_otp'] = "Yubico OTP"; +$lang['tfa']['key_id'] = "Un identificador para tu YubiKey"; +$lang['tfa']['key_id_totp'] = "Un identificador para tu llave"; +$lang['tfa']['api_register'] = 'mailcow utiliza la API de la nube de Yubico. Por favor, obtén una clave API para tu llave <a href="https://upgrade.yubico.com/getapikey/" target="_blank">aquí</a>.'; +$lang['tfa']['u2f'] = "Autenticación U2F"; +$lang['tfa']['none'] = "Desactivar"; +$lang['tfa']['delete_tfa'] = "Desactivar TFA"; +$lang['tfa']['disable_tfa'] = "Desactivar TFA hasta el próximo inicio de sesión exitoso"; +$lang['tfa']['confirm'] = "Confirmar"; +$lang['tfa']['totp'] = "OTP basado en tiempo (Google Authenticator, Authy, etc.)"; +$lang['tfa']['select'] = "Selecciona"; +$lang['tfa']['waiting_usb_auth'] = "<i>Esperando al dispositivo USB...</i><br><br>Toque el botón en su dispositivo USB U2F ahora."; +$lang['tfa']['waiting_usb_register'] = "<i>Esperando al dispositivo USB....</i><br><br>Ingrese su contraseña arriba y confirme su registro U2F tocando el botón en su dispositivo USB U2F."; +$lang['tfa']['scan_qr_code'] = "Escanee el siguiente código con su aplicación de autenticador o ingrese el código manualmente."; +$lang['tfa']['enter_qr_code'] = "Su código TOTP si su dispositivo no puede escanear códigos QR"; +$lang['tfa']['confirm_totp_token'] = "Por favor confirma los cambios ingresando el token generado"; + +$lang['danger']['transport_dest_exists'] = "Destino de la regla de transporte ya existe"; +$lang['danger']['unlimited_quota_acl'] = "Cuota ilimitada restringida por controles administrativos"; +$lang['danger']['mysql_error'] = "MySQL error: %s"; +$lang['danger']['redis_error'] = "Redis error: %s"; +$lang['danger']['unknown_tfa_method'] = "Método TFA desconocido"; +$lang['danger']['totp_verification_failed'] = "Verificación TOTP fallida"; +$lang['success']['verified_totp_login'] = "Inicio de sesión TOTP verificado"; +$lang['danger']['u2f_verification_failed'] = "Verificación U2F fallida: %s"; +$lang['success']['verified_u2f_login'] = "Inicio de sesión U2F verificado"; +$lang['success']['verified_yotp_login'] = "Inicio de sesión Yubico OTP verificado"; +$lang['danger']['yotp_verification_failed'] = "Verificación Yubico OTP fallida: %s"; +$lang['danger']['ip_list_empty'] = "La lista de IP permitidas no puede estar vacía"; +$lang['danger']['invalid_destination'] = "Formato de destino inválido"; +$lang['danger']['invalid_nexthop'] = "Formato del siguiente destino es inválido"; +$lang['danger']['invalid_nexthop_authenticated'] = "Siguiente destino existe con credenciales diferentes, actualice las credenciales existentes para esta entrada primero."; +$lang['danger']['next_hop_interferes'] = "%s interfiere con el siguiente destino %s"; +$lang['danger']['next_hop_interferes_any'] = "Siguiente destino existente interfiere con %s"; +$lang['danger']['rspamd_ui_pw_length'] = "Contraseña de Rspamd UI debe tener al menos 6 carácteres"; +$lang['success']['rspamd_ui_pw_set'] = "Contraseña de Rspamd UI establecida"; +$lang['success']['queue_command_success'] = "Comando de cola completado con éxito"; +$lang['danger']['unknown'] = "Se produjo un error desconocido"; +$lang['danger']['malformed_username'] = "Nombre de usuario mal formado"; +$lang['info']['awaiting_tfa_confirmation'] = "En espera de confirmación de TFA"; +$lang['success']['logged_in_as'] = "Conectado como %s"; +$lang['danger']['login_failed'] = "Inicio de sesión fallido"; +$lang['danger']['set_acl_failed'] = "Error al establecer el ACL"; +$lang['danger']['no_user_defined'] = "No hay usuario definido"; +$lang['danger']['script_empty'] = "El script no puede estar vacío"; +$lang['danger']['sieve_error'] = "Sieve parser error: %s"; +$lang['danger']['value_missing'] = "Por favor proporcione todos los valores"; +$lang['danger']['filter_type'] = "Tipo de filtro incorrecto"; +$lang['danger']['domain_cannot_match_hostname'] = "El dominio no puede coincidir con el nombre de host"; +$lang['warning']['domain_added_sogo_failed'] = "Se agregó el dominio pero no se pudo reiniciar SOGo, revisa los logs del servidor."; +$lang['danger']['rl_timeframe'] = "Marco de tiempo del límite de velocidad esta incorrecto"; +$lang['success']['rl_saved'] = "Marco de tiempo del límite de velocidad para el objecto %s fue guardado"; +$lang['success']['acl_saved'] = "ACL para el objeto %s guardado"; +$lang['success']['deleted_syncjobs'] = "Trabajos de sincronización eliminados: %s"; +$lang['success']['deleted_syncjob'] = "Se eliminó el trabajo de sincronización con ID %s"; +$lang['success']['delete_filters'] = "Filtros eliminados: %s"; +$lang['success']['delete_filter'] = "Se eliminó el filtro con ID %s"; +$lang['danger']['invalid_bcc_map_type'] = "Tipo de BCC inválido"; +$lang['danger']['bcc_empty'] = "Destino del BCC no puede estar vacío"; +$lang['danger']['bcc_must_be_email'] = "Destino del BCC %s no es un buzón válido"; +$lang['danger']['bcc_exists'] = "BCC %s existe para el tipo %s"; +$lang['success']['bcc_saved'] = "BCC guardado"; +$lang['success']['bcc_edited'] = "BCC %s editado"; +$lang['success']['bcc_deleted'] = "Entrada de BCC elimindada: %s"; +$lang['danger']['private_key_error'] = "Error en la llave privada: %s"; +$lang['danger']['map_content_empty'] = "Contenido no puede estar vacío"; +$lang['success']['settings_map_added'] = "Ajustes agregados"; +$lang['danger']['settings_map_invalid'] = "Ajustes de ID %s invalidos"; +$lang['success']['settings_map_removed'] = "Ajustes del ID %s removidos"; +$lang['danger']['invalid_host'] = "Host no válido especificado: %s"; +$lang['danger']['relayhost_invalid'] = "relayhost %s es invalido"; +$lang['success']['saved_settings'] = "Ajustes guardados"; +$lang['success']['db_init_complete'] = "Inicialización de la base de datos completada"; + +$lang['mailbox']['sieve_info'] = 'Puedes almacenar múltiples filtros por usuario, pero solo un prefiltro y un postfiltro pueden estar activos al mismo tiempo.<br> +Cada filtro será procesado en el orden descrito. Ni un script fallido ni un "keep;" emitido detendrá el procesamiento de otros scripts.<br> +<a href="https://github.com/mailcow/mailcow-dockerized/blob/master/data/conf/dovecot/global_sieve_before" target="_blank">Prefiltro sieve global</a> → Prefiltro → Scripts del usuario → Posfiltro → <a href="https://github.com/mailcow/mailcow-dockerized/blob/master/data/conf/dovecot/global_sieve_after" target="_blank">Posfiltro sieve global</a>'; + +$lang['quarantine']['disabled_by_config'] = "La configuración actual del sistema desactiva la funcionalidad de cuarentena."; +$lang['mailbox']['tls_policy_maps'] = 'Políticas de TLS'; +$lang['mailbox']['tls_policy_maps_long'] = 'Forzar póliza de TLS saliente'; +$lang['mailbox']['tls_policy_maps_info'] = 'Esta póliza anula las reglas de transporte TLS salientes independientemente de la configuración de la política TLS de los usuarios.<br> + Revisa <a href="http://www.postfix.org/postconf.5.html#smtp_tls_policy_maps" target="_blank">the "smtp_tls_policy_maps" docs</a> para mas información.'; +$lang['mailbox']['tls_enforce_in'] = 'Forzar TLS entrance'; +$lang['mailbox']['tls_enforce_out'] = 'Forzar TLS saliente'; + +$lang['add']['syncjob'] = 'Añadir trabajo de sincronización'; +$lang['add']['syncjob_hint'] = 'Ten en cuenta que las contraseñas deben guardarse en texto sin cifrado'; +$lang['edit']['syncjob'] = 'Editar trabajo de sincronización'; +$lang['edit']['client_id'] = 'ID de Cliente'; +$lang['edit']['client_secret'] = 'Secreto de cliente'; +$lang['edit']['scope'] = 'Alcance'; +$lang['edit']['grant_types'] = 'Grant types'; +$lang['edit']['redirect_uri'] = 'Redirect/Callback URL'; +$lang['add']['username'] = 'Usuario'; +$lang['edit']['hostname'] = 'Hostname'; +$lang['add']['port'] = 'Puerto'; +$lang['edit']['encryption'] = 'Cifrado'; +$lang['edit']['maxage'] = 'Antigüedad máxima de los mensajes en días que se migrarán<br><small>(0 = ignorar edad)</small>'; +$lang['edit']['maxbytespersecond'] = 'Max. bytes por segundo <br><small>(0 = ilimitado)</small>'; +$lang['edit']['automap'] = 'Intentar enlazar carpetas ("Sent items", "Sent" => "Sent" etc.)'; +$lang['edit']['skipcrossduplicates'] = 'Omitir mensajes duplicados en carpetas (orden de llegada)'; +$lang['add']['automap'] = 'Intentar enlazar carpetas ("Sent items", "Sent" => "Sent" etc.)'; +$lang['add']['skipcrossduplicates'] = 'Omitir mensajes duplicados en carpetas (orden de llegada)'; +$lang['edit']['subfolder2'] = 'Sincronizar en subcarpeta en destino<br><small>(vacío = no usar subcarpeta)</small>'; +$lang['edit']['mins_interval'] = 'Intervalo (min)'; +$lang['edit']['exclude'] = 'Excluir objectos (regex)'; +$lang['add']['enc_method'] = 'Método de cifrado'; +$lang['add']['mins_interval'] = 'Intervalo de sondeo (minutos)'; +$lang['add']['exclude'] = 'Excluir objectos (regex)'; +$lang['add']['delete2duplicates'] = 'Eliminar duplicados en el destino'; +$lang['add']['delete1'] = 'Eliminar de la fuente cuando se complete'; +$lang['add']['delete2'] = 'Eliminar mensajes en el destino que no están en la fuente'; +$lang['add']['custom_params'] = 'Parámetros personalizados'; +$lang['add']['custom_params_hint'] = 'Correcto: --param=xy, Incorrecto: --param xy'; +$lang['add']['subscribeall'] = 'Suscribirse a todas las carpetas'; +$lang['add']['timeout1'] = 'Tiempo de espera para la conexión al host remoto'; +$lang['add']['timeout2'] = 'Tiempo de espera para la conexión al host local'; +$lang['edit']['timeout1'] = 'Tiempo de espera para la conexión al host remoto'; +$lang['edit']['timeout2'] = 'Tiempo de espera para la conexión al host local'; +$lang['edit']['delete2duplicates'] = 'Eliminar duplicados en el destino'; +$lang['edit']['delete1'] = 'Eliminar de la fuente cuando se complete'; +$lang['edit']['delete2'] = 'Eliminar mensajes en el destino que no están en la fuente'; +$lang['edit']['save'] = 'Guardar'; + +$lang['edit']['gal'] = 'Lista global de direcciones (GAL)'; +$lang['add']['gal'] = 'Lista global de direcciones (GAL)'; +$lang['edit']['gal_info'] = 'El GAL contiene todos los objetos de un dominio y no puede ser editado por ningún usuario. Falta información de disponibilidad en SOGo, si está desactivada. <b>Reinicia SOGo para aplicar los cambios.</b>'; +$lang['add']['gal_info'] = 'El GAL contiene todos los objetos de un dominio y no puede ser editado por ningún usuario. Falta información de disponibilidad en SOGo, si está desactivada. <b>Reinicia SOGo para aplicar los cambios.</b>'; +$lang['edit']['force_pw_update'] = 'Forzar cambio de contraseña en el próximo inicio de sesión'; +$lang['edit']['force_pw_update_info'] = 'Este usuario solo podrá iniciar sesión en la interfaz de usuario de mailcow.'; +$lang['edit']['sogo_access'] = 'Permitir acceso a SOGo'; +$lang['edit']['sogo_access_info'] = 'Permitir acceso a SOGo. Este ajuste no afecta el acceso a todos los demás servicios ni elimina o cambia el perfil de SOGo existente de un usuario.'; +$lang['add']['add_domain_restart'] = 'Agregar dominio y reiniciar SOGo'; +$lang['add']['add_domain_only'] = 'Agregar dominio solamente'; +$lang['admin']['rsettings_preset_1'] = 'Deshabilita todos menos DKIM y el límite de velocidad para usuarios autenticados'; +$lang['mailbox']['filter_table'] = 'Filtrar tabla'; +$lang['admin']['filter_table'] = 'Filtrar tabla'; +$lang['admin']['add_admin'] = 'Añadir administrador'; +$lang['admin']['dkim_domains_wo_keys'] = "Seleccionar dominios con llaves faltantes"; +$lang['user']['spamfilter'] = 'Filtro anti-spam'; +$lang['admin']['spamfilter'] = 'Filtro anti-spam'; +$lang['admin']['empty'] = 'Sin resultados'; +$lang['admin']['rsetting_none'] = 'No hay reglas disponibles'; +$lang['admin']['rsetting_add_rule'] = 'Añadir regla'; +$lang['admin']['add_settings_rule'] = 'Añadir regla de ajustes'; +$lang['admin']['rsetting_desc'] = 'Descripción'; +$lang['admin']['rsetting_content'] = 'Contenido de la regla'; +$lang['admin']['rsettings_preset_2'] = 'Postmaster quiere correo no deseado'; +$lang['admin']['rsettings_insert_preset'] = 'Insertar ejemplo preestablecido "%s"'; +$lang['admin']['rspamd-com_settings'] = '<a href="https://rspamd.com/doc/configuration/settings.html#settings-structure" target="_blank">Documentación de Rspamd</a> + - Se generará automáticamente un nombre de configuración, consulte los ajustes preestablecidos de ejemplo a continuación:'; + +$lang['admin']['change_logo'] = "Cambiar logo"; +$lang['admin']['customize'] = "Personalizar"; +$lang['admin']['logo_info'] = "La imagen se escalará a una altura de 40 px para la barra de navegación superior y un máx. de ancho de 250 px para la página de inicio. Un gráfico escalable es muy recomendable."; +$lang['admin']['upload'] = "Cargar"; +$lang['admin']['app_links'] = "Enlaces de las apps"; +$lang['admin']['app_name'] = "Nombre de la app"; +$lang['admin']['link'] = "Enlace"; +$lang['admin']['remove_row'] = "Eliminar fila"; +$lang['admin']['add_row'] = "Agregar fila"; +$lang['admin']['reset_default'] = "Restablecer valores predeterminados"; +$lang['admin']['merged_vars_hint'] = 'Las filas grises fueron incorporadas de <code>vars.(local.)inc.php</code> y no son modificables.'; +$lang['mailbox']['waiting'] = "Esperando"; +$lang['mailbox']['status'] = "Status"; +$lang['mailbox']['running'] = "En marcha"; +$lang['mailbox']['enable_x'] = "Activar"; +$lang['mailbox']['disable_x'] = "Desactivar"; +$lang['admin']['to_top'] = 'Regresar al principio'; +$lang['admin']['in_use_by'] = 'En uso por'; +$lang['admin']['refresh'] = 'Actualizar'; +$lang['add']['validation_success'] = 'Validado exitosamente'; +$lang['add']['activate_filter_warn'] = 'Todos los demás filtros se desactivarán cuando este filtro se active.'; +$lang['add']['validate'] = 'Validar'; +$lang['mailbox']['add_filter'] = 'Añadir filtro'; +$lang['add']['sieve_desc'] = 'Descripción'; +$lang['edit']['sieve_desc'] = 'Descripción'; +$lang['add']['sieve_type'] = 'Tipo de filtro'; +$lang['edit']['sieve_type'] = 'Tipo de filtro'; +$lang['mailbox']['set_prefilter'] = 'Marcar como prefiltro'; +$lang['mailbox']['set_postfilter'] = 'Marcar como posfiltro'; +$lang['mailbox']['filters'] = 'Filtros'; +$lang['mailbox']['inactive'] = 'Inactivo'; +$lang['edit']['validate_save'] = 'Validar y guardar'; + +$lang['acl']['spam_alias'] = 'Aliases temporales'; +$lang['acl']['tls_policy'] = 'Póliza de TLS'; +$lang['acl']['spam_score'] = 'Puntuación de spam'; +$lang['acl']['spam_policy'] = 'Lista blanca/negra'; +$lang['acl']['delimiter_action'] = 'Acción delimitadora'; +$lang['acl']['eas_reset'] = 'Resetear dispositivos EAS'; +$lang['acl']['sogo_profile_reset'] = 'Resetear perfil SOGo'; +$lang['acl']['quarantine'] = 'Acciones de cuarentena'; +$lang['acl']['quarantine_notification'] = 'Notificaciones de cuarentena'; +$lang['acl']['quarantine_attachments'] = 'Archivos ajuntos en cuarentena'; +$lang['acl']['alias_domains'] = 'Añadir dominios alias'; +$lang['acl']['login_as'] = 'Inicie sesión como usuario del buzón'; +$lang['acl']['bcc_maps'] = 'Rutas BCC'; +$lang['acl']['filters'] = 'Filtros'; +$lang['acl']['ratelimit'] = 'Rate limit'; +$lang['acl']['recipient_maps'] = 'Rutas del destinatario'; +$lang['acl']['unlimited_quota'] = 'Cuota ilimitada para buzones'; +$lang['acl']['prohibited'] = 'Prohibido por ACL'; +$lang['add']['generate'] = 'Generar'; + +$lang['add']['goto_ham'] = 'Clasificar como <span class="text-success"><b>correo deseado</b></span>'; +$lang['add']['goto_spam'] = 'Clasificar como <span class="text-danger"><b>spam</b></span>'; +$lang['add']['goto_null'] = 'Descartar correo silenciosamente'; + +$lang['footer']['hibp_nok'] = '¡Se encontró coincidencia - esta es una contraseña <b>no segura</b>, selecciona otra!'; +$lang['footer']['hibp_ok'] = 'No se encontraron coincidencias'; + +$lang['oauth2']['scope_ask_permission'] = 'Una aplicación solicitó los siguientes permisos'; +$lang['oauth2']['profile'] = 'Perfil'; +$lang['oauth2']['profile_desc'] = 'Ver información personal: nombre de usuario, nombre completo, creado, modificado, activo'; +$lang['oauth2']['permit'] = 'Autorizar aplicación'; +$lang['oauth2']['authorize_app'] = 'Autorizar aplicación'; +$lang['oauth2']['deny'] = 'Rechazar'; +$lang['oauth2']['access_denied'] = 'Inicie sesión como propietario del buzón para otorgar acceso a través de OAuth2.'; \ No newline at end of file diff --git a/data/web/lang/lang.fr.php b/data/web/lang/lang.fr.php index 6d40a02e..48b51ae4 100644 --- a/data/web/lang/lang.fr.php +++ b/data/web/lang/lang.fr.php @@ -132,7 +132,7 @@ $lang['user']['spamfilter_red'] = "Rouge: Ce message est un pourriel et sera rej $lang['user']['spamfilter_hint'] = "La première valeur décrit le \"score bas de pourriel\", la seconde représente le \"score haut de pourriel\"."; $lang['user']['spamfilter_table_domain_policy'] = "N/D (Politique du domaine)"; -$lang['user']['tls_policy_warning'] = "<strong>Attention :<strong> Si vous décidez d'imposer le chiffrement des échanges de courriel, vous pouvez perdre des messages.<br> Les messages qui ne respecte pas la politique seront rejetés avec un message d'erreur définitif par le système de courriel.<br> Cette option s'applique à votre adresse de courriel principale (identifiant de connexion), tous les alias de domaine ainsi que les alias d'adresse <b> qui n'ont que cette unique boîte</b> comme destinataire."; +$lang['user']['tls_policy_warning'] = "<strong>Attention :</strong> Si vous décidez d'imposer le chiffrement des échanges de courriel, vous pouvez perdre des messages.<br> Les messages qui ne respectent pas la politique seront rejetés avec un message d'erreur définitif par le système de courriel.<br> Cette option s'applique à votre adresse de courriel principale (identifiant de connexion), tous les alias de domaine ainsi que les alias d'adresse <b> qui n'ont que cette unique boîte</b> comme destinataire."; $lang['user']['tls_policy'] = "Politique de chiffrement"; $lang['user']['tls_enforce_in'] = "Imposer le TLS entrant"; $lang['user']['tls_enforce_out'] = "Imposer le TLS sortant"; diff --git a/data/web/lang/lang.lv.php b/data/web/lang/lang.lv.php index 2fe3290e..a96b20d5 100644 --- a/data/web/lang/lang.lv.php +++ b/data/web/lang/lang.lv.php @@ -227,7 +227,7 @@ $lang['mailbox']['excludes'] = 'Izslēdzot'; $lang['mailbox']['last_run_reset'] = 'Nākamais grafiks'; $lang['mailbox']['sieve_info'] = 'Jūs varat saglabāt vairākus filtrus katram lietotājam, bet tikai viens pirmsfiltrs un viens pēcfiltrs var būt aktīvs vienlaicīgi.<br> Katrs filtrs tiks apstrādāts aprakstītajā kārtībā. Kļūdains vai izdots skripts "Paturēt;" pārtrauks turpmāko skriptu apstrādi.<br> -Pirmsfiltrs → Lietotāja skripts → Pēcfiltrs → <a href="https://github.com/mailcow/mailcow-dockerized/blob/master/data/conf/dovecot/sieve_after" target="_blank">global sieve postfilter</a>'; +<a href="https://github.com/mailcow/mailcow-dockerized/blob/master/data/conf/dovecot/global_sieve_before" target="_blank">Global sieve prefilter</a> → Pirmsfiltrs → Lietotāja skripts → Pēcfiltrs → <a href="https://github.com/mailcow/mailcow-dockerized/blob/master/data/conf/dovecot/global_sieve_after" target="_blank">Global sieve postfilter</a>'; $lang['info']['no_action'] = 'No action applicable'; @@ -494,6 +494,7 @@ $lang['quarantine']['show_item'] = "Parādīt vienumus"; $lang['quarantine']['check_hash'] = "Meklēt faila hašu @ VT"; $lang['quarantine']['qitem'] = "Karantīnas vienumi"; $lang['quarantine']['subj'] = "Priekšmets"; +$lang['quarantine']['recipients'] = "Adresāts"; $lang['quarantine']['text_plain_content'] = "Saturs (teksts/vienkāršs)"; $lang['quarantine']['text_from_html_content'] = "Saturs (konvertēts html)"; $lang['quarantine']['atts'] = "Pielikumi"; diff --git a/data/web/lang/lang.nl.php b/data/web/lang/lang.nl.php index 0c73ee99..a373589e 100644 --- a/data/web/lang/lang.nl.php +++ b/data/web/lang/lang.nl.php @@ -3,7 +3,6 @@ * Dutch language file * * Created and maintained by Geitenijs - * */ $lang['header']['apps'] = 'Apps'; @@ -23,6 +22,8 @@ $lang['footer']['cancel'] = 'Annuleren'; $lang['footer']['hibp_nok'] = 'Dit is een potentieel onveilig wachtwoord!'; $lang['footer']['hibp_ok'] = 'Dit wachtwoord is relatief veilig'; +$lang['danger']['transport_dest_exists'] = "Transportbestemming bestaat reeds"; +$lang['danger']['unlimited_quota_acl'] = "Onbeperkt quotum geweigerd door toegangscontrole"; $lang['danger']['mysql_error'] = "MySQL-fout: %s"; $lang['danger']['redis_error'] = "Redis-fout: %s"; $lang['danger']['unknown_tfa_method'] = "Onbekende tweefactorauthenticatiemethode"; @@ -63,7 +64,7 @@ $lang['success']['delete_filters'] = "Filters %s zijn verwijderd"; $lang['success']['delete_filter'] = "Filter %s is verwijderd"; $lang['danger']['invalid_bcc_map_type'] = "Ongeldig BCC-kaart type"; $lang['danger']['bcc_empty'] = "BCC-bestemming dient ingevuld te worden"; -$lang['danger']['bcc_must_be_email'] = "BCC-kaart %s is geen geldig e-mailadres"; +$lang['danger']['bcc_must_be_email'] = "BCC-bestemming %s is geen geldig e-mailadres"; $lang['danger']['bcc_exists'] = "BCC-kaart %s bestaat voor type %s"; $lang['success']['bcc_saved'] = "BCC-kaart is opgeslagen"; $lang['success']['bcc_edited'] = "BCC-kaart %s is gewijzigd"; @@ -121,6 +122,7 @@ $lang['success']['domain_admin_added'] = "Domeinbeheerder %s is toegevoegd"; $lang['success']['admin_added'] = "Administrator %s is toegevoegd"; $lang['success']['admin_modified'] = "Wijzigingen aan administrator zijn opgeslagen"; $lang['success']['admin_api_modified'] = "Wijzigingen aan de API zijn opgeslagen"; +$lang['success']['license_modified'] = "Wijzigingen aan de licentie zijn opgeslagen"; $lang['danger']['username_invalid'] = "Gebruikersnaam %s kan niet worden gebruikt"; $lang['danger']['password_mismatch'] = "De ingevoerde wachtwoorden komen niet overeen"; $lang['danger']['password_complexity'] = "Wachtwoord voldoet niet aan de vereisten (6 tekens lang, letters en nummers)"; @@ -155,7 +157,7 @@ $lang['danger']['domain_quota_m_in_use'] = "Domeinquotum moet gelijk zijn aan, o $lang['danger']['mailboxes_in_use'] = "Maximaal aantal postvakken moet gelijk zijn aan, of groter zijn dan %d"; $lang['danger']['aliases_in_use'] = "Maximaal aantal aliassen moet gelijk zijn aan, of groter zijn dan %d"; $lang['danger']['sender_acl_invalid'] = "Toegangscontrole van afzender %s is ongeldig"; -$lang['danger']['domain_not_empty'] = "Kan geen domein in gebruik verwijderen"; +$lang['danger']['domain_not_empty'] = "Domein %s is in gebruik, verwijderen niet mogelijk"; $lang['danger']['validity_missing'] = 'Wijs een geldigheidstermijn toe'; $lang['user']['loading'] = "Bezig met laden..."; $lang['user']['force_pw_update'] = 'Er <b>moet</b> een nieuw wachtwoord worden ingesteld voordat er gebruik kan worden gemaakt van deze dienst.'; @@ -324,7 +326,7 @@ $lang['mailbox']['mins_interval'] = 'Interval (min)'; $lang['mailbox']['last_run'] = 'Laatst uitgevoerd'; $lang['mailbox']['excludes'] = 'Exclusief'; $lang['mailbox']['last_run_reset'] = 'Plan volgende'; -$lang['mailbox']['sieve_info'] = 'Het is mogelijk om meerdere filters per gebruiker in te stellen, maar er kan slechts één voorfilter en één nafilter tegelijkertijd actief zijn.<br>Elk filter zal in de aangegeven volgorde worden verwerkt. Noch een mislukt script, noch een gespecificeerde "keep;" stopt met het verwerken van volgende scripts.<br>Voorfilter → Gebruikersscripts → Nafilter → <a href="https://github.com/mailcow/mailcow-dockerized/blob/master/data/conf/dovecot/sieve_after" target="_blank">Globaal filter</a>'; +$lang['mailbox']['sieve_info'] = 'Het is mogelijk om meerdere filters per gebruiker in te stellen, maar er kan slechts één voorfilter en één nafilter tegelijkertijd actief zijn.<br>Elk filter zal in de aangegeven volgorde worden verwerkt. Noch een mislukt script, noch een gespecificeerde "keep;" stopt met het verwerken van volgende scripts.<br><a href="https://github.com/mailcow/mailcow-dockerized/blob/master/data/conf/dovecot/global_sieve_before" target="_blank">Globaal voorfilter</a> → Voorfilter → Gebruikersscripts → Nafilter → <a href="https://github.com/mailcow/mailcow-dockerized/blob/master/data/conf/dovecot/global_sieve_after" target="_blank">Globaal nafilter</a>'; $lang['info']['no_action'] = 'Geen handeling van toepassing'; $lang['edit']['syncjob'] = 'Wijzig synchronisatietaak'; @@ -351,7 +353,9 @@ $lang['edit']['title'] = 'Wijzig object'; $lang['edit']['target_address'] = 'Doeladres(sen) <small>(kommagescheiden)</small>'; $lang['edit']['active'] = 'Actief'; $lang['edit']['gal'] = 'Globale adreslijst'; +$lang['add']['gal'] = 'Globale adreslijst'; $lang['edit']['gal_info'] = 'De globale adreslijst bevat alle objecten van een domein. Deze kunnen door geen enkele gebruiker worden bewerkt. <b>Herstart SOGo om wijzigingen door te voeren.</b>'; +$lang['add']['gal_info'] = 'De globale adreslijst bevat alle objecten van een domein. Deze kunnen door geen enkele gebruiker worden bewerkt. <b>Herstart SOGo om wijzigingen door te voeren.</b>'; $lang['edit']['force_pw_update'] = 'Vereis nieuw wachtwoord bij eerstvolgende login'; $lang['edit']['force_pw_update_info'] = 'Deze gebruiker kan hierdoor enkel inloggen op Mailcow UI, totdat de procedure succesvol doorlopen is.'; $lang['edit']['sogo_access'] = 'Geef toegang to SOGo'; @@ -410,6 +414,7 @@ $lang['acl']['bcc_maps'] = 'BCC-kaarten'; $lang['acl']['filters'] = 'Filters'; $lang['acl']['ratelimit'] = 'Ratelimit'; $lang['acl']['recipient_maps'] = 'Ontvanger-kaarten'; +$lang['acl']['unlimited_quota'] = 'Onbeperkt quotum voor postvakken'; $lang['acl']['prohibited'] = 'Toegang geweigerd'; $lang['mailbox']['quarantine_notification'] = 'Quarantaine-notificaties'; @@ -444,6 +449,7 @@ $lang['edit']['delete2duplicates'] = 'Verwijder duplicaten op de bestemming'; $lang['edit']['delete1'] = 'Verwijder van oorsprong wanneer voltooid'; $lang['edit']['delete2'] = 'Verwijder berichten die zich niet in de oorsprong bevinden'; $lang['add']['custom_params'] = 'Aangepaste parameters'; +$lang['add']['custom_params_hint'] = 'Goed: --param=xy, fout: --param xy'; $lang['add']['subscribeall'] = 'Abonneer op alle mappen'; $lang['add']['timeout1'] = 'Time-out voor verbinding met externe hosts'; $lang['add']['timeout2'] = 'Time-out voor verbinding met lokale hosts'; @@ -482,9 +488,9 @@ $lang['add']['restart_sogo_hint'] = 'SOGo dient opnieuw opgestart te worden nada $lang['add']['goto_null'] = 'Houd e-mail achterwege'; $lang['add']['goto_ham'] = 'Leer als <span class="text-success"><b>ham</b></span>'; $lang['add']['goto_spam'] = 'Leer als <span class="text-danger"><b>spam</b></span>'; -$lang['add']['validation_success'] = 'Succesvol gevalideerd'; +$lang['add']['validation_success'] = 'Succesvol geverifieerd'; $lang['add']['activate_filter_warn'] = 'Alle overige filters zullen worden gedeactiveerd zodra deze geactiveerd.'; -$lang['add']['validate'] = 'Valideer'; +$lang['add']['validate'] = 'Verifieer'; $lang['mailbox']['add_filter'] = 'Voeg filter toe'; $lang['add']['sieve_desc'] = 'Korte beschrijving'; $lang['edit']['sieve_desc'] = 'Korte beschrijving'; @@ -495,7 +501,7 @@ $lang['mailbox']['set_postfilter'] = 'Stel in als nafilter'; $lang['mailbox']['filters'] = 'Filters'; $lang['mailbox']['sync_jobs'] = 'Synchronisatietaken'; $lang['mailbox']['inactive'] = 'Inactief'; -$lang['edit']['validate_save'] = 'Valideer en sla op'; +$lang['edit']['validate_save'] = 'Verifieer en sla op'; $lang['login']['username'] = 'Gebruikersnaam'; @@ -514,7 +520,7 @@ $lang['tfa']['none'] = "Deactiveer"; $lang['tfa']['delete_tfa'] = "Schakel tweefactorauthenticatie uit"; $lang['tfa']['disable_tfa'] = "Pauzeer tweefactorauthenticatie tot de eerstvolgende succesvolle login"; $lang['tfa']['confirm'] = "Bevestig"; -$lang['tfa']['totp'] = "TOTP (30-secondecode)"; +$lang['tfa']['totp'] = "TOTP (Step Two, Authy, etc.)"; $lang['tfa']['select'] = "Selecteer..."; $lang['tfa']['waiting_usb_auth'] = "<i>In afwachting van USB-apparaat...</i><br><br>Druk nu op de knop van je U2F-apparaat."; $lang['tfa']['waiting_usb_register'] = "<i>In afwachting van USB-apparaat...</i><br><br>Voer je wachtwoord hierboven in en bevestig de registratie van het U2F-apparaat door op de knop van het apparaat te drukken."; @@ -544,7 +550,7 @@ $lang['admin']['f2b_netban_ipv4'] = 'Voer de IPv4-subnetgrootte in waar de verba $lang['admin']['f2b_netban_ipv6'] = 'Voer de IPv6-subnetgrootte in waar de verbanning van kracht moet zijn (8-128)'; $lang['admin']['f2b_whitelist'] = 'Netwerken/hosts op de witte lijst'; $lang['admin']['f2b_blacklist'] = 'Netwerken/hosts op de zwarte lijst'; -$lang['admin']['f2b_list_info'] = 'Wees ervan bewust dat een host of netwerk op de zwarte lijst altijd hogere prioriteit heeft dan eenzelfde op de witte lijst.'; +$lang['admin']['f2b_list_info'] = 'Een host of netwerk op de zwarte lijst gaat altijd boven eenzelfde op de witte lijst <b>Het doorvoeren van wijzigingen kan enkele seconden in beslag nemen.</b>'; $lang['admin']['search_domain_da'] = 'Zoek domeinen'; $lang['admin']['r_inactive'] = 'Inactieve beperkingen'; $lang['admin']['r_active'] = 'Actieve beperkingen'; @@ -572,7 +578,7 @@ $lang['admin']['add_admin'] = 'Voeg administrator toe'; $lang['admin']['add_settings_rule'] = 'Voeg regel toe'; $lang['admin']['rsetting_desc'] = 'Korte beschrijving'; $lang['admin']['rsetting_content'] = 'Regelinhoud'; -$lang['admin']['rsetting_none'] = 'Geen regel beschikbaar'; +$lang['admin']['rsetting_none'] = 'Geen regels beschikbaar'; $lang['admin']['rsetting_no_selection'] = 'Selecteer een regel'; $lang['admin']['rsettings_preset_1'] = 'Schakel alles uit voor geauthenticeerde gebruikers, behalve DKIM en ratelimiting'; $lang['admin']['rsettings_preset_2'] = 'Postmeesters willen spam'; @@ -605,7 +611,7 @@ $lang['admin']['time'] = 'Tijd'; $lang['admin']['last_applied'] = 'Voor het laatst toegepast'; $lang['admin']['reset_limit'] = 'Verwijder hash'; $lang['admin']['hash_remove_info'] = 'Het verwijderen van een ratelimit-hash, indien nog aanwezig, zal zijn teller volledig herstellen.<br>Elke hash wordt aangeduid met een aparte kleur.'; -$lang['warning']['hash_not_found'] = 'Hash niet gevonden'; +$lang['warning']['hash_not_found'] = 'Hash niet gevonden, mogelijk is deze al verwijderd.'; $lang['success']['hash_deleted'] = 'Hash verwijderd'; $lang['admin']['authed_user'] = 'Geauthenticeerde gebruiker'; $lang['admin']['priority'] = 'Prioriteit'; @@ -618,7 +624,11 @@ $lang['admin']['forwarding_hosts'] = 'Doorstuurhosts'; $lang['admin']['forwarding_hosts_hint'] = 'Inkomende berichten worden onvoorwaardelijk geaccepteerd vanaf iedere host hieronder vermeld. Deze hosts worden hierdoor niet gecontroleerd op DNSBLs, en zullen de greylisting omzeilen. Spam wordt daarentegen zoals gebruikelijk in de spamfolder geplaatst. Dit wordt vaak gebruikt om mailservers te specificeren die mails doorsturen naar deze Mailcow-server.'; $lang['admin']['forwarding_hosts_add_hint'] = 'Het is mogelijk om IPv4- of IPv6-adressen, netwerken in CIDR-notatie, hostnames (worden omgezet naar IP-adressen) of domeinnamen (worden tevens omgezet naar IP-adressen of, bij gebrek daaraan, MX-records) op te geven.'; $lang['admin']['relayhosts_hint'] = 'Stel afzender-afhankelijke transportkaarten in om deze te kunnen gebruiken bij de configuratie van een domein.<br>De transportservice is altijd "smtp:". Er wordt rekening gehouden met het uitgaande versleutelingsbeleid van individuele gebruikers.'; -$lang['admin']['transports_hint'] = 'Een transportkaart wordt boven een afzender-afhankelijke transportkaart verkozen.<br>Het uitgaande versleutelingsbeleid van individuele gebruikers wordt genegeerd en kan enkel worden gehandhaafd doormiddel van globaal versleutelingsbeleid. De transportservice is altijd "smtp:".<br>Om de inloggegevens van een (voorbeeld) nexthop "[host]:25" te bepalen, zoekt Postfix <b>altijd</b> naar "nexthop" voodat er wordt gekeken naar "[nexthop]:25". Dit maakt het onmogelijk om "nexthop" en "[nexthop]:25" tegelijkertijd te gebruiken.'; +$lang['admin']['transports_hint'] = '→ Een transportkaart wordt boven een afzender-afhankelijke transportkaart verkozen.<br> + → Het uitgaande versleutelingsbeleid van individuele gebruikers wordt genegeerd en kan enkel worden gehandhaafd doormiddel van globaal versleutelingsbeleid.<br> + → De transportservice is altijd "smtp:".<br> + → Adressen overeenkomend met "/localhost$/" zullen altijd via "local:" getransporteerd worden, hierdoor zullen "*"-bestemmingen niet van toepassing zijn op deze adressen.<br> + → Om de inloggegevens van een (voorbeeld) nexthop "[host]:25" te bepalen, zoekt Postfix <b>altijd</b> naar "nexthop" voodat er wordt gekeken naar "[nexthop]:25". Dit maakt het onmogelijk om "nexthop" en "[nexthop]:25" tegelijkertijd te gebruiken.'; $lang['admin']['add_relayhost_hint'] = 'Wees ervan bewust dat de authenticatiedata onversleuteld wordt opgeslagen!'; $lang['admin']['add_transports_hint'] = 'Wees ervan bewust dat de authenticatiedata onversleuteld wordt opgeslagen!'; $lang['admin']['host'] = 'Host'; @@ -665,6 +675,7 @@ $lang['admin']['active_rspamd_settings_map'] = "Huidige instellingen"; $lang['admin']['quota_notifications_info'] = "Quotum-notificaties worden verstuurd naar gebruikers wanneer deze 80% of 95% van hun opslagcapaciteit overschreden hebben."; $lang['admin']['quarantine_retention_size'] = "Maximale retenties per postvak:<br><small>Gebruik 0 om deze functionaliteit <b>uit te schakelen</b>.</small>"; $lang['admin']['quarantine_max_size'] = "Maximale grootte in MiB (mail die de limiet overschrijdt zal worden verwijderd):<br><small>0 betekent <b>niet</b> onbeperkt!</small>"; +$lang['admin']['quarantine_max_age'] = "Maximale leeftijd in dagen<br><small>Dit kan niet minder zijn dan 1 dag.</small>"; $lang['admin']['quarantine_exclude_domains'] = "Sluit de volgende domeinen en aliasdomeinen uit"; $lang['admin']['quarantine_release_format'] = "Verstuur vrijgegeven items als"; $lang['admin']['quarantine_release_format_raw'] = "Origineel"; @@ -731,6 +742,7 @@ $lang['quarantine']['show_item'] = "Laat item zien"; $lang['quarantine']['check_hash'] = "Zoek bestandshash op in VT"; $lang['quarantine']['qitem'] = "Quarantaine-item"; $lang['quarantine']['subj'] = "Onderwerp"; +$lang['quarantine']['recipients'] = "Ontvangers"; $lang['quarantine']['text_plain_content'] = "Inhoud (tekst)"; $lang['quarantine']['text_from_html_content'] = "Inhoud (geconverteerde html)"; $lang['quarantine']['atts'] = "Bijlagen"; @@ -739,9 +751,9 @@ $lang['quarantine']['neutral_danger'] = "Neutraal/geen beoordeling"; $lang['quarantine']['medium_danger'] = "Middelmatig risico"; $lang['quarantine']['high_danger'] = "Hoog risico"; $lang['quarantine']['danger'] = "Risico"; +$lang['quarantine']['spam_score'] = "Score"; $lang['quarantine']['confirm_delete'] = "Bevestig de verwijdering van dit item."; $lang['quarantine']['qhandler_success'] = "Verzoek met succes verzonden naar het systeem. Je kunt het venster nu veilig sluiten."; - $lang['warning']['fuzzy_learn_error'] = "Fuzzy hash training-fout: %s"; $lang['danger']['spam_learn_error'] = "Spamtraining-fout: %s"; $lang['success']['qlearn_spam'] = "Bericht %s werd als spam gemarkeerd en is verwijderd"; @@ -804,6 +816,7 @@ $lang['success']['tls_policy_map_entry_saved'] = 'Versleutelingsbeleid "%s" is o $lang['success']['tls_policy_map_entry_deleted'] = 'Versleutelingsbeleid %s is verwijderd'; $lang['mailbox']['add_recipient_map_entry'] = 'Voeg ontvangerkaart toe'; $lang['danger']['tls_policy_map_parameter_invalid'] = "Beleidsparameter is ongeldig"; +$lang['danger']['temp_error'] = "Tijdelijke fout"; $lang['oauth2']['scope_ask_permission'] = 'Een applicatie heeft toegang tot de volgende onderdelen gevraagd'; $lang['oauth2']['profile'] = 'Profiel'; @@ -829,3 +842,19 @@ $lang['danger']['text_empty'] = 'De tekst dient ingevuld te worden'; $lang['danger']['subject_empty'] = 'Het onderwerp dient ingevuld te worden'; $lang['danger']['from_invalid'] = 'De afzender dient ingevuld te worden'; $lang['danger']['network_host_invalid'] = 'Ongeldig netwerk of host: %s'; + +$lang['add']['mailbox_quota_def'] = 'Standaard postvakquotum'; +$lang['edit']['mailbox_quota_def'] = 'Standaard postvakquotum'; +$lang['danger']['mailbox_defquota_exceeds_mailbox_maxquota'] = 'Standaardquotum overschrijdt de quotumlimiet'; +$lang['danger']['defquota_empty'] = 'Standaardquotum per postvak dient geen 0 te zijn.'; +$lang['mailbox']['mailbox_defquota'] = 'Standaard postvakgrootte'; + +$lang['admin']['api_info'] = 'De API is nog in ontwikkeling.'; + +$lang['admin']['guid_and_license'] = 'Licentie en identificatie'; +$lang['admin']['guid'] = 'Identificatienummer - GUID'; +$lang['admin']['license_info'] = 'Een licentie is niet verplicht, maar je steunt hiermee wel de ontwikkeling.<br><a href="https://www.servercow.de/mailcow?lang=nl#sal" target="_blank" alt="SAL order">Registreer je GUID hier</a>, of <a href="https://www.servercow.de/mailcow?lang=nl#support" target="_blank" alt="Support order">schaf ondersteuning aan voor deze installatie.</a>'; +$lang['admin']['validate_license_now'] = 'Valideer licentie'; + +$lang['admin']['customer_id'] = 'Klantnummer'; +$lang['admin']['service_id'] = 'Servicenummer'; diff --git a/data/web/mailbox.php b/data/web/mailbox.php index 96c2e16d..d254dc50 100644 --- a/data/web/mailbox.php +++ b/data/web/mailbox.php @@ -348,10 +348,16 @@ $is_dual = (!empty($_SESSION["dual-login"]["username"])) ? 'true' : 'false'; echo "var role = '". $role . "';\n"; echo "var is_dual = " . $is_dual . ";\n"; echo "var pagination_size = '". $PAGINATION_SIZE . "';\n"; +$ALLOW_ADMIN_EMAIL_LOGIN = (preg_match( + "/^([yY][eE][sS]|[yY])+$/", + $_ENV["ALLOW_ADMIN_EMAIL_LOGIN"] +)) ? "true" : "false"; +echo "var ALLOW_ADMIN_EMAIL_LOGIN = " . $ALLOW_ADMIN_EMAIL_LOGIN . ";\n"; ?> </script> <?php $js_minifier->add('/web/js/site/mailbox.js'); +$js_minifier->add('/web/js/site/pwgen.js'); require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/footer.inc.php'; } else { diff --git a/data/web/mobileconfig.php b/data/web/mobileconfig.php index ade4f606..38b249c6 100644 --- a/data/web/mobileconfig.php +++ b/data/web/mobileconfig.php @@ -22,7 +22,7 @@ try { $stmt = $pdo->prepare("SELECT `name` FROM `mailbox` WHERE `username`= :username"); $stmt->execute(array(':username' => $email)); $MailboxData = $stmt->fetch(PDO::FETCH_ASSOC); - $displayname = empty($MailboxData['name']) ? $email : $MailboxData['name']; + $displayname = htmlspecialchars(empty($MailboxData['name']) ? $email : $MailboxData['name'], ENT_NOQUOTES); } catch(PDOException $e) { $displayname = $email; diff --git a/data/web/modals/footer.php b/data/web/modals/footer.php index b5e49b15..b7ebaf08 100644 --- a/data/web/modals/footer.php +++ b/data/web/modals/footer.php @@ -81,7 +81,7 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm <ol> <li> <p><?=$lang['tfa']['scan_qr_code'];?></p> - <img src="<?=$tfa->getQRCodeImageAsDataUri($_SESSION['mailcow_cc_username'], $totp_secret);?>"> + <img id="tfa-qr-img" data-totp-secret="<?=$totp_secret;?>" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII="> <p class="help-block"><?=$lang['tfa']['enter_qr_code'];?>:<br /> <code><?=$totp_secret;?></code> </p> diff --git a/data/web/modals/mailbox.php b/data/web/modals/mailbox.php index c12df381..5dd158ca 100644 --- a/data/web/modals/mailbox.php +++ b/data/web/modals/mailbox.php @@ -1,7 +1,7 @@ <?php if (!isset($_SESSION['mailcow_cc_role'])) { - header('Location: /'); - exit(); + header('Location: /'); + exit(); } ?> <!-- add mailbox modal --> @@ -21,7 +21,7 @@ if (!isset($_SESSION['mailcow_cc_role'])) { </div> </div> <div class="form-group"> - <label class="control-label col-sm-2" for="domain"><?=$lang['add']['domain'];?>:</label> + <label class="control-label col-sm-2" for="domain"><?=$lang['add']['domain'];?></label> <div class="col-sm-10"> <select class="full-width-select" data-live-search="true" id="addSelectDomain" name="domain" required> <?php @@ -43,8 +43,8 @@ if (!isset($_SESSION['mailcow_cc_role'])) { <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> - <small class="help-block">min. 1</small> + <input type="text" class="form-control" name="quota" min="0" max="" id="addInputQuota" disabled value="<?=$lang['add']['select_domain'];?>" required> + <small class="help-block">0 = ∞</small> </div> </div> <div class="form-group"> @@ -85,53 +85,67 @@ if (!isset($_SESSION['mailcow_cc_role'])) { <h3 class="modal-title"><?=$lang['mailbox']['add_domain'];?></h3> </div> <div class="modal-body"> - <form class="form-horizontal" data-cached-form="true" data-id="add_domain" role="form"> - <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" required> - </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" required> - </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" value="400" required> - </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" value="10" required> - </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" value="3072" required> - </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" value="10240" required> - </div> - </div> - <div class="form-group"> - <div class="col-sm-offset-2 col-sm-10"> - <div class="checkbox"> - <label><input type="checkbox" value="1" name="active" checked> <?=$lang['add']['active'];?></label> - </div> + <form class="form-horizontal" data-cached-form="true" data-id="add_domain" role="form"> + <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" required> + </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" required> + </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" value="400" required> + </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" value="10" required> </div> </div> + <div class="form-group"> + <label class="control-label col-sm-2" for="defquota"><?=$lang['add']['mailbox_quota_def'];?></label> + <div class="col-sm-10"> + <input type="number" class="form-control" name="defquota" value="3072" required> + </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" value="10240" required> + </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" value="10240" required> + </div> + </div> + <div class="form-group"> + <div class="col-sm-offset-2 col-sm-10"> + <div class="checkbox"> + <label><input type="checkbox" value="1" name="gal" checked> <?=$lang['edit']['gal'];?></label> + <small class="help-block"><?=$lang['edit']['gal_info'];?></small> + </div> + </div> + </div> + <div class="form-group"> + <div class="col-sm-offset-2 col-sm-10"> + <div class="checkbox"> + <label><input type="checkbox" value="1" name="active" checked> <?=$lang['add']['active'];?></label> + </div> + </div> + </div> <hr> - <div class="form-group"> - <label class="control-label col-sm-2" for="rl_frame"><?=$lang['acl']['ratelimit'];?></label> + <div class="form-group"> + <label class="control-label col-sm-2" for="rl_frame"><?=$lang['acl']['ratelimit'];?></label> <div class="col-sm-7"> <input name="rl_value" type="number" value="<?=(!empty($rl['value'])) ? $rl['value'] : null;?>" class="form-control" placeholder="disabled"> </div> @@ -144,26 +158,26 @@ if (!isset($_SESSION['mailcow_cc_role'])) { </div> </div> <hr> - <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" value="1" name="backupmx"> <?=$lang['add']['relay_domain'];?></label> - <br /> - <label><input type="checkbox" value="1" name="relay_all_recipients"> <?=$lang['add']['relay_all'];?></label> - <p><?=$lang['add']['relay_all_info'];?></p> - </div> - </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" value="1" name="backupmx"> <?=$lang['add']['relay_domain'];?></label> + <br /> + <label><input type="checkbox" value="1" name="relay_all_recipients"> <?=$lang['add']['relay_all'];?></label> + <p><?=$lang['add']['relay_all_info'];?></p> + </div> + </div> + </div> <hr> - <div class="form-group"> - <div class="col-sm-offset-2 col-sm-10"> + <div class="form-group"> + <div class="col-sm-offset-2 col-sm-10"> <button class="btn btn-default" data-action="add_item" data-id="add_domain" data-api-url='add/domain' data-api-attr='{}' href="#"><?=$lang['add']['add_domain_only'];?></button> <button class="btn btn-default" data-action="add_item" data-id="add_domain" data-api-url='add/domain' data-api-attr='{"restart_sogo":"1"}' href="#"><?=$lang['add']['add_domain_restart'];?></button> - </div> - </div> - <p><span class="glyphicon glyphicon-exclamation-sign text-danger"></span> <?=$lang['add']['restart_sogo_hint'];?></p> - </form> + </div> + </div> + <p><span class="glyphicon glyphicon-exclamation-sign text-danger"></span> <?=$lang['add']['restart_sogo_hint'];?></p> + </form> </div> </div> </div> @@ -177,63 +191,63 @@ if (!isset($_SESSION['mailcow_cc_role'])) { <h3 class="modal-title"><?=$lang['mailbox']['add_resource'];?></h3> </div> <div class="modal-body"> - <form class="form-horizontal" data-cached-form="true" role="form" data-id="add_resource"> - <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" 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 data-live-search="true" name="domain" title="<?=$lang['add']['select'];?>" required> - <?php + <form class="form-horizontal" data-cached-form="true" role="form" data-id="add_resource"> + <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" 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 data-live-search="true" name="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" 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"> - <label class="control-label col-sm-2" for="multiple_bookings_select"><?=$lang['add']['multiple_bookings'];?>:</label> - <div class="col-sm-10"> - <select name="multiple_bookings_select" id="multiple_bookings_select" title="<?=$lang['add']['select'];?>" required> - <option value="0"><?=$lang['mailbox']['booking_0'];?></option> - <option value="-1" selected><?=$lang['mailbox']['booking_lt0'];?></option> - <option value="custom"><?=$lang['mailbox']['booking_custom'];?></option> - </select> + 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" 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"> + <label class="control-label col-sm-2" for="multiple_bookings_select"><?=$lang['add']['multiple_bookings'];?></label> + <div class="col-sm-10"> + <select name="multiple_bookings_select" id="multiple_bookings_select" title="<?=$lang['add']['select'];?>" required> + <option value="0"><?=$lang['mailbox']['booking_0'];?></option> + <option value="-1" selected><?=$lang['mailbox']['booking_lt0'];?></option> + <option value="custom"><?=$lang['mailbox']['booking_custom'];?></option> + </select> <div style="display:none" id="multiple_bookings_custom_div"> <hr> <input type="number" class="form-control" name="multiple_bookings_custom" id="multiple_bookings_custom"> </div> <input type="hidden" name="multiple_bookings" id="multiple_bookings"> - </div> - </div> - <div class="form-group"> - <div class="col-sm-offset-2 col-sm-10"> - <div class="checkbox"> - <label><input type="checkbox" value="1" name="active" checked> <?=$lang['add']['active'];?></label> - </div> - </div> - </div> - <div class="form-group"> - <div class="col-sm-offset-2 col-sm-10"> + </div> + </div> + <div class="form-group"> + <div class="col-sm-offset-2 col-sm-10"> + <div class="checkbox"> + <label><input type="checkbox" value="1" name="active" checked> <?=$lang['add']['active'];?></label> + </div> + </div> + </div> + <div class="form-group"> + <div class="col-sm-offset-2 col-sm-10"> <button class="btn btn-default" data-action="add_item" data-id="add_resource" data-api-url='add/resource' data-api-attr='{}' href="#"><?=$lang['admin']['add'];?></button> - </div> - </div> - </form> + </div> + </div> + </form> </div> </div> </div> @@ -247,44 +261,44 @@ if (!isset($_SESSION['mailcow_cc_role'])) { <h3 class="modal-title"><?=$lang['mailbox']['add_alias'];?></h3> </div> <div class="modal-body"> - <form class="form-horizontal" data-cached-form="true" role="form" data-id="add_alias"> - <input type="hidden" value="0" name="active"> - <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" required></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 id="textarea_alias_goto" autocorrect="off" autocapitalize="none" class="form-control" rows="5" id="goto" name="goto" required></textarea> - <p><?=$lang['add']['target_address_info'];?></p> - <div class="checkbox"> + <form class="form-horizontal" data-cached-form="true" role="form" data-id="add_alias"> + <input type="hidden" value="0" name="active"> + <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" required></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 id="textarea_alias_goto" autocorrect="off" autocapitalize="none" class="form-control" rows="5" id="goto" name="goto" required></textarea> + <p><?=$lang['add']['target_address_info'];?></p> + <div class="checkbox"> <label><input class="goto_checkbox" type="checkbox" value="1" name="goto_null"> <?=$lang['add']['goto_null'];?></label> - </div> + </div> <div class="checkbox"> <label><input class="goto_checkbox" type="checkbox" value="1" name="goto_spam"> <?=$lang['add']['goto_spam'];?></label> - </div> + </div> <div class="checkbox"> <label><input class="goto_checkbox" type="checkbox" value="1" name="goto_ham"> <?=$lang['add']['goto_ham'];?></label> - </div> - </div> - </div> - <div class="form-group"> - <div class="col-sm-offset-2 col-sm-10"> - <div class="checkbox"> - <label><input type="checkbox" value="1" name="active" checked> <?=$lang['add']['active'];?></label> - </div> - </div> - </div> - <div class="form-group"> - <div class="col-sm-offset-2 col-sm-10"> + </div> + </div> + </div> + <div class="form-group"> + <div class="col-sm-offset-2 col-sm-10"> + <div class="checkbox"> + <label><input type="checkbox" value="1" name="active" checked> <?=$lang['add']['active'];?></label> + </div> + </div> + </div> + <div class="form-group"> + <div class="col-sm-offset-2 col-sm-10"> <button class="btn btn-default" data-action="add_item" data-id="add_alias" data-api-url='add/alias' data-api-attr='{}' href="#"><?=$lang['admin']['add'];?></button> - </div> - </div> - </form> + </div> + </div> + </form> </div> </div> </div> @@ -298,37 +312,37 @@ if (!isset($_SESSION['mailcow_cc_role'])) { <h3 class="modal-title"><?=$lang['mailbox']['add_domain_alias'];?></h3> </div> <div class="modal-body"> - <form class="form-horizontal" data-cached-form="true" role="form" data-id="add_alias_domain"> - <input type="hidden" value="0" name="active"> - <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" required></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 data-live-search="true" name="target_domain" title="<?=$lang['add']['select'];?>" required> - <?php + <form class="form-horizontal" data-cached-form="true" role="form" data-id="add_alias_domain"> + <input type="hidden" value="0" name="active"> + <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" required></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 data-live-search="true" name="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" value="1" name="active" checked> <?=$lang['add']['active'];?></label> - </div> - </div> - </div> + 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" value="1" name="active" checked> <?=$lang['add']['active'];?></label> + </div> + </div> + </div> <hr> - <div class="form-group"> - <label class="control-label col-sm-2" for="rl_frame"><?=$lang['acl']['ratelimit'];?></label> + <div class="form-group"> + <label class="control-label col-sm-2" for="rl_frame"><?=$lang['acl']['ratelimit'];?></label> <div class="col-sm-7"> <input name="rl_value" type="number" value="<?=(!empty($rl['value'])) ? $rl['value'] : null;?>" class="form-control" placeholder="disabled"> </div> @@ -340,12 +354,12 @@ if (!isset($_SESSION['mailcow_cc_role'])) { </select> </div> </div> - <div class="form-group"> - <div class="col-sm-offset-2 col-sm-10"> + <div class="form-group"> + <div class="col-sm-offset-2 col-sm-10"> <button class="btn btn-default" data-action="add_item" data-id="add_alias_domain" data-api-url='add/alias-domain' data-api-attr='{}' href="#"><?=$lang['admin']['add'];?></button> - </div> - </div> - </form> + </div> + </div> + </form> </div> </div> </div> @@ -360,9 +374,9 @@ if (!isset($_SESSION['mailcow_cc_role'])) { </div> <div class="modal-body"> <p class="help-block"><?=$lang['add']['syncjob_hint'];?></p> - <form class="form-horizontal" data-cached-form="true" role="form" data-id="add_syncjob"> + <form class="form-horizontal" data-cached-form="true" role="form" data-id="add_syncjob"> <div class="form-group"> - <label class="control-label col-sm-2" for="username"><?=$lang['add']['username'];?>:</label> + <label class="control-label col-sm-2" for="username"><?=$lang['add']['username'];?></label> <div class="col-sm-10"> <select data-live-search="true" name="username" required> <?php @@ -379,149 +393,150 @@ if (!isset($_SESSION['mailcow_cc_role'])) { </select> </div> </div> - <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" required> - </div> - </div> - <div class="form-group"> - <label class="control-label col-sm-2" for="port1"><?=$lang['add']['port'];?></label> - <div class="col-sm-10"> - <input type="number" class="form-control" name="port1" min="1" max="65535" value="143" required> + <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" required> + </div> + </div> + <div class="form-group"> + <label class="control-label col-sm-2" for="port1"><?=$lang['add']['port'];?></label> + <div class="col-sm-10"> + <input type="number" class="form-control" name="port1" min="1" max="65535" value="143" required> <small class="help-block">1-65535</small> - </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" 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="password" class="form-control" name="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" title="<?=$lang['add']['select'];?>" 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" 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="password" class="form-control" name="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" 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"> + </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="1" max="3600" value="20" required> <small class="help-block">1-3600</small> - </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" 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" min="0" max="32000" value="0"> + </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" 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" min="0" max="32000" value="0"> <small class="help-block">0-32000</small> - </div> - </div> - <div class="form-group"> - <label class="control-label col-sm-2" for="maxbytespersecond"><?=$lang['edit']['maxbytespersecond'];?></label> - <div class="col-sm-10"> - <input type="number" class="form-control" name="maxbytespersecond" min="0" max="125000000" value="0"> + </div> + </div> + <div class="form-group"> + <label class="control-label col-sm-2" for="maxbytespersecond"><?=$lang['edit']['maxbytespersecond'];?></label> + <div class="col-sm-10"> + <input type="number" class="form-control" name="maxbytespersecond" min="0" max="125000000" value="0"> <small class="help-block">0-125000000</small> - </div> - </div> - <div class="form-group"> - <label class="control-label col-sm-2" for="timeout1"><?=$lang['edit']['timeout1'];?></label> - <div class="col-sm-10"> - <input type="number" class="form-control" name="timeout1" min="1" max="32000" value="600"> + </div> + </div> + <div class="form-group"> + <label class="control-label col-sm-2" for="timeout1"><?=$lang['edit']['timeout1'];?></label> + <div class="col-sm-10"> + <input type="number" class="form-control" name="timeout1" min="1" max="32000" value="600"> <small class="help-block">1-32000</small> - </div> - </div> + </div> + </div> <div class="form-group"> - <label class="control-label col-sm-2" for="timeout2"><?=$lang['edit']['timeout2'];?></label> - <div class="col-sm-10"> - <input type="number" class="form-control" name="timeout2" min="1" max="32000" value="600"> + <label class="control-label col-sm-2" for="timeout2"><?=$lang['edit']['timeout2'];?></label> + <div class="col-sm-10"> + <input type="number" class="form-control" name="timeout2" min="1" max="32000" value="600"> <small class="help-block">1-32000</small> - </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" value="(?i)spam|(?i)junk"> - </div> - </div> - <div class="form-group"> - <label class="control-label col-sm-2" for="custom_params"><?=$lang['add']['custom_params'];?></label> - <div class="col-sm-10"> - <input type="text" class="form-control" name="custom_params" placeholder="--delete2folders --otheroption"> - </div> - </div> - <div class="form-group"> - <div class="col-sm-offset-2 col-sm-10"> - <div class="checkbox"> - <label><input type="checkbox" value="1" name="delete2duplicates" checked> <?=$lang['add']['delete2duplicates'];?> (--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" value="1" name="delete1"> <?=$lang['add']['delete1'];?> (--delete1)</label> - </div> - </div> - </div> + </div> + </div> <div class="form-group"> - <div class="col-sm-offset-2 col-sm-10"> - <div class="checkbox"> - <label><input type="checkbox" value="1" name="delete2"> <?=$lang['add']['delete2'];?> (--delete2)</label> - </div> - </div> - </div> + <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" 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" value="1" name="automap" checked> <?=$lang['add']['automap'];?> (--automap)</label> - </div> - </div> - </div> + <label class="control-label col-sm-2" for="custom_params"><?=$lang['add']['custom_params'];?></label> + <div class="col-sm-10"> + <input type="text" class="form-control" name="custom_params" placeholder="--dry --some-param=xy --other-param=yx"> + <small class="help-block"><?=$lang['add']['custom_params_hint'];?></small> + </div> + </div> <div class="form-group"> - <div class="col-sm-offset-2 col-sm-10"> - <div class="checkbox"> - <label><input type="checkbox" value="1" name="skipcrossduplicates"> <?=$lang['add']['skipcrossduplicates'];?> (--skipcrossduplicates)</label> - </div> - </div> - </div> + <div class="col-sm-offset-2 col-sm-10"> + <div class="checkbox"> + <label><input type="checkbox" value="1" name="delete2duplicates" checked> <?=$lang['add']['delete2duplicates'];?> (--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" value="1" name="subscribeall" checked> <?=$lang['add']['subscribeall'];?> (--subscribeall)</label> - </div> - </div> - </div> - <div class="form-group"> - <div class="col-sm-offset-2 col-sm-10"> - <div class="checkbox"> - <label><input type="checkbox" value="1" 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="col-sm-offset-2 col-sm-10"> + <div class="checkbox"> + <label><input type="checkbox" value="1" name="delete1"> <?=$lang['add']['delete1'];?> (--delete1)</label> + </div> + </div> + </div> + <div class="form-group"> + <div class="col-sm-offset-2 col-sm-10"> + <div class="checkbox"> + <label><input type="checkbox" value="1" name="delete2"> <?=$lang['add']['delete2'];?> (--delete2)</label> + </div> + </div> + </div> + <div class="form-group"> + <div class="col-sm-offset-2 col-sm-10"> + <div class="checkbox"> + <label><input type="checkbox" value="1" name="automap" checked> <?=$lang['add']['automap'];?> (--automap)</label> + </div> + </div> + </div> + <div class="form-group"> + <div class="col-sm-offset-2 col-sm-10"> + <div class="checkbox"> + <label><input type="checkbox" value="1" name="skipcrossduplicates"> <?=$lang['add']['skipcrossduplicates'];?> (--skipcrossduplicates)</label> + </div> + </div> + </div> + <div class="form-group"> + <div class="col-sm-offset-2 col-sm-10"> + <div class="checkbox"> + <label><input type="checkbox" value="1" name="subscribeall" checked> <?=$lang['add']['subscribeall'];?> (--subscribeall)</label> + </div> + </div> + </div> + <div class="form-group"> + <div class="col-sm-offset-2 col-sm-10"> + <div class="checkbox"> + <label><input type="checkbox" value="1" name="active" checked> <?=$lang['add']['active'];?></label> + </div> + </div> + </div> + <div class="form-group"> + <div class="col-sm-offset-2 col-sm-10"> <button class="btn btn-default" data-action="add_item" data-id="add_syncjob" data-api-url='add/syncjob' data-api-attr='{}' href="#"><?=$lang['admin']['add'];?></button> - </div> - </div> - </form> + </div> + </div> + </form> </div> </div> </div> @@ -535,9 +550,9 @@ if (!isset($_SESSION['mailcow_cc_role'])) { <h3 class="modal-title">Filter</h3> </div> <div class="modal-body"> - <form class="form-horizontal" data-cached-form="true" role="form" data-id="add_filter"> + <form class="form-horizontal" data-cached-form="true" role="form" data-id="add_filter"> <div class="form-group"> - <label class="control-label col-sm-2" for="username"><?=$lang['add']['username'];?>:</label> + <label class="control-label col-sm-2" for="username"><?=$lang['add']['username'];?></label> <div class="col-sm-10"> <select data-live-search="true" name="username" required> <?php @@ -555,7 +570,7 @@ if (!isset($_SESSION['mailcow_cc_role'])) { </div> </div> <div class="form-group"> - <label class="control-label col-sm-2" for="filter_type"><?=$lang['add']['sieve_type'];?>:</label> + <label class="control-label col-sm-2" for="filter_type"><?=$lang['add']['sieve_type'];?></label> <div class="col-sm-10"> <select id="addFilterType" name="filter_type" required> <option value="prefilter">Prefilter</option> @@ -563,33 +578,33 @@ if (!isset($_SESSION['mailcow_cc_role'])) { </select> </div> </div> - <div class="form-group"> - <label class="control-label col-sm-2" for="script_desc"><?=$lang['add']['sieve_desc'];?>:</label> - <div class="col-sm-10"> - <input type="text" class="form-control" name="script_desc" required maxlength="255"> - </div> - </div> - <div class="form-group"> - <label class="control-label col-sm-2" for="script_data">Script:</label> - <div class="col-sm-10"> - <textarea autocorrect="off" spellcheck="false" autocapitalize="none" class="form-control textarea-code" rows="20" id="script_data" name="script_data" required></textarea> - </div> - </div> - <div class="form-group"> - <div class="col-sm-offset-2 col-sm-10"> + <div class="form-group"> + <label class="control-label col-sm-2" for="script_desc"><?=$lang['add']['sieve_desc'];?></label> + <div class="col-sm-10"> + <input type="text" class="form-control" name="script_desc" required maxlength="255"> + </div> + </div> + <div class="form-group"> + <label class="control-label col-sm-2" for="script_data">Script:</label> + <div class="col-sm-10"> + <textarea autocorrect="off" spellcheck="false" autocapitalize="none" class="form-control textarea-code" rows="20" id="script_data" name="script_data" required></textarea> + </div> + </div> + <div class="form-group"> + <div class="col-sm-offset-2 col-sm-10"> <p class="help-block"><?=$lang['add']['activate_filter_warn'];?></p> - <div class="checkbox"> - <label><input type="checkbox" value="1" name="active" checked> <?=$lang['add']['active'];?></label> - </div> - </div> - </div> - <div class="form-group"> - <div class="col-sm-offset-2 col-sm-10" id="add_filter_btns"> + <div class="checkbox"> + <label><input type="checkbox" value="1" name="active" checked> <?=$lang['add']['active'];?></label> + </div> + </div> + </div> + <div class="form-group"> + <div class="col-sm-offset-2 col-sm-10" id="add_filter_btns"> <button class="btn btn-default" id="validate_sieve" href="#"><?=$lang['add']['validate'];?></button> <button class="btn btn-success" id="add_sieve_script" data-action="add_item" data-id="add_filter" data-api-url='add/filter' data-api-attr='{}' href="#" disabled><?=$lang['admin']['add'];?></button> - </div> - </div> - </form> + </div> + </div> + </form> </div> </div> </div> @@ -603,9 +618,9 @@ if (!isset($_SESSION['mailcow_cc_role'])) { <h3 class="modal-title"><?=$lang['mailbox']['bcc_maps'];?></h3> </div> <div class="modal-body"> - <form class="form-horizontal" data-cached-form="true" role="form" data-id="add_bcc"> + <form class="form-horizontal" data-cached-form="true" role="form" data-id="add_bcc"> <div class="form-group"> - <label class="control-label col-sm-2" for="local_dest"><?=$lang['mailbox']['bcc_local_dest'];?>:</label> + <label class="control-label col-sm-2" for="local_dest"><?=$lang['mailbox']['bcc_local_dest'];?></label> <div class="col-sm-10"> <select data-live-search="true" name="local_dest" required> <?php @@ -634,7 +649,7 @@ if (!isset($_SESSION['mailcow_cc_role'])) { </div> </div> <div class="form-group"> - <label class="control-label col-sm-2" for="type"><?=$lang['mailbox']['bcc_map_type'];?>:</label> + <label class="control-label col-sm-2" for="type"><?=$lang['mailbox']['bcc_map_type'];?></label> <div class="col-sm-10"> <select name="type" required> <option value="sender"><?=$lang['mailbox']['bcc_sender_map'];?></option> @@ -642,25 +657,25 @@ if (!isset($_SESSION['mailcow_cc_role'])) { </select> </div> </div> - <div class="form-group"> - <label class="control-label col-sm-2" for="bcc_dest"><?=$lang['mailbox']['bcc_destination'];?>:</label> - <div class="col-sm-10"> + <div class="form-group"> + <label class="control-label col-sm-2" for="bcc_dest"><?=$lang['mailbox']['bcc_destination'];?></label> + <div class="col-sm-10"> <input type="text" class="form-control" name="bcc_dest"> - </div> - </div> - <div class="form-group"> - <div class="col-sm-offset-2 col-sm-10"> - <div class="checkbox"> - <label><input type="checkbox" value="1" name="active" checked> <?=$lang['add']['active'];?></label> - </div> - </div> - </div> - <div class="form-group"> - <div class="col-sm-offset-2 col-sm-10"> + </div> + </div> + <div class="form-group"> + <div class="col-sm-offset-2 col-sm-10"> + <div class="checkbox"> + <label><input type="checkbox" value="1" name="active" checked> <?=$lang['add']['active'];?></label> + </div> + </div> + </div> + <div class="form-group"> + <div class="col-sm-offset-2 col-sm-10"> <button class="btn btn-success" data-action="add_item" data-id="add_bcc" data-api-url='add/bcc' data-api-attr='{}' href="#"><?=$lang['admin']['add'];?></button> - </div> - </div> - </form> + </div> + </div> + </form> </div> </div> </div> @@ -674,7 +689,7 @@ if (!isset($_SESSION['mailcow_cc_role'])) { <h3 class="modal-title"><?=$lang['mailbox']['recipient_maps'];?></h3> </div> <div class="modal-body"> - <form class="form-horizontal" data-cached-form="true" role="form" data-id="add_recipient_map"> + <form class="form-horizontal" data-cached-form="true" role="form" data-id="add_recipient_map"> <div class="form-group"> <label class="control-label col-sm-2" for="recipient_map_old"><?=$lang['mailbox']['recipient_map_old'];?></label> <div class="col-sm-10"> @@ -689,19 +704,19 @@ if (!isset($_SESSION['mailcow_cc_role'])) { <small><?=$lang['mailbox']['recipient_map_new_info'];?></small> </div> </div> - <div class="form-group"> - <div class="col-sm-offset-2 col-sm-10"> - <div class="checkbox"> - <label><input type="checkbox" value="1" 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="form-group"> + <div class="col-sm-offset-2 col-sm-10"> + <div class="checkbox"> + <label><input type="checkbox" value="1" name="active" checked> <?=$lang['add']['active'];?></label> + </div> + </div> + </div> + <div class="form-group"> + <div class="col-sm-offset-2 col-sm-10"> <button class="btn btn-success" data-action="add_item" data-id="add_recipient_map" data-api-url='add/recipient_map' data-api-attr='{}' href="#"><?=$lang['admin']['add'];?></button> - </div> - </div> - </form> + </div> + </div> + </form> </div> </div> </div> @@ -715,7 +730,7 @@ if (!isset($_SESSION['mailcow_cc_role'])) { <h3 class="modal-title"><?=$lang['mailbox']['tls_policy_maps'];?></h3> </div> <div class="modal-body"> - <form class="form-horizontal" data-cached-form="true" role="form" data-id="add_tls_policy_map"> + <form class="form-horizontal" data-cached-form="true" role="form" data-id="add_tls_policy_map"> <div class="form-group"> <label class="control-label col-sm-2" for="dest"><?=$lang['mailbox']['tls_map_dest'];?></label> <div class="col-sm-10"> @@ -724,7 +739,7 @@ if (!isset($_SESSION['mailcow_cc_role'])) { </div> </div> <div class="form-group"> - <label class="control-label col-sm-2" for="policy"><?=$lang['mailbox']['tls_map_policy'];?>:</label> + <label class="control-label col-sm-2" for="policy"><?=$lang['mailbox']['tls_map_policy'];?></label> <div class="col-sm-10"> <select class="full-width-select" name="policy" required> <option value="none">none</option> @@ -745,19 +760,19 @@ if (!isset($_SESSION['mailcow_cc_role'])) { <small><?=$lang['mailbox']['tls_map_parameters_info'];?></small> </div> </div> - <div class="form-group"> - <div class="col-sm-offset-2 col-sm-10"> - <div class="checkbox"> - <label><input type="checkbox" value="1" 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="form-group"> + <div class="col-sm-offset-2 col-sm-10"> + <div class="checkbox"> + <label><input type="checkbox" value="1" name="active" checked> <?=$lang['add']['active'];?></label> + </div> + </div> + </div> + <div class="form-group"> + <div class="col-sm-offset-2 col-sm-10"> <button class="btn btn-success" data-action="add_item" data-id="add_tls_policy_map" data-api-url='add/tls-policy-map' data-api-attr='{}' href="#"><?=$lang['admin']['add'];?></button> - </div> - </div> - </form> + </div> + </div> + </form> </div> </div> </div> @@ -785,24 +800,3 @@ if (!isset($_SESSION['mailcow_cc_role'])) { </div> </div> </div><!-- DNS info modal --> -<script> -$('#addResourceModal').on('shown.bs.modal', function() { - $("#multiple_bookings").val($("#multiple_bookings_select").val()); - if ($("#multiple_bookings").val() == "custom") { - $("#multiple_bookings_custom_div").show(); - $("#multiple_bookings").val($("#multiple_bookings_custom").val()); - } -}) -$("#multiple_bookings_select").change(function() { - $("#multiple_bookings").val($("#multiple_bookings_select").val()); - if ($("#multiple_bookings").val() == "custom") { - $("#multiple_bookings_custom_div").show(); - } - else { - $("#multiple_bookings_custom_div").hide(); - } -}); -$("#multiple_bookings_custom").bind ("change keypress keyup blur", function () { - $("#multiple_bookings").val($("#multiple_bookings_custom").val()); -}); -</script> diff --git a/data/web/modals/quarantine.php b/data/web/modals/quarantine.php index e1929927..0d091163 100644 --- a/data/web/modals/quarantine.php +++ b/data/web/modals/quarantine.php @@ -17,6 +17,10 @@ if (!isset($_SESSION['mailcow_cc_role'])) { <label for="qid_detail_subj"><h4><?=$lang['quarantine']['subj'];?>:</h4></label> <p id="qid_detail_subj"></p> </div> + <div class="form-group"> + <label for="qid_detail_recipients"><h4><?=$lang['quarantine']['recipients'];?>:</h4></label> + <p id="qid_detail_recipients"></p> + </div> <div class="form-group"> <label for="qid_detail_text"><h4><?=$lang['quarantine']['text_plain_content'];?>:</h4></label> <pre id="qid_detail_text"></pre> diff --git a/data/web/modals/user.php b/data/web/modals/user.php index c45283d2..b89b8ac0 100644 --- a/data/web/modals/user.php +++ b/data/web/modals/user.php @@ -178,7 +178,7 @@ if (!isset($_SESSION['mailcow_cc_role'])) { <div class="modal-dialog" role="document"> <div class="modal-content"> <div class="modal-body"> - <form class="form-horizontal" data-cached-form="true" data-id="pwchange" role="form" method="post" autocomplete="off"> + <form class="form-horizontal" data-cached-form="false" data-id="pwchange" role="form" method="post" autocomplete="off"> <div class="form-group"> <label class="control-label col-sm-3" for="user_new_pass"><?=$lang['user']['new_password'];?></label> <div class="col-sm-5"> diff --git a/data/web/sogo-auth.php b/data/web/sogo-auth.php new file mode 100644 index 00000000..08fb1b0b --- /dev/null +++ b/data/web/sogo-auth.php @@ -0,0 +1,86 @@ +<?php + +$ALLOW_ADMIN_EMAIL_LOGIN = (preg_match( + "/^([yY][eE][sS]|[yY])+$/", + $_ENV["ALLOW_ADMIN_EMAIL_LOGIN"] +)); + +$session_var_user_allowed = 'sogo-sso-user-allowed'; +$session_var_pass = 'sogo-sso-pass'; + +// prevent if feature is disabled +if (!$ALLOW_ADMIN_EMAIL_LOGIN) { + header('HTTP/1.0 403 Forbidden'); + echo "this feature is disabled"; + exit; +} +// validate credentials for basic auth requests +elseif (isset($_SERVER['PHP_AUTH_USER'])) { + // load prerequisites only when required + require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php'; + $username = $_SERVER['PHP_AUTH_USER']; + $password = $_SERVER['PHP_AUTH_PW']; + $login_check = check_login($username, $password); + if ($login_check === 'user') { + header("X-User: $username"); + header("X-Auth: Basic ".base64_encode("$username:$password")); + header("X-Auth-Type: Basic"); + exit; + } else { + header('HTTP/1.0 401 Unauthorized'); + echo 'Invalid login'; + exit; + } +} +// check permissions and redirect for direct GET ?login=xy requests +elseif (isset($_GET['login'])) { + // load prerequisites only when required + require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php'; + // check permissions + if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['acl']['login_as'] == "1") { + $login = html_entity_decode(rawurldecode($_GET["login"])); + if (filter_var($login, FILTER_VALIDATE_EMAIL)) { + if (!empty(mailbox('get', 'mailbox_details', $login))) { + // load master password + $sogo_sso_pass = file_get_contents("/etc/sogo-sso/sogo-sso.pass"); + // register username and password in session + $_SESSION[$session_var_user_allowed][] = $login; + $_SESSION[$session_var_pass] = $sogo_sso_pass; + // redirect to sogo (sogo will get the correct credentials via nginx auth_request + header("Location: /SOGo/so/${login}"); + exit; + } + } + } + header('HTTP/1.0 403 Forbidden'); + exit; +} +// only check for admin-login on sogo GUI requests +elseif ( + strcasecmp(substr($_SERVER['HTTP_X_ORIGINAL_URI'], 0, 9), "/SOGo/so/") === 0 +) { + // this is an nginx auth_request call, we check for existing sogo-sso session variables + session_start(); + // extract email address from "/SOGo/so/user@domain/xy" + $url_parts = explode("/", $_SERVER['HTTP_X_ORIGINAL_URI']); + $email = $url_parts[3]; + // check if this email is in session allowed list + if ( + !empty($email) && + filter_var($email, FILTER_VALIDATE_EMAIL) && + is_array($_SESSION[$session_var_user_allowed]) && + in_array($email, $_SESSION[$session_var_user_allowed]) + ) { + $username = $email; + $password = $_SESSION[$session_var_pass]; + header("X-User: $username"); + header("X-Auth: Basic ".base64_encode("$username:$password")); + header("X-Auth-Type: Basic"); + exit; + } +} + +// if username is empty, SOGo will use the normal login methods / login form +header("X-User: "); +header("X-Auth: "); +header("X-Auth-Type: "); diff --git a/data/web/user.php b/data/web/user.php index b19fbb00..abd8b318 100644 --- a/data/web/user.php +++ b/data/web/user.php @@ -77,7 +77,7 @@ elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == ' $username = $_SESSION['mailcow_cc_username']; $mailboxdata = mailbox('get', 'mailbox_details', $username); - $clientconfigstr = "host=" . urlencode($mailcow_hostname) . "&email=" . urlencode($username) . "&name=" . urlencode($mailboxdata['name']) . "&ui=" . urlencode($_SERVER['HTTP_HOST']) . "&port=" . urlencode($autodiscover_config['caldav']['port']); + $clientconfigstr = "host=" . urlencode($mailcow_hostname) . "&email=" . urlencode($username) . "&name=" . urlencode($mailboxdata['name']) . "&ui=" . urlencode(strtok($_SERVER['HTTP_HOST'], ':')) . "&port=" . urlencode($autodiscover_config['caldav']['port']); if ($autodiscover_config['useEASforOutlook'] == 'yes') $clientconfigstr .= "&outlookEAS=1"; if (file_exists('thunderbird-plugins/version.csv')) { @@ -199,7 +199,7 @@ elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == ' <?=$mailboxdata['percent_in_use'];?>% </div> </div> - <p><?=formatBytes($mailboxdata['quota_used'], 2);?> / <?=formatBytes($mailboxdata['quota'], 2);?>, <?=$mailboxdata['messages'];?> <?=$lang['user']['messages'];?></p> + <p><?=formatBytes($mailboxdata['quota_used'], 2);?> / <?=($mailboxdata['quota'] == 0) ? '∞' : formatBytes($mailboxdata['quota'], 2);?><br><?=$mailboxdata['messages'];?> <?=$lang['user']['messages'];?></p> </div> </div> <hr> diff --git a/docker-compose.yml b/docker-compose.yml index f2776bd7..0563cbdb 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,7 +2,7 @@ version: '2.1' services: unbound-mailcow: - image: mailcow/unbound:1.6 + image: mailcow/unbound:1.8 build: ./data/Dockerfiles/unbound command: /usr/sbin/unbound environment: @@ -30,8 +30,6 @@ services: - MYSQL_USER=${DBUSER} - MYSQL_PASSWORD=${DBPASS} restart: always - dns: - - ${IPV4_NETWORK:-172.22.1}.254 ports: - "${SQL_PORT:-127.0.0.1:13306}:3306" networks: @@ -46,8 +44,6 @@ services: restart: always environment: - TZ=${TZ} - dns: - - ${IPV4_NETWORK:-172.22.1}.254 networks: mailcow-network: ipv4_address: ${IPV4_NETWORK:-172.22.1}.249 @@ -55,34 +51,38 @@ services: - redis clamd-mailcow: - image: mailcow/clamd:1.22 + image: mailcow/clamd:1.28 build: ./data/Dockerfiles/clamd restart: always + dns: + - ${IPV4_NETWORK:-172.22.1}.254 environment: - TZ=${TZ} - SKIP_CLAMD=${SKIP_CLAMD:-n} volumes: - ./data/conf/clamav/:/etc/clamav/ - dns: - - ${IPV4_NETWORK:-172.22.1}.254 networks: mailcow-network: aliases: - clamd rspamd-mailcow: - image: mailcow/rspamd:1.34 + image: mailcow/rspamd:1.46 build: ./data/Dockerfiles/rspamd stop_grace_period: 30s depends_on: - nginx-mailcow + - dovecot-mailcow environment: - TZ=${TZ} volumes: - ./data/conf/rspamd/custom/:/etc/rspamd/custom - ./data/conf/rspamd/override.d/:/etc/rspamd/override.d - ./data/conf/rspamd/local.d/:/etc/rspamd/local.d + - ./data/conf/rspamd/plugins.d/:/etc/rspamd/plugins.d - ./data/conf/rspamd/lua/:/etc/rspamd/lua/:ro + - ./data/conf/rspamd/rspamd.conf.local:/etc/rspamd/rspamd.conf.local + - ./data/conf/rspamd/rspamd.conf.override:/etc/rspamd/rspamd.conf.override - rspamd-vol-1:/var/lib/rspamd restart: always dns: @@ -94,7 +94,7 @@ services: - rspamd php-fpm-mailcow: - image: mailcow/phpfpm:1.34 + image: mailcow/phpfpm:1.44 build: ./data/Dockerfiles/phpfpm command: "php-fpm -d date.timezone=${TZ} -d expose_php=0" depends_on: @@ -106,11 +106,14 @@ services: - mysql-socket-vol-1:/var/run/mysqld/ - ./data/conf/sogo/:/etc/sogo/ - ./data/conf/rspamd/meta_exporter:/meta_exporter:ro + - ./data/conf/phpfpm/sogo-sso/:/etc/sogo-sso/ - ./data/conf/phpfpm/php-fpm.d/pools.conf:/usr/local/etc/php-fpm.d/z-pools.conf - ./data/conf/phpfpm/php-conf.d/opcache-recommended.ini:/usr/local/etc/php/conf.d/opcache-recommended.ini - ./data/conf/phpfpm/php-conf.d/upload.ini:/usr/local/etc/php/conf.d/upload.ini - ./data/conf/phpfpm/php-conf.d/other.ini:/usr/local/etc/php/conf.d/zzz-other.ini - ./data/assets/templates:/tpls + dns: + - ${IPV4_NETWORK:-172.22.1}.254 environment: - LOG_LINES=${LOG_LINES:-9999} - TZ=${TZ} @@ -130,16 +133,15 @@ services: - API_ALLOW_FROM=${API_ALLOW_FROM:-invalid} - COMPOSE_PROJECT_NAME=${COMPOSE_PROJECT_NAME:-mailcow-dockerized} - SKIP_SOLR=${SKIP_SOLR:-y} + - ALLOW_ADMIN_EMAIL_LOGIN=${ALLOW_ADMIN_EMAIL_LOGIN:-n} restart: always - dns: - - ${IPV4_NETWORK:-172.22.1}.254 networks: mailcow-network: aliases: - phpfpm sogo-mailcow: - image: mailcow/sogo:1.52 + image: mailcow/sogo:1.61 build: ./data/Dockerfiles/sogo environment: - DBNAME=${DBNAME} @@ -149,14 +151,18 @@ services: - LOG_LINES=${LOG_LINES:-9999} - MAILCOW_HOSTNAME=${MAILCOW_HOSTNAME} - ACL_ANYONE=${ACL_ANYONE:-disallow} + - ALLOW_ADMIN_EMAIL_LOGIN=${ALLOW_ADMIN_EMAIL_LOGIN:-n} + - IPV4_NETWORK=${IPV4_NETWORK:-172.22.1} + - SOGO_EXPIRE_SESSION=${SOGO_EXPIRE_SESSION:-480} + dns: + - ${IPV4_NETWORK:-172.22.1}.254 volumes: - ./data/conf/sogo/:/etc/sogo/ - ./data/web/inc/init_db.inc.php:/init_db.inc.php - ./data/conf/sogo/custom-sogo.js:/usr/lib/GNUstep/SOGo/WebServerResources/js/custom-sogo.js - mysql-socket-vol-1:/var/run/mysqld/ + - sogo-web-vol-1:/sogo_web restart: always - dns: - - ${IPV4_NETWORK:-172.22.1}.254 networks: mailcow-network: ipv4_address: ${IPV4_NETWORK:-172.22.1}.248 @@ -164,14 +170,17 @@ services: - sogo dovecot-mailcow: - image: mailcow/dovecot:1.63 + image: mailcow/dovecot:1.88 build: ./data/Dockerfiles/dovecot + dns: + - ${IPV4_NETWORK:-172.22.1}.254 cap_add: - NET_BIND_SERVICE volumes: - - ./data/conf/dovecot:/usr/local/etc/dovecot + - ./data/conf/dovecot:/etc/dovecot - ./data/assets/ssl:/etc/ssl/mail/:ro - ./data/conf/sogo/:/etc/sogo/ + - ./data/conf/phpfpm/sogo-sso/:/etc/phpfpm/ - vmail-vol-1:/var/vmail - vmail-attachments-vol-1:/var/attachments - crypt-vol-1:/mail_crypt/ @@ -185,9 +194,13 @@ services: - DBUSER=${DBUSER} - DBPASS=${DBPASS} - TZ=${TZ} + - MAILCOW_HOSTNAME=${MAILCOW_HOSTNAME} + - IPV4_NETWORK=${IPV4_NETWORK:-172.22.1} + - ALLOW_ADMIN_EMAIL_LOGIN=${ALLOW_ADMIN_EMAIL_LOGIN:-n} - MAILDIR_GC_TIME=${MAILDIR_GC_TIME:-1440} - ACL_ANYONE=${ACL_ANYONE:-disallow} - SKIP_SOLR=${SKIP_SOLR:-y} + - MAILDIR_SUB=${MAILDIR_SUB:-} ports: - "${DOVEADM_PORT:-127.0.0.1:19991}:12345" - "${IMAP_PORT:-143}:143" @@ -202,16 +215,15 @@ services: nofile: soft: 20000 hard: 40000 - dns: - - ${IPV4_NETWORK:-172.22.1}.254 hostname: ${MAILCOW_HOSTNAME} networks: mailcow-network: + ipv4_address: ${IPV4_NETWORK:-172.22.1}.250 aliases: - dovecot postfix-mailcow: - image: mailcow/postfix:1.29 + image: mailcow/postfix:1.39 build: ./data/Dockerfiles/postfix volumes: - ./data/conf/postfix:/opt/postfix/conf @@ -244,8 +256,8 @@ services: memcached-mailcow: image: memcached:alpine restart: always - dns: - - ${IPV4_NETWORK:-172.22.1}.254 + environment: + - TZ=${TZ} networks: mailcow-network: aliases: @@ -257,11 +269,14 @@ services: - php-fpm-mailcow - redis-mailcow image: nginx:mainline-alpine + dns: + - ${IPV4_NETWORK:-172.22.1}.254 command: /bin/sh -c "envsubst < /etc/nginx/conf.d/templates/listen_plain.template > /etc/nginx/conf.d/listen_plain.active && envsubst < /etc/nginx/conf.d/templates/listen_ssl.template > /etc/nginx/conf.d/listen_ssl.active && envsubst < /etc/nginx/conf.d/templates/server_name.template > /etc/nginx/conf.d/server_name.active && envsubst < /etc/nginx/conf.d/templates/sogo.template > /etc/nginx/conf.d/sogo.active && envsubst < /etc/nginx/conf.d/templates/sogo_eas.template > /etc/nginx/conf.d/sogo_eas.active && + . /etc/nginx/conf.d/templates/sogo.auth_request.template.sh > /etc/nginx/conf.d/sogo_proxy_auth.active && nginx -qt && until ping phpfpm -c1 > /dev/null; do sleep 1; done && until ping sogo -c1 > /dev/null; do sleep 1; done && @@ -274,20 +289,18 @@ services: - MAILCOW_HOSTNAME=${MAILCOW_HOSTNAME} - IPV4_NETWORK=${IPV4_NETWORK:-172.22.1} - TZ=${TZ} + - ALLOW_ADMIN_EMAIL_LOGIN=${ALLOW_ADMIN_EMAIL_LOGIN:-n} volumes: - ./data/web:/web:ro - ./data/conf/rspamd/dynmaps:/dynmaps:ro - ./data/assets/ssl/:/etc/ssl/mail/:ro - ./data/conf/nginx/:/etc/nginx/conf.d/:rw - ./data/conf/rspamd/meta_exporter:/meta_exporter:ro - volumes_from: - - sogo-mailcow + - sogo-web-vol-1:/usr/lib/GNUstep/SOGo/ ports: - "${HTTPS_BIND:-0.0.0.0}:${HTTPS_PORT:-443}:${HTTPS_PORT:-443}" - "${HTTP_BIND:-0.0.0.0}:${HTTP_PORT:-80}:${HTTP_PORT:-80}" restart: always - dns: - - ${IPV4_NETWORK:-172.22.1}.254 networks: mailcow-network: aliases: @@ -296,7 +309,7 @@ services: acme-mailcow: depends_on: - nginx-mailcow - image: mailcow/acme:1.48 + image: mailcow/acme:1.61 build: ./data/Dockerfiles/acme dns: - ${IPV4_NETWORK:-172.22.1}.254 @@ -309,6 +322,8 @@ services: - DBPASS=${DBPASS} - SKIP_LETS_ENCRYPT=${SKIP_LETS_ENCRYPT:-n} - SKIP_IP_CHECK=${SKIP_IP_CHECK:-n} + - SKIP_HTTP_VERIFICATION=${SKIP_HTTP_VERIFICATION:-n} + - ONLY_MAILCOW_HOSTNAME=${ONLY_MAILCOW_HOSTNAME:-n} - LE_STAGING=${LE_STAGING:-n} - TZ=${TZ} volumes: @@ -323,7 +338,7 @@ services: - acme netfilter-mailcow: - image: mailcow/netfilter:1.22 + image: mailcow/netfilter:1.28 build: ./data/Dockerfiles/netfilter stop_grace_period: 30s depends_on: @@ -341,20 +356,20 @@ services: - SNAT_TO_SOURCE=${SNAT_TO_SOURCE:-n} - SNAT6_TO_SOURCE=${SNAT6_TO_SOURCE:-n} network_mode: "host" - dns: - - ${IPV4_NETWORK:-172.22.1}.254 volumes: - /lib/modules:/lib/modules:ro watchdog-mailcow: - image: mailcow/watchdog:1.35 + image: mailcow/watchdog:1.58 # Debug #command: /watchdog.sh build: ./data/Dockerfiles/watchdog - oom_kill_disable: true + dns: + - ${IPV4_NETWORK:-172.22.1}.254 volumes: - rspamd-vol-1:/var/lib/rspamd - mysql-socket-vol-1:/var/run/mysqld/ + - ./data/assets/ssl:/etc/ssl/mail/:ro restart: always environment: - LOG_LINES=${LOG_LINES:-9999} @@ -364,6 +379,7 @@ services: - DBPASS=${DBPASS} - USE_WATCHDOG=${USE_WATCHDOG:-n} - WATCHDOG_NOTIFY_EMAIL=${WATCHDOG_NOTIFY_EMAIL} + - WATCHDOG_NOTIFY_BAN=${WATCHDOG_NOTIFY_BAN:-y} - MAILCOW_HOSTNAME=${MAILCOW_HOSTNAME} - IPV4_NETWORK=${IPV4_NETWORK:-172.22.1} - IP_BY_DOCKER_API=${IP_BY_DOCKER_API:-0} @@ -371,37 +387,34 @@ services: - SKIP_CLAMD=${SKIP_CLAMD:-n} - SKIP_LETS_ENCRYPT=${SKIP_LETS_ENCRYPT:-n} - HTTPS_PORT=${HTTPS_PORT:-443} - dns: - - ${IPV4_NETWORK:-172.22.1}.254 networks: mailcow-network: aliases: - watchdog dockerapi-mailcow: - image: mailcow/dockerapi:1.26 + image: mailcow/dockerapi:1.32 restart: always build: ./data/Dockerfiles/dockerapi oom_kill_disable: true + dns: + - ${IPV4_NETWORK:-172.22.1}.254 environment: - DBROOT=${DBROOT} - TZ=${TZ} volumes: - /var/run/docker.sock:/var/run/docker.sock:ro - - ./data/conf/rspamd/override.d/worker-controller-password.inc:/access.inc:rw networks: mailcow-network: aliases: - dockerapi solr-mailcow: - image: mailcow/solr:1.2 + image: mailcow/solr:1.6 build: ./data/Dockerfiles/solr restart: always volumes: - - solr-vol-1:/opt/solr/server/solr/dovecot/data - dns: - - ${IPV4_NETWORK:-172.22.1}.254 + - solr-vol-1:/opt/solr/server/solr/dovecot-fts/data environment: - TZ=${TZ} - SOLR_HEAP=${SOLR_HEAP:-1024} @@ -411,6 +424,25 @@ services: aliases: - solr + olefy-mailcow: + image: mailcow/olefy:1.1 + restart: always + build: ./data/Dockerfiles/olefy + environment: + - TZ=${TZ} + - OLEFY_BINDADDRESS=0.0.0.0 + - OLEFY_BINDPORT=10055 + - OLEFY_TMPDIR=/tmp + - OLEFY_PYTHON_PATH=/usr/bin/python3 + - OLEFY_OLEVBA_PATH=/usr/bin/olevba3 + - OLEFY_LOGLVL=20 + - OLEFY_MINLENGTH=500 + - OLEFY_DEL_TMP=1 + networks: + mailcow-network: + aliases: + - olefy + ipv6nat-mailcow: depends_on: - unbound-mailcow @@ -429,6 +461,8 @@ services: - watchdog-mailcow - dockerapi-mailcow - solr-mailcow + environment: + - TZ=${TZ} image: robbertkl/ipv6nat restart: always privileged: true @@ -440,6 +474,8 @@ services: networks: mailcow-network: driver: bridge + driver_opts: + com.docker.network.bridge.name: br-mailcow enable_ipv6: true ipam: driver: default @@ -459,3 +495,4 @@ volumes: solr-vol-1: postfix-vol-1: crypt-vol-1: + sogo-web-vol-1: diff --git a/generate_config.sh b/generate_config.sh index a882ec08..5948313d 100755 --- a/generate_config.sh +++ b/generate_config.sh @@ -2,6 +2,12 @@ set -o pipefail +if [[ "$(uname -r)" =~ ^4\.15\.0-60 ]]; then + echo "DO NOT RUN mailcow ON THIS UBUNTU KERNEL!"; + echo "Please update to 5.x or use another distribution." + exit 1 +fi + if grep --help 2>&1 | grep -q -i "busybox"; then echo "BusybBox grep detected, please install gnu grep, \"apk add --no-cache --upgrade grep\"" exit 1 @@ -16,6 +22,7 @@ if [ -f mailcow.conf ]; then case $response in [yY][eE][sS]|[yY]) mv mailcow.conf mailcow.conf_backup + chmod 600 mailcow.conf_backup ;; *) exit 1 @@ -25,7 +32,7 @@ fi echo "Press enter to confirm the detected value '[value]' where applicable or enter a custom value." while [ -z "${MAILCOW_HOSTNAME}" ]; do - read -p "Hostname (FQDN): " -e MAILCOW_HOSTNAME + read -p "Mail server hostname (FQDN) - this is not your mail domain, but your mail servers hostname: " -e MAILCOW_HOSTNAME DOTS=${MAILCOW_HOSTNAME//[^.]}; if [ ${#DOTS} -lt 2 ] && [ ! -z ${MAILCOW_HOSTNAME} ]; then echo "${MAILCOW_HOSTNAME} is not a FQDN" @@ -115,6 +122,8 @@ DBROOT=$(LC_ALL=C </dev/urandom tr -dc A-Za-z0-9 | head -c 28) # ------------------------------ # You should use HTTPS, but in case of SSL offloaded reverse proxies: +# Might be important: This will also change the binding within the container. +# If you use a proxy within Docker, point it to the ports you set below. HTTP_PORT=80 HTTP_BIND=0.0.0.0 @@ -185,27 +194,40 @@ SKIP_LETS_ENCRYPT=n SKIP_IP_CHECK=n +# Skip HTTP verification in ACME container - y/n + +SKIP_HTTP_VERIFICATION=n + # Skip ClamAV (clamd-mailcow) anti-virus (Rspamd will auto-detect a missing ClamAV container) - y/n SKIP_CLAMD=${SKIP_CLAMD} # Skip Solr on low-memory systems or if you do not want to store a readable index of your mails in solr-vol-1. + SKIP_SOLR=${SKIP_SOLR} # Solr heap size in MB, there is no recommendation, please see Solr docs. # Solr is a prone to run OOM and should be monitored. Unmonitored Solr setups are not recommended. + SOLR_HEAP=1024 # Enable watchdog (watchdog-mailcow) to restart unhealthy containers (experimental) USE_WATCHDOG=n +# Allow admins to log into SOGo as email user (without any password) + +ALLOW_ADMIN_EMAIL_LOGIN=n + # Send notifications by mail (no DKIM signature, sent from watchdog@MAILCOW_HOSTNAME) # Can by multiple rcpts, NO quotation marks #WATCHDOG_NOTIFY_EMAIL=a@example.com,b@example.com,c@example.com #WATCHDOG_NOTIFY_EMAIL= +# Notify about banned IP (includes whois lookup) +WATCHDOG_NOTIFY_BAN=y + # Max log lines per service to keep in Redis logs LOG_LINES=9999 @@ -226,16 +248,24 @@ IPV6_NETWORK=fd4d:6169:6c63:6f77::/64 #SNAT6_TO_SOURCE= -# Create or override API key for web uI +# Create or override API key for web ui # You _must_ define API_ALLOW_FROM, which is a comma separated list of IPs # API_KEY allowed chars: a-z, A-Z, 0-9, - #API_KEY= -#API_ALLOW_FROM=127.0.0.1,1.2.3.4 +#API_ALLOW_FROM=172.22.1.1,127.0.0.1 + +# mail_home is ~/Maildir +MAILDIR_SUB=Maildir + +# SOGo session timeout in minutes +SOGO_EXPIRE_SESSION=480 EOF mkdir -p data/assets/ssl +chmod 600 mailcow.conf + # copy but don't overwrite existing certificate cp -n data/assets/ssl-example/*.pem data/assets/ssl/ diff --git a/helper-scripts/backup_and_restore.sh b/helper-scripts/backup_and_restore.sh index 704997c7..5054d6ee 100755 --- a/helper-scripts/backup_and_restore.sh +++ b/helper-scripts/backup_and_restore.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash if [[ ! -z ${MAILCOW_BACKUP_LOCATION} ]]; then BACKUP_LOCATION="${MAILCOW_BACKUP_LOCATION}" @@ -64,32 +64,32 @@ function backup() { docker run --rm \ -v ${BACKUP_LOCATION}/mailcow-${DATE}:/backup \ -v $(docker volume ls -qf name=${CMPS_PRJ}_vmail-vol-1):/vmail:ro \ - debian:stretch-slim /bin/tar --warning='no-file-ignored' --use-compress-program="gzip --rsyncable -v --best" -Pcvpf /backup/backup_vmail.tar.gz /vmail + debian:stretch-slim /bin/tar --warning='no-file-ignored' --use-compress-program="gzip --rsyncable --best" -Pcvpf /backup/backup_vmail.tar.gz /vmail ;;& crypt|all) docker run --rm \ -v ${BACKUP_LOCATION}/mailcow-${DATE}:/backup \ -v $(docker volume ls -qf name=${CMPS_PRJ}_crypt-vol-1):/crypt:ro \ - debian:stretch-slim /bin/tar --warning='no-file-ignored' --use-compress-program="gzip --rsyncable -v --best" -Pcvpf /backup/backup_crypt.tar.gz /crypt + debian:stretch-slim /bin/tar --warning='no-file-ignored' --use-compress-program="gzip --rsyncable --best" -Pcvpf /backup/backup_crypt.tar.gz /crypt ;;& redis|all) docker exec $(docker ps -qf name=redis-mailcow) redis-cli save docker run --rm \ -v ${BACKUP_LOCATION}/mailcow-${DATE}:/backup \ -v $(docker volume ls -qf name=${CMPS_PRJ}_redis-vol-1):/redis:ro \ - debian:stretch-slim /bin/tar --warning='no-file-ignored' --use-compress-program="gzip --rsyncable -v --best" -Pcvpf /backup/backup_redis.tar.gz /redis + debian:stretch-slim /bin/tar --warning='no-file-ignored' --use-compress-program="gzip --rsyncable --best" -Pcvpf /backup/backup_redis.tar.gz /redis ;;& rspamd|all) docker run --rm \ -v ${BACKUP_LOCATION}/mailcow-${DATE}:/backup \ -v $(docker volume ls -qf name=${CMPS_PRJ}_rspamd-vol-1):/rspamd:ro \ - debian:stretch-slim /bin/tar --warning='no-file-ignored' --use-compress-program="gzip --rsyncable -v --best" -Pcvpf /backup/backup_rspamd.tar.gz /rspamd + debian:stretch-slim /bin/tar --warning='no-file-ignored' --use-compress-program="gzip --rsyncable --best" -Pcvpf /backup/backup_rspamd.tar.gz /rspamd ;;& postfix|all) docker run --rm \ -v ${BACKUP_LOCATION}/mailcow-${DATE}:/backup \ -v $(docker volume ls -qf name=${CMPS_PRJ}_postfix-vol-1):/postfix:ro \ - debian:stretch-slim /bin/tar --warning='no-file-ignored' --use-compress-program="gzip --rsyncable -v --best" -Pcvpf /backup/backup_postfix.tar.gz /postfix + debian:stretch-slim /bin/tar --warning='no-file-ignored' --use-compress-program="gzip --rsyncable --best" -Pcvpf /backup/backup_postfix.tar.gz /postfix ;;& mysql|all) SQLIMAGE=$(grep -iEo '(mysql|mariadb)\:.+' ${COMPOSE_FILE}) diff --git a/helper-scripts/check_translations.rb b/helper-scripts/check_translations.rb index 7568a975..af4da1c2 100755 --- a/helper-scripts/check_translations.rb +++ b/helper-scripts/check_translations.rb @@ -1,4 +1,4 @@ -#!/usr/bin/ruby +#!/usr/bin/env ruby MASTER="en" diff --git a/helper-scripts/mailcow-reset-admin.sh b/helper-scripts/mailcow-reset-admin.sh index 74179798..4afd14c9 100755 --- a/helper-scripts/mailcow-reset-admin.sh +++ b/helper-scripts/mailcow-reset-admin.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash [[ -f mailcow.conf ]] && source mailcow.conf [[ -f ../mailcow.conf ]] && source ../mailcow.conf diff --git a/helper-scripts/nextcloud.sh b/helper-scripts/nextcloud.sh index d04f52d4..395907ab 100755 --- a/helper-scripts/nextcloud.sh +++ b/helper-scripts/nextcloud.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash for bin in curl dirmngr; do if [[ -z $(which ${bin}) ]]; then echo "Cannot find ${bin}, exiting..."; exit 1; fi @@ -69,36 +69,27 @@ elif [[ ${NC_UPDATE} == "y" ]]; then if ! grep -q 'installed: true' <<<$(docker exec -it -u www-data $(docker ps -f name=php-fpm-mailcow -q) bash -c "/web/nextcloud/occ --no-warnings status"); then echo "Nextcloud seems not to be installed." exit 1 - elif ! grep -q 'version: 15\.' <<<$(docker exec -it -u www-data $(docker ps -f name=php-fpm-mailcow -q) bash -c "/web/nextcloud/occ --no-warnings status"); then + elif ! grep -q 'version: 16\.' <<<$(docker exec -it -u www-data $(docker ps -f name=php-fpm-mailcow -q) bash -c "/web/nextcloud/occ --no-warnings status"); then echo "Cannot upgrade to new major version, please update manually." exit 1 else - curl -L# -o nextcloud.tar.bz2 "https://download.nextcloud.com/server/releases/latest-15.tar.bz2" || { echo "Failed to download Nextcloud archive."; exit 1; } \ + curl -L# -o nextcloud.tar.bz2 "https://download.nextcloud.com/server/releases/latest-16.tar.bz2" || { echo "Failed to download Nextcloud archive."; exit 1; } \ && tar -xjf nextcloud.tar.bz2 -C ./data/web/ \ && rm nextcloud.tar.bz2 \ - && rm -rf ./data/web/nextcloud/updater \ && mkdir -p ./data/web/nextcloud/data \ - && mkdir -p ./data/web/nextcloud/custom_apps \ - && chmod +x ./data/web/nextcloud/occ - docker exec -it $(docker ps -f name=php-fpm-mailcow -q) bash -c "chown www-data:www-data -R /web/nextcloud" - docker exec -it -u www-data $(docker ps -f name=php-fpm-mailcow -q) bash -c "/web/nextcloud/occ --no-warnings upgrade" + && chmod +x ./data/web/nextcloud/occ \ + docker exec -it $(docker ps -f name=php-fpm-mailcow -q) bash -c "chown www-data:www-data -R /web/nextcloud" \ + docker exec -it -u www-data $(docker ps -f name=php-fpm-mailcow -q) bash -c "/web/nextcloud/occ --no-warnings upgrade" fi elif [[ ${NC_INSTALL} == "y" ]]; then - NC_TYPE= - while [[ ! ${NC_TYPE} =~ ^subfolder$|^subdomain$ ]]; do - read -p "Configure as subdomain or subfolder? [subdomain/subfolder] " NC_TYPE + NC_SUBD= + while [[ -z ${NC_SUBD} ]]; do + read -p "Subdomain to run Nextcloud from [format: nextcloud.domain.tld]: " NC_SUBD done - - if [[ ${NC_TYPE} == "subdomain" ]]; then - NC_SUBD= - while [[ -z ${NC_SUBD} ]]; do - read -p "Which subdomain? [format: nextcloud.domain.tld] " NC_SUBD - done - if ! ping -q -c2 ${NC_SUBD} > /dev/null 2>&1 ; then - read -p "Cannot ping subdomain, continue anyway? [y|N] " NC_CONT_FAIL - [[ ! ${NC_CONT_FAIL,,} =~ ^(yes|y)$ ]] && { echo "Ok, exiting..."; exit 1; } - fi + if ! ping -q -c2 ${NC_SUBD} > /dev/null 2>&1 ; then + read -p "Cannot ping subdomain, continue anyway? [y|N] " NC_CONT_FAIL + [[ ! ${NC_CONT_FAIL,,} =~ ^(yes|y)$ ]] && { echo "Ok, exiting..."; exit 1; } fi ADMIN_NC_PASS=$(</dev/urandom tr -dc A-Za-z0-9 | head -c 28) @@ -106,12 +97,10 @@ elif [[ ${NC_INSTALL} == "y" ]]; then curl -L# -o nextcloud.tar.bz2 "https://download.nextcloud.com/server/releases/latest-15.tar.bz2" || { echo "Failed to download Nextcloud archive."; exit 1; } \ && tar -xjf nextcloud.tar.bz2 -C ./data/web/ \ && rm nextcloud.tar.bz2 \ - && rm -rf ./data/web/nextcloud/updater \ && mkdir -p ./data/web/nextcloud/data \ - && mkdir -p ./data/web/nextcloud/custom_apps \ && chmod +x ./data/web/nextcloud/occ - docker exec -it $(docker ps -f name=php-fpm-mailcow -q) /bin/bash -c "chown -R www-data:www-data /web/nextcloud/data /web/nextcloud/config /web/nextcloud/apps /web/nextcloud/custom_apps" + docker exec -it $(docker ps -f name=php-fpm-mailcow -q) /bin/bash -c "chown -R www-data:www-data /web/nextcloud" docker exec -it -u www-data $(docker ps -f name=php-fpm-mailcow -q) /web/nextcloud/occ --no-warnings maintenance:install \ --database mysql \ --database-host mysql \ @@ -140,22 +129,21 @@ elif [[ ${NC_INSTALL} == "y" ]]; then /web/nextcloud/occ --no-warnings config:system:set mail_from_address --value=nextcloud; \ /web/nextcloud/occ --no-warnings config:system:set mail_domain --value=${MAILCOW_HOSTNAME}; \ /web/nextcloud/occ --no-warnings config:system:set mail_smtphost --value=postfix; \ - /web/nextcloud/occ --no-warnings config:system:set mail_smtpport --value=588 - /web/nextcloud/occ --no-warnings app:install user_external - /web/nextcloud/occ --no-warnings config:system:set user_backends 0 arguments 0 --value={dovecot:143/imap/tls/novalidate-cert} - /web/nextcloud/occ --no-warnings config:system:set user_backends 0 class --value=OC_User_IMAP + /web/nextcloud/occ --no-warnings config:system:set mail_smtpport --value=588; \ + /web/nextcloud/occ --no-warnings config:system:set trusted_domains 1 --value=${NC_SUBD}; \ + /web/nextcloud/occ --no-warnings config:system:set overwritewebroot --value=/; \ + /web/nextcloud/occ --no-warnings config:system:set overwritehost --value=${NC_SUBD}; \ /web/nextcloud/occ --no-warnings db:convert-filecache-bigint -n" - if [[ ${NC_TYPE} == "subdomain" ]]; then - docker exec -it -u www-data $(docker ps -f name=php-fpm-mailcow -q) /web/nextcloud/occ --no-warnings config:system:set trusted_domains 1 --value=${NC_SUBD} - docker exec -it -u www-data $(docker ps -f name=php-fpm-mailcow -q) /web/nextcloud/occ --no-warnings config:system:set overwritewebroot --value=/ - docker exec -it -u www-data $(docker ps -f name=php-fpm-mailcow -q) /web/nextcloud/occ --no-warnings config:system:set overwritehost --value=${NC_SUBD} + # Not installing by default, broke too often + #/web/nextcloud/occ --no-warnings app:install user_external; \ + #/web/nextcloud/occ --no-warnings config:system:set user_backends 0 arguments 0 --value={dovecot:143/imap/tls/novalidate-cert}; \ + #/web/nextcloud/occ --no-warnings config:system:set user_backends 0 class --value=OC_User_IMAP; \ + cp ./data/assets/nextcloud/nextcloud.conf ./data/conf/nginx/ sed -i "s/NC_SUBD/${NC_SUBD}/g" ./data/conf/nginx/nextcloud.conf - elif [[ ${NC_TYPE} == "subfolder" ]]; then - cp ./data/assets/nextcloud/site.nextcloud.custom ./data/conf/nginx/ - fi + echo "Restarting Nginx..." docker restart $(docker ps -aqf name=nginx-mailcow) echo "Login as admin with password: ${ADMIN_NC_PASS}" diff --git a/helper-scripts/reset-learns.sh b/helper-scripts/reset-learns.sh index 647c0e85..9fd11232 100755 --- a/helper-scripts/reset-learns.sh +++ b/helper-scripts/reset-learns.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash read -r -p "Are you sure you want to reset learned hashes from Rspamd (fuzzy, bayes, neural)? [y/N] " response response=${response,,} # tolower diff --git a/update.sh b/update.sh index 17e818f9..f62352cf 100755 --- a/update.sh +++ b/update.sh @@ -6,9 +6,21 @@ if [ "$(id -u)" -ne "0" ]; then exit 1 fi -#exit on error and pipefail +if [[ "$(uname -r)" =~ ^4\.15\.0-60 ]]; then + echo "DO NOT RUN mailcow ON THIS UBUNTU KERNEL!"; + echo "Please update to 5.x or use another distribution." + exit 1 +fi + +# Exit on error and pipefail set -o pipefail +# Setting high dc timeout +export COMPOSE_HTTP_TIMEOUT=600 + +# Add /opt/bin to PATH +PATH=$PATH:/opt/bin + umask 0022 for bin in curl docker-compose docker git awk sha1sum; do @@ -19,6 +31,20 @@ export LC_ALL=C DATE=$(date +%Y-%m-%d_%H_%M_%S) BRANCH=$(git rev-parse --abbrev-ref HEAD) +function prefetch_images() { + [[ -z ${BRANCH} ]] && { echo -e "\e[33m\nUnknown branch...\e[0m"; exit 1; } + git fetch origin #${BRANCH} + while read image; do + RET_C=0 + until docker pull ${image}; do + RET_C=$((RET_C + 1)) + echo -e "\e[33m\nError pulling $image, retrying...\e[0m" + [ ${RET_C} -gt 3 ] && { echo -e "\e[31m\nToo many failed retries, exiting\e[0m"; exit 1; } + sleep 1 + done + done < <(git show origin/${BRANCH}:docker-compose.yml | grep "image:" | awk '{ gsub("image:","", $3); print $2 }') +} + docker_garbage() { IMGS_TO_DELETE=() for container in $(grep -oP "image: \Kmailcow.+" docker-compose.yml); do @@ -68,8 +94,12 @@ while (($#)); do case "${1}" in --check|-c) echo "Checking remote code for updates..." - git fetch origin #${BRANCH} - if [[ -z $(git log HEAD --pretty=format:"%H" | grep $(git rev-parse origin/${BRANCH})) ]]; then + LATEST_REV=$(git ls-remote --exit-code --refs --quiet https://github.com/mailcow/mailcow-dockerized ${BRANCH} | cut -f1) + if [ $? -ne 0 ]; then + echo "A problem occurred while trying to fetch the latest revision from github." + exit 99 + fi + if [[ -z $(git log HEAD --pretty=format:"%H" | grep "${LATEST_REV}") ]]; then echo "Updated code is available." exit 0 else @@ -85,6 +115,11 @@ while (($#)); do docker_garbage exit 0 ;; + --prefetch) + echo -e "\e[32mPrefetching images...\e[0m" + prefetch_images + exit 0 + ;; --help|-h) echo './update.sh [-c|--check, --ours, --gc, -h|--help] @@ -98,6 +133,7 @@ while (($#)); do done [[ ! -f mailcow.conf ]] && { echo "mailcow.conf is missing"; exit 1;} +chmod 600 mailcow.conf source mailcow.conf DOTS=${MAILCOW_HOSTNAME//[^.]}; if [ ${#DOTS} -lt 2 ]; then @@ -113,6 +149,7 @@ CONFIG_ARRAY=( "SKIP_LETS_ENCRYPT" "USE_WATCHDOG" "WATCHDOG_NOTIFY_EMAIL" + "WATCHDOG_NOTIFY_BAN" "SKIP_CLAMD" "SKIP_IP_CHECK" "ADDITIONAL_SAN" @@ -127,9 +164,13 @@ CONFIG_ARRAY=( "API_KEY" "API_ALLOW_FROM" "MAILDIR_GC_TIME" + "MAILDIR_SUB" "ACL_ANYONE" "SOLR_HEAP" "SKIP_SOLR" + "ALLOW_ADMIN_EMAIL_LOGIN" + "SKIP_HTTP_VERIFICATION" + "SOGO_EXPIRE_SESSION" ) sed -i '$a\' mailcow.conf @@ -235,6 +276,25 @@ for option in ${CONFIG_ARRAY[@]}; do echo '# Disable Solr or if you do not want to store a readable index of your mails in solr-vol-1.' >> mailcow.conf echo "SKIP_SOLR=y" >> mailcow.conf fi + elif [[ ${option} == "MAILDIR_SUB" ]]; then + if ! grep -q ${option} mailcow.conf; then + echo "Adding new option \"${option}\" to mailcow.conf" + echo '# MAILDIR_SUB defines a path in a users virtual home to keep the maildir in. Leave empty for updated setups.' >> mailcow.conf + echo "#MAILDIR_SUB=Maildir" >> mailcow.conf + echo "MAILDIR_SUB=" >> mailcow.conf + fi + elif [[ ${option} == "WATCHDOG_NOTIFY_BAN" ]]; then + if ! grep -q ${option} mailcow.conf; then + echo "Adding new option \"${option}\" to mailcow.conf" + echo '# Notify about banned IP. Includes whois lookup.' >> mailcow.conf + echo "WATCHDOG_NOTIFY_BAN=y" >> mailcow.conf + fi + elif [[ ${option} == "SOGO_EXPIRE_SESSION" ]]; then + if ! grep -q ${option} mailcow.conf; then + echo "Adding new option \"${option}\" to mailcow.conf" + echo '# SOGo session timeout in minutes' >> mailcow.conf + echo "SOGO_EXPIRE_SESSION=480" >> mailcow.conf + fi elif ! grep -q ${option} mailcow.conf; then echo "Adding new option \"${option}\" to mailcow.conf" echo "${option}=n" >> mailcow.conf @@ -242,7 +302,7 @@ for option in ${CONFIG_ARRAY[@]}; do done echo -en "Checking internet connection... " -curl -o /dev/null 1.1.1.1 -sm3 +timeout 3 ping -c 1 9.9.9.9 > /dev/null if [[ $? != 0 ]]; then echo -e "\e[31mfailed\e[0m" exit 1 @@ -274,6 +334,17 @@ if [[ ! "$response" =~ ^([yY][eE][sS]|[yY])+$ ]]; then exit 0 fi +DIFF_DIRECTORY=update_diffs +DIFF_FILE=${DIFF_DIRECTORY}/diff_before_update_$(date +"%Y-%m-%d-%H-%M-%S") +echo -e "\e[32mSaving diff to ${DIFF_FILE}...\e[0m" +mkdir -p ${DIFF_DIRECTORY} +mv diff_before_update* ${DIFF_DIRECTORY}/ 2> /dev/null +git diff --stat > ${DIFF_FILE} +git diff >> ${DIFF_FILE} + +echo -e "\e[32mPrefetching images...\e[0m" +prefetch_images + echo -e "Stopping mailcow... " sleep 2 docker-compose down @@ -283,7 +354,7 @@ git remote set-url origin https://github.com/mailcow/mailcow-dockerized echo -e "\e[32mCommitting current status...\e[0m" [[ -z "$(git config user.name)" ]] && git config user.name moo [[ -z "$(git config user.email)" ]] && git config user.email moo@cow.moo -git update-index --assume-unchanged data/conf/rspamd/override.d/worker-controller-password.inc +[[ ! -z $(git ls-files data/conf/rspamd/override.d/worker-controller-password.inc) ]] && git rm data/conf/rspamd/override.d/worker-controller-password.inc git add -u git commit -am "Before update on ${DATE}" > /dev/null echo -e "\e[32mFetching updated code from remote...\e[0m" @@ -343,7 +414,7 @@ if grep -q 'SYSCTL_IPV6_DISABLED=1' mailcow.conf; then echo echo '!! IMPORTANT !!' echo - echo 'SYSCTL_IPV6_DISABLED was removed due to complications. IPv6 can be disabled by editing "docker-compose.yml" and setting "enabled_ipv6: true" to "enabled_ipv6: false".' + echo 'SYSCTL_IPV6_DISABLED was removed due to complications. IPv6 can be disabled by editing "docker-compose.yml" and setting "enable_ipv6: true" to "enable_ipv6: false".' echo 'This setting will only be active after a complete shutdown of mailcow by running "docker-compose down" followed by "docker-compose up -d".' echo echo '!! IMPORTANT !!' @@ -351,9 +422,8 @@ if grep -q 'SYSCTL_IPV6_DISABLED=1' mailcow.conf; then read -p "Press any key to continue..." < /dev/tty fi -echo -e "Fixing project name... " +# Checking for old project name bug sed -i 's#COMPOSEPROJECT_NAME#COMPOSE_PROJECT_NAME#g' mailcow.conf -sed -i '/COMPOSE_PROJECT_NAME=/s/-//g' mailcow.conf echo -e "Fixing PHP-FPM worker ports for Nginx sites..." sed -i 's#phpfpm:9000#phpfpm:9002#g' data/conf/nginx/*.conf