diff --git a/.gitignore b/.gitignore index 94072693..65fcca3b 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ externals/ .env .vagrant .idea/ +api/docs/api-docs.html diff --git a/CHANGELOG.md b/CHANGELOG.md index 38fb419b..9c10d78e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,39 @@ CHANGELOG ========= +v0.50 (September 25, 2020) +-------------------------- + +Setup: + +* When upgrading from versions before v0.40, setup will now warn that ownCloud/Nextcloud data cannot be migrated rather than failing the installation. + +Mail: + +* An MTA-STS policy for incoming mail is now published (in DNS and over HTTPS) when the primary hostname and email address domain both have a signed TLS certificate installed, allowing senders to know that an encrypted connection should be enforced. +* The per-IP connection limit to the IMAP server has been doubled to allow more devices to connect at once, especially with multiple users behind a NAT. + +DNS: + +* autoconfig and autodiscover subdomains and CalDAV/CardDAV SRV records are no longer generated for domains that don't have user accounts since they are unnecessary. +* IPv6 addresses can now be specified for secondary DNS nameservers in the control panel. + +TLS: + +* TLS certificates are now provisioned in groups by parent domain to limit easy domain enumeration and make provisioning more resilient to errors for particular domains. + +Control Panel: + +* The control panel API is now fully documented at https://mailinabox.email/api-docs.html. +* User passwords can now have spaces. +* Status checks for automatic subdomains have been moved into the section for the parent domain. +* Typo fixed. + +Web: + +* The default web page served on fresh installations now adds the `noindex` meta tag. +* The HSTS header is revised to also be sent on non-success responses. + v0.48 (August 26, 2020) ----------------------- diff --git a/README.md b/README.md index fb2ee61b..28f3d2f8 100644 --- a/README.md +++ b/README.md @@ -69,6 +69,10 @@ Issues Changes ------- +### v0.50-quota-0.22-beta + +* Update to v0.50 of Mail-in-a-Box + ### v0.48-quota-0.22-beta * Update to v0.48 of Mail-in-a-Box @@ -199,27 +203,34 @@ Our goals are to: Additionally, this project has a [Code of Conduct](CODE_OF_CONDUCT.md), which supersedes the goals above. Please review it when joining our community. -The Box -------- + +In The Box +---------- Mail-in-a-Box turns a fresh Ubuntu 18.04 LTS 64-bit machine into a working mail server by installing and configuring various components. -It is a one-click email appliance. There are no user-configurable setup options. It "just works". +It is a one-click email appliance. There are no user-configurable setup options. It "just works." The components installed are: -* SMTP ([postfix](http://www.postfix.org/)), IMAP ([dovecot](http://dovecot.org/)), CardDAV/CalDAV ([Nextcloud](https://nextcloud.com/)), Exchange ActiveSync ([z-push](http://z-push.org/)) -* Webmail ([Roundcube](http://roundcube.net/)), static website hosting ([nginx](http://nginx.org/)) -* Spam filtering ([spamassassin](https://spamassassin.apache.org/)), greylisting ([postgrey](http://postgrey.schweikert.ch/)) -* DNS ([nsd4](https://www.nlnetlabs.nl/projects/nsd/)) with [SPF](https://en.wikipedia.org/wiki/Sender_Policy_Framework), DKIM ([OpenDKIM](http://www.opendkim.org/)), [DMARC](https://en.wikipedia.org/wiki/DMARC), [DNSSEC](https://en.wikipedia.org/wiki/DNSSEC), [DANE TLSA](https://en.wikipedia.org/wiki/DNS-based_Authentication_of_Named_Entities), and [SSHFP](https://tools.ietf.org/html/rfc4255) records automatically set -* Backups ([duplicity](http://duplicity.nongnu.org/)), firewall ([ufw](https://launchpad.net/ufw)), intrusion protection ([fail2ban](http://www.fail2ban.org/wiki/index.php/Main_Page)), system monitoring ([munin](http://munin-monitoring.org/)) +* SMTP ([postfix](http://www.postfix.org/)), IMAP ([Dovecot](http://dovecot.org/)), CardDAV/CalDAV ([Nextcloud](https://nextcloud.com/)), and Exchange ActiveSync ([z-push](http://z-push.org/)) servers +* Webmail ([Roundcube](http://roundcube.net/)), mail filter rules (thanks to Roundcube and Dovecot), and email client autoconfig settings (served by [nginx](http://nginx.org/)) +* Spam filtering ([spamassassin](https://spamassassin.apache.org/)) and greylisting ([postgrey](http://postgrey.schweikert.ch/)) +* DNS ([nsd4](https://www.nlnetlabs.nl/projects/nsd/)) with [SPF](https://en.wikipedia.org/wiki/Sender_Policy_Framework), DKIM ([OpenDKIM](http://www.opendkim.org/)), [DMARC](https://en.wikipedia.org/wiki/DMARC), [DNSSEC](https://en.wikipedia.org/wiki/DNSSEC), [DANE TLSA](https://en.wikipedia.org/wiki/DNS-based_Authentication_of_Named_Entities), [MTA-STS](https://tools.ietf.org/html/rfc8461), and [SSHFP](https://tools.ietf.org/html/rfc4255) policy records automatically set +* TLS certificates are automatically provisioned using [Let's Encrypt](https://letsencrypt.org/) for protecting https and all of the other services on the box +* Backups ([duplicity](http://duplicity.nongnu.org/)), firewall ([ufw](https://launchpad.net/ufw)), intrusion protection ([fail2ban](http://www.fail2ban.org/wiki/index.php/Main_Page)), and basic system monitoring ([munin](http://munin-monitoring.org/)) -It also includes: +It also includes system management tools: -* A control panel and API for adding/removing mail users, aliases, custom DNS records, etc. and detailed system monitoring. +* Comprehensive health monitoring that checks each day that services are running, ports are open, TLS certificates are valid, and DNS records are correct +* A control panel for adding/removing mail users, aliases, custom DNS records, configuring backups, etc. +* An API for all of the actions on the control panel + +It also supports static website hosting since the box is serving HTTPS anyway. (To serve a website for your domains elsewhere, just add a custom DNS "A" record in you Mail-in-a-Box's control panel to point domains to another server.) For more information on how Mail-in-a-Box handles your privacy, see the [security details page](security.md). + Installation ------------ @@ -238,7 +249,7 @@ by him: $ curl -s https://keybase.io/joshdata/key.asc | gpg --import gpg: key C10BDD81: public key "Joshua Tauberer " imported - $ git verify-tag v0.48 + $ git verify-tag v0.50 gpg: Signature made ..... using RSA key ID C10BDD81 gpg: Good signature from "Joshua Tauberer " gpg: WARNING: This key is not certified with a trusted signature! @@ -251,7 +262,7 @@ and on his [personal homepage](https://razor.occams.info/). (Of course, if this Checkout the tag corresponding to the most recent release: - $ git checkout v0.48 + $ git checkout v0.50 Begin the installation. @@ -261,6 +272,9 @@ For help, DO NOT contact Josh directly --- I don't do tech support by email or t Post your question on the [discussion forum](https://discourse.mailinabox.email/) instead, where maintainers and Mail-in-a-Box users may be able to help you. +Note that while we want everything to "just work," we can't control the rest of the Internet. Other mail services might block or spam-filter email sent from your Mail-in-a-Box. +This is a challenge faced by everyone who runs their own mail server, with or without Mail-in-a-Box. See our discussion forum for tips about that. + Contributing and Development ---------------------------- @@ -274,6 +288,7 @@ This project was inspired in part by the ["NSA-proof your email in 2 hours"](htt Mail-in-a-Box is similar to [iRedMail](http://www.iredmail.org/) and [Modoboa](https://github.com/tonioo/modoboa). + The History ----------- diff --git a/api/docs/generate-docs.sh b/api/docs/generate-docs.sh new file mode 100755 index 00000000..e7951d8a --- /dev/null +++ b/api/docs/generate-docs.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env sh + +# Requirements: +# - Node.js +# - redoc-cli (`npm install redoc-cli -g`) + +redoc-cli bundle ../mailinabox.yml \ + -t template.hbs \ + -o api-docs.html \ + --templateOptions.metaDescription="Mail-in-a-Box HTTP API" \ + --title="Mail-in-a-Box HTTP API" \ + --options.expandSingleSchemaField \ + --options.hideSingleRequestSampleTab \ + --options.jsonSampleExpandLevel=10 \ + --options.hideDownloadButton \ + --options.theme.logo.maxHeight=180px \ + --options.theme.logo.maxWidth=180px \ + --options.theme.colors.primary.main="#C52" \ + --options.theme.typography.fontSize=16px \ + --options.theme.typography.fontFamily="Raleway, sans-serif" \ + --options.theme.typography.headings.fontFamily="Ubuntu, Arial, sans-serif" \ + --options.theme.typography.code.fontSize=15px \ + --options.theme.typography.code.fontFamily='"Source Code Pro", monospace' \ No newline at end of file diff --git a/api/docs/template.hbs b/api/docs/template.hbs new file mode 100644 index 00000000..0de7d222 --- /dev/null +++ b/api/docs/template.hbs @@ -0,0 +1,31 @@ + + + + + + {{title}} + + + + + + + + + {{{redocHead}}} + + + + {{{redocHTML}}} + + + diff --git a/api/mailinabox.yml b/api/mailinabox.yml new file mode 100644 index 00000000..57ba5aa4 --- /dev/null +++ b/api/mailinabox.yml @@ -0,0 +1,2531 @@ +openapi: 3.0.3 +info: + title: Mail-in-a-Box + description: | + Mail-in-a-Box API HTTP specification. + + # Introduction + This API is documented in [**OpenAPI format**](http://spec.openapis.org/oas/v3.0.3). + ([View the full HTTP specification](https://raw.githubusercontent.com/mail-in-a-box/mailinabox/api-spec/api/mailinabox.yml).) + + All endpoints are relative to `https://{host}/admin` and are secured with [`Basic Access` authentication](https://en.wikipedia.org/wiki/Basic_access_authentication). + contact: + name: Mail-in-a-Box support + url: https://mailinabox.email/ + license: + name: CC0 1.0 Universal + url: https://creativecommons.org/publicdomain/zero/1.0/legalcode + version: 0.47.0 + x-logo: + url: https://mailinabox.email/static/logo.png + altText: Mail-in-a-Box logo +externalDocs: + description: Find out more about Mail-in-a-box. + url: https://mailinabox.email/ +servers: + - url: https://{host}/admin + variables: + host: + default: box.example.com + description: The API hostname. +security: + - basicAuth: [] +tags: + - name: User + description: Endpoints related to user authentication. + - name: Mail + description: | + Mail operations, which include getting all users, getting all aliases, adding/updating/removing users and aliases and getting all mail domains. + - name: DNS + description: | + DNS operations, which include adding custom records, adding a secondary nameserver and viewing all DNS records. + - name: SSL + description: | + TLS (SSL) Certificates operations, which include checking certificate status + and installing custom certificates. + - name: Web + description: | + Static web hosting operations, which include getting domain information and updating domain root directories. + - name: System + description: | + System operations, which include system status checks, new version checks + and reboot status. +paths: + /me: + get: + tags: + - User + summary: Get user information + description: | + Returns user information. Used for user authentication. + + Authenticate a user by supplying the auth token as a base64 encoded string in + format `email:password` using basic authentication headers. + + If successful, a long-lived `api_key` is returned which can be used for subsequent + requests to the API. + operationId: getMe + x-codeSamples: + - lang: curl + source: | + curl -X GET "https://{host}/admin/me" \ + -u ":" + responses: + 200: + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/MeResponse' + examples: + invalid: + value: + reason: Incorrect username or password + status: invalid + ok: + value: + api_key: 1a2b3c4d5e6f7g8h9i0j + email: user@example.com + privileges: + - admin + status: ok + /system/status: + post: + tags: + - System + summary: Get system status + description: | + Returns an array of statuses which can include headings. + operationId: getSystemStatus + x-codeSamples: + - lang: curl + source: | + curl -X POST "https://{host}/admin/system/status" \ + -u ":" + responses: + 200: + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/SystemStatusResponse' + example: + - type: heading + text: System + extra: [] + - type: warning + text: This domain's DNSSEC DS record is not set + extra: + - monospace: false + text: 'Digest Type: 2 / SHA-25' + 403: + description: Forbidden + content: + text/html: + schema: + type: string + /system/version: + get: + tags: + - System + summary: Get system version + description: Returns installed Mail-in-a-Box version. + operationId: getSystemVersion + x-codeSamples: + - lang: curl + source: | + curl -X GET "https://{host}/admin/system/version" \ + -u ":" + responses: + 200: + description: Successful operation + content: + text/html: + schema: + $ref: '#/components/schemas/SystemVersionResponse' + example: v0.46 + 403: + description: Forbidden + content: + text/html: + schema: + type: string + /system/latest-upstream-version: + post: + tags: + - System + summary: Get system upstream version + description: Returns Mail-in-a-Box upstream version. + operationId: getSystemUpstreamVersion + x-codeSamples: + - lang: curl + source: | + curl -X POST "https://{host}/admin/system/latest-upstream-version" \ + -u ":" + responses: + 200: + description: Successful operation + content: + text/html: + schema: + $ref: '#/components/schemas/SystemVersionUpstreamResponse' + example: v0.47 + 403: + description: Forbidden + content: + text/html: + schema: + type: string + /system/updates: + get: + tags: + - System + summary: Get system updates + description: Returns system (apt) updates. + operationId: getSystemUpdates + x-codeSamples: + - lang: curl + source: | + curl -X GET "https://{host}/admin/system/updates" \ + -u ":" + responses: + 200: + description: Successful operation + content: + text/html: + schema: + $ref: '#/components/schemas/SystemUpdatesResponse' + example: | + libgnutls30 (3.5.18-1ubuntu1.4) + libxau6 (1:1.0.8-1ubuntu1) + 403: + description: Forbidden + content: + text/html: + schema: + type: string + /system/update-packages: + post: + tags: + - System + summary: Update system packages + description: Updates system (apt) packages. + operationId: updateSystemPackages + x-codeSamples: + - lang: curl + source: | + curl -X POST "https://{host}/admin/system/update-packages" \ + -u ":" + responses: + 200: + description: Successful operation + content: + text/html: + schema: + $ref: '#/components/schemas/SystemUpdatePackagesResponse' + example: | + Calculating upgrade... + The following packages will be upgraded: + cloud-init grub-common + 403: + description: Forbidden + content: + text/html: + schema: + type: string + /system/privacy: + get: + tags: + - System + summary: Get system privacy status + description: | + Returns system privacy (new-version check) status. + + Response: + + - `true`: Private, new-version checks will not be performed + - `false`: Not private, new-version checks will be performed + operationId: getSystemPrivacyStatus + x-codeSamples: + - lang: curl + source: | + curl -X GET "https://{host}/admin/system/privacy" \ + -u ":" + responses: + 200: + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/SystemPrivacyStatusResponse' + 403: + description: Forbidden + content: + text/html: + schema: + type: string + post: + tags: + - System + summary: Update system privacy + description: | + Updates system privacy (new-version checks). + + Request: + + - `value: private`: Disable new version checks + - `value: off`: Enable new version checks + operationId: updateSystemPrivacy + requestBody: + required: true + content: + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/SystemPrivacyUpdateRequest' + examples: + enable: + summary: Enable new version checks + value: + value: 'off' + disable: + summary: Disable new version checks + value: + value: private + x-codeSamples: + - lang: curl + source: | + curl -X POST "https://{host}/admin/system/privacy" \ + -d "value=" \ + -u ":" + responses: + 200: + description: Successful operation + content: + text/html: + schema: + $ref: '#/components/schemas/SystemPrivacyUpdateResponse' + example: OK + 400: + description: Bad request + content: + text/html: + schema: + type: string + 403: + description: Forbidden + content: + text/html: + schema: + type: string + /system/reboot: + get: + tags: + - System + summary: Get system reboot status + description: | + Returns the system reboot status. + + Response: + + - `true`: A reboot is required + - `false`: A reboot is not required + operationId: getSystemRebootStatus + x-codeSamples: + - lang: curl + source: | + curl -X GET "https://{host}/admin/system/reboot" \ + -u ":" + responses: + 200: + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/SystemRebootStatusResponse' + 403: + description: Forbidden + content: + text/html: + schema: + type: string + post: + tags: + - System + summary: Reboot system + description: Reboots the system. + operationId: rebootSystem + x-codeSamples: + - lang: curl + source: | + curl -X POST "https://{host}/admin/system/reboot" \ + -u ":" + responses: + 200: + description: Successful operation + content: + text/html: + schema: + $ref: '#/components/schemas/SystemRebootResponse' + example: No reboot is required, so it is not allowed. + 403: + description: Forbidden + content: + text/html: + schema: + type: string + /system/backup/status: + get: + tags: + - System + summary: Get system backup status + description: | + Returns the system backup status. + + If the list of backups is empty, this implies no backups have been made yet. + operationId: getSystemBackupStatus + x-codeSamples: + - lang: curl + source: | + curl -X GET "https://{host}/admin/system/backup/status" \ + -u ":" + responses: + 200: + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/SystemBackupStatusResponse' + 403: + description: Forbidden + content: + text/html: + schema: + type: string + /system/backup/config: + get: + tags: + - System + summary: Get system backup config + description: Returns the system backup config. + operationId: getSystemBackupConfig + x-codeSamples: + - lang: curl + source: | + curl -X GET "https://{host}/admin/system/backup/config" \ + -u ":" + responses: + 200: + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/SystemBackupConfigResponse' + 403: + description: Forbidden + content: + text/html: + schema: + type: string + post: + tags: + - System + summary: Update system backup config + description: Updates the system backup config. + operationId: updateSystemBackupConfig + requestBody: + required: true + content: + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/SystemBackupConfigUpdateRequest' + examples: + s3: + summary: S3 backup + value: + target: s3://s3.eu-central-1.amazonaws.com/box-example-com + target_user: ACCESS_KEY + target_pass: SECRET_ACCESS_KEY + minAge: 3 + local: + summary: Local backup + value: + target: local + target_user: '' + target_pass: '' + minAge: 3 + rsync: + summary: Rsync backup + value: + target: rsync://username@box.example.com//backups/box.example.com + target_user: '' + target_pass: '' + minAge: 3 + off: + summary: Disable backups + value: + target: 'off' + target_user: '' + target_pass: '' + minAge: 0 + x-codeSamples: + - lang: curl + source: | + curl -X POST "https://{host}/admin/system/backup/config" \ + -d "target=" \ + -d "target_user=" \ + -d "target_pass=" \ + -d "min_age=" \ + -u ":" + responses: + 200: + description: Successful operation + content: + text/html: + schema: + $ref: '#/components/schemas/SystemBackupConfigUpdateResponse' + example: OK + 400: + description: Bad request + content: + text/html: + schema: + type: string + 403: + description: Forbidden + content: + text/html: + schema: + type: string + /ssl/status: + get: + tags: + - SSL + summary: Get SSL status + description: Returns the SSL status for all domains. + operationId: getSSLStatus + x-codeSamples: + - lang: curl + source: | + curl -X GET "https://{host}/admin/ssl/status" \ + -u ":" + responses: + 200: + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/SSLStatusResponse' + 403: + description: Forbidden + content: + text/html: + schema: + type: string + /ssl/csr/{domain}: + post: + tags: + - SSL + summary: Generate SSL CSR + description: | + Generates a Certificate Signing Request (CSR) for a domain & country code. + operationId: generateSSLCSR + parameters: + - in: path + name: domain + schema: + $ref: '#/components/schemas/Hostname' + required: true + description: Domain to generate CSR for. + requestBody: + required: true + content: + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/SSLCSRGenerateRequest' + example: + countrycode: 'GB' + x-codeSamples: + - lang: curl + source: | + curl -X POST "https://{host}/admin/ssl/csr/" \ + -d "countrycode=" \ + -u ":" + responses: + 200: + description: Successful operation + content: + text/html: + schema: + $ref: '#/components/schemas/SSLCSRGenerateResponse' + example: | + -----BEGIN CERTIFICATE REQUEST----- + MIICaDCCAVACAQAwIzELMAkGA1UEBhMCQlMxFDASBgNVBAMMC2V4YW1wbGUuY29t + ... + JmFDQESSfUxLPHLC660Wnf3GmrP/duZHpPC+qTe8b1AlQ7zDT3cOaAQ+Mb0= + -----END CERTIFICATE REQUEST----- + 400: + description: Bad request + content: + text/html: + schema: + type: string + 403: + description: Forbidden + content: + text/html: + schema: + type: string + /ssl/install: + post: + tags: + - SSL + summary: Install SSL certificate + description: | + Installs a custom certificate. The chain certificate is optional. + operationId: installSSLCertificate + requestBody: + required: true + content: + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/SSLCertificateInstallRequest' + example: + domain: example.com + cert: CERT_STRING + chain: CHAIN_STRING + x-codeSamples: + - lang: curl + source: | + curl -X POST "https://{host}/admin/ssl/install" \ + -d "domain=" \ + -d "cert=" \ + -d "chain=" \ + -u ":" + responses: + 200: + description: Successful operation + content: + text/html: + schema: + $ref: '#/components/schemas/SSLCertificateInstallResponse' + example: OK + 400: + description: Bad request + content: + text/html: + schema: + type: string + 403: + description: Forbidden + content: + text/html: + schema: + type: string + /ssl/provision: + post: + tags: + - SSL + summary: Provision SSL certificates + description: | + Provisions certificates for all domains. + operationId: provisionSSLCertificates + x-codeSamples: + - lang: curl + source: | + curl -X POST "https://{host}/admin/ssl/provision" \ + -u ":" + responses: + 200: + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/SSLCertificatesProvisionResponse' + 403: + description: Forbidden + content: + text/html: + schema: + type: string + /dns/secondary-nameserver: + get: + tags: + - DNS + summary: Get DNS secondary nameserver + description: | + Returns a list of nameserver hostnames. + operationId: getDnsSecondaryNameserver + x-codeSamples: + - lang: curl + source: | + curl -X GET "https://{host}/admin/dns/secondary-nameserver" \ + -u ":" + responses: + 200: + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/DNSSecondaryNameserverResponse' + 403: + description: Forbidden + content: + text/html: + schema: + type: string + post: + tags: + - DNS + summary: Add DNS secondary nameserver + description: | + Adds one or more secondary nameservers. + operationId: addDnsSecondaryNameserver + requestBody: + required: true + content: + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/DNSSecondaryNameserverAddRequest' + example: + hostnames: ns2.hostingcompany.com, ns3.hostingcompany.com + x-codeSamples: + - lang: curl + source: | + curl -X POST "https://{host}/admin/dns/secondary-nameserver" \ + -d "hostnames=" \ + -u ":" + responses: + 200: + description: Successful operation + content: + text/html: + schema: + $ref: '#/components/schemas/DNSSecondaryNameserverAddResponse' + example: 'updated DNS: example.com' + 400: + description: Bad request + content: + text/html: + schema: + type: string + example: Could not resolve the IP address of badhostname + 403: + description: Forbidden + content: + text/html: + schema: + type: string + /dns/zones: + get: + tags: + - DNS + summary: Get DNS zones + description: Returns an array of all managed top-level domains. + operationId: getDnsZones + x-codeSamples: + - lang: curl + source: | + curl -X GET "https://{host}/admin/dns/zones" \ + -u ":" + responses: + 200: + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/DNSZonesResponse' + 403: + description: Forbidden + content: + text/html: + schema: + type: string + /dns/update: + post: + tags: + - DNS + summary: Update DNS + description: Updates the DNS. Involves creating zone files and restarting `nsd`. + operationId: updateDns + requestBody: + required: true + content: + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/DNSUpdateRequest' + example: + force: 1 + x-codeSamples: + - lang: curl + source: | + curl -X POST "https://{host}/admin/dns/update" \ + -d "force=" \ + -u ":" + responses: + 200: + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/DNSUpdateResponse' + 400: + description: Bad request + content: + text/html: + schema: + type: string + 403: + description: Forbidden + content: + text/html: + schema: + type: string + /dns/custom: + get: + tags: + - DNS + summary: Get DNS custom records + description: Returns all custom DNS records. + operationId: getDnsCustomRecords + x-codeSamples: + - lang: curl + source: | + curl -X GET "https://{host}/admin/dns/custom" \ + -u ":" + responses: + 200: + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/DNSCustomRecordsResponse' + 403: + description: Forbidden + content: + text/html: + schema: + type: string + /dns/custom/{qname}/{rtype}: + parameters: + - in: path + name: qname + schema: + $ref: '#/components/schemas/Hostname' + required: true + description: DNS record query name + - in: path + name: rtype + schema: + $ref: '#/components/schemas/DNSRecordType' + required: true + description: Record type + get: + tags: + - DNS + summary: Get DNS custom records + description: Returns all custom records for the specified query name and type. + operationId: getDnsCustomRecordsForQNameAndType + x-codeSamples: + - lang: curl + source: | + curl -X GET "https://{host}/admin/dns/custom//" \ + -u ":" + responses: + 200: + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/DNSCustomRecordsResponse' + 403: + description: Forbidden + content: + text/html: + schema: + type: string + post: + tags: + - DNS + summary: Add DNS custom record + description: Adds a custom DNS record for the specified query name and type. + operationId: addDnsCustomRecord + requestBody: + $ref: '#/components/requestBodies/DNSCustomRecordRequest' + x-codeSamples: + - lang: curl + source: | + curl -X POST "https://{host}/admin/dns/custom//" \ + -H "Content-Type: text/plain" \ + --data-raw "" \ + -u ":" + responses: + 200: + description: Successful operation + content: + text/html: + schema: + $ref: '#/components/schemas/DNSCustomRecordUpsertResponse' + example: 'updated DNS: example.com' + 400: + description: Bad request + content: + text/html: + schema: + type: string + example: "'badhostname' does not appear to be an IPv4 or IPv6 address" + 403: + description: Forbidden + content: + text/html: + schema: + type: string + put: + tags: + - DNS + summary: Update DNS custom record + description: Updates an existing DNS custom record value for the specified qname and type. + operationId: updateDnsCustomRecord + requestBody: + $ref: '#/components/requestBodies/DNSCustomRecordRequest' + x-codeSamples: + - lang: curl + source: | + curl -x PUT "https://{host}/admin/dns/custom//" \ + -H "Content-Type: text/plain" \ + --data-raw "" \ + -u ":" + responses: + 200: + description: Successful operation + content: + text/html: + schema: + $ref: '#/components/schemas/DNSCustomRecordUpsertResponse' + example: 'updated DNS: example.com' + 400: + description: Bad request + content: + text/html: + schema: + type: string + example: "'badhostname' does not appear to be an IPv4 or IPv6 address" + 403: + description: Forbidden + content: + text/html: + schema: + type: string + delete: + tags: + - DNS + summary: Remove DNS custom record + description: Removes a DNS custom record for the specified domain, type & value. + operationId: removeDnsCustomRecord + requestBody: + $ref: '#/components/requestBodies/DNSCustomRecordRequest' + x-codeSamples: + - lang: curl + source: | + curl -X DELETE "https://{host}/admin/dns/custom//" \ + -H "Content-Type: text/plain" \ + --data-raw "" \ + -u ":" + responses: + 200: + description: Successful operation + content: + text/html: + schema: + $ref: '#/components/schemas/DNSCustomRecordRemoveResponse' + example: 'updated DNS: example.com' + 400: + description: Bad request + content: + text/html: + schema: + type: string + example: badhostname is not a domain name or a subdomain of a domain name managed by this box + 403: + description: Forbidden + content: + text/html: + schema: + type: string + /dns/custom/{qname}: + parameters: + - in: path + name: qname + schema: + $ref: '#/components/schemas/Hostname' + required: true + description: DNS query name. + get: + tags: + - DNS + summary: Get DNS custom A records + description: Returns all custom A records for the specified query name. + operationId: getDnsCustomARecordsForQName + x-codeSamples: + - lang: curl + source: | + curl -X GET "https://{host}/admin/dns/custom/" \ + -u ":" + responses: + 200: + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/DNSCustomRecordsResponse' + 403: + description: Forbidden + content: + text/html: + schema: + type: string + post: + tags: + - DNS + summary: Add DNS custom A record + description: Adds a custom DNS A record for the specified query name. + operationId: addDnsCustomARecord + requestBody: + $ref: '#/components/requestBodies/DNSCustomRecordRequest' + x-codeSamples: + - lang: curl + source: | + curl -X POST "https://{host}/admin/dns/custom/" \ + -H "Content-Type: text/plain" \ + --data-raw "" \ + -u ":" + responses: + 200: + description: Successful operation + content: + text/html: + schema: + $ref: '#/components/schemas/DNSCustomRecordUpsertResponse' + example: 'updated DNS: example.com' + 400: + description: Bad request + content: + text/html: + schema: + type: string + example: "'badhostname' does not appear to be an IPv4 or IPv6 address" + 403: + description: Forbidden + content: + text/html: + schema: + type: string + put: + tags: + - DNS + summary: Update DNS custom A record + description: Updates an existing DNS custom A record value for the specified qname. + operationId: updateDnsCustomARecord + requestBody: + $ref: '#/components/requestBodies/DNSCustomRecordRequest' + x-codeSamples: + - lang: curl + source: | + curl -x PUT "https://{host}/admin/dns/custom/" \ + -H "Content-Type: text/plain" \ + --data-raw "" \ + -u ":" + responses: + 200: + description: Successful operation + content: + text/html: + schema: + $ref: '#/components/schemas/DNSCustomRecordUpsertResponse' + example: 'updated DNS: example.com' + 400: + description: Bad request + content: + text/html: + schema: + type: string + example: "'badhostname' does not appear to be an IPv4 or IPv6 address" + 403: + description: Forbidden + content: + text/html: + schema: + type: string + delete: + tags: + - DNS + summary: Remove DNS custom A record + description: Removes a DNS custom A record for the specified domain & value. + operationId: removeDnsCustomARecord + requestBody: + $ref: '#/components/requestBodies/DNSCustomRecordRequest' + x-codeSamples: + - lang: curl + source: | + curl -X DELETE "https://{host}/admin/dns/custom/" \ + -H "Content-Type: text/plain" \ + --data-raw "" \ + -u ":" + responses: + 200: + description: Successful operation + content: + text/html: + schema: + $ref: '#/components/schemas/DNSCustomRecordRemoveResponse' + example: 'updated DNS: example.com' + 400: + description: Bad request + content: + text/html: + schema: + type: string + example: badhostname is not a domain name or a subdomain of a domain name managed by this box + 403: + description: Forbidden + content: + text/html: + schema: + type: string + /dns/dump: + get: + tags: + - DNS + summary: Get DNS dump + description: Returns all DNS records. + operationId: getDnsDump + x-codeSamples: + - lang: curl + source: | + curl -X GET "https://{host}/admin/dns/dump" \ + -u ":" + responses: + 200: + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/DNSDumpResponse' + example: + - - example1.com + - - explanation: Required. Specifies the hostname (and priority) of the machine that handles @example.com mail. + qname: example1.com + rtype: MX + value: 10 box.example1.com. + - - example2.com + - - explanation: Required. Specifies the hostname (and priority) of the machine that handles @example.com mail. + qname: example2.com + rtype: MX + value: 10 box.example2.com. + 403: + description: Forbidden + content: + text/html: + schema: + type: string + /mail/users: + get: + tags: + - Mail + summary: Get mail users + description: Returns all mail users. + operationId: getMailUsers + parameters: + - in: query + name: format + schema: + $ref: '#/components/schemas/MailUsersResponseFormat' + description: The format of the response. + x-codeSamples: + - lang: curl + source: | + curl -X GET "https://{host}/admin/mail/users?format=" \ + -u ":" + responses: + 200: + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/MailUsersResponse' + text/html: + schema: + $ref: '#/components/schemas/MailUsersSimpleResponse' + example: | + user1@example.com + user2@example.com + 403: + description: Forbidden + content: + text/html: + schema: + type: string + /mail/users/add: + post: + tags: + - Mail + summary: Add mail user + description: Adds a new mail user. + operationId: addMailUser + requestBody: + required: true + content: + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/MailUserAddRequest' + examples: + normal: + summary: Normal user + value: + email: user@example.com + password: s3curE_pa5Sw0rD + privileges: '' + admin: + summary: Admin user + value: + email: user@example.com + password: s3curE_pa5Sw0rD + privileges: admin + x-codeSamples: + - lang: curl + source: | + curl -X POST "https://{host}/admin/mail/users/add" \ + -d "email=" \ + -d "password=" \ + -d "privileges=" \ + -u ":" + responses: + 200: + description: Successful operation + content: + text/html: + schema: + $ref: '#/components/schemas/MailUserAddResponse' + example: | + mail user added + updated DNS: OpenDKIM configuration + 400: + description: Bad request + content: + text/html: + schema: + type: string + example: Invalid email address + 403: + description: Forbidden + content: + text/html: + schema: + type: string + /mail/users/remove: + post: + tags: + - Mail + summary: Remove mail user + description: Removes an existing mail user. + operationId: removeMailUser + requestBody: + required: true + content: + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/MailUserRemoveRequest' + example: + email: user@example.com + x-codeSamples: + - lang: curl + source: | + curl -X POST "https://{host}/admin/mail/users/remove" \ + -d "email=" \ + -u ":" + responses: + 200: + description: Successful operation + content: + text/html: + schema: + $ref: '#/components/schemas/MailUserRemoveResponse' + example: OK + 400: + description: Bad request + content: + text/html: + schema: + type: string + example: That's not a user (invalid@example.com) + 403: + description: Forbidden + content: + text/html: + schema: + type: string + /mail/users/privileges/add: + post: + tags: + - Mail + summary: Add mail user privilege + description: Adds a privilege to an existing mail user. + operationId: addMailUserPrivilege + requestBody: + required: true + content: + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/MailUserAddPrivilegeRequest' + example: + email: user@example.com + privilege: admin + x-codeSamples: + - lang: curl + source: | + curl -X POST "https://{host}/admin/mail/users/privileges/add" \ + -d "email=" \ + -d "privilege=" \ + -u ":" + responses: + 200: + description: Successful operation + content: + text/html: + schema: + $ref: '#/components/schemas/MailUserAddPrivilegeResponse' + example: OK + 400: + description: Bad request + content: + text/html: + schema: + type: string + example: That's not a user (invalid@example.com) + 403: + description: Forbidden + content: + text/html: + schema: + type: string + /mail/users/privileges/remove: + post: + tags: + - Mail + summary: Remove mail user privilege + description: Removes a privilege from an existing mail user. + operationId: removeMailUserPrivilege + requestBody: + required: true + content: + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/MailUserRemovePrivilegeRequest' + example: + email: user@example.com + privilege: admin + x-codeSamples: + - lang: curl + source: | + curl -X POST "https://{host}/admin/mail/users/privileges/remove" \ + -d "email=" \ + -d "privilege=" \ + -u ":" + responses: + 200: + description: Successful operation + content: + text/html: + schema: + $ref: '#/components/schemas/MailUserRemovePrivilegeResponse' + example: OK + 400: + description: Bad request + content: + text/html: + schema: + type: string + example: That's not a user (invalid@example.com) + 403: + description: Forbidden + content: + text/html: + schema: + type: string + /mail/users/password: + post: + tags: + - Mail + summary: Set mail user password + description: Sets a password for an existing mail user. + operationId: setMailUserPassword + requestBody: + required: true + content: + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/MailUserSetPasswordRequest' + example: + email: user@example.com + password: s3curE_pa5Sw0rD + x-codeSamples: + - lang: curl + source: | + curl -X POST "https://{host}/admin/mail/users/password" \ + -d "email=" \ + -d "password=" \ + -u ":" \ + responses: + 200: + description: Successful operation + content: + text/html: + schema: + $ref: '#/components/schemas/MailUserSetPasswordResponse' + example: OK + 400: + description: Bad request + content: + text/html: + schema: + type: string + example: Passwords must be at least eight characters + 403: + description: Forbidden + content: + text/html: + schema: + type: string + /mail/users/privileges: + get: + tags: + - Mail + summary: Get mail user privileges + description: Returns all privileges for an existing mail user. + operationId: getMailUserPrivileges + parameters: + - in: query + name: email + schema: + $ref: '#/components/schemas/Email' + description: The email you want to get privileges for. + x-codeSamples: + - lang: curl + source: | + curl -X GET "https://{host}/admin/mail/users/privileges?email=" \ + -u ":" + responses: + 200: + description: Successful operation + content: + text/html: + schema: + $ref: '#/components/schemas/MailUserPrivilegesResponse' + example: admin + 403: + description: Forbidden + content: + text/html: + schema: + type: string + /mail/domains: + get: + tags: + - Mail + summary: Get mail domains + description: Returns all mail domains. + operationId: getMailDomains + x-codeSamples: + - lang: curl + source: | + curl -X GET "https://{host}/admin/mail/domains" \ + -u ":" + responses: + 200: + description: Successful operation + content: + text/html: + schema: + $ref: '#/components/schemas/MailDomainsResponse' + example: | + example1.com + example2.com + 403: + description: Forbidden + content: + text/html: + schema: + type: string + /mail/aliases: + get: + tags: + - Mail + summary: Get mail aliases + description: Returns all mail aliases. + operationId: getMailAliases + parameters: + - in: query + name: format + schema: + $ref: '#/components/schemas/MailAliasesResponseFormat' + description: The format of the response. + x-codeSamples: + - lang: curl + source: | + curl -X GET "https://{host}/admin/mail/aliases?format=" \ + -u ":" + responses: + 200: + description: Successful operation + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/MailAliasByDomain' + text/html: + schema: + $ref: '#/components/schemas/MailAliasesSimpleResponse' + example: | + abuse@example.com administrator@example.com + admin@example.com administrator@example.com + 403: + description: Forbidden + content: + text/html: + schema: + type: string + /mail/aliases/add: + post: + tags: + - Mail + summary: Upsert mail alias + description: | + Adds or updates a mail alias. If updating, you need to set `update_if_exists: 1`. + operationId: upsertMailAlias + requestBody: + required: true + content: + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/MailAliasUpsertRequest' + examples: + regular: + summary: Regular alias + value: + update_if_exists: 0 + address: user@example.com + forwards_to: user2@example.com + permitted_senders: + catchall: + summary: Catch-all + value: + update_if_exists: 0 + address: '@example.com' + forwards_to: user@otherexample.com + permitted_senders: + domainalias: + summary: Domain alias + value: + update_if_exists: 0 + address: '@example.com' + forwards_to: '@otherexample.com' + permitted_senders: + update: + summary: Update existing alias + value: + update_if_exists: 1 + address: user@example.com + forwards_to: user2@example.com + permitted_senders: user3@example.com, user4@example.com + x-codeSamples: + - lang: curl + source: | + curl -X POST "https://{host}/admin/mail/aliases/add" \ + -d "update_if_exists=" \ + -d "address=" \ + -d "forwards_to=" \ + -d "permitted_senders=" \ + -u ":" + responses: + 200: + description: Successful operation + content: + text/html: + schema: + $ref: '#/components/schemas/MailAliasUpsertResponse' + example: alias updated + 400: + description: Bad request + content: + text/html: + schema: + type: string + example: Invalid email address (invalid@example.com) + 403: + description: Forbidden + content: + text/html: + schema: + type: string + /mail/aliases/remove: + post: + tags: + - Mail + summary: Remove mail alias + description: Removes a mail alias. + operationId: removeMailAlias + requestBody: + required: true + content: + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/MailAliasRemoveRequest' + example: + address: user@example.com + x-codeSamples: + - lang: curl + source: | + curl -X POST "https://{host}/admin/mail/aliases/remove" \ + -d "address=" \ + -u ":" + responses: + 200: + description: Successful operation + content: + text/html: + schema: + $ref: '#/components/schemas/MailAliasRemoveResponse' + example: alias removed + 400: + description: Bad request + content: + text/html: + schema: + type: string + example: That's not an alias (invalid@example) + 403: + description: Forbidden + content: + text/html: + schema: + type: string + /web/domains: + get: + tags: + - Web + summary: Get web domains + description: Returns all static web domains. + operationId: getWebDomains + x-codeSamples: + - lang: curl + source: | + curl -X GET "https://{host}/admin/web/domains" \ + -u ":" + responses: + 200: + description: Successful operation + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/WebDomain' + 403: + description: Forbidden + content: + text/html: + schema: + type: string + /web/update: + post: + tags: + - Web + summary: Update web + description: Updates static websites, used for updating domain root directories. + operationId: updateWeb + x-codeSamples: + - lang: curl + source: | + curl -X POST "https://{host}/admin/web/update" \ + -u ":" + responses: + 200: + description: Successful operation + content: + text/html: + schema: + $ref: '#/components/schemas/WebUpdateResponse' + example: web updated + 403: + description: Forbidden + content: + text/html: + schema: + type: string +components: + securitySchemes: + basicAuth: + type: http + scheme: basic + description: | + Credentials can be supplied using the `Authorization` header in + format `Authorization: Basic {access-token}`. + + The `access-token` is comprised of the Base64 encoding of `username:password`. + The `username` is the mail user's email address, and `password` can either be the mail user's + password, or the `api_key` returned from the `getMe` operation. + + When using `curl`, you can supply user credentials using the `-u` or `--user` parameter. + requestBodies: + DNSCustomRecordRequest: + required: true + content: + text/plain: + schema: + type: string + example: 1.2.3.4 + description: The value of the DNS record. + example: '1.2.3.4' + schemas: + MailUsersResponseFormat: + type: string + enum: + - text + - json + example: json + description: Response format (`application/json` or `text/html`). + MailAliasesResponseFormat: + type: string + enum: + - text + - json + example: json + description: Response format (`application/json` or `text/html`). + MailUserSetPasswordResponse: + type: string + example: OK + description: Mail user set password response. + MailUserRemoveResponse: + type: string + example: OK + description: Mail user remove response. + MailUserAddResponse: + type: string + example: | + mail user added + updated DNS: OpenDKIM configuration + description: | + Mail user add response. + + Can include information about operations related to adding new users, like updating DNS. + MailUserAddPrivilegeResponse: + type: string + example: OK + description: Mail user add admin privilege response. + MailUserRemovePrivilegeResponse: + type: string + example: OK + description: Mail user remove admin privilege response. + MailUsersSimpleResponse: + type: string + example: | + user1@example.com + user2@example.com + description: Get mail users text format response. + MailUserPrivilegesResponse: + $ref: '#/components/schemas/MailUserPrivilege' + description: Mail user privileges response. + example: admin + MailDomainsResponse: + type: string + example: | + example1.com + example2.com + description: Mail domains response. + MailUsersResponse: + type: array + items: + $ref: '#/components/schemas/MailUserByDomain' + description: Get mail aliases JSON format response. + MailUserByDomain: + type: object + required: + - domain + - users + properties: + domain: + $ref: '#/components/schemas/Hostname' + users: + type: array + items: + $ref: '#/components/schemas/MailUser' + description: Mail users by domain. + MailUser: + type: object + required: + - email + - privileges + - status + properties: + email: + $ref: '#/components/schemas/Email' + privileges: + type: array + items: + $ref: '#/components/schemas/MailUserPrivilege' + status: + $ref: '#/components/schemas/MailUserStatus' + mailbox: + type: string + example: /home/user-data/mail/mailboxes/example.com/user + description: Mail user details. + MailAliasesSimpleResponse: + type: string + example: | + abuse@example.com administrator@example.com + admin@example.com administrator@example.com + description: Get mail aliases text format response. + MailAliasByDomain: + type: object + required: + - domain + - aliases + properties: + domain: + $ref: '#/components/schemas/Hostname' + aliases: + type: array + items: + $ref: '#/components/schemas/MailAlias' + description: Mail aliases by domain. + MailAlias: + type: object + required: + - address + - address_display + - forwards_to + - permitted_senders + - required + properties: + address: + $ref: '#/components/schemas/Email' + address_display: + $ref: '#/components/schemas/Email' + forwards_to: + type: array + items: + $ref: '#/components/schemas/Email' + permitted_senders: + type: array + nullable: true + items: + $ref: '#/components/schemas/Email' + required: + type: boolean + example: true + description: Mail alias details. + MailAliasUpsertResponse: + type: string + example: alias updated + description: Mail alias add/update response. + MailAliasUpsertRequest: + type: object + required: + - update_if_exists + - address + - forwards_to + - permitted_senders + properties: + update_if_exists: + type: integer + format: int32 + minimum: 0 + maximum: 1 + example: 1 + description: Set to `1` when updating an alias. + address: + $ref: '#/components/schemas/Email' + forwards_to: + type: string + example: user1@example.com, user2@example.com + description: | + If adding a regular or catch-all alias, the format needs to be `user@example.com`. + Multiple address can be separated by newlines or commas. + + If adding a domain alias, the format needs to be `@example.com`. + permitted_senders: + type: string + nullable: true + example: user1@example.com, user2@example.com + description: | + Mail users that can send mail claiming to be from any address on the alias domain. + Multiple address can be separated by newlines or commas. + + Leave empty to allow any mail user listed in `forwards_to` to send mail claiming to be from any address on the alias domain. + description: Mail alias upsert request. + MailAliasRemoveResponse: + type: string + example: alias removed + description: Mail alias remove response. + MailAliasRemoveRequest: + type: object + required: + - address + properties: + address: + $ref: '#/components/schemas/Email' + description: Mail aliases remove request. + DNSRecordType: + enum: + - A + - AAAA + - CAA + - CNAME + - TXT + - MX + - SRV + - SSHFP + - NS + example: MX + description: DNS record type. + DNSDumpResponse: + type: array + items: + $ref: '#/components/schemas/DNSDumpDomains' + description: DNS dump response. + DNSDumpDomains: + type: array + items: + oneOf: + - $ref: '#/components/schemas/Hostname' + - $ref: '#/components/schemas/DNSDumpDomainRecords' + description: | + A list of records per domain. + + The first item in the list is the domain and the second item is the list of records. + DNSDumpDomainRecords: + type: array + items: + $ref: '#/components/schemas/DNSDumpDomainRecord' + description: List of domain records. + DNSDumpDomainRecord: + type: object + required: + - explanation + - qname + - type + - value + properties: + explanation: + type: string + example: Required. Specifies the hostname (and priority) of the machine that handles @example.com mail + qname: + $ref: '#/components/schemas/Hostname' + rtype: + $ref: '#/components/schemas/DNSRecordType' + value: + type: string + example: 10 example.com. + description: Domain DNS record details. + DNSCustomRecord: + type: object + required: + - qname + - rtype + - value + properties: + qname: + $ref: '#/components/schemas/Hostname' + rtype: + $ref: '#/components/schemas/DNSRecordType' + value: + type: string + example: 10 example.com. + description: Custom DNS record detail detail. + DNSCustomRecordsResponse: + type: array + items: + $ref: '#/components/schemas/DNSCustomRecord' + description: Custom DNS records response. + DNSZonesResponse: + type: array + items: + $ref: '#/components/schemas/Hostname' + description: DNS zones response. + DNSSecondaryNameserverResponse: + type: object + required: + - hostnames + properties: + hostnames: + type: array + items: + type: string + example: ns1.example.com + description: Secondary nameserver/s response. + DNSCustomRecordRemoveResponse: + type: string + example: 'updated DNS: example.com' + description: Custom DNS record remove response. + DNSCustomRecordUpsertResponse: + type: string + example: 'updated DNS: example.com' + description: Custom DNS record add response. + DNSUpdateRequest: + type: object + required: + - force + properties: + force: + type: integer + format: int32 + minimum: 0 + maximum: 1 + example: 1 + description: Force an update even if mailinabox detects no changes are required. + description: DNS update request. + DNSUpdateResponse: + type: string + example: | + updated DNS: example1.com,example2.com + description: DNS update response. + DNSSecondaryNameserverAddRequest: + type: object + required: + - hostnames + properties: + hostnames: + type: string + description: Hostnames separated with commas or spaces. + example: ns2.hostingcompany.com, ns3.hostingcompany.com + description: Secondary nameserver/s add request. + DNSSecondaryNameserverAddResponse: + type: string + example: 'updated DNS: example.com' + description: Secondary nameserver/s add response. + SystemPrivacyUpdateRequest: + type: object + required: + - value + properties: + value: + $ref: '#/components/schemas/SystemPrivacyStatus' + description: Update system privacy request. + SystemPrivacyStatus: + type: string + enum: + - private + - 'off' + example: private + description: System privacy status. + MailUserSetPasswordRequest: + type: object + required: + - email + - password + properties: + email: + $ref: '#/components/schemas/Email' + password: + type: string + format: password + description: Mail user set password request. + MailUserAddRequest: + type: object + required: + - email + - password + - privileges + properties: + email: + $ref: '#/components/schemas/Email' + password: + type: string + format: password + privileges: + $ref: '#/components/schemas/MailUserPrivilege' + description: Mail user add request. + MailUserRemoveRequest: + type: object + required: + - email + properties: + email: + $ref: '#/components/schemas/Email' + description: Mail user remove request. + MailUserStatus: + type: string + enum: + - active + - inactive + example: active + description: Mail user status. + MailUserPrivilege: + type: string + enum: + - admin + - '' + example: admin + description: Mail user privilege. + MailUserAddPrivilegeRequest: + type: object + required: + - email + - privilege + properties: + email: + $ref: '#/components/schemas/Email' + privilege: + $ref: '#/components/schemas/MailUserPrivilege' + description: Mail user add privilege request. + MailUserRemovePrivilegeRequest: + type: object + required: + - email + - privilege + properties: + email: + $ref: '#/components/schemas/Email' + privilege: + $ref: '#/components/schemas/MailUserPrivilege' + description: Mail user remove privilege request. + SSLCSRGenerateRequest: + type: object + required: + - countrycode + properties: + countrycode: + type: string + example: GB + description: Generate SSL CSR request. + SSLCSRGenerateResponse: + type: string + example: | + -----BEGIN CERTIFICATE REQUEST----- + MIICaDCCAVACAQAwIzELMAkGA1UEBhMCQlMxFDASBgNVBAMMC2V4YW1wbGUuY29t + MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA3K6dwLM2Nk8kVhIBaZmp + eY6y7O0T3jrexEKlW839TVYdcH+K35V1NxilbMFKMuHeowGwFyyiqOy/OUYNeq+T + Rz3s4b1qG2p01dwlsXHHYmXLYTAhvqvY+CU5ksieuZbyHRTwbHViQ0xtRXwoVCnj + CkN7kJVpkLfVN0/BG6NBFpv/JI8F+hwp+IHdkC1gUXRrLJNC79ERqFP8HoqdQWNw + OGGFaOe2aQhvj2zt8wFncyKVc40UKVbSzGGzdL2MPiAJHgZ2lmeY1xDyX1lOt12R + IFPwtxmbxaxYaVfe2hxl7m88xV3OjYcKgwVYDusk2XJ37cGew5g+NbBvzEeEUpF9 + 5wIDAQABoAAwDQYJKoZIhvcNAQELBQADggEBAD7UPC3/Nkgpn53mT9puUonYdJg9 + SD8vvTK/N78CzoEgPNyq+bYbqlcvVPKIdItf9TMiqfOSvW3e3NvkRisYle8Qp+0C + 8pafXBvQ9eHt5CFeJn4sH9GnxeflOZT/P9Jnp71KtZQvOobirX4GgEWs79g+/NHb + Zyf8rbadt9HruNhKA5nlP8cn7Rdc/iuJU8MVSQszI1s1DEcXMPxr6iqb2g87/ifH + lWcK59kvRJkCcPhPzjpUy9NulucH4WFA/WqKeDNFS/oC+upV5w8EDEcfnenJFG+N + JmFDQESSfUxLPHLC660Wnf3GmrP/duZHpPC+qTe8b1AlQ7zDT3cOaAQ+Mb0= + -----END CERTIFICATE REQUEST----- + description: Generate SSL CSR response. + SSLCertificateInstallRequest: + type: object + required: + - domain + - cert + - chain + properties: + domain: + $ref: '#/components/schemas/Hostname' + cert: + type: string + description: TLS/SSL certificate. + example: | + -----BEGIN CERTIFICATE----- + MIICaDCCAVACAQAwIzELMAkGA1UEBhMCQlMxFDASBgNVBAMMC2V4YW1wbGUuY29t + MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA3K6dwLM2Nk8kVhIBaZmp + eY6y7O0T3jrexEKlW839TVYdcH+K35V1NxilbMFKMuHeowGwFyyiqOy/OUYNeq+T + Rz3s4b1qG2p01dwlsXHHYmXLYTAhvqvY+CU5ksieuZbyHRTwbHViQ0xtRXwoVCnj + CkN7kJVpkLfVN0/BG6NBFpv/JI8F+hwp+IHdkC1gUXRrLJNC79ERqFP8HoqdQWNw + OGGFaOe2aQhvj2zt8wFncyKVc40UKVbSzGGzdL2MPiAJHgZ2lmeY1xDyX1lOt12R + IFPwtxmbxaxYaVfe2hxl7m88xV3OjYcKgwVYDusk2XJ37cGew5g+NbBvzEeEUpF9 + 5wIDAQABoAAwDQYJKoZIhvcNAQELBQADggEBAD7UPC3/Nkgpn53mT9puUonYdJg9 + SD8vvTK/N78CzoEgPNyq+bYbqlcvVPKIdItf9TMiqfOSvW3e3NvkRisYle8Qp+0C + 8pafXBvQ9eHt5CFeJn4sH9GnxeflOZT/P9Jnp71KtZQvOobirX4GgEWs79g+/NHb + Zyf8rbadt9HruNhKA5nlP8cn7Rdc/iuJU8MVSQszI1s1DEcXMPxr6iqb2g87/ifH + lWcK59kvRJkCcPhPzjpUy9NulucH4WFA/WqKeDNFS/oC+upV5w8EDEcfnenJFG+N + JmFDQESSfUxLPHLC660Wnf3GmrP/duZHpPC+qTe8b1AlQ7zDT3cOaAQ+Mb0= + -----END CERTIFICATE----- + chain: + type: string + description: TLS/SSL intermediate chain (if provided, else empty string). + example: | + -----BEGIN CERTIFICATE----- + MIICaDCCAVACAQAwIzELMAkGA1UEBhMCQlMxFDASBgNVBAMMC2V4YW1wbGUuY29t + MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA3K6dwLM2Nk8kVhIBaZmp + eY6y7O0T3jrexEKlW839TVYdcH+K35V1NxilbMFKMuHeowGwFyyiqOy/OUYNeq+T + Rz3s4b1qG2p01dwlsXHHYmXLYTAhvqvY+CU5ksieuZbyHRTwbHViQ0xtRXwoVCnj + CkN7kJVpkLfVN0/BG6NBFpv/JI8F+hwp+IHdkC1gUXRrLJNC79ERqFP8HoqdQWNw + OGGFaOe2aQhvj2zt8wFncyKVc40UKVbSzGGzdL2MPiAJHgZ2lmeY1xDyX1lOt12R + IFPwtxmbxaxYaVfe2hxl7m88xV3OjYcKgwVYDusk2XJ37cGew5g+NbBvzEeEUpF9 + 5wIDAQABoAAwDQYJKoZIhvcNAQELBQADggEBAD7UPC3/Nkgpn53mT9puUonYdJg9 + SD8vvTK/N78CzoEgPNyq+bYbqlcvVPKIdItf9TMiqfOSvW3e3NvkRisYle8Qp+0C + 8pafXBvQ9eHt5CFeJn4sH9GnxeflOZT/P9Jnp71KtZQvOobirX4GgEWs79g+/NHb + Zyf8rbadt9HruNhKA5nlP8cn7Rdc/iuJU8MVSQszI1s1DEcXMPxr6iqb2g87/ifH + lWcK59kvRJkCcPhPzjpUy9NulucH4WFA/WqKeDNFS/oC+upV5w8EDEcfnenJFG+N + JmFDQESSfUxLPHLC660Wnf3GmrP/duZHpPC+qTe8b1AlQ7zDT3cOaAQ+Mb0= + -----END CERTIFICATE----- + description: Install certificate request. `chain` can be an empty string. + SSLCertificateInstallResponse: + type: string + example: OK + description: Install certificate response. + SSLCertificatesProvisionResponse: + type: object + required: + - requests + properties: + requests: + type: array + items: + type: object + required: + - log + - result + - domains + properties: + log: + type: array + items: + type: string + example: + - 'The domain name does not resolve to this machine: [Not Set] (A), [Not Set] (AAAA).' + result: + type: string + enum: + - installed + - error + - skipped + example: installed + domains: + type: array + items: + $ref: '#/components/schemas/Hostname' + description: SSL certificates provision response. + SystemPrivacyStatusResponse: + type: boolean + description: | + System privacy status response. + + - `true`: Private, new-version checks will not be performed + - `false`: Not private, new-version checks will be performed + example: false + SystemVersionResponse: + type: string + description: System version response. + example: v0.46 + SystemVersionUpstreamResponse: + type: string + description: System version upstream response. + example: v0.47 + SystemUpdatesResponse: + type: string + description: System updates response. + example: | + libgnutls30 (3.5.18-1ubuntu1.4) + libxau6 (1:1.0.8-1ubuntu1) + SystemUpdatePackagesResponse: + type: string + example: | + Reading package lists... + Building dependency tree... + Reading state information... + Calculating upgrade... + The following packages will be upgraded: + cloud-init grub-common grub-pc grub-pc-bin grub2-common libgnutls30 + libldap-2.4-2 libldap-common libxau6 linux-firmware python3-distupgrade + qemu-guest-agent sosreport ubuntu-release-upgrader-core + 14 upgraded, 0 newly installed, 0 to remove and 0 not upgraded. + Need to get 79.9 MB of archives. + After this operation, 3893 kB of additional disk space will be used. + Get:1 http://archive.ubuntu.com/ubuntu bionic-updates/main amd64 libgnutls30 amd64 3.5.18-1ubuntu1.4 [645 kB] + Preconfiguring packages ... + Fetched 79.9 MB in 2s (52.4 MB/s) + (Reading database ... 48457 files and directories currently installed.) + description: System update packages response. + SystemPrivacyUpdateResponse: + type: string + example: OK + description: System privacy update response. + SystemRebootStatusResponse: + type: boolean + description: | + System reboot status response. + + - `true`: A reboot is required + - `false`: A reboot is not required + example: true + SystemRebootResponse: + type: string + example: No reboot is required, so it is not allowed. + description: System reboot response. + SystemStatusResponse: + type: array + items: + $ref: '#/components/schemas/StatusEntry' + description: System status response. + StatusEntry: + type: object + required: + - type + - text + - extra + properties: + type: + $ref: '#/components/schemas/StatusEntryType' + text: + type: string + example: This domain"s DNSSEC DS record is not set + extra: + type: array + items: + $ref: '#/components/schemas/StatusEntryExtra' + description: System status entry. + StatusEntryType: + type: string + enum: + - heading + - ok + - warning + - error + example: warning + description: System status entry type. + StatusEntryExtra: + type: object + required: + - monospace + - text + properties: + monospace: + type: boolean + example: false + text: + type: string + example: 'Digest Type: 2 / SHA-256' + description: System entry extra information. + SystemBackupConfigUpdateRequest: + type: object + required: + - target + - target_user + - target_pass + - min_age + properties: + target: + type: string + format: hostname + example: s3://s3.eu-central-1.amazonaws.com/box-example-com + target_user: + type: string + example: username + target_pass: + type: string + example: password + format: password + min_age: + type: integer + format: int32 + minimum: 1 + example: 3 + description: Backup config update request. + SystemBackupConfigUpdateResponse: + type: string + example: OK + description: Backup config update response. + SystemBackupConfigResponse: + type: object + required: + - enc_pw_file + - file_target_directory + - min_age_in_days + - ssh_pub_key + - target + properties: + enc_pw_file: + type: string + example: /home/user-data/backup/secret_key.txt + file_target_directory: + type: string + example: /home/user-data/backup/encrypted + min_age_in_days: + type: integer + format: int32 + minimum: 1 + example: 3 + ssh_pub_key: + type: string + example: ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDb root@box.example.com\n + target: + type: string + format: hostname + example: s3://s3.eu-central-1.amazonaws.com/box-example-com + target_user: + type: string + target_pass: + type: string + description: Backup config response. + SystemBackupStatusResponse: + type: object + required: + - unmatched_file_size + properties: + backups: + type: array + items: + $ref: '#/components/schemas/SystemBackupStatus' + unmatched_file_size: + type: integer + format: int32 + example: 0 + error: + type: string + example: Something is wrong with the backup + description: Backup status response. Lists the status for all backups. + SystemBackupStatus: + type: object + required: + - date + - date_delta + - date_str + - full + - size + - volumes + properties: + date: + type: string + format: date-time + example: 20200801T023706Z + date_delta: + type: string + example: 15 hours, 40 minutes + date_str: + type: string + example: 2020-08-01 03:37:06 BST + deleted_in: + type: string + example: approx. 6 days + full: + type: boolean + example: false + size: + type: integer + format: int32 + example: 125332 + volumes: + type: integer + format: int32 + example: 1 + description: Backup status details. + SSLStatusResponse: + type: object + required: + - can_provision + - status + properties: + can_provision: + type: array + items: + type: string + status: + type: array + items: + $ref: '#/components/schemas/SSLStatus' + description: SSL status response for all relevant domains. + SSLStatus: + type: object + required: + - domain + - status + - text + properties: + domain: + $ref: '#/components/schemas/Hostname' + status: + $ref: '#/components/schemas/SSLStatusType' + text: + type: string + example: Signed & valid. The certificate expires in 87 days on 10/28/20. + description: SSL status details for domain. + SSLStatusType: + type: string + enum: + - success + - danger + - not-applicable + example: success + description: SSL status type. + Email: + type: string + format: email + example: user@example.com + description: Email format. + Hostname: + type: string + format: hostname + example: example.com + description: Hostname format. + MeResponse: + type: object + required: + - status + properties: + api_key: + type: string + example: 12345abcde + email: + $ref: '#/components/schemas/Email' + privileges: + type: array + items: + $ref: '#/components/schemas/MailUserPrivilege' + reason: + type: string + example: Incorrect username or password + status: + $ref: '#/components/schemas/MeAuthStatus' + description: Me (user) response. + MeAuthStatus: + type: string + enum: + - ok + - invalid + example: invalid + description: Me (user) authentication result. + WebDomain: + type: object + required: + - custom_root + - domain + - root + - ssl_certificate + - static_enabled + properties: + custom_root: + type: string + example: /home/user-data/www/example.com + domain: + $ref: '#/components/schemas/Hostname' + root: + type: string + example: /home/user-data/www/default + ssl_certificate: + type: array + minItems: 2 + maxItems: 2 + uniqueItems: true + items: + oneOf: + - type: string + example: No certificate installed. + - type: string + enum: + - danger + - success + example: danger + static_enabled: + type: boolean + example: true + description: Web domain details. + WebUpdateResponse: + type: string + example: web updated + description: Web update response. diff --git a/conf/mta-sts.txt b/conf/mta-sts.txt new file mode 100644 index 00000000..376102bc --- /dev/null +++ b/conf/mta-sts.txt @@ -0,0 +1,4 @@ +version: STSv1 +mode: MODE +mx: PRIMARY_HOSTNAME +max_age: 86400 \ No newline at end of file diff --git a/conf/nginx-alldomains.conf b/conf/nginx-alldomains.conf index 1b3ad5a9..4c81e3f3 100644 --- a/conf/nginx-alldomains.conf +++ b/conf/nginx-alldomains.conf @@ -21,6 +21,9 @@ location = /mail/config-v1.1.xml { alias /var/lib/mailinabox/mozilla-autoconfig.xml; } + location = /.well-known/mta-sts.txt { + alias /var/lib/mailinabox/mta-sts.txt; + } # Roundcube Webmail configuration. rewrite ^/mail$ /mail/ redirect; diff --git a/conf/www_default.html b/conf/www_default.html index edefc428..68d0366b 100644 --- a/conf/www_default.html +++ b/conf/www_default.html @@ -1,6 +1,7 @@ this is a mail-in-a-box +

this is a mail-in-a-box

diff --git a/management/daemon.py b/management/daemon.py index 9d5f6a36..00f756e9 100755 --- a/management/daemon.py +++ b/management/daemon.py @@ -460,9 +460,8 @@ def system_status(): self.items[-1]["extra"].append({ "text": message, "monospace": monospace }) output = WebOutput() # Create a temporary pool of processes for the status checks - pool = multiprocessing.pool.Pool(processes=5) - run_checks(False, env, output, pool) - pool.terminate() + with multiprocessing.pool.Pool(processes=5) as pool: + run_checks(False, env, output, pool) return json_response(output.items) @app.route('/system/updates') diff --git a/management/daily_tasks.sh b/management/daily_tasks.sh index 2f723352..db496399 100755 --- a/management/daily_tasks.sh +++ b/management/daily_tasks.sh @@ -16,10 +16,10 @@ if [ `date "+%u"` -eq 1 ]; then fi # Take a backup. -management/backup.py | management/email_administrator.py "Backup Status" +management/backup.py 2>&1 | management/email_administrator.py "Backup Status" # Provision any new certificates for new domains or domains with expiring certificates. -management/ssl_certificates.py -q | management/email_administrator.py "TLS Certificate Provisioning Result" +management/ssl_certificates.py -q 2>&1 | management/email_administrator.py "TLS Certificate Provisioning Result" # Run status checks and email the administrator if anything changed. -management/status_checks.py --show-changes | management/email_administrator.py "Status Checks Change Notice" +management/status_checks.py --show-changes 2>&1 | management/email_administrator.py "Status Checks Change Notice" diff --git a/management/dns_update.py b/management/dns_update.py index 7d053d5e..748f87f1 100755 --- a/management/dns_update.py +++ b/management/dns_update.py @@ -9,8 +9,9 @@ import ipaddress import rtyaml import dns.resolver -from mailconfig import get_mail_domains +from mailconfig import get_mail_domains, get_mail_aliases from utils import shell, load_env_vars_from_file, safe_domain_name, sort_domains +from ssl_certificates import get_ssl_certificates, check_certificate # From https://stackoverflow.com/questions/3026957/how-to-validate-a-domain-name-using-regex-php/16491074#16491074 # This regular expression matches domain names according to RFCs, it also accepts fqdn with an leading dot, @@ -280,25 +281,81 @@ def build_zone(domain, all_domains, additional_records, www_redirect_domains, en if not has_rec(dmarc_qname, "TXT", prefix="v=DMARC1; "): records.append((dmarc_qname, "TXT", 'v=DMARC1; p=reject', "Recommended. Prevents use of this domain name for outbound mail by specifying that the SPF rule should be honoured for mail from @%s." % (qname + "." + domain))) - # Add CardDAV/CalDAV SRV records on the non-primary hostname that points to the primary hostname. + # Add CardDAV/CalDAV SRV records on the non-primary hostname that points to the primary hostname + # for autoconfiguration of mail clients (so only domains hosting user accounts need it). # The SRV record format is priority (0, whatever), weight (0, whatever), port, service provider hostname (w/ trailing dot). - if domain != env["PRIMARY_HOSTNAME"]: + if domain != env["PRIMARY_HOSTNAME"] and domain in get_mail_domains(env, users_only=True): for dav in ("card", "cal"): qname = "_" + dav + "davs._tcp" if not has_rec(qname, "SRV"): records.append((qname, "SRV", "0 0 443 " + env["PRIMARY_HOSTNAME"] + ".", "Recommended. Specifies the hostname of the server that handles CardDAV/CalDAV services for email addresses on this domain.")) - # Adds autoconfiguration A records for all domains. + # Adds autoconfiguration A records for all domains that there are user accounts at. # This allows the following clients to automatically configure email addresses in the respective applications. # autodiscover.* - Z-Push ActiveSync Autodiscover # autoconfig.* - Thunderbird Autoconfig - autodiscover_records = [ - ("autodiscover", "A", env["PUBLIC_IP"], "Provides email configuration autodiscovery support for Z-Push ActiveSync Autodiscover."), - ("autodiscover", "AAAA", env["PUBLIC_IPV6"], "Provides email configuration autodiscovery support for Z-Push ActiveSync Autodiscover."), - ("autoconfig", "A", env["PUBLIC_IP"], "Provides email configuration autodiscovery support for Thunderbird Autoconfig."), - ("autoconfig", "AAAA", env["PUBLIC_IPV6"], "Provides email configuration autodiscovery support for Thunderbird Autoconfig.") + if domain in get_mail_domains(env, users_only=True): + autodiscover_records = [ + ("autodiscover", "A", env["PUBLIC_IP"], "Provides email configuration autodiscovery support for Z-Push ActiveSync Autodiscover."), + ("autodiscover", "AAAA", env["PUBLIC_IPV6"], "Provides email configuration autodiscovery support for Z-Push ActiveSync Autodiscover."), + ("autoconfig", "A", env["PUBLIC_IP"], "Provides email configuration autodiscovery support for Thunderbird Autoconfig."), + ("autoconfig", "AAAA", env["PUBLIC_IPV6"], "Provides email configuration autodiscovery support for Thunderbird Autoconfig.") + ] + for qname, rtype, value, explanation in autodiscover_records: + if value is None or value.strip() == "": continue # skip IPV6 if not set + if not has_rec(qname, rtype): + records.append((qname, rtype, value, explanation)) + + # If this is a domain name that there are email addresses configured for, i.e. "something@" + # this domain name, then the domain name is a MTA-STS (https://tools.ietf.org/html/rfc8461) + # Policy Domain. + # + # A "_mta-sts" TXT record signals the presence of a MTA-STS policy. The id field helps clients + # cache the policy. It should be stable so we don't update DNS unnecessarily but change when + # the policy changes. It must be at most 32 letters and numbers, so we compute a hash of the + # policy file. + # + # The policy itself is served at the "mta-sts" (no underscore) subdomain over HTTPS. Therefore + # the TLS certificate used by Postfix for STARTTLS must be a valid certificate for the MX + # domain name (PRIMARY_HOSTNAME) *and* the TLS certificate used by nginx for HTTPS on the mta-sts + # subdomain must be valid certificate for that domain. Do not set an MTA-STS policy if either + # certificate in use is not valid (e.g. because it is self-signed and a valid certificate has not + # yet been provisioned). Since we cannot provision a certificate without A/AAAA records, we + # always set them --- only the TXT records depend on there being valid certificates. + mta_sts_enabled = False + mta_sts_records = [ + ("mta-sts", "A", env["PUBLIC_IP"], "Optional. MTA-STS Policy Host serving /.well-known/mta-sts.txt."), + ("mta-sts", "AAAA", env.get('PUBLIC_IPV6'), "Optional. MTA-STS Policy Host serving /.well-known/mta-sts.txt."), ] - for qname, rtype, value, explanation in autodiscover_records: + if domain in get_mail_domains(env): + # Check that PRIMARY_HOSTNAME and the mta_sts domain both have valid certificates. + for d in (env['PRIMARY_HOSTNAME'], "mta-sts." + domain): + cert = get_ssl_certificates(env).get(d) + if not cert: + break # no certificate provisioned for this domain + cert_status = check_certificate(d, cert['certificate'], cert['private-key']) + if cert_status[0] != 'OK': + break # certificate is not valid + else: + # 'break' was not encountered above, so both domains are good + mta_sts_enabled = True + if mta_sts_enabled: + # Compute an up-to-32-character hash of the policy file. We'll take a SHA-1 hash of the policy + # file (20 bytes) and encode it as base-64 (28 bytes, using alphanumeric alternate characters + # instead of '+' and '/' which are not allowed in an MTA-STS policy id) but then just take its + # first 20 characters, which is more than sufficient to change whenever the policy file changes + # (and ensures any '=' padding at the end of the base64 encoding is dropped). + with open("/var/lib/mailinabox/mta-sts.txt", "rb") as f: + mta_sts_policy_id = base64.b64encode(hashlib.sha1(f.read()).digest(), altchars=b"AA").decode("ascii")[0:20] + mta_sts_records.extend([ + ("_mta-sts", "TXT", "v=STSv1; id=" + mta_sts_policy_id, "Optional. Part of the MTA-STS policy for incoming mail. If set, a MTA-STS policy must also be published.") + ]) + + # Enable SMTP TLS reporting (https://tools.ietf.org/html/rfc8460) if the user has set a config option. + # Skip if the rules below if the user has set a custom _smtp._tls record. + if env.get("MTA_STS_TLSRPT_RUA") and not has_rec("_smtp._tls", "TXT", prefix="v=TLSRPTv1;"): + mta_sts_records.append(("_smtp._tls", "TXT", "v=TLSRPTv1; rua=" + env["MTA_STS_TLSRPT_RUA"], "Optional. Enables MTA-STS reporting.")) + for qname, rtype, value, explanation in mta_sts_records: if value is None or value.strip() == "": continue # skip IPV6 if not set if not has_rec(qname, rtype): records.append((qname, rtype, value, explanation)) @@ -906,18 +963,19 @@ def set_secondary_dns(hostnames, env): try: response = resolver.query(item, "A") except (dns.resolver.NoNameservers, dns.resolver.NXDOMAIN, dns.resolver.NoAnswer): - raise ValueError("Could not resolve the IP address of %s." % item) + try: + response = resolver.query(item, "AAAA") + except (dns.resolver.NoNameservers, dns.resolver.NXDOMAIN, dns.resolver.NoAnswer): + raise ValueError("Could not resolve the IP address of %s." % item) else: # Validate IP address. try: if "/" in item[4:]: v = ipaddress.ip_network(item[4:]) # raises a ValueError if there's a problem - if not isinstance(v, ipaddress.IPv4Network): raise ValueError("That's an IPv6 subnet.") else: v = ipaddress.ip_address(item[4:]) # raises a ValueError if there's a problem - if not isinstance(v, ipaddress.IPv4Address): raise ValueError("That's an IPv6 address.") except ValueError: - raise ValueError("'%s' is not an IPv4 address or subnet." % item[4:]) + raise ValueError("'%s' is not an IPv4 or IPv6 address or subnet." % item[4:]) # Set. set_custom_dns_record("_secondary_nameserver", "A", " ".join(hostnames), "set", env) diff --git a/management/mailconfig.py b/management/mailconfig.py index 383b4cbc..43c4777e 100755 --- a/management/mailconfig.py +++ b/management/mailconfig.py @@ -307,13 +307,15 @@ def get_domain(emailaddr, as_unicode=True): pass return ret -def get_mail_domains(env, filter_aliases=lambda alias : True): +def get_mail_domains(env, filter_aliases=lambda alias : True, users_only=False): # Returns the domain names (IDNA-encoded) of all of the email addresses - # configured on the system. - return set( - [get_domain(login, as_unicode=False) for login in get_mail_users(env)] - + [get_domain(address, as_unicode=False) for address, *_ in get_mail_aliases(env) if filter_aliases(address) ] - ) + # configured on the system. If users_only is True, only return domains + # with email addresses that correspond to user accounts. + domains = [] + domains.extend([get_domain(login, as_unicode=False) for login in get_mail_users(env)]) + if not users_only: + domains.extend([get_domain(address, as_unicode=False) for address, *_ in get_mail_aliases(env) if filter_aliases(address) ]) + return set(domains) def add_mail_user(email, pw, privs, quota, env): # validate email @@ -715,8 +717,6 @@ def validate_password(pw): # validate password if pw.strip() == "": raise ValueError("No password provided.") - if re.search(r"[\s]", pw): - raise ValueError("Passwords cannot contain spaces.") if len(pw) < 8: raise ValueError("Passwords must be at least eight characters.") diff --git a/management/ssl_certificates.py b/management/ssl_certificates.py index 76b0f8fa..1b1e9f83 100755 --- a/management/ssl_certificates.py +++ b/management/ssl_certificates.py @@ -180,7 +180,7 @@ def get_certificates_to_provision(env, limit_domains=None, show_valid_certs=True # for and subtract: # * domains not in limit_domains if limit_domains is not empty # * domains with custom "A" records, i.e. they are hosted elsewhere - # * domains with actual "A" records that point elsewhere + # * domains with actual "A" records that point elsewhere (misconfiguration) # * domains that already have certificates that will be valid for a while from web_update import get_web_domains @@ -256,15 +256,41 @@ def provision_certificates(env, limit_domains): "result": "skipped", }) + # Break into groups by DNS zone: Group every domain with its parent domain, if + # its parent domain is in the list of domains to request a certificate for. + # Start with the zones so that if the zone doesn't need a certificate itself, + # its children will still be grouped together. Sort the provision domains to + # put parents ahead of children. + # Since Let's Encrypt requests are limited to 100 domains at a time, + # we'll create a list of lists of domains where the inner lists have + # at most 100 items. By sorting we also get the DNS zone domain as the first + # entry in each list (unless we overflow beyond 100) which ends up as the + # primary domain listed in each certificate. + from dns_update import get_dns_zones + certs = { } + for zone, zonefile in get_dns_zones(env): + certs[zone] = [[]] + for domain in sort_domains(domains, env): + # Does the domain end with any domain we've seen so far. + for parent in certs.keys(): + if domain.endswith("." + parent): + # Add this to the parent's list of domains. + # Start a new group if the list already has + # 100 items. + if len(certs[parent][-1]) == 100: + certs[parent].append([]) + certs[parent][-1].append(domain) + break + else: + # This domain is not a child of any domain we've seen yet, so + # start a new group. This shouldn't happen since every zone + # was already added. + certs[domain] = [[domain]] - # Break into groups of up to 100 certificates at a time, which is Let's Encrypt's - # limit for a single certificate. We'll sort to put related domains together. - max_domains_per_group = 100 - domains = sort_domains(domains, env) - certs = [] - while len(domains) > 0: - certs.append( domains[:max_domains_per_group] ) - domains = domains[max_domains_per_group:] + # Flatten to a list of lists of domains (from a mapping). Remove empty + # lists (zones with no domains that need certs). + certs = sum(certs.values(), []) + certs = [_ for _ in certs if len(_) > 0] # Prepare to provision. diff --git a/management/status_checks.py b/management/status_checks.py index 0accebb5..27317fed 100755 --- a/management/status_checks.py +++ b/management/status_checks.py @@ -5,11 +5,13 @@ # what to do next. import sys, os, os.path, re, subprocess, datetime, multiprocessing.pool +import asyncio import dns.reversename, dns.resolver import dateutil.parser, dateutil.tz import idna import psutil +import postfix_mta_sts_resolver.resolver from dns_update import get_dns_zones, build_tlsa_record, get_custom_dns_config, get_secondary_dns, get_custom_dns_records from web_update import get_web_domains, get_domains_with_a_records @@ -309,6 +311,17 @@ def run_domain_checks(rounded_time, env, output, pool): domains_to_check = mail_domains | dns_domains | web_domains + # Remove "www", "autoconfig", "autodiscover", and "mta-sts" subdomains, which we group with their parent, + # if their parent is in the domains to check list. + domains_to_check = [ + d for d in domains_to_check + if not ( + d.split(".", 1)[0] in ("www", "autoconfig", "autodiscover", "mta-sts") + and len(d.split(".", 1)) == 2 + and d.split(".", 1)[1] in domains_to_check + ) + ] + # Get the list of domains that we don't serve web for because of a custom CNAME/A record. domains_with_a_records = get_domains_with_a_records(env) @@ -327,6 +340,11 @@ def run_domain_checks(rounded_time, env, output, pool): def run_domain_checks_on_domain(domain, rounded_time, env, dns_domains, dns_zonefiles, mail_domains, web_domains, domains_with_a_records): output = BufferedOutput() + # When running inside Flask, the worker threads don't get a thread pool automatically. + # Also this method is called in a forked worker pool, so creating a new loop is probably + # a good idea. + asyncio.set_event_loop(asyncio.new_event_loop()) + # we'd move this up, but this returns non-pickleable values ssl_certificates = get_ssl_certificates(env) @@ -354,6 +372,26 @@ def run_domain_checks_on_domain(domain, rounded_time, env, dns_domains, dns_zone if domain in dns_domains: check_dns_zone_suggestions(domain, env, output, dns_zonefiles, domains_with_a_records) + # Check auto-configured subdomains. See run_domain_checks. + # Skip mta-sts because we check the policy directly. + for label in ("www", "autoconfig", "autodiscover"): + subdomain = label + "." + domain + if subdomain in web_domains or subdomain in mail_domains: + # Run checks. + subdomain_output = run_domain_checks_on_domain(subdomain, rounded_time, env, dns_domains, dns_zonefiles, mail_domains, web_domains, domains_with_a_records) + + # Prepend the domain name to the start of each check line, and then add to the + # checks for this domain. + for attr, args, kwargs in subdomain_output[1].buf: + if attr == "add_heading": + # Drop the heading, but use its text as the subdomain name in + # each line since it is in Unicode form. + subdomain = args[0] + continue + if len(args) == 1 and isinstance(args[0], str): + args = [ subdomain + ": " + args[0] ] + getattr(output, attr)(*args, **kwargs) + return (domain, output) def check_primary_hostname_dns(domain, env, output, dns_domains, dns_zonefiles): @@ -611,6 +649,19 @@ def check_mail_domain(domain, env, output): if mx != recommended_mx: good_news += " This configuration is non-standard. The recommended configuration is '%s'." % (recommended_mx,) output.print_ok(good_news) + + # Check MTA-STS policy. + loop = asyncio.get_event_loop() + sts_resolver = postfix_mta_sts_resolver.resolver.STSResolver(loop=loop) + valid, policy = loop.run_until_complete(sts_resolver.resolve(domain)) + if valid == postfix_mta_sts_resolver.resolver.STSFetchResult.VALID: + if policy[1].get("mx") == [env['PRIMARY_HOSTNAME']] and policy[1].get("mode") == "enforce": # policy[0] is the policyid + output.print_ok("MTA-STS policy is present.") + else: + output.print_error("MTA-STS policy is present but has unexpected settings. [{}]".format(policy[1])) + else: + output.print_error("MTA-STS policy is missing: {}".format(valid)) + else: output.print_error("""This domain's DNS MX record is incorrect. It is currently set to '%s' but should be '%s'. Mail will not be delivered to this box. It may take several hours for public DNS to update after a change. This problem may result from @@ -970,13 +1021,14 @@ if __name__ == "__main__": from utils import load_environment env = load_environment() - pool = multiprocessing.pool.Pool(processes=10) if len(sys.argv) == 1: - run_checks(False, env, ConsoleOutput(), pool) + with multiprocessing.pool.Pool(processes=10) as pool: + run_checks(False, env, ConsoleOutput(), pool) elif sys.argv[1] == "--show-changes": - run_and_output_changes(env, pool) + with multiprocessing.pool.Pool(processes=10) as pool: + run_and_output_changes(env, pool) elif sys.argv[1] == "--check-primary-hostname": # See if the primary hostname appears resolvable and has a signed certificate. diff --git a/management/templates/aliases.html b/management/templates/aliases.html index e8d0cb1c..848fcf49 100644 --- a/management/templates/aliases.html +++ b/management/templates/aliases.html @@ -288,7 +288,7 @@ function aliases_remove(elem) { }, function(r) { // Responses are multiple lines of pre-formatted text. - show_modal_error("Remove User", $("
").text(r));
+          show_modal_error("Remove Alias", $("
").text(r));
           show_aliases();
         });
     });
diff --git a/management/templates/custom-dns.html b/management/templates/custom-dns.html
index a2d5042d..6984b081 100644
--- a/management/templates/custom-dns.html
+++ b/management/templates/custom-dns.html
@@ -90,7 +90,7 @@
     

Multiple secondary servers can be separated with commas or spaces (i.e., ns2.hostingcompany.com ns3.hostingcompany.com). - To enable zone transfers to additional servers without listing them as secondary nameservers, add an IP address or subnet using xfr:10.20.30.40 or xfr:10.20.30.40/24. + To enable zone transfers to additional servers without listing them as secondary nameservers, add an IP address or subnet using xfr:10.20.30.40 or xfr:10.0.0.0/8.