Merge remote-tracking branch 'origin/master' into gnous
This commit is contained in:
		
						commit
						80d14e4ac5
					
				
							
								
								
									
										7
									
								
								.github/ISSUE_TEMPLATE/Bug_report.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										7
									
								
								.github/ISSUE_TEMPLATE/Bug_report.yml
									
									
									
									
										vendored
									
									
								
							| @ -54,10 +54,11 @@ body: | ||||
|              | --- | --- | | ||||
|              | My operating system | I_DO_REPLY_HERE | | ||||
|              | Is Apparmor, SELinux or similar active? | I_DO_REPLY_HERE | | ||||
|              | Virtualization technlogy (KVM, VMware, Xen, etc - **LXC and OpenVZ are not supported** | I_DO_REPLY_HERE | | ||||
|              | Virtualization technology (KVM, VMware, Xen, etc - **LXC and OpenVZ are not supported** | 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 | | ||||
|              | Docker version (`docker version`) | I_DO_REPLY_HERE | | ||||
|              | docker-compose version (`docker-compose version`) | I_DO_REPLY_HERE | | ||||
|              | mailcow version (```git describe --tags `git rev-list --tags --max-count=1` ```) | I_DO_REPLY_HERE | | ||||
|              | Reverse proxy (custom solution) | I_DO_REPLY_HERE | | ||||
| 
 | ||||
|              Output of `git diff origin/master`, any other changes to the code? If so, **please post them**: | ||||
|  | ||||
| @ -1,4 +1,4 @@ | ||||
| FROM alpine:3.15 | ||||
| FROM alpine:3.16 | ||||
| 
 | ||||
| LABEL maintainer "Andre Peters <andre.peters@servercow.de>" | ||||
| 
 | ||||
|  | ||||
| @ -1,4 +1,4 @@ | ||||
| FROM clamav/clamav:0.104.2-2_base | ||||
| FROM clamav/clamav:0.105.0_base | ||||
| 
 | ||||
| LABEL maintainer "André Peters <andre.peters@servercow.de>" | ||||
| 
 | ||||
|  | ||||
| @ -14,10 +14,10 @@ rm -rf /var/lib/clamav/clamav-*.tmp | ||||
| 
 | ||||
| mkdir -p /run/clamav /var/lib/clamav | ||||
| 
 | ||||
| #if [[ -s /etc/clamav/whitelist.ign2 ]]; then | ||||
| #  echo "Copying non-empty whitelist.ign2 to /var/lib/clamav/whitelist.ign2" | ||||
| #  cp /etc/clamav/whitelist.ign2 /var/lib/clamav/whitelist.ign2 | ||||
| #fi | ||||
| if [[ -s /etc/clamav/whitelist.ign2 ]]; then | ||||
|   echo "Copying non-empty whitelist.ign2 to /var/lib/clamav/whitelist.ign2" | ||||
|   cp /etc/clamav/whitelist.ign2 /var/lib/clamav/whitelist.ign2 | ||||
| fi | ||||
| 
 | ||||
| if [[ ! -f /var/lib/clamav/whitelist.ign2 ]]; then | ||||
|   echo "Creating /var/lib/clamav/whitelist.ign2" | ||||
|  | ||||
| @ -1,4 +1,4 @@ | ||||
| FROM alpine:3.15 | ||||
| FROM alpine:3.16 | ||||
| 
 | ||||
| LABEL maintainer "Andre Peters <andre.peters@servercow.de>" | ||||
| 
 | ||||
|  | ||||
| @ -51,8 +51,8 @@ sub sig_handler { | ||||
|   die "sig_handler received signal, preparing to exit...\n"; | ||||
| }; | ||||
| 
 | ||||
| open my $file, '<', "/etc/sogo/sieve.creds";  | ||||
| my $creds = <$file>;  | ||||
| open my $file, '<', "/etc/sogo/sieve.creds"; | ||||
| my $creds = <$file>; | ||||
| close $file; | ||||
| my ($master_user, $master_pass) = split /:/, $creds; | ||||
| my $sth = $dbh->prepare("SELECT id, | ||||
| @ -166,11 +166,17 @@ while ($row = $sth->fetchrow_arrayref()) { | ||||
|       $success = 1; | ||||
|     } | ||||
| 
 | ||||
|     $update = $dbh->prepare("UPDATE imapsync SET returned_text = ?, success = ?, exit_status = ? WHERE id = ?"); | ||||
|     $keep_job_active = 1; | ||||
|     if (defined $exit_status && $exit_status eq "EXIT_AUTHENTICATION_FAILURE_USER1") { | ||||
|       $keep_job_active = 0; | ||||
|     } | ||||
| 
 | ||||
|     $update = $dbh->prepare("UPDATE imapsync SET returned_text = ?, success = ?, exit_status = ?, active = ? WHERE id = ?"); | ||||
|     $update->bind_param( 1, ${stdout} ); | ||||
|     $update->bind_param( 2, ${success} ); | ||||
|     $update->bind_param( 3, ${exit_status} ); | ||||
|     $update->bind_param( 4, ${id} ); | ||||
|     $update->bind_param( 4, ${keep_job_active} ); | ||||
|     $update->bind_param( 5, ${id} ); | ||||
|     $update->execute(); | ||||
|   } catch { | ||||
|     $update = $dbh->prepare("UPDATE imapsync SET returned_text = 'Could not start or finish imapsync', success = 0 WHERE id = ?"); | ||||
|  | ||||
| @ -1,4 +1,4 @@ | ||||
| FROM alpine:3.15 | ||||
| FROM alpine:3.16 | ||||
| LABEL maintainer "Andre Peters <andre.peters@servercow.de>" | ||||
| 
 | ||||
| ENV XTABLES_LIBDIR /usr/lib/xtables | ||||
|  | ||||
| @ -94,7 +94,7 @@ def refreshF2bregex(): | ||||
|     f2bregex = {} | ||||
|     f2bregex[1] = 'mailcow UI: Invalid password for .+ by ([0-9a-f\.:]+)' | ||||
|     f2bregex[2] = 'Rspamd UI: Invalid password by ([0-9a-f\.:]+)' | ||||
|     f2bregex[3] = 'warning: .*\[([0-9a-f\.:]+)\]: SASL .+ authentication failed' | ||||
|     f2bregex[3] = 'warning: .*\[([0-9a-f\.:]+)\]: SASL .+ authentication failed: (?!.*Connection lost to authentication server).+' | ||||
|     f2bregex[4] = 'warning: non-SMTP command from .*\[([0-9a-f\.:]+)]:.+' | ||||
|     f2bregex[5] = 'NOQUEUE: reject: RCPT from \[([0-9a-f\.:]+)].+Protocol error.+' | ||||
|     f2bregex[6] = '-login: Disconnected \(auth failed, .+\): user=.*, method=.+, rip=([0-9a-f\.:]+),' | ||||
|  | ||||
| @ -1,4 +1,4 @@ | ||||
| FROM alpine:3.15 | ||||
| FROM alpine:3.16 | ||||
| LABEL maintainer "Andre Peters <andre.peters@servercow.de>" | ||||
| 
 | ||||
| WORKDIR /app | ||||
|  | ||||
| @ -1,12 +1,12 @@ | ||||
| FROM php:8.0-fpm-alpine3.14 | ||||
| FROM php:8.0-fpm-alpine3.16 | ||||
| LABEL maintainer "Andre Peters <andre.peters@servercow.de>" | ||||
| 
 | ||||
| ENV APCU_PECL 5.1.20 | ||||
| ENV IMAGICK_PECL 3.5.1 | ||||
| ENV APCU_PECL 5.1.21 | ||||
| ENV IMAGICK_PECL 3.7.0 | ||||
| # Mailparse is pulled from master branch | ||||
| #ENV MAILPARSE_PECL 3.0.2 | ||||
| ENV MEMCACHED_PECL 3.1.5 | ||||
| ENV REDIS_PECL 5.3.4 | ||||
| ENV MEMCACHED_PECL 3.2.0 | ||||
| ENV REDIS_PECL 5.3.7 | ||||
| 
 | ||||
| RUN apk add -U --no-cache autoconf \ | ||||
|   aspell-dev \ | ||||
|  | ||||
| @ -1,4 +1,4 @@ | ||||
| FROM debian:buster-slim | ||||
| FROM debian:bullseye-slim | ||||
| LABEL maintainer "Andre Peters <andre.peters@servercow.de>" | ||||
| 
 | ||||
| ARG DEBIAN_FRONTEND=noninteractive | ||||
|  | ||||
| @ -1,4 +1,4 @@ | ||||
| @version: 3.19 | ||||
| @version: 3.28 | ||||
| @include "scl.conf" | ||||
| options { | ||||
|   chain_hostnames(off); | ||||
|  | ||||
| @ -1,4 +1,4 @@ | ||||
| @version: 3.19 | ||||
| @version: 3.28 | ||||
| @include "scl.conf" | ||||
| options { | ||||
|   chain_hostnames(off); | ||||
|  | ||||
| @ -1,4 +1,4 @@ | ||||
| FROM alpine:3.15 | ||||
| FROM alpine:3.16 | ||||
| 
 | ||||
| LABEL maintainer "Andre Peters <andre.peters@servercow.de>" | ||||
| 
 | ||||
|  | ||||
| @ -1,4 +1,4 @@ | ||||
| FROM alpine:3.15 | ||||
| FROM alpine:3.16 | ||||
| LABEL maintainer "André Peters <andre.peters@servercow.de>" | ||||
| 
 | ||||
| # Installation | ||||
|  | ||||
| @ -65,7 +65,7 @@ | ||||
|   } | ||||
| 
 | ||||
|   location ~ ^/api/v1/(.*)$ { | ||||
|     try_files $uri $uri/ /json_api.php?query=$1; | ||||
|     try_files $uri $uri/ /json_api.php?query=$1&$args; | ||||
|   } | ||||
| 
 | ||||
|   location ^~ /.well-known/acme-challenge/ { | ||||
|  | ||||
| @ -1,24 +0,0 @@ | ||||
| rules { | ||||
|   "LONG" { | ||||
|     train { | ||||
|       max_trains = 200; | ||||
|       max_usages = 20; | ||||
|       max_iterations = 25; | ||||
|       learning_rate = 0.01, | ||||
|     } | ||||
|     symbol_spam = "NEURAL_SPAM_LONG"; | ||||
|     symbol_ham = "NEURAL_HAM_LONG"; | ||||
|     ann_expire = 45d; | ||||
|   } | ||||
|   "SHORT" { | ||||
|     train { | ||||
|       max_trains = 100; | ||||
|       max_usages = 10; | ||||
|       max_iterations = 15; | ||||
|       learning_rate = 0.01, | ||||
|     } | ||||
|     symbol_spam = "NEURAL_SPAM_SHORT"; | ||||
|     symbol_ham = "NEURAL_HAM_SHORT"; | ||||
|     ann_expire = 7d; | ||||
|   } | ||||
| } | ||||
| @ -1,18 +0,0 @@ | ||||
| symbols = { | ||||
|   "NEURAL_SPAM_LONG" { | ||||
|     weight = 3.7; # sample weight | ||||
|     description = "Neural network spam (long)"; | ||||
|   } | ||||
|   "NEURAL_HAM_LONG" { | ||||
|     weight = -4.0; # sample weight | ||||
|     description = "Neural network ham (long)"; | ||||
|   } | ||||
|   "NEURAL_SPAM_SHORT" { | ||||
|     weight = 2.5; # sample weight | ||||
|     description = "Neural network spam (short)"; | ||||
|   } | ||||
|   "NEURAL_HAM_SHORT" { | ||||
|     weight = -2.0; # sample weight | ||||
|     description = "Neural network ham (short)"; | ||||
|   } | ||||
| } | ||||
| @ -39,7 +39,7 @@ | ||||
|     window.onload = function() { | ||||
|       // Begin Swagger UI call region | ||||
|       const ui = SwaggerUIBundle({ | ||||
|         url: "/api/openapi.yaml", | ||||
|         urls: [{url: "/api/openapi.yaml", name: "mailcow API"}], | ||||
|         dom_id: '#swagger-ui', | ||||
|         deepLinking: true, | ||||
|         presets: [ | ||||
|  | ||||
| @ -209,10 +209,17 @@ paths: | ||||
|                         - app_passwd | ||||
|                         - add | ||||
|                         - active: "1" | ||||
|                           app_name: emclient | ||||
|                           username: info@domain.tld | ||||
|                           app_name: wordpress | ||||
|                           app_passwd: keyleudecticidechothistishownsan31 | ||||
|                           app_passwd2: keyleudecticidechothistishownsan31 | ||||
|                           username: hello@mailcow.email | ||||
|                           protocols: | ||||
|                             - imap_access | ||||
|                             - dav_access | ||||
|                             - smtp_access | ||||
|                             - eas_access | ||||
|                             - pop3_access | ||||
|                             - sieve_access | ||||
|                       msg: app_passwd_added | ||||
|                       type: success | ||||
|               schema: | ||||
| @ -249,6 +256,13 @@ paths: | ||||
|                 app_name: wordpress | ||||
|                 app_passwd: keyleudecticidechothistishownsan31 | ||||
|                 app_passwd2: keyleudecticidechothistishownsan31 | ||||
|                 protocols: | ||||
|                   - imap_access | ||||
|                   - dav_access | ||||
|                   - smtp_access | ||||
|                   - eas_access | ||||
|                   - pop3_access | ||||
|                   - sieve_access | ||||
|               properties: | ||||
|                 active: | ||||
|                   description: is alias active or not | ||||
| @ -497,6 +511,7 @@ paths: | ||||
|                           relay_all_recipients: "0" | ||||
|                           rl_frame: s | ||||
|                           rl_value: "10" | ||||
|                           tags: ["tag1", "tag2"] | ||||
|                         - null | ||||
|                       msg: | ||||
|                         - domain_added | ||||
| @ -544,6 +559,7 @@ paths: | ||||
|                 rl_frame: s | ||||
|                 rl_value: "10" | ||||
|                 restart_sogo: "10" | ||||
|                 tags: ["tag1", "tag2"] | ||||
|               properties: | ||||
|                 active: | ||||
|                   description: is domain active or not | ||||
| @ -1010,6 +1026,7 @@ paths: | ||||
|                           force_pw_update: "1" | ||||
|                           tls_enforce_in: "1" | ||||
|                           tls_enforce_out: "1" | ||||
|                           tags: ["tag1", "tag2"] | ||||
|                         - null | ||||
|                       msg: | ||||
|                         - mailbox_added | ||||
| @ -1054,6 +1071,7 @@ paths: | ||||
|                 force_pw_update: "1" | ||||
|                 tls_enforce_in: "1" | ||||
|                 tls_enforce_out: "1" | ||||
|                 tags: ["tag1", "tag2"] | ||||
|               properties: | ||||
|                 active: | ||||
|                   description: is mailbox active or not | ||||
| @ -2716,6 +2734,140 @@ paths: | ||||
|                   type: object | ||||
|               type: object | ||||
|       summary: Delete Transport Maps | ||||
|   "/api/v1/delete/mailbox/tag/{mailbox}": | ||||
|     post: | ||||
|       parameters: | ||||
|         - description: name of mailbox | ||||
|           in: path | ||||
|           name: mailbox | ||||
|           example: info@domain.tld | ||||
|           required: true | ||||
|           schema: | ||||
|             type: string | ||||
|       responses: | ||||
|         "401": | ||||
|           $ref: "#/components/responses/Unauthorized" | ||||
|         "200": | ||||
|           content: | ||||
|             application/json: | ||||
|               examples: | ||||
|                 response: | ||||
|                   value: | ||||
|                     - log: | ||||
|                         - mailbox | ||||
|                         - delete | ||||
|                         - tags_mailbox | ||||
|                         - tags: | ||||
|                           - tag1 | ||||
|                           - tag2 | ||||
|                           mailbox: info@domain.tld | ||||
|                         - null | ||||
|                       msg: | ||||
|                         - mailbox_modified | ||||
|                         - info@domain.tld | ||||
|                       type: success | ||||
|               schema: | ||||
|                 properties: | ||||
|                   log: | ||||
|                     description: contains request object | ||||
|                     items: {} | ||||
|                     type: array | ||||
|                   msg: | ||||
|                     items: {} | ||||
|                     type: array | ||||
|                   type: | ||||
|                     enum: | ||||
|                       - success | ||||
|                       - danger | ||||
|                       - error | ||||
|                     type: string | ||||
|                 type: object | ||||
|           description: OK | ||||
|           headers: {} | ||||
|       tags: | ||||
|         - Mailboxes | ||||
|       description: You can delete one or more mailbox tags. | ||||
|       operationId: Delete mailbox tags | ||||
|       requestBody: | ||||
|         content: | ||||
|           application/json: | ||||
|             schema: | ||||
|               example: | ||||
|                 - tag1 | ||||
|                 - tag2 | ||||
|               properties: | ||||
|                 items: | ||||
|                   description: contains list of mailboxes you want to delete | ||||
|                   type: object | ||||
|               type: object | ||||
|       summary: Delete mailbox tags | ||||
|   "/api/v1/delete/domain/tag/{domain}": | ||||
|     post: | ||||
|       parameters: | ||||
|         - description: name of domain | ||||
|           in: path | ||||
|           name: domain | ||||
|           example: domain.tld | ||||
|           required: true | ||||
|           schema: | ||||
|             type: string | ||||
|       responses: | ||||
|         "401": | ||||
|           $ref: "#/components/responses/Unauthorized" | ||||
|         "200": | ||||
|           content: | ||||
|             application/json: | ||||
|               examples: | ||||
|                 response: | ||||
|                   value: | ||||
|                     - log: | ||||
|                         - mailbox | ||||
|                         - delete | ||||
|                         - tags_domain | ||||
|                         - tags: | ||||
|                           - tag1 | ||||
|                           - tag2 | ||||
|                           domain: domain.tld | ||||
|                         - null | ||||
|                       msg: | ||||
|                         - domain_modified | ||||
|                         - domain.tld | ||||
|                       type: success | ||||
|               schema: | ||||
|                 properties: | ||||
|                   log: | ||||
|                     description: contains request object | ||||
|                     items: {} | ||||
|                     type: array | ||||
|                   msg: | ||||
|                     items: {} | ||||
|                     type: array | ||||
|                   type: | ||||
|                     enum: | ||||
|                       - success | ||||
|                       - danger | ||||
|                       - error | ||||
|                     type: string | ||||
|                 type: object | ||||
|           description: OK | ||||
|           headers: {} | ||||
|       tags: | ||||
|         - Domains | ||||
|       description: You can delete one or more domain tags. | ||||
|       operationId: Delete domain tags | ||||
|       requestBody: | ||||
|         content: | ||||
|           application/json: | ||||
|             schema: | ||||
|               example: | ||||
|                 - tag1 | ||||
|                 - tag2 | ||||
|               properties: | ||||
|                 items: | ||||
|                   description: contains list of domains you want to delete | ||||
|                   type: object | ||||
|               type: object | ||||
|       summary: Delete domain tags | ||||
|   /api/v1/edit/alias: | ||||
|     post: | ||||
|       responses: | ||||
| @ -2865,6 +3017,7 @@ paths: | ||||
|                   quota: "10240" | ||||
|                   relay_all_recipients: "0" | ||||
|                   relayhost: "2" | ||||
|                   tags: ["tag3", "tag4"] | ||||
|                 items: domain.tld | ||||
|               properties: | ||||
|                 attr: | ||||
| @ -3019,6 +3172,7 @@ paths: | ||||
|                           sogo_access: "1" | ||||
|                           username: | ||||
|                             - info@domain.tld | ||||
|                           tags: ["tag3", "tag4"] | ||||
|                         - null | ||||
|                       msg: | ||||
|                         - mailbox_modified | ||||
| @ -3066,6 +3220,7 @@ paths: | ||||
|                     - domain3.tld | ||||
|                     - "*" | ||||
|                   sogo_access: "1" | ||||
|                   tags: ["tag3", "tag4"] | ||||
|                 items: | ||||
|                   - info@domain.tld | ||||
|               properties: | ||||
| @ -3793,6 +3948,11 @@ paths: | ||||
|               - all | ||||
|               - mailcow.tld | ||||
|             type: string | ||||
|         - description: comma seperated list of tags to filter by | ||||
|           example: "tag1,tag2" | ||||
|           in: query | ||||
|           name: tags | ||||
|           required: false | ||||
|         - description: e.g. api-key-string | ||||
|           example: api-key-string | ||||
|           in: header | ||||
| @ -3831,6 +3991,7 @@ paths: | ||||
|                       relay_all_recipients: "0" | ||||
|                       relayhost: "0" | ||||
|                       rl: false | ||||
|                       tags: ["tag1", "tag2"] | ||||
|                     - active: "1" | ||||
|                       aliases_in_domain: 0 | ||||
|                       aliases_left: 400 | ||||
| @ -3853,6 +4014,7 @@ paths: | ||||
|                       relay_all_recipients: "0" | ||||
|                       relayhost: "0" | ||||
|                       rl: false | ||||
|                       tags: ["tag3", "tag4"] | ||||
|           description: OK | ||||
|           headers: {} | ||||
|       tags: | ||||
| @ -4345,6 +4507,11 @@ paths: | ||||
|               - all | ||||
|               - user@domain.tld | ||||
|             type: string | ||||
|         - description: comma seperated list of tags to filter by | ||||
|           example: "tag1,tag2" | ||||
|           in: query | ||||
|           name: tags | ||||
|           required: false | ||||
|         - description: e.g. api-key-string | ||||
|           example: api-key-string | ||||
|           in: header | ||||
| @ -4382,6 +4549,7 @@ paths: | ||||
|                       rl: false | ||||
|                       spam_aliases: 0 | ||||
|                       username: info@doman3.tld | ||||
|                       tags: ["tag1", "tag2"] | ||||
|           description: OK | ||||
|           headers: {} | ||||
|       tags: | ||||
| @ -5072,6 +5240,27 @@ paths: | ||||
|         of used storage. | ||||
|       operationId: Get vmail status | ||||
|       summary: Get vmail status | ||||
|   /api/v1/get/status/version: | ||||
|     get: | ||||
|       responses: | ||||
|         "401": | ||||
|           $ref: "#/components/responses/Unauthorized" | ||||
|         "200": | ||||
|           content: | ||||
|             application/json: | ||||
|               examples: | ||||
|                 response: | ||||
|                   value: | ||||
|                     version: "2022-04" | ||||
|           description: OK | ||||
|           headers: {} | ||||
|       tags: | ||||
|         - Status | ||||
|       description: >- | ||||
|         Using this endpoint you can get the current running release of this | ||||
|         instance. | ||||
|       operationId: Get version status | ||||
|       summary: Get version status | ||||
|   /api/v1/get/syncjobs/all/no_log: | ||||
|     get: | ||||
|       responses: | ||||
|  | ||||
| @ -232,6 +232,9 @@ table.footable>tbody>tr.footable-empty>td { | ||||
|   font-style:italic; | ||||
|   font-size: 1rem; | ||||
| } | ||||
| table>tbody>tr>td>span.footable-toggle { | ||||
|   opacity: 0.75; | ||||
| } | ||||
| .navbar-nav > li { | ||||
|   font-size: 1rem !important; | ||||
| } | ||||
| @ -256,3 +259,41 @@ code { | ||||
| .flag-icon { | ||||
|   margin-right: 5px; | ||||
| } | ||||
| 
 | ||||
| .tag-box { | ||||
|   display: flex; | ||||
|   flex-wrap: wrap; | ||||
|   height: auto; | ||||
| } | ||||
| .tag-badge { | ||||
|   transition: 200ms linear; | ||||
|   margin-top: 5px; | ||||
|   margin-bottom: 5px; | ||||
|   margin-left: 2px; | ||||
|   margin-right: 2px; | ||||
| } | ||||
| .tag-badge.btn-badge { | ||||
|   cursor: pointer; | ||||
| } | ||||
| .tag-badge .bi { | ||||
|   font-size: 12px; | ||||
| } | ||||
| .tag-badge.btn-badge:hover { | ||||
|   filter: brightness(0.9); | ||||
| } | ||||
| .tag-input { | ||||
|   margin-left: 10px; | ||||
|   border: 0; | ||||
|   flex: 1; | ||||
|   height: 24px; | ||||
|   min-width: 150px; | ||||
| } | ||||
| .tag-input:focus { | ||||
|   outline: none; | ||||
| } | ||||
| .tag-add { | ||||
|   padding: 0 5px 0 5px; | ||||
|   align-items: center; | ||||
|   display: inline-flex; | ||||
| } | ||||
| 
 | ||||
|  | ||||
| @ -54,6 +54,7 @@ if (isset($_SESSION['mailcow_cc_role'])) { | ||||
|           'rl' => $rl, | ||||
|           'rlyhosts' => $rlyhosts, | ||||
|           'dkim' => dkim('details', $domain), | ||||
|           'domain_details' => $result, | ||||
|         ]; | ||||
|     } | ||||
|     elseif (isset($_GET['oauth2client']) && | ||||
| @ -99,6 +100,7 @@ if (isset($_SESSION['mailcow_cc_role'])) { | ||||
|         'rlyhosts' => $rlyhosts, | ||||
|         'sender_acl_handles' => mailbox('get', 'sender_acl_handles', $mailbox), | ||||
|         'user_acls' => acl('get', 'user', $mailbox), | ||||
|         'mailbox_details' => $result | ||||
|       ]; | ||||
|     } | ||||
|     elseif (isset($_GET['relayhost']) && is_numeric($_GET["relayhost"]) && !empty($_GET["relayhost"])) { | ||||
|  | ||||
| @ -337,7 +337,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { | ||||
|           $enc1                 = $_data['enc1']; | ||||
|           $custom_params        = (empty(trim($_data['custom_params']))) ? '' : trim($_data['custom_params']); | ||||
|           // Workaround, fixme
 | ||||
|           if (strpos($custom_params, 'pipemess')) { | ||||
|           if (stripos($custom_params, 'pipemess') || stripos($custom_params, 'pipemes')) { | ||||
|             $custom_params = ''; | ||||
|           } | ||||
|           if (empty($subfolder2)) { | ||||
| @ -443,16 +443,15 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { | ||||
|           if ($_SESSION['mailcow_cc_role'] != "admin") { | ||||
|             $_SESSION['return'][] = array( | ||||
|               'type' => 'danger', | ||||
|               'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), | ||||
|               'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_extra), | ||||
|               'msg' => 'access_denied' | ||||
|             ); | ||||
|             return false; | ||||
|           } | ||||
|           $domain       = idn_to_ascii(strtolower(trim($_data['domain'])), 0, INTL_IDNA_VARIANT_UTS46); | ||||
|           $description  = $_data['description']; | ||||
|           if (empty($description)) { | ||||
|             $description = $domain; | ||||
|           } | ||||
|           if (empty($description)) $description = $domain; | ||||
|           $tags         = (array)$_data['tags']; | ||||
|           $aliases      = (int)$_data['aliases']; | ||||
|           $mailboxes    = (int)$_data['mailboxes']; | ||||
|           $defquota     = (int)$_data['defquota']; | ||||
| @ -545,10 +544,12 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { | ||||
|             ); | ||||
|             return false; | ||||
|           } | ||||
| 
 | ||||
|           $stmt = $pdo->prepare("DELETE FROM `sender_acl` WHERE `external` = 1 AND `send_as` LIKE :domain"); | ||||
|           $stmt->execute(array( | ||||
|             ':domain' => '%@' . $domain | ||||
|           )); | ||||
|           // save domain
 | ||||
|           $stmt = $pdo->prepare("INSERT INTO `domain` (`domain`, `description`, `aliases`, `mailboxes`, `defquota`, `maxquota`, `quota`, `backupmx`, `gal`, `active`, `relay_unknown_only`, `relay_all_recipients`)
 | ||||
|             VALUES (:domain, :description, :aliases, :mailboxes, :defquota, :maxquota, :quota, :backupmx, :gal, :active, :relay_unknown_only, :relay_all_recipients)");
 | ||||
|           $stmt->execute(array( | ||||
| @ -565,6 +566,24 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { | ||||
|             ':relay_unknown_only' => $relay_unknown_only, | ||||
|             ':relay_all_recipients' => $relay_all_recipients | ||||
|           )); | ||||
|           // save tags
 | ||||
|           foreach($tags as $index => $tag){ | ||||
|             if (empty($tag)) continue; | ||||
|             if ($index > $GLOBALS['TAGGING_LIMIT']) { | ||||
|               $_SESSION['return'][] = array( | ||||
|                 'type' => 'warning', | ||||
|                 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), | ||||
|                 'msg' => array('tag_limit_exceeded', 'limit '.$GLOBALS['TAGGING_LIMIT']) | ||||
|               ); | ||||
|               break; | ||||
|             } | ||||
|             $stmt = $pdo->prepare("INSERT INTO `tags_domain` (`domain`, `tag_name`) VALUES (:domain, :tag_name)"); | ||||
|             $stmt->execute(array( | ||||
|               ':domain' => $domain, | ||||
|               ':tag_name' => $tag, | ||||
|             )); | ||||
|           } | ||||
| 
 | ||||
|           try { | ||||
|             $redis->hSet('DOMAIN_MAP', $domain, 1); | ||||
|           } | ||||
| @ -942,6 +961,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { | ||||
|           $password     = $_data['password']; | ||||
|           $password2    = $_data['password2']; | ||||
|           $name         = ltrim(rtrim($_data['name'], '>'), '<'); | ||||
|           $tags         = $_data['tags']; | ||||
|           $quota_m      = intval($_data['quota']); | ||||
|           if ((!isset($_SESSION['acl']['unlimited_quota']) || $_SESSION['acl']['unlimited_quota'] != "1") && $quota_m === 0) { | ||||
|             $_SESSION['return'][] = array( | ||||
| @ -1103,6 +1123,23 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { | ||||
|           $stmt->execute(array( | ||||
|             ':username' => $username | ||||
|           )); | ||||
|           // save tags
 | ||||
|           foreach($tags as $index => $tag){ | ||||
|             if (empty($tag)) continue; | ||||
|             if ($index > $GLOBALS['TAGGING_LIMIT']) { | ||||
|               $_SESSION['return'][] = array( | ||||
|                 'type' => 'warning', | ||||
|                 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), | ||||
|                 'msg' => array('tag_limit_exceeded', 'limit '.$GLOBALS['TAGGING_LIMIT']) | ||||
|               ); | ||||
|               break; | ||||
|             } | ||||
|             $stmt = $pdo->prepare("INSERT INTO `tags_mailbox` (`username`, `tag_name`) VALUES (:username, :tag_name)"); | ||||
|             $stmt->execute(array( | ||||
|               ':username' => $username, | ||||
|               ':tag_name' => $tag, | ||||
|             )); | ||||
|           } | ||||
|           $stmt = $pdo->prepare("INSERT INTO `quota2` (`username`, `bytes`, `messages`)
 | ||||
|             VALUES (:username, '0', '0') ON DUPLICATE KEY UPDATE `bytes` = '0', `messages` = '0';");
 | ||||
|           $stmt->execute(array(':username' => $username)); | ||||
| @ -1709,7 +1746,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { | ||||
|               ); | ||||
|               continue; | ||||
|             } | ||||
|             if (strpos($custom_params, 'pipemess')) { | ||||
|             if (stripos($custom_params, 'pipemess') || stripos($custom_params, 'pipemes')) { | ||||
|               $custom_params = ''; | ||||
|             } | ||||
|             if (empty($subfolder2)) { | ||||
| @ -2146,6 +2183,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { | ||||
|                 $gal                  = (isset($_data['gal'])) ? intval($_data['gal']) : $is_now['gal']; | ||||
|                 $description          = (!empty($_data['description']) && isset($_SESSION['acl']['domain_desc']) && $_SESSION['acl']['domain_desc'] == "1") ? $_data['description'] : $is_now['description']; | ||||
|                 (int)$relayhost       = (isset($_data['relayhost']) && isset($_SESSION['acl']['domain_relayhost']) && $_SESSION['acl']['domain_relayhost'] == "1") ? intval($_data['relayhost']) : intval($is_now['relayhost']); | ||||
|                 $tags                 = (is_array($_data['tags']) ? $_data['tags'] : array()); | ||||
|               } | ||||
|               else { | ||||
|                 $_SESSION['return'][] = array( | ||||
| @ -2155,6 +2193,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { | ||||
|                 ); | ||||
|                 continue; | ||||
|               } | ||||
| 
 | ||||
|               $stmt = $pdo->prepare("UPDATE `domain` SET
 | ||||
|               `description` = :description, | ||||
|               `gal` = :gal | ||||
| @ -2164,6 +2203,24 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { | ||||
|                 ':gal' => $gal, | ||||
|                 ':domain' => $domain | ||||
|               )); | ||||
|               // save tags
 | ||||
|               foreach($tags as $index => $tag){ | ||||
|                 if (empty($tag)) continue; | ||||
|                 if ($index > $GLOBALS['TAGGING_LIMIT']) { | ||||
|                   $_SESSION['return'][] = array( | ||||
|                     'type' => 'warning', | ||||
|                     'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), | ||||
|                     'msg' => array('tag_limit_exceeded', 'limit '.$GLOBALS['TAGGING_LIMIT']) | ||||
|                   ); | ||||
|                   break; | ||||
|                 } | ||||
|                 $stmt = $pdo->prepare("INSERT INTO `tags_domain` (`domain`, `tag_name`) VALUES (:domain, :tag_name)"); | ||||
|                 $stmt->execute(array( | ||||
|                   ':domain' => $domain, | ||||
|                   ':tag_name' => $tag, | ||||
|                 )); | ||||
|               } | ||||
| 
 | ||||
|               $_SESSION['return'][] = array( | ||||
|                 'type' => 'success', | ||||
|                 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), | ||||
| @ -2185,6 +2242,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { | ||||
|                 $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']; | ||||
|                 $tags                 = (is_array($_data['tags']) ? $_data['tags'] : array()); | ||||
|                 if ($relay_all_recipients == '1') { | ||||
|                   $backupmx = '1'; | ||||
|                 } | ||||
| @ -2283,6 +2341,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { | ||||
|                 ); | ||||
|                 continue; | ||||
|               } | ||||
| 
 | ||||
|               $stmt = $pdo->prepare("UPDATE `domain` SET
 | ||||
|               `relay_all_recipients` = :relay_all_recipients, | ||||
|               `relay_unknown_only` = :relay_unknown_only, | ||||
| @ -2312,6 +2371,24 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { | ||||
|                 ':description' => $description, | ||||
|                 ':domain' => $domain | ||||
|               )); | ||||
|               // save tags
 | ||||
|               foreach($tags as $index => $tag){ | ||||
|                 if (empty($tag)) continue; | ||||
|                 if ($index > $GLOBALS['TAGGING_LIMIT']) { | ||||
|                   $_SESSION['return'][] = array( | ||||
|                     'type' => 'warning', | ||||
|                     'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), | ||||
|                     'msg' => array('tag_limit_exceeded', 'limit '.$GLOBALS['TAGGING_LIMIT']) | ||||
|                   ); | ||||
|                   break; | ||||
|                 } | ||||
|                 $stmt = $pdo->prepare("INSERT INTO `tags_domain` (`domain`, `tag_name`) VALUES (:domain, :tag_name)"); | ||||
|                 $stmt->execute(array( | ||||
|                   ':domain' => $domain, | ||||
|                   ':tag_name' => $tag, | ||||
|                 )); | ||||
|               } | ||||
| 
 | ||||
|               $_SESSION['return'][] = array( | ||||
|                 'type' => 'success', | ||||
|                 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), | ||||
| @ -2360,6 +2437,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { | ||||
|               $quota_b    = $quota_m * 1048576; | ||||
|               $password   = (!empty($_data['password'])) ? $_data['password'] : null; | ||||
|               $password2  = (!empty($_data['password2'])) ? $_data['password2'] : null; | ||||
|               $tags       = (is_array($_data['tags']) ? $_data['tags'] : array()); | ||||
|             } | ||||
|             else { | ||||
|               $_SESSION['return'][] = array( | ||||
| @ -2636,6 +2714,24 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { | ||||
|               ':relayhost' => $relayhost, | ||||
|               ':username' => $username | ||||
|             )); | ||||
|             // save tags
 | ||||
|             foreach($tags as $index => $tag){ | ||||
|               if (empty($tag)) continue; | ||||
|               if ($index > $GLOBALS['TAGGING_LIMIT']) { | ||||
|                 $_SESSION['return'][] = array( | ||||
|                   'type' => 'warning', | ||||
|                   'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), | ||||
|                   'msg' => array('tag_limit_exceeded', 'limit '.$GLOBALS['TAGGING_LIMIT']) | ||||
|                 ); | ||||
|                 break; | ||||
|               } | ||||
|               $stmt = $pdo->prepare("INSERT INTO `tags_mailbox` (`username`, `tag_name`) VALUES (:username, :tag_name)"); | ||||
|               $stmt->execute(array( | ||||
|                 ':username' => $username, | ||||
|                 ':tag_name' => $tag, | ||||
|               )); | ||||
|             } | ||||
|              | ||||
|             $_SESSION['return'][] = array( | ||||
|               'type' => 'success', | ||||
|               'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), | ||||
| @ -2851,10 +2947,34 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { | ||||
|         break; | ||||
|         case 'mailboxes': | ||||
|           $mailboxes = array(); | ||||
|           if (isset($_data) && !hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $_data)) { | ||||
|             return false; | ||||
|           if (isset($_extra) && is_array($_extra) && isset($_data)) { | ||||
|             // get by domain and tags
 | ||||
|             $tags = is_array($_extra) ? $_extra : array(); | ||||
| 
 | ||||
|             $sql = ""; | ||||
|             foreach ($tags as $key => $tag) { | ||||
|               $sql = $sql."SELECT DISTINCT `username` FROM `tags_mailbox` WHERE `username` LIKE ? AND `tag_name` LIKE ?"; // distinct, avoid duplicates
 | ||||
|               if ($key === array_key_last($tags)) break; | ||||
|               $sql = $sql.' UNION DISTINCT '; // combine querys with union - distinct, avoid duplicates
 | ||||
|             } | ||||
| 
 | ||||
|             // prepend domain to array
 | ||||
|             $params = array(); | ||||
|             foreach ($tags as $key => $val){  | ||||
|               array_push($params, '%'.$_data.'%'); | ||||
|               array_push($params, '%'.$val.'%'); | ||||
|             } | ||||
|             $stmt = $pdo->prepare($sql); | ||||
|             $stmt->execute($params); | ||||
| 
 | ||||
|             $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); | ||||
|             while($row = array_shift($rows)) { | ||||
|               if (hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], explode('@', $row['username'])[1]))  | ||||
|                 $mailboxes[] = $row['username']; | ||||
|             } | ||||
|           } | ||||
|           elseif (isset($_data) && hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $_data)) { | ||||
|             // get by domain
 | ||||
|             $stmt = $pdo->prepare("SELECT `username` FROM `mailbox` WHERE (`kind` = '' OR `kind` = NULL) AND `domain` = :domain"); | ||||
|             $stmt->execute(array( | ||||
|               ':domain' => $_data, | ||||
| @ -3348,20 +3468,46 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { | ||||
|           if ($_SESSION['mailcow_cc_role'] != "admin" && $_SESSION['mailcow_cc_role'] != "domainadmin") { | ||||
|             return false; | ||||
|           } | ||||
|           $stmt = $pdo->prepare("SELECT `domain` FROM `domain`
 | ||||
|             WHERE (`domain` IN ( | ||||
|               SELECT `domain` from `domain_admins` | ||||
|                 WHERE (`active`='1' AND `username` = :username)) | ||||
|               ) | ||||
|               OR 'admin'= :role");
 | ||||
|           $stmt->execute(array( | ||||
|             ':username' => $_SESSION['mailcow_cc_username'], | ||||
|             ':role' => $_SESSION['mailcow_cc_role'], | ||||
|           )); | ||||
|           $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); | ||||
|           while($row = array_shift($rows)) { | ||||
|             $domains[] = $row['domain']; | ||||
| 
 | ||||
|           if (isset($_extra) && is_array($_extra)){ | ||||
|             // get by tags
 | ||||
|             $tags = is_array($_extra) ? $_extra : array(); | ||||
|             // add % as prefix and suffix to every element for relative searching
 | ||||
|             $tags = array_map(function($x){ return '%'.$x.'%'; }, $tags); | ||||
|             $sql = ""; | ||||
|             foreach ($tags as $key => $tag) { | ||||
|               $sql = $sql."SELECT DISTINCT `domain` FROM `tags_domain` WHERE `tag_name` LIKE ?"; // distinct, avoid duplicates
 | ||||
|               if ($key === array_key_last($tags)) break; | ||||
|               $sql = $sql.' UNION DISTINCT '; // combine querys with union - distinct, avoid duplicates
 | ||||
|             } | ||||
|             $stmt = $pdo->prepare($sql); | ||||
|             $stmt->execute($tags); | ||||
| 
 | ||||
|             $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); | ||||
|             while($row = array_shift($rows)) { | ||||
|               if ($_SESSION['mailcow_cc_role'] == "admin") | ||||
|                 $domains[] = $row['domain']; | ||||
|               elseif (hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $row['domain']))  | ||||
|                 $domains[] = $row['domain']; | ||||
|             } | ||||
|           } else { | ||||
|             // get all
 | ||||
|             $stmt = $pdo->prepare("SELECT `domain` FROM `domain`
 | ||||
|               WHERE (`domain` IN ( | ||||
|                 SELECT `domain` from `domain_admins` | ||||
|                   WHERE (`active`='1' AND `username` = :username)) | ||||
|                 ) | ||||
|                 OR 'admin'= :role");
 | ||||
|             $stmt->execute(array( | ||||
|               ':username' => $_SESSION['mailcow_cc_username'], | ||||
|               ':role' => $_SESSION['mailcow_cc_role'], | ||||
|             )); | ||||
|             $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); | ||||
|             while($row = array_shift($rows)) { | ||||
|               $domains[] = $row['domain']; | ||||
|             } | ||||
|           } | ||||
| 
 | ||||
|           return $domains; | ||||
|         break; | ||||
|         case 'domain_details': | ||||
| @ -3478,6 +3624,16 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { | ||||
|               $domain_admins = $stmt->fetch(PDO::FETCH_ASSOC); | ||||
|               (isset($domain_admins['domain_admins'])) ? $domaindata['domain_admins'] = $domain_admins['domain_admins'] : $domaindata['domain_admins'] = "-"; | ||||
|           } | ||||
|           $stmt = $pdo->prepare("SELECT `tag_name`
 | ||||
|             FROM `tags_domain` WHERE `domain`= :domain");
 | ||||
|           $stmt->execute(array( | ||||
|             ':domain' => $_data | ||||
|           )); | ||||
|           $tags = $stmt->fetchAll(PDO::FETCH_ASSOC); | ||||
|           while ($tag = array_shift($tags)) { | ||||
|             $domaindata['tags'][] = $tag['tag_name']; | ||||
|           } | ||||
| 
 | ||||
|           return $domaindata; | ||||
|         break; | ||||
|         case 'mailbox_details': | ||||
| @ -3613,6 +3769,15 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { | ||||
|             } | ||||
|             $mailboxdata['is_relayed'] = $row['backupmx']; | ||||
|           } | ||||
|           $stmt = $pdo->prepare("SELECT `tag_name`
 | ||||
|             FROM `tags_mailbox` WHERE `username`= :username");
 | ||||
|           $stmt->execute(array( | ||||
|             ':username' => $_data | ||||
|           )); | ||||
|           $tags = $stmt->fetchAll(PDO::FETCH_ASSOC); | ||||
|           while ($tag = array_shift($tags)) { | ||||
|             $mailboxdata['tags'][] = $tag['tag_name']; | ||||
|           } | ||||
| 
 | ||||
|           return $mailboxdata; | ||||
|         break; | ||||
| @ -4342,6 +4507,108 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { | ||||
|             ); | ||||
|           } | ||||
|         break; | ||||
|         case 'tags_domain':     | ||||
|           if (!is_array($_data['domain'])) { | ||||
|             $domains = array(); | ||||
|             $domains[] = $_data['domain']; | ||||
|           } | ||||
|           else { | ||||
|             $domains = $_data['domain']; | ||||
|           } | ||||
|           $tags = $_data['tags']; | ||||
|           if (!is_array($tags)) $tags = array(); | ||||
| 
 | ||||
|            | ||||
|           if ($_SESSION['mailcow_cc_role'] != "admin") { | ||||
|             $_SESSION['return'][] = array( | ||||
|               'type' => 'danger', | ||||
|               'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), | ||||
|               'msg' => 'access_denied' | ||||
|             ); | ||||
|             return false; | ||||
|           } | ||||
| 
 | ||||
|           $wasModified = false; | ||||
|           foreach ($domains as $domain) {             | ||||
|             if (!is_valid_domain_name($domain)) { | ||||
|               $_SESSION['return'][] = array( | ||||
|                 'type' => 'danger', | ||||
|                 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), | ||||
|                 'msg' => 'domain_invalid' | ||||
|               ); | ||||
|               continue; | ||||
|             } | ||||
| 
 | ||||
|             foreach($tags as $tag){ | ||||
|               // delete tag
 | ||||
|               $wasModified = true; | ||||
|               $stmt = $pdo->prepare("DELETE FROM `tags_domain` WHERE `domain` = :domain AND `tag_name` = :tag_name"); | ||||
|               $stmt->execute(array( | ||||
|                 ':domain' => $domain, | ||||
|                 ':tag_name' => $tag, | ||||
|               )); | ||||
|             } | ||||
|           } | ||||
| 
 | ||||
|           if (!$wasModified) return false; | ||||
|           $_SESSION['return'][] = array( | ||||
|             'type' => 'success', | ||||
|             'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), | ||||
|             'msg' => array('domain_modified', $domain) | ||||
|           ); | ||||
|         break; | ||||
|         case 'tags_mailbox': | ||||
|           if (!is_array($_data['username'])) { | ||||
|             $usernames = array(); | ||||
|             $usernames[] = $_data['username']; | ||||
|           } | ||||
|           else { | ||||
|             $usernames = $_data['username']; | ||||
|           } | ||||
|           $tags = $_data['tags']; | ||||
|           if (!is_array($tags)) $tags = array(); | ||||
| 
 | ||||
|           $wasModified = false; | ||||
|           foreach ($usernames as $username) { | ||||
|             if (!filter_var($username, FILTER_VALIDATE_EMAIL)) { | ||||
|               $_SESSION['return'][] = array( | ||||
|                 'type' => 'danger', | ||||
|                 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), | ||||
|                 'msg' => 'email invalid' | ||||
|               ); | ||||
|               continue; | ||||
|             } | ||||
| 
 | ||||
|             $is_now = mailbox('get', 'mailbox_details', $username); | ||||
|             $domain     = $is_now['domain']; | ||||
|             if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $domain)) { | ||||
|               $_SESSION['return'][] = array( | ||||
|                 'type' => 'danger', | ||||
|                 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), | ||||
|                 'msg' => 'access_denied' | ||||
|               ); | ||||
|               continue; | ||||
|             } | ||||
| 
 | ||||
|             // delete tags
 | ||||
|             foreach($tags as $tag){ | ||||
|               $wasModified = true; | ||||
|                | ||||
|               $stmt = $pdo->prepare("DELETE FROM `tags_mailbox` WHERE `username` = :username AND `tag_name` = :tag_name"); | ||||
|               $stmt->execute(array( | ||||
|                 ':username' => $username, | ||||
|                 ':tag_name' => $tag, | ||||
|               )); | ||||
|             } | ||||
|           } | ||||
| 
 | ||||
|           if (!$wasModified) return false; | ||||
|           $_SESSION['return'][] = array( | ||||
|             'type' => 'success', | ||||
|             'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), | ||||
|             'msg' => array('mailbox_modified', $username) | ||||
|           ); | ||||
|         break; | ||||
|       } | ||||
|     break; | ||||
|   } | ||||
|  | ||||
| @ -3,7 +3,7 @@ function init_db_schema() { | ||||
|   try { | ||||
|     global $pdo; | ||||
| 
 | ||||
|     $db_version = "22032022_1330"; | ||||
|     $db_version = "20052022_0938"; | ||||
| 
 | ||||
|     $stmt = $pdo->query("SHOW TABLES LIKE 'versions'"); | ||||
|     $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); | ||||
| @ -23,35 +23,35 @@ function init_db_schema() { | ||||
|     } | ||||
| 
 | ||||
|     $views = array( | ||||
|     "grouped_mail_aliases" => "CREATE VIEW grouped_mail_aliases (username, aliases) AS
 | ||||
|       SELECT goto, IFNULL(GROUP_CONCAT(address ORDER BY address SEPARATOR ' '), '') AS address FROM alias | ||||
|       WHERE address!=goto | ||||
|       AND active = '1' | ||||
|       AND sogo_visible = '1' | ||||
|       AND address NOT LIKE '@%' | ||||
|       GROUP BY goto;",
 | ||||
|     // START
 | ||||
|     // 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;",
 | ||||
|     // END 
 | ||||
|     "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 | ||||
|       GROUP BY username;",
 | ||||
|     "sieve_before" => "CREATE VIEW sieve_before (id, username, script_name, script_data) AS
 | ||||
|       SELECT md5(script_data), username, script_name, script_data FROM sieve_filters | ||||
|       WHERE filter_type = 'prefilter';",
 | ||||
|     "sieve_after" => "CREATE VIEW sieve_after (id, username, script_name, script_data) AS
 | ||||
|       SELECT md5(script_data), username, script_name, script_data FROM sieve_filters | ||||
|       WHERE filter_type = 'postfilter';" | ||||
|       "grouped_mail_aliases" => "CREATE VIEW grouped_mail_aliases (username, aliases) AS
 | ||||
|         SELECT goto, IFNULL(GROUP_CONCAT(address ORDER BY address SEPARATOR ' '), '') AS address FROM alias | ||||
|         WHERE address!=goto | ||||
|         AND active = '1' | ||||
|         AND sogo_visible = '1' | ||||
|         AND address NOT LIKE '@%' | ||||
|         GROUP BY goto;",
 | ||||
|       // START
 | ||||
|       // 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;",
 | ||||
|       // END 
 | ||||
|       "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 | ||||
|         GROUP BY username;",
 | ||||
|       "sieve_before" => "CREATE VIEW sieve_before (id, username, script_name, script_data) AS
 | ||||
|         SELECT md5(script_data), username, script_name, script_data FROM sieve_filters | ||||
|         WHERE filter_type = 'prefilter';",
 | ||||
|       "sieve_after" => "CREATE VIEW sieve_after (id, username, script_name, script_data) AS
 | ||||
|         SELECT md5(script_data), username, script_name, script_data FROM sieve_filters | ||||
|         WHERE filter_type = 'postfilter';" | ||||
|     ); | ||||
| 
 | ||||
|     $tables = array( | ||||
| @ -251,6 +251,26 @@ function init_db_schema() { | ||||
|         ), | ||||
|         "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" | ||||
|       ), | ||||
|       "tags_domain" => array( | ||||
|         "cols" => array( | ||||
|           "tag_name" => "VARCHAR(255) NOT NULL", | ||||
|           "domain" => "VARCHAR(255) NOT NULL" | ||||
|         ), | ||||
|         "keys" => array( | ||||
|           "fkey" => array( | ||||
|             "fk_tags_domain" => array( | ||||
|               "col" => "domain", | ||||
|               "ref" => "domain.domain", | ||||
|               "delete" => "CASCADE", | ||||
|               "update" => "NO ACTION" | ||||
|             ) | ||||
|           ), | ||||
|           "unique" => array( | ||||
|             "tag_name" => array("tag_name", "domain") | ||||
|           ) | ||||
|         ), | ||||
|         "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" | ||||
|       ), | ||||
|       "tls_policy_override" => array( | ||||
|         "cols" => array( | ||||
|           "id" => "INT NOT NULL AUTO_INCREMENT", | ||||
| @ -325,6 +345,26 @@ function init_db_schema() { | ||||
|         ), | ||||
|         "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" | ||||
|       ), | ||||
|       "tags_mailbox" => array( | ||||
|         "cols" => array( | ||||
|           "tag_name" => "VARCHAR(255) NOT NULL", | ||||
|           "username" => "VARCHAR(255) NOT NULL" | ||||
|         ), | ||||
|         "keys" => array( | ||||
|           "fkey" => array( | ||||
|             "fk_tags_mailbox" => array( | ||||
|               "col" => "username", | ||||
|               "ref" => "mailbox.username", | ||||
|               "delete" => "CASCADE", | ||||
|               "update" => "NO ACTION" | ||||
|             ) | ||||
|           ), | ||||
|           "unique" => array( | ||||
|             "tag_name" => array("tag_name", "username") | ||||
|           ) | ||||
|         ), | ||||
|         "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" | ||||
|       ), | ||||
|       "sieve_filters" => array( | ||||
|         "cols" => array( | ||||
|           "id" => "INT NOT NULL AUTO_INCREMENT", | ||||
| @ -1188,7 +1228,7 @@ function init_db_schema() { | ||||
|     } | ||||
|      | ||||
|     // Mitigate imapsync pipemess issue
 | ||||
|     $pdo->query("UPDATE `imapsync` SET `custom_params` = '' WHERE `custom_params` LIKE '%pipemess%';"); | ||||
|     $pdo->query("UPDATE `imapsync` SET `custom_params` = '' WHERE `custom_params` LIKE '%pipemess%' OR `custom_params` LIKE '%pipemes%';"); | ||||
|      | ||||
|     // Migrate webauthn tfa
 | ||||
|     $stmt = $pdo->query("ALTER TABLE `tfa` MODIFY COLUMN `authmech` ENUM('yubi_otp', 'u2f', 'hotp', 'totp', 'webauthn')"); | ||||
|  | ||||
| @ -100,6 +100,7 @@ $AVAILABLE_LANGUAGES = array( | ||||
|   'ru' => 'Pусский (Russian)', | ||||
|   'sk' => 'Slovenčina (Slovak)', | ||||
|   'sv' => 'Svenska (Swedish)', | ||||
|   'uk' => 'Українська (Ukrainian)', | ||||
|   'zh' => '中文 (Chinese)' | ||||
| ); | ||||
| 
 | ||||
| @ -148,6 +149,9 @@ $ACCESS_TOKEN_LIFETIME = 86400; | ||||
| // Logout from mailcow after first OAuth2 session profile request
 | ||||
| $OAUTH2_FORGET_SESSION_AFTER_LOGIN = false; | ||||
| 
 | ||||
| // Set a limit for mailbox and domain tagging
 | ||||
| $TAGGING_LIMIT = 25; | ||||
| 
 | ||||
| // MAILBOX_DEFAULT_ATTRIBUTES define default attributes for new mailboxes
 | ||||
| // These settings will not change existing mailboxes
 | ||||
| 
 | ||||
|  | ||||
| @ -156,6 +156,12 @@ $(document).ready(function() { | ||||
|       }); | ||||
|       if (!invalid) { | ||||
|         var attr_to_merge = $(this).closest("form").serializeObject(); | ||||
|         // parse possible JSON Strings
 | ||||
|         for (var [key, value] of Object.entries(attr_to_merge)) { | ||||
|           try { | ||||
|             attr_to_merge[key] = JSON.parse(attr_to_merge[key]); | ||||
|           } catch {} | ||||
|         } | ||||
|         var api_attr = $.extend(api_attr, attr_to_merge) | ||||
|       } else { | ||||
|         return false; | ||||
| @ -263,6 +269,12 @@ $(document).ready(function() { | ||||
|       }); | ||||
|       if (!invalid) { | ||||
|         var attr_to_merge = $(this).closest("form").serializeObject(); | ||||
|         // parse possible JSON Strings
 | ||||
|         for (var [key, value] of Object.entries(attr_to_merge)) { | ||||
|           try { | ||||
|             attr_to_merge[key] = JSON.parse(attr_to_merge[key]); | ||||
|           } catch {} | ||||
|         } | ||||
|         var api_attr = $.extend(api_attr, attr_to_merge) | ||||
|       } else { | ||||
|         return false; | ||||
| @ -329,6 +341,7 @@ $(document).ready(function() { | ||||
|       multi_data[id].splice($.inArray($(this).data('item'), multi_data[id]), 1); | ||||
|       multi_data[id].push($(this).data('item')); | ||||
|     } | ||||
| 
 | ||||
|     if (typeof $(this).data('text') !== 'undefined') { | ||||
|       $("#DeleteText").empty(); | ||||
|       $("#DeleteText").text($(this).data('text')); | ||||
| @ -340,9 +353,9 @@ $(document).ready(function() { | ||||
|       $("#ItemsToDelete").empty(); | ||||
|       for (var i in data_array) { | ||||
|         data_array[i] = decodeURIComponent(data_array[i]); | ||||
|         $("#ItemsToDelete").append("<li>" + data_array[i] + "</li>"); | ||||
|         $("#ItemsToDelete").append("<li>" + escapeHtml(data_array[i]) + "</li>"); | ||||
|       } | ||||
|     }) | ||||
|     }); | ||||
|     $('#ConfirmDeleteModal').modal({ | ||||
|         backdrop: 'static', | ||||
|         keyboard: false | ||||
|  | ||||
| @ -48,7 +48,7 @@ $(document).ready(function() { | ||||
|       $(div).animate({ left: ((iter%2==0 ? distance : distance*-1))}, interval); | ||||
|     } | ||||
|     $(div).animate({ left: 0},interval); | ||||
|   } | ||||
|   }  | ||||
| 
 | ||||
|   // form cache
 | ||||
|   $('[data-cached-form="true"]').formcache({key: $(this).data('id')}); | ||||
| @ -273,4 +273,51 @@ $(document).ready(function() { | ||||
|         } | ||||
|       } | ||||
|   }); | ||||
| 
 | ||||
|   // tag boxes
 | ||||
|   $('.tag-box .tag-add').click(function(){ | ||||
|     addTag(this); | ||||
|   }); | ||||
|   $(".tag-box .tag-input").keydown(function (e) { | ||||
|     if (e.which == 13){ | ||||
|       e.preventDefault(); | ||||
|       addTag(this); | ||||
|     }  | ||||
|   }); | ||||
|   function addTag(tagAddElem){ | ||||
|     var tagboxElem = $(tagAddElem).parent(); | ||||
|     var tagInputElem = $(tagboxElem).find(".tag-input")[0]; | ||||
|     var tagValuesElem = $(tagboxElem).find(".tag-values")[0]; | ||||
| 
 | ||||
|     var tag = escapeHtml($(tagInputElem).val()); | ||||
|     if (!tag) return; | ||||
|     var value_tags = []; | ||||
|     try { | ||||
|       value_tags = JSON.parse($(tagValuesElem).val()); | ||||
|     } catch {} | ||||
|     if (!Array.isArray(value_tags)) value_tags = []; | ||||
|     if (value_tags.includes(tag)) return; | ||||
| 
 | ||||
|     $('<span class="badge badge-primary tag-badge btn-badge"><i class="bi bi-tag-fill"></i> ' + tag + '</span>').insertBefore('.tag-input').click(function(){ | ||||
|       var del_tag = unescapeHtml($(this).text()); | ||||
|       var del_tags = []; | ||||
|       try { | ||||
|         del_tags = JSON.parse($(tagValuesElem).val()); | ||||
|       } catch {} | ||||
|       if (Array.isArray(del_tags)){ | ||||
|         del_tags.splice(del_tags.indexOf(del_tag), 1); | ||||
|         $(tagValuesElem).val(JSON.stringify(del_tags)); | ||||
|       } | ||||
|       $(this).remove(); | ||||
|     }); | ||||
| 
 | ||||
|     value_tags.push($(tagInputElem).val()); | ||||
|     $(tagValuesElem).val(JSON.stringify(value_tags)); | ||||
|     $(tagInputElem).val(''); | ||||
|   } | ||||
| }); | ||||
| 
 | ||||
| 
 | ||||
| // http://stackoverflow.com/questions/24816/escaping-html-strings-with-jquery
 | ||||
| function escapeHtml(n){var entityMap={"&":"&","<":"<",">":">",'"':""","'":"'","/":"/","`":"`","=":"="}; return String(n).replace(/[&<>"'`=\/]/g,function(n){return entityMap[n]})} | ||||
| function unescapeHtml(t){var n={"&":"&","<":"<",">":">",""":'"',"'":"'","/":"/","`":"`","=":"="};return String(t).replace(/&|<|>|"|'|/|`|=/g,function(t){return n[t]})} | ||||
|  | ||||
| @ -99,37 +99,6 @@ $(document).ready(function() { | ||||
|   }); | ||||
|   auto_fill_quota($('#addSelectDomain').val()); | ||||
| 
 | ||||
|   // Read bcc local dests
 | ||||
|   // Using ajax to not be a blocking moo
 | ||||
|   $.get("/api/v1/get/bcc-destination-options", function(data){ | ||||
|     // Domains
 | ||||
|     var optgroup = "<optgroup label='" + lang.domains + "'>"; | ||||
|     $.each(data.domains, function(index, domain){ | ||||
|       optgroup += "<option value='" + domain + "'>" + domain + "</option>" | ||||
|     }); | ||||
|     optgroup += "</optgroup>" | ||||
|     $('#bcc-local-dest').append(optgroup); | ||||
|     // Alias domains
 | ||||
|     var optgroup = "<optgroup label='" + lang.domain_aliases + "'>"; | ||||
|     $.each(data.alias_domains, function(index, alias_domain){ | ||||
|       optgroup += "<option value='" + alias_domain + "'>" + alias_domain + "</option>" | ||||
|     }); | ||||
|     optgroup += "</optgroup>" | ||||
|     $('#bcc-local-dest').append(optgroup); | ||||
|     // Mailboxes and aliases
 | ||||
|     $.each(data.mailboxes, function(mailbox, aliases){ | ||||
|       var optgroup = "<optgroup label='" + mailbox + "'>"; | ||||
|       $.each(aliases, function(index, alias){ | ||||
|         optgroup += "<option value='" + alias + "'>" + alias + "</option>" | ||||
|       }); | ||||
|       optgroup += "</optgroup>" | ||||
|       $('#bcc-local-dest').append(optgroup); | ||||
|     }); | ||||
|     // Finish
 | ||||
|     $('#bcc-local-dest').find('option:selected').remove(); | ||||
|     $('#bcc-local-dest').selectpicker('refresh'); | ||||
|   }); | ||||
| 
 | ||||
|   $(".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) { | ||||
| @ -236,9 +205,6 @@ $(document).ready(function() { | ||||
| 
 | ||||
| }); | ||||
| 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 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 unix_time_format(i){return""==i?'<i class="bi bi-x-lg"></i>':new Date(i?1e3*i:0).toLocaleDateString(void 0,{year:"numeric",month:"2-digit",day:"2-digit",hour:"2-digit",minute:"2-digit",second:"2-digit"})} | ||||
| @ -293,6 +259,7 @@ jQuery(function($){ | ||||
|         {"name":"rl","title":"RL","breakpoints":"xs sm md lg","style":{"min-width":"100px","width":"100px"}}, | ||||
|         {"name":"backupmx","filterable": false,"style":{"min-width":"120px","width":"120px"},"title":lang.backup_mx,"breakpoints":"xs sm md lg","formatter": function(value){return 1==value?'<i class="bi bi-check-lg"></i>':0==value&&'<i class="bi bi-x-lg"></i>';}}, | ||||
|         {"name":"domain_admins","title":lang.domain_admins,"style":{"word-break":"break-all","min-width":"200px"},"breakpoints":"xs sm md lg","filterable":(role == "admin"),"visible":(role == "admin")}, | ||||
|         {"name":"tags","title":"Tags","style":{},"breakpoints":"xs sm md lg"}, | ||||
|         {"name":"active","filterable": false,"style":{"min-width":"80px","width":"80px"},"title":lang.active,"formatter": function(value){return 1==value?'<i class="bi bi-check-lg"></i>':0==value&&'<i class="bi bi-x-lg"></i>';}}, | ||||
|         {"name":"action","filterable": false,"sortable": false,"style":{"text-align":"right","min-width":"240px","width":"240px"},"type":"html","title":lang.action,"breakpoints":"xs sm md"} | ||||
|       ], | ||||
| @ -330,6 +297,13 @@ jQuery(function($){ | ||||
|               '<a href="#dnsInfoModal" class="btn btn-xs btn-xs-half btn-info" data-toggle="modal" data-domain="' + encodeURIComponent(item.domain_name) + '"><i class="bi bi-globe2"></i> DNS</a></div>'; | ||||
|             } | ||||
| 
 | ||||
|             if (Array.isArray(item.tags)){ | ||||
|               var tags = ''; | ||||
|               for (var i = 0; i < item.tags.length; i++) | ||||
|                 tags += '<span class="badge badge-primary tag-badge"><i class="bi bi-tag-fill"></i> ' + escapeHtml(item.tags[i]) + '</span>'; | ||||
|               item.tags = tags; | ||||
|             } | ||||
| 
 | ||||
|             if (item.backupmx == 1) { | ||||
|               if (item.relay_unknown_only == 1) { | ||||
|                 item.domain_name = '<div class="label label-info">Relay Non-Local</div> ' + item.domain_name; | ||||
| @ -418,6 +392,7 @@ jQuery(function($){ | ||||
|         }, | ||||
|         {"name":"messages","filterable": false,"title":lang.msg_num,"breakpoints":"xs sm md"}, | ||||
|         /* {"name":"rl","title":"RL","breakpoints":"all","style":{"width":"125px"}}, */ | ||||
|         {"name":"tags","title":"Tags","style":{},"breakpoints":"xs sm md lg"}, | ||||
|         {"name":"active","filterable": false,"style":{"min-width":"80px","width":"80px"},"title":lang.active,"formatter": function(value){return 1==value?'<i class="bi bi-check-lg"></i>':(0==value?'<i class="bi bi-x-lg"></i>':2==value&&'—');}}, | ||||
|         {"name":"action","filterable": false,"sortable": false,"style":{"min-width":"290px","text-align":"right"},"type":"html","title":lang.action,"breakpoints":"xs sm md"} | ||||
|       ], | ||||
| @ -497,6 +472,13 @@ jQuery(function($){ | ||||
|               '<div class="progress-bar-mailbox 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); | ||||
|              | ||||
|             if (Array.isArray(item.tags)){ | ||||
|               var tags = ''; | ||||
|               for (var i = 0; i < item.tags.length; i++) | ||||
|                 tags += '<span class="badge badge-primary tag-badge"><i class="bi bi-tag-fill"></i> ' + escapeHtml(item.tags[i]) + '</span>'; | ||||
|               item.tags = tags; | ||||
|             } | ||||
|           }); | ||||
|         } | ||||
|       }), | ||||
| @ -571,6 +553,7 @@ jQuery(function($){ | ||||
|               '</div>'; | ||||
|             item.chkbox = '<input type="checkbox" data-id="resource" name="multi_select" value="' + encodeURIComponent(item.name) + '" />'; | ||||
|             item.name = escapeHtml(item.name); | ||||
|             item.description = escapeHtml(item.description); | ||||
|           }); | ||||
|         } | ||||
|       }), | ||||
| @ -610,6 +593,37 @@ jQuery(function($){ | ||||
|     }); | ||||
|   } | ||||
|   function draw_bcc_table() { | ||||
|   // Read bcc local dests
 | ||||
|   // Using ajax to not be a blocking moo
 | ||||
|   $.get("/api/v1/get/bcc-destination-options", function(data){ | ||||
|     // Domains
 | ||||
|     var optgroup = "<optgroup label='" + lang.domains + "'>"; | ||||
|     $.each(data.domains, function(index, domain){ | ||||
|       optgroup += "<option value='" + domain + "'>" + domain + "</option>" | ||||
|     }); | ||||
|     optgroup += "</optgroup>" | ||||
|     $('#bcc-local-dest').append(optgroup); | ||||
|     // Alias domains
 | ||||
|     var optgroup = "<optgroup label='" + lang.domain_aliases + "'>"; | ||||
|     $.each(data.alias_domains, function(index, alias_domain){ | ||||
|       optgroup += "<option value='" + alias_domain + "'>" + alias_domain + "</option>" | ||||
|     }); | ||||
|     optgroup += "</optgroup>" | ||||
|     $('#bcc-local-dest').append(optgroup); | ||||
|     // Mailboxes and aliases
 | ||||
|     $.each(data.mailboxes, function(mailbox, aliases){ | ||||
|       var optgroup = "<optgroup label='" + mailbox + "'>"; | ||||
|       $.each(aliases, function(index, alias){ | ||||
|         optgroup += "<option value='" + alias + "'>" + alias + "</option>" | ||||
|       }); | ||||
|       optgroup += "</optgroup>" | ||||
|       $('#bcc-local-dest').append(optgroup); | ||||
|     }); | ||||
|     // Finish
 | ||||
|     $('#bcc-local-dest').find('option:selected').remove(); | ||||
|     $('#bcc-local-dest').selectpicker('refresh'); | ||||
|   }); | ||||
| 
 | ||||
|     ft_bcc_table = FooTable.init('#bcc_table', { | ||||
|       "columns": [ | ||||
|         {"name":"chkbox","title":"","style":{"min-width":"60px","width":"60px"},"filterable": false,"sortable": false,"type":"html"}, | ||||
| @ -1009,7 +1023,7 @@ jQuery(function($){ | ||||
|             if (!item.exclude > 0) { | ||||
|               item.exclude = '-'; | ||||
|             } else { | ||||
|               item.exclude  = '<code>' + item.exclude + '</code>'; | ||||
|               item.exclude  = '<code>' + escapeHtml(item.exclude) + '</code>'; | ||||
|             } | ||||
|             item.server_w_port = escapeHtml(item.user1) + '@' + item.host1 + ':' + item.port1; | ||||
|             item.action = '<div class="btn-group footable-actions">' + | ||||
| @ -1147,15 +1161,33 @@ jQuery(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(); | ||||
|   // detect element visibility changes
 | ||||
|   function onVisible(element, callback) { | ||||
|     $(element).ready(function() { | ||||
|       element_object = document.querySelector(element) | ||||
|       new IntersectionObserver((entries, observer) => { | ||||
|         entries.forEach(entry => { | ||||
|           if(entry.intersectionRatio > 0) { | ||||
|             callback(element_object); | ||||
|             observer.disconnect(); | ||||
|           } | ||||
|         }); | ||||
|       }).observe(element_object); | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   // Load only if the tab is visible
 | ||||
|   onVisible("[id^=tab-domains]", () => draw_domain_table()); | ||||
|   onVisible("[id^=tab-mailboxes]", () => draw_mailbox_table()); | ||||
|   onVisible("[id^=tab-resources]", () => draw_resource_table()); | ||||
|   onVisible("[id^=tab-mbox-aliases]", () => draw_alias_table()); | ||||
|   onVisible("[id^=tab-domain-aliases]", () => draw_aliasdomain_table()); | ||||
|   onVisible("[id^=tab-syncjobs]", () => draw_sync_job_table()); | ||||
|   onVisible("[id^=tab-filters]", () => draw_filter_table()); | ||||
|   onVisible("[id^=tab-bcc]", () => { | ||||
|     draw_bcc_table(); | ||||
|     draw_recipient_map_table(); | ||||
|   }); | ||||
|   onVisible("[id^=tab-tls-policy]", () => draw_tls_policy_table()); | ||||
| 
 | ||||
| }); | ||||
|  | ||||
| @ -14,17 +14,20 @@ function api_log($_data) { | ||||
|     if ($data == 'csrf_token') { | ||||
|       continue; | ||||
|     } | ||||
|     if ($value = json_decode($value, true)) { | ||||
|       unset($value["csrf_token"]); | ||||
| 
 | ||||
|     $value = json_decode($value, true);      | ||||
|     if ($value) { | ||||
|       if (is_array($value)) unset($value["csrf_token"]); | ||||
|       foreach ($value as $key => &$val) { | ||||
|         if(preg_match("/pass/i", $key)) { | ||||
|           $val = '*'; | ||||
|         } | ||||
|       } | ||||
|       $value = json_encode($value); | ||||
|       $value = json_encode($value);   | ||||
|     } | ||||
|     $data_var[] = $data . "='" . $value . "'"; | ||||
|   } | ||||
| 
 | ||||
|   try { | ||||
|     $log_line = array( | ||||
|       'time' => time(), | ||||
| @ -41,7 +44,7 @@ function api_log($_data) { | ||||
|       'msg' => 'Redis: '.$e | ||||
|     ); | ||||
|     return false; | ||||
|   } | ||||
|   }      | ||||
| } | ||||
| 
 | ||||
| if (isset($_GET['query'])) { | ||||
| @ -82,10 +85,10 @@ if (isset($_GET['query'])) { | ||||
|     if ($action == 'delete') { | ||||
|       $_POST['items'] = $request; | ||||
|     } | ||||
| 
 | ||||
|   } | ||||
|   api_log($_POST); | ||||
| 
 | ||||
| 
 | ||||
|   $request_incomplete = json_encode(array( | ||||
|     'type' => 'error', | ||||
|     'msg' => 'Cannot find attributes in post data' | ||||
| @ -486,7 +489,12 @@ if (isset($_GET['query'])) { | ||||
|           case "domain": | ||||
|             switch ($object) { | ||||
|               case "all": | ||||
|                 $domains = mailbox('get', 'domains'); | ||||
|                 $tags = null; | ||||
|                 if (isset($_GET['tags']) && $_GET['tags'] != '')  | ||||
|                   $tags = explode(',', $_GET['tags']); | ||||
| 
 | ||||
|                 $domains = mailbox('get', 'domains', null, $tags); | ||||
| 
 | ||||
|                 if (!empty($domains)) { | ||||
|                   foreach ($domains as $domain) { | ||||
|                     if ($details = mailbox('get', 'domain_details', $domain)) { | ||||
| @ -952,23 +960,20 @@ if (isset($_GET['query'])) { | ||||
|             switch ($object) { | ||||
|               case "all": | ||||
|               case "reduced": | ||||
|                 if (empty($extra)) { | ||||
|                   $domains = mailbox('get', 'domains'); | ||||
|                 } | ||||
|                 else { | ||||
|                   $domains = explode(',', $extra); | ||||
|                 } | ||||
|                 $tags = null; | ||||
|                 if (isset($_GET['tags']) && $_GET['tags'] != '')  | ||||
|                   $tags = explode(',', $_GET['tags']); | ||||
| 
 | ||||
|                 if (empty($extra)) $domains = mailbox('get', 'domains'); | ||||
|                 else $domains = explode(',', $extra); | ||||
| 
 | ||||
|                 if (!empty($domains)) { | ||||
|                   foreach ($domains as $domain) { | ||||
|                     $mailboxes = mailbox('get', 'mailboxes', $domain); | ||||
|                     $mailboxes = mailbox('get', 'mailboxes', $domain, $tags); | ||||
|                     if (!empty($mailboxes)) { | ||||
|                       foreach ($mailboxes as $mailbox) { | ||||
|                         if ($details = mailbox('get', 'mailbox_details', $mailbox, $object)) { | ||||
|                           $data[] = $details; | ||||
|                         } | ||||
|                         else { | ||||
|                           continue; | ||||
|                         } | ||||
|                         if ($details = mailbox('get', 'mailbox_details', $mailbox, $object)) $data[] = $details; | ||||
|                         else continue; | ||||
|                       } | ||||
|                     } | ||||
|                   } | ||||
| @ -980,8 +985,23 @@ if (isset($_GET['query'])) { | ||||
|               break; | ||||
| 
 | ||||
|               default: | ||||
|                 $data = mailbox('get', 'mailbox_details', $object); | ||||
|                 process_get_return($data); | ||||
|                 $tags = null; | ||||
|                 if (isset($_GET['tags']) && $_GET['tags'] != '')  | ||||
|                   $tags = explode(',', $_GET['tags']); | ||||
| 
 | ||||
|                 if ($tags === null) { | ||||
|                   $data = mailbox('get', 'mailbox_details', $object); | ||||
|                   process_get_return($data); | ||||
|                 } else { | ||||
|                   $mailboxes = mailbox('get', 'mailboxes', $object, $tags); | ||||
|                   if (is_array($mailboxes)) { | ||||
|                     foreach ($mailboxes as $mailbox) { | ||||
|                       if ($details = mailbox('get', 'mailbox_details', $mailbox))  | ||||
|                         $data[] = $details; | ||||
|                     } | ||||
|                   } | ||||
|                   process_get_return($data, false); | ||||
|                 } | ||||
|               break; | ||||
|             } | ||||
|           break; | ||||
| @ -1472,6 +1492,11 @@ if (isset($_GET['query'])) { | ||||
|                   'solr_documents' => $solr_documents | ||||
|                 )); | ||||
|               break; | ||||
|               case "version": | ||||
|                 echo json_encode(array( | ||||
|                   'version' => $GLOBALS['MAILCOW_GIT_VERSION'] | ||||
|                 )); | ||||
|               break; | ||||
|               } | ||||
|             } | ||||
|           break; | ||||
| @ -1575,13 +1600,25 @@ if (isset($_GET['query'])) { | ||||
|           process_delete_return(dkim('delete', array('domains' => $items))); | ||||
|         break; | ||||
|         case "domain": | ||||
|           process_delete_return(mailbox('delete', 'domain', array('domain' => $items))); | ||||
|           switch ($object){ | ||||
|             case "tag": | ||||
|               process_delete_return(mailbox('delete', 'tags_domain', array('tags' => $items, 'domain' => $extra))); | ||||
|             break; | ||||
|             default: | ||||
|               process_delete_return(mailbox('delete', 'domain', array('domain' => $items))); | ||||
|           } | ||||
|         break; | ||||
|         case "alias-domain": | ||||
|           process_delete_return(mailbox('delete', 'alias_domain', array('alias_domain' => $items))); | ||||
|         break; | ||||
|         case "mailbox": | ||||
|           process_delete_return(mailbox('delete', 'mailbox', array('username' => $items))); | ||||
|           switch ($object){ | ||||
|             case "tag": | ||||
|               process_delete_return(mailbox('delete', 'tags_mailbox', array('tags' => $items, 'username' => $extra))); | ||||
|             break; | ||||
|             default: | ||||
|               process_delete_return(mailbox('delete', 'mailbox', array('username' => $items))); | ||||
|           } | ||||
|         break; | ||||
|         case "resource": | ||||
|           process_delete_return(mailbox('delete', 'resource', array('name' => $items))); | ||||
|  | ||||
| @ -106,7 +106,8 @@ | ||||
|         "timeout2": "Timeout für Verbindung zum lokalen Host", | ||||
|         "username": "Benutzername", | ||||
|         "validate": "Validieren", | ||||
|         "validation_success": "Erfolgreich validiert" | ||||
|         "validation_success": "Erfolgreich validiert", | ||||
|         "tags": "Tags" | ||||
|     }, | ||||
|     "admin": { | ||||
|         "access": "Zugang", | ||||
|  | ||||
| @ -99,6 +99,7 @@ | ||||
|         "subscribeall": "Subscribe all folders", | ||||
|         "syncjob": "Add sync job", | ||||
|         "syncjob_hint": "Be aware that passwords need to be saved plain-text!", | ||||
|         "tags": "Tags", | ||||
|         "target_address": "Goto addresses", | ||||
|         "target_address_info": "<small>Full email address/es (comma-separated).</small>", | ||||
|         "target_domain": "Target domain", | ||||
|  | ||||
| @ -2,8 +2,8 @@ | ||||
|     "acl": { | ||||
|         "alias_domains": "Aggiungi alias di dominio", | ||||
|         "app_passwds": "Gestisci le password delle app", | ||||
|         "bcc_maps": "BCC maps", | ||||
|         "delimiter_action": "Delimiter action", | ||||
|         "bcc_maps": "Mappe CCN", | ||||
|         "delimiter_action": "Azione delimitatrice", | ||||
|         "domain_desc": "Modifica la descrizione del dominio", | ||||
|         "domain_relayhost": "Modifica relayhost per un dominio", | ||||
|         "eas_reset": "Ripristina i dispositivi EAS", | ||||
| @ -106,7 +106,8 @@ | ||||
|         "validate": "Convalida", | ||||
|         "validation_success": "Convalidato con successo", | ||||
|         "bcc_dest_format": "Il destinatario in copia nascosta deve essere un singolo indirizzo email.<br>Se si vuole spedire una copia del messaggio a più destinatari, bisogna creare un alias ed utilizzarlo per questa opzione.", | ||||
|         "app_passwd_protocols": "Protocolli consentiti per la password dell'app" | ||||
|         "app_passwd_protocols": "Protocolli consentiti per la password dell'app", | ||||
|         "tags": "Tag" | ||||
|     }, | ||||
|     "admin": { | ||||
|         "access": "Accedi", | ||||
| @ -983,7 +984,7 @@ | ||||
|         "enter_qr_code": "Il codice TOTP se il tuo dispositivo non è in grado di acquisire i codici QR", | ||||
|         "error_code": "Codice di errore", | ||||
|         "init_webauthn": "Inizializzazione, attendere prego...", | ||||
|         "key_id": "Identificatore per il tuo YubiKey", | ||||
|         "key_id": "Identificatore per il tuo dispositivo", | ||||
|         "key_id_totp": "Identificatore per la tua chiave", | ||||
|         "none": "Disattivato", | ||||
|         "reload_retry": "- (ricaricare la pagina se l'errore persiste)", | ||||
| @ -997,7 +998,9 @@ | ||||
|         "waiting_usb_auth": "<i>In attesa del device USB...</i><br /><br />Tocca ora il pulsante sul dispositivo WebAuthn USB.", | ||||
|         "waiting_usb_register": "<i>In attesa del device USB...</i><br /><br />Inserisci la tua password qui sopra e conferma la tua registrazione WebAuthn toccando il pulsante del dispositivo WebAuthn USB.", | ||||
|         "yubi_otp": "Autenticazione Yubico OTP", | ||||
|         "tfa_token_invalid": "Token TFA non valido" | ||||
|         "tfa_token_invalid": "Token TFA non valido", | ||||
|         "u2f_deprecated": "Sembra che la tua chiave sia stata registrata utilizzando il metodo U2F deprecato. Disattiveremo Two-Factor-Authenticaiton per te e cancelleremo la tua chiave.", | ||||
|         "u2f_deprecated_important": "Registra la tua chiave nel pannello di amministrazione con il nuovo metodo WebAuthn." | ||||
|     }, | ||||
|     "user": { | ||||
|         "action": "Azione", | ||||
|  | ||||
| @ -105,7 +105,9 @@ | ||||
|         "timeout2": "Тайм-аут для подключения к локальному хосту", | ||||
|         "username": "Имя пользователя", | ||||
|         "validate": "Проверить", | ||||
|         "validation_success": "Проверка прошла успешно" | ||||
|         "validation_success": "Проверка прошла успешно", | ||||
|         "tags": "Теги", | ||||
|         "app_passwd_protocols": "Разрешенные протоколы для пароля приложения" | ||||
|     }, | ||||
|     "admin": { | ||||
|         "access": "Настройки доступа", | ||||
| @ -190,7 +192,7 @@ | ||||
|         "flush_queue": "Отправить все сообщения", | ||||
|         "forwarding_hosts": "Переадресация хостов", | ||||
|         "forwarding_hosts_add_hint": "Можно указывать: IPv4/IPv6 подсети в нотации CIDR, имена хостов (которые будут разрешаться в IP-адреса) или доменные имена (которые будут решаться с IP-адресами путем запроса SPF записей или, в случае их отсутствия - запросом MX записей).", | ||||
|         "forwarding_hosts_hint": "Входящие сообщения безоговорочно принимаются от любых хостов, перечисленных здесь. Эти хосты не проходят проверку DNSBL и graylisting. Спам, полученный от них, никогда не отклоняется, но при желании можно включить спам фильтр и письма с плохим рейтингом будут попадать в Junk. Наиболее распространенное использование - указать почтовые серверы, на которых вы установили правило, которое перенаправляет входящие электронные письма на ваш почтовый сервер.", | ||||
|         "forwarding_hosts_hint": "Входящие сообщения безоговорочно принимаются от любых хостов, перечисленных здесь. Эти хосты не проходят проверку DNSBL и graylisting. Спам, полученный от них, никогда не отклоняется, но при желании можно включить спам фильтр и письма с плохим рейтингом будут попадать в Junk. Наиболее распространенное использование - указать почтовые серверы, на которых вы установили правило, которое перенаправляет входящие электронные письма на ваш почтовый сервер mailcow.", | ||||
|         "from": "От", | ||||
|         "generate": "сгенерировать", | ||||
|         "guid": "GUID - уникальный ID", | ||||
| @ -460,7 +462,8 @@ | ||||
|         "unlimited_quota_acl": "Неограниченная квота запрещена политикой доступа", | ||||
|         "username_invalid": "Имя пользователя %s нельзя использовать", | ||||
|         "validity_missing": "Пожалуйста, назначьте срок действия", | ||||
|         "value_missing": "Пожалуйста заполните все поля" | ||||
|         "value_missing": "Пожалуйста заполните все поля", | ||||
|         "yotp_verification_failed": "Ошибка валидации Yubico OTP: %s" | ||||
|     }, | ||||
|     "debug": { | ||||
|         "chart_this_server": "Диаграмма (текущий сервер)", | ||||
| @ -886,11 +889,11 @@ | ||||
|         "type": "Тип" | ||||
|     }, | ||||
|     "ratelimit": { | ||||
|       "disabled": "Отключен", | ||||
|       "second": "сообщений / секунду", | ||||
|       "minute": "сообщений / минуту", | ||||
|       "hour": "сообщений / час", | ||||
|       "day": "сообщений / день" | ||||
|         "disabled": "Отключен", | ||||
|         "second": "сообщений / секунду", | ||||
|         "minute": "сообщений / минуту", | ||||
|         "hour": "сообщений / час", | ||||
|         "day": "сообщений / день" | ||||
|     }, | ||||
|     "start": { | ||||
|         "help": "Справка", | ||||
|  | ||||
							
								
								
									
										1186
									
								
								data/web/lang/lang.uk.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1186
									
								
								data/web/lang/lang.uk.json
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @ -23,6 +23,22 @@ | ||||
|           <input type="text" class="form-control" name="description" value="{{ result.description }}"> | ||||
|         </div> | ||||
|       </div> | ||||
|       <div class="form-group"> | ||||
|         <label class="control-label col-sm-2">{{ lang.add.tags }}</label> | ||||
|         <div class="col-sm-10"> | ||||
|           <div class="form-control tag-box"> | ||||
|             {% for tag in domain_details.tags %} | ||||
|               <span data-action='delete_selected' data-item="{{ tag|url_encode }}" data-id="domain_tag_{{ tag }}" data-api-url='delete/domain/tag/{{ domain }}' class="badge badge-primary tag-badge btn-badge"> | ||||
|                 <i class="bi bi-tag-fill"></i>  | ||||
|                 {{ tag }} | ||||
|               </span> | ||||
|             {% endfor %} | ||||
|             <input type="text" class="tag-input"> | ||||
|             <span class="btn tag-add"><i class="bi bi-plus-lg"></i></span> | ||||
|             <input type="hidden" value="" name="tags" class="tag-values" /> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|       <div class="form-group"> | ||||
|         <label class="control-label col-sm-2" for="relayhost">{{ lang.edit.relayhost }}</label> | ||||
|         <div class="col-sm-10"> | ||||
|  | ||||
| @ -22,6 +22,22 @@ | ||||
|           <input type="text" class="form-control" name="name" value="{{ result.name }}"> | ||||
|         </div> | ||||
|       </div> | ||||
|       <div class="form-group"> | ||||
|         <label class="control-label col-sm-2">{{ lang.add.tags }}</label> | ||||
|         <div class="col-sm-10"> | ||||
|           <div class="form-control tag-box"> | ||||
|             {% for tag in mailbox_details.tags %} | ||||
|               <span data-action='delete_selected' data-item="{{ tag }}" data-id="mailbox_tag_{{ tag }}" data-api-url='delete/mailbox/tag/{{ mailbox }}' class="badge badge-primary tag-badge btn-badge"> | ||||
|                 <i class="bi bi-tag-fill"></i>  | ||||
|                 {{ tag }} | ||||
|               </span> | ||||
|             {% endfor %} | ||||
|             <input type="text" class="tag-input"> | ||||
|             <span class="btn tag-add"><i class="bi bi-plus-lg"></i></span> | ||||
|             <input type="hidden" value="" name="tags" class="tag-values" /> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|       <div class="form-group"> | ||||
|         <label class="control-label col-sm-2" for="quota">{{ lang.edit.quota_mb }} | ||||
|           <br><span id="quotaBadge" class="badge">max. {{ (result.max_new_quota / 1048576) }} MiB</span> | ||||
| @ -154,12 +170,16 @@ | ||||
|         <div class="col-sm-10"> | ||||
|           <div class="btn-group" data-acl="{{ acl.tls_policy }}"> | ||||
|             <button type="button" class="btn btn-sm btn-xs-half visible-xs-block visible-sm-inline visible-md-inline visible-lg-inline btn-default{% if get_tls_policy.tls_enforce_in == '1' %} active"{% endif %}" | ||||
|             role="switch" | ||||
|             aria-checked="{% if get_tls_policy.tls_enforce_in == '1' %}true{% else %}false{% endif %}" | ||||
|             data-action="edit_selected" | ||||
|             data-item="{{ mailbox }}" | ||||
|             data-id="tls_policy" | ||||
|             data-api-url='edit/tls_policy' | ||||
|             data-api-attr='{"tls_enforce_in": {% if get_tls_policy.tls_enforce_in == '1' %}0{% else %}1{% endif %} }'>{{ lang.user.tls_enforce_in }}</button> | ||||
|             <button type="button" class="btn btn-sm btn-xs-half visible-xs-block visible-sm-inline visible-md-inline visible-lg-inline btn-default{% if get_tls_policy.tls_enforce_out == '1' %} active"{% endif %}" | ||||
|             role="switch" | ||||
|             aria-checked="{% if get_tls_policy.tls_enforce_out == '1' %}true{% else %}false{% endif %}" | ||||
|             data-action="edit_selected" | ||||
|             data-item="{{ mailbox }}" | ||||
|             data-id="tls_policy" | ||||
|  | ||||
| @ -31,7 +31,7 @@ | ||||
|           <li><a data-action="edit_selected" data-id="filter_item" data-api-url='edit/filter' data-api-attr='{"filter_type":"prefilter"}' href="#">{{ lang.mailbox.set_prefilter }}</a></li> | ||||
|           <li><a data-action="edit_selected" data-id="filter_item" data-api-url='edit/filter' data-api-attr='{"filter_type":"postfilter"}' href="#">{{ lang.mailbox.set_postfilter }}</a></li> | ||||
|           <li role="separator" class="divider"></li> | ||||
|           <li><a data-action="delete_selected" data-text="{{ lang.user.eas_reset }}?" data-id="filter_item" data-api-url='delete/filter' href="#">{{ lang.mailbox.remove }}</a></li> | ||||
|           <li><a data-action="delete_selected" data-text="{{ lang.edit.delete_ays }}" data-id="filter_item" data-api-url='delete/filter' href="#">{{ lang.mailbox.remove }}</a></li> | ||||
|         </ul> | ||||
|         <div class="clearfix visible-xs"></div> | ||||
|         <a class="btn btn-sm visible-xs-block visible-sm-inline visible-md-inline visible-lg-inline btn-success" href="#" data-toggle="modal" data-target="#addFilterModalAdmin"><i class="bi bi-plus-lg"></i> {{ lang.mailbox.add_filter }}</a> | ||||
|  | ||||
| @ -30,6 +30,16 @@ | ||||
|               <input type="text" class="form-control" name="name"> | ||||
|             </div> | ||||
|           </div> | ||||
|           <div class="form-group"> | ||||
|             <label class="control-label col-sm-2">{{ lang.add.tags }}</label> | ||||
|             <div class="col-sm-10"> | ||||
|               <div class="form-control tag-box"> | ||||
|                 <input type="text" class="tag-input"> | ||||
|                 <span class="btn tag-add"><i class="bi bi-plus-lg"></i></span> | ||||
|                 <input type="hidden" value="" name="tags" class="tag-values" /> | ||||
|               </div> | ||||
|             </div> | ||||
|           </div> | ||||
|           <div class="form-group"> | ||||
|             <label class="control-label col-sm-2" for="addInputQuota">{{ lang.add.quota_mb }} | ||||
|               <br /><span id="quotaBadge" class="badge">max. - MiB</span> | ||||
| @ -94,6 +104,16 @@ | ||||
|               <input type="text" class="form-control" name="description"> | ||||
|             </div> | ||||
|           </div> | ||||
|           <div class="form-group"> | ||||
|             <label class="control-label col-sm-2">{{ lang.add.tags }}</label> | ||||
|             <div class="col-sm-10"> | ||||
|               <div class="form-control tag-box"> | ||||
|                 <input type="text" class="tag-input"> | ||||
|                 <span class="btn tag-add"><i class="bi bi-plus-lg"></i></span> | ||||
|                 <input type="hidden" value="" name="tags" class="tag-values" /> | ||||
|               </div> | ||||
|             </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"> | ||||
| @ -188,11 +208,11 @@ | ||||
|           <div class="form-group"> | ||||
|             <div class="col-sm-offset-2 col-sm-10 btn-group"> | ||||
|               {% if not skip_sogo %} | ||||
|               <button class="btn btn-xs-lg btn-xs-half visible-xs-block visible-sm-inline visible-md-inline visible-lg-inline 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-xs-lg btn-xs-half visible-xs-block visible-sm-inline visible-md-inline visible-lg-inline 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> | ||||
|               <button class="btn btn-xs-lg btn-xs-half visible-xs-block visible-sm-inline visible-md-inline visible-lg-inline btn-default" data-action="add_item" data-id="add_domain" data-api-url='add/domain' data-api-attr='{"tags": []}' href="#">{{ lang.add.add_domain_only }}</button> | ||||
|               <button class="btn btn-xs-lg btn-xs-half visible-xs-block visible-sm-inline visible-md-inline visible-lg-inline btn-default" data-action="add_item" data-id="add_domain" data-api-url='add/domain' data-api-attr='{"restart_sogo":"1", "tags": []}' href="#">{{ lang.add.add_domain_restart }}</button> | ||||
|               <div class="clearfix visible-xs"></div> | ||||
|               {% else %} | ||||
|               <button class="btn btn-xs-lg visible-xs-block visible-sm-inline visible-md-inline visible-lg-inline btn-success" data-action="add_item" data-id="add_domain" data-api-url='add/domain' data-api-attr='{}' href="#">{{ lang.add.add }}</button> | ||||
|               <button class="btn btn-xs-lg visible-xs-block visible-sm-inline visible-md-inline visible-lg-inline btn-success" data-action="add_item" data-id="add_domain" data-api-url='add/domain' data-api-attr='{"tags": []}' href="#">{{ lang.add.add }}</button> | ||||
|               {% endif %} | ||||
|             </div> | ||||
|           </div> | ||||
|  | ||||
| @ -2,11 +2,14 @@ | ||||
|   <div class="panel panel-default"> | ||||
|     <div class="panel-heading">{{ lang.user.mailbox_general }}</div> | ||||
|     <div class="panel-body"> | ||||
|     {% if mailboxdata.attributes.force_pw_update == '1' %} | ||||
|         <div class="alert alert-danger">{{ lang.user.force_pw_update|raw }}</div> | ||||
|     {% endif %} | ||||
|       {% if not skip_sogo %} | ||||
|       <div class="row"> | ||||
|         <div class="hidden-xs col-md-3 col-xs-5 text-right"></div> | ||||
|         <div class="col-md-3 col-xs-12"> | ||||
|           {% if dual_login and allow_admin_email_login == 'n' %} | ||||
|           {% if dual_login and allow_admin_email_login == 'n' or mailboxdata.attributes.force_pw_update == '1' %} | ||||
|             <button disabled class="btn btn-default btn-block btn-xs-lg"> | ||||
|               <i class="bi bi-inbox-fill"></i> {{ lang.user.open_webmail_sso }} | ||||
|             </button> | ||||
| @ -115,9 +118,6 @@ | ||||
|       <hr> | ||||
|       <div class="row"> | ||||
|         <div class="col-sm-offset-3 col-sm-9"> | ||||
|           {% if mailboxdata.attributes.force_pw_update == '1' %} | ||||
|           <div class="alert alert-danger">{{ lang.user.force_pw_update|raw }}</div> | ||||
|           {% endif %} | ||||
|           <p><a target="_blank" href="https://mailcow.github.io/mailcow-dockerized-docs/client/client/#{{ clientconfigstr }}">[{{ lang.user.client_configuration }}]</a></p> | ||||
|           <p><a href="#userFilterModal" data-toggle="modal">[{{ lang.user.show_sieve_filters }}]</a></p> | ||||
|           <hr> | ||||
|  | ||||
| @ -37,12 +37,16 @@ | ||||
|         <div class="col-sm-9 col-xs-12"> | ||||
|           <div class="btn-group" data-acl="{{ acl.tls_policy }}"> | ||||
|             <button type="button" class="btn btn-sm btn-xs-half visible-xs-block visible-sm-inline visible-md-inline visible-lg-inline btn-default{% if get_tls_policy.tls_enforce_in == '1' %} active"{% endif %}" | ||||
|             role="switch" | ||||
|             aria-checked="{% if get_tls_policy.tls_enforce_in == '1' %}true{% else %}false{% endif %}" | ||||
|             data-action="edit_selected" | ||||
|             data-item="{{ mailcow_cc_username }}" | ||||
|             data-id="tls_policy" | ||||
|             data-api-url='edit/tls_policy' | ||||
|             data-api-attr='{"tls_enforce_in": {% if get_tls_policy.tls_enforce_in == '1' %}0{% else %}1{% endif %} }'>{{ lang.user.tls_enforce_in }}</button> | ||||
|             <button type="button" class="btn btn-sm btn-xs-half visible-xs-block visible-sm-inline visible-md-inline visible-lg-inline btn-default{% if get_tls_policy.tls_enforce_out == '1' %} active"{% endif %}" | ||||
|             role="switch" | ||||
|             aria-checked="{% if get_tls_policy.tls_enforce_out == '1' %}true{% else %}false{% endif %}" | ||||
|             data-action="edit_selected" | ||||
|             data-item="{{ mailcow_cc_username }}" | ||||
|             data-id="tls_policy" | ||||
|  | ||||
| @ -2,7 +2,7 @@ version: '2.1' | ||||
| services: | ||||
| 
 | ||||
|     unbound-mailcow: | ||||
|       image: mailcow/unbound:1.15 | ||||
|       image: mailcow/unbound:1.16 | ||||
|       environment: | ||||
|         - TZ=${TZ} | ||||
|       volumes: | ||||
| @ -22,8 +22,8 @@ services: | ||||
|         - unbound-mailcow | ||||
|       stop_grace_period: 45s | ||||
|       volumes: | ||||
|         - mysql-vol-1:/var/lib/mysql/:Z | ||||
|         - mysql-socket-vol-1:/var/run/mysqld/:z | ||||
|         - mysql-vol-1:/var/lib/mysql/ | ||||
|         - mysql-socket-vol-1:/var/run/mysqld/ | ||||
|         - ./data/conf/mysql/:/etc/mysql/conf.d/:ro,Z | ||||
|       environment: | ||||
|         - TZ=${TZ} | ||||
| @ -43,7 +43,7 @@ services: | ||||
|     redis-mailcow: | ||||
|       image: redis:6-alpine | ||||
|       volumes: | ||||
|         - redis-vol-1:/data/:Z | ||||
|         - redis-vol-1:/data/ | ||||
|       restart: always | ||||
|       ports: | ||||
|         - "${REDIS_PORT:-127.0.0.1:7654}:6379" | ||||
| @ -58,8 +58,10 @@ services: | ||||
|             - redis | ||||
| 
 | ||||
|     clamd-mailcow: | ||||
|       image: mailcow/clamd:1.50 | ||||
|       image: mailcow/clamd:1.52 | ||||
|       restart: always | ||||
|       depends_on: | ||||
|         - unbound-mailcow | ||||
|       dns: | ||||
|         - ${IPV4_NETWORK:-172.22.1}.254 | ||||
|       environment: | ||||
| @ -67,7 +69,7 @@ services: | ||||
|         - SKIP_CLAMD=${SKIP_CLAMD:-n} | ||||
|       volumes: | ||||
|         - ./data/conf/clamav/:/etc/clamav/:Z | ||||
|         - clamd-db-vol-1:/var/lib/clamav:z | ||||
|         - clamd-db-vol-1:/var/lib/clamav | ||||
|       networks: | ||||
|         mailcow-network: | ||||
|           aliases: | ||||
| @ -93,7 +95,7 @@ services: | ||||
|         - ./data/conf/rspamd/lua/:/etc/rspamd/lua/:ro,Z | ||||
|         - ./data/conf/rspamd/rspamd.conf.local:/etc/rspamd/rspamd.conf.local:Z | ||||
|         - ./data/conf/rspamd/rspamd.conf.override:/etc/rspamd/rspamd.conf.override:Z | ||||
|         - rspamd-vol-1:/var/lib/rspamd:z | ||||
|         - rspamd-vol-1:/var/lib/rspamd | ||||
|       restart: always | ||||
|       hostname: rspamd | ||||
|       dns: | ||||
| @ -104,7 +106,7 @@ services: | ||||
|             - rspamd | ||||
| 
 | ||||
|     php-fpm-mailcow: | ||||
|       image: mailcow/phpfpm:1.78 | ||||
|       image: mailcow/phpfpm:1.79 | ||||
|       command: "php-fpm -d date.timezone=${TZ} -d expose_php=0" | ||||
|       depends_on: | ||||
|         - redis-mailcow | ||||
| @ -113,8 +115,8 @@ services: | ||||
|         - ./data/web:/web:z | ||||
|         - ./data/conf/rspamd/dynmaps:/dynmaps:ro,z | ||||
|         - ./data/conf/rspamd/custom/:/rspamd_custom_maps:z | ||||
|         - rspamd-vol-1:/var/lib/rspamd:z | ||||
|         - mysql-socket-vol-1:/var/run/mysqld/:z | ||||
|         - rspamd-vol-1:/var/lib/rspamd | ||||
|         - mysql-socket-vol-1:/var/run/mysqld/ | ||||
|         - ./data/conf/sogo/:/etc/sogo/:z | ||||
|         - ./data/conf/rspamd/meta_exporter:/meta_exporter:ro,z | ||||
|         - ./data/conf/phpfpm/sogo-sso/:/etc/sogo-sso/:z | ||||
| @ -166,7 +168,7 @@ services: | ||||
|             - phpfpm | ||||
| 
 | ||||
|     sogo-mailcow: | ||||
|       image: mailcow/sogo:1.107 | ||||
|       image: mailcow/sogo:1.108 | ||||
|       environment: | ||||
|         - DBNAME=${DBNAME} | ||||
|         - DBUSER=${DBUSER} | ||||
| @ -192,9 +194,9 @@ services: | ||||
|         - ./data/conf/sogo/custom-favicon.ico:/usr/lib/GNUstep/SOGo/WebServerResources/img/sogo.ico:z | ||||
|         - ./data/conf/sogo/custom-theme.js:/usr/lib/GNUstep/SOGo/WebServerResources/js/theme.js:z | ||||
|         - ./data/conf/sogo/custom-sogo.js:/usr/lib/GNUstep/SOGo/WebServerResources/js/custom-sogo.js:z | ||||
|         - mysql-socket-vol-1:/var/run/mysqld/:z | ||||
|         - sogo-web-vol-1:/sogo_web:z | ||||
|         - sogo-userdata-backup-vol-1:/sogo_backup:Z | ||||
|         - mysql-socket-vol-1:/var/run/mysqld/ | ||||
|         - sogo-web-vol-1:/sogo_web | ||||
|         - sogo-userdata-backup-vol-1:/sogo_backup | ||||
|       labels: | ||||
|         ofelia.enabled: "true" | ||||
|         ofelia.job-exec.sogo_sessions.schedule: "@every 1m" | ||||
| @ -213,7 +215,7 @@ services: | ||||
|             - sogo | ||||
| 
 | ||||
|     dovecot-mailcow: | ||||
|       image: mailcow/dovecot:1.161 | ||||
|       image: mailcow/dovecot:1.162 | ||||
|       depends_on: | ||||
|         - mysql-mailcow | ||||
|       dns: | ||||
| @ -226,13 +228,13 @@ services: | ||||
|         - ./data/assets/ssl:/etc/ssl/mail/:ro,z | ||||
|         - ./data/conf/sogo/:/etc/sogo/:z | ||||
|         - ./data/conf/phpfpm/sogo-sso/:/etc/phpfpm/:z | ||||
|         - vmail-vol-1:/var/vmail:Z | ||||
|         - vmail-index-vol-1:/var/vmail_index:Z | ||||
|         - crypt-vol-1:/mail_crypt/:z | ||||
|         - vmail-vol-1:/var/vmail | ||||
|         - vmail-index-vol-1:/var/vmail_index | ||||
|         - crypt-vol-1:/mail_crypt/ | ||||
|         - ./data/conf/rspamd/custom/:/etc/rspamd/custom:z | ||||
|         - ./data/assets/templates:/templates:z | ||||
|         - rspamd-vol-1:/var/lib/rspamd:z | ||||
|         - mysql-socket-vol-1:/var/run/mysqld/:z | ||||
|         - rspamd-vol-1:/var/lib/rspamd | ||||
|         - mysql-socket-vol-1:/var/run/mysqld/ | ||||
|       environment: | ||||
|         - DOVECOT_MASTER_USER=${DOVECOT_MASTER_USER:-} | ||||
|         - DOVECOT_MASTER_PASS=${DOVECOT_MASTER_PASS:-} | ||||
| @ -293,17 +295,17 @@ services: | ||||
|             - dovecot | ||||
| 
 | ||||
|     postfix-mailcow: | ||||
|       image: mailcow/postfix:1.66 | ||||
|       image: mailcow/postfix:1.67 | ||||
|       depends_on: | ||||
|         - mysql-mailcow | ||||
|       volumes: | ||||
|         - ./data/hooks/postfix:/hooks:Z | ||||
|         - ./data/conf/postfix:/opt/postfix/conf:z | ||||
|         - ./data/assets/ssl:/etc/ssl/mail/:ro,z | ||||
|         - postfix-vol-1:/var/spool/postfix:z | ||||
|         - crypt-vol-1:/var/lib/zeyple:z | ||||
|         - rspamd-vol-1:/var/lib/rspamd:z | ||||
|         - mysql-socket-vol-1:/var/run/mysqld/:z | ||||
|         - postfix-vol-1:/var/spool/postfix | ||||
|         - crypt-vol-1:/var/lib/zeyple | ||||
|         - rspamd-vol-1:/var/lib/rspamd | ||||
|         - mysql-socket-vol-1:/var/run/mysqld/ | ||||
|       environment: | ||||
|         - LOG_LINES=${LOG_LINES:-9999} | ||||
|         - TZ=${TZ} | ||||
| @ -415,7 +417,7 @@ services: | ||||
|     acme-mailcow: | ||||
|       depends_on: | ||||
|         - nginx-mailcow | ||||
|       image: mailcow/acme:1.81 | ||||
|       image: mailcow/acme:1.82 | ||||
|       dns: | ||||
|         - ${IPV4_NETWORK:-172.22.1}.254 | ||||
|       environment: | ||||
| @ -443,7 +445,7 @@ services: | ||||
|         - ./data/web/.well-known/acme-challenge:/var/www/acme:z | ||||
|         - ./data/assets/ssl:/var/lib/acme/:z | ||||
|         - ./data/assets/ssl-example:/var/lib/ssl-example/:ro,Z | ||||
|         - mysql-socket-vol-1:/var/run/mysqld/:z | ||||
|         - mysql-socket-vol-1:/var/run/mysqld/ | ||||
|       restart: always | ||||
|       networks: | ||||
|         mailcow-network: | ||||
| @ -451,7 +453,7 @@ services: | ||||
|             - acme | ||||
| 
 | ||||
|     netfilter-mailcow: | ||||
|       image: mailcow/netfilter:1.46 | ||||
|       image: mailcow/netfilter:1.48 | ||||
|       stop_grace_period: 30s | ||||
|       depends_on: | ||||
|         - dovecot-mailcow | ||||
| @ -480,9 +482,9 @@ services: | ||||
|       tmpfs: | ||||
|         - /tmp | ||||
|       volumes: | ||||
|         - rspamd-vol-1:/var/lib/rspamd:z | ||||
|         - mysql-socket-vol-1:/var/run/mysqld/:z | ||||
|         - postfix-vol-1:/var/spool/postfix:z | ||||
|         - rspamd-vol-1:/var/lib/rspamd | ||||
|         - mysql-socket-vol-1:/var/run/mysqld/ | ||||
|         - postfix-vol-1:/var/spool/postfix | ||||
|         - ./data/assets/ssl:/etc/ssl/mail/:ro,z | ||||
|       restart: always | ||||
|       environment: | ||||
| @ -536,7 +538,7 @@ services: | ||||
|             - watchdog | ||||
| 
 | ||||
|     dockerapi-mailcow: | ||||
|       image: mailcow/dockerapi:1.41 | ||||
|       image: mailcow/dockerapi:1.42 | ||||
|       security_opt: | ||||
|         - label=disable | ||||
|       restart: always | ||||
| @ -557,7 +559,7 @@ services: | ||||
|       image: mailcow/solr:1.8.1 | ||||
|       restart: always | ||||
|       volumes: | ||||
|         - solr-vol-1:/opt/solr/server/solr/dovecot-fts/data:Z | ||||
|         - solr-vol-1:/opt/solr/server/solr/dovecot-fts/data | ||||
|       ports: | ||||
|         - "${SOLR_PORT:-127.0.0.1:18983}:8983" | ||||
|       environment: | ||||
| @ -570,7 +572,7 @@ services: | ||||
|             - solr | ||||
| 
 | ||||
|     olefy-mailcow: | ||||
|       image: mailcow/olefy:1.9 | ||||
|       image: mailcow/olefy:1.10 | ||||
|       restart: always | ||||
|       environment: | ||||
|         - TZ=${TZ} | ||||
|  | ||||
| @ -25,10 +25,29 @@ if cp --help 2>&1 | grep -q -i "busybox"; then | ||||
|   exit 1 | ||||
| fi | ||||
| 
 | ||||
| for bin in openssl curl docker-compose docker git awk sha1sum; do | ||||
| for bin in openssl curl docker git awk sha1sum; do | ||||
|   if [[ -z $(which ${bin}) ]]; then echo "Cannot find ${bin}, exiting..."; exit 1; fi | ||||
| done | ||||
| 
 | ||||
| echo "checking docker compose version..."; | ||||
| if docker compose >/dev/null 2>&1; then | ||||
|   echo -e "\e[32mFound Compose v2!\e[0m" | ||||
| elif docker-compose version --short | grep -m1 "^2" > /dev/null 2>&1; then | ||||
|   echo -e "\e[32mFound Compose v2!\e[0m" | ||||
|   COMPOSE_COMMAND="docker-compose"   | ||||
| elif docker-compose version --short | grep -m1 "^1" > /dev/null 2>&1; then | ||||
|   echo -e "\e[33mWARN: Your machine is using Docker-Compose v1!\e[0m" | ||||
|   echo -e "\e[33mmailcow will drop the Docker-Compose v1 Support in December 2022\e[0m" | ||||
|   echo -e "\e[33mPlease consider a upgrade to Docker-Compose v2.\e[0m" | ||||
|   echo | ||||
|   echo | ||||
|   echo -e "\e[33mContinuing...\e[0m" | ||||
|   sleep 3 | ||||
| else | ||||
|   echo -e "\e[31mCannot find Docker-Compose v1 or v2 on your System. Please install Docker-Compose v2 and re-run the Script.\e[0m" | ||||
|   exit 1 | ||||
| fi | ||||
| 
 | ||||
| if [ -f mailcow.conf ]; then | ||||
|   read -r -p "A config file exists and will be overwritten, are you sure you want to continue? [y/N] " response | ||||
|   case $response in | ||||
| @ -144,7 +163,7 @@ DBROOT=$(LC_ALL=C </dev/urandom tr -dc A-Za-z0-9 | head -c 28) | ||||
| # Do _not_ use IP:PORT in HTTP(S)_BIND or HTTP(S)_PORT | ||||
| # IMPORTANT: Do not use port 8081, 9081 or 65510! | ||||
| # Example: HTTP_BIND=1.2.3.4 | ||||
| # For IPv4 and IPv6 leave it empty: HTTP_BIND= & HTTPS_PORT= | ||||
| # For IPv4 leave it as it is: HTTP_BIND= & HTTPS_PORT= | ||||
| # For IPv6 see https://mailcow.github.io/mailcow-dockerized-docs/post_installation/firststeps-ip_bindings/ | ||||
| 
 | ||||
| HTTP_PORT=80 | ||||
|  | ||||
| @ -77,15 +77,35 @@ function preflight_local_checks() { | ||||
|     exit 1 | ||||
|   fi | ||||
| 
 | ||||
|   for bin in rsync docker-compose docker grep cut; do | ||||
|   for bin in rsync docker grep cut; do | ||||
|     if [[ -z $(which ${bin}) ]]; then | ||||
|       >&2 echo -e "\e[31mCannot find ${bin} in local PATH, exiting...\e[0m" | ||||
|       exit 1 | ||||
|     fi | ||||
|   done | ||||
| 
 | ||||
| 
 | ||||
|   echo "checking docker compose version..."; | ||||
|   if docker compose >/dev/null 2>&1; then | ||||
|     echo -e "\e[32mFound Compose v2 on local machine!\e[0m" | ||||
|   elif docker-compose version --short | grep -m1 "^2" > /dev/null 2>&1; then | ||||
|   echo -e "\e[32mFound Compose v2!\e[0m" | ||||
|   COMPOSE_COMMAND="docker-compose"   | ||||
|   elif docker-compose version --short | grep -m1 "^1" > /dev/null 2>&1; then | ||||
|     echo -e "\e[33mWARN: Your machine is using Docker-Compose v1!\e[0m" | ||||
|     echo -e "\e[33mmailcow will drop the Docker-Compose v1 Support in December 2022\e[0m" | ||||
|     echo -e "\e[33mPlease consider a upgrade to Docker-Compose v2.\e[0m" | ||||
|     echo | ||||
|     echo | ||||
|     echo -e "\e[33mContinuing...\e[0m" | ||||
|     sleep 3 | ||||
|   else | ||||
|     echo -e "\e[31mCannot find Docker-Compose v1 or v2 on your System. Please install Docker-Compose v2 and re-run the Script.\e[0m" | ||||
|     exit 1 | ||||
|   fi | ||||
| 
 | ||||
|   if grep --help 2>&1 | head -n 1 | grep -q -i "busybox"; then | ||||
|     >&2 echo -e "\e[31mBusyBox grep detected on local system, please install GNU grep\e[0m" | ||||
|     echo -e "\e[31mBusyBox grep detected on local system, please install GNU grep\e[0m" | ||||
|     exit 1 | ||||
|   fi | ||||
| } | ||||
| @ -111,7 +131,7 @@ function preflight_remote_checks() { | ||||
|       exit 1 | ||||
|   fi | ||||
| 
 | ||||
|   for bin in rsync docker-compose docker; do | ||||
|   for bin in rsync docker; do | ||||
|     if ! ssh -o StrictHostKeyChecking=no \ | ||||
|       -i "${REMOTE_SSH_KEY}" \ | ||||
|       ${REMOTE_SSH_HOST} \ | ||||
| @ -122,6 +142,38 @@ function preflight_remote_checks() { | ||||
|     fi | ||||
|   done | ||||
| 
 | ||||
|   echo "checking docker compose version on remote..."; | ||||
|   if ssh -q -o StrictHostKeyChecking=no \ | ||||
|       -i "${REMOTE_SSH_KEY}" \ | ||||
|       ${REMOTE_SSH_HOST} \ | ||||
|       -p ${REMOTE_SSH_PORT} \ | ||||
|      -t 'docker compose' >/dev/null 2>&1; then | ||||
|     echo -e "\e[32mFound Compose v2 on remote!\e[0m" | ||||
|     COMPOSE_COMMAND="docker compose" | ||||
|   elif ssh -q -o StrictHostKeyChecking=no \ | ||||
|       -i "${REMOTE_SSH_KEY}" \ | ||||
|       ${REMOTE_SSH_HOST} \ | ||||
|       -p ${REMOTE_SSH_PORT} \ | ||||
|       -t 'docker-compose version --short' | grep -m1 "^2" > /dev/null 2>&1; then | ||||
|     echo -e "\e[32mFound Compose v2!\e[0m" | ||||
|     COMPOSE_COMMAND="docker-compose" | ||||
|   elif ssh -q -o StrictHostKeyChecking=no \ | ||||
|       -i "${REMOTE_SSH_KEY}" \ | ||||
|       ${REMOTE_SSH_HOST} \ | ||||
|       -p ${REMOTE_SSH_PORT} \ | ||||
|       -t 'docker-compose version --short' | grep -m1 "^1" > /dev/null 2>&1; then | ||||
|     echo -e "\e[33mWARN: The remote is using Docker-Compose v1!\e[0m" | ||||
|     echo -e "\e[33mmailcow will drop the Docker-Compose v1 Support in December 2022\e[0m" | ||||
|     echo -e "\e[33mPlease consider a upgrade to Docker-Compose v2 on remote.\e[0m" | ||||
|     echo | ||||
|     echo | ||||
|     echo -e "\e[33mContinuing...\e[0m" | ||||
|     sleep 3 | ||||
|     COMPOSE_COMMAND="docker-compose" | ||||
|   else | ||||
|     echo -e "\e[31mCannot find Docker-Compose v1 or v2 on the Remote Machine! Please install Docker-Compose v2 on that and re-run the script.\e[0m" | ||||
|     exit 1 | ||||
|   fi | ||||
| } | ||||
| 
 | ||||
| preflight_local_checks | ||||
| @ -252,16 +304,18 @@ if ! ssh -o StrictHostKeyChecking=no \ | ||||
| fi | ||||
| echo "OK" | ||||
| 
 | ||||
| echo -e "\033[1mPulling images on remote...\033[0m" | ||||
| if ! ssh -o StrictHostKeyChecking=no \ | ||||
|   -i "${REMOTE_SSH_KEY}" \ | ||||
|   ${REMOTE_SSH_HOST} \ | ||||
|   -p ${REMOTE_SSH_PORT} \ | ||||
|   docker-compose -f "${SCRIPT_DIR}/../docker-compose.yml" pull --no-parallel 2>&1 ; then | ||||
|     >&2 echo -e "\e[31m[ERR]\e[0m - Could not pull images on remote" | ||||
| fi | ||||
|   echo -e "\e[33mPulling images on remote...\e[0m" | ||||
|   echo -e "\e[33mProcess is NOT stuck! Please wait...\e[0m" | ||||
| 
 | ||||
| echo -e "\033[1mForcing garbage cleanup on remote...\033[0m" | ||||
|   if ! ssh -o StrictHostKeyChecking=no \ | ||||
|     -i "${REMOTE_SSH_KEY}" \ | ||||
|     ${REMOTE_SSH_HOST} \ | ||||
|     -p ${REMOTE_SSH_PORT} \ | ||||
|     $COMPOSE_COMMAND -f "${SCRIPT_DIR}/../docker-compose.yml" pull --no-parallel --quiet 2>&1 ; then | ||||
|       >&2 echo -e "\e[31m[ERR]\e[0m - Could not pull images on remote" | ||||
|   fi | ||||
| 
 | ||||
| echo -e "\033[1mExecuting update script and forcing garbage cleanup on remote...\033[0m" | ||||
| if ! ssh -o StrictHostKeyChecking=no \ | ||||
|   -i "${REMOTE_SSH_KEY}" \ | ||||
|   ${REMOTE_SSH_HOST} \ | ||||
|  | ||||
| @ -76,6 +76,33 @@ else | ||||
|   CMPS_PRJ=$(echo ${COMPOSE_PROJECT_NAME} | tr -cd "[0-9A-Za-z-_]") | ||||
| fi | ||||
| 
 | ||||
| echo "checking docker compose version..."; | ||||
| if docker compose >/dev/null 2>&1; then | ||||
|   echo -e "\e[32mFound Compose v2!\e[0m" | ||||
|   COMPOSE_COMMAND="docker compose" | ||||
| elif docker-compose version --short | grep -m1 "^2" > /dev/null 2>&1; then | ||||
|   echo -e "\e[32mFound Compose v2!\e[0m" | ||||
|   COMPOSE_COMMAND="docker-compose"   | ||||
| elif docker-compose version --short | grep -m1 "^1" > /dev/null 2>&1; then | ||||
|   echo -e "\e[33mWARN: Your machine is using Docker-Compose v1!\e[0m" | ||||
|   echo -e "\e[33mmailcow will drop the Docker-Compose v1 Support in December 2022\e[0m" | ||||
|   echo -e "\e[33mPlease consider a upgrade to Docker-Compose v2.\e[0m" | ||||
|   echo | ||||
|   echo | ||||
|   echo -e "\e[33mContinuing...\e[0m" | ||||
|   sleep 3 | ||||
|   COMPOSE_COMMAND="docker-compose" | ||||
| else | ||||
|   echo -e "\e[31mCannot find Docker-Compose v1 or v2 on your System. Please install Docker-Compose v2 and re-run the Script.\e[0m" | ||||
|   exit 1 | ||||
| fi | ||||
| 
 | ||||
| if grep --help 2>&1 | head -n 1 | grep -q -i "busybox"; then | ||||
|   >&2 echo -e "\e[31mBusyBox grep detected on local system, please install GNU grep\e[0m" | ||||
|   exit 1 | ||||
| fi | ||||
| 
 | ||||
| 
 | ||||
| function backup() { | ||||
|   DATE=$(date +"%Y-%m-%d-%H-%M-%S") | ||||
|   mkdir -p "${BACKUP_LOCATION}/mailcow-${DATE}" | ||||
| @ -226,7 +253,7 @@ function restore() { | ||||
|           continue | ||||
|         else | ||||
|           echo "Stopping mailcow..." | ||||
|           docker-compose -f ${COMPOSE_FILE} --env-file ${ENV_FILE} down | ||||
|           ${COMPOSE_COMMAND} -f ${COMPOSE_FILE} --env-file ${ENV_FILE} down | ||||
|         fi | ||||
|         #docker stop $(docker ps -qf name=mysql-mailcow) | ||||
|         if [[ -d "${RESTORE_LOCATION}/mysql" ]]; then | ||||
| @ -264,7 +291,7 @@ function restore() { | ||||
|         sed -i --follow-symlinks "/DBROOT/c\DBROOT=${DBROOT}" ${SCRIPT_DIR}/../mailcow.conf | ||||
|         source ${SCRIPT_DIR}/../mailcow.conf | ||||
|         echo "Starting mailcow..." | ||||
|         docker-compose -f ${COMPOSE_FILE} --env-file ${ENV_FILE} up -d | ||||
|         ${COMPOSE_COMMAND} -f ${COMPOSE_FILE} --env-file ${ENV_FILE} up -d | ||||
|         #docker start $(docker ps -aqf name=mysql-mailcow) | ||||
|       fi | ||||
|       ;; | ||||
|  | ||||
							
								
								
									
										108
									
								
								update.sh
									
									
									
									
									
								
							
							
						
						
									
										108
									
								
								update.sh
									
									
									
									
									
								
							| @ -40,10 +40,31 @@ PATH=$PATH:/opt/bin | ||||
| 
 | ||||
| umask 0022 | ||||
| 
 | ||||
| for bin in curl docker-compose docker git awk sha1sum; do | ||||
| for bin in curl docker git awk sha1sum; do | ||||
|   if [[ -z $(which ${bin}) ]]; then echo "Cannot find ${bin}, exiting..."; exit 1; fi | ||||
| done | ||||
| 
 | ||||
| 
 | ||||
| echo "checking docker compose version..."; | ||||
| if docker compose >/dev/null 2>&1; then | ||||
|   echo -e "\e[32mFound Compose v2!\e[0m" | ||||
|   COMPOSE_COMMAND="docker compose" | ||||
| elif docker-compose version --short | grep -m1 "^2" > /dev/null 2>&1; then | ||||
|   echo -e "\e[32mFound Compose v2!\e[0m" | ||||
|   COMPOSE_COMMAND="docker-compose" | ||||
| elif docker-compose version --short | grep -m1 "^1" > /dev/null 2>&1; then | ||||
|   echo -e "\e[33mWARN: Your machine is using Docker-Compose v1!\e[0m" | ||||
|   echo -e "\e[33mmailcow will drop the Docker-Compose v1 Support in December 2022\e[0m" | ||||
|   echo -e "\e[33mPlease consider a upgrade to Docker-Compose v2.\e[0m" | ||||
|   echo | ||||
|   echo | ||||
|   echo -e "\e[33mContinuing...\e[0m" | ||||
|   sleep 3 | ||||
|   COMPOSE_COMMAND="docker-compose" | ||||
| else | ||||
|   echo -e "\e[31mCannot find Docker-Compose v1 or v2 on your System. Please install Docker-Compose v2 and re-run the Script.\e[0m" | ||||
|   exit 1 | ||||
| fi | ||||
| export LC_ALL=C | ||||
| DATE=$(date +%Y-%m-%d_%H_%M_%S) | ||||
| BRANCH=$(cd ${SCRIPT_DIR}; git rev-parse --abbrev-ref HEAD) | ||||
| @ -235,18 +256,18 @@ while (($#)); do | ||||
|       echo -e "\e[32mRunning in forced mode...\e[0m" | ||||
|       FORCE=y | ||||
|     ;; | ||||
|     --no-update-compose) | ||||
|       NO_UPDATE_COMPOSE=y | ||||
|     --skip-ping-check) | ||||
|       SKIP_PING_CHECK=y | ||||
|     ;; | ||||
|     --help|-h) | ||||
|     echo './update.sh [-c|--check, --ours, --gc, --no-update-compose, --prefetch, --skip-start, -f|--force, -h|--help] | ||||
|     echo './update.sh [-c|--check, --ours, --gc, --no-update-compose, --prefetch, --skip-start, --skip-ping-check, -f|--force, -h|--help] | ||||
| 
 | ||||
|   -c|--check           -   Check for updates and exit (exit codes => 0: update available, 3: no updates) | ||||
|   --ours               -   Use merge strategy option "ours" to solve conflicts in favor of non-mailcow code (local changes over remote changes), not recommended! | ||||
|   --gc                 -   Run garbage collector to delete old image tags | ||||
|   --no-update-compose  -   Do not update docker-compose | ||||
|   --prefetch           -   Only prefetch new images and exit (useful to prepare updates) | ||||
|   --skip-start         -   Do not start mailcow after update | ||||
|   --skip-ping-check    -   Skip ICMP Check to public DNS resolvers (Use it only if you´ve blocked any ICMP Connections to your mailcow machine). | ||||
|   -f|--force           -   Force update, do not ask questions | ||||
| ' | ||||
|     exit 1 | ||||
| @ -260,7 +281,7 @@ source mailcow.conf | ||||
| DOTS=${MAILCOW_HOSTNAME//[^.]}; | ||||
| if [ ${#DOTS} -lt 2 ]; then | ||||
|   echo "MAILCOW_HOSTNAME (${MAILCOW_HOSTNAME}) is not a FQDN!" | ||||
|   echo "Please change it to a FQDN and run docker-compose down followed by docker-compose up -d" | ||||
|   echo "Please change it to a FQDN and run ${COMPOSE_COMMAND} down followed by ${COMPOSE_COMMAND} up -d" | ||||
|   exit 1 | ||||
| fi | ||||
| 
 | ||||
| @ -533,12 +554,17 @@ elif [[ ${option} == "WATCHDOG_VERBOSE" ]]; then | ||||
|   fi | ||||
| done | ||||
| 
 | ||||
| echo -en "Checking internet connection... " | ||||
| if ! check_online_status; then | ||||
|   echo -e "\e[31mfailed\e[0m" | ||||
|   exit 1 | ||||
| if [[( ${SKIP_PING_CHECK} == "y")]]; then | ||||
| echo -e "\e[32mSkipping Ping Check...\e[0m" | ||||
| 
 | ||||
| else | ||||
|   echo -e "\e[32mOK\e[0m" | ||||
|    echo -en "Checking internet connection... " | ||||
|    if ! check_online_status; then | ||||
|       echo -e "\e[31mfailed\e[0m" | ||||
|       exit 1 | ||||
|    else | ||||
|       echo -e "\e[32mOK\e[0m" | ||||
|    fi | ||||
| fi | ||||
| 
 | ||||
| echo -e "\e[32mChecking for newer update script...\e[0m" | ||||
| @ -569,13 +595,13 @@ if [ ! $FORCE ]; then | ||||
| fi | ||||
| 
 | ||||
| echo -e "\e[32mValidating docker-compose stack configuration...\e[0m" | ||||
| if ! docker-compose config -q; then | ||||
| if ! ${COMPOSE_COMMAND} config -q; then | ||||
|   echo -e "\e[31m\nOh no, something went wrong. Please check the error message above.\e[0m" | ||||
|   exit 1 | ||||
| fi | ||||
| 
 | ||||
| echo -e "\e[32mChecking for conflicting bridges...\e[0m" | ||||
| MAILCOW_BRIDGE=$(docker-compose config | grep -i com.docker.network.bridge.name | cut -d':' -f2) | ||||
| MAILCOW_BRIDGE=$(${COMPOSE_COMMAND} config | grep -i com.docker.network.bridge.name | cut -d':' -f2) | ||||
| while read NAT_ID; do | ||||
|   iptables -t nat -D POSTROUTING $NAT_ID | ||||
| done < <(iptables -L -vn -t nat --line-numbers | grep $IPV4_NETWORK | grep -E 'MASQUERADE.*all' | grep -v ${MAILCOW_BRIDGE} | cut -d' ' -f1) | ||||
| @ -595,8 +621,8 @@ prefetch_images | ||||
| 
 | ||||
| echo -e "\e[32mStopping mailcow...\e[0m" | ||||
| sleep 2 | ||||
| MAILCOW_CONTAINERS=($(docker-compose ps -q)) | ||||
| docker-compose down | ||||
| MAILCOW_CONTAINERS=($(${COMPOSE_COMMAND} ps -q)) | ||||
| ${COMPOSE_COMMAND} down | ||||
| echo -e "\e[32mChecking for remaining containers...\e[0m" | ||||
| sleep 2 | ||||
| for container in "${MAILCOW_CONTAINERS[@]}"; do | ||||
| @ -633,51 +659,16 @@ elif [[ ${MERGE_RETURN} == 1 ]]; then | ||||
| elif [[ ${MERGE_RETURN} != 0 ]]; then | ||||
|   echo -e "\e[31m\nOh no, something went wrong. Please check the error message above.\e[0m" | ||||
|   echo | ||||
|   echo "Run docker-compose up -d to restart your stack without updates or try again after fixing the mentioned errors." | ||||
|   echo "Run ${COMPOSE_COMMAND} up -d to restart your stack without updates or try again after fixing the mentioned errors." | ||||
|   exit 1 | ||||
| fi | ||||
| 
 | ||||
| if [[ ${NO_UPDATE_COMPOSE} == "y" ]]; then | ||||
|   echo -e "\e[33mNot fetching latest docker-compose, please check for updates manually!\e[0m" | ||||
| elif [[ -e /etc/alpine-release ]]; then | ||||
|   echo -e "\e[33mNot fetching latest docker-compose, because you are using Alpine Linux without glibc support. Please update docker-compose via apk!\e[0m" | ||||
| else | ||||
|   echo -e "\e[32mFetching new docker-compose version...\e[0m" | ||||
|   echo -e "\e[32mTrying to determine GLIBC version...\e[0m" | ||||
|   if ldd --version > /dev/null; then | ||||
|     GLIBC_V=$(ldd --version | grep -E '(GLIBC|GNU libc)' | rev | cut -d ' ' -f1 | rev | cut -d '.' -f2) | ||||
|     if [ ! -z "${GLIBC_V}" ] && [ ${GLIBC_V} -gt 27 ]; then | ||||
|       DC_DL_SUFFIX= | ||||
|     else | ||||
|       DC_DL_SUFFIX=legacy | ||||
|     fi | ||||
|   else | ||||
|     DC_DL_SUFFIX=legacy | ||||
|   fi | ||||
|   sleep 1 | ||||
|   if [[ ! -z $(which pip) && $(pip list --local 2>&1 | grep -v DEPRECATION | grep -c docker-compose) == 1 ]]; then | ||||
|     true | ||||
|     #prevent breaking a working docker-compose installed with pip | ||||
|   elif [[ $(curl -sL -w "%{http_code}" https://www.servercow.de/docker-compose/latest.php?vers=${DC_DL_SUFFIX} -o /dev/null) == "200" ]]; then | ||||
|     LATEST_COMPOSE=$(curl -#L https://www.servercow.de/docker-compose/latest.php) | ||||
|     COMPOSE_VERSION=$(docker-compose version --short) | ||||
|     if [[ "$LATEST_COMPOSE" != "$COMPOSE_VERSION" ]]; then | ||||
|       COMPOSE_PATH=$(which docker-compose) | ||||
|       if [[ -w ${COMPOSE_PATH} ]]; then | ||||
|         curl -#L https://github.com/docker/compose/releases/download/${LATEST_COMPOSE}/docker-compose-$(uname -s)-$(uname -m) > $COMPOSE_PATH | ||||
|         chmod +x $COMPOSE_PATH | ||||
|       else | ||||
|         echo -e "\e[33mWARNING: $COMPOSE_PATH is not writable, but new version $LATEST_COMPOSE is available (installed: $COMPOSE_VERSION)\e[0m" | ||||
|       fi | ||||
|     fi | ||||
|   else | ||||
|     echo -e "\e[33mCannot determine latest docker-compose version, skipping...\e[0m" | ||||
|   fi | ||||
| fi | ||||
| echo -e "\e[33mNot fetching latest docker-compose, please check for updates manually!\e[0m" | ||||
| sleep 3 | ||||
| 
 | ||||
| echo -e "\e[32mFetching new images, if any...\e[0m" | ||||
| sleep 2 | ||||
| docker-compose pull | ||||
| ${COMPOSE_COMMAND} pull | ||||
| 
 | ||||
| # Fix missing SSL, does not overwrite existing files | ||||
| [[ ! -d data/assets/ssl ]] && mkdir -p data/assets/ssl | ||||
| @ -698,9 +689,6 @@ fi | ||||
| 
 | ||||
| # Checking for old project name bug | ||||
| sed -i --follow-symlinks 's#COMPOSEPROJECT_NAME#COMPOSE_PROJECT_NAME#g' mailcow.conf | ||||
| # Checking old, wrong bindings | ||||
| sed -i --follow-symlinks 's/HTTP_BIND=0.0.0.0/HTTP_BIND=/g' mailcow.conf | ||||
| sed -i --follow-symlinks 's/HTTPS_BIND=0.0.0.0/HTTPS_BIND=/g' mailcow.conf | ||||
| 
 | ||||
| # Fix Rspamd maps | ||||
| if [ -f data/conf/rspamd/custom/global_from_blacklist.map ]; then | ||||
| @ -735,11 +723,11 @@ else | ||||
| fi | ||||
| 
 | ||||
| if [[ ${SKIP_START} == "y" ]]; then | ||||
|   echo -e "\e[33mNot starting mailcow, please run \"docker-compose up -d --remove-orphans\" to start mailcow.\e[0m" | ||||
|   echo -e "\e[33mNot starting mailcow, please run \"${COMPOSE_COMMAND} up -d --remove-orphans\" to start mailcow.\e[0m" | ||||
| else | ||||
|   echo -e "\e[32mStarting mailcow...\e[0m" | ||||
|   sleep 2 | ||||
|   docker-compose up -d --remove-orphans | ||||
|   ${COMPOSE_COMMAND} up -d --remove-orphans | ||||
| fi | ||||
| 
 | ||||
| echo -e "\e[32mCollecting garbage...\e[0m" | ||||
| @ -754,4 +742,4 @@ fi | ||||
| #echo | ||||
| #git reflog --color=always | grep "Before update on " | ||||
| #echo | ||||
| #echo "Use \"git reset --hard hash-on-the-left\" and run docker-compose up -d afterwards." | ||||
| #echo "Use \"git reset --hard hash-on-the-left\" and run ${COMPOSE_COMMAND} up -d afterwards." | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user