mirror of
https://github.com/mail-in-a-box/mailinabox.git
synced 2026-03-12 17:07:23 +01:00
Compare commits
95 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
34b7638342 | ||
|
|
acd91665b5 | ||
|
|
b503ea1cf7 | ||
|
|
091c2e45bf | ||
|
|
0a78d1d2fa | ||
|
|
ff4780d5fb | ||
|
|
0924f8ca7a | ||
|
|
6302aa6c12 | ||
|
|
da4d9ff607 | ||
|
|
e57e08088a | ||
|
|
5e43c394d5 | ||
|
|
b2eaaeca4b | ||
|
|
e2d9a523c3 | ||
|
|
11df1e4680 | ||
|
|
53d5542402 | ||
|
|
bfda3f40b9 | ||
|
|
c0ddceb2bd | ||
|
|
42a506231b | ||
|
|
e3252f53da | ||
|
|
aa33428311 | ||
|
|
ca5d228be6 | ||
|
|
f89a98c78a | ||
|
|
a3087d8815 | ||
|
|
23d2df7a93 | ||
|
|
1cd97d46a2 | ||
|
|
53f84a8092 | ||
|
|
6441de63ba | ||
|
|
b2553aea33 | ||
|
|
5ef1cfbdc7 | ||
|
|
7527b4dc27 | ||
|
|
1367816b04 | ||
|
|
299a2315c1 | ||
|
|
9a6aea6940 | ||
|
|
98cd04cccf | ||
|
|
0cc20cbb97 | ||
|
|
ef6a17d4a6 | ||
|
|
17a149947a | ||
|
|
a2c50ae967 | ||
|
|
13958ba4df | ||
|
|
8eb71483f3 | ||
|
|
d8e30883fa | ||
|
|
47acbbf332 | ||
|
|
dece359c90 | ||
|
|
6a9eb4e367 | ||
|
|
fc03ce9b2f | ||
|
|
ce17c12ca2 | ||
|
|
5edaeb8c7b | ||
|
|
3a28d1b073 | ||
|
|
6f2226bfcd | ||
|
|
97cd4c64ad | ||
|
|
43d50d0667 | ||
|
|
6258a7f311 | ||
|
|
ab36cc8968 | ||
|
|
33b71c6b3c | ||
|
|
34e821c102 | ||
|
|
2af557139d | ||
|
|
9e0dcd8718 | ||
|
|
be2b5a62de | ||
|
|
0cbba71c72 | ||
|
|
d28563fb45 | ||
|
|
38632f0f90 | ||
|
|
0754ce01b1 | ||
|
|
1ef455d37d | ||
|
|
d152603abd | ||
|
|
9e125aec00 | ||
|
|
2c90c267bd | ||
|
|
47de93961e | ||
|
|
1990f32ca4 | ||
|
|
807939c0e4 | ||
|
|
a1c7bf0883 | ||
|
|
5008cc603e | ||
|
|
9857db96cd | ||
|
|
e9e6d94e3b | ||
|
|
462a79cf47 | ||
|
|
f792deeebd | ||
|
|
95173bb327 | ||
|
|
1d09e2406b | ||
|
|
c9add7a8bf | ||
|
|
e4caed9277 | ||
|
|
1760eaa601 | ||
|
|
b25ce67fe1 | ||
|
|
b23ba6f75e | ||
|
|
cf904a05cc | ||
|
|
cac6a251cc | ||
|
|
31d26a7bad | ||
|
|
a9ed9ae936 | ||
|
|
a9892efe38 | ||
|
|
f02e0a3ccb | ||
|
|
32f5632620 | ||
|
|
005cc08b40 | ||
|
|
6a659fe10d | ||
|
|
6941ca2f63 | ||
|
|
e4eba49c1b | ||
|
|
f289439d1d | ||
|
|
f78cff225b |
54
CHANGELOG.md
54
CHANGELOG.md
@@ -1,6 +1,60 @@
|
|||||||
CHANGELOG
|
CHANGELOG
|
||||||
=========
|
=========
|
||||||
|
|
||||||
|
v0.12 (July 3, 2015)
|
||||||
|
--------------------
|
||||||
|
|
||||||
|
This is a minor update to v0.11, which was a major update. Please read v0.11's advisories.
|
||||||
|
|
||||||
|
* v0.12b was posted shortly after the initial posting of v0.12 correcting a minor regression in v0.12 related to creating aliases targetting multiple addresses.
|
||||||
|
|
||||||
|
* The administrator@ alias was incorrectly created starting with v0.11. If your first install was v0.11, check that the administrator@ alias forwards mail to you.
|
||||||
|
* Intrusion detection rules (fail2ban) are relaxed (i.e. less is blocked).
|
||||||
|
* SSL certificates could not be installed for the new automatic 'www.' redirect domains.
|
||||||
|
* PHP's default character encoding is changed from no default to UTF8. The effect of this change is unclear but should prevent possible future text conversion issues.
|
||||||
|
* User-installed SSL private keys in the BEGIN PRIVATE KEY format were not accepted.
|
||||||
|
* SSL certificates with SAN domains with IDNA encoding were broken in v0.11.
|
||||||
|
* Some IDNA functionality was using IDNA 2003 rather than IDNA 2008.
|
||||||
|
|
||||||
|
|
||||||
|
v0.11 (June 29, 2015)
|
||||||
|
---------------------
|
||||||
|
|
||||||
|
Advisories:
|
||||||
|
* Users can no longer spoof arbitrary email addresses in outbound mail. When sending mail, the email address configured in your mail client must match the SMTP login username being used, or the email address must be an alias with the SMTP login username listed as one of the alias's targets.
|
||||||
|
* This update replaces your DKIM signing key with a stronger key. Because of DNS caching/propagation, mail sent within a few hours after this update could be marked as spam by recipients. If you use External DNS, you will need to update your DNS records.
|
||||||
|
* The box will now install software from a new Mail-in-a-Box PPA on Launchpad.net, where we are distributing two of our own packages: a patched postgrey and dovecot-lucene.
|
||||||
|
* v0.11b was posted shortly after the initial posting of v0.11 correcting a missing dependency for the new PPA.
|
||||||
|
|
||||||
|
Mail:
|
||||||
|
* Greylisting will now let some reputable senders pass through immediately.
|
||||||
|
* Searching mail (via IMAP) will now be much faster using the dovecot lucene full text search plugin.
|
||||||
|
* Users can no longer spoof arbitrary email addresses in outbound mail (see above).
|
||||||
|
* Fix for deleting admin@ and postmaster@ addresses.
|
||||||
|
* Roundcube is updated to version 1.1.2, plugins updated.
|
||||||
|
* Exchange/ActiveSync autoconfiguration was not working on all devices (e.g. iPhone) because of a case-sensitive URL.
|
||||||
|
* The DKIM signing key has been increased to 2048 bits, from 1024, replacing the existing key.
|
||||||
|
|
||||||
|
Web:
|
||||||
|
* 'www' subdomains now automatically redirect to their parent domain (but you'll need to install an SSL certificate).
|
||||||
|
* OCSP no longer uses Google Public DNS.
|
||||||
|
* The installed PHP version is no longer exposed through HTTP response headers, for better security.
|
||||||
|
|
||||||
|
DNS:
|
||||||
|
* Default IPv6 AAAA records were missing since version 0.09.
|
||||||
|
|
||||||
|
Control panel:
|
||||||
|
* Resetting a user's password now forces them to log in again everywhere.
|
||||||
|
* Status checks were not working if an ssh server was not installed.
|
||||||
|
* SSL certificate validation now uses the Python cryptography module in some places where openssl was used.
|
||||||
|
* There is a new tab to show the installed version of Mail-in-a-Box and to fetch the latest released version.
|
||||||
|
|
||||||
|
System:
|
||||||
|
* The munin system monitoring tool is now installed and accessible at /admin/munin.
|
||||||
|
* ownCloud updated to version 8.0.4. The ownCloud installation step now is reslient to download problems. The ownCloud configuration file is now stored in STORAGE_ROOT to fix loss of data when moving STORAGE_ROOT to a new machine.
|
||||||
|
* The setup scripts now run `apt-get update` prior to installing anything to ensure the apt database is in sync with the packages actually available.
|
||||||
|
|
||||||
|
|
||||||
v0.10 (June 1, 2015)
|
v0.10 (June 1, 2015)
|
||||||
--------------------
|
--------------------
|
||||||
|
|
||||||
|
|||||||
14
README.md
14
README.md
@@ -32,8 +32,12 @@ The components installed are:
|
|||||||
* Webmail ([Roundcube](http://roundcube.net/)), static website hosting ([nginx](http://nginx.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/))
|
* Spam filtering ([spamassassin](https://spamassassin.apache.org/)), greylisting ([postgrey](http://postgrey.schweikert.ch/))
|
||||||
* DNS ([nsd4](http://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
|
* DNS ([nsd4](http://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
|
||||||
* Firewall ([ufw](https://launchpad.net/ufw)), intrusion protection ([fail2ban](http://www.fail2ban.org/wiki/index.php/Main_Page))
|
* 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/))
|
||||||
* A control panel and API for adding/removing mail users, aliases, custom DNS records, etc. and system monitoring.
|
|
||||||
|
It also includes:
|
||||||
|
|
||||||
|
* A control panel and API for adding/removing mail users, aliases, custom DNS records, etc. and detailed system monitoring.
|
||||||
|
* Our own builds of postgrey and dovecot-lucene distributed via the [Mail-in-a-Box PPA](https://launchpad.net/~mail-in-a-box/+archive/ubuntu/ppa) on Launchpad.
|
||||||
|
|
||||||
For more information on how Mail-in-a-Box handles your privacy, see the [security details page](security.md).
|
For more information on how Mail-in-a-Box handles your privacy, see the [security details page](security.md).
|
||||||
|
|
||||||
@@ -53,13 +57,16 @@ I sign the release tags on git. To verify that a tag is signed by me, you can pe
|
|||||||
$ cd mailinabox
|
$ cd mailinabox
|
||||||
|
|
||||||
# Verify the tag.
|
# Verify the tag.
|
||||||
$ git verify-tag v0.10
|
$ git verify-tag v0.12b
|
||||||
gpg: Signature made ..... using RSA key ID C10BDD81
|
gpg: Signature made ..... using RSA key ID C10BDD81
|
||||||
gpg: Good signature from "Joshua Tauberer <jt@occams.info>"
|
gpg: Good signature from "Joshua Tauberer <jt@occams.info>"
|
||||||
gpg: WARNING: This key is not certified with a trusted signature!
|
gpg: WARNING: This key is not certified with a trusted signature!
|
||||||
gpg: There is no indication that the signature belongs to the owner.
|
gpg: There is no indication that the signature belongs to the owner.
|
||||||
Primary key fingerprint: 5F4C 0E73 13CC D744 693B 2AEA B920 41F4 C10B DD81
|
Primary key fingerprint: 5F4C 0E73 13CC D744 693B 2AEA B920 41F4 C10B DD81
|
||||||
|
|
||||||
|
# Check out the tag.
|
||||||
|
$ git checkout v0.12b
|
||||||
|
|
||||||
The key ID and fingerprint above should match my [Keybase.io key](https://keybase.io/joshdata) and the fingerprint I publish on [my homepage](https://razor.occams.info/).
|
The key ID and fingerprint above should match my [Keybase.io key](https://keybase.io/joshdata) and the fingerprint I publish on [my homepage](https://razor.occams.info/).
|
||||||
|
|
||||||
The Acknowledgements
|
The Acknowledgements
|
||||||
@@ -76,3 +83,4 @@ The History
|
|||||||
* In August 2013 I began Mail-in-a-Box by combining my own mail server configuration with the setup in ["NSA-proof your email in 2 hours"](http://sealedabstract.com/code/nsa-proof-your-e-mail-in-2-hours/) and making the setup steps reproducible with bash scripts.
|
* In August 2013 I began Mail-in-a-Box by combining my own mail server configuration with the setup in ["NSA-proof your email in 2 hours"](http://sealedabstract.com/code/nsa-proof-your-e-mail-in-2-hours/) and making the setup steps reproducible with bash scripts.
|
||||||
* Mail-in-a-Box was a semifinalist in the 2014 [Knight News Challenge](https://www.newschallenge.org/challenge/2014/submissions/mail-in-a-box), but it was not selected as a winner.
|
* Mail-in-a-Box was a semifinalist in the 2014 [Knight News Challenge](https://www.newschallenge.org/challenge/2014/submissions/mail-in-a-box), but it was not selected as a winner.
|
||||||
* Mail-in-a-Box hit the front page of Hacker News in [April](https://news.ycombinator.com/item?id=7634514) 2014, [September](https://news.ycombinator.com/item?id=8276171) 2014, and [May](https://news.ycombinator.com/item?id=9624267) 2015.
|
* Mail-in-a-Box hit the front page of Hacker News in [April](https://news.ycombinator.com/item?id=7634514) 2014, [September](https://news.ycombinator.com/item?id=8276171) 2014, and [May](https://news.ycombinator.com/item?id=9624267) 2015.
|
||||||
|
* FastCompany mentioned Mail-in-a-Box a [roundup of privacy projects](http://www.fastcompany.com/3047645/your-own-private-cloud) on June 26, 2015.
|
||||||
|
|||||||
@@ -1,34 +1,19 @@
|
|||||||
# Fail2Ban configuration file.
|
# Fail2Ban configuration file for Mail-in-a-Box
|
||||||
# For Mail-in-a-Box
|
|
||||||
[DEFAULT]
|
|
||||||
|
|
||||||
# bantime in seconds
|
[DEFAULT]
|
||||||
bantime = 60
|
|
||||||
|
|
||||||
# This should ban dumb brute-force attacks, not oblivious users.
|
# This should ban dumb brute-force attacks, not oblivious users.
|
||||||
findtime = 30
|
findtime = 30
|
||||||
maxretry = 20
|
maxretry = 20
|
||||||
|
|
||||||
#
|
|
||||||
# JAILS
|
# JAILS
|
||||||
#
|
|
||||||
|
|
||||||
[ssh]
|
|
||||||
|
|
||||||
enabled = true
|
|
||||||
logpath = /var/log/auth.log
|
|
||||||
maxretry = 20
|
|
||||||
|
|
||||||
[ssh-ddos]
|
[ssh-ddos]
|
||||||
|
|
||||||
enabled = true
|
enabled = true
|
||||||
maxretry = 20
|
|
||||||
|
|
||||||
[sasl]
|
[sasl]
|
||||||
|
|
||||||
enabled = true
|
enabled = true
|
||||||
|
|
||||||
[dovecot]
|
[dovecot]
|
||||||
|
|
||||||
enabled = true
|
enabled = true
|
||||||
filter = dovecotimap
|
filter = dovecotimap
|
||||||
|
|||||||
79
conf/nginx-alldomains.conf
Normal file
79
conf/nginx-alldomains.conf
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
# Expose this directory as static files.
|
||||||
|
root $ROOT;
|
||||||
|
index index.html index.htm;
|
||||||
|
|
||||||
|
location = /robots.txt {
|
||||||
|
log_not_found off;
|
||||||
|
access_log off;
|
||||||
|
}
|
||||||
|
|
||||||
|
location = /favicon.ico {
|
||||||
|
log_not_found off;
|
||||||
|
access_log off;
|
||||||
|
}
|
||||||
|
|
||||||
|
location = /mailinabox.mobileconfig {
|
||||||
|
alias /var/lib/mailinabox/mobileconfig.xml;
|
||||||
|
}
|
||||||
|
location = /.well-known/autoconfig/mail/config-v1.1.xml {
|
||||||
|
alias /var/lib/mailinabox/mozilla-autoconfig.xml;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Roundcube Webmail configuration.
|
||||||
|
rewrite ^/mail$ /mail/ redirect;
|
||||||
|
rewrite ^/mail/$ /mail/index.php;
|
||||||
|
location /mail/ {
|
||||||
|
index index.php;
|
||||||
|
alias /usr/local/lib/roundcubemail/;
|
||||||
|
}
|
||||||
|
location ~ /mail/config/.* {
|
||||||
|
# A ~-style location is needed to give this precedence over the next block.
|
||||||
|
return 403;
|
||||||
|
}
|
||||||
|
location ~ /mail/.*\.php {
|
||||||
|
# note: ~ has precendence over a regular location block
|
||||||
|
include fastcgi_params;
|
||||||
|
fastcgi_split_path_info ^/mail(/.*)()$;
|
||||||
|
fastcgi_index index.php;
|
||||||
|
fastcgi_param SCRIPT_FILENAME /usr/local/lib/roundcubemail/$fastcgi_script_name;
|
||||||
|
fastcgi_pass php-fpm;
|
||||||
|
|
||||||
|
# Outgoing mail also goes through this endpoint, so increase the maximum
|
||||||
|
# file upload limit to match the corresponding Postfix limit.
|
||||||
|
client_max_body_size 128M;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Z-Push (Microsoft Exchange ActiveSync)
|
||||||
|
location /Microsoft-Server-ActiveSync {
|
||||||
|
include /etc/nginx/fastcgi_params;
|
||||||
|
fastcgi_param SCRIPT_FILENAME /usr/local/lib/z-push/index.php;
|
||||||
|
fastcgi_param PHP_VALUE "include_path=.:/usr/share/php:/usr/share/pear:/usr/share/awl/inc";
|
||||||
|
fastcgi_read_timeout 630;
|
||||||
|
fastcgi_pass php-fpm;
|
||||||
|
|
||||||
|
# Outgoing mail also goes through this endpoint, so increase the maximum
|
||||||
|
# file upload limit to match the corresponding Postfix limit.
|
||||||
|
client_max_body_size 128M;
|
||||||
|
}
|
||||||
|
location ~* ^/autodiscover/autodiscover.xml$ {
|
||||||
|
include fastcgi_params;
|
||||||
|
fastcgi_param SCRIPT_FILENAME /usr/local/lib/z-push/autodiscover/autodiscover.php;
|
||||||
|
fastcgi_param PHP_VALUE "include_path=.:/usr/share/php:/usr/share/pear:/usr/share/awl/inc";
|
||||||
|
fastcgi_pass php-fpm;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ADDITIONAL DIRECTIVES HERE
|
||||||
|
|
||||||
|
# Disable viewing dotfiles (.htaccess, .svn, .git, etc.)
|
||||||
|
# This block is placed at the end. Nginx's precedence rules means this block
|
||||||
|
# takes precedence over all non-regex matches and only regex matches that
|
||||||
|
# come after it (i.e. none of those, since this is the last one.) That means
|
||||||
|
# we're blocking dotfiles in the static hosted sites but not the FastCGI-
|
||||||
|
# handled locations for ownCloud (which serves user-uploaded files that might
|
||||||
|
# have this pattern, see #414) or some of the other services.
|
||||||
|
location ~ /\.(ht|svn|git|hg|bzr) {
|
||||||
|
log_not_found off;
|
||||||
|
access_log off;
|
||||||
|
deny all;
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
# Proxy /admin to our Python based control panel daemon. It is
|
# Proxy /admin to our Python based control panel daemon. It is
|
||||||
# listening on IPv4 only so use an IP address and not 'localhost'.
|
# listening on IPv4 only so use an IP address and not 'localhost'.
|
||||||
rewrite ^/admin$ /admin/;
|
rewrite ^/admin$ /admin/;
|
||||||
|
rewrite ^/admin/munin$ /admin/munin/ redirect;
|
||||||
location /admin/ {
|
location /admin/ {
|
||||||
proxy_pass http://127.0.0.1:10222/;
|
proxy_pass http://127.0.0.1:10222/;
|
||||||
proxy_set_header X-Forwarded-For $remote_addr;
|
proxy_set_header X-Forwarded-For $remote_addr;
|
||||||
@@ -29,8 +30,8 @@
|
|||||||
fastcgi_param SCRIPT_FILENAME /usr/local/lib/owncloud/$2;
|
fastcgi_param SCRIPT_FILENAME /usr/local/lib/owncloud/$2;
|
||||||
fastcgi_param SCRIPT_NAME $1$2;
|
fastcgi_param SCRIPT_NAME $1$2;
|
||||||
fastcgi_param PATH_INFO $3;
|
fastcgi_param PATH_INFO $3;
|
||||||
# TODO: see the dispreferred "method 2" for xaccel at https://doc.owncloud.org/server/8.1/admin_manual/configuration_files/serving_static_files_configuration.html
|
|
||||||
fastcgi_param MOD_X_ACCEL_REDIRECT_ENABLED on;
|
fastcgi_param MOD_X_ACCEL_REDIRECT_ENABLED on;
|
||||||
|
fastcgi_param MOD_X_ACCEL_REDIRECT_PREFIX /owncloud-xaccel;
|
||||||
fastcgi_read_timeout 630;
|
fastcgi_read_timeout 630;
|
||||||
fastcgi_pass php-fpm;
|
fastcgi_pass php-fpm;
|
||||||
error_page 403 /cloud/core/templates/403.php;
|
error_page 403 /cloud/core/templates/403.php;
|
||||||
@@ -38,15 +39,13 @@
|
|||||||
client_max_body_size 1G;
|
client_max_body_size 1G;
|
||||||
fastcgi_buffers 64 4K;
|
fastcgi_buffers 64 4K;
|
||||||
}
|
}
|
||||||
location ^~ $STORAGE_ROOT/owncloud {
|
location ^~ /owncloud-xaccel/ {
|
||||||
# This directory is for MOD_X_ACCEL_REDIRECT_ENABLED. It works a little weird.
|
# This directory is for MOD_X_ACCEL_REDIRECT_ENABLED. ownCloud sends the full file
|
||||||
# The full path on disk of the file is passed as the URL path. ownCloud 8 totally
|
# path on disk as a subdirectory under this virtual path.
|
||||||
# busted the sane way this worked in ownCloud 7. There's a pending change using
|
# We must only allow 'internal' redirects within nginx so that the filesystem
|
||||||
# a new parameter to make this make more sense.
|
|
||||||
# We need to only allow 'internal' redirects within nginx so that the filesystem
|
|
||||||
# is not exposed to the world.
|
# is not exposed to the world.
|
||||||
internal;
|
internal;
|
||||||
alias $STORAGE_ROOT/owncloud;
|
alias /;
|
||||||
}
|
}
|
||||||
location ~ ^/((caldav|carddav|webdav).*)$ {
|
location ~ ^/((caldav|carddav|webdav).*)$ {
|
||||||
# Z-Push doesn't like getting a redirect, and a plain rewrite didn't work either.
|
# Z-Push doesn't like getting a redirect, and a plain rewrite didn't work either.
|
||||||
@@ -58,3 +57,4 @@
|
|||||||
rewrite ^/.well-known/carddav /cloud/remote.php/carddav/ redirect;
|
rewrite ^/.well-known/carddav /cloud/remote.php/carddav/ redirect;
|
||||||
rewrite ^/.well-known/caldav /cloud/remote.php/caldav/ redirect;
|
rewrite ^/.well-known/caldav /cloud/remote.php/caldav/ redirect;
|
||||||
|
|
||||||
|
# ADDITIONAL DIRECTIVES HERE
|
||||||
|
|||||||
@@ -70,6 +70,5 @@ ssl_dhparam STORAGE_ROOT/ssl/dh2048.pem;
|
|||||||
# nginx will use them to talk to the CA.
|
# nginx will use them to talk to the CA.
|
||||||
ssl_stapling on;
|
ssl_stapling on;
|
||||||
ssl_stapling_verify on;
|
ssl_stapling_verify on;
|
||||||
resolver 8.8.8.8 8.8.4.4 valid=86400;
|
resolver 127.0.0.1 valid=86400;
|
||||||
resolver_timeout 10;
|
resolver_timeout 10;
|
||||||
#ssl_trusted_certificate /path/to/all-certs-in-chain.crt;
|
|
||||||
|
|||||||
@@ -33,84 +33,5 @@ server {
|
|||||||
ssl_certificate_key $SSL_KEY;
|
ssl_certificate_key $SSL_KEY;
|
||||||
include /etc/nginx/nginx-ssl.conf;
|
include /etc/nginx/nginx-ssl.conf;
|
||||||
|
|
||||||
# Expose this directory as static files.
|
|
||||||
root $ROOT;
|
|
||||||
index index.html index.htm;
|
|
||||||
|
|
||||||
location = /robots.txt {
|
|
||||||
log_not_found off;
|
|
||||||
access_log off;
|
|
||||||
}
|
|
||||||
|
|
||||||
location = /favicon.ico {
|
|
||||||
log_not_found off;
|
|
||||||
access_log off;
|
|
||||||
}
|
|
||||||
|
|
||||||
location = /mailinabox.mobileconfig {
|
|
||||||
alias /var/lib/mailinabox/mobileconfig.xml;
|
|
||||||
}
|
|
||||||
location = /.well-known/autoconfig/mail/config-v1.1.xml {
|
|
||||||
alias /var/lib/mailinabox/mozilla-autoconfig.xml;
|
|
||||||
}
|
|
||||||
|
|
||||||
# Roundcube Webmail configuration.
|
|
||||||
rewrite ^/mail$ /mail/ redirect;
|
|
||||||
rewrite ^/mail/$ /mail/index.php;
|
|
||||||
location /mail/ {
|
|
||||||
index index.php;
|
|
||||||
alias /usr/local/lib/roundcubemail/;
|
|
||||||
}
|
|
||||||
location ~ /mail/config/.* {
|
|
||||||
# A ~-style location is needed to give this precedence over the next block.
|
|
||||||
return 403;
|
|
||||||
}
|
|
||||||
location ~ /mail/.*\.php {
|
|
||||||
# note: ~ has precendence over a regular location block
|
|
||||||
include fastcgi_params;
|
|
||||||
fastcgi_split_path_info ^/mail(/.*)()$;
|
|
||||||
fastcgi_index index.php;
|
|
||||||
fastcgi_param SCRIPT_FILENAME /usr/local/lib/roundcubemail/$fastcgi_script_name;
|
|
||||||
fastcgi_pass php-fpm;
|
|
||||||
|
|
||||||
# Outgoing mail also goes through this endpoint, so increase the maximum
|
|
||||||
# file upload limit to match the corresponding Postfix limit.
|
|
||||||
client_max_body_size 128M;
|
|
||||||
}
|
|
||||||
|
|
||||||
# Z-Push (Microsoft Exchange ActiveSync)
|
|
||||||
location /Microsoft-Server-ActiveSync {
|
|
||||||
include /etc/nginx/fastcgi_params;
|
|
||||||
fastcgi_param SCRIPT_FILENAME /usr/local/lib/z-push/index.php;
|
|
||||||
fastcgi_param PHP_VALUE "include_path=.:/usr/share/php:/usr/share/pear:/usr/share/awl/inc";
|
|
||||||
fastcgi_read_timeout 630;
|
|
||||||
fastcgi_pass php-fpm;
|
|
||||||
|
|
||||||
# Outgoing mail also goes through this endpoint, so increase the maximum
|
|
||||||
# file upload limit to match the corresponding Postfix limit.
|
|
||||||
client_max_body_size 128M;
|
|
||||||
}
|
|
||||||
location /autodiscover/autodiscover.xml {
|
|
||||||
include fastcgi_params;
|
|
||||||
fastcgi_param SCRIPT_FILENAME /usr/local/lib/z-push/autodiscover/autodiscover.php;
|
|
||||||
fastcgi_param PHP_VALUE "include_path=.:/usr/share/php:/usr/share/pear:/usr/share/awl/inc";
|
|
||||||
fastcgi_pass php-fpm;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
# ADDITIONAL DIRECTIVES HERE
|
# ADDITIONAL DIRECTIVES HERE
|
||||||
|
|
||||||
# Disable viewing dotfiles (.htaccess, .svn, .git, etc.)
|
|
||||||
# This block is placed at the end. Nginx's precedence rules means this block
|
|
||||||
# takes precedence over all non-regex matches and only regex matches that
|
|
||||||
# come after it (i.e. none of those, since this is the last one.) That means
|
|
||||||
# we're blocking dotfiles in the static hosted sites but not the FastCGI-
|
|
||||||
# handled locations for ownCloud (which serves user-uploaded files that might
|
|
||||||
# have this pattern, see #414) or some of the other services.
|
|
||||||
location ~ /\.(ht|svn|git|hg|bzr) {
|
|
||||||
log_not_found off;
|
|
||||||
access_log off;
|
|
||||||
deny all;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -88,8 +88,9 @@ class KeyAuthService:
|
|||||||
if email == "" or pw == "":
|
if email == "" or pw == "":
|
||||||
raise ValueError("Enter an email address and password.")
|
raise ValueError("Enter an email address and password.")
|
||||||
|
|
||||||
# The password might be a user-specific API key.
|
# The password might be a user-specific API key. create_user_key raises
|
||||||
if hmac.compare_digest(self.create_user_key(email), pw):
|
# a ValueError if the user does not exist.
|
||||||
|
if hmac.compare_digest(self.create_user_key(email, env), pw):
|
||||||
# OK.
|
# OK.
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
@@ -111,18 +112,26 @@ class KeyAuthService:
|
|||||||
# Login failed.
|
# Login failed.
|
||||||
raise ValueError("Invalid password.")
|
raise ValueError("Invalid password.")
|
||||||
|
|
||||||
# Get privileges for authorization. This call should never fail on a valid user,
|
# Get privileges for authorization. This call should never fail because by this
|
||||||
# but if the caller passed a user-specific API key then the user may no longer
|
# point we know the email address is a valid user. But on error the call will
|
||||||
# exist --- in that case, get_mail_user_privileges will return a tuple of an
|
# return a tuple of an error message and an HTTP status code.
|
||||||
# error message and an HTTP status code.
|
|
||||||
privs = get_mail_user_privileges(email, env)
|
privs = get_mail_user_privileges(email, env)
|
||||||
if isinstance(privs, tuple): raise ValueError(privs[0])
|
if isinstance(privs, tuple): raise ValueError(privs[0])
|
||||||
|
|
||||||
# Return a list of privileges.
|
# Return a list of privileges.
|
||||||
return privs
|
return privs
|
||||||
|
|
||||||
def create_user_key(self, email):
|
def create_user_key(self, email, env):
|
||||||
return hmac.new(self.key.encode('ascii'), b"AUTH:" + email.encode("utf8"), digestmod="sha1").hexdigest()
|
# Store an HMAC with the client. The hashed message of the HMAC will be the user's
|
||||||
|
# email address & hashed password and the key will be the master API key. The user of
|
||||||
|
# course has their own email address and password. We assume they do not have the master
|
||||||
|
# API key (unless they are trusted anyway). The HMAC proves that they authenticated
|
||||||
|
# with us in some other way to get the HMAC. Including the password means that when
|
||||||
|
# a user's password is reset, the HMAC changes and they will correctly need to log
|
||||||
|
# in to the control panel again. This method raises a ValueError if the user does
|
||||||
|
# not exist, due to get_mail_password.
|
||||||
|
msg = b"AUTH:" + email.encode("utf8") + b" " + get_mail_password(email, env).encode("utf8")
|
||||||
|
return hmac.new(self.key.encode('ascii'), msg, digestmod="sha256").hexdigest()
|
||||||
|
|
||||||
def _generate_key(self):
|
def _generate_key(self):
|
||||||
raw_key = os.urandom(32)
|
raw_key = os.urandom(32)
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import os, os.path, re, json
|
|||||||
|
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
|
|
||||||
from flask import Flask, request, render_template, abort, Response
|
from flask import Flask, request, render_template, abort, Response, send_from_directory
|
||||||
|
|
||||||
import auth, utils
|
import auth, utils
|
||||||
from mailconfig import get_mail_users, get_mail_users_ex, get_admins, add_mail_user, set_mail_password, remove_mail_user
|
from mailconfig import get_mail_users, get_mail_users_ex, get_admins, add_mail_user, set_mail_password, remove_mail_user
|
||||||
@@ -118,7 +118,7 @@ def me():
|
|||||||
|
|
||||||
# Is authorized as admin? Return an API key for future use.
|
# Is authorized as admin? Return an API key for future use.
|
||||||
if "admin" in privs:
|
if "admin" in privs:
|
||||||
resp["api_key"] = auth_service.create_user_key(email)
|
resp["api_key"] = auth_service.create_user_key(email, env)
|
||||||
|
|
||||||
# Return.
|
# Return.
|
||||||
return json_response(resp)
|
return json_response(resp)
|
||||||
@@ -340,6 +340,24 @@ def web_update():
|
|||||||
|
|
||||||
# System
|
# System
|
||||||
|
|
||||||
|
@app.route('/system/version', methods=["GET"])
|
||||||
|
@authorized_personnel_only
|
||||||
|
def system_version():
|
||||||
|
from status_checks import what_version_is_this
|
||||||
|
try:
|
||||||
|
return what_version_is_this(env)
|
||||||
|
except Exception as e:
|
||||||
|
return (str(e), 500)
|
||||||
|
|
||||||
|
@app.route('/system/latest-upstream-version', methods=["POST"])
|
||||||
|
@authorized_personnel_only
|
||||||
|
def system_latest_upstream_version():
|
||||||
|
from status_checks import get_latest_miab_version
|
||||||
|
try:
|
||||||
|
return get_latest_miab_version()
|
||||||
|
except Exception as e:
|
||||||
|
return (str(e), 500)
|
||||||
|
|
||||||
@app.route('/system/status', methods=["POST"])
|
@app.route('/system/status', methods=["POST"])
|
||||||
@authorized_personnel_only
|
@authorized_personnel_only
|
||||||
def system_status():
|
def system_status():
|
||||||
@@ -384,6 +402,17 @@ def backup_status():
|
|||||||
from backup import backup_status
|
from backup import backup_status
|
||||||
return json_response(backup_status(env))
|
return json_response(backup_status(env))
|
||||||
|
|
||||||
|
# MUNIN
|
||||||
|
|
||||||
|
@app.route('/munin/')
|
||||||
|
@app.route('/munin/<path:filename>')
|
||||||
|
@authorized_personnel_only
|
||||||
|
def munin(filename=""):
|
||||||
|
# Checks administrative access (@authorized_personnel_only) and then just proxies
|
||||||
|
# the request to static files.
|
||||||
|
if filename == "": filename = "index.html"
|
||||||
|
return send_from_directory("/var/cache/munin/www", filename)
|
||||||
|
|
||||||
# APP
|
# APP
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|||||||
@@ -57,13 +57,15 @@ def do_dns_update(env, force=False):
|
|||||||
|
|
||||||
# Custom records to add to zones.
|
# Custom records to add to zones.
|
||||||
additional_records = list(get_custom_dns_config(env))
|
additional_records = list(get_custom_dns_config(env))
|
||||||
|
from web_update import get_default_www_redirects
|
||||||
|
www_redirect_domains = get_default_www_redirects(env)
|
||||||
|
|
||||||
# Write zone files.
|
# Write zone files.
|
||||||
os.makedirs('/etc/nsd/zones', exist_ok=True)
|
os.makedirs('/etc/nsd/zones', exist_ok=True)
|
||||||
updated_domains = []
|
updated_domains = []
|
||||||
for i, (domain, zonefile) in enumerate(zonefiles):
|
for i, (domain, zonefile) in enumerate(zonefiles):
|
||||||
# Build the records to put in the zone.
|
# Build the records to put in the zone.
|
||||||
records = build_zone(domain, domains, additional_records, env)
|
records = build_zone(domain, domains, additional_records, www_redirect_domains, env)
|
||||||
|
|
||||||
# See if the zone has changed, and if so update the serial number
|
# See if the zone has changed, and if so update the serial number
|
||||||
# and write the zone file.
|
# and write the zone file.
|
||||||
@@ -126,7 +128,7 @@ def do_dns_update(env, force=False):
|
|||||||
|
|
||||||
########################################################################
|
########################################################################
|
||||||
|
|
||||||
def build_zone(domain, all_domains, additional_records, env, is_zone=True):
|
def build_zone(domain, all_domains, additional_records, www_redirect_domains, env, is_zone=True):
|
||||||
records = []
|
records = []
|
||||||
|
|
||||||
# For top-level zones, define the authoritative name servers.
|
# For top-level zones, define the authoritative name servers.
|
||||||
@@ -177,7 +179,7 @@ def build_zone(domain, all_domains, additional_records, env, is_zone=True):
|
|||||||
subdomains = [d for d in all_domains if d.endswith("." + domain)]
|
subdomains = [d for d in all_domains if d.endswith("." + domain)]
|
||||||
for subdomain in subdomains:
|
for subdomain in subdomains:
|
||||||
subdomain_qname = subdomain[0:-len("." + domain)]
|
subdomain_qname = subdomain[0:-len("." + domain)]
|
||||||
subzone = build_zone(subdomain, [], additional_records, env, is_zone=False)
|
subzone = build_zone(subdomain, [], additional_records, www_redirect_domains, env, is_zone=False)
|
||||||
for child_qname, child_rtype, child_value, child_explanation in subzone:
|
for child_qname, child_rtype, child_value, child_explanation in subzone:
|
||||||
if child_qname == None:
|
if child_qname == None:
|
||||||
child_qname = subdomain_qname
|
child_qname = subdomain_qname
|
||||||
@@ -211,14 +213,20 @@ def build_zone(domain, all_domains, additional_records, env, is_zone=True):
|
|||||||
records.append((qname, rtype, value, "(Set by user.)"))
|
records.append((qname, rtype, value, "(Set by user.)"))
|
||||||
|
|
||||||
# Add defaults if not overridden by the user's custom settings (and not otherwise configured).
|
# Add defaults if not overridden by the user's custom settings (and not otherwise configured).
|
||||||
# Any "CNAME" record on the qname overrides A and AAAA.
|
# Any CNAME or A record on the qname overrides A and AAAA. But when we set the default A record,
|
||||||
has_rec_base = records
|
# we should not cause the default AAAA record to be skipped because it thinks a custom A record
|
||||||
|
# was set. So set has_rec_base to a clone of the current set of DNS settings, and don't update
|
||||||
|
# during this process.
|
||||||
|
has_rec_base = list(records)
|
||||||
defaults = [
|
defaults = [
|
||||||
(None, "A", env["PUBLIC_IP"], "Required. May have a different value. Sets the IP address that %s resolves to for web hosting and other services besides mail. The A record must be present but its value does not affect mail delivery." % domain),
|
(None, "A", env["PUBLIC_IP"], "Required. May have a different value. Sets the IP address that %s resolves to for web hosting and other services besides mail. The A record must be present but its value does not affect mail delivery." % domain),
|
||||||
("www", "A", env["PUBLIC_IP"], "Optional. Sets the IP address that www.%s resolves to, e.g. for web hosting." % domain),
|
|
||||||
(None, "AAAA", env.get('PUBLIC_IPV6'), "Optional. Sets the IPv6 address that %s resolves to, e.g. for web hosting. (It is not necessary for receiving mail on this domain.)" % domain),
|
(None, "AAAA", env.get('PUBLIC_IPV6'), "Optional. Sets the IPv6 address that %s resolves to, e.g. for web hosting. (It is not necessary for receiving mail on this domain.)" % domain),
|
||||||
("www", "AAAA", env.get('PUBLIC_IPV6'), "Optional. Sets the IPv6 address that www.%s resolves to, e.g. for web hosting." % domain),
|
|
||||||
]
|
]
|
||||||
|
if "www." + domain in www_redirect_domains:
|
||||||
|
defaults += [
|
||||||
|
("www", "A", env["PUBLIC_IP"], "Optional. Sets the IP address that www.%s resolves to so that the box can provide a redirect to the parent domain." % domain),
|
||||||
|
("www", "AAAA", env.get('PUBLIC_IPV6'), "Optional. Sets the IPv6 address that www.%s resolves to so that the box can provide a redirect to the parent domain." % domain),
|
||||||
|
]
|
||||||
for qname, rtype, value, explanation in defaults:
|
for qname, rtype, value, explanation in defaults:
|
||||||
if value is None or value.strip() == "": continue # skip IPV6 if not set
|
if value is None or value.strip() == "": continue # skip IPV6 if not set
|
||||||
if not is_zone and qname == "www": continue # don't create any default 'www' subdomains on what are themselves subdomains
|
if not is_zone and qname == "www": continue # don't create any default 'www' subdomains on what are themselves subdomains
|
||||||
@@ -229,6 +237,9 @@ def build_zone(domain, all_domains, additional_records, env, is_zone=True):
|
|||||||
if not has_rec(qname, rtype) and not has_rec(qname, "CNAME") and not has_rec(qname, "A"):
|
if not has_rec(qname, rtype) and not has_rec(qname, "CNAME") and not has_rec(qname, "A"):
|
||||||
records.append((qname, rtype, value, explanation))
|
records.append((qname, rtype, value, explanation))
|
||||||
|
|
||||||
|
# Don't pin the list of records that has_rec checks against anymore.
|
||||||
|
has_rec_base = records
|
||||||
|
|
||||||
# SPF record: Permit the box ('mx', see above) to send mail on behalf of
|
# SPF record: Permit the box ('mx', see above) to send mail on behalf of
|
||||||
# the domain, and no one else.
|
# the domain, and no one else.
|
||||||
# Skip if the user has set a custom SPF record.
|
# Skip if the user has set a custom SPF record.
|
||||||
@@ -239,8 +250,8 @@ def build_zone(domain, all_domains, additional_records, env, is_zone=True):
|
|||||||
# Skip if the user has set a DKIM record already.
|
# Skip if the user has set a DKIM record already.
|
||||||
opendkim_record_file = os.path.join(env['STORAGE_ROOT'], 'mail/dkim/mail.txt')
|
opendkim_record_file = os.path.join(env['STORAGE_ROOT'], 'mail/dkim/mail.txt')
|
||||||
with open(opendkim_record_file) as orf:
|
with open(opendkim_record_file) as orf:
|
||||||
m = re.match(r'(\S+)\s+IN\s+TXT\s+\( "([^"]+)"\s+"([^"]+)"\s*\)', orf.read(), re.S)
|
m = re.match(r'(\S+)\s+IN\s+TXT\s+\( ((?:"[^"]+"\s+)+)\)', orf.read(), re.S)
|
||||||
val = m.group(2) + m.group(3)
|
val = "".join(re.findall(r'"([^"]+)"', m.group(2)))
|
||||||
if not has_rec(m.group(1), "TXT", prefix="v=DKIM1; "):
|
if not has_rec(m.group(1), "TXT", prefix="v=DKIM1; "):
|
||||||
records.append((m.group(1), "TXT", val, "Recommended. Provides a way for recipients to verify that this machine sent @%s mail." % domain))
|
records.append((m.group(1), "TXT", val, "Recommended. Provides a way for recipients to verify that this machine sent @%s mail." % domain))
|
||||||
|
|
||||||
@@ -362,9 +373,16 @@ $TTL 1800 ; default time to live
|
|||||||
zone += subdomain
|
zone += subdomain
|
||||||
zone += "\tIN\t" + querytype + "\t"
|
zone += "\tIN\t" + querytype + "\t"
|
||||||
if querytype == "TXT":
|
if querytype == "TXT":
|
||||||
value = value.replace('\\', '\\\\') # escape backslashes
|
# Divide into 255-byte max substrings.
|
||||||
value = value.replace('"', '\\"') # escape quotes
|
v2 = ""
|
||||||
value = '"' + value + '"' # wrap in quotes
|
while len(value) > 0:
|
||||||
|
s = value[0:255]
|
||||||
|
value = value[255:]
|
||||||
|
s = s.replace('\\', '\\\\') # escape backslashes
|
||||||
|
s = s.replace('"', '\\"') # escape quotes
|
||||||
|
s = '"' + s + '"' # wrap in quotes
|
||||||
|
v2 += s + " "
|
||||||
|
value = v2
|
||||||
zone += value + "\n"
|
zone += value + "\n"
|
||||||
|
|
||||||
# DNSSEC requires re-signing a zone periodically. That requires
|
# DNSSEC requires re-signing a zone periodically. That requires
|
||||||
@@ -847,8 +865,10 @@ def build_recommended_dns(env):
|
|||||||
domains = get_dns_domains(env)
|
domains = get_dns_domains(env)
|
||||||
zonefiles = get_dns_zones(env)
|
zonefiles = get_dns_zones(env)
|
||||||
additional_records = list(get_custom_dns_config(env))
|
additional_records = list(get_custom_dns_config(env))
|
||||||
|
from web_update import get_default_www_redirects
|
||||||
|
www_redirect_domains = get_default_www_redirects(env)
|
||||||
for domain, zonefile in zonefiles:
|
for domain, zonefile in zonefiles:
|
||||||
records = build_zone(domain, domains, additional_records, env)
|
records = build_zone(domain, domains, additional_records, www_redirect_domains, env)
|
||||||
|
|
||||||
# remove records that we don't dislay
|
# remove records that we don't dislay
|
||||||
records = [r for r in records if r[3] is not False]
|
records = [r for r in records if r[3] is not False]
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import subprocess, shutil, os, sqlite3, re
|
import subprocess, shutil, os, sqlite3, re
|
||||||
import utils
|
import utils
|
||||||
from email_validator import validate_email as validate_email_, EmailNotValidError
|
from email_validator import validate_email as validate_email_, EmailNotValidError
|
||||||
|
import idna
|
||||||
|
|
||||||
def validate_email(email, mode=None):
|
def validate_email(email, mode=None):
|
||||||
# Checks that an email address is syntactically valid. Returns True/False.
|
# Checks that an email address is syntactically valid. Returns True/False.
|
||||||
@@ -52,11 +53,13 @@ def sanitize_idn_email_address(email):
|
|||||||
# to the underlying protocols.
|
# to the underlying protocols.
|
||||||
try:
|
try:
|
||||||
localpart, domainpart = email.split("@")
|
localpart, domainpart = email.split("@")
|
||||||
domainpart = domainpart.encode("idna").decode('ascii')
|
domainpart = idna.encode(domainpart).decode('ascii')
|
||||||
return localpart + "@" + domainpart
|
return localpart + "@" + domainpart
|
||||||
except:
|
except (ValueError, idna.IDNAError):
|
||||||
# Domain part is not IDNA-valid, so leave unchanged. If there
|
# ValueError: String does not have a single @-sign, so it is not
|
||||||
# are non-ASCII characters it will be filtered out by
|
# a valid email address. IDNAError: Domain part is not IDNA-valid.
|
||||||
|
# Validation is not this function's job, so return value unchanged.
|
||||||
|
# If there are non-ASCII characters it will be filtered out by
|
||||||
# validate_email.
|
# validate_email.
|
||||||
return email
|
return email
|
||||||
|
|
||||||
@@ -65,10 +68,11 @@ def prettify_idn_email_address(email):
|
|||||||
# names in IDNA in the database, but we want to show Unicode to the user.
|
# names in IDNA in the database, but we want to show Unicode to the user.
|
||||||
try:
|
try:
|
||||||
localpart, domainpart = email.split("@")
|
localpart, domainpart = email.split("@")
|
||||||
domainpart = domainpart.encode("ascii").decode('idna')
|
domainpart = idna.decode(domainpart.encode("ascii"))
|
||||||
return localpart + "@" + domainpart
|
return localpart + "@" + domainpart
|
||||||
except:
|
except (ValueError, UnicodeError, idna.IDNAError):
|
||||||
# Failed to decode IDNA. Should never happen.
|
# Failed to decode IDNA, or the email address does not have a
|
||||||
|
# single @-sign. Should never happen.
|
||||||
return email
|
return email
|
||||||
|
|
||||||
def is_dcv_address(email):
|
def is_dcv_address(email):
|
||||||
@@ -211,7 +215,7 @@ def get_mail_aliases_ex(env):
|
|||||||
for source, destination in get_mail_aliases(env):
|
for source, destination in get_mail_aliases(env):
|
||||||
# get alias info
|
# get alias info
|
||||||
domain = get_domain(source)
|
domain = get_domain(source)
|
||||||
required = ((source in required_aliases) or (source == get_system_administrator(env)))
|
required = (source in required_aliases)
|
||||||
|
|
||||||
# add to list
|
# add to list
|
||||||
if not domain in domains:
|
if not domain in domains:
|
||||||
@@ -238,7 +242,7 @@ def get_domain(emailaddr, as_unicode=True):
|
|||||||
# Gets the domain part of an email address. Turns IDNA
|
# Gets the domain part of an email address. Turns IDNA
|
||||||
# back to Unicode for display.
|
# back to Unicode for display.
|
||||||
ret = emailaddr.split('@', 1)[1]
|
ret = emailaddr.split('@', 1)[1]
|
||||||
if as_unicode: ret = ret.encode('ascii').decode('idna')
|
if as_unicode: ret = idna.decode(ret.encode('ascii'))
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
def get_mail_domains(env, filter_aliases=lambda alias : True):
|
def get_mail_domains(env, filter_aliases=lambda alias : True):
|
||||||
@@ -493,15 +497,17 @@ def get_required_aliases(env):
|
|||||||
# These are the aliases that must exist.
|
# These are the aliases that must exist.
|
||||||
aliases = set()
|
aliases = set()
|
||||||
|
|
||||||
|
# The system administrator alias is required.
|
||||||
|
aliases.add(get_system_administrator(env))
|
||||||
|
|
||||||
# The hostmaster alias is exposed in the DNS SOA for each zone.
|
# The hostmaster alias is exposed in the DNS SOA for each zone.
|
||||||
aliases.add("hostmaster@" + env['PRIMARY_HOSTNAME'])
|
aliases.add("hostmaster@" + env['PRIMARY_HOSTNAME'])
|
||||||
|
|
||||||
# Get a list of domains we serve mail for, except ones for which the only
|
# Get a list of domains we serve mail for, except ones for which the only
|
||||||
# email on that domain is a postmaster/admin alias to the administrator
|
# email on that domain are the required aliases or a catch-all/domain-forwarder.
|
||||||
# or a wildcard alias (since it will forward postmaster/admin).
|
|
||||||
real_mail_domains = get_mail_domains(env,
|
real_mail_domains = get_mail_domains(env,
|
||||||
filter_aliases = lambda alias :
|
filter_aliases = lambda alias :
|
||||||
((not alias[0].startswith("postmaster@") and not alias[0].startswith("admin@")) or alias[1] != get_system_administrator(env))
|
not alias[0].startswith("postmaster@") and not alias[0].startswith("admin@")
|
||||||
and not alias[0].startswith("@")
|
and not alias[0].startswith("@")
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -538,12 +544,13 @@ def kick(env, mail_result=None):
|
|||||||
for s, t in existing_aliases:
|
for s, t in existing_aliases:
|
||||||
if s == source:
|
if s == source:
|
||||||
return
|
return
|
||||||
|
|
||||||
# Doesn't exist.
|
# Doesn't exist.
|
||||||
administrator = get_system_administrator(env)
|
administrator = get_system_administrator(env)
|
||||||
|
if source == administrator: return # don't make an alias from the administrator to itself --- this alias must be created manually
|
||||||
add_mail_alias(source, administrator, env, do_kick=False)
|
add_mail_alias(source, administrator, env, do_kick=False)
|
||||||
results.append("added alias %s (=> %s)\n" % (source, administrator))
|
results.append("added alias %s (=> %s)\n" % (source, administrator))
|
||||||
|
|
||||||
|
|
||||||
for alias in required_aliases:
|
for alias in required_aliases:
|
||||||
ensure_admin_alias_exists(alias)
|
ensure_admin_alias_exists(alias)
|
||||||
|
|
||||||
|
|||||||
@@ -10,9 +10,10 @@ import sys, os, os.path, re, subprocess, datetime, multiprocessing.pool
|
|||||||
|
|
||||||
import dns.reversename, dns.resolver
|
import dns.reversename, dns.resolver
|
||||||
import dateutil.parser, dateutil.tz
|
import dateutil.parser, dateutil.tz
|
||||||
|
import idna
|
||||||
|
|
||||||
from dns_update import get_dns_zones, build_tlsa_record, get_custom_dns_config, get_secondary_dns
|
from dns_update import get_dns_zones, build_tlsa_record, get_custom_dns_config, get_secondary_dns
|
||||||
from web_update import get_web_domains, get_domain_ssl_files
|
from web_update import get_web_domains, get_default_www_redirects, get_domain_ssl_files
|
||||||
from mailconfig import get_mail_domains, get_mail_aliases
|
from mailconfig import get_mail_domains, get_mail_aliases
|
||||||
|
|
||||||
from utils import shell, sort_domains, load_env_vars_from_file
|
from utils import shell, sort_domains, load_env_vars_from_file
|
||||||
@@ -41,15 +42,22 @@ def run_checks(rounded_values, env, output, pool):
|
|||||||
run_domain_checks(rounded_values, env, output, pool)
|
run_domain_checks(rounded_values, env, output, pool)
|
||||||
|
|
||||||
def get_ssh_port():
|
def get_ssh_port():
|
||||||
# Returns ssh port
|
# Returns ssh port
|
||||||
output = shell('check_output', ['sshd', '-T'])
|
try:
|
||||||
returnNext = False
|
output = shell('check_output', ['sshd', '-T'])
|
||||||
|
except FileNotFoundError:
|
||||||
|
# sshd is not installed. That's ok.
|
||||||
|
return None
|
||||||
|
|
||||||
for e in output.split():
|
returnNext = False
|
||||||
if returnNext:
|
for e in output.split():
|
||||||
return int(e)
|
if returnNext:
|
||||||
if e == "port":
|
return int(e)
|
||||||
returnNext = True
|
if e == "port":
|
||||||
|
returnNext = True
|
||||||
|
|
||||||
|
# Did not find port!
|
||||||
|
return None
|
||||||
|
|
||||||
def run_services_checks(env, output, pool):
|
def run_services_checks(env, output, pool):
|
||||||
# Check that system services are running.
|
# Check that system services are running.
|
||||||
@@ -81,6 +89,7 @@ def run_services_checks(env, output, pool):
|
|||||||
fatal = False
|
fatal = False
|
||||||
ret = pool.starmap(check_service, ((i, service, env) for i, service in enumerate(services)), chunksize=1)
|
ret = pool.starmap(check_service, ((i, service, env) for i, service in enumerate(services)), chunksize=1)
|
||||||
for i, running, fatal2, output2 in sorted(ret):
|
for i, running, fatal2, output2 in sorted(ret):
|
||||||
|
if output2 is None: continue # skip check (e.g. no port was set, e.g. no sshd)
|
||||||
all_running = all_running and running
|
all_running = all_running and running
|
||||||
fatal = fatal or fatal2
|
fatal = fatal or fatal2
|
||||||
output2.playback(output)
|
output2.playback(output)
|
||||||
@@ -91,6 +100,10 @@ def run_services_checks(env, output, pool):
|
|||||||
return not fatal
|
return not fatal
|
||||||
|
|
||||||
def check_service(i, service, env):
|
def check_service(i, service, env):
|
||||||
|
if not service["port"]:
|
||||||
|
# Skip check (no port, e.g. no sshd).
|
||||||
|
return (i, None, None, None)
|
||||||
|
|
||||||
import socket
|
import socket
|
||||||
output = BufferedOutput()
|
output = BufferedOutput()
|
||||||
running = False
|
running = False
|
||||||
@@ -227,7 +240,7 @@ def run_domain_checks(rounded_time, env, output, pool):
|
|||||||
dns_domains = set(dns_zonefiles)
|
dns_domains = set(dns_zonefiles)
|
||||||
|
|
||||||
# Get the list of domains we serve HTTPS for.
|
# Get the list of domains we serve HTTPS for.
|
||||||
web_domains = set(get_web_domains(env))
|
web_domains = set(get_web_domains(env) + get_default_www_redirects(env))
|
||||||
|
|
||||||
domains_to_check = mail_domains | dns_domains | web_domains
|
domains_to_check = mail_domains | dns_domains | web_domains
|
||||||
|
|
||||||
@@ -247,7 +260,7 @@ def run_domain_checks_on_domain(domain, rounded_time, env, dns_domains, dns_zone
|
|||||||
output = BufferedOutput()
|
output = BufferedOutput()
|
||||||
|
|
||||||
# The domain is IDNA-encoded, but for display use Unicode.
|
# The domain is IDNA-encoded, but for display use Unicode.
|
||||||
output.add_heading(domain.encode('ascii').decode('idna'))
|
output.add_heading(idna.decode(domain.encode('ascii')))
|
||||||
|
|
||||||
if domain == env["PRIMARY_HOSTNAME"]:
|
if domain == env["PRIMARY_HOSTNAME"]:
|
||||||
check_primary_hostname_dns(domain, env, output, dns_domains, dns_zonefiles)
|
check_primary_hostname_dns(domain, env, output, dns_domains, dns_zonefiles)
|
||||||
@@ -593,103 +606,115 @@ def check_ssl_cert(domain, rounded_time, env, output):
|
|||||||
output.print_line(cert_status_details)
|
output.print_line(cert_status_details)
|
||||||
output.print_line("")
|
output.print_line("")
|
||||||
|
|
||||||
def check_certificate(domain, ssl_certificate, ssl_private_key, warn_if_expiring_soon=True, rounded_time=False):
|
def check_certificate(domain, ssl_certificate, ssl_private_key, warn_if_expiring_soon=True, rounded_time=False, just_check_domain=False):
|
||||||
# Use openssl verify to check the status of a certificate.
|
# Check that the ssl_certificate & ssl_private_key files are good
|
||||||
|
# for the provided domain.
|
||||||
|
|
||||||
# First check that the certificate is for the right domain. The domain
|
from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey
|
||||||
# must be found in the Subject Common Name (CN) or be one of the
|
from cryptography.x509 import Certificate, DNSName, ExtensionNotFound, OID_COMMON_NAME, OID_SUBJECT_ALTERNATIVE_NAME
|
||||||
# Subject Alternative Names. A wildcard might also appear as the CN
|
import idna
|
||||||
# or in the SAN list, so check for that tool.
|
|
||||||
retcode, cert_dump = shell('check_output', [
|
|
||||||
"openssl", "x509",
|
|
||||||
"-in", ssl_certificate,
|
|
||||||
"-noout", "-text", "-nameopt", "rfc2253",
|
|
||||||
], trap=True)
|
|
||||||
|
|
||||||
# If the certificate is catastrophically bad, catch that now and report it.
|
# The ssl_certificate file may contain a chain of certificates. We'll
|
||||||
# More information was probably written to stderr (which we aren't capturing),
|
# need to split that up before we can pass anything to openssl or
|
||||||
# but it is probably not helpful to the user anyway.
|
# parse them in Python. Parse it with the cryptography library.
|
||||||
if retcode != 0:
|
try:
|
||||||
return ("The SSL certificate appears to be corrupted or not a PEM-formatted SSL certificate file. (%s)" % ssl_certificate, None)
|
ssl_cert_chain = load_cert_chain(ssl_certificate)
|
||||||
|
cert = load_pem(ssl_cert_chain[0])
|
||||||
|
if not isinstance(cert, Certificate): raise ValueError("This is not a certificate file.")
|
||||||
|
except ValueError as e:
|
||||||
|
return ("There is a problem with the certificate file: %s" % str(e), None)
|
||||||
|
|
||||||
cert_dump = cert_dump.split("\n")
|
# First check that the domain name is one of the names allowed by
|
||||||
certificate_names = set()
|
# the certificate.
|
||||||
cert_expiration_date = None
|
if domain is not None:
|
||||||
while len(cert_dump) > 0:
|
# The domain may be found in the Subject Common Name (CN). This comes back as an IDNA (ASCII)
|
||||||
line = cert_dump.pop(0)
|
# string, which is the format we store domains in - so good.
|
||||||
|
certificate_names = set()
|
||||||
|
try:
|
||||||
|
certificate_names.add(
|
||||||
|
cert.subject.get_attributes_for_oid(OID_COMMON_NAME)[0].value
|
||||||
|
)
|
||||||
|
except IndexError:
|
||||||
|
# No common name? Certificate is probably generated incorrectly.
|
||||||
|
# But we'll let it error-out when it doesn't find the domain.
|
||||||
|
pass
|
||||||
|
|
||||||
# Grab from the Subject Common Name. We include the indentation
|
# ... or be one of the Subject Alternative Names. The cryptography library handily IDNA-decodes
|
||||||
# at the start of the line in case maybe the cert includes the
|
# the names for us. We must encode back to ASCII, but wildcard certificates can't pass through
|
||||||
# common name of some other referenced entity (which would be
|
# IDNA encoding/decoding so we must special-case. See https://github.com/pyca/cryptography/pull/2071.
|
||||||
# indented, I hope).
|
def idna_decode_dns_name(dns_name):
|
||||||
m = re.match(" Subject: CN=([^,]+)", line)
|
if dns_name.startswith("*."):
|
||||||
if m:
|
return "*." + idna.encode(dns_name[2:]).decode('ascii')
|
||||||
certificate_names.add(m.group(1))
|
else:
|
||||||
|
return idna.encode(dns_name).decode('ascii')
|
||||||
# Grab from the Subject Alternative Name, which is a comma-delim
|
|
||||||
# list of names, like DNS:mydomain.com, DNS:otherdomain.com.
|
try:
|
||||||
m = re.match(" X509v3 Subject Alternative Name:", line)
|
sans = cert.extensions.get_extension_for_oid(OID_SUBJECT_ALTERNATIVE_NAME).value.get_values_for_type(DNSName)
|
||||||
if m:
|
for san in sans:
|
||||||
names = re.split(",\s*", cert_dump.pop(0).strip())
|
certificate_names.add(idna_decode_dns_name(san))
|
||||||
for n in names:
|
except ExtensionNotFound:
|
||||||
m = re.match("DNS:(.*)", n)
|
pass
|
||||||
if m:
|
|
||||||
certificate_names.add(m.group(1))
|
|
||||||
|
|
||||||
# Grab the expiration date for testing later.
|
# Check that the domain appears among the acceptable names, or a wildcard
|
||||||
m = re.match(" Not After : (.*)", line)
|
# form of the domain name (which is a stricter check than the specs but
|
||||||
if m:
|
# should work in normal cases).
|
||||||
cert_expiration_date = dateutil.parser.parse(m.group(1))
|
wildcard_domain = re.sub("^[^\.]+", "*", domain)
|
||||||
|
if domain not in certificate_names and wildcard_domain not in certificate_names:
|
||||||
|
return ("The certificate is for the wrong domain name. It is for %s."
|
||||||
|
% ", ".join(sorted(certificate_names)), None)
|
||||||
|
|
||||||
wildcard_domain = re.sub("^[^\.]+", "*", domain)
|
# Second, check that the certificate matches the private key.
|
||||||
if domain is not None and domain not in certificate_names and wildcard_domain not in certificate_names:
|
|
||||||
return ("The certificate is for the wrong domain name. It is for %s."
|
|
||||||
% ", ".join(sorted(certificate_names)), None)
|
|
||||||
|
|
||||||
# Second, check that the certificate matches the private key. Get the modulus of the
|
|
||||||
# private key and of the public key in the certificate. They should match. The output
|
|
||||||
# of each command looks like "Modulus=XXXXX".
|
|
||||||
if ssl_private_key is not None:
|
if ssl_private_key is not None:
|
||||||
private_key_modulus = shell('check_output', [
|
try:
|
||||||
"openssl", "rsa",
|
priv_key = load_pem(open(ssl_private_key, 'rb').read())
|
||||||
"-inform", "PEM",
|
except ValueError as e:
|
||||||
"-noout", "-modulus",
|
return ("The private key file %s is not a private key file: %s" % (ssl_private_key, str(e)), None)
|
||||||
"-in", ssl_private_key])
|
|
||||||
cert_key_modulus = shell('check_output', [
|
if not isinstance(priv_key, RSAPrivateKey):
|
||||||
"openssl", "x509",
|
return ("The private key file %s is not a private key file." % ssl_private_key, None)
|
||||||
"-in", ssl_certificate,
|
|
||||||
"-noout", "-modulus"])
|
if priv_key.public_key().public_numbers() != cert.public_key().public_numbers():
|
||||||
if private_key_modulus != cert_key_modulus:
|
return ("The certificate does not correspond to the private key at %s." % ssl_private_key, None)
|
||||||
return ("The certificate installed at %s does not correspond to the private key at %s." % (ssl_certificate, ssl_private_key), None)
|
|
||||||
|
# We could also use the openssl command line tool to get the modulus
|
||||||
|
# listed in each file. The output of each command below looks like "Modulus=XXXXX".
|
||||||
|
# $ openssl rsa -inform PEM -noout -modulus -in ssl_private_key
|
||||||
|
# $ openssl x509 -in ssl_certificate -noout -modulus
|
||||||
|
|
||||||
|
# Third, check if the certificate is self-signed. Return a special flag string.
|
||||||
|
if cert.issuer == cert.subject:
|
||||||
|
return ("SELF-SIGNED", None)
|
||||||
|
|
||||||
|
# When selecting which certificate to use for non-primary domains, we check if the primary
|
||||||
|
# certificate or a www-parent-domain certificate is good for the domain. There's no need
|
||||||
|
# to run extra checks beyond this point.
|
||||||
|
if just_check_domain:
|
||||||
|
return ("OK", None)
|
||||||
|
|
||||||
|
# Check that the certificate hasn't expired. The datetimes returned by the
|
||||||
|
# certificate are 'naive' and in UTC. We need to get the current time in UTC.
|
||||||
|
now = datetime.datetime.utcnow()
|
||||||
|
if not(cert.not_valid_before <= now <= cert.not_valid_after):
|
||||||
|
return ("The certificate has expired or is not yet valid. It is valid from %s to %s." % (cert.not_valid_before, cert.not_valid_after), None)
|
||||||
|
|
||||||
# Next validate that the certificate is valid. This checks whether the certificate
|
# Next validate that the certificate is valid. This checks whether the certificate
|
||||||
# is self-signed, that the chain of trust makes sense, that it is signed by a CA
|
# is self-signed, that the chain of trust makes sense, that it is signed by a CA
|
||||||
# that Ubuntu has installed on this machine's list of CAs, and I think that it hasn't
|
# that Ubuntu has installed on this machine's list of CAs, and I think that it hasn't
|
||||||
# expired.
|
# expired.
|
||||||
|
|
||||||
# In order to verify with openssl, we need to split out any
|
# The certificate chain has to be passed separately and is given via STDIN.
|
||||||
# intermediary certificates in the chain (if any) from our
|
|
||||||
# certificate (at the top). They need to be passed separately.
|
|
||||||
|
|
||||||
cert = open(ssl_certificate).read()
|
|
||||||
m = re.match(r'(-*BEGIN CERTIFICATE-*.*?-*END CERTIFICATE-*)(.*)', cert, re.S)
|
|
||||||
if m == None:
|
|
||||||
return ("The certificate file is an invalid PEM certificate.", None)
|
|
||||||
mycert, chaincerts = m.groups()
|
|
||||||
|
|
||||||
# This command returns a non-zero exit status in most cases, so trap errors.
|
# This command returns a non-zero exit status in most cases, so trap errors.
|
||||||
|
|
||||||
retcode, verifyoutput = shell('check_output', [
|
retcode, verifyoutput = shell('check_output', [
|
||||||
"openssl",
|
"openssl",
|
||||||
"verify", "-verbose",
|
"verify", "-verbose",
|
||||||
"-purpose", "sslserver", "-policy_check",]
|
"-purpose", "sslserver", "-policy_check",]
|
||||||
+ ([] if chaincerts.strip() == "" else ["-untrusted", "/dev/stdin"])
|
+ ([] if len(ssl_cert_chain) == 1 else ["-untrusted", "/dev/stdin"])
|
||||||
+ [ssl_certificate],
|
+ [ssl_certificate],
|
||||||
input=chaincerts.encode('ascii'),
|
input=b"\n\n".join(ssl_cert_chain[1:]),
|
||||||
trap=True)
|
trap=True)
|
||||||
|
|
||||||
if "self signed" in verifyoutput:
|
if "self signed" in verifyoutput:
|
||||||
# Certificate is self-signed.
|
# Certificate is self-signed. Probably we detected this above.
|
||||||
return ("SELF-SIGNED", None)
|
return ("SELF-SIGNED", None)
|
||||||
|
|
||||||
elif retcode != 0:
|
elif retcode != 0:
|
||||||
@@ -704,7 +729,7 @@ def check_certificate(domain, ssl_certificate, ssl_private_key, warn_if_expiring
|
|||||||
# good.
|
# good.
|
||||||
|
|
||||||
# But is it expiring soon?
|
# But is it expiring soon?
|
||||||
now = datetime.datetime.now(dateutil.tz.tzlocal())
|
cert_expiration_date = cert.not_valid_after
|
||||||
ndays = (cert_expiration_date-now).days
|
ndays = (cert_expiration_date-now).days
|
||||||
if not rounded_time or ndays < 7:
|
if not rounded_time or ndays < 7:
|
||||||
expiry_info = "The certificate expires in %d days on %s." % (ndays, cert_expiration_date.strftime("%x"))
|
expiry_info = "The certificate expires in %d days on %s." % (ndays, cert_expiration_date.strftime("%x"))
|
||||||
@@ -721,6 +746,33 @@ def check_certificate(domain, ssl_certificate, ssl_private_key, warn_if_expiring
|
|||||||
# Return the special OK code.
|
# Return the special OK code.
|
||||||
return ("OK", expiry_info)
|
return ("OK", expiry_info)
|
||||||
|
|
||||||
|
def load_cert_chain(pemfile):
|
||||||
|
# A certificate .pem file may contain a chain of certificates.
|
||||||
|
# Load the file and split them apart.
|
||||||
|
re_pem = rb"(-+BEGIN (?:.+)-+[\r\n](?:[A-Za-z0-9+/=]{1,64}[\r\n])+-+END (?:.+)-+[\r\n])"
|
||||||
|
with open(pemfile, "rb") as f:
|
||||||
|
pem = f.read() + b"\n" # ensure trailing newline
|
||||||
|
pemblocks = re.findall(re_pem, pem)
|
||||||
|
if len(pemblocks) == 0:
|
||||||
|
raise ValueError("File does not contain valid PEM data.")
|
||||||
|
return pemblocks
|
||||||
|
|
||||||
|
def load_pem(pem):
|
||||||
|
# Parse a "---BEGIN .... END---" PEM string and return a Python object for it
|
||||||
|
# using classes from the cryptography package.
|
||||||
|
from cryptography.x509 import load_pem_x509_certificate
|
||||||
|
from cryptography.hazmat.primitives import serialization
|
||||||
|
from cryptography.hazmat.backends import default_backend
|
||||||
|
pem_type = re.match(b"-+BEGIN (.*?)-+\n", pem)
|
||||||
|
if pem_type is None:
|
||||||
|
raise ValueError("File is not a valid PEM-formatted file.")
|
||||||
|
pem_type = pem_type.group(1)
|
||||||
|
if pem_type in (b"RSA PRIVATE KEY", b"PRIVATE KEY"):
|
||||||
|
return serialization.load_pem_private_key(pem, password=None, backend=default_backend())
|
||||||
|
if pem_type == b"CERTIFICATE":
|
||||||
|
return load_pem_x509_certificate(pem, default_backend())
|
||||||
|
raise ValueError("Unsupported PEM object type: " + pem_type.decode("ascii", "replace"))
|
||||||
|
|
||||||
_apt_updates = None
|
_apt_updates = None
|
||||||
def list_apt_updates(apt_update=True):
|
def list_apt_updates(apt_update=True):
|
||||||
# See if we have this information cached recently.
|
# See if we have this information cached recently.
|
||||||
@@ -755,6 +807,20 @@ def list_apt_updates(apt_update=True):
|
|||||||
|
|
||||||
return pkgs
|
return pkgs
|
||||||
|
|
||||||
|
def what_version_is_this(env):
|
||||||
|
# This function runs `git describe` on the Mail-in-a-Box installation directory.
|
||||||
|
# Git may not be installed and Mail-in-a-Box may not have been cloned from github,
|
||||||
|
# so this function may raise all sorts of exceptions.
|
||||||
|
miab_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
tag = shell("check_output", ["/usr/bin/git", "describe"], env={"GIT_DIR": os.path.join(miab_dir, '.git')}).strip()
|
||||||
|
return tag
|
||||||
|
|
||||||
|
def get_latest_miab_version():
|
||||||
|
# This pings https://mailinabox.email/bootstrap.sh and extracts the tag named in
|
||||||
|
# the script to determine the current product version.
|
||||||
|
import urllib.request
|
||||||
|
return re.search(b'TAG=(.*)', urllib.request.urlopen("https://mailinabox.email/bootstrap.sh?ping=1").read()).group(1).decode("utf8")
|
||||||
|
|
||||||
def run_and_output_changes(env, pool, send_via_email):
|
def run_and_output_changes(env, pool, send_via_email):
|
||||||
import json
|
import json
|
||||||
from difflib import SequenceMatcher
|
from difflib import SequenceMatcher
|
||||||
@@ -935,3 +1001,6 @@ if __name__ == "__main__":
|
|||||||
if cert_status != "OK":
|
if cert_status != "OK":
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|
||||||
|
elif sys.argv[1] == "--version":
|
||||||
|
print(what_version_is_this(env))
|
||||||
|
|||||||
@@ -98,9 +98,10 @@
|
|||||||
<li><a href="#ssl" onclick="return show_panel(this);">SSL Certificates</a></li>
|
<li><a href="#ssl" onclick="return show_panel(this);">SSL Certificates</a></li>
|
||||||
<li><a href="#system_backup" onclick="return show_panel(this);">Backup Status</a></li>
|
<li><a href="#system_backup" onclick="return show_panel(this);">Backup Status</a></li>
|
||||||
<li class="divider"></li>
|
<li class="divider"></li>
|
||||||
<li class="dropdown-header">Advanced Options</li>
|
<li class="dropdown-header">Advanced Pages</li>
|
||||||
<li><a href="#custom_dns" onclick="return show_panel(this);">Custom DNS</a></li>
|
<li><a href="#custom_dns" onclick="return show_panel(this);">Custom DNS</a></li>
|
||||||
<li><a href="#external_dns" onclick="return show_panel(this);">External DNS</a></li>
|
<li><a href="#external_dns" onclick="return show_panel(this);">External DNS</a></li>
|
||||||
|
<li><a href="/admin/munin">Munin Monitoring</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<li class="dropdown">
|
<li class="dropdown">
|
||||||
@@ -113,6 +114,7 @@
|
|||||||
</li>
|
</li>
|
||||||
<li><a href="#sync_guide" onclick="return show_panel(this);">Contacts/Calendar</a></li>
|
<li><a href="#sync_guide" onclick="return show_panel(this);">Contacts/Calendar</a></li>
|
||||||
<li><a href="#web" onclick="return show_panel(this);">Web</a></li>
|
<li><a href="#web" onclick="return show_panel(this);">Web</a></li>
|
||||||
|
<li><a href="#version" onclick="return show_panel(this);">Version</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
<ul class="nav navbar-nav navbar-right">
|
<ul class="nav navbar-nav navbar-right">
|
||||||
<li><a href="#" onclick="do_logout(); return false;" style="color: white">Log out?</a></li>
|
<li><a href="#" onclick="do_logout(); return false;" style="color: white">Log out?</a></li>
|
||||||
@@ -166,6 +168,10 @@
|
|||||||
{% include "ssl.html" %}
|
{% include "ssl.html" %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="panel_version" class="admin_panel">
|
||||||
|
{% include "version.html" %}
|
||||||
|
</div>
|
||||||
|
|
||||||
<hr>
|
<hr>
|
||||||
|
|
||||||
<footer>
|
<footer>
|
||||||
|
|||||||
@@ -60,7 +60,7 @@
|
|||||||
<p>Your box using a technique called greylisting to cut down on spam. Greylisting works by delaying mail from people you haven’t received mail from before for up to about 10 minutes. The vast majority of spam gets tricked by this. If you are waiting for an email from someone new, such as if you are registering on a new website and are waiting for an email confirmation, please give it up to 10-15 minutes to arrive.</p>
|
<p>Your box using a technique called greylisting to cut down on spam. Greylisting works by delaying mail from people you haven’t received mail from before for up to about 10 minutes. The vast majority of spam gets tricked by this. If you are waiting for an email from someone new, such as if you are registering on a new website and are waiting for an email confirmation, please give it up to 10-15 minutes to arrive.</p>
|
||||||
|
|
||||||
<h4>+tag addresses</h4>
|
<h4>+tag addresses</h4>
|
||||||
<p>Every incoming email address also receives mail for <code>+tag</code> addresses. If your email address is <code>you@yourdomain.com</code>, you can also accept mail at <code>you+anythinghere@yourdomain.com</code>. Use this as a fast way to create aliases or to segment incoming mail for your own filtering rules.</p>
|
<p>Every incoming email address also receives mail for <code>+tag</code> addresses. If your email address is <code>you@yourdomain.com</code>, you’ll also automatically get mail sent to <code>you+anythinghere@yourdomain.com</code>. Use this as a fast way to segment incoming mail for your own filtering rules without having to create aliases in this control panel.</p>
|
||||||
|
|
||||||
<h4>Use only this box to send as you</h4>
|
<h4>Use only this box to send as you</h4>
|
||||||
<p>Your box sets strict email sending policies for your domain names to make it harder for spam and other fraudulent mail to claim to be you. Only this machine is authorized to send email on behalf of your domain names. If you use any other service to send email as you, it will likely get spam filtered by recipients.</p>
|
<p>Your box sets strict email sending policies for your domain names to make it harder for spam and other fraudulent mail to claim to be you. Only this machine is authorized to send email on behalf of your domain names. If you use any other service to send email as you, it will likely get spam filtered by recipients.</p>
|
||||||
|
|||||||
@@ -164,9 +164,14 @@ function do_add_user() {
|
|||||||
|
|
||||||
function users_set_password(elem) {
|
function users_set_password(elem) {
|
||||||
var email = $(elem).parents('tr').attr('data-email');
|
var email = $(elem).parents('tr').attr('data-email');
|
||||||
|
|
||||||
|
var yourpw = "";
|
||||||
|
if (api_credentials != null && email == api_credentials[0])
|
||||||
|
yourpw = "<p class='text-danger'>If you change your own password, you will be logged out of this control panel and will need to log in again.</p>";
|
||||||
|
|
||||||
show_modal_confirm(
|
show_modal_confirm(
|
||||||
"Archive User",
|
"Archive User",
|
||||||
$("<p>Set a new password for <b>" + email + "</b>?</p> <p><label for='users_set_password_pw' style='display: block; font-weight: normal'>New Password:</label><input type='password' id='users_set_password_pw'></p><p><small>Passwords must be at least four characters and may not contain spaces.</small></p>"),
|
$("<p>Set a new password for <b>" + email + "</b>?</p> <p><label for='users_set_password_pw' style='display: block; font-weight: normal'>New Password:</label><input type='password' id='users_set_password_pw'></p><p><small>Passwords must be at least four characters and may not contain spaces.</small>" + yourpw + "</p>"),
|
||||||
"Set Password",
|
"Set Password",
|
||||||
function() {
|
function() {
|
||||||
api(
|
api(
|
||||||
|
|||||||
36
management/templates/version.html
Normal file
36
management/templates/version.html
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
<style>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<h2>Mail-in-a-Box Version</h2>
|
||||||
|
|
||||||
|
<p>You are running Mail-in-a-Box version <span id="miab-version" style="font-weight: bold">...</span>.</p>
|
||||||
|
|
||||||
|
<p>The latest version of Mail-in-a-Box is <button id="miab-get-latest-upstream" onclick="check_latest_version()">Check</button>.</p>
|
||||||
|
|
||||||
|
<p>To find the latest version and for upgrade instructions, see <a href="https://mailinabox.email/">https://mailinabox.email/</a>, <a href="https://github.com/mail-in-a-box/mailinabox/blob/master/CHANGELOG.md">release notes</a>, and <a href="https://mailinabox.email/maintenance.html#updating-mail-in-a-box">upgrade instructions</a>.</p>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function show_version() {
|
||||||
|
$('#miab-version').text('loading...');
|
||||||
|
api(
|
||||||
|
"/system/version",
|
||||||
|
"GET",
|
||||||
|
{
|
||||||
|
},
|
||||||
|
function(version) {
|
||||||
|
$('#miab-version').text(version);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function check_latest_version() {
|
||||||
|
$('#miab-get-latest-upstream').text('loading...');
|
||||||
|
api(
|
||||||
|
"/system/latest-upstream-version",
|
||||||
|
"POST",
|
||||||
|
{
|
||||||
|
},
|
||||||
|
function(version) {
|
||||||
|
$('#miab-get-latest-upstream').text(version);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
import os, os.path, shutil, re, tempfile, rtyaml
|
import os, os.path, shutil, re, tempfile, rtyaml
|
||||||
|
|
||||||
from mailconfig import get_mail_domains
|
from mailconfig import get_mail_domains
|
||||||
from dns_update import get_custom_dns_config, do_dns_update
|
from dns_update import get_custom_dns_config, do_dns_update, get_dns_zones
|
||||||
from utils import shell, safe_domain_name, sort_domains
|
from utils import shell, safe_domain_name, sort_domains
|
||||||
|
|
||||||
def get_web_domains(env):
|
def get_web_domains(env):
|
||||||
@@ -19,31 +19,71 @@ def get_web_domains(env):
|
|||||||
# Also serve web for all mail domains so that we might at least
|
# Also serve web for all mail domains so that we might at least
|
||||||
# provide auto-discover of email settings, and also a static website
|
# provide auto-discover of email settings, and also a static website
|
||||||
# if the user wants to make one. These will require an SSL cert.
|
# if the user wants to make one. These will require an SSL cert.
|
||||||
domains |= get_mail_domains(env)
|
|
||||||
|
|
||||||
# ...Unless the domain has an A/AAAA record that maps it to a different
|
# ...Unless the domain has an A/AAAA record that maps it to a different
|
||||||
# IP address than this box. Remove those domains from our list.
|
# IP address than this box. Remove those domains from our list.
|
||||||
dns = get_custom_dns_config(env)
|
domains |= (get_mail_domains(env) - get_domains_with_a_records(env))
|
||||||
for domain, rtype, value in dns:
|
|
||||||
if domain not in domains: continue
|
|
||||||
if rtype == "CNAME" or (rtype in ("A", "AAAA") and value != "local"):
|
|
||||||
domains.remove(domain)
|
|
||||||
|
|
||||||
# Sort the list. Put PRIMARY_HOSTNAME first so it becomes the
|
# Sort the list so the nginx conf gets written in a stable order.
|
||||||
# default server (nginx's default_server).
|
|
||||||
domains = sort_domains(domains, env)
|
domains = sort_domains(domains, env)
|
||||||
|
|
||||||
return domains
|
return domains
|
||||||
|
|
||||||
|
def get_domains_with_a_records(env):
|
||||||
|
domains = set()
|
||||||
|
dns = get_custom_dns_config(env)
|
||||||
|
for domain, rtype, value in dns:
|
||||||
|
if rtype == "CNAME" or (rtype in ("A", "AAAA") and value != "local"):
|
||||||
|
domains.add(domain)
|
||||||
|
return domains
|
||||||
|
|
||||||
|
def get_web_domains_with_root_overrides(env):
|
||||||
|
# Load custom settings so we can tell what domains have a redirect or proxy set up on '/',
|
||||||
|
# which means static hosting is not happening.
|
||||||
|
root_overrides = { }
|
||||||
|
nginx_conf_custom_fn = os.path.join(env["STORAGE_ROOT"], "www/custom.yaml")
|
||||||
|
if os.path.exists(nginx_conf_custom_fn):
|
||||||
|
custom_settings = rtyaml.load(open(nginx_conf_custom_fn))
|
||||||
|
for domain, settings in custom_settings.items():
|
||||||
|
for type, value in [('redirect', settings.get('redirects', {}).get('/')),
|
||||||
|
('proxy', settings.get('proxies', {}).get('/'))]:
|
||||||
|
if value:
|
||||||
|
root_overrides[domain] = (type, value)
|
||||||
|
return root_overrides
|
||||||
|
|
||||||
|
|
||||||
|
def get_default_www_redirects(env):
|
||||||
|
# Returns a list of www subdomains that we want to provide default redirects
|
||||||
|
# for, i.e. any www's that aren't domains the user has actually configured
|
||||||
|
# to serve for real. Which would be unusual.
|
||||||
|
web_domains = set(get_web_domains(env))
|
||||||
|
www_domains = set('www.' + zone for zone, zonefile in get_dns_zones(env))
|
||||||
|
return sort_domains(www_domains - web_domains - get_domains_with_a_records(env), env)
|
||||||
|
|
||||||
def do_web_update(env):
|
def do_web_update(env):
|
||||||
# Build an nginx configuration file.
|
# Build an nginx configuration file.
|
||||||
nginx_conf = open(os.path.join(os.path.dirname(__file__), "../conf/nginx-top.conf")).read()
|
nginx_conf = open(os.path.join(os.path.dirname(__file__), "../conf/nginx-top.conf")).read()
|
||||||
|
|
||||||
# Add configuration for each web domain.
|
# Load the templates.
|
||||||
template1 = open(os.path.join(os.path.dirname(__file__), "../conf/nginx.conf")).read()
|
template0 = open(os.path.join(os.path.dirname(__file__), "../conf/nginx.conf")).read()
|
||||||
|
template1 = open(os.path.join(os.path.dirname(__file__), "../conf/nginx-alldomains.conf")).read()
|
||||||
template2 = open(os.path.join(os.path.dirname(__file__), "../conf/nginx-primaryonly.conf")).read()
|
template2 = open(os.path.join(os.path.dirname(__file__), "../conf/nginx-primaryonly.conf")).read()
|
||||||
|
template3 = "\trewrite ^(.*) https://$REDIRECT_DOMAIN$1 permanent;\n"
|
||||||
|
|
||||||
|
# Add the PRIMARY_HOST configuration first so it becomes nginx's default server.
|
||||||
|
nginx_conf += make_domain_config(env['PRIMARY_HOSTNAME'], [template0, template1, template2], env)
|
||||||
|
|
||||||
|
# Add configuration all other web domains.
|
||||||
|
has_root_proxy_or_redirect = get_web_domains_with_root_overrides(env)
|
||||||
for domain in get_web_domains(env):
|
for domain in get_web_domains(env):
|
||||||
nginx_conf += make_domain_config(domain, template1, template2, env)
|
if domain == env['PRIMARY_HOSTNAME']: continue # handled above
|
||||||
|
if domain not in has_root_proxy_or_redirect:
|
||||||
|
nginx_conf += make_domain_config(domain, [template0, template1], env)
|
||||||
|
else:
|
||||||
|
nginx_conf += make_domain_config(domain, [template0], env)
|
||||||
|
|
||||||
|
# Add default www redirects.
|
||||||
|
for domain in get_default_www_redirects(env):
|
||||||
|
nginx_conf += make_domain_config(domain, [template0, template3], env)
|
||||||
|
|
||||||
# Did the file change? If not, don't bother writing & restarting nginx.
|
# Did the file change? If not, don't bother writing & restarting nginx.
|
||||||
nginx_conf_fn = "/etc/nginx/conf.d/local.conf"
|
nginx_conf_fn = "/etc/nginx/conf.d/local.conf"
|
||||||
@@ -64,11 +104,10 @@ def do_web_update(env):
|
|||||||
|
|
||||||
return "web updated\n"
|
return "web updated\n"
|
||||||
|
|
||||||
def make_domain_config(domain, template, template_for_primaryhost, env):
|
def make_domain_config(domain, templates, env):
|
||||||
# How will we configure this domain.
|
# GET SOME VARIABLES
|
||||||
|
|
||||||
# Where will its root directory be for static files?
|
# Where will its root directory be for static files?
|
||||||
|
|
||||||
root = get_web_root(domain, env)
|
root = get_web_root(domain, env)
|
||||||
|
|
||||||
# What private key and SSL certificate will we use for this domain?
|
# What private key and SSL certificate will we use for this domain?
|
||||||
@@ -78,18 +117,9 @@ def make_domain_config(domain, template, template_for_primaryhost, env):
|
|||||||
# available. Make a self-signed one now if one doesn't exist.
|
# available. Make a self-signed one now if one doesn't exist.
|
||||||
ensure_ssl_certificate_exists(domain, ssl_key, ssl_certificate, env)
|
ensure_ssl_certificate_exists(domain, ssl_key, ssl_certificate, env)
|
||||||
|
|
||||||
# Put pieces together.
|
# ADDITIONAL DIRECTIVES.
|
||||||
nginx_conf_parts = re.split("\s*# ADDITIONAL DIRECTIVES HERE\s*", template)
|
|
||||||
nginx_conf = nginx_conf_parts[0] + "\n"
|
|
||||||
if domain == env['PRIMARY_HOSTNAME']:
|
|
||||||
nginx_conf += template_for_primaryhost + "\n"
|
|
||||||
|
|
||||||
# Replace substitution strings in the template & return.
|
nginx_conf_extra = ""
|
||||||
nginx_conf = nginx_conf.replace("$STORAGE_ROOT", env['STORAGE_ROOT'])
|
|
||||||
nginx_conf = nginx_conf.replace("$HOSTNAME", domain)
|
|
||||||
nginx_conf = nginx_conf.replace("$ROOT", root)
|
|
||||||
nginx_conf = nginx_conf.replace("$SSL_KEY", ssl_key)
|
|
||||||
nginx_conf = nginx_conf.replace("$SSL_CERTIFICATE", ssl_certificate)
|
|
||||||
|
|
||||||
# Because the certificate may change, we should recognize this so we
|
# Because the certificate may change, we should recognize this so we
|
||||||
# can trigger an nginx update.
|
# can trigger an nginx update.
|
||||||
@@ -102,7 +132,7 @@ def make_domain_config(domain, template, template_for_primaryhost, env):
|
|||||||
finally:
|
finally:
|
||||||
f.close()
|
f.close()
|
||||||
return sha1.hexdigest()
|
return sha1.hexdigest()
|
||||||
nginx_conf += "# ssl files sha1: %s / %s\n" % (hashfile(ssl_key), hashfile(ssl_certificate))
|
nginx_conf_extra += "# ssl files sha1: %s / %s\n" % (hashfile(ssl_key), hashfile(ssl_certificate))
|
||||||
|
|
||||||
# Add in any user customizations in YAML format.
|
# Add in any user customizations in YAML format.
|
||||||
nginx_conf_custom_fn = os.path.join(env["STORAGE_ROOT"], "www/custom.yaml")
|
nginx_conf_custom_fn = os.path.join(env["STORAGE_ROOT"], "www/custom.yaml")
|
||||||
@@ -111,17 +141,29 @@ def make_domain_config(domain, template, template_for_primaryhost, env):
|
|||||||
if domain in yaml:
|
if domain in yaml:
|
||||||
yaml = yaml[domain]
|
yaml = yaml[domain]
|
||||||
for path, url in yaml.get("proxies", {}).items():
|
for path, url in yaml.get("proxies", {}).items():
|
||||||
nginx_conf += "\tlocation %s {\n\t\tproxy_pass %s;\n\t}\n" % (path, url)
|
nginx_conf_extra += "\tlocation %s {\n\t\tproxy_pass %s;\n\t}\n" % (path, url)
|
||||||
for path, url in yaml.get("redirects", {}).items():
|
for path, url in yaml.get("redirects", {}).items():
|
||||||
nginx_conf += "\trewrite %s %s permanent;\n" % (path, url)
|
nginx_conf_extra += "\trewrite %s %s permanent;\n" % (path, url)
|
||||||
|
|
||||||
# Add in any user customizations in the includes/ folder.
|
# Add in any user customizations in the includes/ folder.
|
||||||
nginx_conf_custom_include = os.path.join(env["STORAGE_ROOT"], "www", safe_domain_name(domain) + ".conf")
|
nginx_conf_custom_include = os.path.join(env["STORAGE_ROOT"], "www", safe_domain_name(domain) + ".conf")
|
||||||
if os.path.exists(nginx_conf_custom_include):
|
if os.path.exists(nginx_conf_custom_include):
|
||||||
nginx_conf += "\tinclude %s;\n" % (nginx_conf_custom_include)
|
nginx_conf_extra += "\tinclude %s;\n" % (nginx_conf_custom_include)
|
||||||
|
# PUT IT ALL TOGETHER
|
||||||
|
|
||||||
# Ending.
|
# Combine the pieces. Iteratively place each template into the "# ADDITIONAL DIRECTIVES HERE" placeholder
|
||||||
nginx_conf += nginx_conf_parts[1]
|
# of the previous template.
|
||||||
|
nginx_conf = "# ADDITIONAL DIRECTIVES HERE\n"
|
||||||
|
for t in templates + [nginx_conf_extra]:
|
||||||
|
nginx_conf = re.sub("[ \t]*# ADDITIONAL DIRECTIVES HERE *\n", t, nginx_conf)
|
||||||
|
|
||||||
|
# Replace substitution strings in the template & return.
|
||||||
|
nginx_conf = nginx_conf.replace("$STORAGE_ROOT", env['STORAGE_ROOT'])
|
||||||
|
nginx_conf = nginx_conf.replace("$HOSTNAME", domain)
|
||||||
|
nginx_conf = nginx_conf.replace("$ROOT", root)
|
||||||
|
nginx_conf = nginx_conf.replace("$SSL_KEY", ssl_key)
|
||||||
|
nginx_conf = nginx_conf.replace("$SSL_CERTIFICATE", ssl_certificate)
|
||||||
|
nginx_conf = nginx_conf.replace("$REDIRECT_DOMAIN", re.sub(r"^www\.", "", domain)) # for default www redirects to parent domain
|
||||||
|
|
||||||
return nginx_conf
|
return nginx_conf
|
||||||
|
|
||||||
@@ -159,14 +201,14 @@ def get_domain_ssl_files(domain, env, allow_shared_cert=True):
|
|||||||
# the user has uploaded a different private key for this domain.
|
# the user has uploaded a different private key for this domain.
|
||||||
if not ssl_key_is_alt and allow_shared_cert:
|
if not ssl_key_is_alt and allow_shared_cert:
|
||||||
from status_checks import check_certificate
|
from status_checks import check_certificate
|
||||||
if check_certificate(domain, ssl_certificate_primary, None)[0] == "OK":
|
if check_certificate(domain, ssl_certificate_primary, None, just_check_domain=True)[0] == "OK":
|
||||||
ssl_certificate = ssl_certificate_primary
|
ssl_certificate = ssl_certificate_primary
|
||||||
ssl_via = "Using multi/wildcard certificate of %s." % env['PRIMARY_HOSTNAME']
|
ssl_via = "Using multi/wildcard certificate of %s." % env['PRIMARY_HOSTNAME']
|
||||||
|
|
||||||
# For a 'www.' domain, see if we can reuse the cert of the parent.
|
# For a 'www.' domain, see if we can reuse the cert of the parent.
|
||||||
elif domain.startswith('www.'):
|
elif domain.startswith('www.'):
|
||||||
ssl_certificate_parent = os.path.join(env["STORAGE_ROOT"], 'ssl/%s/ssl_certificate.pem' % safe_domain_name(domain[4:]))
|
ssl_certificate_parent = os.path.join(env["STORAGE_ROOT"], 'ssl/%s/ssl_certificate.pem' % safe_domain_name(domain[4:]))
|
||||||
if os.path.exists(ssl_certificate_parent) and check_certificate(domain, ssl_certificate_parent, None)[0] == "OK":
|
if os.path.exists(ssl_certificate_parent) and check_certificate(domain, ssl_certificate_parent, None, just_check_domain=True)[0] == "OK":
|
||||||
ssl_certificate = ssl_certificate_parent
|
ssl_certificate = ssl_certificate_parent
|
||||||
ssl_via = "Using multi/wildcard certificate of %s." % domain[4:]
|
ssl_via = "Using multi/wildcard certificate of %s." % domain[4:]
|
||||||
|
|
||||||
@@ -212,7 +254,7 @@ def create_csr(domain, ssl_key, env):
|
|||||||
"-subj", "/C=%s/ST=/L=/O=/CN=%s" % (env["CSR_COUNTRY"], domain)])
|
"-subj", "/C=%s/ST=/L=/O=/CN=%s" % (env["CSR_COUNTRY"], domain)])
|
||||||
|
|
||||||
def install_cert(domain, ssl_cert, ssl_chain, env):
|
def install_cert(domain, ssl_cert, ssl_chain, env):
|
||||||
if domain not in get_web_domains(env):
|
if domain not in get_web_domains(env) + get_default_www_redirects(env):
|
||||||
return "Invalid domain name."
|
return "Invalid domain name."
|
||||||
|
|
||||||
# Write the combined cert+chain to a temporary path and validate that it is OK.
|
# Write the combined cert+chain to a temporary path and validate that it is OK.
|
||||||
@@ -255,14 +297,7 @@ def install_cert(domain, ssl_cert, ssl_chain, env):
|
|||||||
return "\n".join(ret)
|
return "\n".join(ret)
|
||||||
|
|
||||||
def get_web_domains_info(env):
|
def get_web_domains_info(env):
|
||||||
# load custom settings so we can tell what domains have a redirect or proxy set up on '/',
|
has_root_proxy_or_redirect = get_web_domains_with_root_overrides(env)
|
||||||
# which means static hosting is not happening
|
|
||||||
custom_settings = { }
|
|
||||||
nginx_conf_custom_fn = os.path.join(env["STORAGE_ROOT"], "www/custom.yaml")
|
|
||||||
if os.path.exists(nginx_conf_custom_fn):
|
|
||||||
custom_settings = rtyaml.load(open(nginx_conf_custom_fn))
|
|
||||||
def has_root_proxy_or_redirect(domain):
|
|
||||||
return custom_settings.get(domain, {}).get('redirects', {}).get('/') or custom_settings.get(domain, {}).get('proxies', {}).get('/')
|
|
||||||
|
|
||||||
# for the SSL config panel, get cert status
|
# for the SSL config panel, get cert status
|
||||||
def check_cert(domain):
|
def check_cert(domain):
|
||||||
@@ -288,7 +323,15 @@ def get_web_domains_info(env):
|
|||||||
"root": get_web_root(domain, env),
|
"root": get_web_root(domain, env),
|
||||||
"custom_root": get_web_root(domain, env, test_exists=False),
|
"custom_root": get_web_root(domain, env, test_exists=False),
|
||||||
"ssl_certificate": check_cert(domain),
|
"ssl_certificate": check_cert(domain),
|
||||||
"static_enabled": not has_root_proxy_or_redirect(domain),
|
"static_enabled": domain not in has_root_proxy_or_redirect,
|
||||||
}
|
}
|
||||||
for domain in get_web_domains(env)
|
for domain in get_web_domains(env)
|
||||||
|
] + \
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"domain": domain,
|
||||||
|
"ssl_certificate": check_cert(domain),
|
||||||
|
"static_enabled": False,
|
||||||
|
}
|
||||||
|
for domain in get_default_www_redirects(env)
|
||||||
]
|
]
|
||||||
|
|||||||
28
ppa/Makefile
28
ppa/Makefile
@@ -1,27 +1,35 @@
|
|||||||
|
POSTGREY_VERSION=1.35-1+miab1
|
||||||
|
DOVECOT_VERSION=2.2.9-1ubuntu2.1+miab1
|
||||||
|
|
||||||
all: clean build_postgrey build_dovecot_lucene
|
all: clean build_postgrey build_dovecot_lucene
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
# Clean.
|
# Clean.
|
||||||
rm -rf /tmp/build
|
rm -rf /tmp/build
|
||||||
|
|
||||||
# Prepare to build source packages.
|
|
||||||
mkdir -p /tmp/build
|
mkdir -p /tmp/build
|
||||||
|
|
||||||
build_postgrey:
|
build_postgrey: clean
|
||||||
# Download our fork of the Debian postgrey package.
|
# Download the latest Debian postgrey package. It is ahead of Ubuntu,
|
||||||
git clone https://github.com/mail-in-a-box/postgrey /tmp/build/postgrey
|
# and we might as well jump ahead.
|
||||||
|
git clone git://git.debian.org/git/collab-maint/postgrey.git /tmp/build/postgrey
|
||||||
|
|
||||||
# Download the corresponding upstream package.
|
# Download the corresponding upstream package.
|
||||||
wget -O /tmp/build/postgrey_1.35.orig.tar.gz http://postgrey.schweikert.ch/pub/postgrey-1.35.tar.gz
|
wget -O /tmp/build/postgrey_1.35.orig.tar.gz http://postgrey.schweikert.ch/pub/postgrey-1.35.tar.gz
|
||||||
|
|
||||||
|
# Add our source patch to the debian packaging listing.
|
||||||
|
cp postgrey_sources.diff /tmp/build/postgrey/debian/patches/mailinabox
|
||||||
|
|
||||||
|
# Patch the packaging to give it a new version.
|
||||||
|
patch -p1 -d /tmp/build/postgrey < postgrey.diff
|
||||||
|
|
||||||
# Build the source package.
|
# Build the source package.
|
||||||
(cd /tmp/build/postgrey; dpkg-buildpackage -S -us -uc -nc)
|
(cd /tmp/build/postgrey; dpkg-buildpackage -S -us -uc -nc)
|
||||||
|
|
||||||
# Sign the packages.
|
# Sign the packages.
|
||||||
debsign /tmp/build/postgrey_1.35-1miab1_source.changes
|
debsign /tmp/build/postgrey_$(POSTGREY_VERSION)_source.changes
|
||||||
|
|
||||||
# Upload to PPA.
|
# Upload to PPA.
|
||||||
dput ppa:mail-in-a-box/ppa /tmp/build/postgrey_1.35-1miab1_source.changes
|
dput ppa:mail-in-a-box/ppa /tmp/build/postgrey_$(POSTGREY_VERSION)_source.changes
|
||||||
|
|
||||||
# Clear the intermediate files.
|
# Clear the intermediate files.
|
||||||
rm -rf /tmp/build/postgrey
|
rm -rf /tmp/build/postgrey
|
||||||
@@ -30,7 +38,7 @@ build_postgrey:
|
|||||||
#sudo apt-get build-dep -y postgrey
|
#sudo apt-get build-dep -y postgrey
|
||||||
#(cd /tmp/build/postgrey; dpkg-buildpackage -us -uc -nc)
|
#(cd /tmp/build/postgrey; dpkg-buildpackage -us -uc -nc)
|
||||||
|
|
||||||
build_dovecot_lucene:
|
build_dovecot_lucene: clean
|
||||||
# Get the upstream source.
|
# Get the upstream source.
|
||||||
(cd /tmp/build; apt-get source dovecot)
|
(cd /tmp/build; apt-get source dovecot)
|
||||||
|
|
||||||
@@ -41,10 +49,10 @@ build_dovecot_lucene:
|
|||||||
(cd /tmp/build/dovecot-2.2.9; dpkg-buildpackage -S -us -uc -nc)
|
(cd /tmp/build/dovecot-2.2.9; dpkg-buildpackage -S -us -uc -nc)
|
||||||
|
|
||||||
# Sign the packages.
|
# Sign the packages.
|
||||||
#debsign /tmp/build/dovecot_2.2.9-1ubuntu2.1_amd64.changes
|
debsign /tmp/build/dovecot_$(DOVECOT_VERSION)_source.changes
|
||||||
|
|
||||||
# Upload it.
|
# Upload it.
|
||||||
#dput ppa:mail-in-a-box/ppa /tmp/build/dovecot_2.2.9-1ubuntu2.1_amd64.changes
|
dput ppa:mail-in-a-box/ppa /tmp/build/dovecot_$(DOVECOT_VERSION)_source.changes
|
||||||
|
|
||||||
# TESTING BINARY PACKAGE
|
# TESTING BINARY PACKAGE
|
||||||
# Install build dependencies and build dependencies we've added in our patch,
|
# Install build dependencies and build dependencies we've added in our patch,
|
||||||
|
|||||||
@@ -6,7 +6,9 @@ Mail-in-a-Box maintains a Launchpad.net PPA ([Mail-in-a-Box PPA](https://launchp
|
|||||||
Packages
|
Packages
|
||||||
--------
|
--------
|
||||||
|
|
||||||
* [postgrey](https://github.com/mail-in-a-box/postgrey), with a modification to whitelist senders that are whitelisted by [dnswl.org](https://www.dnswl.org/) (i.e. don't greylist mail from them).
|
* postgrey, a fork of [postgrey](http://postgrey.schweikert.ch/) based on the [latest Debian package](http://git.debian.org/?p=collab-maint/postgrey.git), with a modification to whitelist senders that are whitelisted by [dnswl.org](https://www.dnswl.org/) (i.e. don't greylist mail from known good senders).
|
||||||
|
|
||||||
|
* dovecot-lucene, [dovecot's lucene full text search plugin](http://wiki2.dovecot.org/Plugins/FTS/Lucene), which isn't built by Ubuntu's dovecot package maintainer unfortunately.
|
||||||
|
|
||||||
Building
|
Building
|
||||||
--------
|
--------
|
||||||
@@ -30,9 +32,9 @@ To build:
|
|||||||
# Build & upload to launchpad.
|
# Build & upload to launchpad.
|
||||||
vagrant ssh -- "cd /vagrant && make"
|
vagrant ssh -- "cd /vagrant && make"
|
||||||
|
|
||||||
To use on a Mail-in-a-Box box, add the PPA and then upgrade packages:
|
Mail-in-a-Box adds our PPA during setup, but if you need to do that yourself for testing:
|
||||||
|
|
||||||
apt-add-repository ppa:mail-in-a-box/ppa
|
apt-add-repository ppa:mail-in-a-box/ppa
|
||||||
apt-get update
|
apt-get update
|
||||||
apt-get upgrade
|
apt-get install postgrey dovecot-lucene
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
--- a/debian/control
|
--- a/debian/control
|
||||||
+++ b/debian/control
|
+++ b/debian/control
|
||||||
@@ -1,210 +1,22 @@
|
@@ -1,210 +1,23 @@
|
||||||
Source: dovecot
|
Source: dovecot
|
||||||
Section: mail
|
Section: mail
|
||||||
Priority: optional
|
Priority: optional
|
||||||
@@ -13,11 +13,14 @@
|
|||||||
+Build-Depends: debhelper (>= 7.2.3~), dpkg-dev (>= 1.16.1), pkg-config, libssl-dev, libpam0g-dev, libldap2-dev, libpq-dev, libmysqlclient-dev, libsqlite3-dev, libsasl2-dev, zlib1g-dev, libkrb5-dev, drac-dev (>= 1.12-5), libbz2-dev, libdb-dev, libcurl4-gnutls-dev, libexpat-dev, libwrap0-dev, dh-systemd, po-debconf, lsb-release, libclucene-dev (>= 2.3), liblzma-dev, libexttextcat-dev, libstemmer-dev, hardening-wrapper, dh-autoreconf, autotools-dev
|
+Build-Depends: debhelper (>= 7.2.3~), dpkg-dev (>= 1.16.1), pkg-config, libssl-dev, libpam0g-dev, libldap2-dev, libpq-dev, libmysqlclient-dev, libsqlite3-dev, libsasl2-dev, zlib1g-dev, libkrb5-dev, drac-dev (>= 1.12-5), libbz2-dev, libdb-dev, libcurl4-gnutls-dev, libexpat-dev, libwrap0-dev, dh-systemd, po-debconf, lsb-release, libclucene-dev (>= 2.3), liblzma-dev, libexttextcat-dev, libstemmer-dev, hardening-wrapper, dh-autoreconf, autotools-dev
|
||||||
Standards-Version: 3.9.4
|
Standards-Version: 3.9.4
|
||||||
Homepage: http://dovecot.org/
|
Homepage: http://dovecot.org/
|
||||||
Vcs-Git: git://git.debian.org/git/collab-maint/dovecot.git
|
-Vcs-Git: git://git.debian.org/git/collab-maint/dovecot.git
|
||||||
Vcs-Browser: http://git.debian.org/?p=collab-maint/dovecot.git
|
-Vcs-Browser: http://git.debian.org/?p=collab-maint/dovecot.git
|
||||||
|
+Vcs-Git: https://github.com/mail-in-a-box/mailinabox
|
||||||
|
+Vcs-Browser: https://github.com/mail-in-a-box/mailinabox
|
||||||
|
|
||||||
-Package: dovecot-core
|
-Package: dovecot-core
|
||||||
-Architecture: any
|
+Package: dovecot-lucene
|
||||||
|
Architecture: any
|
||||||
-Depends: ${shlibs:Depends}, ${misc:Depends}, libpam-runtime (>= 0.76-13.1), openssl, adduser, ucf (>= 2.0020), ssl-cert (>= 1.0-11ubuntu1), lsb-base (>= 3.2-12ubuntu3)
|
-Depends: ${shlibs:Depends}, ${misc:Depends}, libpam-runtime (>= 0.76-13.1), openssl, adduser, ucf (>= 2.0020), ssl-cert (>= 1.0-11ubuntu1), lsb-base (>= 3.2-12ubuntu3)
|
||||||
-Suggests: ntp, dovecot-gssapi, dovecot-sieve, dovecot-pgsql, dovecot-mysql, dovecot-sqlite, dovecot-ldap, dovecot-imapd, dovecot-pop3d, dovecot-lmtpd, dovecot-managesieved, dovecot-solr, ufw
|
-Suggests: ntp, dovecot-gssapi, dovecot-sieve, dovecot-pgsql, dovecot-mysql, dovecot-sqlite, dovecot-ldap, dovecot-imapd, dovecot-pop3d, dovecot-lmtpd, dovecot-managesieved, dovecot-solr, ufw
|
||||||
-Recommends: ntpdate
|
-Recommends: ntpdate
|
||||||
@@ -25,12 +28,14 @@
|
|||||||
-Replaces: dovecot-common (<< 1:2.0.14-2~), mailavenger (<< 0.8.1-4)
|
-Replaces: dovecot-common (<< 1:2.0.14-2~), mailavenger (<< 0.8.1-4)
|
||||||
-Breaks: dovecot-common (<< 1:2.0.14-2~), mailavenger (<< 0.8.1-4)
|
-Breaks: dovecot-common (<< 1:2.0.14-2~), mailavenger (<< 0.8.1-4)
|
||||||
-Description: secure POP3/IMAP server - core files
|
-Description: secure POP3/IMAP server - core files
|
||||||
- Dovecot is a mail server whose major goals are security and extreme
|
+Depends: ${shlibs:Depends}, ${misc:Depends}, dovecot-core (>= 1:2.2.9-1ubuntu2.1)
|
||||||
- reliability. It tries very hard to handle all error conditions and verify
|
+Description: secure POP3/IMAP server - Lucene support
|
||||||
- that all data is valid, making it nearly impossible to crash. It supports
|
Dovecot is a mail server whose major goals are security and extreme
|
||||||
- mbox/Maildir and its own dbox/mdbox formats, and should also be pretty
|
reliability. It tries very hard to handle all error conditions and verify
|
||||||
- fast, extensible, and portable.
|
that all data is valid, making it nearly impossible to crash. It supports
|
||||||
- .
|
mbox/Maildir and its own dbox/mdbox formats, and should also be pretty
|
||||||
|
fast, extensible, and portable.
|
||||||
|
.
|
||||||
- This package contains the Dovecot main server and its command line utility.
|
- This package contains the Dovecot main server and its command line utility.
|
||||||
-
|
-
|
||||||
-Package: dovecot-dev
|
-Package: dovecot-dev
|
||||||
@@ -151,17 +156,15 @@
|
|||||||
- This package provides LDAP support for Dovecot.
|
- This package provides LDAP support for Dovecot.
|
||||||
-
|
-
|
||||||
-Package: dovecot-gssapi
|
-Package: dovecot-gssapi
|
||||||
+Package: dovecot-lucene
|
-Architecture: any
|
||||||
Architecture: any
|
-Depends: ${shlibs:Depends}, ${misc:Depends}, dovecot-core (= ${binary:Version})
|
||||||
Depends: ${shlibs:Depends}, ${misc:Depends}, dovecot-core (= ${binary:Version})
|
|
||||||
-Description: secure POP3/IMAP server - GSSAPI support
|
-Description: secure POP3/IMAP server - GSSAPI support
|
||||||
+Description: secure POP3/IMAP server - Lucene support
|
- Dovecot is a mail server whose major goals are security and extreme
|
||||||
Dovecot is a mail server whose major goals are security and extreme
|
- reliability. It tries very hard to handle all error conditions and verify
|
||||||
reliability. It tries very hard to handle all error conditions and verify
|
- that all data is valid, making it nearly impossible to crash. It supports
|
||||||
that all data is valid, making it nearly impossible to crash. It supports
|
- mbox/Maildir and its own dbox/mdbox formats, and should also be pretty
|
||||||
mbox/Maildir and its own dbox/mdbox formats, and should also be pretty
|
- fast, extensible, and portable.
|
||||||
fast, extensible, and portable.
|
- .
|
||||||
.
|
|
||||||
- This package provides GSSAPI authentication support for Dovecot.
|
- This package provides GSSAPI authentication support for Dovecot.
|
||||||
-
|
-
|
||||||
-Package: dovecot-sieve
|
-Package: dovecot-sieve
|
||||||
@@ -216,7 +219,9 @@
|
|||||||
- This package contains configuration files for dovecot.
|
- This package contains configuration files for dovecot.
|
||||||
- .
|
- .
|
||||||
- This package modifies postfix's configuration to integrate with dovecot
|
- This package modifies postfix's configuration to integrate with dovecot
|
||||||
+ This package provides Lucene full text search support for Dovecot.
|
+ This package provides Lucene full text search support for Dovecot. It has been modified by Mail-in-a-Box
|
||||||
|
+ to supply a dovecot-lucene package compatible with the official ubuntu trusty dovecot-core.
|
||||||
|
|
||||||
diff --git a/debian/dovecot-lucene.links b/debian/dovecot-lucene.links
|
diff --git a/debian/dovecot-lucene.links b/debian/dovecot-lucene.links
|
||||||
new file mode 100644
|
new file mode 100644
|
||||||
index 0000000..6ffcbeb
|
index 0000000..6ffcbeb
|
||||||
@@ -247,11 +252,9 @@ index 0000000..3d933a5
|
|||||||
+++ b/debian/dovecot-lucene.triggers
|
+++ b/debian/dovecot-lucene.triggers
|
||||||
@@ -0,0 +1 @@
|
@@ -0,0 +1 @@
|
||||||
+activate register-dovecot-plugin
|
+activate register-dovecot-plugin
|
||||||
diff --git a/debian/rules b/debian/rules
|
|
||||||
index dcee2f6..9533a4a 100755
|
|
||||||
--- a/debian/rules
|
--- a/debian/rules
|
||||||
+++ b/debian/rules
|
+++ b/debian/rules
|
||||||
@@ -40,6 +40,7 @@ config-stamp: configure
|
@@ -40,6 +40,7 @@
|
||||||
--with-solr \
|
--with-solr \
|
||||||
--with-ioloop=best \
|
--with-ioloop=best \
|
||||||
--with-libwrap \
|
--with-libwrap \
|
||||||
@@ -259,18 +262,18 @@ index dcee2f6..9533a4a 100755
|
|||||||
--host=$(DEB_HOST_GNU_TYPE) \
|
--host=$(DEB_HOST_GNU_TYPE) \
|
||||||
--build=$(DEB_BUILD_GNU_TYPE) \
|
--build=$(DEB_BUILD_GNU_TYPE) \
|
||||||
--prefix=/usr \
|
--prefix=/usr \
|
||||||
@@ -95,6 +96,10 @@ install: build
|
@@ -95,6 +96,10 @@
|
||||||
dh_testroot
|
dh_testroot
|
||||||
dh_clean -k
|
dh_clean -k
|
||||||
dh_installdirs
|
dh_installdirs
|
||||||
+ mkdir -p $(CURDIR)/debian/dovecot-lucene/usr/lib/dovecot/modules
|
+ mkdir -p $(CURDIR)/debian/dovecot-lucene/usr/lib/dovecot/modules
|
||||||
+ mv $(CURDIR)/src/plugins/fts-lucene/.libs/* $(CURDIR)/debian/dovecot-lucene/usr/lib/dovecot/modules/
|
+ mv $(CURDIR)/src/plugins/fts-lucene/.libs/* $(CURDIR)/debian/dovecot-lucene/usr/lib/dovecot/modules/
|
||||||
+
|
+
|
||||||
+rest_disabled_by_miab:
|
+rest_disabled_by_miab:
|
||||||
$(MAKE) install DESTDIR=$(CURDIR)/debian/dovecot-core
|
$(MAKE) install DESTDIR=$(CURDIR)/debian/dovecot-core
|
||||||
$(MAKE) -C $(PIGEONHOLE_DIR) install DESTDIR=$(CURDIR)/debian/dovecot-core
|
$(MAKE) -C $(PIGEONHOLE_DIR) install DESTDIR=$(CURDIR)/debian/dovecot-core
|
||||||
rm `find $(CURDIR)/debian -name '*.la'`
|
rm `find $(CURDIR)/debian -name '*.la'`
|
||||||
@@ -209,7 +214,7 @@ binary-arch: build install
|
@@ -209,7 +214,7 @@
|
||||||
dh_installdocs -a
|
dh_installdocs -a
|
||||||
dh_installexamples -a
|
dh_installexamples -a
|
||||||
dh_installpam -a
|
dh_installpam -a
|
||||||
@@ -279,7 +282,7 @@ index dcee2f6..9533a4a 100755
|
|||||||
dh_systemd_enable
|
dh_systemd_enable
|
||||||
dh_installinit -pdovecot-core --name=dovecot
|
dh_installinit -pdovecot-core --name=dovecot
|
||||||
dh_systemd_start
|
dh_systemd_start
|
||||||
@@ -220,10 +225,10 @@ binary-arch: build install
|
@@ -220,10 +225,10 @@
|
||||||
dh_lintian -a
|
dh_lintian -a
|
||||||
dh_installchangelogs -a ChangeLog
|
dh_installchangelogs -a ChangeLog
|
||||||
dh_link -a
|
dh_link -a
|
||||||
@@ -292,3 +295,25 @@ index dcee2f6..9533a4a 100755
|
|||||||
dh_makeshlibs -a -n
|
dh_makeshlibs -a -n
|
||||||
dh_installdeb -a
|
dh_installdeb -a
|
||||||
dh_shlibdeps -a
|
dh_shlibdeps -a
|
||||||
|
--- a/debian/changelog
|
||||||
|
+++ a/debian/changelog
|
||||||
|
@@ -1,3 +1,9 @@
|
||||||
|
+dovecot (1:2.2.9-1ubuntu2.1+miab1) trusty; urgency=low
|
||||||
|
+
|
||||||
|
+ * Changed to just build dovecot-lucene for Mail-in-a-box PPA
|
||||||
|
+
|
||||||
|
+ -- Joshua Tauberer <jt@occams.info> Sat, 14 May 2015 16:13:00 -0400
|
||||||
|
+
|
||||||
|
dovecot (1:2.2.9-1ubuntu2.1) trusty-security; urgency=medium
|
||||||
|
|
||||||
|
* SECURITY UPDATE: denial of service via SSL connection exhaustion
|
||||||
|
--- a/debian/copyright 2014-03-07 07:26:37.000000000 -0500
|
||||||
|
+++ b/debian/copyright 2015-05-23 18:17:42.668005535 -0400
|
||||||
|
@@ -1,3 +1,7 @@
|
||||||
|
+This package is a fork by Mail-in-a-box (https://mailinabox.email). Original
|
||||||
|
+copyright statement follows:
|
||||||
|
+----------------------------------------------------------------------------
|
||||||
|
+
|
||||||
|
This package was debianized by Jaldhar H. Vyas <jaldhar@debian.org> on
|
||||||
|
Tue, 3 Dec 2002 01:10:07 -0500.
|
||||||
|
|
||||||
|
|||||||
80
ppa/postgrey.diff
Normal file
80
ppa/postgrey.diff
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
diff --git a/debian/NEWS b/debian/NEWS
|
||||||
|
index dd09744..de7b640 100644
|
||||||
|
--- a/debian/NEWS
|
||||||
|
+++ b/debian/NEWS
|
||||||
|
@@ -1,3 +1,9 @@
|
||||||
|
+postgrey (1.35-1+miab1)
|
||||||
|
+
|
||||||
|
+ Added DNSWL.org whitelisting.
|
||||||
|
+
|
||||||
|
+ -- Joshua Tauberer <jt@occams.info> Mon May 18 18:58:40 EDT 2015
|
||||||
|
+
|
||||||
|
postgrey (1.32-1) unstable; urgency=low
|
||||||
|
|
||||||
|
Postgrey is now listening to port 10023 and not 60000. The latter was an
|
||||||
|
diff --git a/debian/changelog b/debian/changelog
|
||||||
|
index 1058e15..e5e3557 100644
|
||||||
|
--- a/debian/changelog
|
||||||
|
+++ b/debian/changelog
|
||||||
|
@@ -1,3 +1,9 @@
|
||||||
|
+postgrey (1.35-1+miab1) trusty; urgency=low
|
||||||
|
+
|
||||||
|
+ * Added DNSWL.org whitelisting.
|
||||||
|
+
|
||||||
|
+ -- Joshua Tauberer <jt@occams.info> Mon, 18 May 2015 21:58:40 +0000
|
||||||
|
+
|
||||||
|
postgrey (1.35-1) unstable; urgency=low
|
||||||
|
|
||||||
|
* New upstream release (Closes: 756486)
|
||||||
|
diff --git a/debian/control b/debian/control
|
||||||
|
index ce12ba6..0a82855 100644
|
||||||
|
--- a/debian/control
|
||||||
|
+++ b/debian/control
|
||||||
|
@@ -1,14 +1,11 @@
|
||||||
|
Source: postgrey
|
||||||
|
Section: mail
|
||||||
|
Priority: optional
|
||||||
|
-Maintainer: Antonio Radici <antonio@debian.org>
|
||||||
|
-Uploaders: Jon Daley <jondaley-guest@alioth.debian.org>
|
||||||
|
+Maintainer: Joshua Tauberer <jt@occams.info>
|
||||||
|
Build-Depends: debhelper (>= 7), quilt
|
||||||
|
Build-Depends-Indep: po-debconf
|
||||||
|
Standards-Version: 3.9.6
|
||||||
|
Homepage: http://postgrey.schweikert.ch/
|
||||||
|
-Vcs-Browser: http://git.debian.org/?p=collab-maint/postgrey.git
|
||||||
|
-Vcs-Git: git://git.debian.org/git/collab-maint/postgrey.git
|
||||||
|
|
||||||
|
Package: postgrey
|
||||||
|
Architecture: all
|
||||||
|
@@ -25,3 +22,6 @@ Description: greylisting implementation for Postfix
|
||||||
|
.
|
||||||
|
While Postgrey is designed for use with Postfix, it can also be used
|
||||||
|
with Exim.
|
||||||
|
+ .
|
||||||
|
+ This version has been modified by Mail-in-a-Box to whitelist senders
|
||||||
|
+ in the DNSWL.org list. See https://mailinabox.email.
|
||||||
|
diff --git a/debian/copyright b/debian/copyright
|
||||||
|
index 3cbe377..bf09b89 100644
|
||||||
|
--- a/debian/copyright
|
||||||
|
+++ b/debian/copyright
|
||||||
|
@@ -1,6 +1,10 @@
|
||||||
|
+This package is a fork by Mail-in-a-Box (https://mailinabox.email). Original
|
||||||
|
+copyright statement follows:
|
||||||
|
+----------------------------------------------------------------------------
|
||||||
|
+
|
||||||
|
This Debian package was prepared by Adrian von Bidder <cmot@debian.org> in
|
||||||
|
July 2004, then the package was adopted by Antonio Radici <antonio@dyne.org>
|
||||||
|
-in Sept 2009
|
||||||
|
+in Sept 2009.
|
||||||
|
|
||||||
|
It was downloaded from http://postgrey.schweikert.ch/
|
||||||
|
|
||||||
|
diff --git a/debian/patches/series b/debian/patches/series
|
||||||
|
index f4c5e31..3cd62b8 100644
|
||||||
|
--- a/debian/patches/series
|
||||||
|
+++ b/debian/patches/series
|
||||||
|
@@ -1,3 +1,3 @@
|
||||||
|
imported-upstream-diff
|
||||||
|
disable-transaction-logic
|
||||||
|
-
|
||||||
|
+mailinabox
|
||||||
100
ppa/postgrey_sources.diff
Normal file
100
ppa/postgrey_sources.diff
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
Description: whitelist whatever dnswl.org whitelists
|
||||||
|
.
|
||||||
|
postgrey (1.35-1+miab1) unstable; urgency=low
|
||||||
|
.
|
||||||
|
* Added DNSWL.org whitelisting.
|
||||||
|
Author: Joshua Tauberer <jt@occams.info>
|
||||||
|
|
||||||
|
--- postgrey-1.35.orig/README
|
||||||
|
+++ postgrey-1.35/README
|
||||||
|
@@ -13,7 +13,7 @@ Requirements
|
||||||
|
- BerkeleyDB (Perl Module)
|
||||||
|
- Berkeley DB >= 4.1 (Library)
|
||||||
|
- Digest::SHA (Perl Module, only for --privacy option)
|
||||||
|
-
|
||||||
|
+- Net::DNS (Perl Module)
|
||||||
|
|
||||||
|
Documentation
|
||||||
|
-------------
|
||||||
|
--- postgrey-1.35.orig/postgrey
|
||||||
|
+++ postgrey-1.35/postgrey
|
||||||
|
@@ -18,6 +18,7 @@ use Fcntl ':flock'; # import LOCK_* cons
|
||||||
|
use Sys::Hostname;
|
||||||
|
use Sys::Syslog; # used only to find out which version we use
|
||||||
|
use POSIX qw(strftime setlocale LC_ALL);
|
||||||
|
+use Net::DNS; # for DNSWL.org whitelisting
|
||||||
|
|
||||||
|
use vars qw(@ISA);
|
||||||
|
@ISA = qw(Net::Server::Multiplex);
|
||||||
|
@@ -26,6 +27,8 @@ my $VERSION = '1.35';
|
||||||
|
my $DEFAULT_DBDIR = '/var/lib/postgrey';
|
||||||
|
my $CONFIG_DIR = '/etc/postgrey';
|
||||||
|
|
||||||
|
+my $dns_resolver = Net::DNS::Resolver->new;
|
||||||
|
+
|
||||||
|
sub cidr_parse($)
|
||||||
|
{
|
||||||
|
defined $_[0] or return undef;
|
||||||
|
@@ -48,6 +51,36 @@ sub cidr_match($$$)
|
||||||
|
return ($addr & $mask) == $net;
|
||||||
|
}
|
||||||
|
|
||||||
|
+sub reverseDottedQuad {
|
||||||
|
+ # This is the sub _chkValidPublicIP from Net::DNSBL by PJ Goodwin
|
||||||
|
+ # at http://www.the42.net/net-dnsbl.
|
||||||
|
+ my ($quad) = @_;
|
||||||
|
+ if ($quad =~ /^(\d+)\.(\d+)\.(\d+)\.(\d+)$/) {
|
||||||
|
+ my ($ip1,$ip2,$ip3,$ip4) = ($1, $2, $3, $4);
|
||||||
|
+ if (
|
||||||
|
+ $ip1 == 10 || #10.0.0.0/8 (10/8)
|
||||||
|
+ ($ip1 == 172 && $ip2 >= 16 && $ip2 <= 31) || #172.16.0.0/12 (172.16/12)
|
||||||
|
+ ($ip1 == 192 && $ip2 == 168) || #192.168.0.0/16 (192.168/16)
|
||||||
|
+ $quad eq '127.0.0.1' # localhost
|
||||||
|
+ ) {
|
||||||
|
+ # toss the RFC1918 specified privates
|
||||||
|
+ return undef;
|
||||||
|
+ } elsif (
|
||||||
|
+ ($ip1 <= 1 || $ip1 > 254) ||
|
||||||
|
+ ($ip2 < 0 || $ip2 > 255) ||
|
||||||
|
+ ($ip3 < 0 || $ip3 > 255) ||
|
||||||
|
+ ($ip4 < 0 || $ip4 > 255)
|
||||||
|
+ ) {
|
||||||
|
+ #invalid oct, toss it;
|
||||||
|
+ return undef;
|
||||||
|
+ }
|
||||||
|
+ my $revquad = $ip4 . "." . $ip3 . "." . $ip2 . "." . $ip1;
|
||||||
|
+ return $revquad;
|
||||||
|
+ } else { # invalid quad
|
||||||
|
+ return undef;
|
||||||
|
+ }
|
||||||
|
+}
|
||||||
|
+
|
||||||
|
sub read_clients_whitelists($)
|
||||||
|
{
|
||||||
|
my ($self) = @_;
|
||||||
|
@@ -361,6 +394,25 @@ sub smtpd_access_policy($$)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
+ # whitelist clients in dnswl.org
|
||||||
|
+ my $revip = reverseDottedQuad($attr->{client_address});
|
||||||
|
+ if ($revip) { # valid IP / plausibly in DNSWL
|
||||||
|
+ my $answer = $dns_resolver->send($revip . '.list.dnswl.org');
|
||||||
|
+ if ($answer && scalar($answer->answer) > 0) {
|
||||||
|
+ my @rrs = $answer->answer;
|
||||||
|
+ if ($rrs[0]->type eq 'A' && $rrs[0]->address ne '127.0.0.255') {
|
||||||
|
+ # Address appears in DNSWL. (127.0.0.255 means we were rate-limited.)
|
||||||
|
+ my $code = $rrs[0]->address;
|
||||||
|
+ if ($code =~ /^127.0.(\d+)\.([0-3])$/) {
|
||||||
|
+ my %dnswltrust = (0 => 'legitimate', 1 => 'occasional spam', 2 => 'rare spam', 3 => 'highly unlikely to send spam');
|
||||||
|
+ $code = $2 . '/' . $dnswltrust{$2};
|
||||||
|
+ }
|
||||||
|
+ $self->mylog_action($attr, 'pass', 'client whitelisted by dnswl.org (' . $code . ')');
|
||||||
|
+ return 'DUNNO';
|
||||||
|
+ }
|
||||||
|
+ }
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
# auto whitelist clients (see below for explanation)
|
||||||
|
my ($cawl_db, $cawl_key, $cawl_count, $cawl_last);
|
||||||
|
if($self->{postgrey}{awl_clients}) {
|
||||||
20
security.md
20
security.md
@@ -56,6 +56,8 @@ The cipher and protocol selection are chosen to support the following clients:
|
|||||||
|
|
||||||
The passwords for mail users are stored on disk using the [SHA512-CRYPT](http://man7.org/linux/man-pages/man3/crypt.3.html) hashing scheme. ([source](management/mailconfig.py))
|
The passwords for mail users are stored on disk using the [SHA512-CRYPT](http://man7.org/linux/man-pages/man3/crypt.3.html) hashing scheme. ([source](management/mailconfig.py))
|
||||||
|
|
||||||
|
When using the web-based administrative control panel, after logging in an API key is placed in the browser's local storage (rather than, say, the user's actual password). The API key is an HMAC based on the user's email address and current password, and it is keyed by a secret known only to the control panel service. By resetting an administrator's password, any HMACs previously generated for that user will expire.
|
||||||
|
|
||||||
### Console access
|
### Console access
|
||||||
|
|
||||||
Console access (e.g. via SSH) is configured by the system image used to create the box, typically from by a cloud virtual machine provider (e.g. Digital Ocean). Mail-in-a-Box does not set any console access settings, although it will warn the administrator in the System Status Checks if password-based login is turned on.
|
Console access (e.g. via SSH) is configured by the system image used to create the box, typically from by a cloud virtual machine provider (e.g. Digital Ocean). Mail-in-a-Box does not set any console access settings, although it will warn the administrator in the System Status Checks if password-based login is turned on.
|
||||||
@@ -67,17 +69,27 @@ If DNSSEC is enabled at the box's domain name's registrar, the SSHFP record that
|
|||||||
Outbound Mail
|
Outbound Mail
|
||||||
-------------
|
-------------
|
||||||
|
|
||||||
### Domain Policy Records
|
The basic protocols of email delivery did not plan for the presence of adversaries on the network. For a number of reasons it is not possible in most cases to guarantee that a connection to a recipient server is secure.
|
||||||
|
|
||||||
Domain policy records allow recipient MTAs to detect when the _domain_ part of incoming mail has been spoofed. All outbound mail is signed with [DKIM](https://en.wikipedia.org/wiki/DomainKeys_Identified_Mail) and "quarantine" [DMARC](https://en.wikipedia.org/wiki/DMARC) records are automatically set in DNS. Receiving MTAs that implement DMARC will automatically quarantine mail that is "From:" a domain hosted by the box but which was not sent by the box. (Strong [SPF](https://en.wikipedia.org/wiki/Sender_Policy_Framework) records are also automatically set in DNS.) ([source](management/dns_update.py))
|
### DNSSEC
|
||||||
|
|
||||||
|
The first step in resolving the destination server for an email address is performing a DNS look-up for the MX record of the domain name. The box uses a locally-running [DNSSEC](https://en.wikipedia.org/wiki/DNSSEC)-aware nameserver to perform the lookup. If the domain name has DNSSEC enabled, DNSSEC guards against DNS records being tampered with.
|
||||||
|
|
||||||
### Encryption
|
### Encryption
|
||||||
|
|
||||||
The basic protocols of email delivery did not plan for the need for encryption. For a number of reasons it is not possible in most cases to guarantee that a connection to a recipient server is secure. However, the box --- along with the vast majority of mail servers --- uses [opportunistic encryption](https://en.wikipedia.org/wiki/Opportunistic_encryption), meaning the mail is encrypted in transit and protected from passive eavesdropping, but it is not protected from an active man-in-the-middle attack. Modern encryption settings will be used to the extent the recipient server supports them. ([source](setup/mail-postfix.sh))
|
The box (along with the vast majority of mail servers) uses [opportunistic encryption](https://en.wikipedia.org/wiki/Opportunistic_encryption), meaning the mail is encrypted in transit and protected from passive eavesdropping, but it is not protected from an active man-in-the-middle attack. Modern encryption settings will be used to the extent the recipient server supports them. ([source](setup/mail-postfix.sh))
|
||||||
|
|
||||||
### DANE
|
### DANE
|
||||||
|
|
||||||
The box is [DNSSEC](https://en.wikipedia.org/wiki/DNSSEC)-aware (via a locally running DNSSEC-aware nameserver). When sending outbound mail, if the recipient's domain name supports DNSSEC and has published a [DANE TLSA](https://en.wikipedia.org/wiki/DNS-based_Authentication_of_Named_Entities) record, which contains a certificate fingerprint, the receiving MTA (server) must support TLS and its certificate must match the fingerprint. In other words, when a DANE TLSA record is published by the recipient, then on-the-wire encryption is forced between the box and the recipient MTA. ([source](setup/mail-postfix.sh))
|
If the recipient's domain name supports DNSSEC and has published a [DANE TLSA](https://en.wikipedia.org/wiki/DNS-based_Authentication_of_Named_Entities) record, then on-the-wire encryption is forced between the box and the recipient MTA and this encryption is not subject to a man-in-the-middle attack. The TLSA record contains a certificate fingerprint which the receiving MTA (server) must present to the box. ([source](setup/mail-postfix.sh))
|
||||||
|
|
||||||
|
### Domain Policy Records
|
||||||
|
|
||||||
|
Domain policy records allow recipient MTAs to detect when the _domain_ part of of the sender address in incoming mail has been spoofed. All outbound mail is signed with [DKIM](https://en.wikipedia.org/wiki/DomainKeys_Identified_Mail) and "quarantine" [DMARC](https://en.wikipedia.org/wiki/DMARC) records are automatically set in DNS. Receiving MTAs that implement DMARC will automatically quarantine mail that is "From:" a domain hosted by the box but which was not sent by the box. (Strong [SPF](https://en.wikipedia.org/wiki/Sender_Policy_Framework) records are also automatically set in DNS.) ([source](management/dns_update.py))
|
||||||
|
|
||||||
|
### User Policy
|
||||||
|
|
||||||
|
While domain policy records prevent other servers from sending mail with a "From:" header that matches a domain hosted on the box (see above), those policy records do not guarnatee that the user portion of the sender email address matches the actual sender. In enterprise environments where the box may host the mail of untrusted users, it is important to guard against users impersonating other users. The box restricts the envelope sender address that users may put into outbound mail to either a) their own email address (their SMTP login username) or b) any alias that they are listed as a direct recipient of. Note that the envelope sender address is not the same as the "From:" header.
|
||||||
|
|
||||||
Incoming Mail
|
Incoming Mail
|
||||||
-------------
|
-------------
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
#########################################################
|
#########################################################
|
||||||
|
|
||||||
if [ -z "$TAG" ]; then
|
if [ -z "$TAG" ]; then
|
||||||
TAG=v0.10
|
TAG=v0.12b
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Are we running as root?
|
# Are we running as root?
|
||||||
@@ -18,9 +18,12 @@ fi
|
|||||||
|
|
||||||
# Clone the Mail-in-a-Box repository if it doesn't exist.
|
# Clone the Mail-in-a-Box repository if it doesn't exist.
|
||||||
if [ ! -d $HOME/mailinabox ]; then
|
if [ ! -d $HOME/mailinabox ]; then
|
||||||
echo Installing git . . .
|
if [ ! -f /usr/bin/git ]; then
|
||||||
DEBIAN_FRONTEND=noninteractive apt-get -q -q install -y git < /dev/null
|
echo Installing git . . .
|
||||||
echo
|
apt-get -q -q update
|
||||||
|
DEBIAN_FRONTEND=noninteractive apt-get -q -q install -y git < /dev/null
|
||||||
|
echo
|
||||||
|
fi
|
||||||
|
|
||||||
echo Downloading Mail-in-a-Box $TAG. . .
|
echo Downloading Mail-in-a-Box $TAG. . .
|
||||||
git clone \
|
git clone \
|
||||||
|
|||||||
@@ -35,13 +35,18 @@ RequireSafeKeys false
|
|||||||
EOF
|
EOF
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Create a new DKIM key. This creates
|
# Create a new DKIM key. This creates mail.private and mail.txt
|
||||||
# mail.private and mail.txt in $STORAGE_ROOT/mail/dkim. The former
|
# in $STORAGE_ROOT/mail/dkim. The former is the private key and
|
||||||
# is the actual private key and the latter is the suggested DNS TXT
|
# the latter is the suggested DNS TXT entry which we'll include
|
||||||
# entry which we'll want to include in our DNS setup.
|
# in our DNS setup. Note tha the files are named after the
|
||||||
|
# 'selector' of the key, which we can change later on to support
|
||||||
|
# key rotation.
|
||||||
|
#
|
||||||
|
# A 1024-bit key is seen as a minimum standard by several providers
|
||||||
|
# such as Google. But they and others use a 2048 bit key, so we'll
|
||||||
|
# do the same. Keys beyond 2048 bits may exceed DNS record limits.
|
||||||
if [ ! -f "$STORAGE_ROOT/mail/dkim/mail.private" ]; then
|
if [ ! -f "$STORAGE_ROOT/mail/dkim/mail.private" ]; then
|
||||||
# Should we specify -h rsa-sha256?
|
opendkim-genkey -b 2048 -r -s mail -D $STORAGE_ROOT/mail/dkim
|
||||||
opendkim-genkey -r -s mail -D $STORAGE_ROOT/mail/dkim
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Ensure files are owned by the opendkim user and are private otherwise.
|
# Ensure files are owned by the opendkim user and are private otherwise.
|
||||||
|
|||||||
@@ -18,11 +18,14 @@
|
|||||||
source setup/functions.sh # load our functions
|
source setup/functions.sh # load our functions
|
||||||
source /etc/mailinabox.conf # load global vars
|
source /etc/mailinabox.conf # load global vars
|
||||||
|
|
||||||
# Install packages...
|
|
||||||
|
# Install packages for dovecot. These are all core dovecot plugins,
|
||||||
|
# but dovecot-lucene is packaged by *us* in the Mail-in-a-Box PPA,
|
||||||
|
# not by Ubuntu.
|
||||||
|
|
||||||
apt_install \
|
apt_install \
|
||||||
dovecot-core dovecot-imapd dovecot-pop3d dovecot-lmtpd dovecot-sqlite sqlite3 \
|
dovecot-core dovecot-imapd dovecot-pop3d dovecot-lmtpd dovecot-sqlite sqlite3 \
|
||||||
dovecot-sieve dovecot-managesieved
|
dovecot-sieve dovecot-managesieved dovecot-lucene
|
||||||
|
|
||||||
# The `dovecot-imapd`, `dovecot-pop3d`, and `dovecot-lmtpd` packages automatically
|
# The `dovecot-imapd`, `dovecot-pop3d`, and `dovecot-lmtpd` packages automatically
|
||||||
# enable IMAP, POP and LMTP protocols.
|
# enable IMAP, POP and LMTP protocols.
|
||||||
@@ -92,6 +95,17 @@ tools/editconf.py /etc/dovecot/conf.d/20-imap.conf \
|
|||||||
tools/editconf.py /etc/dovecot/conf.d/20-pop3.conf \
|
tools/editconf.py /etc/dovecot/conf.d/20-pop3.conf \
|
||||||
pop3_uidl_format="%08Xu%08Xv"
|
pop3_uidl_format="%08Xu%08Xv"
|
||||||
|
|
||||||
|
# Full Text Search - Enable full text search of mail using dovecot's lucene plugin,
|
||||||
|
# which *we* package and distribute (dovecot-lucene package).
|
||||||
|
tools/editconf.py /etc/dovecot/conf.d/10-mail.conf \
|
||||||
|
mail_plugins="\$mail_plugins fts fts_lucene"
|
||||||
|
cat > /etc/dovecot/conf.d/90-plugin-fts.conf << EOF;
|
||||||
|
plugin {
|
||||||
|
fts = lucene
|
||||||
|
fts_lucene = whitespace_chars=@.
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
# ### LDA (LMTP)
|
# ### LDA (LMTP)
|
||||||
|
|
||||||
# Enable Dovecot's LDA service with the LMTP protocol. It will listen
|
# Enable Dovecot's LDA service with the LMTP protocol. It will listen
|
||||||
|
|||||||
@@ -41,6 +41,13 @@ source /etc/mailinabox.conf # load global vars
|
|||||||
# always will.
|
# always will.
|
||||||
# * `ca-certificates`: A trust store used to squelch postfix warnings about
|
# * `ca-certificates`: A trust store used to squelch postfix warnings about
|
||||||
# untrusted opportunistically-encrypted connections.
|
# untrusted opportunistically-encrypted connections.
|
||||||
|
#
|
||||||
|
# postgrey is going to come in via the Mail-in-a-Box PPA, which publishes
|
||||||
|
# a modified version of postgrey that lets senders whitelisted by dnswl.org
|
||||||
|
# pass through without being greylisted. So please note [dnswl's license terms](https://www.dnswl.org/?page_id=9):
|
||||||
|
# > Every user with more than 100’000 queries per day on the public nameserver
|
||||||
|
# > infrastructure and every commercial vendor of dnswl.org data (eg through
|
||||||
|
# > anti-spam solutions) must register with dnswl.org and purchase a subscription.
|
||||||
|
|
||||||
apt_install postfix postfix-pcre postgrey ca-certificates
|
apt_install postfix postfix-pcre postgrey ca-certificates
|
||||||
|
|
||||||
@@ -153,6 +160,7 @@ tools/editconf.py /etc/postfix/main.cf virtual_transport=lmtp:[127.0.0.1]:10025
|
|||||||
#
|
#
|
||||||
# * `reject_non_fqdn_sender`: Reject not-nice-looking return paths.
|
# * `reject_non_fqdn_sender`: Reject not-nice-looking return paths.
|
||||||
# * `reject_unknown_sender_domain`: Reject return paths with invalid domains.
|
# * `reject_unknown_sender_domain`: Reject return paths with invalid domains.
|
||||||
|
# * `reject_authenticated_sender_login_mismatch`: Reject if mail FROM address does not match the client SASL login
|
||||||
# * `reject_rhsbl_sender`: Reject return paths that use blacklisted domains.
|
# * `reject_rhsbl_sender`: Reject return paths that use blacklisted domains.
|
||||||
# * `permit_sasl_authenticated`: Authenticated users (i.e. on port 587) can skip further checks.
|
# * `permit_sasl_authenticated`: Authenticated users (i.e. on port 587) can skip further checks.
|
||||||
# * `permit_mynetworks`: Mail that originates locally can skip further checks.
|
# * `permit_mynetworks`: Mail that originates locally can skip further checks.
|
||||||
@@ -166,7 +174,7 @@ tools/editconf.py /etc/postfix/main.cf virtual_transport=lmtp:[127.0.0.1]:10025
|
|||||||
# whitelisted) then postfix does a DEFER_IF_REJECT, which results in all "unknown user" sorts of messages turning into #NODOC
|
# whitelisted) then postfix does a DEFER_IF_REJECT, which results in all "unknown user" sorts of messages turning into #NODOC
|
||||||
# "450 4.7.1 Client host rejected: Service unavailable". This is a retry code, so the mail doesn't properly bounce. #NODOC
|
# "450 4.7.1 Client host rejected: Service unavailable". This is a retry code, so the mail doesn't properly bounce. #NODOC
|
||||||
tools/editconf.py /etc/postfix/main.cf \
|
tools/editconf.py /etc/postfix/main.cf \
|
||||||
smtpd_sender_restrictions="reject_non_fqdn_sender,reject_unknown_sender_domain,reject_rhsbl_sender dbl.spamhaus.org" \
|
smtpd_sender_restrictions="reject_non_fqdn_sender,reject_unknown_sender_domain,reject_authenticated_sender_login_mismatch,reject_rhsbl_sender dbl.spamhaus.org" \
|
||||||
smtpd_recipient_restrictions=permit_sasl_authenticated,permit_mynetworks,"reject_rbl_client zen.spamhaus.org",reject_unlisted_recipient,"check_policy_service inet:127.0.0.1:10023"
|
smtpd_recipient_restrictions=permit_sasl_authenticated,permit_mynetworks,"reject_rbl_client zen.spamhaus.org",reject_unlisted_recipient,"check_policy_service inet:127.0.0.1:10023"
|
||||||
|
|
||||||
# Postfix connects to Postgrey on the 127.0.0.1 interface specifically. Ensure that
|
# Postfix connects to Postgrey on the 127.0.0.1 interface specifically. Ensure that
|
||||||
|
|||||||
@@ -69,6 +69,22 @@ tools/editconf.py /etc/postfix/main.cf \
|
|||||||
smtpd_sasl_path=private/auth \
|
smtpd_sasl_path=private/auth \
|
||||||
smtpd_sasl_auth_enable=yes
|
smtpd_sasl_auth_enable=yes
|
||||||
|
|
||||||
|
# ### Sender Validation
|
||||||
|
|
||||||
|
# Use a Sqlite3 database to set login maps. This is used with
|
||||||
|
# reject_authenticated_sender_login_mismatch to see if user is
|
||||||
|
# allowed to send mail using FROM field specified in the request.
|
||||||
|
tools/editconf.py /etc/postfix/main.cf \
|
||||||
|
smtpd_sender_login_maps=sqlite:/etc/postfix/sender-login-maps.cf
|
||||||
|
|
||||||
|
# SQL statement to set login map which includes the case when user is
|
||||||
|
# sending email using a valid alias.
|
||||||
|
# This is the same as virtual-alias-maps.cf, See below
|
||||||
|
cat > /etc/postfix/sender-login-maps.cf << EOF;
|
||||||
|
dbpath=$db_path
|
||||||
|
query = SELECT destination from (SELECT destination, 0 as priority FROM aliases WHERE source='%s' UNION SELECT email as destination, 1 as priority FROM users WHERE email='%s') ORDER BY priority LIMIT 1;
|
||||||
|
EOF
|
||||||
|
|
||||||
# ### Destination Validation
|
# ### Destination Validation
|
||||||
|
|
||||||
# Use a Sqlite3 database to check whether a destination email address exists,
|
# Use a Sqlite3 database to check whether a destination email address exists,
|
||||||
@@ -92,13 +108,25 @@ query = SELECT 1 FROM users WHERE email='%s'
|
|||||||
EOF
|
EOF
|
||||||
|
|
||||||
# SQL statement to rewrite an email address if an alias is present.
|
# SQL statement to rewrite an email address if an alias is present.
|
||||||
# Aliases have precedence over users, but that's counter-intuitive for
|
#
|
||||||
# catch-all aliases ("@domain.com") which should *not* catch mail users.
|
# Postfix makes multiple queries for each incoming mail. It first
|
||||||
# To fix this, not only query the aliases table but also the users
|
# queries the whole email address, then just the user part in certain
|
||||||
# table, i.e. turn users into aliases from themselves to themselves.
|
# locally-directed cases (but we don't use this), then just `@`+the
|
||||||
|
# domain part. The first query that returns something wins. See
|
||||||
|
# http://www.postfix.org/virtual.5.html.
|
||||||
|
#
|
||||||
|
# virtual-alias-maps has precedence over virtual-mailbox-maps, but
|
||||||
|
# we don't want catch-alls and domain aliases to catch mail for users
|
||||||
|
# that have been defined on those domains. To fix this, we not only
|
||||||
|
# query the aliases table but also the users table when resolving
|
||||||
|
# aliases, i.e. we turn users into aliases from themselves to
|
||||||
|
# themselves. That means users will match in postfix's first query
|
||||||
|
# before postfix gets to the third query for catch-alls/domain alises.
|
||||||
|
#
|
||||||
# If there is both an alias and a user for the same address either
|
# If there is both an alias and a user for the same address either
|
||||||
# might be returned by the UNION, so the whole query is wrapped in
|
# might be returned by the UNION, so the whole query is wrapped in
|
||||||
# another select that prioritizes the alias definition.
|
# another select that prioritizes the alias definition to preserve
|
||||||
|
# postfix's preference for aliases for whole email addresses.
|
||||||
cat > /etc/postfix/virtual-alias-maps.cf << EOF;
|
cat > /etc/postfix/virtual-alias-maps.cf << EOF;
|
||||||
dbpath=$db_path
|
dbpath=$db_path
|
||||||
query = SELECT destination from (SELECT destination, 0 as priority FROM aliases WHERE source='%s' UNION SELECT email as destination, 1 as priority FROM users WHERE email='%s') ORDER BY priority LIMIT 1;
|
query = SELECT destination from (SELECT destination, 0 as priority FROM aliases WHERE source='%s' UNION SELECT email as destination, 1 as priority FROM users WHERE email='%s') ORDER BY priority LIMIT 1;
|
||||||
|
|||||||
@@ -2,8 +2,10 @@
|
|||||||
|
|
||||||
source setup/functions.sh
|
source setup/functions.sh
|
||||||
|
|
||||||
apt_install python3-flask links duplicity libyaml-dev python3-dnspython python3-dateutil
|
# build-essential libssl-dev libffi-dev python3-dev: Required to pip install cryptography.
|
||||||
hide_output pip3 install rtyaml "email_validator==0.1.0-rc5"
|
apt_install python3-flask links duplicity libyaml-dev python3-dnspython python3-dateutil \
|
||||||
|
build-essential libssl-dev libffi-dev python3-dev
|
||||||
|
hide_output pip3 install --upgrade rtyaml email_validator idna cryptography
|
||||||
# email_validator is repeated in setup/questions.sh
|
# email_validator is repeated in setup/questions.sh
|
||||||
|
|
||||||
# Create a backup directory and a random key for encrypting backups.
|
# Create a backup directory and a random key for encrypting backups.
|
||||||
@@ -42,8 +44,5 @@ EOF
|
|||||||
chmod +x /etc/cron.daily/mailinabox-statuschecks
|
chmod +x /etc/cron.daily/mailinabox-statuschecks
|
||||||
|
|
||||||
|
|
||||||
# Start it. Remove the api key file first so that start.sh
|
# Start it.
|
||||||
# can wait for it to be created to know that the management
|
|
||||||
# server is ready.
|
|
||||||
rm -f /var/lib/mailinabox/api.key
|
|
||||||
restart_service mailinabox
|
restart_service mailinabox
|
||||||
|
|||||||
@@ -95,6 +95,11 @@ def migration_7(env):
|
|||||||
# Save.
|
# Save.
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|
||||||
|
def migration_8(env):
|
||||||
|
# Delete DKIM keys. We had generated 1024-bit DKIM keys.
|
||||||
|
# By deleting the key file we'll automatically generate
|
||||||
|
# a new key, which will be 2048 bits.
|
||||||
|
os.unlink(os.path.join(env['STORAGE_ROOT'], 'mail/dkim/mail.private'))
|
||||||
|
|
||||||
def get_current_migration():
|
def get_current_migration():
|
||||||
ver = 0
|
ver = 0
|
||||||
|
|||||||
32
setup/munin.sh
Executable file
32
setup/munin.sh
Executable file
@@ -0,0 +1,32 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Munin: resource monitoring tool
|
||||||
|
#################################################
|
||||||
|
|
||||||
|
source setup/functions.sh # load our functions
|
||||||
|
source /etc/mailinabox.conf # load global vars
|
||||||
|
|
||||||
|
# install Munin
|
||||||
|
apt_install munin munin-node
|
||||||
|
|
||||||
|
# edit config
|
||||||
|
cat > /etc/munin/munin.conf <<EOF;
|
||||||
|
dbdir /var/lib/munin
|
||||||
|
htmldir /var/cache/munin/www
|
||||||
|
logdir /var/log/munin
|
||||||
|
rundir /var/run/munin
|
||||||
|
tmpldir /etc/munin/templates
|
||||||
|
|
||||||
|
includedir /etc/munin/munin-conf.d
|
||||||
|
|
||||||
|
# a simple host tree
|
||||||
|
[$PRIMARY_HOSTNAME]
|
||||||
|
address 127.0.0.1
|
||||||
|
|
||||||
|
# send alerts to the following address
|
||||||
|
contacts admin
|
||||||
|
contact.admin.command mail -s "Munin notification ${var:host}" administrator@$PRIMARY_HOSTNAME
|
||||||
|
contact.admin.always_send warning critical
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# generate initial statistics so the directory isn't empty
|
||||||
|
sudo -u munin munin-cron
|
||||||
@@ -15,25 +15,36 @@ apt_install \
|
|||||||
apt-get purge -qq -y owncloud*
|
apt-get purge -qq -y owncloud*
|
||||||
|
|
||||||
# Install ownCloud from source of this version:
|
# Install ownCloud from source of this version:
|
||||||
owncloud_ver=8.0.3
|
owncloud_ver=8.0.4
|
||||||
owncloud_hash=3192f3d783f81247eaf2914df63afdd593def4e5
|
owncloud_hash=625b1c561ea51426047a3e79eda51ca05e9f978a
|
||||||
|
|
||||||
|
# Migrate <= v0.10 setups that stored the ownCloud config.php in /usr/local rather than
|
||||||
|
# in STORAGE_ROOT. Move the file to STORAGE_ROOT.
|
||||||
|
if [ ! -f $STORAGE_ROOT/owncloud/config.php ] \
|
||||||
|
&& [ -f /usr/local/lib/owncloud/config/config.php ]; then
|
||||||
|
|
||||||
|
# Move config.php and symlink back into previous location.
|
||||||
|
echo "Migrating owncloud/config.php to new location."
|
||||||
|
mv /usr/local/lib/owncloud/config/config.php $STORAGE_ROOT/owncloud/config.php \
|
||||||
|
&& \
|
||||||
|
ln -sf $STORAGE_ROOT/owncloud/config.php /usr/local/lib/owncloud/config/config.php
|
||||||
|
fi
|
||||||
|
|
||||||
# Check if ownCloud dir exist, and check if version matches owncloud_ver (if either doesn't - install/upgrade)
|
# Check if ownCloud dir exist, and check if version matches owncloud_ver (if either doesn't - install/upgrade)
|
||||||
if [ ! -d /usr/local/lib/owncloud/ ] \
|
if [ ! -d /usr/local/lib/owncloud/ ] \
|
||||||
|| ! grep -q $owncloud_ver /usr/local/lib/owncloud/version.php; then
|
|| ! grep -q $owncloud_ver /usr/local/lib/owncloud/version.php; then
|
||||||
|
|
||||||
|
# Download and verify
|
||||||
|
echo "installing ownCloud..."
|
||||||
|
wget_verify https://download.owncloud.org/community/owncloud-$owncloud_ver.zip $owncloud_hash /tmp/owncloud.zip
|
||||||
|
|
||||||
# Clear out the existing ownCloud.
|
# Clear out the existing ownCloud.
|
||||||
rm -f /tmp/owncloud-config.php
|
if [ -d /usr/local/lib/owncloud/ ]; then
|
||||||
if [ ! -d /usr/local/lib/owncloud/ ]; then
|
|
||||||
echo installing ownCloud...
|
|
||||||
else
|
|
||||||
echo "upgrading ownCloud to $owncloud_ver (backing up existing ownCloud directory to /tmp/owncloud-backup-$$)..."
|
echo "upgrading ownCloud to $owncloud_ver (backing up existing ownCloud directory to /tmp/owncloud-backup-$$)..."
|
||||||
cp /usr/local/lib/owncloud/config/config.php /tmp/owncloud-config.php
|
|
||||||
mv /usr/local/lib/owncloud /tmp/owncloud-backup-$$
|
mv /usr/local/lib/owncloud /tmp/owncloud-backup-$$
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Download and extract ownCloud.
|
# Extract ownCloud
|
||||||
wget_verify https://download.owncloud.org/community/owncloud-$owncloud_ver.zip $owncloud_hash /tmp/owncloud.zip
|
|
||||||
unzip -u -o -q /tmp/owncloud.zip -d /usr/local/lib #either extracts new or replaces current files
|
unzip -u -o -q /tmp/owncloud.zip -d /usr/local/lib #either extracts new or replaces current files
|
||||||
rm -f /tmp/owncloud.zip
|
rm -f /tmp/owncloud.zip
|
||||||
|
|
||||||
@@ -46,10 +57,9 @@ if [ ! -d /usr/local/lib/owncloud/ ] \
|
|||||||
# Fix weird permissions.
|
# Fix weird permissions.
|
||||||
chmod 750 /usr/local/lib/owncloud/{apps,config}
|
chmod 750 /usr/local/lib/owncloud/{apps,config}
|
||||||
|
|
||||||
# Restore configuration file if we're doing an upgrade.
|
# Create a symlink to the config.php in STORAGE_ROOT (for upgrades we're restoring the symlink we previously
|
||||||
if [ -f /tmp/owncloud-config.php ]; then
|
# put in, and in new installs we're creating a symlink and will create the actual config later).
|
||||||
mv /tmp/owncloud-config.php /usr/local/lib/owncloud/config/config.php
|
ln -sf $STORAGE_ROOT/owncloud/config.php /usr/local/lib/owncloud/config/config.php
|
||||||
fi
|
|
||||||
|
|
||||||
# Make sure permissions are correct or the upgrade step won't run.
|
# Make sure permissions are correct or the upgrade step won't run.
|
||||||
# $STORAGE_ROOT/owncloud may not yet exist, so use -f to suppress
|
# $STORAGE_ROOT/owncloud may not yet exist, so use -f to suppress
|
||||||
@@ -65,17 +75,20 @@ fi
|
|||||||
# Setup ownCloud if the ownCloud database does not yet exist. Running setup when
|
# Setup ownCloud if the ownCloud database does not yet exist. Running setup when
|
||||||
# the database does exist wipes the database and user data.
|
# the database does exist wipes the database and user data.
|
||||||
if [ ! -f $STORAGE_ROOT/owncloud/owncloud.db ]; then
|
if [ ! -f $STORAGE_ROOT/owncloud/owncloud.db ]; then
|
||||||
|
# Create user data directory
|
||||||
|
mkdir -p $STORAGE_ROOT/owncloud
|
||||||
|
|
||||||
# Create a configuration file.
|
# Create a configuration file.
|
||||||
TIMEZONE=$(cat /etc/timezone)
|
TIMEZONE=$(cat /etc/timezone)
|
||||||
instanceid=oc$(echo $PRIMARY_HOSTNAME | sha1sum | fold -w 10 | head -n 1)
|
instanceid=oc$(echo $PRIMARY_HOSTNAME | sha1sum | fold -w 10 | head -n 1)
|
||||||
cat > /usr/local/lib/owncloud/config/config.php <<EOF;
|
cat > $STORAGE_ROOT/owncloud/config.php <<EOF;
|
||||||
<?php
|
<?php
|
||||||
\$CONFIG = array (
|
\$CONFIG = array (
|
||||||
'datadirectory' => '$STORAGE_ROOT/owncloud',
|
'datadirectory' => '$STORAGE_ROOT/owncloud',
|
||||||
|
|
||||||
'instanceid' => '$instanceid',
|
'instanceid' => '$instanceid',
|
||||||
|
|
||||||
'trusted_domains' =>
|
'trusted_domains' =>
|
||||||
array (
|
array (
|
||||||
0 => '$PRIMARY_HOSTNAME',
|
0 => '$PRIMARY_HOSTNAME',
|
||||||
),
|
),
|
||||||
@@ -125,12 +138,12 @@ EOF
|
|||||||
?>
|
?>
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
# Create user data directory and set permissions
|
# Set permissions
|
||||||
mkdir -p $STORAGE_ROOT/owncloud
|
|
||||||
chown -R www-data.www-data $STORAGE_ROOT/owncloud /usr/local/lib/owncloud
|
chown -R www-data.www-data $STORAGE_ROOT/owncloud /usr/local/lib/owncloud
|
||||||
|
|
||||||
# Execute ownCloud's setup step, which creates the ownCloud sqlite database.
|
# Execute ownCloud's setup step, which creates the ownCloud sqlite database.
|
||||||
# It also wipes it if it exists. And it deletes the autoconfig.php file.
|
# It also wipes it if it exists. And it updates config.php with database
|
||||||
|
# settings and deletes the autoconfig.php file.
|
||||||
(cd /usr/local/lib/owncloud; sudo -u www-data php /usr/local/lib/owncloud/index.php;)
|
(cd /usr/local/lib/owncloud; sudo -u www-data php /usr/local/lib/owncloud/index.php;)
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
@@ -6,11 +6,14 @@ if [ -z "$NONINTERACTIVE" ]; then
|
|||||||
# use a shell flag instead. Really supress any output from installing dialog.
|
# use a shell flag instead. Really supress any output from installing dialog.
|
||||||
#
|
#
|
||||||
# Also install depencies needed to validate the email address.
|
# Also install depencies needed to validate the email address.
|
||||||
echo Installing packages needed for setup...
|
if [ ! -f /usr/bin/dialog ] || [ ! -f /usr/bin/python3 ] || [ ! -f /usr/bin/pip3 ]; then
|
||||||
apt_get_quiet install dialog python3 python3-pip || exit 1
|
echo Installing packages needed for setup...
|
||||||
|
apt-get -q -q update
|
||||||
|
apt_get_quiet install dialog python3 python3-pip || exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
# email_validator is repeated in setup/management.sh
|
# email_validator is repeated in setup/management.sh
|
||||||
hide_output pip3 install "email_validator==0.1.0-rc5" || exit 1
|
hide_output pip3 install email_validator || exit 1
|
||||||
|
|
||||||
message_box "Mail-in-a-Box Installation" \
|
message_box "Mail-in-a-Box Installation" \
|
||||||
"Hello and thanks for deploying a Mail-in-a-Box!
|
"Hello and thanks for deploying a Mail-in-a-Box!
|
||||||
@@ -193,3 +196,48 @@ if [ -z "$CSR_COUNTRY" ]; then
|
|||||||
exit
|
exit
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Automatic configuration, e.g. as used in our Vagrant configuration.
|
||||||
|
if [ "$PUBLIC_IP" = "auto" ]; then
|
||||||
|
# Use a public API to get our public IP address, or fall back to local network configuration.
|
||||||
|
PUBLIC_IP=$(get_publicip_from_web_service 4 || get_default_privateip 4)
|
||||||
|
fi
|
||||||
|
if [ "$PUBLIC_IPV6" = "auto" ]; then
|
||||||
|
# Use a public API to get our public IPv6 address, or fall back to local network configuration.
|
||||||
|
PUBLIC_IPV6=$(get_publicip_from_web_service 6 || get_default_privateip 6)
|
||||||
|
fi
|
||||||
|
if [ "$PRIMARY_HOSTNAME" = "auto" ]; then
|
||||||
|
# Use reverse DNS to get this machine's hostname. Install bind9-host early.
|
||||||
|
hide_output apt-get -y install bind9-host
|
||||||
|
PRIMARY_HOSTNAME=$(get_default_hostname)
|
||||||
|
elif [ "$PRIMARY_HOSTNAME" = "auto-easy" ]; then
|
||||||
|
# Generate a probably-unique subdomain under our justtesting.email domain.
|
||||||
|
PRIMARY_HOSTNAME=`echo $PUBLIC_IP | sha1sum | cut -c1-5`.justtesting.email
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Set STORAGE_USER and STORAGE_ROOT to default values (user-data and /home/user-data), unless
|
||||||
|
# we've already got those values from a previous run.
|
||||||
|
if [ -z "$STORAGE_USER" ]; then
|
||||||
|
STORAGE_USER=$([[ -z "$DEFAULT_STORAGE_USER" ]] && echo "user-data" || echo "$DEFAULT_STORAGE_USER")
|
||||||
|
fi
|
||||||
|
if [ -z "$STORAGE_ROOT" ]; then
|
||||||
|
STORAGE_ROOT=$([[ -z "$DEFAULT_STORAGE_ROOT" ]] && echo "/home/$STORAGE_USER" || echo "$DEFAULT_STORAGE_ROOT")
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Show the configuration, since the user may have not entered it manually.
|
||||||
|
echo
|
||||||
|
echo "Primary Hostname: $PRIMARY_HOSTNAME"
|
||||||
|
echo "Public IP Address: $PUBLIC_IP"
|
||||||
|
if [ ! -z "$PUBLIC_IPV6" ]; then
|
||||||
|
echo "Public IPv6 Address: $PUBLIC_IPV6"
|
||||||
|
fi
|
||||||
|
if [ "$PRIVATE_IP" != "$PUBLIC_IP" ]; then
|
||||||
|
echo "Private IP Address: $PRIVATE_IP"
|
||||||
|
fi
|
||||||
|
if [ "$PRIVATE_IPV6" != "$PUBLIC_IPV6" ]; then
|
||||||
|
echo "Private IPv6 Address: $PRIVATE_IPV6"
|
||||||
|
fi
|
||||||
|
if [ -f /usr/bin/git ] && [ -d .git ]; then
|
||||||
|
echo "Mail-in-a-Box Version: " $(git describe)
|
||||||
|
fi
|
||||||
|
echo
|
||||||
|
|||||||
@@ -47,71 +47,26 @@ chmod +x /usr/local/bin/mailinabox
|
|||||||
|
|
||||||
# Ask the user for the PRIMARY_HOSTNAME, PUBLIC_IP, PUBLIC_IPV6, and CSR_COUNTRY
|
# Ask the user for the PRIMARY_HOSTNAME, PUBLIC_IP, PUBLIC_IPV6, and CSR_COUNTRY
|
||||||
# if values have not already been set in environment variables. When running
|
# if values have not already been set in environment variables. When running
|
||||||
# non-interactively, be sure to set values for all!
|
# non-interactively, be sure to set values for all! Also sets STORAGE_USER and
|
||||||
|
# STORAGE_ROOT.
|
||||||
source setup/questions.sh
|
source setup/questions.sh
|
||||||
|
|
||||||
# Automatic configuration, e.g. as used in our Vagrant configuration.
|
|
||||||
if [ "$PUBLIC_IP" = "auto" ]; then
|
|
||||||
# Use a public API to get our public IP address, or fall back to local network configuration.
|
|
||||||
PUBLIC_IP=$(get_publicip_from_web_service 4 || get_default_privateip 4)
|
|
||||||
fi
|
|
||||||
if [ "$PUBLIC_IPV6" = "auto" ]; then
|
|
||||||
# Use a public API to get our public IPv6 address, or fall back to local network configuration.
|
|
||||||
PUBLIC_IPV6=$(get_publicip_from_web_service 6 || get_default_privateip 6)
|
|
||||||
fi
|
|
||||||
if [ "$PRIMARY_HOSTNAME" = "auto-easy" ]; then
|
|
||||||
# Generate a probably-unique subdomain under our justtesting.email domain.
|
|
||||||
PRIMARY_HOSTNAME=`echo $PUBLIC_IP | sha1sum | cut -c1-5`.justtesting.email
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Show the configuration, since the user may have not entered it manually.
|
|
||||||
echo
|
|
||||||
echo "Primary Hostname: $PRIMARY_HOSTNAME"
|
|
||||||
echo "Public IP Address: $PUBLIC_IP"
|
|
||||||
if [ ! -z "$PUBLIC_IPV6" ]; then
|
|
||||||
echo "Public IPv6 Address: $PUBLIC_IPV6"
|
|
||||||
fi
|
|
||||||
if [ "$PRIVATE_IP" != "$PUBLIC_IP" ]; then
|
|
||||||
echo "Private IP Address: $PRIVATE_IP"
|
|
||||||
fi
|
|
||||||
if [ "$PRIVATE_IPV6" != "$PUBLIC_IPV6" ]; then
|
|
||||||
echo "Private IPv6 Address: $PRIVATE_IPV6"
|
|
||||||
fi
|
|
||||||
if [ -f .git ]; then
|
|
||||||
echo "Mail-in-a-Box Version: " $(git describe)
|
|
||||||
fi
|
|
||||||
echo
|
|
||||||
|
|
||||||
# Run some network checks to make sure setup on this machine makes sense.
|
# Run some network checks to make sure setup on this machine makes sense.
|
||||||
if [ -z "$SKIP_NETWORK_CHECKS" ]; then
|
if [ -z "$SKIP_NETWORK_CHECKS" ]; then
|
||||||
. setup/network-checks.sh
|
source setup/network-checks.sh
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# For the first time (if the config file (/etc/mailinabox.conf) not exists):
|
# Create the STORAGE_USER and STORAGE_ROOT directory if they don't already exist.
|
||||||
# Create the user named "user-data" and store all persistent user
|
# If the STORAGE_ROOT is missing the mailinabox.version file that lists a
|
||||||
# data (mailboxes, etc.) in that user's home directory.
|
# migration (schema) number for the files stored there, assume this is a fresh
|
||||||
#
|
# installation to that directory and write the file to contain the current
|
||||||
# If the config file exists:
|
# migration number for this version of Mail-in-a-Box.
|
||||||
# Apply the existing configuration options for STORAGE_USER/ROOT
|
|
||||||
if [ -z "$STORAGE_USER" ]; then
|
|
||||||
STORAGE_USER=$([[ -z "$DEFAULT_STORAGE_USER" ]] && echo "user-data" || echo "$DEFAULT_STORAGE_USER")
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -z "$STORAGE_ROOT" ]; then
|
|
||||||
STORAGE_ROOT=$([[ -z "$DEFAULT_STORAGE_ROOT" ]] && echo "/home/$STORAGE_USER" || echo "$DEFAULT_STORAGE_ROOT")
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Create the STORAGE_USER if it not exists
|
|
||||||
if ! id -u $STORAGE_USER >/dev/null 2>&1; then
|
if ! id -u $STORAGE_USER >/dev/null 2>&1; then
|
||||||
useradd -m $STORAGE_USER
|
useradd -m $STORAGE_USER
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Create the STORAGE_ROOT if it not exists
|
|
||||||
if [ ! -d $STORAGE_ROOT ]; then
|
if [ ! -d $STORAGE_ROOT ]; then
|
||||||
mkdir -p $STORAGE_ROOT
|
mkdir -p $STORAGE_ROOT
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Create mailinabox.version file if not exists
|
|
||||||
if [ ! -f $STORAGE_ROOT/mailinabox.version ]; then
|
if [ ! -f $STORAGE_ROOT/mailinabox.version ]; then
|
||||||
echo $(setup/migrate.py --current) > $STORAGE_ROOT/mailinabox.version
|
echo $(setup/migrate.py --current) > $STORAGE_ROOT/mailinabox.version
|
||||||
chown $STORAGE_USER.$STORAGE_USER $STORAGE_ROOT/mailinabox.version
|
chown $STORAGE_USER.$STORAGE_USER $STORAGE_ROOT/mailinabox.version
|
||||||
@@ -145,9 +100,11 @@ source setup/webmail.sh
|
|||||||
source setup/owncloud.sh
|
source setup/owncloud.sh
|
||||||
source setup/zpush.sh
|
source setup/zpush.sh
|
||||||
source setup/management.sh
|
source setup/management.sh
|
||||||
|
source setup/munin.sh
|
||||||
|
|
||||||
# Ping the management daemon to write the DNS and nginx configuration files.
|
# Ping the management daemon to write the DNS and nginx configuration files.
|
||||||
while [ ! -f /var/lib/mailinabox/api.key ]; do
|
until nc -z -w 4 localhost 10222
|
||||||
|
do
|
||||||
echo Waiting for the Mail-in-a-Box management daemon to start...
|
echo Waiting for the Mail-in-a-Box management daemon to start...
|
||||||
sleep 2
|
sleep 2
|
||||||
done
|
done
|
||||||
|
|||||||
@@ -3,7 +3,30 @@ source setup/functions.sh # load our functions
|
|||||||
# Basic System Configuration
|
# Basic System Configuration
|
||||||
# -------------------------
|
# -------------------------
|
||||||
|
|
||||||
# ### Install Packages
|
# ### Add Mail-in-a-Box's PPA.
|
||||||
|
|
||||||
|
# We've built several .deb packages on our own that we want to include.
|
||||||
|
# One is a replacement for Ubuntu's stock postgrey package that makes
|
||||||
|
# some enhancements. The other is dovecot-lucene, a Lucene-based full
|
||||||
|
# text search plugin for (and by) dovecot, which is not available in
|
||||||
|
# Ubuntu currently.
|
||||||
|
#
|
||||||
|
# Add that to the system's list of repositories using add-apt-repository.
|
||||||
|
# But add-apt-repository may not be installed. If it's not available,
|
||||||
|
# then install it. But we have to run apt-get update before we try to
|
||||||
|
# install anything so the package index is up to date. After adding the
|
||||||
|
# PPA, we have to run apt-get update *again* to load the PPA's index,
|
||||||
|
# so this must precede the apt-get update line below.
|
||||||
|
|
||||||
|
if [ ! -f /usr/bin/add-apt-repository ]; then
|
||||||
|
echo "Installing add-apt-repository..."
|
||||||
|
hide_output apt-get update
|
||||||
|
apt_install software-properties-common
|
||||||
|
fi
|
||||||
|
|
||||||
|
hide_output add-apt-repository -y ppa:mail-in-a-box/ppa
|
||||||
|
|
||||||
|
# ### Update Packages
|
||||||
|
|
||||||
# Update system packages to make sure we have the latest upstream versions of things from Ubuntu.
|
# Update system packages to make sure we have the latest upstream versions of things from Ubuntu.
|
||||||
|
|
||||||
@@ -11,6 +34,8 @@ echo Updating system packages...
|
|||||||
hide_output apt-get update
|
hide_output apt-get update
|
||||||
apt_get_quiet upgrade
|
apt_get_quiet upgrade
|
||||||
|
|
||||||
|
# ### Install System Packages
|
||||||
|
|
||||||
# Install basic utilities.
|
# Install basic utilities.
|
||||||
#
|
#
|
||||||
# * haveged: Provides extra entropy to /dev/random so it doesn't stall
|
# * haveged: Provides extra entropy to /dev/random so it doesn't stall
|
||||||
@@ -20,13 +45,14 @@ apt_get_quiet upgrade
|
|||||||
# * cron: Runs background processes periodically.
|
# * cron: Runs background processes periodically.
|
||||||
# * ntp: keeps the system time correct
|
# * ntp: keeps the system time correct
|
||||||
# * fail2ban: scans log files for repeated failed login attempts and blocks the remote IP at the firewall
|
# * fail2ban: scans log files for repeated failed login attempts and blocks the remote IP at the firewall
|
||||||
|
# * netcat-openbsd: `nc` command line networking tool
|
||||||
# * git: we install some things directly from github
|
# * git: we install some things directly from github
|
||||||
# * sudo: allows privileged users to execute commands as root without being root
|
# * sudo: allows privileged users to execute commands as root without being root
|
||||||
# * coreutils: includes `nproc` tool to report number of processors
|
# * coreutils: includes `nproc` tool to report number of processors
|
||||||
# * bc: allows us to do math to compute sane defaults
|
# * bc: allows us to do math to compute sane defaults
|
||||||
|
|
||||||
apt_install python3 python3-dev python3-pip \
|
apt_install python3 python3-dev python3-pip \
|
||||||
wget curl git sudo coreutils bc \
|
netcat-openbsd wget curl git sudo coreutils bc \
|
||||||
haveged unattended-upgrades cron ntp fail2ban
|
haveged unattended-upgrades cron ntp fail2ban
|
||||||
|
|
||||||
# Allow apt to install system updates automatically every day.
|
# Allow apt to install system updates automatically every day.
|
||||||
|
|||||||
@@ -32,6 +32,14 @@ sed "s#STORAGE_ROOT#$STORAGE_ROOT#" \
|
|||||||
tools/editconf.py /etc/nginx/nginx.conf -s \
|
tools/editconf.py /etc/nginx/nginx.conf -s \
|
||||||
server_names_hash_bucket_size="64;"
|
server_names_hash_bucket_size="64;"
|
||||||
|
|
||||||
|
# Tell PHP not to expose its version number in the X-Powered-By header.
|
||||||
|
tools/editconf.py /etc/php5/fpm/php.ini -c ';' \
|
||||||
|
expose_php=Off
|
||||||
|
|
||||||
|
# Set PHPs default charset to UTF-8, since we use it. See #367.
|
||||||
|
tools/editconf.py /etc/php5/fpm/php.ini -c ';' \
|
||||||
|
default_charset="UTF-8"
|
||||||
|
|
||||||
# Bump up PHP's max_children to support more concurrent connections
|
# Bump up PHP's max_children to support more concurrent connections
|
||||||
tools/editconf.py /etc/php5/fpm/pool.d/www.conf -c ';' \
|
tools/editconf.py /etc/php5/fpm/pool.d/www.conf -c ';' \
|
||||||
pm.max_children=8
|
pm.max_children=8
|
||||||
|
|||||||
@@ -32,10 +32,10 @@ apt-get purge -qq -y roundcube* #NODOC
|
|||||||
# Install Roundcube from source if it is not already present or if it is out of date.
|
# Install Roundcube from source if it is not already present or if it is out of date.
|
||||||
# Combine the Roundcube version number with the commit hash of vacation_sieve to track
|
# Combine the Roundcube version number with the commit hash of vacation_sieve to track
|
||||||
# whether we have the latest version.
|
# whether we have the latest version.
|
||||||
VERSION=1.1.1
|
VERSION=1.1.2
|
||||||
HASH=08222f382a8dd89bba7dbbad595f48443bec0aa2
|
HASH=df88deae691da3ecf3e9f0aee674c1f3042ea1eb
|
||||||
VACATION_SIEVE_VERSION=91ea6f52216390073d1f5b70b5f6bea0bfaee7e5
|
VACATION_SIEVE_VERSION=91ea6f52216390073d1f5b70b5f6bea0bfaee7e5
|
||||||
PERSISTENT_LOGIN_VERSION=9a0bc59493beb573d515f82aec443e2098365d11
|
PERSISTENT_LOGIN_VERSION=117fbd8f93b56b2bf72ad055193464803ef3bc36
|
||||||
UPDATE_KEY=$VERSION:$VACATION_SIEVE_VERSION:$PERSISTENT_LOGIN_VERSION
|
UPDATE_KEY=$VERSION:$VACATION_SIEVE_VERSION:$PERSISTENT_LOGIN_VERSION
|
||||||
needs_update=0 #NODOC
|
needs_update=0 #NODOC
|
||||||
if [ ! -f /usr/local/lib/roundcubemail/version ]; then
|
if [ ! -f /usr/local/lib/roundcubemail/version ]; then
|
||||||
|
|||||||
@@ -14,6 +14,8 @@
|
|||||||
#
|
#
|
||||||
# NAME VALUE
|
# NAME VALUE
|
||||||
#
|
#
|
||||||
|
# If the -c option is given, then the supplied character becomes the comment character
|
||||||
|
#
|
||||||
# If the -w option is given, then setting lines continue onto following
|
# If the -w option is given, then setting lines continue onto following
|
||||||
# lines while the lines start with whitespace, e.g.:
|
# lines while the lines start with whitespace, e.g.:
|
||||||
#
|
#
|
||||||
@@ -24,7 +26,7 @@ import sys, re
|
|||||||
|
|
||||||
# sanity check
|
# sanity check
|
||||||
if len(sys.argv) < 3:
|
if len(sys.argv) < 3:
|
||||||
print("usage: python3 editconf.py /etc/file.conf [-s] [-w] [-t] NAME=VAL [NAME=VAL ...]")
|
print("usage: python3 editconf.py /etc/file.conf [-s] [-w] [-c <CHARACTER>] [-t] NAME=VAL [NAME=VAL ...]")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
# parse command line arguments
|
# parse command line arguments
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ for fn in glob.glob("/var/log/nginx/access.log*"):
|
|||||||
with f:
|
with f:
|
||||||
for line in f:
|
for line in f:
|
||||||
# Find lines that are GETs on /bootstrap.sh by either curl or wget.
|
# Find lines that are GETs on /bootstrap.sh by either curl or wget.
|
||||||
|
# (Note that we purposely skip ...?ping=1 requests which is the admin panel querying us for updates.)
|
||||||
m = re.match(rb"(?P<ip>\S+) - - \[(?P<date>.*?)\] \"GET /bootstrap.sh HTTP/.*\" 200 \d+ .* \"(?:curl|wget)", line, re.I)
|
m = re.match(rb"(?P<ip>\S+) - - \[(?P<date>.*?)\] \"GET /bootstrap.sh HTTP/.*\" 200 \d+ .* \"(?:curl|wget)", line, re.I)
|
||||||
if m:
|
if m:
|
||||||
date, time = m.group("date").decode("ascii").split(":", 1)
|
date, time = m.group("date").decode("ascii").split(":", 1)
|
||||||
|
|||||||
Reference in New Issue
Block a user