1
0
mirror of https://github.com/mail-in-a-box/mailinabox.git synced 2026-03-12 17:07:23 +01:00

Compare commits

..

87 Commits
v57a ... v65

Author SHA1 Message Date
Joshua Tauberer
0314554207 Version 65 2023-10-27 06:02:22 -04:00
matidau
46d55f7866 Update zpush.sh to version 2.7.1 (#2315)
Updating to latest release, bugfixes no new features.
2023-10-26 09:04:13 -04:00
KiekerJan
2bbc317873 Update Roundcube to 1.6.4 (#2317) 2023-10-26 09:03:29 -04:00
clpo13
28f929dc13 Fix typo in system-backup.html: Amazone -> Amazon (#2311) 2023-10-10 13:22:19 -04:00
Joshua Tauberer
e419b62034 Version 64 2023-09-02 19:46:24 -04:00
Joshua Tauberer
a966913963 Fix command line arguments for duplicity 2.1 (#2301) 2023-09-02 15:54:16 -04:00
Joshua Tauberer
08defb12be Add a new backup.py command to print the duplicity command to the console to help debugging 2023-09-02 07:49:41 -04:00
Jeff Volkenant
7be687e601 Move source and target positional arguments to the end, required for Duplicity 2.1.0
(Modified by JT.)
2023-09-02 07:28:48 -04:00
Aaron Ten Clay
62efe985f1 Disable OpenDMARC sending reports (#2299)
OpenDMARC report messages, while potentially useful for peer operators of mail servers, are abusable and should not be enabled by default. This change prioritizes the safety of the Box's reputation.
2023-09-02 07:10:04 -04:00
Alex
df44056bae Fix checksums in nextcloud.sh (#2293) 2023-09-02 07:07:12 -04:00
Dmytro Kyrychuk
3148c621d2 Fix issue with slash (/) characters in B2 Application Key (#2281)
Urlencode B2 Application Key when saving configuration, urldecode it
back when reading. Duplicity accepts urlencoded target directly, no
decoding is necessary when backup is performed.

Resolve #1964
2023-09-02 07:03:24 -04:00
Michael Heuberger
81866de229 Amend --always option to all git describe commands (#2275) 2023-09-02 06:59:39 -04:00
matidau
674ce92e92 Fix z-push-admin broken in v60 (#2263)
Update zpush.sh to create two sbin bash scripts for z-push-admin and z-push-top using PHP_VER.
2023-09-02 06:55:15 -04:00
Darren Sanders
c034b0f789 Fix how the value is being passed for the gpg-options parameter
Duplicity v2.1.0 backups are failing with the error:
"... --gpg-options expected one argument".

The issue is that duplicity v2.1.0 began using the argparse Python
library and the parse_known_args function. This function
interprets the argument being passed, "--cipher-algo=AES256",
as an argument name (because of the leading '-') and not as an
argument value. Because of that it exits with an error and
reports that the --gpg-options arg is missing its value.

Adding an extra set of quotes around this string causes
parse_known_args to interpret the string as an argument
value.
2023-08-30 16:34:17 -07:00
Joshua Tauberer
cd45d08409 Version 63 2023-07-29 12:11:29 -04:00
Michael Heuberger
98628622c7 Bump Nextcloud to v25.0.7 (#2268)
Also
- bumps calendar and contacts apps
- adds extra migration steps between these versions
- adds cron job for Calendar updates
- rotates nextloud log file after upgrading
- adds primary key indices migrations
- adjusts configs slightly
- adds more well-known entries in nginx to improve service discovery
- reformats some comments (line-breaking)
2023-06-16 11:49:55 -04:00
Joshua Tauberer
8b19d15735 Version 62 2023-05-20 08:57:32 -04:00
matidau
93380b243f Update zpush.sh to version 2.7.0 (#2236) 2023-05-13 10:27:42 -04:00
Joshua Tauberer
fb0a3b0489 Restore Roundcube's password reset tool by removing PRAGMA journal_mode = WAL from Roundcube source (#2199) 2023-05-13 10:26:41 -04:00
Joshua Tauberer
3bc9d07aeb Roundcube 1.6.1 2023-05-13 07:00:54 -04:00
Joshua Tauberer
51ed030917 Allow setting the S3 region name in backup settings to pass to duplicity
It's stuffed inside the username portion of the target URL. We already mangle the target before passing it to duplicity so there wasn't a need for a new field.

Fixes the issue raised in #2200, #2216.
2023-05-13 07:00:29 -04:00
Joshua Tauberer
e828d63a85 Allow secondary DNS xfr: items to be hostnames that are resolved to IP addresses when generating the nsd configuration 2023-05-13 07:00:10 -04:00
Joshua Tauberer
0ee0784bde Changelog entries 2023-05-13 06:59:49 -04:00
Peter Tóth
6d43d24552 Improve control panel panel switching behaviour by using the URL fragment (#2252) 2023-05-13 06:49:34 -04:00
Peter Tóth
963fb9f2e6 email_administrator.py: fix report formatting (#2249) 2023-05-13 06:40:31 -04:00
KiekerJan
c9584148a0 Fix issue where sshkeygen fails when ipv6 is disabled (#2248) 2023-05-13 06:39:46 -04:00
Tomas P
9a33f9c5ff Fix dynazoom due to change in handling su (#2247)
Seems that in Ubuntu 22.04 the behavior in su changed, making - ( alias for -l, --login ) mutually exclusive with --preserve-environment which is required for passing enviroment variables for cgi to work for dynazoom in munin.dropping - fixes the issue
2023-05-13 06:38:00 -04:00
Michael Heuberger
95530affbf Bump Nextcloud to v23.0.12 and its apps (#2244) 2023-05-13 06:37:24 -04:00
Hugh Secker-Walker
f72be0be7c feat(rsync-backup-ui): Add a Copy button to put public key on clipboard in rsync UI (#2227) 2023-05-13 06:36:31 -04:00
KiekerJan
8aa98b25b5 Update configuration of Roundcube password plugin for Roundcube 1.6 2023-05-13 06:22:28 -04:00
KiekerJan
3c15081673 Remove journal PRAGMA from Roundcube source which broke the database for postfix
See #2185.
2023-05-13 06:20:13 -04:00
Joshua Tauberer
01d8e9f3b4 Revert "Disable Roundcube password plugin since it was corrupting the user database (#2198)"
This reverts commit 1587248762.

See subsequent commits.
2023-05-13 06:20:13 -04:00
Adam Elaoumari
88260bb610 Fixed year in changelog (#2241)
Fixed year of version 61.1 (2022 -> 2023)
2023-03-08 10:29:02 -05:00
Joshua Tauberer
6f94412204 v61.1 2023-01-28 11:25:21 -05:00
Joshua Tauberer
c77d1697a7 Revert "Improve error messages in the management tools when external command-line tools are run"
Command line arguments have user secrets in some cases which should not be included in error messages.

This reverts commit 26709a3c1d.

Reported by AK.
2023-01-28 11:24:38 -05:00
Hugh Secker-Walker
31bbef3401 chore(setup): Make sed fingerprint patterns in start.sh be case insensitive (#2201) 2023-01-28 11:12:40 -05:00
Hugh Secker-Walker
7af713592a feat(status page): Add summary of ok/error/warning counts (#2204)
* feat(status page): Add summary of ok/error/warning counts

* simplify a bit

---------

Co-authored-by: Hugh Secker-Walker <hsw+miac@hodain.net>
Co-authored-by: Joshua Tauberer <jt@occams.info>
2023-01-28 11:11:17 -05:00
Hugh Secker-Walker
4408cb1fba fix(rsync-backup): Provide default port 22 for rsync usage in backup.py (#2226)
Co-authored-by: Hugh Secker-Walker <hsw+miac@hodain.net>
2023-01-28 11:04:46 -05:00
Joshua Tauberer
5e3e4a2161 v61 2023-01-21 08:20:48 -05:00
Joshua Tauberer
61d1ea1ea7 Changelog entries 2023-01-15 10:17:10 -05:00
Joshua Tauberer
b3743a31e9 Add a status checks check that fail2ban is running using fail2ban-client 2023-01-15 10:17:10 -05:00
Joshua Tauberer
26709a3c1d Improve error messages in the management tools when external command-line tools are run 2023-01-15 10:17:10 -05:00
jcm-shove-it
20ec6c2080 Updated security.md to reflect the support of ubuntu 22.04 (#2219) 2023-01-15 10:05:36 -05:00
Steven Conaway
7a79153afe Remove old darkmode background color (#2218)
Removing this old background color solves the problem of the bottom of short pages (like `/admin`'s login page) being white. The background was being set to black, which would be inverted, so it'd appear white. Since the `filter:` css has [~97% support](https://caniuse.com/?search=filter), I think that this change should be made. Tested on latest versions of Chrome (mac and iOS), Firefox, and Safari (mac and iOS).
2023-01-15 10:05:13 -05:00
Hugh Secker-Walker
a2565227f2 feat(rsync-port): Add support for non-standard ssh port for rsync backup (#2208) 2023-01-15 10:03:05 -05:00
Hugh Secker-Walker
02b34ce699 fix(backup-display): Fix parsing of rsync target in system-backup.html, fixes #2206 (#2207) 2023-01-15 10:01:07 -05:00
Hugh Secker-Walker
820a39b865 chore(python open): Refactor open and gzip.open to use context manager (#2203)
Co-authored-by: Hugh Secker-Walker <hsw+miac@hodain.net>
2023-01-15 08:28:43 -05:00
Hugh Secker-Walker
57047d96e9 chore(setup): Update obsolete chown group syntax (#2202)
Co-authored-by: Hugh Secker-Walker <hsw+miac@hodain.net>
2023-01-15 08:25:36 -05:00
KiekerJan
1587248762 Disable Roundcube password plugin since it was corrupting the user database (#2198) 2023-01-15 08:22:43 -05:00
KiekerJan
0fc5105da5 Fixes to DNS lookups during status checks when there are timeouts, enforce timeouts better (#2191)
* add dns query handling changes

* replace exception pass with error message

* simplify dns exception catching

* Add not set case to blacklist lookup result handling
2023-01-15 08:20:08 -05:00
KiekerJan
c29593b5ef explicitly enable fail2ban which didn't start (#2190) 2023-01-15 08:10:04 -05:00
Joshua Tauberer
3314c4f7de v60.1 2022-10-30 08:18:13 -04:00
Joshua Tauberer
1f60236985 Upgrade Nextcloud to 23.0.4 (contacts to 4.2.0, calendar to 3.5.0)
This fixes the monthly view calendar items being in random order.
2022-10-30 08:16:54 -04:00
alento-group
32c68874c5 Fix NSD not restarting (#2182)
A previous commit (0a970f4bb2) broke nsd restarting. This fixes that change by reverting it.

Josh added: Use nsd-control with reconfig and reload if they succeed and only fall back to restarting nsd if they fail

Co-authored-by: Joshua Tauberer <jt@occams.info>
2022-10-30 08:16:03 -04:00
Joshua Tauberer
286a4bd9e7 Remove stray quote in bootstrap.sh
Reported at https://discourse.mailinabox.email/t/version-60-for-ubuntu-22-04-is-released/9558/4.
2022-10-12 06:11:02 -04:00
Joshua Tauberer
ddf8e857fd Support Ubuntu 22.04 Jammy Jellyfish (#2083) 2022-10-11 21:18:34 -04:00
Joshua Tauberer
4d5ff0210b Version 60 2022-10-11 21:14:31 -04:00
Joshua Tauberer
89cd9fb611 Increase gunicorn's worker timeout since some /admin commands take a long time 2022-10-08 08:23:48 -04:00
Joshua Tauberer
22a6270657 Remove old setup step to uninstall acme library 2022-10-08 08:23:48 -04:00
Joshua Tauberer
0a970f4bb2 Use nsd-control to refresh nsd after zone files are rewritten rather than 'service nsd restart'
I am not sure if this was the problem but nsd didn't serve updated zonefiles on my box and 'service nsd restart' must have been used, so maybe it doesn't reload zones.
2022-10-08 07:24:57 -04:00
Joshua Tauberer
9b111e2493 Update to Nextcloud 23.0.8 (contacts 4.2.0, calendar 3.5.0) 2022-10-08 07:23:21 -04:00
jvolkenant
b8feb77ef4 Move postgrey database under $STORAGE_ROOT (#2077) 2022-09-24 13:17:55 -04:00
Joshua Tauberer
3c44604316 Install 'file' package
The command is used in mailinabox-postgrey-whitelist. Reported missing (on systems that don't install it by default) in #2083.
2022-09-24 10:10:50 -04:00
Steve Hay
1e1a054686 BUGFIX: Correctly handle the multiprocessing for run_checks in the management daemon (#2163)
See discussion here: #2083

Co-authored-by: Steve Hay <hay.steve@gmail.com>
2022-09-24 09:56:27 -04:00
kiekerjan
d584a41e60 Update Roundcube to 1.6.0 (#2153) 2022-09-17 09:20:20 -04:00
downtownallday
56074ae035 Tighten roundcube session config (#2138)
Merges #2138.
2022-09-17 09:09:00 -04:00
downtownallday
30631b0fc5 Fix undefined variable 'val' in tools/editconf.py (#2137)
Merges #2137.
2022-09-17 09:09:00 -04:00
Steve Hay
84da4e6000 Update dovecot to use same DH parameters file as the other services
Originally from #2157.
2022-09-17 09:07:54 -04:00
Joshua Tauberer
58ded74181 Restore the backup S3 host select box if an S3 target has been set
Also remove unnecessary import added in 7cda439c. Was a mistake from edits during PR review.
2022-09-17 09:07:54 -04:00
Steve Hay
3fd2e3efa9 Replace Flask built-in WSGI server with gunicorn (#2158) 2022-09-17 08:03:16 -04:00
Steve Hay
7cda439c80 Port boto to boto3 and fix asyncio issue in the management daemon (#2156)
Co-authored-by: Steve Hay <hay.steve@gmail.com>
2022-09-17 07:57:12 -04:00
Joshua Tauberer
91fc74b408 Setup fixes for Ubuntu 22.04
Nextcloud:
* The Nextcloud user_external 1.0.0 package for Nextcloud 21.0.7 isn't available from Nextcloud's releases page, but it's not needed in an intermediate upgrade step (hopefully), so we can skip it.
* Nextcloud updgrade steps should not be elifs because multiple intermediate upgrades may be needed.
* Continue if the user_external backend migration fails. Maybe it's not necessary. It gives a scary error message though.
* Remove a line that removes an old file that hasn't been in use since 2019 and the expectation is that Ubuntu 22.04 installations are on fresh machines.

Backups:
* For duplicity, we now need boto3 for AWS.
2022-09-03 07:50:36 -04:00
Sudheesh Singanamalla
d7244ed920 Fixes #2149 Append ; in policy strings for DMARC settings (#2151)
Signed-off-by: Sudheesh Singanamalla <sudheesh@cloudflare.com>
2022-08-19 13:23:42 -04:00
David Duque
e0c0b5053c Upgrade Nextcloud External User Backend to v3.0.0
Co-Authored-By: Joshua Tauberer <jt@occams.info>
2022-07-28 14:42:51 -04:00
Joshua Tauberer
268b31685d Ensure STORAGE_ROOT has a+rx permission since processes run by different system users need to access files within it 2022-07-28 14:42:51 -04:00
Joshua Tauberer
ab71abbc7c Update to latest cryptography Python package, add missing source at top of management.sh so it can run standalone (needs STORAGE_ROOT) 2022-07-28 14:42:51 -04:00
Joshua Tauberer
87e6df9e28 Fix roundcube dependency missing imap and unneeded ldap 2022-07-28 14:42:51 -04:00
Felix Matouschek
558f2db31f system.sh: Remove no longer needed haveged (#2090)
Starting from kernels 5.6 haveged is obsolete. Therefore remove it in
Ubuntu 22.04.

See https://github.com/jirka-h/haveged/issues/57
2022-07-28 14:42:51 -04:00
Joshua Tauberer
c23dd701f0 Start changelog and instructions updates for version 60 supporting Ubuntu 22.04
To scan for updated apt packages in Ubuntu 22.04, I ran on Ubuntu 18.04 and 22.04 and compared the output:

```
for package in openssl openssh-client haveged pollinate fail2ban ufw bind9 nsd ldnsutils nginx dovecot-core postfix opendkim opendkim-tools opendmarc postgrey spampd razor pyzor dovecot-antispam sqlite3 duplicity certbot munin munin-node php python3; do
  echo -n "$package ";
  dpkg-query --showformat='${Version}' --show $package;
  echo
done
```
2022-07-28 14:42:51 -04:00
Joshua Tauberer
0a7b9d5089 Update dovecot, spampd settings for Ubuntu 22.04
* dovecot's ssl_protocols became ssl_min_protocol in 2.3
* spampd fixed a bug so we can remove lmtp_destination_recipient_limit=1 in postfix
2022-07-28 14:34:45 -04:00
Joshua Tauberer
1eddf9a220 Upgrade to Nextcloud 23.0.4
The first version supporting PHP 8.0 is Nextcloud 21. Therefore we can add migrations only to Nextcloud 21 forward, and so we only support migrating from Nextcloud 20 (Mail-in-a-Box versions v0.51+). Migration steps through Nextcloud 21 and 22 are added.

Also:

* Fix PHP APUc settings to be before Nextcloud tools are run.
2022-07-28 14:34:45 -04:00
Joshua Tauberer
78d71498fa Upgrade from PHP 7.2 to 8.0 for Ubuntu 22.04
* Add the PHP PPA.
* Specify the version when invoking the php CLI.
* Specify the version in package names.
* Update paths to 8.0 (using a variable in the setup scripts).
* Update z-push's php-xsl dependency to php8.0-xml.
* php-json is now built-into PHP.

Although PHP 8.1 is the stock version in Ubuntu 22.04, it's not supported by Nextcloud yet, and it likely will never be supported by the the version of Nextcloud that succeeds the last version of Nextcloud that supports PHP 7.2, and we have to install the next version so that an upgrade is permitted, so skipping to PHP 8.1 may not be easily possible.
2022-07-28 14:02:46 -04:00
Joshua Tauberer
b41a0ad80e Drop some hacks that we needed for Ubuntu 18.04
* certbot's PPA is no longer needed because a recent version is now included in the Ubuntu respository.
* Un-pin b2sdk (reverts 69d8fdef99 and d829d74048).
* Revert boto+s3 workaround for duplicity (partial revert of 99474b348f).
* Revert old "fix boto 2 conflict on Google Compute Engine instances" (cf33be4596) which is probably no longer needed.
2022-07-28 14:02:46 -04:00
Rauno Moisto
78569e9a88 Fix DeprecationWarning in dnspython query vs resolve method
The resolve method disables resolving relative names by default. This change probably makes a7710e90 unnecessary. @JoshData added some additional changes from query to resolve.
2022-07-28 14:02:46 -04:00
Daniel Mabbett
8cb360fe36 Configure nsd listening interfaces before installing nsd so that it does not interfere with bind9 2022-07-28 14:02:46 -04:00
Joshua Tauberer
f534a530d4 Update and drop some package and file names for Ubuntu 22.04
* Fix path to bind9 startup options file in Ubuntu 22.04.
* tinymce has not been a Roundcube requirement recently and is no longer a package in Ubuntu 22.04
* Upgrade Vagrant box to Ubuntu 22.04
2022-07-28 14:02:46 -04:00
Joshua Tauberer
2abcafd670 Update Ubuntu version checks from 18.04 to 22.04 2022-07-28 14:02:44 -04:00
52 changed files with 865 additions and 495 deletions

View File

@@ -1,6 +1,112 @@
CHANGELOG CHANGELOG
========= =========
Version 65 (October 27, 2023)
-----------------------------
* Roundcube updated to 1.6.4 fixing a security vulnerability.
* zpush.sh updated to version 2.7.1.
* Fixed a typo in the control panel.
Version 64 (September 2, 2023)
------------------------------
* Fixed broken installation when upgrading from Mail-in-a-Box version 56 (Nextcloud 22) and earlier because of an upstream packaging issue.
* Fixed backups to work with the latest duplicity package which was not backwards compatible.
* Fixed setting B2 as a backup target with a slash in the application key.
* Turned off OpenDMARC diagnostic reports sent in response to incoming mail.
* Fixed some crashes when using an unrelased version of Mail-in-a-Box.
* Added z-push administration scripts.
Version 63 (July 27, 2023)
--------------------------
* Nextcloud updated to 25.0.7.
Version 62 (May 20, 2023)
-------------------------
Package updates:
* Nextcloud updated to 23.0.12 (and its apps also updated).
* Roundcube updated to 1.6.1.
* Z-Push to 2.7.0, which has compatibility for Ubuntu 22.04, so it works again.
Mail:
* Roundcube's password change page is now working again.
Control panel:
* Allow setting the backup location's S3 region name for non-AWS S3-compatible backup hosts.
* Control panel pages can be opened in a new tab/window and bookmarked and browser history navigation now works.
* Add a Copy button to put the rsync backup public key on clipboard.
* Allow secondary DNS xfr: items added in the control panel to be hostnames too.
* Fixed issue where sshkeygen fails when IPv6 is disabled.
* Fixed issue opening munin reports.
* Fixed report formatting in status emails sent to the administrator.
Version 61.1 (January 28, 2023)
-------------------------------
* Fixed rsync backups not working with the default port.
* Reverted "Improve error messages in the management tools when external command-line tools are run." because of the possibility of user secrets being included in error messages.
* Fix for TLS certificate SHA fingerprint not being displayed during setup.
Version 61 (January 21, 2023)
-----------------------------
System:
* fail2ban didn't start after setup.
Mail:
* Disable Roundcube password plugin since it was corrupting the user database.
Control panel:
* Fix changing existing backup settings when the rsync type is used.
* Allow setting a custom port for rsync backups.
* Fixes to DNS lookups during status checks when there are timeouts, enforce timeouts better.
* A new check is added to ensure fail2ban is running.
* Fixed a color.
* Improve error messages in the management tools when external command-line tools are run.
Version 60.1 (October 30, 2022)
-------------------------------
* A setup issue where the DNS server nsd isn't running at the end of setup is (hopefully) fixed.
* Nextcloud is updated to 23.0.10 (contacts to 4.2.2, calendar to 3.5.1).
Version 60 (October 11, 2022)
-----------------------------
This is the first release for Ubuntu 22.04.
**Before upgrading**, you must **first upgrade your existing Ubuntu 18.04 box to Mail-in-a-Box v0.51 or later**, if you haven't already done so. That may not be possible after Ubuntu 18.04 reaches its end of life in April 2023, so please complete the upgrade well before then. (If you are not using Nextcloud's contacts or calendar, you can migrate to the latest version of Mail-in-a-Box from any previous version.)
For complete upgrade instructions, see:
https://discourse.mailinabox.email/t/version-60-for-ubuntu-22-04-is-about-to-be-released/9558
No major features of Mail-in-a-Box have changed in this release, although some minor fixes were made.
With the newer version of Ubuntu the following software packages we use are updated:
* dovecot is upgraded to 2.3.16, postfix to 3.6.4, opendmark to 1.4 (which adds ARC-Authentication-Results headers), and spampd to 2.53 (alleviating a mail delivery rate limiting bug).
* Nextcloud is upgraded to 23.0.4 (contacts to 4.2.0, calendar to 3.5.0).
* Roundcube is upgraded to 1.6.0.
* certbot is upgraded to 1.21 (via the Ubuntu repository instead of a PPA).
* fail2ban is upgraded to 0.11.2.
* nginx is upgraded to 1.18.
* PHP is upgraded from 7.2 to 8.0.
Also:
* Roundcube's login session cookie was tightened. Existing sessions may require a manual logout.
* Moved Postgrey's database under $STORAGE_ROOT.
Version 57a (June 19, 2022) Version 57a (June 19, 2022)
--------------------------- ---------------------------

View File

@@ -23,7 +23,7 @@ Additionally, this project has a [Code of Conduct](CODE_OF_CONDUCT.md), which su
In The Box In The Box
---------- ----------
Mail-in-a-Box turns a fresh Ubuntu 18.04 LTS 64-bit machine into a working mail server by installing and configuring various components. Mail-in-a-Box turns a fresh Ubuntu 22.04 LTS 64-bit machine into a working mail server by installing and configuring various components.
It is a one-click email appliance. There are no user-configurable setup options. It "just works." It is a one-click email appliance. There are no user-configurable setup options. It "just works."
@@ -54,13 +54,13 @@ Installation
See the [setup guide](https://mailinabox.email/guide.html) for detailed, user-friendly instructions. See the [setup guide](https://mailinabox.email/guide.html) for detailed, user-friendly instructions.
For experts, start with a completely fresh (really, I mean it) Ubuntu 18.04 LTS 64-bit machine. On the machine... For experts, start with a completely fresh (really, I mean it) Ubuntu 22.04 LTS 64-bit machine. On the machine...
Clone this repository and checkout the tag corresponding to the most recent release: Clone this repository and checkout the tag corresponding to the most recent release:
$ git clone https://github.com/mail-in-a-box/mailinabox $ git clone https://github.com/mail-in-a-box/mailinabox
$ cd mailinabox $ cd mailinabox
$ git checkout v57a $ git checkout v65
Begin the installation. Begin the installation.

2
Vagrantfile vendored
View File

@@ -2,7 +2,7 @@
# vi: set ft=ruby : # vi: set ft=ruby :
Vagrant.configure("2") do |config| Vagrant.configure("2") do |config|
config.vm.box = "ubuntu/bionic64" config.vm.box = "ubuntu/jammy64"
# Network config: Since it's a mail server, the machine must be connected # Network config: Since it's a mail server, the machine must be connected
# to the public web. However, we currently don't want to expose SSH since # to the public web. However, we currently don't want to expose SSH since

View File

@@ -4,6 +4,7 @@ After=multi-user.target
[Service] [Service]
Type=idle Type=idle
IgnoreSIGPIPE=False
ExecStart=/usr/local/lib/mailinabox/start ExecStart=/usr/local/lib/mailinabox/start
[Install] [Install]

View File

@@ -73,4 +73,9 @@
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;
# This addresses those service discovery issues mentioned in:
# https://docs.nextcloud.com/server/23/admin_manual/issues/general_troubleshooting.html#service-discovery
rewrite ^/.well-known/webfinger /cloud/index.php/.well-known/webfinger redirect;
rewrite ^/.well-known/nodeinfo /cloud/index.php/.well-known/nodeinfo redirect;
# ADDITIONAL DIRECTIVES HERE # ADDITIONAL DIRECTIVES HERE

View File

@@ -7,6 +7,6 @@
## your own --- please do not ask for help from us. ## your own --- please do not ask for help from us.
upstream php-fpm { upstream php-fpm {
server unix:/var/run/php/php7.2-fpm.sock; server unix:/var/run/php/php8.0-fpm.sock;
} }

View File

@@ -22,20 +22,8 @@ class AuthService:
def init_system_api_key(self): def init_system_api_key(self):
"""Write an API key to a local file so local processes can use the API""" """Write an API key to a local file so local processes can use the API"""
def create_file_with_mode(path, mode): with open(self.key_path, 'r') as file:
# Based on answer by A-B-B: http://stackoverflow.com/a/15015748 self.key = file.read()
old_umask = os.umask(0)
try:
return os.fdopen(os.open(path, os.O_WRONLY | os.O_CREAT, mode), 'w')
finally:
os.umask(old_umask)
self.key = secrets.token_hex(32)
os.makedirs(os.path.dirname(self.key_path), exist_ok=True)
with create_file_with_mode(self.key_path, 0o640) as key_file:
key_file.write(self.key + '\n')
def authenticate(self, request, env, login_only=False, logout=False): def authenticate(self, request, env, login_only=False, logout=False):
"""Test if the HTTP Authorization header's username matches the system key, a session key, """Test if the HTTP Authorization header's username matches the system key, a session key,

View File

@@ -12,7 +12,7 @@ import dateutil.parser, dateutil.relativedelta, dateutil.tz
import rtyaml import rtyaml
from exclusiveprocess import Lock from exclusiveprocess import Lock
from utils import load_environment, shell, wait_for_service, fix_boto from utils import load_environment, shell, wait_for_service
def backup_status(env): def backup_status(env):
# If backups are dissbled, return no status. # If backups are dissbled, return no status.
@@ -57,10 +57,11 @@ def backup_status(env):
"/usr/bin/duplicity", "/usr/bin/duplicity",
"collection-status", "collection-status",
"--archive-dir", backup_cache_dir, "--archive-dir", backup_cache_dir,
"--gpg-options", "--cipher-algo=AES256", "--gpg-options", "'--cipher-algo=AES256'",
"--log-fd", "1", "--log-fd", "1",
get_duplicity_target_url(config), ] + get_duplicity_additional_args(env) + [
] + get_duplicity_additional_args(env), get_duplicity_target_url(config)
],
get_duplicity_env_vars(env), get_duplicity_env_vars(env),
trap=True) trap=True)
if code != 0: if code != 0:
@@ -197,17 +198,14 @@ def get_duplicity_target_url(config):
from urllib.parse import urlsplit, urlunsplit from urllib.parse import urlsplit, urlunsplit
target = list(urlsplit(target)) target = list(urlsplit(target))
# Duplicity now defaults to boto3 as the backend for S3, but we have # Although we store the S3 hostname in the target URL,
# legacy boto installed (boto3 doesn't support Ubuntu 18.04) so
# we retarget for classic boto.
target[0] = "boto+" + target[0]
# In addition, although we store the S3 hostname in the target URL,
# duplicity no longer accepts it in the target URL. The hostname in # duplicity no longer accepts it in the target URL. The hostname in
# the target URL must be the bucket name. The hostname is passed # the target URL must be the bucket name. The hostname is passed
# via get_duplicity_additional_args. Move the first part of the # via get_duplicity_additional_args. Move the first part of the
# path (the bucket name) into the hostname URL component, and leave # path (the bucket name) into the hostname URL component, and leave
# the rest for the path. # the rest for the path. (The S3 region name is also stored in the
# hostname part of the URL, in the username portion, which we also
# have to drop here).
target[1], target[2] = target[2].lstrip('/').split('/', 1) target[1], target[2] = target[2].lstrip('/').split('/', 1)
target = urlunsplit(target) target = urlunsplit(target)
@@ -218,16 +216,32 @@ def get_duplicity_additional_args(env):
config = get_backup_config(env) config = get_backup_config(env)
if get_target_type(config) == 'rsync': if get_target_type(config) == 'rsync':
# Extract a port number for the ssh transport. Duplicity accepts the
# optional port number syntax in the target, but it doesn't appear to act
# on it, so we set the ssh port explicitly via the duplicity options.
from urllib.parse import urlsplit
try:
port = urlsplit(config["target"]).port
except ValueError:
port = 22
if port is None:
port = 22
return [ return [
"--ssh-options= -i /root/.ssh/id_rsa_miab", f"--ssh-options='-i /root/.ssh/id_rsa_miab -p {port}'",
"--rsync-options= -e \"/usr/bin/ssh -oStrictHostKeyChecking=no -oBatchMode=yes -p 22 -i /root/.ssh/id_rsa_miab\"", f"--rsync-options='-e \"/usr/bin/ssh -oStrictHostKeyChecking=no -oBatchMode=yes -p {port} -i /root/.ssh/id_rsa_miab\"'",
] ]
elif get_target_type(config) == 's3': elif get_target_type(config) == 's3':
# See note about hostname in get_duplicity_target_url. # See note about hostname in get_duplicity_target_url.
# The region name, which is required by some non-AWS endpoints,
# is saved inside the username portion of the URL.
from urllib.parse import urlsplit, urlunsplit from urllib.parse import urlsplit, urlunsplit
target = urlsplit(config["target"]) target = urlsplit(config["target"])
endpoint_url = urlunsplit(("https", target.netloc, '', '', '')) endpoint_url = urlunsplit(("https", target.hostname, '', '', ''))
return ["--s3-endpoint-url", endpoint_url] args = ["--s3-endpoint-url", endpoint_url]
if target.username: # region name is stuffed here
args += ["--s3-region-name", target.username]
return args
return [] return []
@@ -283,9 +297,10 @@ def perform_backup(full_backup):
if quit: if quit:
sys.exit(code) sys.exit(code)
service_command("php7.2-fpm", "stop", quit=True) service_command("php8.0-fpm", "stop", quit=True)
service_command("postfix", "stop", quit=True) service_command("postfix", "stop", quit=True)
service_command("dovecot", "stop", quit=True) service_command("dovecot", "stop", quit=True)
service_command("postgrey", "stop", quit=True)
# Execute a pre-backup script that copies files outside the homedir. # Execute a pre-backup script that copies files outside the homedir.
# Run as the STORAGE_USER user, not as root. Pass our settings in # Run as the STORAGE_USER user, not as root. Pass our settings in
@@ -307,17 +322,19 @@ def perform_backup(full_backup):
"--archive-dir", backup_cache_dir, "--archive-dir", backup_cache_dir,
"--exclude", backup_root, "--exclude", backup_root,
"--volsize", "250", "--volsize", "250",
"--gpg-options", "--cipher-algo=AES256", "--gpg-options", "'--cipher-algo=AES256'",
"--allow-source-mismatch"
] + get_duplicity_additional_args(env) + [
env["STORAGE_ROOT"], env["STORAGE_ROOT"],
get_duplicity_target_url(config), get_duplicity_target_url(config),
"--allow-source-mismatch" ],
] + get_duplicity_additional_args(env),
get_duplicity_env_vars(env)) get_duplicity_env_vars(env))
finally: finally:
# Start services again. # Start services again.
service_command("postgrey", "start", quit=False)
service_command("dovecot", "start", quit=False) service_command("dovecot", "start", quit=False)
service_command("postfix", "start", quit=False) service_command("postfix", "start", quit=False)
service_command("php7.2-fpm", "start", quit=False) service_command("php8.0-fpm", "start", quit=False)
# Remove old backups. This deletes all backup data no longer needed # Remove old backups. This deletes all backup data no longer needed
# from more than 3 days ago. # from more than 3 days ago.
@@ -328,8 +345,9 @@ def perform_backup(full_backup):
"--verbosity", "error", "--verbosity", "error",
"--archive-dir", backup_cache_dir, "--archive-dir", backup_cache_dir,
"--force", "--force",
] + get_duplicity_additional_args(env) + [
get_duplicity_target_url(config) get_duplicity_target_url(config)
] + get_duplicity_additional_args(env), ],
get_duplicity_env_vars(env)) get_duplicity_env_vars(env))
# From duplicity's manual: # From duplicity's manual:
@@ -343,8 +361,9 @@ def perform_backup(full_backup):
"--verbosity", "error", "--verbosity", "error",
"--archive-dir", backup_cache_dir, "--archive-dir", backup_cache_dir,
"--force", "--force",
] + get_duplicity_additional_args(env) + [
get_duplicity_target_url(config) get_duplicity_target_url(config)
] + get_duplicity_additional_args(env), ],
get_duplicity_env_vars(env)) get_duplicity_env_vars(env))
# Change ownership of backups to the user-data user, so that the after-bcakup # Change ownership of backups to the user-data user, so that the after-bcakup
@@ -381,9 +400,10 @@ def run_duplicity_verification():
"--compare-data", "--compare-data",
"--archive-dir", backup_cache_dir, "--archive-dir", backup_cache_dir,
"--exclude", backup_root, "--exclude", backup_root,
] + get_duplicity_additional_args(env) + [
get_duplicity_target_url(config), get_duplicity_target_url(config),
env["STORAGE_ROOT"], env["STORAGE_ROOT"],
] + get_duplicity_additional_args(env), get_duplicity_env_vars(env)) ], get_duplicity_env_vars(env))
def run_duplicity_restore(args): def run_duplicity_restore(args):
env = load_environment() env = load_environment()
@@ -393,9 +413,23 @@ def run_duplicity_restore(args):
"/usr/bin/duplicity", "/usr/bin/duplicity",
"restore", "restore",
"--archive-dir", backup_cache_dir, "--archive-dir", backup_cache_dir,
get_duplicity_target_url(config), ] + get_duplicity_additional_args(env) + [
] + get_duplicity_additional_args(env) + args, get_duplicity_target_url(config)
get_duplicity_env_vars(env)) ] + args,
get_duplicity_env_vars(env))
def print_duplicity_command():
import shlex
env = load_environment()
config = get_backup_config(env)
backup_cache_dir = os.path.join(env["STORAGE_ROOT"], 'backup', 'cache')
for k, v in get_duplicity_env_vars(env).items():
print(f"export {k}={shlex.quote(v)}")
print("duplicity", "{command}", shlex.join([
"--archive-dir", backup_cache_dir,
] + get_duplicity_additional_args(env) + [
get_duplicity_target_url(config)
]))
def list_target_files(config): def list_target_files(config):
import urllib.parse import urllib.parse
@@ -411,6 +445,16 @@ def list_target_files(config):
rsync_fn_size_re = re.compile(r'.* ([^ ]*) [^ ]* [^ ]* (.*)') rsync_fn_size_re = re.compile(r'.* ([^ ]*) [^ ]* [^ ]* (.*)')
rsync_target = '{host}:{path}' rsync_target = '{host}:{path}'
# Strip off any trailing port specifier because it's not valid in rsync's
# DEST syntax. Explicitly set the port number for the ssh transport.
user_host, *_ = target.netloc.rsplit(':', 1)
try:
port = target.port
except ValueError:
port = 22
if port is None:
port = 22
target_path = target.path target_path = target.path
if not target_path.endswith('/'): if not target_path.endswith('/'):
target_path = target_path + '/' target_path = target_path + '/'
@@ -419,11 +463,11 @@ def list_target_files(config):
rsync_command = [ 'rsync', rsync_command = [ 'rsync',
'-e', '-e',
'/usr/bin/ssh -i /root/.ssh/id_rsa_miab -oStrictHostKeyChecking=no -oBatchMode=yes', f'/usr/bin/ssh -i /root/.ssh/id_rsa_miab -oStrictHostKeyChecking=no -oBatchMode=yes -p {port}',
'--list-only', '--list-only',
'-r', '-r',
rsync_target.format( rsync_target.format(
host=target.netloc, host=user_host,
path=target_path) path=target_path)
] ]
@@ -451,26 +495,13 @@ def list_target_files(config):
raise ValueError("Connection to rsync host failed: {}".format(reason)) raise ValueError("Connection to rsync host failed: {}".format(reason))
elif target.scheme == "s3": elif target.scheme == "s3":
# match to a Region import boto3.s3
fix_boto() # must call prior to importing boto from botocore.exceptions import ClientError
import boto.s3
from boto.exception import BotoServerError # separate bucket from path in target
custom_region = False
for region in boto.s3.regions():
if region.endpoint == target.hostname:
break
else:
# If region is not found this is a custom region
custom_region = True
bucket = target.path[1:].split('/')[0] bucket = target.path[1:].split('/')[0]
path = '/'.join(target.path[1:].split('/')[1:]) + '/' path = '/'.join(target.path[1:].split('/')[1:]) + '/'
# Create a custom region with custom endpoint
if custom_region:
from boto.s3.connection import S3Connection
region = boto.s3.S3RegionInfo(name=bucket, endpoint=target.hostname, connection_cls=S3Connection)
# If no prefix is specified, set the path to '', otherwise boto won't list the files # If no prefix is specified, set the path to '', otherwise boto won't list the files
if path == '/': if path == '/':
path = '' path = ''
@@ -480,18 +511,15 @@ def list_target_files(config):
# connect to the region & bucket # connect to the region & bucket
try: try:
conn = region.connect(aws_access_key_id=config["target_user"], aws_secret_access_key=config["target_pass"]) s3 = boto3.client('s3', \
bucket = conn.get_bucket(bucket) endpoint_url=f'https://{target.hostname}', \
except BotoServerError as e: aws_access_key_id=config['target_user'], \
if e.status == 403: aws_secret_access_key=config['target_pass'])
raise ValueError("Invalid S3 access key or secret access key.") bucket_objects = s3.list_objects_v2(Bucket=bucket, Prefix=path)['Contents']
elif e.status == 404: backup_list = [(key['Key'][len(path):], key['Size']) for key in bucket_objects]
raise ValueError("Invalid S3 bucket name.") except ClientError as e:
elif e.status == 301: raise ValueError(e)
raise ValueError("Incorrect region for this bucket.") return backup_list
raise ValueError(e.reason)
return [(key.name[len(path):], key.size) for key in bucket.list(prefix=path)]
elif target.scheme == 'b2': elif target.scheme == 'b2':
from b2sdk.v1 import InMemoryAccountInfo, B2Api from b2sdk.v1 import InMemoryAccountInfo, B2Api
from b2sdk.v1.exception import NonExistentBucket from b2sdk.v1.exception import NonExistentBucket
@@ -500,7 +528,7 @@ def list_target_files(config):
# Extract information from target # Extract information from target
b2_application_keyid = target.netloc[:target.netloc.index(':')] b2_application_keyid = target.netloc[:target.netloc.index(':')]
b2_application_key = target.netloc[target.netloc.index(':')+1:target.netloc.index('@')] b2_application_key = urllib.parse.unquote(target.netloc[target.netloc.index(':')+1:target.netloc.index('@')])
b2_bucket = target.netloc[target.netloc.index('@')+1:] b2_bucket = target.netloc[target.netloc.index('@')+1:]
try: try:
@@ -550,7 +578,8 @@ def get_backup_config(env, for_save=False, for_ui=False):
# Merge in anything written to custom.yaml. # Merge in anything written to custom.yaml.
try: try:
custom_config = rtyaml.load(open(os.path.join(backup_root, 'custom.yaml'))) with open(os.path.join(backup_root, 'custom.yaml'), 'r') as f:
custom_config = rtyaml.load(f)
if not isinstance(custom_config, dict): raise ValueError() # caught below if not isinstance(custom_config, dict): raise ValueError() # caught below
config.update(custom_config) config.update(custom_config)
except: except:
@@ -575,7 +604,8 @@ def get_backup_config(env, for_save=False, for_ui=False):
config["target"] = "file://" + config["file_target_directory"] config["target"] = "file://" + config["file_target_directory"]
ssh_pub_key = os.path.join('/root', '.ssh', 'id_rsa_miab.pub') ssh_pub_key = os.path.join('/root', '.ssh', 'id_rsa_miab.pub')
if os.path.exists(ssh_pub_key): if os.path.exists(ssh_pub_key):
config["ssh_pub_key"] = open(ssh_pub_key, 'r').read() with open(ssh_pub_key, 'r') as f:
config["ssh_pub_key"] = f.read()
return config return config
@@ -607,6 +637,9 @@ if __name__ == "__main__":
# to duplicity. The restore path should be specified. # to duplicity. The restore path should be specified.
run_duplicity_restore(sys.argv[2:]) run_duplicity_restore(sys.argv[2:])
elif sys.argv[-1] == "--duplicity-command":
print_duplicity_command()
else: else:
# Perform a backup. Add --full to force a full backup rather than # Perform a backup. Add --full to force a full backup rather than
# possibly performing an incremental backup. # possibly performing an incremental backup.

View File

@@ -47,7 +47,8 @@ def read_password():
return first return first
def setup_key_auth(mgmt_uri): def setup_key_auth(mgmt_uri):
key = open('/var/lib/mailinabox/api.key').read().strip() with open('/var/lib/mailinabox/api.key', 'r') as f:
key = f.read().strip()
auth_handler = urllib.request.HTTPBasicAuthHandler() auth_handler = urllib.request.HTTPBasicAuthHandler()
auth_handler.add_password( auth_handler.add_password(

View File

@@ -121,9 +121,9 @@ def index():
no_users_exist = (len(get_mail_users(env)) == 0) no_users_exist = (len(get_mail_users(env)) == 0)
no_admins_exist = (len(get_admins(env)) == 0) no_admins_exist = (len(get_admins(env)) == 0)
utils.fix_boto() # must call prior to importing boto import boto3.s3
import boto.s3 backup_s3_hosts = [(r, f"s3.{r}.amazonaws.com") for r in boto3.session.Session().get_available_regions('s3')]
backup_s3_hosts = [(r.name, r.endpoint) for r in boto.s3.regions()]
return render_template('index.html', return render_template('index.html',
hostname=env['PRIMARY_HOSTNAME'], hostname=env['PRIMARY_HOSTNAME'],
@@ -571,6 +571,8 @@ def system_status():
# Create a temporary pool of processes for the status checks # Create a temporary pool of processes for the status checks
with multiprocessing.pool.Pool(processes=5) as pool: with multiprocessing.pool.Pool(processes=5) as pool:
run_checks(False, env, output, pool) run_checks(False, env, output, pool)
pool.close()
pool.join()
return json_response(output.items) return json_response(output.items)
@app.route('/system/updates') @app.route('/system/updates')
@@ -707,7 +709,7 @@ def munin_cgi(filename):
support infrastructure like spawn-fcgi. support infrastructure like spawn-fcgi.
""" """
COMMAND = 'su - munin --preserve-environment --shell=/bin/bash -c /usr/lib/munin/cgi/munin-cgi-graph' COMMAND = 'su munin --preserve-environment --shell=/bin/bash -c /usr/lib/munin/cgi/munin-cgi-graph'
# su changes user, we use the munin user here # su changes user, we use the munin user here
# --preserve-environment retains the environment, which is where Popen's `env` data is # --preserve-environment retains the environment, which is where Popen's `env` data is
# --shell=/bin/bash ensures the shell used is bash # --shell=/bin/bash ensures the shell used is bash

View File

@@ -96,9 +96,18 @@ def do_dns_update(env, force=False):
if len(updated_domains) == 0: if len(updated_domains) == 0:
updated_domains.append("DNS configuration") updated_domains.append("DNS configuration")
# Kick nsd if anything changed. # Tell nsd to reload changed zone files.
if len(updated_domains) > 0: if len(updated_domains) > 0:
shell('check_call', ["/usr/sbin/service", "nsd", "restart"]) # 'reconfig' is needed if there are added or removed zones, but
# it may not reload existing zones, so we call 'reload' too. If
# nsd isn't running, nsd-control fails, so in that case revert
# to restarting nsd to make sure it is running. Restarting nsd
# should also refresh everything.
try:
shell('check_call', ["/usr/sbin/nsd-control", "reconfig"])
shell('check_call', ["/usr/sbin/nsd-control", "reload"])
except:
shell('check_call', ["/usr/sbin/service", "nsd", "restart"])
# Write the OpenDKIM configuration tables for all of the mail domains. # Write the OpenDKIM configuration tables for all of the mail domains.
from mailconfig import get_mail_domains from mailconfig import get_mail_domains
@@ -298,7 +307,7 @@ def build_zone(domain, domain_properties, additional_records, env, is_zone=True)
# Append a DMARC record. # Append a DMARC record.
# Skip if the user has set a DMARC record already. # Skip if the user has set a DMARC record already.
if not has_rec("_dmarc", "TXT", prefix="v=DMARC1; "): if not has_rec("_dmarc", "TXT", prefix="v=DMARC1; "):
records.append(("_dmarc", "TXT", 'v=DMARC1; p=quarantine', "Recommended. Specifies that mail that does not originate from the box but claims to be from @%s or which does not have a valid DKIM signature is suspect and should be quarantined by the recipient's mail system." % domain)) records.append(("_dmarc", "TXT", 'v=DMARC1; p=quarantine;', "Recommended. Specifies that mail that does not originate from the box but claims to be from @%s or which does not have a valid DKIM signature is suspect and should be quarantined by the recipient's mail system." % domain))
if domain_properties[domain]["user"]: if domain_properties[domain]["user"]:
# Add CardDAV/CalDAV SRV records on the non-primary hostname that points to the primary hostname # Add CardDAV/CalDAV SRV records on the non-primary hostname that points to the primary hostname
@@ -363,7 +372,7 @@ def build_zone(domain, domain_properties, additional_records, env, is_zone=True)
if not has_rec(qname, "TXT", prefix="v=spf1 "): if not has_rec(qname, "TXT", prefix="v=spf1 "):
records.append((qname, "TXT", 'v=spf1 -all', "Recommended. Prevents use of this domain name for outbound mail by specifying that no servers are valid sources for mail from @%s. If you do send email from this domain name you should either override this record such that the SPF rule does allow the originating server, or, take the recommended approach and have the box handle mail for this domain (simply add any receiving alias at this domain name to make this machine treat the domain name as one of its mail domains)." % d)) records.append((qname, "TXT", 'v=spf1 -all', "Recommended. Prevents use of this domain name for outbound mail by specifying that no servers are valid sources for mail from @%s. If you do send email from this domain name you should either override this record such that the SPF rule does allow the originating server, or, take the recommended approach and have the box handle mail for this domain (simply add any receiving alias at this domain name to make this machine treat the domain name as one of its mail domains)." % d))
if not has_rec("_dmarc" + ("."+qname if qname else ""), "TXT", prefix="v=DMARC1; "): if not has_rec("_dmarc" + ("."+qname if qname else ""), "TXT", prefix="v=DMARC1; "):
records.append(("_dmarc" + ("."+qname if qname else ""), "TXT", 'v=DMARC1; p=reject', "Recommended. Prevents use of this domain name for outbound mail by specifying that the SPF rule should be honoured for mail from @%s." % d)) records.append(("_dmarc" + ("."+qname if qname else ""), "TXT", 'v=DMARC1; p=reject;', "Recommended. Prevents use of this domain name for outbound mail by specifying that the SPF rule should be honoured for mail from @%s." % d))
# And with a null MX record (https://explained-from-first-principles.com/email/#null-mx-record) # And with a null MX record (https://explained-from-first-principles.com/email/#null-mx-record)
if not has_rec(qname, "MX"): if not has_rec(qname, "MX"):
@@ -456,7 +465,7 @@ def build_sshfp_records():
pass pass
break break
keys = shell("check_output", ["ssh-keyscan", "-t", "rsa,dsa,ecdsa,ed25519", "-p", str(port), "localhost"]) keys = shell("check_output", ["ssh-keyscan", "-4", "-t", "rsa,dsa,ecdsa,ed25519", "-p", str(port), "localhost"])
keys = sorted(keys.split("\n")) keys = sorted(keys.split("\n"))
for key in keys: for key in keys:
@@ -806,7 +815,8 @@ def write_opendkim_tables(domains, env):
def get_custom_dns_config(env, only_real_records=False): def get_custom_dns_config(env, only_real_records=False):
try: try:
custom_dns = rtyaml.load(open(os.path.join(env['STORAGE_ROOT'], 'dns/custom.yaml'))) with open(os.path.join(env['STORAGE_ROOT'], 'dns/custom.yaml'), 'r') as f:
custom_dns = rtyaml.load(f)
if not isinstance(custom_dns, dict): raise ValueError() # caught below if not isinstance(custom_dns, dict): raise ValueError() # caught below
except: except:
return [ ] return [ ]
@@ -983,6 +993,7 @@ def set_custom_dns_record(qname, rtype, value, action, env):
def get_secondary_dns(custom_dns, mode=None): def get_secondary_dns(custom_dns, mode=None):
resolver = dns.resolver.get_default_resolver() resolver = dns.resolver.get_default_resolver()
resolver.timeout = 10 resolver.timeout = 10
resolver.lifetime = 10
values = [] values = []
for qname, rtype, value in custom_dns: for qname, rtype, value in custom_dns:
@@ -994,25 +1005,33 @@ def get_secondary_dns(custom_dns, mode=None):
values.append(hostname) values.append(hostname)
continue continue
# This is a hostname. Before including in zone xfr lines, # If the entry starts with "xfr:" only include it in the zone transfer settings.
# resolve to an IP address. Otherwise just return the hostname. if hostname.startswith("xfr:"):
# It may not resolve to IPv6, so don't throw an exception if it if mode != "xfr": continue
# doesn't. hostname = hostname[4:]
if not hostname.startswith("xfr:"):
if mode == "xfr":
response = dns.resolver.query(hostname+'.', "A", raise_on_no_answer=False)
values.extend(map(str, response))
response = dns.resolver.query(hostname+'.', "AAAA", raise_on_no_answer=False)
values.extend(map(str, response))
continue
values.append(hostname)
# This is a zone-xfer-only IP address. Do not return if # If is a hostname, before including in zone xfr lines,
# we're querying for NS record hostnames. Only return if # resolve to an IP address.
# we're querying for zone xfer IP addresses - return the # It may not resolve to IPv6, so don't throw an exception if it
# IP address. # doesn't. Skip the entry if there is a DNS error.
elif mode == "xfr": if mode == "xfr":
values.append(hostname[4:]) try:
ipaddress.ip_interface(hostname) # test if it's an IP address or CIDR notation
values.append(hostname)
except ValueError:
try:
response = dns.resolver.resolve(hostname+'.', "A", raise_on_no_answer=False)
values.extend(map(str, response))
except dns.exception.DNSException:
pass
try:
response = dns.resolver.resolve(hostname+'.', "AAAA", raise_on_no_answer=False)
values.extend(map(str, response))
except dns.exception.DNSException:
pass
else:
values.append(hostname)
return values return values
@@ -1021,15 +1040,17 @@ def set_secondary_dns(hostnames, env):
# Validate that all hostnames are valid and that all zone-xfer IP addresses are valid. # Validate that all hostnames are valid and that all zone-xfer IP addresses are valid.
resolver = dns.resolver.get_default_resolver() resolver = dns.resolver.get_default_resolver()
resolver.timeout = 5 resolver.timeout = 5
resolver.lifetime = 5
for item in hostnames: for item in hostnames:
if not item.startswith("xfr:"): if not item.startswith("xfr:"):
# Resolve hostname. # Resolve hostname.
try: try:
response = resolver.query(item, "A") response = resolver.resolve(item, "A")
except (dns.resolver.NoNameservers, dns.resolver.NXDOMAIN, dns.resolver.NoAnswer): except (dns.resolver.NoNameservers, dns.resolver.NXDOMAIN, dns.resolver.NoAnswer, dns.resolver.Timeout):
try: try:
response = resolver.query(item, "AAAA") response = resolver.resolve(item, "AAAA")
except (dns.resolver.NoNameservers, dns.resolver.NXDOMAIN, dns.resolver.NoAnswer): except (dns.resolver.NoNameservers, dns.resolver.NXDOMAIN, dns.resolver.NoAnswer, dns.resolver.Timeout):
raise ValueError("Could not resolve the IP address of %s." % item) raise ValueError("Could not resolve the IP address of %s." % item)
else: else:
# Validate IP address. # Validate IP address.
@@ -1062,7 +1083,7 @@ def get_custom_dns_records(custom_dns, qname, rtype):
def build_recommended_dns(env): def build_recommended_dns(env):
ret = [] ret = []
for (domain, zonefile, records) in build_zones(env): for (domain, zonefile, records) in build_zones(env):
# remove records that we don't dislay # remove records that we don't display
records = [r for r in records if r[3] is not False] records = [r for r in records if r[3] is not False]
# put Required at the top, then Recommended, then everythiing else # put Required at the top, then Recommended, then everythiing else

View File

@@ -29,7 +29,7 @@ content = sys.stdin.read().strip()
# If there's nothing coming in, just exit. # If there's nothing coming in, just exit.
if content == "": if content == "":
sys.exit(0) sys.exit(0)
# create MIME message # create MIME message
msg = MIMEMultipart('alternative') msg = MIMEMultipart('alternative')
@@ -41,7 +41,7 @@ msg['From'] = "\"%s\" <%s>" % (env['PRIMARY_HOSTNAME'], admin_addr)
msg['To'] = admin_addr msg['To'] = admin_addr
msg['Subject'] = "[%s] %s" % (env['PRIMARY_HOSTNAME'], subject) msg['Subject'] = "[%s] %s" % (env['PRIMARY_HOSTNAME'], subject)
content_html = "<html><body><pre>{}</pre></body></html>".format(html.escape(content)) content_html = '<html><body><pre style="overflow-x: scroll; white-space: pre;">{}</pre></body></html>'.format(html.escape(content))
msg.attach(MIMEText(content, 'plain')) msg.attach(MIMEText(content, 'plain'))
msg.attach(MIMEText(content_html, 'html')) msg.attach(MIMEText(content_html, 'html'))

View File

@@ -73,7 +73,8 @@ def scan_files(collector):
continue continue
elif fn[-3:] == '.gz': elif fn[-3:] == '.gz':
tmp_file = tempfile.NamedTemporaryFile() tmp_file = tempfile.NamedTemporaryFile()
shutil.copyfileobj(gzip.open(fn), tmp_file) with gzip.open(fn, 'rb') as f:
shutil.copyfileobj(f, tmp_file)
if VERBOSE: if VERBOSE:
print("Processing file", fn, "...") print("Processing file", fn, "...")

View File

@@ -58,36 +58,33 @@ def get_ssl_certificates(env):
# Not a valid PEM format for a PEM type we care about. # Not a valid PEM format for a PEM type we care about.
continue continue
# Remember where we got this object.
pem._filename = fn
# Is it a private key? # Is it a private key?
if isinstance(pem, RSAPrivateKey): if isinstance(pem, RSAPrivateKey):
private_keys[pem.public_key().public_numbers()] = pem private_keys[pem.public_key().public_numbers()] = { "filename": fn, "key": pem }
# Is it a certificate? # Is it a certificate?
if isinstance(pem, Certificate): if isinstance(pem, Certificate):
certificates.append(pem) certificates.append({ "filename": fn, "cert": pem })
# Process the certificates. # Process the certificates.
domains = { } domains = { }
for cert in certificates: for cert in certificates:
# What domains is this certificate good for? # What domains is this certificate good for?
cert_domains, primary_domain = get_certificate_domains(cert) cert_domains, primary_domain = get_certificate_domains(cert["cert"])
cert._primary_domain = primary_domain cert["primary_domain"] = primary_domain
# Is there a private key file for this certificate? # Is there a private key file for this certificate?
private_key = private_keys.get(cert.public_key().public_numbers()) private_key = private_keys.get(cert["cert"].public_key().public_numbers())
if not private_key: if not private_key:
continue continue
cert._private_key = private_key cert["private_key"] = private_key
# Add this cert to the list of certs usable for the domains. # Add this cert to the list of certs usable for the domains.
for domain in cert_domains: for domain in cert_domains:
# The primary hostname can only use a certificate mapped # The primary hostname can only use a certificate mapped
# to the system private key. # to the system private key.
if domain == env['PRIMARY_HOSTNAME']: if domain == env['PRIMARY_HOSTNAME']:
if cert._private_key._filename != os.path.join(env['STORAGE_ROOT'], 'ssl', 'ssl_private_key.pem'): if cert["private_key"]["filename"] != os.path.join(env['STORAGE_ROOT'], 'ssl', 'ssl_private_key.pem'):
continue continue
domains.setdefault(domain, []).append(cert) domains.setdefault(domain, []).append(cert)
@@ -100,10 +97,10 @@ def get_ssl_certificates(env):
#for c in cert_list: print(domain, c.not_valid_before, c.not_valid_after, "("+str(now)+")", c.issuer, c.subject, c._filename) #for c in cert_list: print(domain, c.not_valid_before, c.not_valid_after, "("+str(now)+")", c.issuer, c.subject, c._filename)
cert_list.sort(key = lambda cert : ( cert_list.sort(key = lambda cert : (
# must be valid NOW # must be valid NOW
cert.not_valid_before <= now <= cert.not_valid_after, cert["cert"].not_valid_before <= now <= cert["cert"].not_valid_after,
# prefer one that is not self-signed # prefer one that is not self-signed
cert.issuer != cert.subject, cert["cert"].issuer != cert["cert"].subject,
########################################################### ###########################################################
# The above lines ensure that valid certificates are chosen # The above lines ensure that valid certificates are chosen
@@ -113,7 +110,7 @@ def get_ssl_certificates(env):
# prefer one with the expiration furthest into the future so # prefer one with the expiration furthest into the future so
# that we can easily rotate to new certs as we get them # that we can easily rotate to new certs as we get them
cert.not_valid_after, cert["cert"].not_valid_after,
########################################################### ###########################################################
# We always choose the certificate that is good for the # We always choose the certificate that is good for the
@@ -128,15 +125,15 @@ def get_ssl_certificates(env):
# in case a certificate is installed in multiple paths, # in case a certificate is installed in multiple paths,
# prefer the... lexicographically last one? # prefer the... lexicographically last one?
cert._filename, cert["filename"],
), reverse=True) ), reverse=True)
cert = cert_list.pop(0) cert = cert_list.pop(0)
ret[domain] = { ret[domain] = {
"private-key": cert._private_key._filename, "private-key": cert["private_key"]["filename"],
"certificate": cert._filename, "certificate": cert["filename"],
"primary-domain": cert._primary_domain, "primary-domain": cert["primary_domain"],
"certificate_object": cert, "certificate_object": cert["cert"],
} }
return ret return ret
@@ -538,7 +535,8 @@ def check_certificate(domain, ssl_certificate, ssl_private_key, warn_if_expiring
# Second, check that the certificate matches the private key. # Second, check that the certificate matches the private key.
if ssl_private_key is not None: if ssl_private_key is not None:
try: try:
priv_key = load_pem(open(ssl_private_key, 'rb').read()) with open(ssl_private_key, 'rb') as f:
priv_key = load_pem(f.read())
except ValueError as e: except ValueError as e:
return ("The private key file %s is not a private key file: %s" % (ssl_private_key, str(e)), None) return ("The private key file %s is not a private key file: %s" % (ssl_private_key, str(e)), None)

View File

@@ -95,6 +95,12 @@ def run_services_checks(env, output, pool):
fatal = fatal or fatal2 fatal = fatal or fatal2
output2.playback(output) output2.playback(output)
# Check fail2ban.
code, ret = shell('check_output', ["fail2ban-client", "status"], capture_stderr=True, trap=True)
if code != 0:
output.print_error("fail2ban is not running.")
all_running = False
if all_running: if all_running:
output.print_ok("All system services are running.") output.print_ok("All system services are running.")
@@ -207,7 +213,8 @@ def check_ssh_password(env, output):
# the configuration file. # the configuration file.
if not os.path.exists("/etc/ssh/sshd_config"): if not os.path.exists("/etc/ssh/sshd_config"):
return return
sshd = open("/etc/ssh/sshd_config").read() with open("/etc/ssh/sshd_config", "r") as f:
sshd = f.read()
if re.search("\nPasswordAuthentication\s+yes", sshd) \ if re.search("\nPasswordAuthentication\s+yes", sshd) \
or not re.search("\nPasswordAuthentication\s+no", sshd): or not re.search("\nPasswordAuthentication\s+no", sshd):
output.print_error("""The SSH server on this machine permits password-based login. A more secure output.print_error("""The SSH server on this machine permits password-based login. A more secure
@@ -308,6 +315,8 @@ def run_network_checks(env, output):
output.print_ok("IP address is not blacklisted by zen.spamhaus.org.") output.print_ok("IP address is not blacklisted by zen.spamhaus.org.")
elif zen == "[timeout]": elif zen == "[timeout]":
output.print_warning("Connection to zen.spamhaus.org timed out. We could not determine whether your server's IP address is blacklisted. Please try again later.") output.print_warning("Connection to zen.spamhaus.org timed out. We could not determine whether your server's IP address is blacklisted. Please try again later.")
elif zen == "[Not Set]":
output.print_warning("Could not connect to zen.spamhaus.org. We could not determine whether your server's IP address is blacklisted. Please try again later.")
else: else:
output.print_error("""The IP address of this machine %s is listed in the Spamhaus Block List (code %s), output.print_error("""The IP address of this machine %s is listed in the Spamhaus Block List (code %s),
which may prevent recipients from receiving your email. See http://www.spamhaus.org/query/ip/%s.""" which may prevent recipients from receiving your email. See http://www.spamhaus.org/query/ip/%s."""
@@ -541,7 +550,7 @@ def check_dns_zone(domain, env, output, dns_zonefiles):
for ns in custom_secondary_ns: for ns in custom_secondary_ns:
# We must first resolve the nameserver to an IP address so we can query it. # We must first resolve the nameserver to an IP address so we can query it.
ns_ips = query_dns(ns, "A") ns_ips = query_dns(ns, "A")
if not ns_ips: if not ns_ips or ns_ips in {'[Not Set]', '[timeout]'}:
output.print_error("Secondary nameserver %s is not valid (it doesn't resolve to an IP address)." % ns) output.print_error("Secondary nameserver %s is not valid (it doesn't resolve to an IP address)." % ns)
continue continue
# Choose the first IP if nameserver returns multiple # Choose the first IP if nameserver returns multiple
@@ -592,7 +601,8 @@ def check_dnssec(domain, env, output, dns_zonefiles, is_checking_primary=False):
# record that we suggest using is for the KSK (and that's how the DS records were generated). # record that we suggest using is for the KSK (and that's how the DS records were generated).
# We'll also give the nice name for the key algorithm. # We'll also give the nice name for the key algorithm.
dnssec_keys = load_env_vars_from_file(os.path.join(env['STORAGE_ROOT'], 'dns/dnssec/%s.conf' % alg_name_map[ds_alg])) dnssec_keys = load_env_vars_from_file(os.path.join(env['STORAGE_ROOT'], 'dns/dnssec/%s.conf' % alg_name_map[ds_alg]))
dnsssec_pubkey = open(os.path.join(env['STORAGE_ROOT'], 'dns/dnssec/' + dnssec_keys['KSK'] + '.key')).read().split("\t")[3].split(" ")[3] with open(os.path.join(env['STORAGE_ROOT'], 'dns/dnssec/' + dnssec_keys['KSK'] + '.key'), 'r') as f:
dnsssec_pubkey = f.read().split("\t")[3].split(" ")[3]
expected_ds_records[ (ds_keytag, ds_alg, ds_digalg, ds_digest) ] = { expected_ds_records[ (ds_keytag, ds_alg, ds_digalg, ds_digest) ] = {
"record": rr_ds, "record": rr_ds,
@@ -715,7 +725,7 @@ def check_mail_domain(domain, env, output):
output.print_ok(good_news) output.print_ok(good_news)
# Check MTA-STS policy. # Check MTA-STS policy.
loop = asyncio.get_event_loop() loop = asyncio.new_event_loop()
sts_resolver = postfix_mta_sts_resolver.resolver.STSResolver(loop=loop) sts_resolver = postfix_mta_sts_resolver.resolver.STSResolver(loop=loop)
valid, policy = loop.run_until_complete(sts_resolver.resolve(domain)) valid, policy = loop.run_until_complete(sts_resolver.resolve(domain))
if valid == postfix_mta_sts_resolver.resolver.STSFetchResult.VALID: if valid == postfix_mta_sts_resolver.resolver.STSFetchResult.VALID:
@@ -744,6 +754,8 @@ def check_mail_domain(domain, env, output):
output.print_ok("Domain is not blacklisted by dbl.spamhaus.org.") output.print_ok("Domain is not blacklisted by dbl.spamhaus.org.")
elif dbl == "[timeout]": elif dbl == "[timeout]":
output.print_warning("Connection to dbl.spamhaus.org timed out. We could not determine whether the domain {} is blacklisted. Please try again later.".format(domain)) output.print_warning("Connection to dbl.spamhaus.org timed out. We could not determine whether the domain {} is blacklisted. Please try again later.".format(domain))
elif dbl == "[Not Set]":
output.print_warning("Could not connect to dbl.spamhaus.org. We could not determine whether the domain {} is blacklisted. Please try again later.".format(domain))
else: else:
output.print_error("""This domain is listed in the Spamhaus Domain Block List (code %s), output.print_error("""This domain is listed in the Spamhaus Domain Block List (code %s),
which may prevent recipients from receiving your mail. which may prevent recipients from receiving your mail.
@@ -788,16 +800,21 @@ def query_dns(qname, rtype, nxdomain='[Not Set]', at=None, as_list=False):
# running bind server), or if the 'at' argument is specified, use that host # running bind server), or if the 'at' argument is specified, use that host
# as the nameserver. # as the nameserver.
resolver = dns.resolver.get_default_resolver() resolver = dns.resolver.get_default_resolver()
if at:
# Make sure at is not a string that cannot be used as a nameserver
if at and at not in {'[Not set]', '[timeout]'}:
resolver = dns.resolver.Resolver() resolver = dns.resolver.Resolver()
resolver.nameservers = [at] resolver.nameservers = [at]
# Set a timeout so that a non-responsive server doesn't hold us back. # Set a timeout so that a non-responsive server doesn't hold us back.
resolver.timeout = 5 resolver.timeout = 5
# The number of seconds to spend trying to get an answer to the question. If the
# lifetime expires a dns.exception.Timeout exception will be raised.
resolver.lifetime = 5
# Do the query. # Do the query.
try: try:
response = resolver.query(qname, rtype) response = resolver.resolve(qname, rtype)
except (dns.resolver.NoNameservers, dns.resolver.NXDOMAIN, dns.resolver.NoAnswer): except (dns.resolver.NoNameservers, dns.resolver.NXDOMAIN, dns.resolver.NoAnswer):
# Host did not have an answer for this query; not sure what the # Host did not have an answer for this query; not sure what the
# difference is between the two exceptions. # difference is between the two exceptions.
@@ -895,11 +912,11 @@ def list_apt_updates(apt_update=True):
return pkgs return pkgs
def what_version_is_this(env): def what_version_is_this(env):
# This function runs `git describe --abbrev=0` on the Mail-in-a-Box installation directory. # This function runs `git describe --always --abbrev=0` 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, # 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. # so this function may raise all sorts of exceptions.
miab_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) miab_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
tag = shell("check_output", ["/usr/bin/git", "describe", "--abbrev=0"], env={"GIT_DIR": os.path.join(miab_dir, '.git')}).strip() tag = shell("check_output", ["/usr/bin/git", "describe", "--always", "--abbrev=0"], env={"GIT_DIR": os.path.join(miab_dir, '.git')}).strip()
return tag return tag
def get_latest_miab_version(): def get_latest_miab_version():
@@ -947,7 +964,8 @@ def run_and_output_changes(env, pool):
# Load previously saved status checks. # Load previously saved status checks.
cache_fn = "/var/cache/mailinabox/status_checks.json" cache_fn = "/var/cache/mailinabox/status_checks.json"
if os.path.exists(cache_fn): if os.path.exists(cache_fn):
prev = json.load(open(cache_fn)) with open(cache_fn, 'r') as f:
prev = json.load(f)
# Group the serial output into categories by the headings. # Group the serial output into categories by the headings.
def group_by_heading(lines): def group_by_heading(lines):

View File

@@ -7,7 +7,7 @@
<h3>Add a mail alias</h3> <h3>Add a mail alias</h3>
<p>Aliases are email forwarders. An alias can forward email to a <a href="#" onclick="return show_panel('users')">mail user</a> or to any email address.</p> <p>Aliases are email forwarders. An alias can forward email to a <a href="#users">mail user</a> or to any email address.</p>
<p>To use an alias or any address besides your own login username in outbound mail, the sending user must be included as a permitted sender for the alias.</p> <p>To use an alias or any address besides your own login username in outbound mail, the sending user must be included as a permitted sender for the alias.</p>

View File

@@ -77,7 +77,7 @@
<h3>Using a secondary nameserver</h3> <h3>Using a secondary nameserver</h3>
<p>If your TLD requires you to have two separate nameservers, you can either set up <a href="#" onclick="return show_panel('external_dns')">external DNS</a> and ignore the DNS server on this box entirely, or use the DNS server on this box but add a secondary (aka &ldquo;slave&rdquo;) nameserver.</p> <p>If your TLD requires you to have two separate nameservers, you can either set up <a href="#external_dns">external DNS</a> and ignore the DNS server on this box entirely, or use the DNS server on this box but add a secondary (aka &ldquo;slave&rdquo;) nameserver.</p>
<p>If you choose to use a secondary nameserver, you must find a secondary nameserver service provider. Your domain name registrar or virtual cloud provider may provide this service for you. Once you set up the secondary nameserver service, enter the hostname (not the IP address) of <em>their</em> secondary nameserver in the box below.</p> <p>If you choose to use a secondary nameserver, you must find a secondary nameserver service provider. Your domain name registrar or virtual cloud provider may provide this service for you. Once you set up the secondary nameserver service, enter the hostname (not the IP address) of <em>their</em> secondary nameserver in the box below.</p>
<form class="form-horizontal" role="form" onsubmit="do_set_secondary_dns(); return false;"> <form class="form-horizontal" role="form" onsubmit="do_set_secondary_dns(); return false;">
@@ -96,7 +96,7 @@
<div class="col-sm-offset-1 col-sm-11"> <div class="col-sm-offset-1 col-sm-11">
<p class="small"> <p class="small">
Multiple secondary servers can be separated with commas or spaces (i.e., <code>ns2.hostingcompany.com ns3.hostingcompany.com</code>). Multiple secondary servers can be separated with commas or spaces (i.e., <code>ns2.hostingcompany.com ns3.hostingcompany.com</code>).
To enable zone transfers to additional servers without listing them as secondary nameservers, add an IP address or subnet using <code>xfr:10.20.30.40</code> or <code>xfr:10.0.0.0/8</code>. To enable zone transfers to additional servers without listing them as secondary nameservers, prefix a hostname, IP address, or subnet with <code>xfr:</code>, e.g. <code>xfr:10.20.30.40</code> or <code>xfr:10.0.0.0/8</code>.
</p> </p>
<p id="secondarydns-clear-instructions" style="display: none" class="small"> <p id="secondarydns-clear-instructions" style="display: none" class="small">
Clear the input field above and click Update to use this machine itself as secondary DNS, which is the default/normal setup. Clear the input field above and click Update to use this machine itself as secondary DNS, which is the default/normal setup.

View File

@@ -11,9 +11,9 @@
<link rel="stylesheet" href="/admin/assets/bootstrap/css/bootstrap.min.css"> <link rel="stylesheet" href="/admin/assets/bootstrap/css/bootstrap.min.css">
<style> <style>
body { body {
overflow-y: scroll; overflow-y: scroll;
padding-bottom: 20px; padding-bottom: 20px;
} }
p { p {
@@ -36,20 +36,20 @@
margin-bottom: 13px; margin-bottom: 13px;
margin-top: 30px; margin-top: 30px;
} }
.panel-heading h3 { .panel-heading h3 {
border: none; border: none;
padding: 0; padding: 0;
margin: 0; margin: 0;
} }
h4 { h4 {
font-size: 110%; font-size: 110%;
margin-bottom: 13px; margin-bottom: 13px;
margin-top: 18px; margin-top: 18px;
} }
h4:first-child { h4:first-child {
margin-top: 6px; margin-top: 6px;
} }
.admin_panel { .admin_panel {
display: none; display: none;
@@ -59,10 +59,10 @@
margin: 1.5em 0; margin: 1.5em 0;
} }
ol li { ol li {
margin-bottom: 1em; margin-bottom: 1em;
} }
.if-logged-in { display: none; } .if-logged-in { display: none; }
.if-logged-in-admin { display: none; } .if-logged-in-admin { display: none; }
@@ -73,21 +73,16 @@
filter: invert(100%) hue-rotate(180deg); filter: invert(100%) hue-rotate(180deg);
} }
/* Set explicit background color (necessary for Firefox) */
html {
background-color: #111;
}
/* Override Boostrap theme here to give more contrast. The black turns to white by the filter. */ /* Override Boostrap theme here to give more contrast. The black turns to white by the filter. */
.form-control { .form-control {
color: black !important; color: black !important;
} }
/* Revert the invert for the navbar */ /* Revert the invert for the navbar */
button, div.navbar { button, div.navbar {
filter: invert(100%) hue-rotate(180deg); filter: invert(100%) hue-rotate(180deg);
} }
/* Revert the revert for the dropdowns */ /* Revert the revert for the dropdowns */
ul.dropdown-menu { ul.dropdown-menu {
filter: invert(100%) hue-rotate(180deg); filter: invert(100%) hue-rotate(180deg);
@@ -117,30 +112,30 @@
<li class="dropdown if-logged-in-admin"> <li class="dropdown if-logged-in-admin">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">System <b class="caret"></b></a> <a href="#" class="dropdown-toggle" data-toggle="dropdown">System <b class="caret"></b></a>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
<li><a href="#system_status" onclick="return show_panel(this);">Status Checks</a></li> <li><a href="#system_status">Status Checks</a></li>
<li><a href="#tls" onclick="return show_panel(this);">TLS (SSL) Certificates</a></li> <li><a href="#tls">TLS (SSL) Certificates</a></li>
<li><a href="#system_backup" onclick="return show_panel(this);">Backup Status</a></li> <li><a href="#system_backup">Backup Status</a></li>
<li class="divider"></li> <li class="divider"></li>
<li class="dropdown-header">Advanced Pages</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">Custom DNS</a></li>
<li><a href="#external_dns" onclick="return show_panel(this);">External DNS</a></li> <li><a href="#external_dns">External DNS</a></li>
<li><a href="#munin" onclick="return show_panel(this);">Munin Monitoring</a></li> <li><a href="#munin">Munin Monitoring</a></li>
</ul> </ul>
</li> </li>
<li><a href="#mail-guide" onclick="return show_panel(this);" class="if-logged-in-not-admin">Mail</a></li> <li><a href="#mail-guide" class="if-logged-in-not-admin">Mail</a></li>
<li class="dropdown if-logged-in-admin"> <li class="dropdown if-logged-in-admin">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">Mail &amp; Users <b class="caret"></b></a> <a href="#" class="dropdown-toggle" data-toggle="dropdown">Mail &amp; Users <b class="caret"></b></a>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
<li><a href="#mail-guide" onclick="return show_panel(this);">Instructions</a></li> <li><a href="#mail-guide">Instructions</a></li>
<li><a href="#users" onclick="return show_panel(this);">Users</a></li> <li><a href="#users">Users</a></li>
<li><a href="#aliases" onclick="return show_panel(this);">Aliases</a></li> <li><a href="#aliases">Aliases</a></li>
<li class="divider"></li> <li class="divider"></li>
<li class="dropdown-header">Your Account</li> <li class="dropdown-header">Your Account</li>
<li><a href="#mfa" onclick="return show_panel(this);">Two-Factor Authentication</a></li> <li><a href="#mfa">Two-Factor Authentication</a></li>
</ul> </ul>
</li> </li>
<li><a href="#sync_guide" onclick="return show_panel(this);" class="if-logged-in">Contacts/Calendar</a></li> <li><a href="#sync_guide" class="if-logged-in">Contacts/Calendar</a></li>
<li><a href="#web" onclick="return show_panel(this);" class="if-logged-in-admin">Web</a></li> <li><a href="#web" class="if-logged-in-admin">Web</a></li>
</ul> </ul>
<ul class="nav navbar-nav navbar-right"> <ul class="nav navbar-nav navbar-right">
<li class="if-logged-in"><a href="#" onclick="do_logout(); return false;" style="color: white">Log out</a></li> <li class="if-logged-in"><a href="#" onclick="do_logout(); return false;" style="color: white">Log out</a></li>
@@ -426,23 +421,25 @@ function do_logout() {
} }
function show_panel(panelid) { function show_panel(panelid) {
if (panelid.getAttribute) if (panelid.getAttribute) {
// we might be passed an HTMLElement <a>. // we might be passed an HTMLElement <a>.
panelid = panelid.getAttribute('href').substring(1); panelid = panelid.getAttribute('href').substring(1);
}
$('.admin_panel').hide(); $('.admin_panel').hide();
$('#panel_' + panelid).show(); $('#panel_' + panelid).show();
if (typeof localStorage != 'undefined')
localStorage.setItem("miab-cp-lastpanel", panelid);
if (window["show_" + panelid]) if (window["show_" + panelid])
window["show_" + panelid](); window["show_" + panelid]();
current_panel = panelid; current_panel = panelid;
switch_back_to_panel = null; switch_back_to_panel = null;
return false; // when called from onclick, cancel navigation
} }
window.onhashchange = function() {
var panelid = window.location.hash.substring(1);
show_panel(panelid);
};
$(function() { $(function() {
// Recall saved user credentials. // Recall saved user credentials.
try { try {
@@ -457,8 +454,9 @@ $(function() {
show_hide_menus(); show_hide_menus();
// Recall what the user was last looking at. // Recall what the user was last looking at.
if (api_credentials != null && typeof localStorage != 'undefined' && localStorage.getItem("miab-cp-lastpanel")) { if (api_credentials != null && window.location.hash) {
show_panel(localStorage.getItem("miab-cp-lastpanel")); var panelid = window.location.hash.substring(1);
show_panel(panelid);
} else if (api_credentials != null) { } else if (api_credentials != null) {
show_panel('welcome'); show_panel('welcome');
} else { } else {

View File

@@ -168,7 +168,18 @@ function do_login() {
// Open the next panel the user wants to go to. Do this after the XHR response // Open the next panel the user wants to go to. Do this after the XHR response
// is over so that we don't start a new XHR request while this one is finishing, // is over so that we don't start a new XHR request while this one is finishing,
// which confuses the loading indicator. // which confuses the loading indicator.
setTimeout(function() { show_panel(!switch_back_to_panel || switch_back_to_panel == "login" ? 'welcome' : switch_back_to_panel) }, 300); setTimeout(function() {
if (window.location.hash) {
var panelid = window.location.hash.substring(1);
show_panel(panelid);
} else {
show_panel(
!switch_back_to_panel || switch_back_to_panel == "login"
? 'welcome'
: switch_back_to_panel)
}
}, 300);
} }
}, },
undefined, undefined,

View File

@@ -36,7 +36,7 @@
<tr><th>Password:</th> <td>Your mail password.</td></tr> <tr><th>Password:</th> <td>Your mail password.</td></tr>
</table> </table>
<p>In addition to setting up your email, you&rsquo;ll also need to set up <a href="#sync_guide" onclick="return show_panel(this);">contacts and calendar synchronization</a> separately.</p> <p>In addition to setting up your email, you&rsquo;ll also need to set up <a href="#sync_guide">contacts and calendar synchronization</a> separately.</p>
<p>As an alternative to IMAP you can also use the POP protocol: choose POP as the protocol, port 995, and SSL or TLS security in your mail client. The SMTP settings and usernames and passwords remain the same. However, we recommend you use IMAP instead.</p> <p>As an alternative to IMAP you can also use the POP protocol: choose POP as the protocol, port 995, and SSL or TLS security in your mail client. The SMTP settings and usernames and passwords remain the same. However, we recommend you use IMAP instead.</p>

View File

@@ -17,14 +17,14 @@
<tr><th>Calendar</td> <td><a href="https://{{hostname}}/cloud/calendar">https://{{hostname}}/cloud/calendar</a></td></tr> <tr><th>Calendar</td> <td><a href="https://{{hostname}}/cloud/calendar">https://{{hostname}}/cloud/calendar</a></td></tr>
</table> </table>
<p>Log in settings are the same as with <a href="#mail-guide" onclick="return show_panel(this);">mail</a>: your <p>Log in settings are the same as with <a href="#mail-guide">mail</a>: your
complete email address and your mail password.</p> complete email address and your mail password.</p>
</div> </div>
<div class="col-sm-6"> <div class="col-sm-6">
<h4>On your mobile device</h4> <h4>On your mobile device</h4>
<p>If you set up your <a href="#mail-guide" onclick="return show_panel(this);">mail</a> using Exchange/ActiveSync, <p>If you set up your <a href="#mail-guide">mail</a> using Exchange/ActiveSync,
your contacts and calendar may already appear on your device.</p> your contacts and calendar may already appear on your device.</p>
<p>Otherwise, here are some apps that can synchronize your contacts and calendar to your Android phone.</p> <p>Otherwise, here are some apps that can synchronize your contacts and calendar to your Android phone.</p>

View File

@@ -5,7 +5,7 @@
<h2>Backup Status</h2> <h2>Backup Status</h2>
<p>The box makes an incremental backup each night. By default the backup is stored on the machine itself, but you can also store it on S3-compatible services like Amazon Web Services (AWS).</p> <p>The box makes an incremental backup each night. You can store the backup on any Amazon Web Services S3-compatible service, or other options.</p>
<h3>Configuration</h3> <h3>Configuration</h3>
@@ -45,6 +45,10 @@
<label for="backup-target-rsync-host" class="col-sm-2 control-label">Hostname</label> <label for="backup-target-rsync-host" class="col-sm-2 control-label">Hostname</label>
<div class="col-sm-8"> <div class="col-sm-8">
<input type="text" placeholder="hostname.local" class="form-control" rows="1" id="backup-target-rsync-host"> <input type="text" placeholder="hostname.local" class="form-control" rows="1" id="backup-target-rsync-host">
<div class="small" style="margin-top: 2px">
The hostname at your rsync provider, e.g. <tt>da2327.rsync.net</tt>. Optionally includes a colon
and the provider's non-standard ssh port number, e.g. <tt>u215843.your-storagebox.de:23</tt>.
</div>
</div> </div>
</div> </div>
<div class="form-group backup-target-rsync"> <div class="form-group backup-target-rsync">
@@ -66,9 +70,12 @@
<div class="small" style="margin-top: 2px"> <div class="small" style="margin-top: 2px">
Copy the Public SSH Key above, and paste it within the <tt>~/.ssh/authorized_keys</tt> Copy the Public SSH Key above, and paste it within the <tt>~/.ssh/authorized_keys</tt>
of target user on the backup server specified above. That way you'll enable secure and of target user on the backup server specified above. That way you'll enable secure and
passwordless authentication from your mail-in-a-box server and your backup server. passwordless authentication from your Mail-in-a-Box server and your backup server.
</div> </div>
</div> </div>
<div id="copy_pub_key_div" class="col-sm">
<button type="button" class="btn btn-small" onclick="copy_pub_key_to_clipboard()">Copy</button>
</div>
</div> </div>
<!-- S3 BACKUP --> <!-- S3 BACKUP -->
<div class="form-group backup-target-s3"> <div class="form-group backup-target-s3">
@@ -91,13 +98,19 @@
<div class="form-group backup-target-s3"> <div class="form-group backup-target-s3">
<label for="backup-target-s3-host" class="col-sm-2 control-label">S3 Host / Endpoint</label> <label for="backup-target-s3-host" class="col-sm-2 control-label">S3 Host / Endpoint</label>
<div class="col-sm-8"> <div class="col-sm-8">
<input type="text" placeholder="Endpoint" class="form-control" rows="1" id="backup-target-s3-host"> <input type="text" placeholder="https://s3.backuphost.com" class="form-control" rows="1" id="backup-target-s3-host">
</div> </div>
</div> </div>
<div class="form-group backup-target-s3"> <div class="form-group backup-target-s3">
<label for="backup-target-s3-path" class="col-sm-2 control-label">S3 Path</label> <label for="backup-target-s3-region-name" class="col-sm-2 control-label">S3 Region Name <span style="font-weight: normal">(if required)</span></label>
<div class="col-sm-8"> <div class="col-sm-8">
<input type="text" placeholder="your-bucket-name/backup-directory" class="form-control" rows="1" id="backup-target-s3-path"> <input type="text" placeholder="region.name" class="form-control" rows="1" id="backup-target-s3-region-name">
</div>
</div>
<div class="form-group backup-target-s3">
<label for="backup-target-s3-path" class="col-sm-2 control-label">S3 Bucket &amp; Path</label>
<div class="col-sm-8">
<input type="text" placeholder="bucket-name/backup-directory" class="form-control" rows="1" id="backup-target-s3-path">
</div> </div>
</div> </div>
<div class="form-group backup-target-s3"> <div class="form-group backup-target-s3">
@@ -259,18 +272,18 @@ function show_custom_backup() {
} else if (r.target == "off") { } else if (r.target == "off") {
$("#backup-target-type").val("off"); $("#backup-target-type").val("off");
} else if (r.target.substring(0, 8) == "rsync://") { } else if (r.target.substring(0, 8) == "rsync://") {
$("#backup-target-type").val("rsync"); const spec = url_split(r.target);
var path = r.target.substring(8).split('//'); $("#backup-target-type").val(spec.scheme);
var host_parts = path.shift().split('@'); $("#backup-target-rsync-user").val(spec.user);
$("#backup-target-rsync-user").val(host_parts[0]); $("#backup-target-rsync-host").val(spec.host);
$("#backup-target-rsync-host").val(host_parts[1]); $("#backup-target-rsync-path").val(spec.path);
$("#backup-target-rsync-path").val('/'+path[0]);
} else if (r.target.substring(0, 5) == "s3://") { } else if (r.target.substring(0, 5) == "s3://") {
const spec = url_split(r.target);
$("#backup-target-type").val("s3"); $("#backup-target-type").val("s3");
var hostpath = r.target.substring(5).split('/'); $("#backup-target-s3-host-select").val(spec.host);
var host = hostpath.shift(); $("#backup-target-s3-host").val(spec.host);
$("#backup-target-s3-host").val(host); $("#backup-target-s3-region-name").val(spec.user); // stuffing the region name in the username
$("#backup-target-s3-path").val(hostpath.join('/')); $("#backup-target-s3-path").val(spec.path);
} else if (r.target.substring(0, 5) == "b2://") { } else if (r.target.substring(0, 5) == "b2://") {
$("#backup-target-type").val("b2"); $("#backup-target-type").val("b2");
var targetPath = r.target.substring(5); var targetPath = r.target.substring(5);
@@ -278,7 +291,7 @@ function show_custom_backup() {
var b2_applicationkey = targetPath.split(':')[1].split('@')[0]; var b2_applicationkey = targetPath.split(':')[1].split('@')[0];
var b2_bucket = targetPath.split('@')[1]; var b2_bucket = targetPath.split('@')[1];
$("#backup-target-b2-user").val(b2_application_keyid); $("#backup-target-b2-user").val(b2_application_keyid);
$("#backup-target-b2-pass").val(b2_applicationkey); $("#backup-target-b2-pass").val(decodeURIComponent(b2_applicationkey));
$("#backup-target-b2-bucket").val(b2_bucket); $("#backup-target-b2-bucket").val(b2_bucket);
} }
toggle_form() toggle_form()
@@ -294,13 +307,16 @@ function set_custom_backup() {
if (target_type == "local" || target_type == "off") if (target_type == "local" || target_type == "off")
target = target_type; target = target_type;
else if (target_type == "s3") else if (target_type == "s3")
target = "s3://" + $("#backup-target-s3-host").val() + "/" + $("#backup-target-s3-path").val(); target = "s3://"
+ ($("#backup-target-s3-region-name").val() ? ($("#backup-target-s3-region-name").val() + "@") : "")
+ $("#backup-target-s3-host").val()
+ "/" + $("#backup-target-s3-path").val();
else if (target_type == "rsync") { else if (target_type == "rsync") {
target = "rsync://" + $("#backup-target-rsync-user").val() + "@" + $("#backup-target-rsync-host").val() target = "rsync://" + $("#backup-target-rsync-user").val() + "@" + $("#backup-target-rsync-host").val()
+ "/" + $("#backup-target-rsync-path").val(); + "/" + $("#backup-target-rsync-path").val();
target_user = ''; target_user = '';
} else if (target_type == "b2") { } else if (target_type == "b2") {
target = 'b2://' + $('#backup-target-b2-user').val() + ':' + $('#backup-target-b2-pass').val() target = 'b2://' + $('#backup-target-b2-user').val() + ':' + encodeURIComponent($('#backup-target-b2-pass').val())
+ '@' + $('#backup-target-b2-bucket').val() + '@' + $('#backup-target-b2-bucket').val()
target_user = ''; target_user = '';
target_pass = ''; target_pass = '';
@@ -343,4 +359,42 @@ function init_inputs(target_type) {
set_host($('#backup-target-s3-host-select').val()); set_host($('#backup-target-s3-host-select').val());
} }
} }
// Return a two-element array of the substring preceding and the substring following
// the first occurence of separator in string. Return [undefined, string] if the
// separator does not appear in string.
const split1_rest = (string, separator) => {
const index = string.indexOf(separator);
return (index >= 0) ? [string.substring(0, index), string.substring(index + separator.length)] : [undefined, string];
};
// Note: The manifest JS URL class does not work in some security-conscious
// settings, e.g. Brave browser, so we roll our own that handles only what we need.
//
// Use greedy separator parsing to get parts of a MIAB backup target url.
// Note: path will not include a leading forward slash '/'
const url_split = url => {
const [ scheme, scheme_rest ] = split1_rest(url, '://');
const [ user, user_rest ] = split1_rest(scheme_rest, '@');
const [ host, path ] = split1_rest(user_rest, '/');
return {
scheme,
user,
host,
path,
}
};
// Hide Copy button if not in a modern clipboard-supporting environment.
// Using document API because jQuery is not necessarily available in this script scope.
if (!(navigator && navigator.clipboard && navigator.clipboard.writeText)) {
document.getElementById('copy_pub_key_div').hidden = true;
}
function copy_pub_key_to_clipboard() {
const ssh_pub_key = $("#ssh-pub-key").val();
navigator.clipboard.writeText(ssh_pub_key);
}
</script> </script>

View File

@@ -10,13 +10,13 @@
border-top: none; border-top: none;
padding-top: 0; padding-top: 0;
} }
#system-checks .status-error td { #system-checks .status-error td, .summary-error {
color: #733; color: #733;
} }
#system-checks .status-warning td { #system-checks .status-warning td, .summary-warning {
color: #770; color: #770;
} }
#system-checks .status-ok td { #system-checks .status-ok td, .summary-ok {
color: #040; color: #040;
} }
#system-checks div.extra { #system-checks div.extra {
@@ -52,6 +52,9 @@
</div> <!-- /col --> </div> <!-- /col -->
<div class="col-md-pull-3 col-md-8"> <div class="col-md-pull-3 col-md-8">
<div id="system-checks-summary">
</div>
<table id="system-checks" class="table" style="max-width: 60em"> <table id="system-checks" class="table" style="max-width: 60em">
<thead> <thead>
</thead> </thead>
@@ -64,6 +67,9 @@
<script> <script>
function show_system_status() { function show_system_status() {
const summary = $('#system-checks-summary');
summary.html("");
$('#system-checks tbody').html("<tr><td colspan='2' class='text-muted'>Loading...</td></tr>") $('#system-checks tbody').html("<tr><td colspan='2' class='text-muted'>Loading...</td></tr>")
api( api(
@@ -93,6 +99,12 @@ function show_system_status() {
{ }, { },
function(r) { function(r) {
$('#system-checks tbody').html(""); $('#system-checks tbody').html("");
const ok_symbol = "✓";
const error_symbol = "✖";
const warning_symbol = "?";
let count_by_status = { ok: 0, error: 0, warning: 0 };
for (var i = 0; i < r.length; i++) { for (var i = 0; i < r.length; i++) {
var n = $("<tr><td class='status'/><td class='message'><p style='margin: 0'/><div class='extra'/><a class='showhide' href='#'/></tr>"); var n = $("<tr><td class='status'/><td class='message'><p style='margin: 0'/><div class='extra'/><a class='showhide' href='#'/></tr>");
if (i == 0) n.addClass('first') if (i == 0) n.addClass('first')
@@ -100,9 +112,12 @@ function show_system_status() {
n.addClass(r[i].type) n.addClass(r[i].type)
else else
n.addClass("status-" + r[i].type) n.addClass("status-" + r[i].type)
if (r[i].type == "ok") n.find('td.status').text("✓")
if (r[i].type == "error") n.find('td.status').text("✖") if (r[i].type == "ok") n.find('td.status').text(ok_symbol);
if (r[i].type == "warning") n.find('td.status').text("?") if (r[i].type == "error") n.find('td.status').text(error_symbol);
if (r[i].type == "warning") n.find('td.status').text(warning_symbol);
count_by_status[r[i].type]++;
n.find('td.message p').text(r[i].text) n.find('td.message p').text(r[i].text)
$('#system-checks tbody').append(n); $('#system-checks tbody').append(n);
@@ -122,8 +137,17 @@ function show_system_status() {
n.find('> td.message > div').append(m); n.find('> td.message > div').append(m);
} }
} }
})
// Summary counts
summary.html("Summary: ");
if (count_by_status['error'] + count_by_status['warning'] == 0) {
summary.append($('<span class="summary-ok"/>').text(`All ${count_by_status['ok']} ${ok_symbol} OK`));
} else {
summary.append($('<span class="summary-ok"/>').text(`${count_by_status['ok']} ${ok_symbol} OK, `));
summary.append($('<span class="summary-error"/>').text(`${count_by_status['error']} ${error_symbol} Error, `));
summary.append($('<span class="summary-warning"/>').text(`${count_by_status['warning']} ${warning_symbol} Warning`));
}
})
} }
var current_privacy_setting = null; var current_privacy_setting = null;

View File

@@ -31,9 +31,9 @@
</form> </form>
<ul style="margin-top: 1em; padding-left: 1.5em; font-size: 90%;"> <ul style="margin-top: 1em; padding-left: 1.5em; font-size: 90%;">
<li>Passwords must be at least eight characters consisting of English letters and numbers only. For best results, <a href="#" onclick="return generate_random_password()">generate a random password</a>.</li> <li>Passwords must be at least eight characters consisting of English letters and numbers only. For best results, <a href="#" onclick="return generate_random_password()">generate a random password</a>.</li>
<li>Use <a href="#" onclick="return show_panel('aliases')">aliases</a> to create email addresses that forward to existing accounts.</li> <li>Use <a href="#aliases">aliases</a> to create email addresses that forward to existing accounts.</li>
<li>Administrators get access to this control panel.</li> <li>Administrators get access to this control panel.</li>
<li>User accounts cannot contain any international (non-ASCII) characters, but <a href="#" onclick="return show_panel('aliases');">aliases</a> can.</li> <li>User accounts cannot contain any international (non-ASCII) characters, but <a href="#aliases">aliases</a> can.</li>
</ul> </ul>
<h3>Existing mail users</h3> <h3>Existing mail users</h3>

View File

@@ -10,7 +10,7 @@
<p>You can replace the default website with your own HTML pages and other static files. This control panel won&rsquo;t help you design a website, but once you have <tt>.html</tt> files you can upload them following these instructions:</p> <p>You can replace the default website with your own HTML pages and other static files. This control panel won&rsquo;t help you design a website, but once you have <tt>.html</tt> files you can upload them following these instructions:</p>
<ol> <ol>
<li>Ensure that any domains you are publishing a website for have no problems on the <a href="#system_status" onclick="return show_panel(this);">Status Checks</a> page.</li> <li>Ensure that any domains you are publishing a website for have no problems on the <a href="#system_status">Status Checks</a> page.</li>
<li>On your personal computer, install an SSH file transfer program such as <a href="https://filezilla-project.org/">FileZilla</a> or <a href="http://linuxcommand.org/man_pages/scp1.html">scp</a>.</li> <li>On your personal computer, install an SSH file transfer program such as <a href="https://filezilla-project.org/">FileZilla</a> or <a href="http://linuxcommand.org/man_pages/scp1.html">scp</a>.</li>
@@ -32,7 +32,7 @@
</tbody> </tbody>
</table> </table>
<p>To add a domain to this table, create a dummy <a href="#users" onclick="return show_panel(this);">mail user</a> or <a href="#aliases" onclick="return show_panel(this);">alias</a> on the domain first and see the <a href="https://mailinabox.email/guide.html#domain-name-configuration">setup guide</a> for adding nameserver records to the new domain at your registrar (but <i>not</i> glue records).</p> <p>To add a domain to this table, create a dummy <a href="#users">mail user</a> or <a href="#aliases">alias</a> on the domain first and see the <a href="https://mailinabox.email/guide.html#domain-name-configuration">setup guide</a> for adding nameserver records to the new domain at your registrar (but <i>not</i> glue records).</p>
</ol> </ol>

View File

@@ -14,7 +14,9 @@ def load_env_vars_from_file(fn):
# Load settings from a KEY=VALUE file. # Load settings from a KEY=VALUE file.
import collections import collections
env = collections.OrderedDict() env = collections.OrderedDict()
for line in open(fn): env.setdefault(*line.strip().split("=", 1)) with open(fn, 'r') as f:
for line in f:
env.setdefault(*line.strip().split("=", 1))
return env return env
def save_environment(env): def save_environment(env):
@@ -34,7 +36,8 @@ def load_settings(env):
import rtyaml import rtyaml
fn = os.path.join(env['STORAGE_ROOT'], 'settings.yaml') fn = os.path.join(env['STORAGE_ROOT'], 'settings.yaml')
try: try:
config = rtyaml.load(open(fn, "r")) with open(fn, "r") as f:
config = rtyaml.load(f)
if not isinstance(config, dict): raise ValueError() # caught below if not isinstance(config, dict): raise ValueError() # caught below
return config return config
except: except:
@@ -175,14 +178,6 @@ def wait_for_service(port, public, env, timeout):
return False return False
time.sleep(min(timeout/4, 1)) time.sleep(min(timeout/4, 1))
def fix_boto():
# Google Compute Engine instances install some Python-2-only boto plugins that
# conflict with boto running under Python 3. Disable boto's default configuration
# file prior to importing boto so that GCE's plugin is not loaded:
import os
os.environ["BOTO_CONFIG"] = "/etc/boto3.cfg"
if __name__ == "__main__": if __name__ == "__main__":
from web_update import get_web_domains from web_update import get_web_domains
env = load_environment() env = load_environment()

View File

@@ -63,7 +63,8 @@ def get_web_domains_with_root_overrides(env):
root_overrides = { } root_overrides = { }
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")
if os.path.exists(nginx_conf_custom_fn): if os.path.exists(nginx_conf_custom_fn):
custom_settings = rtyaml.load(open(nginx_conf_custom_fn)) with open(nginx_conf_custom_fn, 'r') as f:
custom_settings = rtyaml.load(f)
for domain, settings in custom_settings.items(): for domain, settings in custom_settings.items():
for type, value in [('redirect', settings.get('redirects', {}).get('/')), for type, value in [('redirect', settings.get('redirects', {}).get('/')),
('proxy', settings.get('proxies', {}).get('/'))]: ('proxy', settings.get('proxies', {}).get('/'))]:
@@ -75,13 +76,18 @@ def do_web_update(env):
# Pre-load what SSL certificates we will use for each domain. # Pre-load what SSL certificates we will use for each domain.
ssl_certificates = get_ssl_certificates(env) ssl_certificates = get_ssl_certificates(env)
# Helper for reading config files and templates
def read_conf(conf_fn):
with open(os.path.join(os.path.dirname(__file__), "../conf", conf_fn), "r") as f:
return f.read()
# 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 = read_conf("nginx-top.conf")
# Load the templates. # Load the templates.
template0 = open(os.path.join(os.path.dirname(__file__), "../conf/nginx.conf")).read() template0 = read_conf("nginx.conf")
template1 = open(os.path.join(os.path.dirname(__file__), "../conf/nginx-alldomains.conf")).read() template1 = read_conf("nginx-alldomains.conf")
template2 = open(os.path.join(os.path.dirname(__file__), "../conf/nginx-primaryonly.conf")).read() template2 = read_conf("nginx-primaryonly.conf")
template3 = "\trewrite ^(.*) https://$REDIRECT_DOMAIN$1 permanent;\n" template3 = "\trewrite ^(.*) https://$REDIRECT_DOMAIN$1 permanent;\n"
# Add the PRIMARY_HOST configuration first so it becomes nginx's default server. # Add the PRIMARY_HOST configuration first so it becomes nginx's default server.
@@ -141,11 +147,8 @@ def make_domain_config(domain, templates, ssl_certificates, env):
def hashfile(filepath): def hashfile(filepath):
import hashlib import hashlib
sha1 = hashlib.sha1() sha1 = hashlib.sha1()
f = open(filepath, 'rb') with open(filepath, 'rb') as f:
try:
sha1.update(f.read()) sha1.update(f.read())
finally:
f.close()
return sha1.hexdigest() return sha1.hexdigest()
nginx_conf_extra += "\t# ssl files sha1: %s / %s\n" % (hashfile(tls_cert["private-key"]), hashfile(tls_cert["certificate"])) nginx_conf_extra += "\t# ssl files sha1: %s / %s\n" % (hashfile(tls_cert["private-key"]), hashfile(tls_cert["certificate"]))
@@ -153,7 +156,8 @@ def make_domain_config(domain, templates, ssl_certificates, env):
hsts = "yes" hsts = "yes"
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")
if os.path.exists(nginx_conf_custom_fn): if os.path.exists(nginx_conf_custom_fn):
yaml = rtyaml.load(open(nginx_conf_custom_fn)) with open(nginx_conf_custom_fn, 'r') as f:
yaml = rtyaml.load(f)
if domain in yaml: if domain in yaml:
yaml = yaml[domain] yaml = yaml[domain]

7
management/wsgi.py Normal file
View File

@@ -0,0 +1,7 @@
from daemon import app
import auth, utils
app.logger.addHandler(utils.create_syslog_handler())
if __name__ == "__main__":
app.run(port=10222)

View File

@@ -1,7 +1,7 @@
Mail-in-a-Box Security Guide Mail-in-a-Box Security Guide
============================ ============================
Mail-in-a-Box turns a fresh Ubuntu 18.04 LTS 64-bit machine into a mail server appliance by installing and configuring various components. Mail-in-a-Box turns a fresh Ubuntu 22.04 LTS 64-bit machine into a mail server appliance by installing and configuring various components.
This page documents the security posture of Mail-in-a-Box. The term “box” is used below to mean a configured Mail-in-a-Box. This page documents the security posture of Mail-in-a-Box. The term “box” is used below to mean a configured Mail-in-a-Box.

View File

@@ -9,32 +9,37 @@
if [ -z "$TAG" ]; then if [ -z "$TAG" ]; then
# If a version to install isn't explicitly given as an environment # If a version to install isn't explicitly given as an environment
# variable, then install the latest version. But the latest version # variable, then install the latest version. But the latest version
# depends on the operating system. Existing Ubuntu 14.04 users need # depends on the machine's version of Ubuntu. Existing users need to
# to be able to upgrade to the latest version supporting Ubuntu 14.04, # be able to upgrade to the latest version available for that version
# in part because an upgrade is required before jumping to Ubuntu 18.04. # of Ubuntu to satisfy the migration requirements.
# New users on Ubuntu 18.04 need to get the latest version number too.
# #
# Also, the system status checks read this script for TAG = (without the # Also, the system status checks read this script for TAG = (without the
# space, but if we put it in a comment it would confuse the status checks!) # space, but if we put it in a comment it would confuse the status checks!)
# to get the latest version, so the first such line must be the one that we # to get the latest version, so the first such line must be the one that we
# want to display in status checks. # want to display in status checks.
if [ "$(lsb_release -d | sed 's/.*:\s*//' | sed 's/18\.04\.[0-9]/18.04/' )" == "Ubuntu 18.04 LTS" ]; then #
# This machine is running Ubuntu 18.04. # Allow point-release versions of the major releases, e.g. 22.04.1 is OK.
UBUNTU_VERSION=$( lsb_release -d | sed 's/.*:\s*//' | sed 's/\([0-9]*\.[0-9]*\)\.[0-9]/\1/' )
if [ "$UBUNTU_VERSION" == "Ubuntu 22.04 LTS" ]; then
# This machine is running Ubuntu 22.04, which is supported by
# Mail-in-a-Box versions 60 and later.
TAG=v65
elif [ "$UBUNTU_VERSION" == "Ubuntu 18.04 LTS" ]; then
# This machine is running Ubuntu 18.04, which is supported by
# Mail-in-a-Box versions 0.40 through 5x.
echo "Support is ending for Ubuntu 18.04."
echo "Please immediately begin to migrate your data to"
echo "a new machine running Ubuntu 22.04. See:"
echo "https://mailinabox.email/maintenance.html#upgrade"
TAG=v57a TAG=v57a
elif [ "$UBUNTU_VERSION" == "Ubuntu 14.04 LTS" ]; then
elif [ "$(lsb_release -d | sed 's/.*:\s*//' | sed 's/14\.04\.[0-9]/14.04/' )" == "Ubuntu 14.04 LTS" ]; then # This machine is running Ubuntu 14.04, which is supported by
# This machine is running Ubuntu 14.04. # Mail-in-a-Box versions 1 through v0.30.
echo "You are installing the last version of Mail-in-a-Box that will" echo "Ubuntu 14.04 is no longer supported."
echo "support Ubuntu 14.04. If this is a new installation of Mail-in-a-Box," echo "The last version of Mail-in-a-Box supporting Ubuntu 14.04 will be installed."
echo "stop now and switch to a machine running Ubuntu 18.04. If you are"
echo "upgrading an existing Mail-in-a-Box --- great. After upgrading this"
echo "box, please visit https://mailinabox.email for notes on how to upgrade"
echo "to Ubuntu 18.04."
echo ""
TAG=v0.30 TAG=v0.30
else else
echo "This script must be run on a system running Ubuntu 18.04 or Ubuntu 14.04." echo "This script may be used only on a machine running Ubuntu 14.04, 18.04, or 22.04."
exit 1 exit 1
fi fi
fi fi
@@ -68,7 +73,7 @@ fi
cd $HOME/mailinabox cd $HOME/mailinabox
# Update it. # Update it.
if [ "$TAG" != $(git describe) ]; then if [ "$TAG" != $(git describe --always) ]; then
echo Updating Mail-in-a-Box to $TAG . . . echo Updating Mail-in-a-Box to $TAG . . .
git fetch --depth 1 --force --prune origin tag $TAG git fetch --depth 1 --force --prune origin tag $TAG
if ! git checkout -q $TAG; then if ! git checkout -q $TAG; then

View File

@@ -63,7 +63,7 @@ chmod go-rwx $STORAGE_ROOT/mail/dkim
tools/editconf.py /etc/opendmarc.conf -s \ tools/editconf.py /etc/opendmarc.conf -s \
"Syslog=true" \ "Syslog=true" \
"Socket=inet:8893@[127.0.0.1]" \ "Socket=inet:8893@[127.0.0.1]" \
"FailureReports=true" "FailureReports=false"
# SPFIgnoreResults causes the filter to ignore any SPF results in the header # SPFIgnoreResults causes the filter to ignore any SPF results in the header
# of the message. This is useful if you want the filter to perfrom SPF checks # of the message. This is useful if you want the filter to perfrom SPF checks
@@ -82,11 +82,11 @@ tools/editconf.py /etc/opendmarc.conf -s \
tools/editconf.py /etc/opendmarc.conf -s \ tools/editconf.py /etc/opendmarc.conf -s \
"SPFSelfValidate=true" "SPFSelfValidate=true"
# Enables generation of failure reports for sending domains that publish a # Disables generation of failure reports for sending domains that publish a
# "none" policy. # "none" policy.
tools/editconf.py /etc/opendmarc.conf -s \ tools/editconf.py /etc/opendmarc.conf -s \
"FailureReportsOnNone=true" "FailureReportsOnNone=false"
# AlwaysAddARHeader Adds an "Authentication-Results:" header field even to # AlwaysAddARHeader Adds an "Authentication-Results:" header field even to
# unsigned messages from domains with no "signs all" policy. The reported DKIM # unsigned messages from domains with no "signs all" policy. The reported DKIM

View File

@@ -10,17 +10,13 @@
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 the packages.
#
# * nsd: The non-recursive nameserver that publishes our DNS records.
# * ldnsutils: Helper utilities for signing DNSSEC zones.
# * openssh-client: Provides ssh-keyscan which we use to create SSHFP records.
echo "Installing nsd (DNS server)..."
apt_install nsd ldnsutils openssh-client
# Prepare nsd's configuration. # Prepare nsd's configuration.
# We configure nsd before installation as we only want it to bind to some addresses
# and it otherwise will have port / bind conflicts with bind9 used as the local resolver
mkdir -p /var/run/nsd mkdir -p /var/run/nsd
mkdir -p /etc/nsd
mkdir -p /etc/nsd/zones
touch /etc/nsd/zones.conf
cat > /etc/nsd/nsd.conf << EOF; cat > /etc/nsd/nsd.conf << EOF;
# Do not edit. Overwritten by Mail-in-a-Box setup. # Do not edit. Overwritten by Mail-in-a-Box setup.
@@ -42,18 +38,6 @@ server:
EOF EOF
# Add log rotation
cat > /etc/logrotate.d/nsd <<EOF;
/var/log/nsd.log {
weekly
missingok
rotate 12
compress
delaycompress
notifempty
}
EOF
# Since we have bind9 listening on localhost for locally-generated # Since we have bind9 listening on localhost for locally-generated
# DNS queries that require a recursive nameserver, and the system # DNS queries that require a recursive nameserver, and the system
# might have other network interfaces for e.g. tunnelling, we have # might have other network interfaces for e.g. tunnelling, we have
@@ -70,6 +54,26 @@ echo "include: /etc/nsd/nsd.conf.d/*.conf" >> /etc/nsd/nsd.conf;
# now be stored in /etc/nsd/nsd.conf.d. # now be stored in /etc/nsd/nsd.conf.d.
rm -f /etc/nsd/zones.conf rm -f /etc/nsd/zones.conf
# Add log rotation
cat > /etc/logrotate.d/nsd <<EOF;
/var/log/nsd.log {
weekly
missingok
rotate 12
compress
delaycompress
notifempty
}
EOF
# Install the packages.
#
# * nsd: The non-recursive nameserver that publishes our DNS records.
# * ldnsutils: Helper utilities for signing DNSSEC zones.
# * openssh-client: Provides ssh-keyscan which we use to create SSHFP records.
echo "Installing nsd (DNS server)..."
apt_install nsd ldnsutils openssh-client
# Create DNSSEC signing keys. # Create DNSSEC signing keys.
mkdir -p "$STORAGE_ROOT/dns/dnssec"; mkdir -p "$STORAGE_ROOT/dns/dnssec";

View File

@@ -4,6 +4,8 @@
# -o pipefail: don't ignore errors in the non-last command in a pipeline # -o pipefail: don't ignore errors in the non-last command in a pipeline
set -euo pipefail set -euo pipefail
PHP_VER=8.0
function hide_output { function hide_output {
# This function hides the output of a command unless the command fails # This function hides the output of a command unless the command fails
# and returns a non-zero exit code. # and returns a non-zero exit code.

View File

@@ -84,10 +84,11 @@ tools/editconf.py /etc/dovecot/conf.d/10-ssl.conf \
ssl=required \ ssl=required \
"ssl_cert=<$STORAGE_ROOT/ssl/ssl_certificate.pem" \ "ssl_cert=<$STORAGE_ROOT/ssl/ssl_certificate.pem" \
"ssl_key=<$STORAGE_ROOT/ssl/ssl_private_key.pem" \ "ssl_key=<$STORAGE_ROOT/ssl/ssl_private_key.pem" \
"ssl_protocols=TLSv1.2" \ "ssl_min_protocol=TLSv1.2" \
"ssl_cipher_list=ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384" \ "ssl_cipher_list=ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384" \
"ssl_prefer_server_ciphers=no" \ "ssl_prefer_server_ciphers=no" \
"ssl_dh_parameters_length=2048" "ssl_dh_parameters_length=2048" \
"ssl_dh=<$STORAGE_ROOT/ssl/dh2048.pem"
# Disable in-the-clear IMAP/POP because there is no reason for a user to transmit # Disable in-the-clear IMAP/POP because there is no reason for a user to transmit
# login credentials outside of an encrypted connection. Only the over-TLS versions # login credentials outside of an encrypted connection. Only the over-TLS versions
@@ -201,13 +202,13 @@ chmod -R o-rwx /etc/dovecot
# Ensure mailbox files have a directory that exists and are owned by the mail user. # Ensure mailbox files have a directory that exists and are owned by the mail user.
mkdir -p $STORAGE_ROOT/mail/mailboxes mkdir -p $STORAGE_ROOT/mail/mailboxes
chown -R mail.mail $STORAGE_ROOT/mail/mailboxes chown -R mail:mail $STORAGE_ROOT/mail/mailboxes
# Same for the sieve scripts. # Same for the sieve scripts.
mkdir -p $STORAGE_ROOT/mail/sieve mkdir -p $STORAGE_ROOT/mail/sieve
mkdir -p $STORAGE_ROOT/mail/sieve/global_before mkdir -p $STORAGE_ROOT/mail/sieve/global_before
mkdir -p $STORAGE_ROOT/mail/sieve/global_after mkdir -p $STORAGE_ROOT/mail/sieve/global_after
chown -R mail.mail $STORAGE_ROOT/mail/sieve chown -R mail:mail $STORAGE_ROOT/mail/sieve
# Allow the IMAP/POP ports in the firewall. # Allow the IMAP/POP ports in the firewall.
ufw_allow imaps ufw_allow imaps

View File

@@ -13,8 +13,8 @@
# destinations according to aliases, and passses email on to # destinations according to aliases, and passses email on to
# another service for local mail delivery. # another service for local mail delivery.
# #
# The first hop in local mail delivery is to Spamassassin via # The first hop in local mail delivery is to spampd via
# LMTP. Spamassassin then passes mail over to Dovecot for # LMTP. spampd then passes mail over to Dovecot for
# storage in the user's mailbox. # storage in the user's mailbox.
# #
# Postfix also listens on ports 465/587 (SMTPS, SMTP+STARTLS) for # Postfix also listens on ports 465/587 (SMTPS, SMTP+STARTLS) for
@@ -193,16 +193,17 @@ tools/editconf.py /etc/postfix/main.cf \
# ### Incoming Mail # ### Incoming Mail
# Pass any incoming mail over to a local delivery agent. Spamassassin # Pass mail to spampd, which acts as the local delivery agent (LDA),
# will act as the LDA agent at first. It is listening on port 10025 # which then passes the mail over to the Dovecot LMTP server after.
# with LMTP. Spamassassin will pass the mail over to Dovecot after. # spampd runs on port 10025 by default.
# #
# In a basic setup we would pass mail directly to Dovecot by setting # In a basic setup we would pass mail directly to Dovecot by setting
# virtual_transport to `lmtp:unix:private/dovecot-lmtp`. # virtual_transport to `lmtp:unix:private/dovecot-lmtp`.
tools/editconf.py /etc/postfix/main.cf "virtual_transport=lmtp:[127.0.0.1]:10025" tools/editconf.py /etc/postfix/main.cf "virtual_transport=lmtp:[127.0.0.1]:10025"
# Because of a spampd bug, limit the number of recipients in each connection. # Clear the lmtp_destination_recipient_limit setting which in previous
# versions of Mail-in-a-Box was set to 1 because of a spampd bug.
# See https://github.com/mail-in-a-box/mailinabox/issues/1523. # See https://github.com/mail-in-a-box/mailinabox/issues/1523.
tools/editconf.py /etc/postfix/main.cf lmtp_destination_recipient_limit=1 tools/editconf.py /etc/postfix/main.cf -e lmtp_destination_recipient_limit=
# Who can send mail to us? Some basic filters. # Who can send mail to us? Some basic filters.
@@ -232,11 +233,32 @@ tools/editconf.py /etc/postfix/main.cf \
# As a matter of fact RFC is not strict about retry timer so postfix and # As a matter of fact RFC is not strict about retry timer so postfix and
# other MTA have their own intervals. To fix the problem of receiving # other MTA have their own intervals. To fix the problem of receiving
# e-mails really latter, delay of greylisting has been set to # e-mails really latter, delay of greylisting has been set to
# 180 seconds (default is 300 seconds). # 180 seconds (default is 300 seconds). We will move the postgrey database
# under $STORAGE_ROOT. This prevents a "warming up" that would have occured
# previously with a migrated or reinstalled OS. We will specify this new path
# with the --dbdir=... option. Arguments within POSTGREY_OPTS can not have spaces,
# including dbdir. This is due to the way the init script sources the
# /etc/default/postgrey file. --dbdir=... either needs to be a path without spaces
# (luckily $STORAGE_ROOT does not currently work with spaces), or it needs to be a
# symlink without spaces that can point to a folder with spaces). We'll just assume
# $STORAGE_ROOT won't have spaces to simplify things.
tools/editconf.py /etc/default/postgrey \ tools/editconf.py /etc/default/postgrey \
POSTGREY_OPTS=\"'--inet=127.0.0.1:10023 --delay=180'\" POSTGREY_OPTS=\""--inet=127.0.0.1:10023 --delay=180 --dbdir=$STORAGE_ROOT/mail/postgrey/db"\"
# If the $STORAGE_ROOT/mail/postgrey is empty, copy the postgrey database over from the old location
if [ ! -d $STORAGE_ROOT/mail/postgrey/db ]; then
# Stop the service
service postgrey stop
# Ensure the new paths for postgrey db exists
mkdir -p $STORAGE_ROOT/mail/postgrey/db
# Move over database files
mv /var/lib/postgrey/* $STORAGE_ROOT/mail/postgrey/db/ || true
fi
# Ensure permissions are set
chown -R postgrey:postgrey $STORAGE_ROOT/mail/postgrey/
chmod 700 $STORAGE_ROOT/mail/postgrey/{,db}
# We are going to setup a newer whitelist for postgrey, the version included in the distribution is old # We are going to setup a newer whitelist for postgrey, the version included in the distribution is old
cat > /etc/cron.daily/mailinabox-postgrey-whitelist << EOF; cat > /etc/cron.daily/mailinabox-postgrey-whitelist << EOF;
#!/bin/bash #!/bin/bash

View File

@@ -1,23 +1,12 @@
#!/bin/bash #!/bin/bash
source setup/functions.sh source setup/functions.sh
source /etc/mailinabox.conf # load global vars
echo "Installing Mail-in-a-Box system management daemon..." echo "Installing Mail-in-a-Box system management daemon..."
# DEPENDENCIES # DEPENDENCIES
# We used to install management daemon-related Python packages
# directly to /usr/local/lib. We moved to a virtualenv because
# these packages might conflict with apt-installed packages.
# We may have a lingering version of acme that conflcits with
# certbot, which we're about to install below, so remove it
# first. Once acme is installed by an apt package, this might
# break the package version and `apt-get install --reinstall python3-acme`
# might be needed in that case.
while [ -d /usr/local/lib/python3.4/dist-packages/acme ]; do
pip3 uninstall -y acme;
done
# duplicity is used to make backups of user data. # duplicity is used to make backups of user data.
# #
# virtualenv is used to isolate the Python 3 packages we # virtualenv is used to isolate the Python 3 packages we
@@ -25,12 +14,12 @@ done
# #
# certbot installs EFF's certbot which we use to # certbot installs EFF's certbot which we use to
# provision free TLS certificates. # provision free TLS certificates.
apt_install duplicity python-pip virtualenv certbot rsync apt_install duplicity python3-pip virtualenv certbot rsync
# b2sdk is used for backblaze backups. # b2sdk is used for backblaze backups.
# boto is used for amazon aws backups. # boto3 is used for amazon aws backups.
# Both are installed outside the pipenv, so they can be used by duplicity # Both are installed outside the pipenv, so they can be used by duplicity
hide_output pip3 install --upgrade b2sdk==1.14.1 boto hide_output pip3 install --upgrade b2sdk boto3
# Create a virtualenv for the installation of Python 3 packages # Create a virtualenv for the installation of Python 3 packages
# used by the management daemon. # used by the management daemon.
@@ -49,10 +38,10 @@ hide_output $venv/bin/pip install --upgrade pip
# NOTE: email_validator is repeated in setup/questions.sh, so please keep the versions synced. # NOTE: email_validator is repeated in setup/questions.sh, so please keep the versions synced.
hide_output $venv/bin/pip install --upgrade \ hide_output $venv/bin/pip install --upgrade \
rtyaml "email_validator>=1.0.0" "exclusiveprocess" \ rtyaml "email_validator>=1.0.0" "exclusiveprocess" \
flask dnspython python-dateutil expiringdict \ flask dnspython python-dateutil expiringdict gunicorn \
qrcode[pil] pyotp \ qrcode[pil] pyotp \
"idna>=2.0.0" "cryptography==2.2.2" psutil postfix-mta-sts-resolver \ "idna>=2.0.0" "cryptography==37.0.2" psutil postfix-mta-sts-resolver \
b2sdk==1.14.1 boto b2sdk boto3
# CONFIGURATION # CONFIGURATION
@@ -89,6 +78,9 @@ rm -f /tmp/bootstrap.zip
# Create an init script to start the management daemon and keep it # Create an init script to start the management daemon and keep it
# running after a reboot. # running after a reboot.
# Set a long timeout since some commands take a while to run, matching
# the timeout we set for PHP (fastcgi_read_timeout in the nginx confs).
# Note: Authentication currently breaks with more than 1 gunicorn worker.
cat > $inst_dir/start <<EOF; cat > $inst_dir/start <<EOF;
#!/bin/bash #!/bin/bash
# Set character encoding flags to ensure that any non-ASCII don't cause problems. # Set character encoding flags to ensure that any non-ASCII don't cause problems.
@@ -97,8 +89,13 @@ export LC_ALL=en_US.UTF-8
export LANG=en_US.UTF-8 export LANG=en_US.UTF-8
export LC_TYPE=en_US.UTF-8 export LC_TYPE=en_US.UTF-8
mkdir -p /var/lib/mailinabox
tr -cd '[:xdigit:]' < /dev/urandom | head -c 32 > /var/lib/mailinabox/api.key
chmod 640 /var/lib/mailinabox/api.key
source $venv/bin/activate source $venv/bin/activate
exec python $(pwd)/management/daemon.py export PYTHONPATH=$(pwd)/management
exec gunicorn -b localhost:10222 -w 1 --timeout 630 wsgi:app
EOF EOF
chmod +x $inst_dir/start chmod +x $inst_dir/start
cp --remove-destination conf/mailinabox.service /lib/systemd/system/mailinabox.service # target was previously a symlink so remove it first cp --remove-destination conf/mailinabox.service /lib/systemd/system/mailinabox.service # target was previously a symlink so remove it first

View File

@@ -34,8 +34,8 @@ contact.admin.always_send warning critical
EOF EOF
# The Debian installer touches these files and chowns them to www-data:adm for use with spawn-fcgi # The Debian installer touches these files and chowns them to www-data:adm for use with spawn-fcgi
chown munin. /var/log/munin/munin-cgi-html.log chown munin /var/log/munin/munin-cgi-html.log
chown munin. /var/log/munin/munin-cgi-graph.log chown munin /var/log/munin/munin-cgi-graph.log
# ensure munin-node knows the name of this machine # ensure munin-node knows the name of this machine
# and reduce logging level to warning # and reduce logging level to warning

View File

@@ -21,33 +21,42 @@ echo "Installing Nextcloud (contacts/calendar)..."
# we automatically install intermediate versions as needed. # we automatically install intermediate versions as needed.
# * The hash is the SHA1 hash of the ZIP package, which you can find by just running this script and # * The hash is the SHA1 hash of the ZIP package, which you can find by just running this script and
# copying it from the error message when it doesn't match what is below. # copying it from the error message when it doesn't match what is below.
nextcloud_ver=20.0.14 nextcloud_ver=25.0.7
nextcloud_hash=92cac708915f51ee2afc1787fd845476fd090c81 nextcloud_hash=a5a565c916355005c7b408dd41a1e53505e1a080
# Nextcloud apps # Nextcloud apps
# -------------- # --------------
# * Find the most recent tag that is compatible with the Nextcloud version above by # * Find the most recent tag that is compatible with the Nextcloud version above by
# consulting the <dependencies>...<nextcloud> node at: # consulting the <dependencies>...<nextcloud> node at:
# https://github.com/nextcloud-releases/contacts/blob/master/appinfo/info.xml # https://github.com/nextcloud-releases/contacts/blob/main/appinfo/info.xml
# https://github.com/nextcloud-releases/calendar/blob/master/appinfo/info.xml # https://github.com/nextcloud-releases/calendar/blob/main/appinfo/info.xml
# https://github.com/nextcloud/user_external/blob/master/appinfo/info.xml # https://github.com/nextcloud/user_external/blob/master/appinfo/info.xml
# * The hash is the SHA1 hash of the ZIP package, which you can find by just running this script and # * The hash is the SHA1 hash of the ZIP package, which you can find by just running this script and
# copying it from the error message when it doesn't match what is below. # copying it from the error message when it doesn't match what is below.
contacts_ver=4.0.7 contacts_ver=5.3.0
contacts_hash=45e7cf4bfe99cd8d03625cf9e5a1bb2e90549136 contacts_hash=4b0a6666374e3b55cfd2ae9b72e1d458b87d4c8c
calendar_ver=3.0.4
calendar_hash=d0284b68135777ec9ca713c307216165b294d0fe # Always ensure the versions are supported, see https://apps.nextcloud.com/apps/calendar
user_external_ver=1.0.0 calendar_ver=4.4.2
user_external_hash=3bf2609061d7214e7f0f69dd8883e55c4ec8f50a calendar_hash=21a42e15806adc9b2618760ef94f1797ef399e2f
# And https://apps.nextcloud.com/apps/user_external
user_external_ver=3.2.0
user_external_hash=a494073dcdecbbbc79a9c77f72524ac9994d2eec
# Clear prior packages and install dependencies from apt. # Clear prior packages and install dependencies from apt.
apt-get purge -qq -y owncloud* # we used to use the package manager apt-get purge -qq -y owncloud* # we used to use the package manager
apt_install php php-fpm \ apt_install curl php${PHP_VER} php${PHP_VER}-fpm \
php-cli php-sqlite3 php-gd php-imap php-curl php-pear curl \ php${PHP_VER}-cli php${PHP_VER}-sqlite3 php${PHP_VER}-gd php${PHP_VER}-imap php${PHP_VER}-curl \
php-dev php-gd php-xml php-mbstring php-zip php-apcu php-json \ php${PHP_VER}-dev php${PHP_VER}-gd php${PHP_VER}-xml php${PHP_VER}-mbstring php${PHP_VER}-zip php${PHP_VER}-apcu \
php-intl php-imagick php-gmp php-bcmath php${PHP_VER}-intl php${PHP_VER}-imagick php${PHP_VER}-gmp php${PHP_VER}-bcmath
# Enable APC before Nextcloud tools are run.
tools/editconf.py /etc/php/$PHP_VER/mods-available/apcu.ini -c ';' \
apc.enabled=1 \
apc.enable_cli=1
InstallNextcloud() { InstallNextcloud() {
@@ -64,8 +73,8 @@ InstallNextcloud() {
echo "Upgrading to Nextcloud version $version" echo "Upgrading to Nextcloud version $version"
echo echo
# Download and verify # Download and verify
wget_verify https://download.nextcloud.com/server/releases/nextcloud-$version.zip $hash /tmp/nextcloud.zip wget_verify https://download.nextcloud.com/server/releases/nextcloud-$version.zip $hash /tmp/nextcloud.zip
# Remove the current owncloud/Nextcloud # Remove the current owncloud/Nextcloud
rm -rf /usr/local/lib/owncloud rm -rf /usr/local/lib/owncloud
@@ -90,7 +99,7 @@ InstallNextcloud() {
# Starting with Nextcloud 15, the app user_external is no longer included in Nextcloud core, # Starting with Nextcloud 15, the app user_external is no longer included in Nextcloud core,
# we will install from their github repository. # we will install from their github repository.
if [ -n "$version_user_external" ]; then if [ -n "$version_user_external" ]; then
wget_verify https://github.com/nextcloud/user_external/releases/download/v$version_user_external/user_external-$version_user_external.tar.gz $hash_user_external /tmp/user_external.tgz wget_verify https://github.com/nextcloud-releases/user_external/releases/download/v$version_user_external/user_external-v$version_user_external.tar.gz $hash_user_external /tmp/user_external.tgz
tar -xf /tmp/user_external.tgz -C /usr/local/lib/owncloud/apps/ tar -xf /tmp/user_external.tgz -C /usr/local/lib/owncloud/apps/
rm /tmp/user_external.tgz rm /tmp/user_external.tgz
fi fi
@@ -105,27 +114,28 @@ InstallNextcloud() {
# 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
# that error. # that error.
chown -f -R www-data.www-data $STORAGE_ROOT/owncloud /usr/local/lib/owncloud || /bin/true chown -f -R www-data:www-data $STORAGE_ROOT/owncloud /usr/local/lib/owncloud || /bin/true
# If this isn't a new installation, immediately run the upgrade script. # If this isn't a new installation, immediately run the upgrade script.
# Then check for success (0=ok and 3=no upgrade needed, both are success). # Then check for success (0=ok and 3=no upgrade needed, both are success).
if [ -e $STORAGE_ROOT/owncloud/owncloud.db ]; then if [ -e $STORAGE_ROOT/owncloud/owncloud.db ]; then
# ownCloud 8.1.1 broke upgrades. It may fail on the first attempt, but # ownCloud 8.1.1 broke upgrades. It may fail on the first attempt, but
# that can be OK. # that can be OK.
sudo -u www-data php /usr/local/lib/owncloud/occ upgrade sudo -u www-data php$PHP_VER /usr/local/lib/owncloud/occ upgrade
if [ \( $? -ne 0 \) -a \( $? -ne 3 \) ]; then if [ \( $? -ne 0 \) -a \( $? -ne 3 \) ]; then
echo "Trying ownCloud upgrade again to work around ownCloud upgrade bug..." echo "Trying ownCloud upgrade again to work around ownCloud upgrade bug..."
sudo -u www-data php /usr/local/lib/owncloud/occ upgrade sudo -u www-data php$PHP_VER /usr/local/lib/owncloud/occ upgrade
if [ \( $? -ne 0 \) -a \( $? -ne 3 \) ]; then exit 1; fi if [ \( $? -ne 0 \) -a \( $? -ne 3 \) ]; then exit 1; fi
sudo -u www-data php /usr/local/lib/owncloud/occ maintenance:mode --off sudo -u www-data php$PHP_VER /usr/local/lib/owncloud/occ maintenance:mode --off
echo "...which seemed to work." echo "...which seemed to work."
fi fi
# Add missing indices. NextCloud didn't include this in the normal upgrade because it might take some time. # Add missing indices. NextCloud didn't include this in the normal upgrade because it might take some time.
sudo -u www-data php /usr/local/lib/owncloud/occ db:add-missing-indices sudo -u www-data php$PHP_VER /usr/local/lib/owncloud/occ db:add-missing-indices
sudo -u www-data php$PHP_VER /usr/local/lib/owncloud/occ db:add-missing-primary-keys
# Run conversion to BigInt identifiers, this process may take some time on large tables. # Run conversion to BigInt identifiers, this process may take some time on large tables.
sudo -u www-data php /usr/local/lib/owncloud/occ db:convert-filecache-bigint --no-interaction sudo -u www-data php$PHP_VER /usr/local/lib/owncloud/occ db:convert-filecache-bigint --no-interaction
fi fi
} }
@@ -137,7 +147,7 @@ InstallNextcloud() {
# If config.php exists, get version number, otherwise CURRENT_NEXTCLOUD_VER is empty. # If config.php exists, get version number, otherwise CURRENT_NEXTCLOUD_VER is empty.
if [ -f "$STORAGE_ROOT/owncloud/config.php" ]; then if [ -f "$STORAGE_ROOT/owncloud/config.php" ]; then
CURRENT_NEXTCLOUD_VER=$(php -r "include(\"$STORAGE_ROOT/owncloud/config.php\"); echo(\$CONFIG['version']);") CURRENT_NEXTCLOUD_VER=$(php$PHP_VER -r "include(\"$STORAGE_ROOT/owncloud/config.php\"); echo(\$CONFIG['version']);")
else else
CURRENT_NEXTCLOUD_VER="" CURRENT_NEXTCLOUD_VER=""
fi fi
@@ -146,8 +156,8 @@ fi
# from the version currently installed, do the install/upgrade # from the version currently installed, do the install/upgrade
if [ ! -d /usr/local/lib/owncloud/ ] || [[ ! ${CURRENT_NEXTCLOUD_VER} =~ ^$nextcloud_ver ]]; then if [ ! -d /usr/local/lib/owncloud/ ] || [[ ! ${CURRENT_NEXTCLOUD_VER} =~ ^$nextcloud_ver ]]; then
# Stop php-fpm if running. If theyre not running (which happens on a previously failed install), dont bail. # Stop php-fpm if running. If they are not running (which happens on a previously failed install), dont bail.
service php7.2-fpm stop &> /dev/null || /bin/true service php$PHP_VER-fpm stop &> /dev/null || /bin/true
# Backup the existing ownCloud/Nextcloud. # Backup the existing ownCloud/Nextcloud.
# Create a backup directory to store the current installation and database to # Create a backup directory to store the current installation and database to
@@ -168,48 +178,42 @@ if [ ! -d /usr/local/lib/owncloud/ ] || [[ ! ${CURRENT_NEXTCLOUD_VER} =~ ^$nextc
if [ ! -z ${CURRENT_NEXTCLOUD_VER} ]; then if [ ! -z ${CURRENT_NEXTCLOUD_VER} ]; then
# Database migrations from ownCloud are no longer possible because ownCloud cannot be run under # Database migrations from ownCloud are no longer possible because ownCloud cannot be run under
# PHP 7. # PHP 7.
if [ -e $STORAGE_ROOT/owncloud/config.php ]; then
# Remove the read-onlyness of the config, which is needed for migrations, especially for v24
sed -i -e '/config_is_read_only/d' $STORAGE_ROOT/owncloud/config.php
fi
if [[ ${CURRENT_NEXTCLOUD_VER} =~ ^[89] ]]; then if [[ ${CURRENT_NEXTCLOUD_VER} =~ ^[89] ]]; then
echo "Upgrades from Mail-in-a-Box prior to v0.28 (dated July 30, 2018) with Nextcloud < 13.0.6 (you have ownCloud 8 or 9) are not supported. Upgrade to Mail-in-a-Box version v0.30 first. Setup will continue, but skip the Nextcloud migration." echo "Upgrades from Mail-in-a-Box prior to v0.28 (dated July 30, 2018) with Nextcloud < 13.0.6 (you have ownCloud 8 or 9) are not supported. Upgrade to Mail-in-a-Box version v0.30 first. Setup will continue, but skip the Nextcloud migration."
return 0 return 0
elif [[ ${CURRENT_NEXTCLOUD_VER} =~ ^1[012] ]]; then elif [[ ${CURRENT_NEXTCLOUD_VER} =~ ^1[012] ]]; then
echo "Upgrades from Mail-in-a-Box prior to v0.28 (dated July 30, 2018) with Nextcloud < 13.0.6 (you have ownCloud 10, 11 or 12) are not supported. Upgrade to Mail-in-a-Box version v0.30 first. Setup will continue, but skip the Nextcloud migration." echo "Upgrades from Mail-in-a-Box prior to v0.28 (dated July 30, 2018) with Nextcloud < 13.0.6 (you have ownCloud 10, 11 or 12) are not supported. Upgrade to Mail-in-a-Box version v0.30 first. Setup will continue, but skip the Nextcloud migration."
return 0 return 0
elif [[ ${CURRENT_NEXTCLOUD_VER} =~ ^13 ]]; then elif [[ ${CURRENT_NEXTCLOUD_VER} =~ ^1[3456789] ]]; then
# If we are running Nextcloud 13, upgrade to Nextcloud 14 echo "Upgrades from Mail-in-a-Box prior to v60 with Nextcloud 19 or earlier are not supported. Upgrade to the latest Mail-in-a-Box version supported on your machine first. Setup will continue, but skip the Nextcloud migration."
InstallNextcloud 14.0.6 4e43a57340f04c2da306c8eea98e30040399ae5a 3.3.0 e55d0357c6785d3b1f3b5f21780cb6d41d32443a 2.0.3 9d9717b29337613b72c74e9914c69b74b346c466 return 0
CURRENT_NEXTCLOUD_VER="14.0.6"
fi fi
if [[ ${CURRENT_NEXTCLOUD_VER} =~ ^14 ]]; then
# During the upgrade from Nextcloud 14 to 15, user_external may cause the upgrade to fail. if [[ ${CURRENT_NEXTCLOUD_VER} =~ ^20 ]]; then
# We will disable it here before the upgrade and install it again after the upgrade. InstallNextcloud 21.0.7 f5c7079c5b56ce1e301c6a27c0d975d608bb01c9 4.0.7 45e7cf4bfe99cd8d03625cf9e5a1bb2e90549136 3.0.4 d0284b68135777ec9ca713c307216165b294d0fe
hide_output sudo -u www-data php /usr/local/lib/owncloud/console.php app:disable user_external CURRENT_NEXTCLOUD_VER="21.0.7"
InstallNextcloud 15.0.8 4129d8d4021c435f2e86876225fb7f15adf764a3 3.3.0 e55d0357c6785d3b1f3b5f21780cb6d41d32443a 2.0.3 a1f3835c752929e3598eb94f22300516867ac6ab 0.7.0 555a94811daaf5bdd336c5e48a78aa8567b86437 fi
CURRENT_NEXTCLOUD_VER="15.0.8" if [[ ${CURRENT_NEXTCLOUD_VER} =~ ^21 ]]; then
InstallNextcloud 22.2.6 9d39741f051a8da42ff7df46ceef2653a1dc70d9 4.1.0 697f6b4a664e928d72414ea2731cb2c9d1dc3077 3.2.2 ce4030ab57f523f33d5396c6a81396d440756f5f 3.0.0 0df781b261f55bbde73d8c92da3f99397000972f
CURRENT_NEXTCLOUD_VER="22.2.6"
fi
if [[ ${CURRENT_NEXTCLOUD_VER} =~ ^22 ]]; then
InstallNextcloud 23.0.12 d138641b8e7aabebe69bb3ec7c79a714d122f729 4.1.0 697f6b4a664e928d72414ea2731cb2c9d1dc3077 3.2.2 ce4030ab57f523f33d5396c6a81396d440756f5f 3.0.0 0df781b261f55bbde73d8c92da3f99397000972f
CURRENT_NEXTCLOUD_VER="23.0.12"
fi
if [[ ${CURRENT_NEXTCLOUD_VER} =~ ^23 ]]; then
InstallNextcloud 24.0.12 7aa5d61632c1ccf4ca3ff00fb6b295d318c05599 4.1.0 697f6b4a664e928d72414ea2731cb2c9d1dc3077 3.2.2 ce4030ab57f523f33d5396c6a81396d440756f5f 3.0.0 0df781b261f55bbde73d8c92da3f99397000972f
CURRENT_NEXTCLOUD_VER="24.0.12"
fi fi
if [[ ${CURRENT_NEXTCLOUD_VER} =~ ^15 ]]; then
InstallNextcloud 16.0.6 0bb3098455ec89f5af77a652aad553ad40a88819 3.3.0 e55d0357c6785d3b1f3b5f21780cb6d41d32443a 2.0.3 a1f3835c752929e3598eb94f22300516867ac6ab 0.7.0 555a94811daaf5bdd336c5e48a78aa8567b86437
CURRENT_NEXTCLOUD_VER="16.0.6"
fi
if [[ ${CURRENT_NEXTCLOUD_VER} =~ ^16 ]]; then
InstallNextcloud 17.0.6 50b98d2c2f18510b9530e558ced9ab51eb4f11b0 3.3.0 e55d0357c6785d3b1f3b5f21780cb6d41d32443a 2.0.3 a1f3835c752929e3598eb94f22300516867ac6ab 0.7.0 555a94811daaf5bdd336c5e48a78aa8567b86437
CURRENT_NEXTCLOUD_VER="17.0.6"
fi
if [[ ${CURRENT_NEXTCLOUD_VER} =~ ^17 ]]; then
# Don't exit the install if this column already exists (see #2076)
(echo "ALTER TABLE oc_flow_operations ADD COLUMN entity VARCHAR;" | sqlite3 $STORAGE_ROOT/owncloud/owncloud.db 2>/dev/null) || true
InstallNextcloud 18.0.10 39c0021a8b8477c3f1733fddefacfa5ebf921c68 3.4.1 8f685e7dc99758636d660d595e389c324e51e9d1 2.0.3 a1f3835c752929e3598eb94f22300516867ac6ab 1.0.0 3bf2609061d7214e7f0f69dd8883e55c4ec8f50a
CURRENT_NEXTCLOUD_VER="18.0.10"
fi
if [[ ${CURRENT_NEXTCLOUD_VER} =~ ^18 ]]; then
InstallNextcloud 19.0.4 01e98791ba12f4860d3d4047b9803f97a1b55c60 3.4.1 8f685e7dc99758636d660d595e389c324e51e9d1 2.0.3 a1f3835c752929e3598eb94f22300516867ac6ab 1.0.0 3bf2609061d7214e7f0f69dd8883e55c4ec8f50a
CURRENT_NEXTCLOUD_VER="19.0.4"
fi
fi fi
InstallNextcloud $nextcloud_ver $nextcloud_hash $contacts_ver $contacts_hash $calendar_ver $calendar_hash $user_external_ver $user_external_hash InstallNextcloud $nextcloud_ver $nextcloud_hash $contacts_ver $contacts_hash $calendar_ver $calendar_hash $user_external_ver $user_external_hash
# Nextcloud 20 needs to have some optional columns added
sudo -u www-data php /usr/local/lib/owncloud/occ db:add-missing-columns
fi fi
# ### Configuring Nextcloud # ### Configuring Nextcloud
@@ -235,10 +239,10 @@ if [ ! -f $STORAGE_ROOT/owncloud/owncloud.db ]; then
'overwrite.cli.url' => '/cloud', 'overwrite.cli.url' => '/cloud',
'user_backends' => array( 'user_backends' => array(
array( array(
'class' => 'OC_User_IMAP', 'class' => '\OCA\UserExternal\IMAP',
'arguments' => array( 'arguments' => array(
'127.0.0.1', 143, null '127.0.0.1', 143, null, null, false, false
), ),
), ),
), ),
'memcache.local' => '\OC\Memcache\APCu', 'memcache.local' => '\OC\Memcache\APCu',
@@ -275,12 +279,12 @@ EOF
EOF EOF
# Set permissions # Set permissions
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 Nextcloud's setup step, which creates the Nextcloud sqlite database. # Execute Nextcloud's setup step, which creates the Nextcloud sqlite database.
# It also wipes it if it exists. And it updates config.php with database # It also wipes it if it exists. And it updates config.php with database
# settings and deletes the autoconfig.php file. # 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$PHP_VER /usr/local/lib/owncloud/index.php;)
fi fi
# Update config.php. # Update config.php.
@@ -296,14 +300,16 @@ fi
# Use PHP to read the settings file, modify it, and write out the new settings array. # Use PHP to read the settings file, modify it, and write out the new settings array.
TIMEZONE=$(cat /etc/timezone) TIMEZONE=$(cat /etc/timezone)
CONFIG_TEMP=$(/bin/mktemp) CONFIG_TEMP=$(/bin/mktemp)
php <<EOF > $CONFIG_TEMP && mv $CONFIG_TEMP $STORAGE_ROOT/owncloud/config.php; php$PHP_VER <<EOF > $CONFIG_TEMP && mv $CONFIG_TEMP $STORAGE_ROOT/owncloud/config.php;
<?php <?php
include("$STORAGE_ROOT/owncloud/config.php"); include("$STORAGE_ROOT/owncloud/config.php");
\$CONFIG['config_is_read_only'] = false;
\$CONFIG['trusted_domains'] = array('$PRIMARY_HOSTNAME'); \$CONFIG['trusted_domains'] = array('$PRIMARY_HOSTNAME');
\$CONFIG['memcache.local'] = '\OC\Memcache\APCu'; \$CONFIG['memcache.local'] = '\OC\Memcache\APCu';
\$CONFIG['overwrite.cli.url'] = '/cloud'; \$CONFIG['overwrite.cli.url'] = 'https://${PRIMARY_HOSTNAME}/cloud';
\$CONFIG['mail_from_address'] = 'administrator'; # just the local part, matches our master administrator address \$CONFIG['mail_from_address'] = 'administrator'; # just the local part, matches our master administrator address
\$CONFIG['logtimezone'] = '$TIMEZONE'; \$CONFIG['logtimezone'] = '$TIMEZONE';
@@ -311,38 +317,45 @@ include("$STORAGE_ROOT/owncloud/config.php");
\$CONFIG['mail_domain'] = '$PRIMARY_HOSTNAME'; \$CONFIG['mail_domain'] = '$PRIMARY_HOSTNAME';
\$CONFIG['user_backends'] = array(array('class' => 'OC_User_IMAP','arguments' => array('127.0.0.1', 143, null),),); \$CONFIG['user_backends'] = array(
array(
'class' => '\OCA\UserExternal\IMAP',
'arguments' => array(
'127.0.0.1', 143, null, null, false, false
),
),
);
echo "<?php\n\\\$CONFIG = "; echo "<?php\n\\\$CONFIG = ";
var_export(\$CONFIG); var_export(\$CONFIG);
echo ";"; echo ";";
?> ?>
EOF EOF
chown www-data.www-data $STORAGE_ROOT/owncloud/config.php chown www-data:www-data $STORAGE_ROOT/owncloud/config.php
# Enable/disable apps. Note that this must be done after the Nextcloud setup. # Enable/disable apps. Note that this must be done after the Nextcloud setup.
# The firstrunwizard gave Josh all sorts of problems, so disabling that. # The firstrunwizard gave Josh all sorts of problems, so disabling that.
# user_external is what allows Nextcloud to use IMAP for login. The contacts # user_external is what allows Nextcloud to use IMAP for login. The contacts
# and calendar apps are the extensions we really care about here. # and calendar apps are the extensions we really care about here.
hide_output sudo -u www-data php /usr/local/lib/owncloud/console.php app:disable firstrunwizard hide_output sudo -u www-data php$PHP_VER /usr/local/lib/owncloud/console.php app:disable firstrunwizard
hide_output sudo -u www-data php /usr/local/lib/owncloud/console.php app:enable user_external hide_output sudo -u www-data php$PHP_VER /usr/local/lib/owncloud/console.php app:enable user_external
hide_output sudo -u www-data php /usr/local/lib/owncloud/console.php app:enable contacts hide_output sudo -u www-data php$PHP_VER /usr/local/lib/owncloud/console.php app:enable contacts
hide_output sudo -u www-data php /usr/local/lib/owncloud/console.php app:enable calendar hide_output sudo -u www-data php$PHP_VER /usr/local/lib/owncloud/console.php app:enable calendar
# When upgrading, run the upgrade script again now that apps are enabled. It seems like # When upgrading, run the upgrade script again now that apps are enabled. It seems like
# the first upgrade at the top won't work because apps may be disabled during upgrade? # the first upgrade at the top won't work because apps may be disabled during upgrade?
# Check for success (0=ok, 3=no upgrade needed). # Check for success (0=ok, 3=no upgrade needed).
sudo -u www-data php /usr/local/lib/owncloud/occ upgrade sudo -u www-data php$PHP_VER /usr/local/lib/owncloud/occ upgrade
if [ \( $? -ne 0 \) -a \( $? -ne 3 \) ]; then exit 1; fi if [ \( $? -ne 0 \) -a \( $? -ne 3 \) ]; then exit 1; fi
# Disable default apps that we don't support # Disable default apps that we don't support
sudo -u www-data \ sudo -u www-data \
php /usr/local/lib/owncloud/occ app:disable photos dashboard activity \ php$PHP_VER /usr/local/lib/owncloud/occ app:disable photos dashboard activity \
| (grep -v "No such app enabled" || /bin/true) | (grep -v "No such app enabled" || /bin/true)
# Set PHP FPM values to support large file uploads # Set PHP FPM values to support large file uploads
# (semicolon is the comment character in this file, hashes produce deprecation warnings) # (semicolon is the comment character in this file, hashes produce deprecation warnings)
tools/editconf.py /etc/php/7.2/fpm/php.ini -c ';' \ tools/editconf.py /etc/php/$PHP_VER/fpm/php.ini -c ';' \
upload_max_filesize=16G \ upload_max_filesize=16G \
post_max_size=16G \ post_max_size=16G \
output_buffering=16384 \ output_buffering=16384 \
@@ -351,7 +364,7 @@ tools/editconf.py /etc/php/7.2/fpm/php.ini -c ';' \
short_open_tag=On short_open_tag=On
# Set Nextcloud recommended opcache settings # Set Nextcloud recommended opcache settings
tools/editconf.py /etc/php/7.2/cli/conf.d/10-opcache.ini -c ';' \ tools/editconf.py /etc/php/$PHP_VER/cli/conf.d/10-opcache.ini -c ';' \
opcache.enable=1 \ opcache.enable=1 \
opcache.enable_cli=1 \ opcache.enable_cli=1 \
opcache.interned_strings_buffer=8 \ opcache.interned_strings_buffer=8 \
@@ -360,22 +373,44 @@ tools/editconf.py /etc/php/7.2/cli/conf.d/10-opcache.ini -c ';' \
opcache.save_comments=1 \ opcache.save_comments=1 \
opcache.revalidate_freq=1 opcache.revalidate_freq=1
# If apc is explicitly disabled we need to enable it # Migrate users_external data from <0.6.0 to version 3.0.0
if grep -q apc.enabled=0 /etc/php/7.2/mods-available/apcu.ini; then # (see https://github.com/nextcloud/user_external).
tools/editconf.py /etc/php/7.2/mods-available/apcu.ini -c ';' \ # This version was probably in use in Mail-in-a-Box v0.41 (February 26, 2019) and earlier.
apc.enabled=1 # We moved to v0.6.3 in 193763f8. Ignore errors - maybe there are duplicated users with the
fi # correct backend already.
sqlite3 $STORAGE_ROOT/owncloud/owncloud.db "UPDATE oc_users_external SET backend='127.0.0.1';" || /bin/true
# Set up a cron job for Nextcloud. # Set up a general cron job for Nextcloud.
# Also add another job for Calendar updates, per advice in the Nextcloud docs
# https://docs.nextcloud.com/server/24/admin_manual/groupware/calendar.html#background-jobs
cat > /etc/cron.d/mailinabox-nextcloud << EOF; cat > /etc/cron.d/mailinabox-nextcloud << EOF;
#!/bin/bash #!/bin/bash
# Mail-in-a-Box # Mail-in-a-Box
*/5 * * * * root sudo -u www-data php -f /usr/local/lib/owncloud/cron.php */5 * * * * root sudo -u www-data php$PHP_VER -f /usr/local/lib/owncloud/cron.php
*/5 * * * * root sudo -u www-data php$PHP_VER -f /usr/local/lib/owncloud/occ dav:send-event-reminders
EOF EOF
chmod +x /etc/cron.d/mailinabox-nextcloud chmod +x /etc/cron.d/mailinabox-nextcloud
# Remove previous hourly cronjob # We also need to change the sending mode from background-job to occ.
rm -f /etc/cron.hourly/mailinabox-owncloud # Or else the reminders will just be sent as soon as possible when the background jobs run.
hide_output sudo -u www-data php$PHP_VER -f /usr/local/lib/owncloud/occ config:app:set dav sendEventRemindersMode --value occ
# Now set the config to read-only.
# Do this only at the very bottom when no further occ commands are needed.
sed -i'' "s/'config_is_read_only'\s*=>\s*false/'config_is_read_only' => true/" $STORAGE_ROOT/owncloud/config.php
# Rotate the nextcloud.log file
cat > /etc/logrotate.d/nextcloud <<EOF
# Nextcloud logs
$STORAGE_ROOT/owncloud/nextcloud.log {
size 10M
create 640 www-data www-data
rotate 30
copytruncate
missingok
compress
}
EOF
# There's nothing much of interest that a user could do as an admin for Nextcloud, # There's nothing much of interest that a user could do as an admin for Nextcloud,
# and there's a lot they could mess up, so we don't make any users admins of Nextcloud. # and there's a lot they could mess up, so we don't make any users admins of Nextcloud.
@@ -387,4 +422,4 @@ rm -f /etc/cron.hourly/mailinabox-owncloud
# ``` # ```
# Enable PHP modules and restart PHP. # Enable PHP modules and restart PHP.
restart_service php7.2-fpm restart_service php$PHP_VER-fpm

View File

@@ -7,11 +7,11 @@ if [[ $EUID -ne 0 ]]; then
exit 1 exit 1
fi fi
# Check that we are running on Ubuntu 18.04 LTS (or 18.04.xx). # Check that we are running on Ubuntu 20.04 LTS (or 20.04.xx).
if [ "$(lsb_release -d | sed 's/.*:\s*//' | sed 's/18\.04\.[0-9]/18.04/' )" != "Ubuntu 18.04 LTS" ]; then if [ "$( lsb_release --id --short )" != "Ubuntu" ] || [ "$( lsb_release --release --short )" != "22.04" ]; then
echo "Mail-in-a-Box only supports being installed on Ubuntu 18.04, sorry. You are running:" echo "Mail-in-a-Box only supports being installed on Ubuntu 22.04, sorry. You are running:"
echo echo
lsb_release -d | sed 's/.*:\s*//' lsb_release --description --short
echo echo
echo "We can't write scripts that run on every possible setup, sorry." echo "We can't write scripts that run on every possible setup, sorry."
exit 1 exit 1

View File

@@ -207,6 +207,6 @@ if [ "$PRIVATE_IPV6" != "$PUBLIC_IPV6" ]; then
echo "Private IPv6 Address: $PRIVATE_IPV6" echo "Private IPv6 Address: $PRIVATE_IPV6"
fi fi
if [ -f /usr/bin/git ] && [ -d .git ]; then if [ -f /usr/bin/git ] && [ -d .git ]; then
echo "Mail-in-a-Box Version: " $(git describe) echo "Mail-in-a-Box Version: " $(git describe --always)
fi fi
echo echo

View File

@@ -67,6 +67,10 @@ fi
fi fi
# Create the STORAGE_USER and STORAGE_ROOT directory if they don't already exist. # Create the STORAGE_USER and STORAGE_ROOT directory if they don't already exist.
#
# Set the directory and all of its parent directories' permissions to world
# readable since it holds files owned by different processes.
#
# If the STORAGE_ROOT is missing the mailinabox.version file that lists a # If the STORAGE_ROOT is missing the mailinabox.version file that lists a
# migration (schema) number for the files stored there, assume this is a fresh # 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 # installation to that directory and write the file to contain the current
@@ -77,9 +81,11 @@ fi
if [ ! -d $STORAGE_ROOT ]; then if [ ! -d $STORAGE_ROOT ]; then
mkdir -p $STORAGE_ROOT mkdir -p $STORAGE_ROOT
fi fi
f=$STORAGE_ROOT
while [[ $f != / ]]; do chmod a+rx "$f"; f=$(dirname "$f"); done;
if [ ! -f $STORAGE_ROOT/mailinabox.version ]; then if [ ! -f $STORAGE_ROOT/mailinabox.version ]; then
setup/migrate.py --current > $STORAGE_ROOT/mailinabox.version 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
fi fi
# Save the global options in /etc/mailinabox.conf so that standalone # Save the global options in /etc/mailinabox.conf so that standalone
@@ -161,7 +167,7 @@ if management/status_checks.py --check-primary-hostname; then
echo "If you have a DNS problem put the box's IP address in the URL" echo "If you have a DNS problem put the box's IP address in the URL"
echo "(https://$PUBLIC_IP/admin) but then check the TLS fingerprint:" echo "(https://$PUBLIC_IP/admin) but then check the TLS fingerprint:"
openssl x509 -in $STORAGE_ROOT/ssl/ssl_certificate.pem -noout -fingerprint -sha256\ openssl x509 -in $STORAGE_ROOT/ssl/ssl_certificate.pem -noout -fingerprint -sha256\
| sed "s/SHA256 Fingerprint=//" | sed "s/SHA256 Fingerprint=//i"
else else
echo https://$PUBLIC_IP/admin echo https://$PUBLIC_IP/admin
echo echo
@@ -169,7 +175,7 @@ else
echo the certificate fingerprint matches: echo the certificate fingerprint matches:
echo echo
openssl x509 -in $STORAGE_ROOT/ssl/ssl_certificate.pem -noout -fingerprint -sha256\ openssl x509 -in $STORAGE_ROOT/ssl/ssl_certificate.pem -noout -fingerprint -sha256\
| sed "s/SHA256 Fingerprint=//" | sed "s/SHA256 Fingerprint=//i"
echo echo
echo Then you can confirm the security exception and continue. echo Then you can confirm the security exception and continue.
echo echo

View File

@@ -97,12 +97,13 @@ fi
# come from there and minimal Ubuntu installs may have it turned off. # come from there and minimal Ubuntu installs may have it turned off.
hide_output add-apt-repository -y universe hide_output add-apt-repository -y universe
# Install the certbot PPA.
hide_output add-apt-repository -y ppa:certbot/certbot
# Install the duplicity PPA. # Install the duplicity PPA.
hide_output add-apt-repository -y ppa:duplicity-team/duplicity-release-git hide_output add-apt-repository -y ppa:duplicity-team/duplicity-release-git
# Stock PHP is now 8.1, but we're transitioning through 8.0 because
# of Nextcloud.
hide_output add-apt-repository --y ppa:ondrej/php
# ### Update Packages # ### Update Packages
# Update system packages to make sure we have the latest upstream versions # Update system packages to make sure we have the latest upstream versions
@@ -123,9 +124,6 @@ apt_get_quiet autoremove
# Install basic utilities. # Install basic utilities.
# #
# * haveged: Provides extra entropy to /dev/random so it doesn't stall
# when generating random numbers for private keys (e.g. during
# ldns-keygen).
# * unattended-upgrades: Apt tool to install security updates automatically. # * unattended-upgrades: Apt tool to install security updates automatically.
# * cron: Runs background processes periodically. # * cron: Runs background processes periodically.
# * ntp: keeps the system time correct # * ntp: keeps the system time correct
@@ -139,8 +137,8 @@ apt_get_quiet autoremove
echo Installing system packages... echo Installing system packages...
apt_install python3 python3-dev python3-pip python3-setuptools \ apt_install python3 python3-dev python3-pip python3-setuptools \
netcat-openbsd wget curl git sudo coreutils bc \ netcat-openbsd wget curl git sudo coreutils bc file \
haveged pollinate openssh-client unzip \ pollinate openssh-client unzip \
unattended-upgrades cron ntp fail2ban rsyslog unattended-upgrades cron ntp fail2ban rsyslog
# ### Suppress Upgrade Prompts # ### Suppress Upgrade Prompts
@@ -331,7 +329,7 @@ fi #NODOC
# If more queries than specified are sent, bind9 returns SERVFAIL. After flushing the cache during system checks, # If more queries than specified are sent, bind9 returns SERVFAIL. After flushing the cache during system checks,
# we ran into the limit thus we are increasing it from 75 (default value) to 100. # we ran into the limit thus we are increasing it from 75 (default value) to 100.
apt_install bind9 apt_install bind9
tools/editconf.py /etc/default/bind9 \ tools/editconf.py /etc/default/named \
"OPTIONS=\"-u bind -4\"" "OPTIONS=\"-u bind -4\""
if ! grep -q "listen-on " /etc/bind/named.conf.options; then if ! grep -q "listen-on " /etc/bind/named.conf.options; then
# Add a listen-on directive if it doesn't exist inside the options block. # Add a listen-on directive if it doesn't exist inside the options block.
@@ -375,3 +373,5 @@ cp -f conf/fail2ban/filter.d/* /etc/fail2ban/filter.d/
# scripts will ensure the files exist and then fail2ban is given another # scripts will ensure the files exist and then fail2ban is given another
# restart at the very end of setup. # restart at the very end of setup.
restart_service fail2ban restart_service fail2ban
systemctl enable fail2ban

View File

@@ -19,7 +19,7 @@ fi
echo "Installing Nginx (web server)..." echo "Installing Nginx (web server)..."
apt_install nginx php-cli php-fpm idn2 apt_install nginx php${PHP_VER}-cli php${PHP_VER}-fpm idn2
rm -f /etc/nginx/sites-enabled/default rm -f /etc/nginx/sites-enabled/default
@@ -46,15 +46,15 @@ tools/editconf.py /etc/nginx/nginx.conf -s \
ssl_protocols="TLSv1.2 TLSv1.3;" ssl_protocols="TLSv1.2 TLSv1.3;"
# Tell PHP not to expose its version number in the X-Powered-By header. # Tell PHP not to expose its version number in the X-Powered-By header.
tools/editconf.py /etc/php/7.2/fpm/php.ini -c ';' \ tools/editconf.py /etc/php/$PHP_VER/fpm/php.ini -c ';' \
expose_php=Off expose_php=Off
# Set PHPs default charset to UTF-8, since we use it. See #367. # Set PHPs default charset to UTF-8, since we use it. See #367.
tools/editconf.py /etc/php/7.2/fpm/php.ini -c ';' \ tools/editconf.py /etc/php/$PHP_VER/fpm/php.ini -c ';' \
default_charset="UTF-8" default_charset="UTF-8"
# Configure the path environment for php-fpm # Configure the path environment for php-fpm
tools/editconf.py /etc/php/7.2/fpm/pool.d/www.conf -c ';' \ tools/editconf.py /etc/php/$PHP_VER/fpm/pool.d/www.conf -c ';' \
env[PATH]=/usr/local/bin:/usr/bin:/bin \ env[PATH]=/usr/local/bin:/usr/bin:/bin \
# Configure php-fpm based on the amount of memory the machine has # Configure php-fpm based on the amount of memory the machine has
@@ -64,7 +64,7 @@ tools/editconf.py /etc/php/7.2/fpm/pool.d/www.conf -c ';' \
TOTAL_PHYSICAL_MEM=$(head -n 1 /proc/meminfo | awk '{print $2}' || /bin/true) TOTAL_PHYSICAL_MEM=$(head -n 1 /proc/meminfo | awk '{print $2}' || /bin/true)
if [ $TOTAL_PHYSICAL_MEM -lt 1000000 ] if [ $TOTAL_PHYSICAL_MEM -lt 1000000 ]
then then
tools/editconf.py /etc/php/7.2/fpm/pool.d/www.conf -c ';' \ tools/editconf.py /etc/php/$PHP_VER/fpm/pool.d/www.conf -c ';' \
pm=ondemand \ pm=ondemand \
pm.max_children=8 \ pm.max_children=8 \
pm.start_servers=2 \ pm.start_servers=2 \
@@ -72,7 +72,7 @@ then
pm.max_spare_servers=3 pm.max_spare_servers=3
elif [ $TOTAL_PHYSICAL_MEM -lt 2000000 ] elif [ $TOTAL_PHYSICAL_MEM -lt 2000000 ]
then then
tools/editconf.py /etc/php/7.2/fpm/pool.d/www.conf -c ';' \ tools/editconf.py /etc/php/$PHP_VER/fpm/pool.d/www.conf -c ';' \
pm=ondemand \ pm=ondemand \
pm.max_children=16 \ pm.max_children=16 \
pm.start_servers=4 \ pm.start_servers=4 \
@@ -80,14 +80,14 @@ then
pm.max_spare_servers=6 pm.max_spare_servers=6
elif [ $TOTAL_PHYSICAL_MEM -lt 3000000 ] elif [ $TOTAL_PHYSICAL_MEM -lt 3000000 ]
then then
tools/editconf.py /etc/php/7.2/fpm/pool.d/www.conf -c ';' \ tools/editconf.py /etc/php/$PHP_VER/fpm/pool.d/www.conf -c ';' \
pm=dynamic \ pm=dynamic \
pm.max_children=60 \ pm.max_children=60 \
pm.start_servers=6 \ pm.start_servers=6 \
pm.min_spare_servers=3 \ pm.min_spare_servers=3 \
pm.max_spare_servers=9 pm.max_spare_servers=9
else else
tools/editconf.py /etc/php/7.2/fpm/pool.d/www.conf -c ';' \ tools/editconf.py /etc/php/$PHP_VER/fpm/pool.d/www.conf -c ';' \
pm=dynamic \ pm=dynamic \
pm.max_children=120 \ pm.max_children=120 \
pm.start_servers=12 \ pm.start_servers=12 \
@@ -147,7 +147,7 @@ chown -R $STORAGE_USER $STORAGE_ROOT/www
# Start services. # Start services.
restart_service nginx restart_service nginx
restart_service php7.2-fpm restart_service php$PHP_VER-fpm
# Open ports. # Open ports.
ufw_allow http ufw_allow http

View File

@@ -22,8 +22,9 @@ source /etc/mailinabox.conf # load global vars
echo "Installing Roundcube (webmail)..." echo "Installing Roundcube (webmail)..."
apt_install \ apt_install \
dbconfig-common \ dbconfig-common \
php-cli php-sqlite3 php-intl php-json php-common php-curl php-ldap \ php${PHP_VER}-cli php${PHP_VER}-sqlite3 php${PHP_VER}-intl php${PHP_VER}-common php${PHP_VER}-curl php${PHP_VER}-imap \
php-gd php-pspell tinymce libjs-jquery libjs-jquery-mousewheel libmagic1 php-mbstring php${PHP_VER}-gd php${PHP_VER}-pspell php${PHP_VER}-mbstring libjs-jquery libjs-jquery-mousewheel libmagic1 \
sqlite3
# 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 plugins to track # Combine the Roundcube version number with the commit hash of plugins to track
@@ -35,12 +36,12 @@ apt_install \
# https://github.com/mstilkerich/rcmcarddav/releases # https://github.com/mstilkerich/rcmcarddav/releases
# The easiest way to get the package hashes is to run this script and get the hash from # The easiest way to get the package hashes is to run this script and get the hash from
# the error message. # the error message.
VERSION=1.5.2 VERSION=1.6.4
HASH=208ce4ca0be423cc0f7070ff59bd03588b4439bf HASH=bfc693d6590542d63171e6a3997fc29f0a5f12ca
PERSISTENT_LOGIN_VERSION=59ca1b0d3a02cff5fa621c1ad581d15f9d642fe8 PERSISTENT_LOGIN_VERSION=bde7b6840c7d91de627ea14e81cf4133cbb3c07a # version 5.3
HTML5_NOTIFIER_VERSION=68d9ca194212e15b3c7225eb6085dbcf02fd13d7 # version 0.6.4+ HTML5_NOTIFIER_VERSION=68d9ca194212e15b3c7225eb6085dbcf02fd13d7 # version 0.6.4+
CARDDAV_VERSION=4.3.0 CARDDAV_VERSION=4.4.3
CARDDAV_HASH=4ad7df8843951062878b1375f77c614f68bc5c61 CARDDAV_HASH=74f8ba7aee33e78beb9de07f7f44b81f6071b644
UPDATE_KEY=$VERSION:$PERSISTENT_LOGIN_VERSION:$HTML5_NOTIFIER_VERSION:$CARDDAV_VERSION UPDATE_KEY=$VERSION:$PERSISTENT_LOGIN_VERSION:$HTML5_NOTIFIER_VERSION:$CARDDAV_VERSION
@@ -83,7 +84,7 @@ if [ $needs_update == 1 ]; then
# download and verify the full release of the carddav plugin # download and verify the full release of the carddav plugin
wget_verify \ wget_verify \
https://github.com/blind-coder/rcmcarddav/releases/download/v${CARDDAV_VERSION}/carddav-v${CARDDAV_VERSION}.tar.gz \ https://github.com/mstilkerich/rcmcarddav/releases/download/v${CARDDAV_VERSION}/carddav-v${CARDDAV_VERSION}.tar.gz \
$CARDDAV_HASH \ $CARDDAV_HASH \
/tmp/carddav.tar.gz /tmp/carddav.tar.gz
@@ -115,8 +116,7 @@ cat > $RCM_CONFIG <<EOF;
\$config['log_dir'] = '/var/log/roundcubemail/'; \$config['log_dir'] = '/var/log/roundcubemail/';
\$config['temp_dir'] = '/var/tmp/roundcubemail/'; \$config['temp_dir'] = '/var/tmp/roundcubemail/';
\$config['db_dsnw'] = 'sqlite:///$STORAGE_ROOT/mail/roundcube/roundcube.sqlite?mode=0640'; \$config['db_dsnw'] = 'sqlite:///$STORAGE_ROOT/mail/roundcube/roundcube.sqlite?mode=0640';
\$config['default_host'] = 'ssl://localhost'; \$config['imap_host'] = 'ssl://localhost:993';
\$config['default_port'] = 993;
\$config['imap_conn_options'] = array( \$config['imap_conn_options'] = array(
'ssl' => array( 'ssl' => array(
'verify_peer' => false, 'verify_peer' => false,
@@ -124,7 +124,7 @@ cat > $RCM_CONFIG <<EOF;
), ),
); );
\$config['imap_timeout'] = 15; \$config['imap_timeout'] = 15;
\$config['smtp_server'] = 'tls://127.0.0.1'; \$config['smtp_host'] = 'tls://127.0.0.1';
\$config['smtp_conn_options'] = array( \$config['smtp_conn_options'] = array(
'ssl' => array( 'ssl' => array(
'verify_peer' => false, 'verify_peer' => false,
@@ -141,6 +141,10 @@ cat > $RCM_CONFIG <<EOF;
\$config['login_username_filter'] = 'email'; \$config['login_username_filter'] = 'email';
\$config['password_charset'] = 'UTF-8'; \$config['password_charset'] = 'UTF-8';
\$config['junk_mbox'] = 'Spam'; \$config['junk_mbox'] = 'Spam';
/* ensure roudcube session id's aren't leaked to other parts of the server */
\$config['session_path'] = '/mail/';
/* prevent CSRF, requires php 7.3+ */
\$config['session_samesite'] = 'Strict';
?> ?>
EOF EOF
@@ -154,7 +158,7 @@ cat > ${RCM_PLUGIN_DIR}/carddav/config.inc.php <<EOF;
'name' => 'ownCloud', 'name' => 'ownCloud',
'username' => '%u', // login username 'username' => '%u', // login username
'password' => '%p', // login password 'password' => '%p', // login password
'url' => 'https://${PRIMARY_HOSTNAME}/cloud/remote.php/carddav/addressbooks/%u/contacts', 'url' => 'https://${PRIMARY_HOSTNAME}/cloud/remote.php/dav/addressbooks/users/%u/contacts/',
'active' => true, 'active' => true,
'readonly' => false, 'readonly' => false,
'refresh_time' => '02:00:00', 'refresh_time' => '02:00:00',
@@ -167,7 +171,7 @@ EOF
# Create writable directories. # Create writable directories.
mkdir -p /var/log/roundcubemail /var/tmp/roundcubemail $STORAGE_ROOT/mail/roundcube mkdir -p /var/log/roundcubemail /var/tmp/roundcubemail $STORAGE_ROOT/mail/roundcube
chown -R www-data.www-data /var/log/roundcubemail /var/tmp/roundcubemail $STORAGE_ROOT/mail/roundcube chown -R www-data:www-data /var/log/roundcubemail /var/tmp/roundcubemail $STORAGE_ROOT/mail/roundcube
# Ensure the log file monitored by fail2ban exists, or else fail2ban can't start. # Ensure the log file monitored by fail2ban exists, or else fail2ban can't start.
sudo -u www-data touch /var/log/roundcubemail/errors.log sudo -u www-data touch /var/log/roundcubemail/errors.log
@@ -181,31 +185,40 @@ cp ${RCM_PLUGIN_DIR}/password/config.inc.php.dist \
tools/editconf.py ${RCM_PLUGIN_DIR}/password/config.inc.php \ tools/editconf.py ${RCM_PLUGIN_DIR}/password/config.inc.php \
"\$config['password_minimum_length']=8;" \ "\$config['password_minimum_length']=8;" \
"\$config['password_db_dsn']='sqlite:///$STORAGE_ROOT/mail/users.sqlite';" \ "\$config['password_db_dsn']='sqlite:///$STORAGE_ROOT/mail/users.sqlite';" \
"\$config['password_query']='UPDATE users SET password=%D WHERE email=%u';" \ "\$config['password_query']='UPDATE users SET password=%P WHERE email=%u';" \
"\$config['password_dovecotpw']='/usr/bin/doveadm pw';" \ "\$config['password_algorithm']='sha512-crypt';" \
"\$config['password_dovecotpw_method']='SHA512-CRYPT';" \ "\$config['password_algorithm_prefix']='{SHA512-CRYPT}';"
"\$config['password_dovecotpw_with_method']=true;"
# so PHP can use doveadm, for the password changing plugin # so PHP can use doveadm, for the password changing plugin
usermod -a -G dovecot www-data usermod -a -G dovecot www-data
# set permissions so that PHP can use users.sqlite # set permissions so that PHP can use users.sqlite
# could use dovecot instead of www-data, but not sure it matters # could use dovecot instead of www-data, but not sure it matters
chown root.www-data $STORAGE_ROOT/mail chown root:www-data $STORAGE_ROOT/mail
chmod 775 $STORAGE_ROOT/mail chmod 775 $STORAGE_ROOT/mail
chown root.www-data $STORAGE_ROOT/mail/users.sqlite chown root:www-data $STORAGE_ROOT/mail/users.sqlite
chmod 664 $STORAGE_ROOT/mail/users.sqlite chmod 664 $STORAGE_ROOT/mail/users.sqlite
# Fix Carddav permissions: # Fix Carddav permissions:
chown -f -R root.www-data ${RCM_PLUGIN_DIR}/carddav chown -f -R root:www-data ${RCM_PLUGIN_DIR}/carddav
# root.www-data need all permissions, others only read # root:www-data need all permissions, others only read
chmod -R 774 ${RCM_PLUGIN_DIR}/carddav chmod -R 774 ${RCM_PLUGIN_DIR}/carddav
# Run Roundcube database migration script (database is created if it does not exist) # Run Roundcube database migration script (database is created if it does not exist)
${RCM_DIR}/bin/updatedb.sh --dir ${RCM_DIR}/SQL --package roundcube php$PHP_VER ${RCM_DIR}/bin/updatedb.sh --dir ${RCM_DIR}/SQL --package roundcube
chown www-data:www-data $STORAGE_ROOT/mail/roundcube/roundcube.sqlite chown www-data:www-data $STORAGE_ROOT/mail/roundcube/roundcube.sqlite
chmod 664 $STORAGE_ROOT/mail/roundcube/roundcube.sqlite chmod 664 $STORAGE_ROOT/mail/roundcube/roundcube.sqlite
# Patch the Roundcube code to eliminate an issue that causes postfix to reject our sqlite
# user database (see https://github.com/mail-in-a-box/mailinabox/issues/2185)
sed -i.miabold 's/^[^#]\+.\+PRAGMA journal_mode = WAL.\+$/#&/' \
/usr/local/lib/roundcubemail/program/lib/Roundcube/db/sqlite.php
# Because Roundcube wants to set the PRAGMA we just deleted from the source, we apply it here
# to the roundcube database (see https://github.com/roundcube/roundcubemail/issues/8035)
# Database should exist, created by migration script
sqlite3 $STORAGE_ROOT/mail/roundcube/roundcube.sqlite 'PRAGMA journal_mode=WAL;'
# Enable PHP modules. # Enable PHP modules.
phpenmod -v php mcrypt imap phpenmod -v $PHP_VER imap
restart_service php7.2-fpm restart_service php$PHP_VER-fpm

View File

@@ -17,13 +17,13 @@ source /etc/mailinabox.conf # load global vars
echo "Installing Z-Push (Exchange/ActiveSync server)..." echo "Installing Z-Push (Exchange/ActiveSync server)..."
apt_install \ apt_install \
php-soap php-imap libawl-php php-xsl php${PHP_VER}-soap php${PHP_VER}-imap libawl-php php$PHP_VER-xml
phpenmod -v php imap phpenmod -v $PHP_VER imap
# Copy Z-Push into place. # Copy Z-Push into place.
VERSION=2.6.2 VERSION=2.7.1
TARGETHASH=f0e8091a8030e5b851f5ba1f9f0e1a05b8762d80 TARGETHASH=f15c566b1ad50de24f3f08f505f0c3d8155c2d0d
needs_update=0 #NODOC needs_update=0 #NODOC
if [ ! -f /usr/local/lib/z-push/version ]; then if [ ! -f /usr/local/lib/z-push/version ]; then
needs_update=1 #NODOC needs_update=1 #NODOC
@@ -41,9 +41,15 @@ if [ $needs_update == 1 ]; then
mv /tmp/z-push/*/src /usr/local/lib/z-push mv /tmp/z-push/*/src /usr/local/lib/z-push
rm -rf /tmp/z-push.zip /tmp/z-push rm -rf /tmp/z-push.zip /tmp/z-push
# Create admin and top scripts with PHP_VER
rm -f /usr/sbin/z-push-{admin,top} rm -f /usr/sbin/z-push-{admin,top}
ln -s /usr/local/lib/z-push/z-push-admin.php /usr/sbin/z-push-admin echo '#!/bin/bash' > /usr/sbin/z-push-admin
ln -s /usr/local/lib/z-push/z-push-top.php /usr/sbin/z-push-top echo php$PHP_VER /usr/local/lib/z-push/z-push-admin.php '"$@"' >> /usr/sbin/z-push-admin
chmod 755 /usr/sbin/z-push-admin
echo '#!/bin/bash' > /usr/sbin/z-push-top
echo php$PHP_VER /usr/local/lib/z-push/z-push-top.php '"$@"' >> /usr/sbin/z-push-top
chmod 755 /usr/sbin/z-push-top
echo $VERSION > /usr/local/lib/z-push/version echo $VERSION > /usr/local/lib/z-push/version
fi fi
@@ -102,8 +108,8 @@ EOF
# Restart service. # Restart service.
restart_service php7.2-fpm restart_service php$PHP_VER-fpm
# Fix states after upgrade # Fix states after upgrade
hide_output z-push-admin -a fixstates hide_output php$PHP_VER /usr/local/lib/z-push/z-push-admin.php -a fixstates

View File

@@ -30,7 +30,7 @@ def test(server, description):
(hostname, "TXT", "\"v=spf1 mx -all\""), (hostname, "TXT", "\"v=spf1 mx -all\""),
("mail._domainkey." + hostname, "TXT", "\"v=DKIM1; k=rsa; s=email; \" \"p=__KEY__\""), ("mail._domainkey." + hostname, "TXT", "\"v=DKIM1; k=rsa; s=email; \" \"p=__KEY__\""),
#("_adsp._domainkey." + hostname, "TXT", "\"dkim=all\""), #("_adsp._domainkey." + hostname, "TXT", "\"dkim=all\""),
("_dmarc." + hostname, "TXT", "\"v=DMARC1; p=quarantine\""), ("_dmarc." + hostname, "TXT", "\"v=DMARC1; p=quarantine;\""),
] ]
return test2(tests, server, description) return test2(tests, server, description)
@@ -48,7 +48,7 @@ def test2(tests, server, description):
for qname, rtype, expected_answer in tests: for qname, rtype, expected_answer in tests:
# do the query and format the result as a string # do the query and format the result as a string
try: try:
response = dns.resolver.query(qname, rtype) response = dns.resolver.resolve(qname, rtype)
except dns.resolver.NoNameservers: except dns.resolver.NoNameservers:
# host did not have an answer for this query # host did not have an answer for this query
print("Could not connect to %s for DNS query." % server) print("Could not connect to %s for DNS query." % server)

View File

@@ -48,7 +48,7 @@ server = smtplib.SMTP_SSL(host)
ipaddr = socket.gethostbyname(host) # IPv4 only! ipaddr = socket.gethostbyname(host) # IPv4 only!
reverse_ip = dns.reversename.from_address(ipaddr) # e.g. "1.0.0.127.in-addr.arpa." reverse_ip = dns.reversename.from_address(ipaddr) # e.g. "1.0.0.127.in-addr.arpa."
try: try:
reverse_dns = dns.resolver.query(reverse_ip, 'PTR')[0].target.to_text(omit_final_dot=True) # => hostname reverse_dns = dns.resolver.resolve(reverse_ip, 'PTR')[0].target.to_text(omit_final_dot=True) # => hostname
except dns.resolver.NXDOMAIN: except dns.resolver.NXDOMAIN:
print("Reverse DNS lookup failed for %s. SMTP EHLO name check skipped." % ipaddr) print("Reverse DNS lookup failed for %s. SMTP EHLO name check skipped." % ipaddr)
reverse_dns = None reverse_dns = None

View File

@@ -14,6 +14,10 @@
# #
# NAME VALUE # NAME VALUE
# #
# If the -e option is given and VALUE is empty, the setting is removed
# from the configuration file if it is set (i.e. existing occurrences
# are commented out and no new setting is added).
#
# If the -c option is given, then the supplied character becomes the comment character # 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
@@ -35,6 +39,7 @@ settings = sys.argv[2:]
delimiter = "=" delimiter = "="
delimiter_re = r"\s*=\s*" delimiter_re = r"\s*=\s*"
erase_setting = False
comment_char = "#" comment_char = "#"
folded_lines = False folded_lines = False
testing = False testing = False
@@ -44,6 +49,9 @@ while settings[0][0] == "-" and settings[0] != "--":
# Space is the delimiter # Space is the delimiter
delimiter = " " delimiter = " "
delimiter_re = r"\s+" delimiter_re = r"\s+"
elif opt == "-e":
# Erase settings that have empty values.
erase_setting = True
elif opt == "-w": elif opt == "-w":
# Line folding is possible in this file. # Line folding is possible in this file.
folded_lines = True folded_lines = True
@@ -68,7 +76,8 @@ for setting in settings:
found = set() found = set()
buf = "" buf = ""
input_lines = list(open(filename)) with open(filename, "r") as f:
input_lines = list(f)
while len(input_lines) > 0: while len(input_lines) > 0:
line = input_lines.pop(0) line = input_lines.pop(0)
@@ -81,7 +90,7 @@ while len(input_lines) > 0:
# See if this line is for any settings passed on the command line. # See if this line is for any settings passed on the command line.
for i in range(len(settings)): for i in range(len(settings)):
# Check that this line contain this setting from the command-line arguments. # Check if this line contain this setting from the command-line arguments.
name, val = settings[i].split("=", 1) name, val = settings[i].split("=", 1)
m = re.match( m = re.match(
"(\s*)" "(\s*)"
@@ -91,8 +100,10 @@ while len(input_lines) > 0:
if not m: continue if not m: continue
indent, is_comment, existing_val = m.groups() indent, is_comment, existing_val = m.groups()
# If this is already the setting, do nothing. # If this is already the setting, keep it in the file, except:
if is_comment is None and existing_val == val: # * If we've already seen it before, then remove this duplicate line.
# * If val is empty and erase_setting is on, then comment it out.
if is_comment is None and existing_val == val and not (not val and erase_setting):
# It may be that we've already inserted this setting higher # It may be that we've already inserted this setting higher
# in the file so check for that first. # in the file so check for that first.
if i in found: break if i in found: break
@@ -107,8 +118,9 @@ while len(input_lines) > 0:
# the line is already commented, pass it through # the line is already commented, pass it through
buf += line buf += line
# if this option oddly appears more than once, don't add the setting again # if this option already is set don't add the setting again,
if i in found: # or if we're clearing the setting with -e, don't add it
if (i in found) or (not val and erase_setting):
break break
# add the new setting # add the new setting
@@ -122,11 +134,13 @@ while len(input_lines) > 0:
# If did not match any setting names, pass this line through. # If did not match any setting names, pass this line through.
buf += line buf += line
# Put any settings we didn't see at the end of the file. # Put any settings we didn't see at the end of the file,
# except settings being cleared.
for i in range(len(settings)): for i in range(len(settings)):
if i not in found: if i not in found:
name, val = settings[i].split("=", 1) name, val = settings[i].split("=", 1)
buf += name + delimiter + val + "\n" if not (not val and erase_setting):
buf += name + delimiter + val + "\n"
if not testing: if not testing:
# Write out the new file. # Write out the new file.

View File

@@ -26,7 +26,7 @@ if [ ! -f $1/config.php ]; then
fi fi
echo "Restoring backup from $1" echo "Restoring backup from $1"
service php7.2-fpm stop service php8.0-fpm stop
# remove the current ownCloud/Nextcloud installation # remove the current ownCloud/Nextcloud installation
rm -rf /usr/local/lib/owncloud/ rm -rf /usr/local/lib/owncloud/
@@ -40,10 +40,10 @@ cp "$1/owncloud.db" $STORAGE_ROOT/owncloud/
cp "$1/config.php" $STORAGE_ROOT/owncloud/ cp "$1/config.php" $STORAGE_ROOT/owncloud/
ln -sf $STORAGE_ROOT/owncloud/config.php /usr/local/lib/owncloud/config/config.php ln -sf $STORAGE_ROOT/owncloud/config.php /usr/local/lib/owncloud/config/config.php
chown -f -R www-data.www-data $STORAGE_ROOT/owncloud /usr/local/lib/owncloud chown -f -R www-data:www-data $STORAGE_ROOT/owncloud /usr/local/lib/owncloud
chown www-data.www-data $STORAGE_ROOT/owncloud/config.php chown www-data:www-data $STORAGE_ROOT/owncloud/config.php
sudo -u www-data php /usr/local/lib/owncloud/occ maintenance:mode --off sudo -u www-data php$PHP_VER /usr/local/lib/owncloud/occ maintenance:mode --off
service php7.2-fpm start service php8.0-fpm start
echo "Done" echo "Done"

View File

@@ -20,4 +20,4 @@ echo
echo Press enter to continue. echo Press enter to continue.
read read
sudo -u www-data php /usr/local/lib/owncloud/occ group:adduser admin $ADMIN && echo Done. sudo -u www-data php$PHP_VER /usr/local/lib/owncloud/occ group:adduser admin $ADMIN && echo Done.

View File

@@ -17,13 +17,8 @@ accesses = set()
# Scan the current and rotated access logs. # Scan the current and rotated access logs.
for fn in glob.glob("/var/log/nginx/access.log*"): for fn in glob.glob("/var/log/nginx/access.log*"):
# Gunzip if necessary. # Gunzip if necessary.
if fn.endswith(".gz"):
f = gzip.open(fn)
else:
f = open(fn, "rb")
# Loop through the lines in the access log. # Loop through the lines in the access log.
with f: with (gzip.open if fn.endswith(".gz") else open)(fn, "rb") as f:
for line in f: for line in f:
# Find lines that are GETs on the bootstrap script by either curl or wget. # Find lines that are GETs on the bootstrap script by either curl or wget.
# (Note that we purposely skip ...?ping=1 requests which is the admin panel querying us for updates.) # (Note that we purposely skip ...?ping=1 requests which is the admin panel querying us for updates.)
@@ -43,7 +38,8 @@ for date, ip in accesses:
# Since logs are rotated, store the statistics permanently in a JSON file. # Since logs are rotated, store the statistics permanently in a JSON file.
# Load in the stats from an existing file. # Load in the stats from an existing file.
if os.path.exists(outfn): if os.path.exists(outfn):
existing_data = json.load(open(outfn)) with open(outfn, "r") as f:
existing_data = json.load(f)
for date, count in existing_data: for date, count in existing_data:
if date not in by_date: if date not in by_date:
by_date[date] = count by_date[date] = count

View File

@@ -124,13 +124,14 @@ def generate_documentation():
""") """)
parser = Source.parser() parser = Source.parser()
for line in open("setup/start.sh"): with open("setup/start.sh", "r") as start_file:
try: for line in start_file:
fn = parser.parse_string(line).filename() try:
except: fn = parser.parse_string(line).filename()
continue except:
if fn in ("setup/start.sh", "setup/preflight.sh", "setup/questions.sh", "setup/firstuser.sh", "setup/management.sh"): continue
continue if fn in ("setup/start.sh", "setup/preflight.sh", "setup/questions.sh", "setup/firstuser.sh", "setup/management.sh"):
continue
import sys import sys
print(fn, file=sys.stderr) print(fn, file=sys.stderr)
@@ -401,7 +402,8 @@ class BashScript(Grammar):
@staticmethod @staticmethod
def parse(fn): def parse(fn):
if fn in ("setup/functions.sh", "/etc/mailinabox.conf"): return "" if fn in ("setup/functions.sh", "/etc/mailinabox.conf"): return ""
string = open(fn).read() with open(fn, "r") as f:
string = f.read()
# tokenize # tokenize
string = re.sub(".* #NODOC\n", "", string) string = re.sub(".* #NODOC\n", "", string)