Compare commits

...

1961 commits
v0.02 ... main

Author SHA1 Message Date
beenull
4d68d1f08e Update setup/bootstrap.sh 2024-10-19 12:35:36 +00:00
David Duque
197aa5fdaa
Normalize line-endings 2023-02-12 18:08:25 +00:00
David Duque
f3994d8b89
Fixing the previous commit 2023-02-12 18:07:01 +00:00
David Duque
222ae0d960
For folks on Windows: Unix line-endings
Ensure that folks on windows Workspace still get Unix line-endings.
Might prevent cases where setup scripts will refuse to run.
2023-02-12 18:01:57 +00:00
David Duque
701e214929 v60.5 2022-11-21 02:21:31 +00:00
David Duque
1d857c7f00 Update SMTP Relay docs 2022-11-21 02:11:15 +00:00
David Duque
d5f327755c vagrant: Remove unneeded configuration step
* Local IPv6 is now assured by the setup script itself
2022-11-21 02:05:34 +00:00
David Duque
9193dbe56b Software Updates
* FontAwesome to v6.2.1
2022-11-21 02:04:28 +00:00
David Duque
289f311c4f Hide some unnecessary configuration output 2022-11-21 01:54:18 +00:00
David Duque
465bd4dc57 SMTP Relays: Allow the user to not configure DKIM
* Not all relays provide their own DKIM signatures (which is ok)
* Closes #64
2022-11-21 01:50:33 +00:00
David Duque
f9815662c1 S3: Fix display issue with current configuration
* Especially confusing when using non-AWS buckets
2022-11-20 22:36:08 +00:00
David Duque
74d88787c8 S3: Handle the bucket path not having separators
* Fixes #98
2022-11-20 21:40:21 +00:00
Dom
9215b011f8
Fix bug with quota field detection (#97)
* Fix bug with quota field detection

This is related to Issue #96 "Upgrade to v60.4 fails"

* Update setup/mail-users.sh

sqlite_schema isn't supported everywhere yet

Co-authored-by: David Duque <github@duqued.net>
2022-11-20 18:35:44 +00:00
David Duque
748adfebd9 v60.4 2022-11-07 22:07:35 +00:00
David Duque
1f778bc94c Hotfix 2022-11-07 22:07:24 +00:00
David Duque
6abed54756 Nextcloud: update to v24.0.7 2022-11-07 21:49:29 +00:00
David Duque
b5bc886561 Create a command alias for management/cli.py 2022-11-07 21:33:43 +00:00
David Duque
1af976a093 Move setup cli command to /usr/local/sbin 2022-11-07 21:28:04 +00:00
David Duque
3451dadde5
Roundcube: Use Mail-in-a-Box admin API to drive password changes (#92)
* Use Mail-in-a-Box driver
We're using the user's own credentials to authenticate themselves.
There are some issues if we release as-is:
* Only usable if the user in question is an admin
* Cannot be used if the user has 2FA enabled

* daemon: Add selective gatekeeper
* Allows us to give access to features for logged in, non-admin users

* Allow non-admins to change their own password

* Begin password management self service, frontend

* Allow all users to enable 2FA

* Password change front-end form

* Self password change front-end functionality

* Force logout after successful password change

* Clear fields after successful password change, also fix error modal
2022-11-07 21:07:37 +00:00
David Duque
b961a2b74a Hook version checking to the version router 2022-11-06 22:15:25 +00:00
David Duque
774ab4f1e8 "Recover" database if hit by #85 2022-11-06 22:14:35 +00:00
David Duque
26714cee49
Only ensure IPv6 is enabled at the loopback level
We shouldn't exactly try forcing IPv6 to be enabled everywhere.
2022-11-02 09:51:07 +00:00
David Duque
1efa0ed408 v60.3 2022-10-30 23:05:36 +00:00
David Duque
97683aa9ba Upgrading and version pinning quick start
* Fixes #84
2022-10-30 22:27:43 +00:00
David Duque
be678a126e Remove nextcloud config lock before upgrading
Fixes #86
2022-10-30 18:39:25 +00:00
David Duque
662639d905 Software updates
* Nextcloud: Calendar plugin to v3.5.2
* Webmail: CardDAV/CalDAV plugin to v4.4.4
2022-10-30 17:10:18 +00:00
David Duque
03b36c958c Merge v60.1 (from upstream) 2022-10-30 17:02:05 +00: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
David Duque
ed7859eab8 v60.2 2022-10-20 00:36:29 +01:00
David Duque
003ff10d07 Fix editconf issue
See mail-in-a-box/mailinabox#2137
2022-10-20 00:33:47 +01:00
David Duque
8b4a49597e v60.1 2022-10-18 21:58:46 +01:00
David Duque
01cf61deab gettext is a build dependency for duplicity
* Fixes #81
2022-10-18 20:36:18 +01:00
David Duque
619d3781f5 noreply addresses: Use correct db path
* Fixes #82
2022-10-18 20:35:25 +01:00
David Duque
025494ed9c v60.0 2022-10-17 03:41:20 +01:00
David Duque
824762d18d Last-minute setup patches 2022-10-17 03:40:08 +01:00
David Duque
9243ea9374 Software Updates
* Nextcloud to 24.0.6
* * Contacts to 4.2.2
* JQuery to 3.6.1
* Bootstrap to 5.2.2
* FontAwesome to 6.2.0
2022-10-17 02:56:03 +01:00
David Duque
132cfe6e32 Enable local ipv6 at kernel level
* We need :: for nsd
2022-10-17 02:45:31 +01:00
David Duque
bc72c58ae5 Move away from doveadm pw for password changes
* Fixes #77
2022-10-17 02:36:38 +01:00
David Duque
866c185a08 Fix #66
Make sure OOO notices are sent as if the recipients sent them
2022-10-16 23:26:00 +01:00
David Duque
97a84bc2b8 Editconf: merge from v60 2022-10-16 20:56:31 +01:00
David Duque
6c1a98dc4b Status Checks on packages
* Pending upgrades will be written as warnings instead of errors
* Removed Ubuntu Pro advertisment (hacky solution,
need to check for consequences)
2022-10-16 20:46:54 +01:00
David Duque
a03707e5f8 Confirm boto3 installation 2022-10-16 20:46:04 +01:00
David Duque
913a4a4585 Alternative duplicity installation (build from pip) 2022-10-16 19:21:07 +01:00
David Duque
c30a18a30b Import changes from upstream v60 2022-10-16 18:26:39 +01: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
David Duque
9700b59cf8 Bump test machine RAM allocation
* Fixes kernel panics on Jammy Jellyfish
2022-09-10 16:07:37 +01:00
David Duque
e7ee4bc5b4 Fix Nextcloud installation step 2022-09-10 15:52:17 +01:00
David Duque
dc787b67b4 Handle the case where the raw spf record exists but is None
* Fixes #70
2022-09-09 16:29:01 +01:00
David Duque
09cf3b3755 Allow dashes in DKIM selectors
(when they're not the first character)
* Fixes #68
2022-09-08 16:00:08 +01:00
David Duque
fc04c8d723 Nextcloud: Implement old version safeguards 2022-09-08 13:05:41 +01:00
Johga
2aa4229e0b
fix error when user_external version under 3.0.0 (#69) 2022-09-08 13:03:51 +01:00
David Duque
e1be9a5eeb
Ubuntu 22.04 support (#59)
* Vagrantfile: Add Ubuntu 22.04 image

* Recognize Ubuntu 22.04 as supported

* Bump nextcloud to v24.0.0

* Bump Roundcube to 1.6-beta

Still waiting for the final release to come out

* Fix version checking functions

* NextCloud fixes

* Update Roundcube config

* Bump roundcube to 1.6-rc

* FIx nextcloud installation step

* rcm: Update CardDAV plugin to v4.4.0 (Guzzle v7)

* Fix STORAGE_ROOT permissions

* Update RC CardDAV plugin to v4.4.1

* Unpin b2sdk for Ubuntu 22.04

* Comment fix

* Drop support for Debian 10 from this point forward

* Software Updates
* Nextcloud: 24.0.2
* Nextcloud Calendar: 3.4.2
* Roundcube CardDAV: 4.4.2

* Update Roundcube to v1.6.0

* Update Nextcloud to v24.0.3
* Contacts to v4.2.0

* Upgrade Nextcloud to v24.0.4
* Calendar to v3.5.0

Webmail:
* CardDAV to v4.4.3
2022-09-08 12:26:39 +01: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
Joshua Tauberer
3c3d62ac27 Version 57a 2022-06-19 08:58:09 -04:00
Joshua Tauberer
d829d74048 Pin b2sdk to version 1.14.1 in the virtualenv also
We install b2sdk in two places: Once globally for duplicity (see
9d8fdef9915127f016eb6424322a149cdff25d7 for #2125) and once in
a virtualenv used by our control panel. The latter wasn't pinned
when the former was but should be to fix new Python compatibility
issues.

Anyone who updated Python packages recently (so anyone who upgraded
Mail-in-a-Box) started encountering these issues.

Fixes #2131.

See https://discourse.mailinabox.email/t/backblaze-b2-backup-not-working-since-v57/9231.
2022-06-18 13:15:59 -04:00
Joshua Tauberer
2aca421415 Version 57 2022-06-12 08:18:42 -04:00
Joshua Tauberer
99474b348f Update backup to be compatible with duplicity 0.8.23
We were using duplicity 0.8.21-ppa202111091602~ubuntu1 from the duplicity PPA probably until June 5, which is when my box automatically updated to 0.8.23-ppa202205151528~ubuntu18.04.1. Starting with that version, two changes broke backups:

* The default s3 backend was changed to boto3. But boto3 depends on the AWS SDK which does not support Ubuntu 18.04, so we can't install it. Instead, we map s3: backup target URLs to the boto+s3 scheme which tells duplicity to use legacy boto. This should be reverted when we can switch to boto3.
* Contrary to the documentation, the s3 target no longer accepts a S3 hostname in the URL. It now reads the bucket from the hostname part of the URL. So we now drop the hostname from our target URL before passing it to duplicity and we pass the endpoint URL in a separate command-line argument. (The boto backend was dropped from duplicity's "uses_netloc" in 74d4cf44b1 (f5a07610d36bd242c3e5b98f8348879a468b866a_37_34), but other changes may be related.)

The change of target URL (due to both changes) seems to also cause duplicity to store cached data in a different directory within $STORAGE_ROOT/backup/cache, so on the next backup it will re-download cached manifest/signature files. Since the cache directory will still hold the prior data which is no longer needed, it might be a good idea to clear out the cache directory to save space. A system status checks message is added about that.

Fixes #2123
2022-06-12 08:17:48 -04:00
Joshua Tauberer
8bebaf6a48 Simplify duplicity command line by omitting rsync options if the backup target type is not rsync 2022-06-11 15:12:31 -04:00
David Duque
a0d44f3d05 v56.5 2022-06-07 11:38:10 +01:00
David Duque
55dbd9d93c Nextcloud user permission fixes 2022-06-07 11:24:12 +01:00
David Duque
3503b5a1f0 Update nextcloud to v23.0.5
* Contacts to v4.1.1
* Calendar to 3.3.1
2022-06-07 11:18:22 +01:00
David Duque
ddcc929a13 Status Checks: Fix "Show More" button not showing 2022-06-07 11:10:25 +01:00
jbandholz
9004bb6e8e
Add IPV6 addresses to fail2ban ignoreip (#2069)
Update jails.conf to include IPV6 localhost and external ip to ignoreip line.  Update system.sh to include IPV6 address in replacement.  See mail-in-a-box#2066 for details.
2022-06-05 09:40:54 -04:00
m-picc
69d8fdef99
Specify b2sdk version 1.14.1 (#2125)
pin b2sdk version to 1.14.1 to resolve exception that occurs when attempting to use backblaze backups. See https://github.com/mail-in-a-box/mailinabox/issues/2124 for details.
2022-06-05 09:24:32 -04:00
Austin Ewens
eeee712cf3
Switched to using tags over releases for NextCloud contacts/calendar (#2105)
See [mailinabox issue #2088](https://github.com/mail-in-a-box/mailinabox/issues/2088). This also updates the commit hashes to for anyone updating from NextCloud version 17 (as shown in the related issue) since a different hash is used for tags vs releases.

This was tested and verified to work on a setup previously running v0.44 and then updating to the latest version (v56).
2022-05-04 17:09:53 -04:00
Joshua Tauberer
8f42d97b54
Merge pull request #2109 from lamberete/main 2022-05-04 17:08:48 -04:00
David Duque
04f4910b51 Fix Nextcloud admin unlocking 2022-04-27 23:23:53 +01:00
David Duque
e5ffae7791 Upgrade Nextcloud to v23.0.4, External User Backend to v3.0.0 2022-04-27 23:22:55 +01:00
David Duque
7df762b46b v56.4 2022-03-29 17:29:20 +01:00
David Duque
c15c1d84d7 More assertive notice of unsupported versions 2022-03-29 17:22:58 +01:00
David Duque
f605e55997 Handle the absence of openssh-server
It is not strictly required for us to have sshd installed,
for example on baremetal machines where shell access
is physical-only.

Instead we'll skip certain tasks that depend on sshd if
it is not installed.
2022-03-29 17:14:06 +01:00
lamberete
6e40c69cb5
Error message using IPv4 instead of failing IPv6.
One of the error messages around IPv6 was using the IPv4 for the output, making the error message confusing.
2022-03-26 13:50:24 +01:00
lamberete
c0e54f87d7
Sorting ds records on report.
When building the part of the report about the current DS records founded, they are added in the same order as they were received when calling query_dns(), which can differ from run to run. This was making the difflib.SequenceMatcher() method to find the same line removed and added one line later, and sending an Status Checks Change Notice email with the same line added and removed when there was actually no real changes.
2022-03-26 13:45:49 +01:00
David Duque
aa0994b1d7
Update README 2022-03-22 18:32:35 +00:00
David Duque
1fcc1a489b
v56.3 2022-03-22 18:25:36 +00:00
David Duque
9defa6c54d
Add ko-fi button on the README 2022-03-22 18:24:47 +00:00
David Duque
53c0c3aec8
Create FUNDING.yml 2022-03-22 18:23:02 +00:00
David Duque
6bcf1169bd
Project home is now at power-mailinabox.net 2022-03-22 18:05:02 +00:00
David Duque
fece9355cf
Update NextCloud to v23.0.3 2022-03-22 18:01:51 +00:00
David Duque
83a109908e
Remove non-working copy button 2022-03-22 16:47:32 +00:00
David Duque
e4cbbccfde Dark mode - add a different color for disabled form elements 2022-03-22 16:43:25 +00:00
David Duque
a56bb235d7
Upgrade FontAwesome to v6.1.1, Persistent Login Plugin to v5.3.0 2022-03-22 16:41:54 +00:00
David Duque
6602d2c774
Merge from main 2022-02-20 20:45:14 +00:00
David Duque
2cee801002
v56.2 2022-02-18 01:07:01 +00:00
David Duque
d8d742d74d
Update contacts to v4.0.8, cal to v3.0.6 2022-02-18 00:56:26 +00:00
David Duque
d5069fc81e
README adjustments 2022-02-18 00:47:06 +00:00
David Duque
4da6f66b94
Update Postfix TLS configuration (#45)
* Update the list of very old ciphers that shouldn't be used at all
* Enforce cipher preference server side
2022-02-18 00:43:13 +00:00
David Duque
307c678066
Add a note post-configuration about DMARC configurations 2022-02-18 00:28:05 +00:00
David Duque
c111a8920c
Adjust the box's own DKIM selector when the relay provider wants the 'mail' selector 2022-02-18 00:25:03 +00:00
David Duque
115fee4212
SMTP Relays: Allow the user to paste an SPF record directly from their provider 2022-02-18 00:03:41 +00:00
David Duque
51fa2a6fd9
Change the SMTP banner as not to disclose the operating system (which was set to be always 'Debian') 2022-02-17 23:28:20 +00:00
David Duque
deaecbe0d2
Fix nextcloud issue piping stderr to a file 2022-02-17 23:27:45 +00:00
David Duque
56b0fc02da
Install libsasl2-modules (#44)
Required for SMTP relays to work
2022-02-17 23:25:01 +00:00
David Duque
a8f5a78518
Add some level of style guidance, reformat files 2022-02-04 23:26:24 +00:00
David Duque
14534fd0cb
Fix nextcloud cleanup typo 2022-01-31 16:10:28 +00:00
David Duque
a0abc9c0d2
Vagrant: bump nfs to v4 for all images 2022-01-31 00:54:29 +00:00
David Duque
6b4ee8f576
Merge from main 2022-01-31 00:53:59 +00:00
David Duque
ed886ee934
Admin panel refactoring (#41) 2022-01-31 00:52:22 +00:00
David Duque
16b521c2f0
v56.1 2022-01-24 01:52:48 +00:00
David Duque
6436bad483
Recreate Python env after an in-place OS upgrade
Fixes #42
2022-01-24 01:48:34 +00:00
David Duque
271b03a346
v56.0 2022-01-22 20:00:01 +00:00
David Duque
37a97a394f
Suppress complaints about owncloud not existing (legacy setup code) 2022-01-22 19:58:03 +00:00
David Duque
152d7c39bc
Band-aid fix for B2 backups on Debian 10 2022-01-22 18:53:49 +00:00
David Duque
b622df6b03
Update Software:
- Bootstrap to v4.6.1
- FontAwesome to v5.15.4
- NextCloud Calendar to v3.0.5
- Roundcube Persistent Login to latest commit
2022-01-20 15:49:06 +00:00
David Duque
cd9bd51ed0
Import changes from upstream (v56) 2022-01-20 15:02:16 +00:00
Joshua Tauberer
3a7de051ee Version 56 (January 19, 2022) 2022-01-19 16:59:34 -05:00
Darek Kowalski
f11cb04a72
Update Vagrant private IP address, fix issue #2062 (#2064) 2022-01-08 18:29:23 -05:00
Joshua Tauberer
cb564a130a Fix DNS secondary nameserver refesh failure retry period
Fixes #1979
2022-01-08 09:38:41 -05:00
Joshua Tauberer
d1d6318862 Set systemd journald log retention to 10 days (from no limit) to reduce disk usage 2022-01-08 09:11:48 -05:00
Joshua Tauberer
34b7a02f4f Update Roundcube to 1.5.2 2022-01-08 09:00:12 -05:00
Joshua Tauberer
a312acc3bc Update to Nextcloud 20.0.8 and update apps 2022-01-08 09:00:12 -05:00
Joshua Tauberer
aab1ec691c CHANGELOG entries 2022-01-08 07:46:24 -05:00
Erik Hennig
520caf6557
fix: typo in system backup template (#2081) 2022-01-02 08:11:41 -05:00
jvolkenant
c92fd02262
Don't die if column already exists on Nextcloud 18 upgrade (#2078) 2021-12-25 10:17:34 -05:00
Arno Hautala
a85c429a85
regex change to exclude comma from sasl_username (#2074)
as proposed in #2071 by @jvolkenant
2021-12-19 08:33:59 -05:00
Ilnahro
50a5cb90bc
Include rsync to the installed basic packages (#2067)
Some VPS providers strip this package from their Ubuntu 18.04 VM images. This will help avoid errors.
2021-11-30 19:50:01 -05:00
steadfasterX
aac878dce5
fix: key flag id for KSK, fix format (#2063)
as mentioned (https://github.com/mail-in-a-box/mailinabox/pull/2033#issuecomment-976365087) KSK is 257, not 256
2021-11-23 11:06:17 -05:00
David Duque
4812ffd6b3
v55.1 2021-11-17 22:58:33 +00:00
David Duque
2e3b113756
Move from 'master' to 'main' 2021-11-17 22:57:46 +00:00
David Duque
faacc1d674
Update to above: Bail only at the second failure. 2021-11-17 22:55:18 +00:00
David Duque
5e66af63f2
Don't bail if pollinate fails 2021-11-17 22:42:45 +00:00
David Duque
972c413b19
nginx: Overhaul upstream directives (#38)
* Add a way for users to specify custom upstream directives via .upstream.conf file;
* MiaB-managed applications and user applications will now use different sockets;
2021-11-17 22:41:36 +00:00
jvolkenant
58b0323b36
Update persistent_login for Roundcube 1.5 (#2055) 2021-11-04 18:59:10 -04:00
kiekerjan
646f971d8b
Update mailinabox.yml (#2054)
The examples for login and logout use GET instead of POST. GET gives me an error when using it, while POST seems to work.
2021-10-31 12:49:26 -04:00
Felix Spöttel
86067be142
fix(docs): set a schema for /logout responses (#2051)
* this remedies an OpenAPI syntax violation resulting in a redoc-cli crash
2021-10-27 12:27:54 -04:00
Joshua Tauberer
c67ff241c4
Updates to security.md 2021-10-23 08:57:05 -04:00
Joshua Tauberer
7b4cd443bf
How to report security issues 2021-10-22 18:49:16 -04:00
Joshua Tauberer
34017548d5 Don't crash if a custom DNS entry is not under a zone managed by the box, fixes #1961 2021-10-22 18:39:53 -04:00
David Duque
109267c7b0
v55.0 2021-10-19 21:12:17 +01:00
David Duque
cb7cbe419e
Update external software checksums 2021-10-19 21:08:24 +01:00
David Duque
dab920042e
Update external software 2021-10-19 21:05:19 +01:00
David Duque
402df4515b
Fix weird grammar
Fixes #33
2021-10-19 18:44:51 +01:00
David Duque
7caa4f5c4e
Disable dark mode CSS for now
It doesn't play nice with the current admin panel. I'll design a better dark mode in the future.
2021-10-19 17:26:05 +01:00
David
856260bf29
Check for lsb_release, and if it doesn't exist, install it 2021-10-19 14:43:01 +01:00
David
a6c8bbed20
Fix incorrect info about passwords 2021-10-19 14:42:36 +01:00
David
f6d7d5689c
Merge v55 from upstream 2021-10-19 14:42:07 +01:00
David
b757e4c339
(Admin Panel) Fix an issue where all TXT records from a given qname would be deleted 2021-10-19 13:07:34 +01:00
David Duque
8a0805dae8
Backups: When rsync option is selected, allow modifying the target port (#30) 2021-10-19 11:20:41 +01:00
Joshua Tauberer
65861c68b7 Version 55 2021-10-18 20:40:51 -04:00
Joshua Tauberer
71a7a3e201 Upgrade to Roundcube 1.5 2021-10-18 20:40:51 -04:00
Richard Willis
1c3bca53bb
Fix broken link in external-dns.html (#2045) 2021-10-18 07:36:48 -04:00
ukfhVp0zms
b643cb3478
Update calendar/contacts android app info (#2044)
DAVdroid has been renamed to DAVx⁵ and price increased from $3.69 to $5.99.
CardDAV-Sync free is no longer in beta.
CalDAV-Sync price increased from $2.89 to $2.99.
2021-10-13 19:09:05 -04:00
Joshua Tauberer
113b7bd827 Disable SMTPUTF8 in Postfix because Dovecot LMTP doesn't support it and bounces messages that require SMTPUTF8
By not advertising SMTPUTF8 support at the start, senders may opt to transmit recipient internationalized domain names in IDNA form instead, which will be deliverable.

Incoming mail with internationalized domains was probably working prior to our move to Ubuntu 18.04 when postfix's SMTPUTF8 support became enabled by default.

The previous commit is retained because Mail-in-a-Box users might prefer to keep SMTPUTF8 on for outbound mail, if they are not using internationalized domains for email, in which case the previous commit fixes the 'relay access denied' error even if the emails aren't deliverable.
2021-09-24 08:11:36 -04:00
Joshua Tauberer
3e19f85fad Add domain maps from Unicode forms of internationalized domains to their ASCII forms
When an email is received by Postfix using SMTPUTF8 and the recipient domain is a Unicode internationalized domain, it was failing to be delivered (bouncing with 'relay access denied') because our users and aliases tables only store ASCII (IDNA) forms of internationalized domains. In this commit, domain maps are added to the auto_aliases table from the Unicode form of each mail domain to its IDNA form, if those forms are different. The Postfix domains query is updated to look at the auto_aliases table now as well, since it is the only table with Unicode forms of the mail domains.

However, mail delivery is still not working since the Dovecot LMTP server does not support SMTPUTF8, and mail still bounces but with an error that SMTPUTF8 is not supported.
2021-09-24 08:11:36 -04:00
Joshua Tauberer
11e84d0d40 Move automatically generated aliases to a separate database table
They really should never have been conflated with the user-provided aliases.

Update the postfix alias map to query the automatically generated aliases with lowest priority.
2021-09-24 08:11:36 -04:00
Joshua Tauberer
79966e36e3 Set a cookie for /admin/munin pages to grant access to Munin reports
The /admin/munin routes used the same Authorization: header logic as the other API routes, but they are browsed directly in the browser because they are handled as static pages or as a proxy to a CGI script.

This required users to enter their email username/password for HTTP basic authentication in the standard browser auth prompt, which wasn't ideal (and may leak the password in browser storage). It also stopped working when MFA was enabled for user accounts.

A token is now set in a cookie when visiting /admin/munin which is then checked in the routes that proxy the Munin pages. The cookie's lifetime is kept limited to limit the opportunity for any unknown CSRF attacks via the Munin CGI script.
2021-09-24 08:11:36 -04:00
Joshua Tauberer
66b15d42a5 CHANGELOG entries 2021-09-24 08:11:36 -04:00
drpixie
df46e1311b
Include NSD config files from /etc/nsd/nsd.conf.d/*.conf (#2035)
And write MIAB dns zone config into /etc/nsd/nsd.conf.d/zones.conf. Delete lingering old zones.conf file.

Co-authored-by: Joshua Tauberer <jt@occams.info>
2021-09-24 08:07:40 -04:00
Elsie Hupp
353084ce67
Use "smart invert" for dark mode (#2038)
* Use "smart invert" for dark mode

Signed-off-by: Elsie Hupp <9206310+elsiehupp@users.noreply.github.com>

* Add more contrast to form controls

Co-authored-by: Joshua Tauberer <jt@occams.info>
2021-09-19 09:53:03 -04:00
David Duque
04e1c0647e
Ability to set custom TTL values for custom DNS records (#28) 2021-09-16 15:35:04 +01:00
mailinabox-contributor
91079ab934
add numeric flag value to DNSSEC DS status message (#2033)
Some registrars (e.g. Porkbun) accept Key Data when creating a DS RR,
but accept only a numeric flags value to indicate the key type (256 for KSK, 257 for ZSK).

https://datatracker.ietf.org/doc/html/rfc5910#section-4.3
2021-09-10 16:12:41 -04:00
David Duque
4c4cce836e
Merge changes from upstream 2021-09-07 17:08:05 +01:00
Joshua Tauberer
e5909a6287 Allow non-admin login to the control panel and show/hide menu items depending on the login state
* When logged out, no menu items are shown.
* When logged in, Log Out is shown.
* When logged in as an admin, the remaining menu items are also shown.
* When logged in as a non-admin, the mail and contacts/calendar instruction pages are shown.

Fixes #1987
2021-09-06 09:23:58 -04:00
Joshua Tauberer
26932ecb10 Add a 'welcome' panel to the control panel and make it the default page instead of the status checks which take too long to load
Fixes #2014
2021-09-06 09:23:58 -04:00
Joshua Tauberer
e884c4774f Replace HMAC-based session API keys with tokens stored in memory in the daemon process
Since the session cache clears keys after a period of time, this fixes #1821.

Based on https://github.com/mail-in-a-box/mailinabox/pull/2012, and so:

Co-Authored-By: NewbieOrange <NewbieOrange@users.noreply.github.com>

Also fixes #2029 by not revealing through the login failure error message whether a user exists or not.
2021-09-06 09:23:58 -04:00
Joshua Tauberer
53ec0f39cb Use 'secrets' to generate the system API key and remove some debugging-related code
* Rename the 'master' API key to be called the 'system' API key
* Generate the key using the Python secrets module which is meant for this
* Remove some debugging helper code which will be obsoleted by the upcoming changes for session keys
2021-09-06 09:23:58 -04:00
Joshua Tauberer
700188c443 Roundcube 1.5 RC 2021-09-06 09:23:58 -04:00
David Duque
09f635c1f9
v0.54.POWER.5 2021-08-26 17:17:56 +01:00
Greatest125
db1330795f
fixed spelling mistake (#26) 2021-08-26 16:59:19 +01:00
David Duque
ba80d9e72d
Show backup retention period form when configuring B2 backups (#2024) 2021-08-23 06:25:41 -04:00
David
aca49ea674
(vagrant) Permanently enable IPv6 on loopback
Required by NSD; The Ubuntu image we use comes with it disabled so we need to
enable it ourselves.
2021-08-23 02:10:34 +01:00
David Duque
d8c77527bd
Debian 11 support (#25) 2021-08-23 02:06:38 +01:00
David Duque
d557885aab
SMTP Relay feature rework (#23) 2021-08-23 02:06:10 +01:00
Joshua Tauberer
a71a58e816
Re-order DS record algorithms by digest type and revise warning message (#2002) 2021-08-22 14:45:56 -04:00
Joshua Tauberer
67b5711c68 Recommend that DS records be updated to not use SHA1 and exclude MUST NOT methods (SHA1) and the unlikely option RSASHA1-NSEC3-SHA1 (7) + SHA-384 (4) from the DS record suggestions 2021-08-22 14:43:46 -04:00
myfirstnameispaul
20ccda8710 Re-order DS record algorithms by digest type and revise warning message.
Note that 7, 4 is printed last in the status checks page but does not appear in the file, and I couldn't figure out why.
2021-08-22 14:29:36 -04:00
NewbieOrange
0ba841c7b6
fail2ban now supports ipv6 (#2015)
Since fail2ban 0.10.0, ipv6 support has been added. The current Ubuntu 18.04 repository has fail2ban 0.10.2, which does have ipv6 protection.
2021-08-22 14:13:58 -04:00
lamkin
daad122236
Ignore bad encoding in email addresses when parsing maillog files (#2017)
local/domain parts of email address should be standard ASCII or
UTF-8. Some email addresses contain extended ASCII, leading to
decode failure by the UTF-8 codec (and thus failure of the
Usage-Report script)

This change allows maillog parsing to continue over lines
containing such addresses
2021-08-16 11:46:32 -04:00
David
2c975e43cc
Add all supported deployments; adjust box sizes to reflect a very bad case scenario 2021-08-14 16:51:49 +01:00
David
a4c5f14237
Revert nsd changes 2021-08-02 15:02:54 +01:00
David
8b2f7f2e4c
v0.54.POWER.4 2021-07-29 01:42:55 +01:00
David
aae5fb4b74
nsd: Don't rely on private and public ip's being the same 2021-07-29 01:33:49 +01:00
David
fd15b28b3d
bind9: Configure on default/bind9 and default/named 2021-07-29 01:33:24 +01:00
NewbieOrange
21ad26e452
Disable auto-complete for 2FA code in the control panel login form (#2013) 2021-07-28 16:39:40 -04:00
David
e98d830f7f
v0.54.POWER.3 2021-07-04 11:24:11 +01:00
David
c171b6491a
Forgot to account that b2sdk had modules :c 2021-07-04 11:17:21 +01:00
David
9ab5733af4
v0.54.POWER.2 2021-07-03 22:55:12 +01:00
David
8cb60ecad7
Fix script warnings on Ubuntu 2021-07-03 22:50:49 +01:00
David
63081c647a
v0.54.POWER.1 2021-07-03 21:27:01 +01:00
David
b2854c3afb
duplicity on debian 10 runs on python 2 2021-07-03 21:26:07 +01:00
David
dc7bae04c7
Fix pgp.sh complaining about grep 2021-07-03 01:08:40 +01:00
David
b53add2798
Make output from setup scripts a little bit less noisy 2021-07-03 00:46:31 +01:00
David
e1aabc4504
Backups: Alternative imports for legacy path (debian 10) 2021-07-03 00:25:42 +01:00
David
f55c0a68b6
Management: Add get_os_code() function 2021-07-03 00:21:21 +01:00
David
c125f462e1
Add get_os_code helper function to setup scripts 2021-07-02 21:28:25 +01:00
David
3018cdd698
v0.54.POWER.0 2021-06-28 00:17:23 +01:00
David
edfb1cf623
Resolve dovecot deprecations 2021-06-27 23:11:24 +01:00
David
d0b5794588
New version notice - point to the correct page 2021-06-27 22:26:19 +01:00
David
afe7123f70
Merge v0.54 from upstream 2021-06-27 22:24:26 +01:00
Joshua Tauberer
4cb46ea465 v0.54 2021-06-20 15:50:04 -04:00
David Duque
9f9eb920b3
v0.53.POWER.2 2021-05-16 23:20:55 +01:00
David Duque
217b0b51ff
Bad bootstrap script, fixing! 2021-05-16 23:20:20 +01:00
David Duque
f382a55a0a
v0.53.POWER.1 2021-05-16 21:41:37 +01:00
David Duque
483817440e
Fetch updates from upstream 2021-05-16 21:18:40 +01:00
Joshua Tauberer
35fa3fe891 Changelog entries 2021-05-15 16:50:19 -04:00
Joshua Tauberer
d510c8ae2a Enable and recommend port 465 for mail submission instead of port 587 (fixes #1849)
Port 465 with "implicit" (i.e. always-on) TLS is a more secure approach than port 587 with explicit (i.e. optional and only on with STARTTLS). Although we reject credentials on port 587 without STARTTLS, by that point credentials have already been sent.
2021-05-15 16:42:14 -04:00
Joshua Tauberer
e283a12047 Add null SPF, DMARC, and MX records for automatically generated autoconfig, autodiscover, and mta-sts subdomains; add null MX records for custom A-record subdomains
All A/AAAA-resolvable domains that don't send or receive mail should have these null records.

This simplifies the handling of domains a bit by handling automatically generated subdomains more like other domains.
2021-05-15 16:42:14 -04:00
Joshua Tauberer
e421addf1c Pre-load domain purpopses when building DNS zonefiles rather than querying mail domains at each subdomain 2021-05-09 08:16:07 -04:00
Joshua Tauberer
354a774989 Remove a debug line added in 8cda58fb 2021-05-09 07:34:44 -04:00
Joshua Tauberer
aaa81ec879 Fix indentation issue in bc4ae51c2d 2021-05-08 09:06:18 -04:00
Joshua Tauberer
dbd6dae5ce Fix exit status issue cased by 69fc2fdd 2021-05-08 09:02:48 -04:00
John @ S4
d4c5872547
Make clear that non-AWS S3 backups are supported (#1947)
Just a few wording changes to show that it is possible to make S3 backups to other services than AWS - prompted by a thread on MIAB discourse.
2021-05-08 08:32:58 -04:00
Thomas Urban
3701e05d92
Rewrite envelope from address in sieve forwards (#1949)
Fixes #1946.
2021-05-08 08:30:53 -04:00
Hala Alajlan
bc4ae51c2d
Handle query dns timeout unhandled error (#1950)
Co-authored-by: hala alajlan <halalajlan@gmail.com>
2021-05-08 08:26:40 -04:00
Jawad Seddar
12aaebfc54
custom.yaml: add support for X-Frame-Options header and proxy_redirect off (#1954) 2021-05-08 08:25:33 -04:00
jvolkenant
49813534bd
Updated Nextcloud to 20.0.8, contacts to 3.5.1, calendar to 2.2.0 (#1960) 2021-05-08 08:24:04 -04:00
jvolkenant
16e81e1439
Fix to allow for non forced "enforce" MTA_STS_MODE (#1970) 2021-05-08 08:18:49 -04:00
Joshua Tauberer
b7b67e31b7 Merged point release branch for v0.53a
Changed the Z-Push download URL.
2021-05-08 08:14:39 -04:00
Joshua Tauberer
2e7f2835e7 v0.53a 2021-05-08 08:13:37 -04:00
Joshua Tauberer
8a5f9f464a Download Z-Push from alternate site
The old server has been down for a few days.

Solution from https://discourse.mailinabox.email/t/temporary-fix-for-failed-wget-o-tmp-z-push-zip-https-stash-z-hub-io/8028. Fixes #1974.
2021-05-08 07:59:53 -04:00
Joshua Tauberer
69fc2fdd3a Hide spurrious Nextcloud setup output 2021-05-03 19:41:00 -04:00
Joshua Tauberer
9b07d86bf7 Use $(...) notation instead of legacy backtick notation for embedded shell commands
shellcheck reported

    SC2006: Use $(...) notation instead of legacy backticked `...`.

Fixed by applying shellcheck's diff output as a patch.
2021-05-03 19:28:23 -04:00
Joshua Tauberer
ae3feebd80 Fix warnings reported by shellcheck
* SC2068: Double quote array expansions to avoid re-splitting elements.
* SC2186: tempfile is deprecated. Use mktemp instead.
* SC2124: Assigning an array to a string! Assign as array, or use * instead of @ to concatenate.
* SC2102: Ranges can only match single chars (mentioned due to duplicates).
* SC2005: Useless echo? Instead of 'echo $(cmd)', just use 'cmd'.
2021-05-03 19:25:09 -04:00
Joshua Tauberer
2c295bcafd Upgrade the Roundcube persistent login cookie encryption to AES-256-CBC and increase the key length accordingly
This change will force everyone to be logged out of Roundcube since the encryption key and cipher won't match anyone's already-set cookie, but this happens anyway after every Mail-in-a-Box update since we generate a new key each time already.

Fixes #1968.
2021-04-23 17:04:56 -04:00
David Duque
9f13ee6d55
v0.53.POWER.0 2021-04-13 23:02:08 +01:00
David Duque
40babe3e03
do_web_update: Order the domains in some way before writing to the nginx local.conf 2021-04-13 23:01:18 +01:00
David Duque
b9bdf50628
Merge v0.53 from upstream 2021-04-13 16:35:02 +01:00
Joshua Tauberer
8cda58fb22 Speed up status checks a bit by removing a redundant check if the PRIMARY_HOSTNAME certificate is signed and valid 2021-04-12 19:42:12 -04:00
Joshua Tauberer
178c587654 Migrate to the ECDSAP256SHA256 (13) DNSSEC algorithm
* Stop generating RSASHA1-NSEC3-SHA1 keys on new installs since it is no longer recommended, but preserve the key on existing installs so that we continue to sign zones with existing keys to retain the chain of trust with existing DS records.
* Start generating ECDSAP256SHA256 keys during setup, the current best practice (in addition to RSASHA256 which is also ok). See https://www.iana.org/assignments/dns-sec-alg-numbers/dns-sec-alg-numbers.xhtml#dns-sec-alg-numbers-1 and https://www.cloudflare.com/dns/dnssec/ecdsa-and-dnssec/.
* Sign zones using all available keys rather than choosing just one based on the TLD to enable rotation/migration to the new key and to give the user some options since not every registrar/TLD supports every algorithm.
* Allow a user to drop a key from signing specific domains using DOMAINS= in our key configuration file. Signing the zones with extraneous keys may increase the size of DNS responses, which isn't ideal, although I don't know if this is a problem in practice. (Although a user can delete the RSASHA1-NSEC3-SHA1 key file, the other keys will be re-generated on upgrade.)
* When generating zonefiles, add a hash of all of the DNSSEC signing keys so that when the keys change the zone is definitely regenerated and re-signed.
* In status checks, if DNSSEC is not active (or not valid), offer to use all of the keys that have been generated (for RSASHA1-NSEC3-SHA1 on existing installs, RSASHA256, and now ECDSAP256SHA256) with all digest types, since not all registers support everything, but list them in an order that guides users to the best practice.
* In status checks, if the deployed DS record doesn't use a ECDSAP256SHA256 key, prompt the user to update their DS record.
* In status checks, if multiple DS records are set, only fail if none are valid. If some use ECDSAP256SHA256 and some don't, remind the user to delete the DS records that don't.
* Don't fail if the DS record uses the SHA384 digest (by pre-generating a DS record with that digest type) but don't recommend it because it is not in the IANA mandatory list yet (https://www.iana.org/assignments/ds-rr-types/ds-rr-types.xhtml).

See #1953
2021-04-12 19:42:12 -04:00
Joshua Tauberer
34569d24a9 v0.53 2021-04-11 12:45:37 -04:00
David Duque
a3851bd6be
v0.52.POWER.3 2021-03-30 20:34:27 +01:00
David Duque
f4df9a5e1a
When updating, kickoff the database again so that the noreply address is added 2021-03-30 20:32:56 +01:00
David Duque
c2f627ea82
update_wkd(): Return 'OK' at the end 2021-03-30 20:32:01 +01:00
David Duque
5e20a00e25
v0.52.POWER.2 2021-03-30 20:09:01 +01:00
David Duque
103ff39500
get_web_domains(): Only return www redirects when asked to (include_www_redirects) 2021-03-30 20:01:46 +01:00
David Duque
aa41702825
When creating reports, do not output the number of days until the key expires 2021-03-30 16:23:08 +01:00
David Duque
a2193289e2
Merge jrsupplee's quota fork 2021-03-30 13:09:35 +01:00
David Duque
aa0da22614
Add a nice welcome page 2021-03-30 01:35:16 +01:00
David Duque
071002b755
Changes to the noreply-daemon key 2021-03-30 00:14:00 +01:00
David Duque
127629611b
Force WKD rebuild when importing or deleting keys 2021-03-29 17:54:58 +01:00
David Duque
c260e164eb
Remove print() calls 2021-03-29 17:54:24 +01:00
David Duque
c25a935f2b
strip_and_export(): Parse key contents to determine id's to exclude 2021-03-29 17:14:44 +01:00
David Duque
013f6f2ed1
Don't calculate uidlists when parsing the list, as they're not ordered accurately 2021-03-29 16:32:47 +01:00
David Duque
0f5a5bfbb1
Handle 'KEYEXPIRED' warnings 2021-03-29 16:25:50 +01:00
David Duque
200aefee00
Add data sink to the fork_context decorator 2021-03-29 16:23:42 +01:00
David Duque
353645f8db
Sync with master 2021-03-24 11:05:03 +00:00
David Duque
2ac999fca4
v0.52.POWER.1 2021-03-24 10:48:45 +00:00
David Duque
e8aee7483e
Update vendor software:
- Nextcloud to 20.0.8
- - Contacts to 3.5.1
- JQuery to 3.6.0
- FontAwesome to 5.15.3
- Roundcube to 1.4.11
2021-03-24 10:44:37 +00:00
David Duque
676f3aaf24 Do not disclose the distro in the admin login page (#9) 2021-03-24 10:06:45 +00:00
David Duque
531e67bcdb Change some status check types to 'Not Applicable' as they do not really qualify as warnings 2021-03-24 10:04:41 +00:00
David Duque
d3cf70db27 Add 'not applicable' status check type 2021-03-24 10:01:05 +00:00
David Duque
13b1b31fb1
Fix email_administrator.py script 2021-03-08 23:54:31 +00:00
David Duque
2827d33597
wkd: Sort by domain (web implementation) 2021-03-08 23:48:40 +00:00
David Duque
046dac6f8f
wkd: Sort the email addresses by domain 2021-03-08 23:39:50 +00:00
David Duque
0085dfbbe2
wkd: Use get_all_mail_addresses() 2021-03-07 23:39:31 +00:00
David Duque
b570cca23c
Final bug-fixing 2021-03-07 23:38:34 +00:00
David Duque
c575aea958
get_all_mail_addresses() umbrella function 2021-03-07 23:28:19 +00:00
David Duque
b3a3136859
remove_noreply_address() - use sanitized address 2021-03-07 23:05:46 +00:00
David Duque
6078a994f2
remove_noreply_address() 2021-03-07 23:05:10 +00:00
David Duque
dca9cc09cd
Do not forget to commit at the end 2021-03-07 23:00:41 +00:00
David Duque
96dec08fca
Noreply backend stub 2021-03-07 22:57:41 +00:00
David Duque
2e47d84972
Add noreply table migration step 2021-03-07 20:42:11 +00:00
David Duque
e5eb47b765
Do not use naked domains on the 'From' field
Decreases the chances of spamassassin yeeting the message
to the Spam folder
2021-03-07 20:37:43 +00:00
David Duque
c663363668
Do not disclose the distro in the admin login page (#9) 2021-03-07 20:28:33 +00:00
David Duque
0a7d7c41d1
Refactor PGP key generation
Extract new fingerprint from generation command output;
Verify that the generation has suceeded;
Verify that the current key fingerprint actually exists;
2021-03-07 18:32:21 +00:00
Joshua Tauberer
6653dbb2e2 Sort the Custom DNS by zone and qname, and add an option to go back to the old sort order (creation order)
Update the zone grouping style on the users and aliases page to match.

Fixes #1927
2021-02-28 09:40:32 -05:00
Joshua Tauberer
5fc1162355 Other CHANGELOG entries 2021-02-28 08:22:30 -05:00
Paul
a839602cba
Enable sending DMARC failure reports (#1929)
Configures opendmarc to send failure reports for domains that request them, including when p=none.

The emails are sent as the package default of package name and user@hostname: OpenDMARC Filter <opendmarc@box.example.com>

Note I have been running this for several months with a configuration I did not include in the PR to have reports BCC'd to me (FailureReportsBcc postmaster@example.com). Very low load for my personal server of rarely more than a dozen emails sent out per day.

I am not familiar with editing scripts, so apologies in advance and please feel free to correct me.
2021-02-28 08:21:15 -05:00
Joshua Tauberer
f21a41dc84 Merge #1932, with some edits 2021-02-28 08:16:50 -05:00
davDevOps
055ac07663 Update roundcube to 1.4.11
roundcube Bug Fixes:

Fix for Cross-Site Scripting (XSS) via HTML messages with malicious CSS content
General Improvements from roundcube's Issue Tracker
2021-02-28 08:14:17 -05:00
davDevOps
c7b295f403 Update zpush to 2.6.2 2021-02-28 08:05:40 -05:00
Joshua Tauberer
d36a2cc938 Enable Backblaze B2 backups
This reverts commit b1d703a5e7 and adds python3-setuptools per the first version of #1899 which fixes an installation error for the b2sdk Python package.
2021-02-28 08:04:14 -05:00
jeremitu
82ca54df96
Fixed #1894 log date over year change, START_DATE < END_DATE now. (#1905)
* Fixed #1894 log date over year change, START_DATE < END_DATE now.

* Corrected mail_log.py argument help and message.

Co-authored-by: Jarek <jarek@box.jurasz.de>
2021-02-28 07:59:26 -05:00
David Duque
966ded762e
vagrant: Add on-start provisioner
Allows for faster deployments/start times
2021-02-17 12:17:10 +00:00
David Duque
8567011f9d
Add noreply table to database
Incoming email to addresses in this table will be automatically rejected
(because it doesn't have a mailbox)
2021-02-17 02:55:50 +00:00
David Duque
3862892568
Do not overwrite domain flags 2021-02-14 00:42:16 +00:00
David Duque
8764a69ea2
Change some status check types to 'Not Applicable' as they do not really qualify as warnings 2021-02-13 02:47:22 +00:00
David Duque
d11d0a1708
Add 'not applicable' status check type 2021-02-13 02:43:03 +00:00
David Duque
a5d7ffe29d
Include 'openpgpkey.domain' on status checks 2021-02-13 02:09:16 +00:00
David Duque
36dd640610
Fix WKD folder permissions and 'touch' file before reading it 2021-02-13 01:42:04 +00:00
David Duque
9b50d05751
Resync with master branch 2021-02-13 01:00:37 +00:00
David Duque
708e53adf0 Merge branch 'dev-wkd' into development 2021-02-13 00:57:16 +00:00
David Duque
ca96f5f5ba
Fix bugs 2021-02-13 00:53:10 +00:00
David Duque
b165e6157d
Implement flags into nginx building 2021-02-13 00:43:13 +00:00
David Duque
40f73985ea
(WIP) web domain flags 2021-02-12 16:23:26 +00:00
David Duque
06d210fdc3
Move wkd nginx configuration to a separate template 2021-02-11 23:19:58 +00:00
David Duque
4e6aa8f0d2
web: Optimize get_web_domains function. Will use something similar but different internally 2021-02-11 23:19:35 +00:00
David Duque
7e4325afd8
dns: Use A/AAAA records instead of CNAME's 2021-02-11 23:18:52 +00:00
David Duque
75fa24c8e9
Less debugging messages 2021-02-11 01:00:44 +00:00
David Duque
ff71bd2c93
Fix typo 2021-02-11 01:00:32 +00:00
David Duque
824b368891
Client-side POST'ing 2021-02-11 00:56:21 +00:00
David Duque
2fe8522585
Always overwrite the .wkdlist.yml file 2021-02-11 00:55:56 +00:00
David Duque
4f3002090e
Fix PGP forking issues (don't copy pipes/sockets/etc.) 2021-02-11 00:55:10 +00:00
David Duque
d218f771a9
Fix WKD building issues (SHA1 backend, hash encoding) 2021-02-11 00:38:23 +00:00
David Duque
7a2edb11a0
Refactor POST config mechanics 2021-02-11 00:34:36 +00:00
David Duque
4641ec112e
Dirty configuration mechanisms 2021-02-11 00:04:52 +00:00
David Duque
49394741d3
Don't display buttons/dropdowns for emails with no keys 2021-02-10 23:49:32 +00:00
David Duque
704335dc1c
Update button 2021-02-10 23:39:23 +00:00
David Duque
3d777d86d2
Don't wrap email addresses 2021-02-10 15:27:32 +00:00
David Duque
f942be515f
Process key emails 2021-02-10 02:56:00 +00:00
David Duque
867be9ba37
Options field is a set 2021-02-10 02:21:36 +00:00
David Duque
108f619880
WKD Maps: Ignore user id's that contain email addresses not hosted by our box 2021-02-10 02:06:00 +00:00
David Duque
1ea90a5dbb
Include unique set of UID emails on key representations 2021-02-10 01:54:45 +00:00
John Supplee
bd38f5bcca Merge tag 'v0.52' of https://github.com/mail-in-a-box/mailinabox
v0.52 (January 31, 2021)
------------------------

Software updates:

* Upgraded Roundcube to version 1.4.10.
* Upgraded zpush to 2.6.1.

Mail:

* Incoming emails with SPF/DKIM/DMARC failures now get a higher spam score, and these messages are more likely to appear in the junk folder, since they are often spam/phishing.
* Fixed the MTA-STS policy file's line endings.

Control panel:

* A new Download button in the control panel's External DNS page can be used to download the required DNS records in zonefile format.
* Fixed the problem when the control panel would report DNS entries as Not Set by increasing a bind query limit.
* Fixed a control panel startup bug on some systems.
* Improved an error message on a DNS lookup timeout.
* A typo was fixed.

DNS:

* The TTL for NS records has been increased to 1 day to comply with some registrar requirements.

System:

* Nextcloud's photos, dashboard, and activity apps are disabled since we only support contacts and calendar.

# Conflicts:
#	setup/bootstrap.sh
2021-02-08 23:51:32 +02:00
David Duque
a4675b821a
Front-end implementation: Purpose/expiration info 2021-02-07 17:34:12 +00:00
David Duque
cd0aae6bbf
Front-end implementation: Change selection according to user 2021-02-07 17:13:24 +00:00
David Duque
77bd9e1ab5
Front-end implementation: Display current selection 2021-02-07 17:06:27 +00:00
David Duque
4d055e38b3
Front-end implementation: Start creating dropdown items 2021-02-07 17:03:08 +00:00
David Duque
4ce497deb7
Front-end implementation: Do email, key dropdown left 2021-02-07 15:42:27 +00:00
David Duque
18237029a0
Begin front-end implementation 2021-02-07 15:24:34 +00:00
David Duque
25bd251d10
Pretty "long id" function
The long id is simply the last 16 characters (64 bits) of the
key's fingerprint.
2021-02-07 13:16:51 +00:00
David Duque
1d657191eb
Change /system/pgp/wkd endpoint so that keys are able to be indexed by their fingerprint 2021-02-07 12:34:05 +00:00
David Duque
378ebcf9a4
Start WKD management front-end 2021-02-07 12:33:10 +00:00
David Duque
1ad7ad855a
Document /system/pgp/wkd endpoint in ReDoc 2021-02-07 03:23:14 +00:00
David Duque
071f1581c0
Implement /system/pgp/wkd to return WKD status 2021-02-07 02:42:16 +00:00
David Duque
4bdd4c7478
Add an explicit notice that WKD is experimental
Because the underlying documents are still drafts (and not set-in-stone
internet standards), the feature may be modified, or even retired, as the
drafts are updated and/or expire.
2021-02-06 23:34:17 +00:00
jvolkenant
af62e7a99b
Fixes unbound variable when upgrading from Nextcloud 13 (#1913) 2021-02-06 16:49:43 -05:00
David Duque
a002ee9355
Add WKD dropdown template 2021-02-06 16:41:01 +00:00
David Duque
f41eeb37c1
Release v0.52.POWER.0 2021-02-01 02:22:15 +00:00
David Duque
ba68bd9941
Automatically import existing local CA cerificates 2021-02-01 02:20:38 +00:00
David Duque
e6f22c53e5
Update admin panel dependencies 2021-02-01 01:57:38 +00:00
David Duque
18d36831dc
Update NextCloud components 2021-02-01 01:49:05 +00:00
David Duque
f47cdbaee1
External DNS: Add some margin between dropdown and buttons 2021-02-01 01:29:23 +00:00
David Duque
4829e687ff
Merge changes from master 2021-01-31 16:20:15 +00:00
Joshua Tauberer
90d63fd208 v0.52 2021-01-31 08:48:14 -05:00
Joshua Tauberer
e81963e585 Remove the instructions for checking that release tags are signed by me since I am not going to do that anymore 2021-01-31 08:47:59 -05:00
Joshua Tauberer
b1d703a5e7 Disable Backblaze B2 backups until #1899 is resolved 2021-01-31 08:33:56 -05:00
David Duque
f036389645
Add full Bootstrap bundle (Contains Popper.js); Add more info to the WKD page 2021-01-31 00:54:39 +00:00
Felix Spöttel
e3d98b781e
Warn when connection to Spamhaus times out (#1817) 2021-01-28 18:22:43 -05:00
jvolkenant
50d50ba653
Update zpush to 2.6.1 (#1908) 2021-01-28 18:20:19 -05:00
Josh Brown
879467d358
Fix typo in users.html (#1895)
lettters -> letters
fixes #1888
2021-01-05 21:12:01 -05:00
Nicolas North
8025c41ee4
Bump TTL for NS records to 1800 (30 min) to 86400 (1 day) as some registries require this (#1892)
Co-authored-by: Nicolas North [norðurljósahviða] <nz@tillverka.xyz>
2021-01-03 17:57:54 -05:00
Josh Brown
7a5d729a53
Fix misspelling (#1893)
Change Blackblaze to Backblaze. Include B2 as the integration name.
2021-01-03 17:54:31 -05:00
jcm-shove-it
e2f9cd845a
Update roundcube to 1.4.10 (#1891) 2020-12-28 08:11:33 -05:00
David Duque
8085fe149b
Fix WKD DNS update 2020-12-27 01:47:21 +00:00
David Duque
52be85d0cc
WKD Management menu 2020-12-27 00:46:56 +00:00
David Duque
012605b5b2
User-key maps for WKD management 2020-12-26 22:34:19 +00:00
Joshua Tauberer
e26cf4512c Update CHANGELOG 2020-12-25 17:28:34 -05:00
jvolkenant
c7280055a8
Implement SPF/DMARC checks, add spam weight to those mails (#1836) 2020-12-25 17:22:24 -05:00
Hilko
003e8b7bb1
Adjust max-recursion-queries to fix alternating rdns status (#1876) 2020-12-25 17:19:16 -05:00
David Duque
492cfd0309
Add openpgpkey CNAME records on DNS zones 2020-12-21 00:21:03 +00:00
David Duque
7744b33e2b
WKD Building 2020-12-20 23:49:36 +00:00
David Duque
b9445f61f4
parse_wkd_list: Always return full key fingerprints 2020-12-20 23:18:02 +00:00
David Duque
9be5d2b454
Add public nginx endpoints for WKD 2020-12-20 23:11:28 +00:00
Hilko
3422cc61ce
Include en_US.UTF-8 locale in daemon startup (#1883)
Fixes #1881.
2020-12-19 19:11:58 -05:00
David Duque
6a08551402
Deepcopy writeable config file 2020-12-08 22:30:43 +00:00
David Duque
05172328d5
Advances in WKD building 2020-12-08 22:18:59 +00:00
David Duque
3e489f59f2
Clarify on docs that aliases also count 2020-12-08 20:05:21 +00:00
David Duque
ef58b897d6
Refactor configuration functions 2020-12-08 19:54:59 +00:00
David Duque
15ac7988f3
Implement key stripping 2020-12-06 02:32:28 +00:00
David Duque
7f2c43219e
pgp: @fork_context decorator 2020-12-06 01:37:20 +00:00
David Duque
36794269b4
pgp: Allow other contexts to be used. Default to our main working context 2020-12-06 00:35:53 +00:00
John Supplee
682550f6c5 Update README 2020-12-03 23:25:22 +02:00
John Supplee
19e2066969 v0.51 (November 14, 2020)
Software updates:
 
 * Upgraded Nextcloud from 17.0.6 to 20.0.1 (with Contacts from 3.3.0 to 3.4.1 and Calendar from 2.0.3 to 2.1.2)
 * Upgraded Roundcube to version 1.4.9.
 
 Mail:
 
 * The MTA-STA max_age value was increased to the normal one week.
 
 Control Panel:
 
 * Two-factor authentication can now be enabled for logins to the control panel. However, keep in mind that many online services (including domain name registrars, cloud server providers, and TLS certificate providers) may allow an attacker to take over your account or issue a fraudulent TLS certificate with only access to your email address, and this new two-factor authentication does not protect access to your inbox. It therefore remains very important that user accounts with administrative email addresses have strong passwords.
 * TLS certificate expiry dates are now shown in ISO8601 format for clarity.
 -----BEGIN PGP SIGNATURE-----
 
 iQFDBAABCgAtFiEEX0wOcxPM10RpOyrquSBB9MEL3YEFAl+v8k4PHGp0QG9jY2Ft
 cy5pbmZvAAoJELkgQfTBC92BMYUIAJTD1iKzY1SoDNSp8JMPn2sWusOnJNrnvYEV
 vsrBM4AzwJv3DIZKSkYCitbTQW2FsTcjF6Jl5PCavEmAGe55AIKAPM/52Uq6jqDE
 aR8EZvI9ca1i7yR7DOHEI043QSPmp/iCFD48vvmKgN/LZy67TaHaOlGJbc3nfpk0
 y7ejMpF/6RP6ik4snnRQoWTFShaOpB9WcEVnUO7CHZdWcpSCZ55c9yi6A6ExGk7e
 97R5+JN1MgOdZ6rzWZuMWiz7EZ/Ew4jYLZpOwg8qJm0HNbYJ6+/xxsQBwaQzyBw3
 TsTl4GmunNPfoNrmKdJeLy0sBwiVBv/rysjWjim5v8jAYBoKoUQ=
 =2oRU
 -----END PGP SIGNATURE-----

Merge tag 'v0.51' of https://github.com/mail-in-a-box/mailinabox

v0.51 (November 14, 2020)

Software updates:

* Upgraded Nextcloud from 17.0.6 to 20.0.1 (with Contacts from 3.3.0 to 3.4.1 and Calendar from 2.0.3 to 2.1.2)
* Upgraded Roundcube to version 1.4.9.

Mail:

* The MTA-STA max_age value was increased to the normal one week.

Control Panel:

* Two-factor authentication can now be enabled for logins to the control panel. However, keep in mind that many online services (including domain name registrars, cloud server providers, and TLS certificate providers) may allow an attacker to take over your account or issue a fraudulent TLS certificate with only access to your email address, and this new two-factor authentication does not protect access to your inbox. It therefore remains very important that user accounts with administrative email addresses have strong passwords.
* TLS certificate expiry dates are now shown in ISO8601 format for clarity.

# gpg verification failed.

# Conflicts:
#	management/daemon.py
#	setup/bootstrap.sh
#	setup/mail-users.sh
#	tools/mail.py
2020-12-03 23:21:37 +02:00
Hilko
8664afa997
Implement Backblaze for Backup (#1812)
* Installing b2sdk for b2 support
* Added Duplicity PPA so the most recent version is used
* Implemented list_target_files for b2
* Implemented b2 in frontend
* removed python2 boto package
2020-11-26 07:13:31 -05:00
Joshua Tauberer
82229ce04b Document how to start the control panel from the command line and in debugging use a stable API key 2020-11-26 07:11:49 -05:00
Richard Willis
f66e609d3f
Api spec cleanup (#1869)
* Fix indentation

* Add parameter definition and remove unused model

* Update version

* Quote example string
2020-11-26 06:56:04 -05:00
David Duque
e519cea350
PGP Keyring: Display expiry dates in ISO format 2020-11-21 15:29:21 +00:00
David Duque
822cc9957f Merge branch 'development' into dev-wkd 2020-11-21 03:26:46 +00:00
David Duque
392f05b975
PGP Keyring: Display expiry dates in ISO format 2020-11-21 03:26:27 +00:00
David Duque
fbd0df6fad Merge branch 'development' into dev-wkd 2020-11-21 03:12:23 +00:00
David Duque
570a22903a Merge branch 'master' into development 2020-11-21 03:12:00 +00:00
David Duque
ef282fc7d0
Version bump - v0.51.POWER.1 2020-11-21 02:42:26 +00:00
David Duque
ef116f13de
system-status: Make system status cover the whole page width 2020-11-21 02:41:37 +00:00
David Duque
a35b885fac
Replace dead glyphicons (with FontAwesome alternative) 2020-11-21 02:27:52 +00:00
David Duque
4fcde9223e
modal: Reorder header positions 2020-11-21 02:02:43 +00:00
Victor
b85b86e6de
Add download zonefile button to external DNS page (#1853)
Co-authored-by: Joshua Tauberer <jt@occams.info>
2020-11-16 06:03:41 -05:00
Joshua Tauberer
7fd35bbd11 Disable default Nextcloud apps that we don't support
Contacts and calendar are the only supported apps in Mail-in-a-Box.

Files can't be disabled.

Fixes #1864
2020-11-15 17:17:58 -05:00
David Duque
3d58edacdb Merge branch 'development' into dev-wkd 2020-11-15 19:20:00 +00:00
David Duque
13ef53de71
Merge changes from master 2020-11-15 19:15:20 +00:00
David Duque
2f50c5e6c2
Version bump 2020-11-15 18:45:13 +00:00
David Duque
c767f9eebe
Update Bootstrap to version 4.5.3 2020-11-15 18:33:46 +00:00
David Duque
a67a57913d v0.51 (November 14, 2020)
Software updates:
 
 * Upgraded Nextcloud from 17.0.6 to 20.0.1 (with Contacts from 3.3.0 to 3.4.1 and Calendar from 2.0.3 to 2.1.2)
 * Upgraded Roundcube to version 1.4.9.
 
 Mail:
 
 * The MTA-STA max_age value was increased to the normal one week.
 
 Control Panel:
 
 * Two-factor authentication can now be enabled for logins to the control panel. However, keep in mind that many online services (including domain name registrars, cloud server providers, and TLS certificate providers) may allow an attacker to take over your account or issue a fraudulent TLS certificate with only access to your email address, and this new two-factor authentication does not protect access to your inbox. It therefore remains very important that user accounts with administrative email addresses have strong passwords.
 * TLS certificate expiry dates are now shown in ISO8601 format for clarity.
 -----BEGIN PGP SIGNATURE-----
 
 iQFDBAABCgAtFiEEX0wOcxPM10RpOyrquSBB9MEL3YEFAl+v8k4PHGp0QG9jY2Ft
 cy5pbmZvAAoJELkgQfTBC92BMYUIAJTD1iKzY1SoDNSp8JMPn2sWusOnJNrnvYEV
 vsrBM4AzwJv3DIZKSkYCitbTQW2FsTcjF6Jl5PCavEmAGe55AIKAPM/52Uq6jqDE
 aR8EZvI9ca1i7yR7DOHEI043QSPmp/iCFD48vvmKgN/LZy67TaHaOlGJbc3nfpk0
 y7ejMpF/6RP6ik4snnRQoWTFShaOpB9WcEVnUO7CHZdWcpSCZ55c9yi6A6ExGk7e
 97R5+JN1MgOdZ6rzWZuMWiz7EZ/Ew4jYLZpOwg8qJm0HNbYJ6+/xxsQBwaQzyBw3
 TsTl4GmunNPfoNrmKdJeLy0sBwiVBv/rysjWjim5v8jAYBoKoUQ=
 =2oRU
 -----END PGP SIGNATURE-----
gpgsig -----BEGIN PGP SIGNATURE-----
 
 iQIzBAABCAAdFiEEAKK/toPAcMkE+dinLzJ3OKPArjoFAl+xc7sACgkQLzJ3OKPA
 rjo6Zw//eYyTBlfQfFHIsLYKxJbwh6fDrIG6/Za6898cPhkJ/ugBeJlNEyT/EjpU
 MvtIgEU9xbG/tjsnQXsgAXJ6s7ZWm1QB5D+wqUIEeAFUn5IkCnXo0wPZJhSTNZhD
 4InnWsicYZj/ByuSH179xHyTAx2uYDBbPT4HjUlzIsaopvWOKLvAfzY3r42AiNvZ
 e79MhKbtOs9kDkrB2LULRzz6WzJDKb11fJccf7UaBerwFvOarMr8hSpOysK0ocHk
 H0wbrGxjb8iBjczVP4OFh36satQ5l4B1W+QVIxZG9ufVAOe3dhv8HngaHqAVyUgF
 gWjDYTnL/anoMMew+kbn2sjeKH6m2ZA+u9g+mDyMGSECVVYhkpOpcbPjZlmlNAQN
 C5BHmHltIg90uicrhzEEPFDBR1JF7JrYO42EwnOWMwjhzRkH2cepVw86lDr+pbrH
 s3hvoWiFFt7cs5ShCpgZDL20ey1e+9wL6b72Qlo7ls7MK3vfZvLPxJLpTi+bnymD
 CNt82Mjpu3BrhjCIGp+px9E2JU/7wUwqyUbgWFtyqxCdJOZXA4ZXVtDs5pQFzhug
 G+Z1HxFmhxck17SD0uHhXJKRD8IRttnO5sBESJaLNB4Ws/KspHVPePNskB/1XSfr
 pFOqikZsoKOICZnpd/eTnUlciqFygqvB0WuFsJNttQN2dBpJViA=
 =ZMFZ
 -----END PGP SIGNATURE-----

Merge upstream v0.51
2020-11-15 18:30:19 +00:00
gumida
7ce41e3865
Changed mta-sts.txt end of line from LF to CRLF per RFC 8461 (#1863) 2020-11-15 07:54:34 -05:00
Joshua Tauberer
92221f9efb v0.51 2020-11-14 10:05:20 -05:00
David Duque
69e6299662
PGP module: clean up code 2020-11-01 20:07:40 +00:00
Joshua Tauberer
0bd3977cde CHANGELOG updates 2020-10-31 10:36:40 -04:00
Joshua Tauberer
6a979f4f52
Add TOTP two-factor authentication to admin panel login (#1814)
* add user interface for managing 2fa

* update user schema with 2fa columns

* implement two factor check during login

* Use pyotp for validating TOTP codes

* also implements resynchronisation support via `pyotp`'s `valid_window option

* Update API route naming, update setup page

* Rename /two-factor-auth/ => /2fa/
* Nest totp routes under /2fa/totp/
* Update ids and methods in panel to allow for different setup types

* Autofocus otp input when logging in, update layout

* Extract TOTPStrategy class to totp.py

* this decouples `TOTP` validation and storage logic from `auth` and moves it to `totp`
* reduce `pyotp.validate#valid_window` from `2` to `1`

* Update OpenApi docs, rename /2fa/ => /mfa/

* Decouple totp from users table by moving to totp_credentials table

* this allows implementation of other mfa schemes in the future (webauthn)
* also makes key management easier and enforces one totp credentials per user on db-level

* Add sqlite migration

* Rename internal validate_two_factor_secret => validate_two_factor_secret

* conn.close() if mru_token update can't .commit()

* Address review feedback, thanks @hija

* Use hmac.compare_digest() to compare mru_token

* Safeguard against empty mru_token column

* hmac.compare_digest() expects arguments of type string, make sure we don't pass None
 * Currently, this cannot happen but we might not want to store `mru_token` during setup

* Do not log failed login attempts for MissingToken errors

* Due to the way that the /login UI works, this persists at least one failed login each time a user logs into the admin panel. This in turn triggers fail2ban at some point.

* Add TOTP secret to user_key hash

thanks @downtownallday
* this invalidates all user_keys after TOTP status is changed for user
* after changing TOTP state, a login is required
* due to the forced login, we can't and don't need to store the code used for setup in `mru_code`

* Typo

* Reorganize the MFA backend methods

* Reorganize MFA front-end and add label column

* Fix handling of bad input when enabling mfa

* Update openAPI docs

* Remove unique key constraint on foreign key user_id in mfa table

* Don't expose mru_token and secret for enabled mfas over HTTP

* Only update mru_token for matched mfa row

* Exclude mru_token in user key hash

* Rename tools/mail.py to management/cli.py

* Add MFA list/disable to the management CLI so admins can restore access if MFA device is lost

Co-authored-by: Joshua Tauberer <jt@occams.info>
2020-10-31 10:27:38 -04:00
Joshua Tauberer
545e7a52e4 Add MFA list/disable to the management CLI so admins can restore access if MFA device is lost 2020-10-31 10:23:43 -04:00
David Duque
48c233ebe5
Update Roundcube to version 1.4.9 (#1830) 2020-10-31 10:01:14 -04:00
Michael Kroes
9a588de754
Upgrade Nextcloud to version 20.0.1 (#1848) 2020-10-31 09:58:26 -04:00
Joshua Tauberer
ac9ecc3bd3 Rename tools/mail.py to management/cli.py 2020-10-29 15:41:54 -04:00
David Duque
c4050964fe
Implement mapping primitives 2020-10-17 22:12:40 +01:00
David Duque
a2ead96bf3
Begin WKD manager 2020-10-17 20:41:58 +01:00
David Duque
8b166f3041
Display certificate expiry dates in ISO format (#1841) 2020-10-16 16:22:36 -04:00
Joshua Tauberer
5509420637 s/Days/Retention Days/ on the backup settings page 2020-10-15 14:11:43 -04:00
John Supplee
38ac127344 v0.50 (September 25, 2020)
--------------------------
 
 Setup:
 
 * When upgrading from versions before v0.40, setup will now warn that ownCloud/Nextcloud data cannot be migrated rather than failing the installation.
 
 Mail:
 
 * An MTA-STS policy for incoming mail is now published (in DNS and over HTTPS) when the primary hostname and email address domain both have a signed TLS certificate installed, allowing senders to know that an encrypted connection should be enforced.
 * The per-IP connection limit to the IMAP server has been doubled to allow more devices to connect at once, especially with multiple users behind a NAT.
 
 DNS:
 
 * autoconfig and autodiscover subdomains and CalDAV/CardDAV SRV records are no longer generated for domains that don't have user accounts since they are unnecessary.
 * IPv6 addresses can now be specified for secondary DNS nameservers in the control panel.
 
 TLS:
 
 * TLS certificates are now provisioned in groups by parent domain to limit easy domain enumeration and make provisioning more resilient to errors for particular domains.
 
 Control Panel:
 
 * The control panel API is now fully documented at https://mailinabox.email/api-docs.html.
 * User passwords can now have spaces.
 * Status checks for automatic subdomains have been moved into the section for the parent domain.
 * Typo fixed.
 
 Web:
 
 * The default web page served on fresh installations now adds the `noindex` meta tag.
 * The HSTS header is revised to also be sent on non-success responses.
 -----BEGIN PGP SIGNATURE-----
 
 iQFDBAABCgAtFiEEX0wOcxPM10RpOyrquSBB9MEL3YEFAl9t2AgPHGp0QG9jY2Ft
 cy5pbmZvAAoJELkgQfTBC92BZNkH/1jIGoWTz0xlS+e+TeXpHoCp/7zYAvQq/a/y
 vj9t1N1+bBg6Ywbd8UxyvOHwuL/UQU/5LTq6hk3gD+2ARfJUvDRbb047Xzlisg3N
 LhNoVhVbsxqKP1X2ZjeIBq9DgzMavuB64Bwd5UNdceM0Addi8KuCDOMF+FNY2t8k
 xytGjYdBi1/BG6SLBX+FAm5yrJghmkUJs2FnJjebSyyeV2HP3L1iBrk2N8UBd6PU
 fVjde534lgygFZK/8yXJpY2olfLMYJv7CaOMxvaW6RpbMI8VeLwDLfRt5LcrQZqq
 YXkuEnUI0eygbQYkeK/Vr1Vey6uQAWzIfbImEglHfvOXsZSYFXs=
 =SJNM
 -----END PGP SIGNATURE-----

Merge tag 'v0.50' of https://github.com/mail-in-a-box/mailinabox into master

v0.50 (September 25, 2020)
--------------------------

Setup:

* When upgrading from versions before v0.40, setup will now warn that ownCloud/Nextcloud data cannot be migrated rather than failing the installation.

Mail:

* An MTA-STS policy for incoming mail is now published (in DNS and over HTTPS) when the primary hostname and email address domain both have a signed TLS certificate installed, allowing senders to know that an encrypted connection should be enforced.
* The per-IP connection limit to the IMAP server has been doubled to allow more devices to connect at once, especially with multiple users behind a NAT.

DNS:

* autoconfig and autodiscover subdomains and CalDAV/CardDAV SRV records are no longer generated for domains that don't have user accounts since they are unnecessary.
* IPv6 addresses can now be specified for secondary DNS nameservers in the control panel.

TLS:

* TLS certificates are now provisioned in groups by parent domain to limit easy domain enumeration and make provisioning more resilient to errors for particular domains.

Control Panel:

* The control panel API is now fully documented at https://mailinabox.email/api-docs.html.
* User passwords can now have spaces.
* Status checks for automatic subdomains have been moved into the section for the parent domain.
* Typo fixed.

Web:

* The default web page served on fresh installations now adds the `noindex` meta tag.
* The HSTS header is revised to also be sent on non-success responses.

# gpg verification failed.

# Conflicts:
#	.gitignore
#	setup/bootstrap.sh
2020-10-11 18:16:36 +02:00
John R. Supplee
c13343ec7f v0.48
Roundcube XSS vulnerability fixed.
 -----BEGIN PGP SIGNATURE-----
 
 iQFDBAABCgAtFiEEX0wOcxPM10RpOyrquSBB9MEL3YEFAl9GpkcPHGp0QG9jY2Ft
 cy5pbmZvAAoJELkgQfTBC92BoYAH/2NjdfN2d6f45uPq/X32bBAc6wfI7Cs9yCKp
 LOrAfPlmE0jRSm9ThATfZvaWci2r2IFhsFzQ9bWHpbIP5YD7mDD50I2uTnZa9BV4
 MsI40VXoh0BAgkWRqK60rTw0lQ9YGT+1TNLDEs1Y7vBjfTCOh4MMn4jUXkIEHDQg
 2pSHY1RUq7T0wRaHS+rTPDccotS/xCGg6uZJ+gSlvhRdxakAe9mo8139KD/4fjT8
 HK6igpwHsn3POg7mmJoSYXtScmWRYfnSV9kyfYyVyjhu5/uIowdICwFOzX7G7ruM
 yA/azBlyMs898e5jYFR1tQqQ1rVYVy/nqCQOiyJa34ngHGSi41U=
 =a9fn
 -----END PGP SIGNATURE-----

Merge tag 'v0.48' of https://github.com/mail-in-a-box/mailinabox

v0.48

Roundcube XSS vulnerability fixed.
2020-10-07 14:33:26 +02:00
David Duque
a210261e34
Be consistent with notation 2020-10-04 22:20:17 +01:00
David Duque
d46b98dcda
Update api documentation file 2020-10-04 22:10:53 +01:00
David Duque
295fd32ade
Cover case where import is invalid 2020-10-04 22:10:30 +01:00
David Duque
f04a0da39f Merge branch 'master' into development 2020-10-04 16:38:10 +01:00
David Duque
dcbc320502
Just a little hotfix 2020-10-04 16:37:40 +01:00
David Duque
8519b7fc0e
Add pgp keyring management (#5)
This PR adds into the admin panel a front-end to manage PGP keys. Possibilities are many.
2020-10-04 16:35:59 +01:00
Felix Spöttel
7d6c7b6610
Increase mta-sts max_age to one week (#1829)
This aligns the policy with the example policy found in the  spec
see https://tools.ietf.org/html/rfc8461#section-3.2
2020-10-02 21:27:21 -04:00
Felix Spöttel
1f0e493b8c Exclude mru_token in user key hash 2020-09-30 12:34:26 +02:00
Felix Spöttel
ada2167d08 Only update mru_token for matched mfa row 2020-09-29 20:05:58 +02:00
Felix Spöttel
be5032ffbe Don't expose mru_token and secret for enabled mfas over HTTP 2020-09-29 19:46:02 +02:00
Felix Spöttel
00b3a3b0a9 Remove unique key constraint on foreign key user_id in mfa table 2020-09-29 19:39:40 +02:00
Felix Spöttel
6d82c0035a Update openAPI docs 2020-09-28 21:27:24 +02:00
Felix Spöttel
4dced10a3f Fix handling of bad input when enabling mfa 2020-09-28 21:06:59 +02:00
Joshua Tauberer
b80f225691 Reorganize MFA front-end and add label column 2020-09-27 08:31:23 -04:00
0pis
7f0f28f8e3
Use tabs instead of spaces in nginx conf (#1827)
* conf/nginx-primaryonly.conf: Use tabs instead of spaces
* management/web_update.py: Includes the tabs so they display with the correct indentation when added to the local.conf

Co-authored-by: 0pis <0pis>
2020-09-27 07:13:33 -04:00
David Duque
59f36b4dd6
Release v0.50.POWER.1 2020-09-27 02:36:03 +01:00
David Duque
d3b5ddf891
Update Bootstrap: 4.5.2 2020-09-27 02:34:37 +01:00
David Duque
5caaf4fd98
Update Nextcloud: 19.0.3 2020-09-27 02:23:45 +01:00
David Duque
2bfa65329a
Keep root in actual local.conf file 2020-09-27 02:17:49 +01:00
David Duque
81c64f0a8f
Re-add webmail roundcube (to primaryonly) 2020-09-27 02:05:16 +01:00
David Duque
4b7f6e20da
Update nginx files to discard non-essential locations for non-primary domains 2020-09-27 02:01:17 +01:00
David Duque
7725e6efe6
Revert .nginx.conf file features 2020-09-27 01:31:51 +01:00
David Duque
689df9cff5
Version bump 2020-09-27 00:38:53 +01:00
David Duque
6c608ea868
Update API docs 2020-09-27 00:36:33 +01:00
Joshua Tauberer
a8ea456b49 Reorganize the MFA backend methods 2020-09-26 09:58:25 -04:00
David Duque
7de99aa690 v0.50 (September 25, 2020)
--------------------------
 
 Setup:
 
 * When upgrading from versions before v0.40, setup will now warn that ownCloud/Nextcloud data cannot be migrated rather than failing the installation.
 
 Mail:
 
 * An MTA-STS policy for incoming mail is now published (in DNS and over HTTPS) when the primary hostname and email address domain both have a signed TLS certificate installed, allowing senders to know that an encrypted connection should be enforced.
 * The per-IP connection limit to the IMAP server has been doubled to allow more devices to connect at once, especially with multiple users behind a NAT.
 
 DNS:
 
 * autoconfig and autodiscover subdomains and CalDAV/CardDAV SRV records are no longer generated for domains that don't have user accounts since they are unnecessary.
 * IPv6 addresses can now be specified for secondary DNS nameservers in the control panel.
 
 TLS:
 
 * TLS certificates are now provisioned in groups by parent domain to limit easy domain enumeration and make provisioning more resilient to errors for particular domains.
 
 Control Panel:
 
 * The control panel API is now fully documented at https://mailinabox.email/api-docs.html.
 * User passwords can now have spaces.
 * Status checks for automatic subdomains have been moved into the section for the parent domain.
 * Typo fixed.
 
 Web:
 
 * The default web page served on fresh installations now adds the `noindex` meta tag.
 * The HSTS header is revised to also be sent on non-success responses.
 -----BEGIN PGP SIGNATURE-----
 
 iQFDBAABCgAtFiEEX0wOcxPM10RpOyrquSBB9MEL3YEFAl9t2AgPHGp0QG9jY2Ft
 cy5pbmZvAAoJELkgQfTBC92BZNkH/1jIGoWTz0xlS+e+TeXpHoCp/7zYAvQq/a/y
 vj9t1N1+bBg6Ywbd8UxyvOHwuL/UQU/5LTq6hk3gD+2ARfJUvDRbb047Xzlisg3N
 LhNoVhVbsxqKP1X2ZjeIBq9DgzMavuB64Bwd5UNdceM0Addi8KuCDOMF+FNY2t8k
 xytGjYdBi1/BG6SLBX+FAm5yrJghmkUJs2FnJjebSyyeV2HP3L1iBrk2N8UBd6PU
 fVjde534lgygFZK/8yXJpY2olfLMYJv7CaOMxvaW6RpbMI8VeLwDLfRt5LcrQZqq
 YXkuEnUI0eygbQYkeK/Vr1Vey6uQAWzIfbImEglHfvOXsZSYFXs=
 =SJNM
 -----END PGP SIGNATURE-----
gpgsig -----BEGIN PGP SIGNATURE-----
 
 iQIzBAABCAAdFiEEAKK/toPAcMkE+dinLzJ3OKPArjoFAl9vB/0ACgkQLzJ3OKPA
 rjpXTg/+L2W6LXtqJcDdPiLb7uRJ1a+R7DAPPLhZOXT8alFt6g2nAJHHI3NxKWVM
 KsrSGlL+XSw744tfEzw21WsDuoME2F536/q4V4iprQx0LSJ61EQtqFYABbHT7lSc
 EyJellcIBxvK9ZTrHhJy3jVJL5eEkrHr4YpaRd68CZGneziMbxZusrlD23OfOn+U
 Pi6O39+Xh9lB4nxMfzkjYwCPEyNsTaCieKforPE+7TYh6d5NFHp22e2/yNEwYHhv
 90txul+/ByeT6UNFsVQ+QXCpMr/m06W9zbCDgrArol12MlgeAg4bL2trgDUV2D9j
 Dpfo1SYo/VUYetlT98adxW7BK2JuGe3SsFDrgjNPDyMBZRoybLY/l1X5TF5d7dq/
 bhgDcHXSJ6iBmhZ8nGDuBWhiEld9orn/9vfj/nHmleurXxgDwMcGKn0eINDuX8Xd
 NauJdhyOiZLfy8+Rha9ltLlFC/sX8nq0o6iM1Xr+4UOTFVVxlVadkPTMOxuRIQfD
 +JaMRCoXLfbAknoGdKfAcxEAzzyylO6z4Ztj/fVp9SHjQgby1paLpJMHEVUaQzEZ
 VYqdOzmz7vrV1H5OHOIy6mthQrTw+Mg4KubJs7w99e3pZKJBpvp55+DLvA0JhKLD
 dVXqr7rBTkLk/tg4u2SWlj3aZOnkzMz0Iwiu5X+hx3kLl0f3Zgk=
 =VgsY
 -----END PGP SIGNATURE-----

Merge v0.50 from upstream
2020-09-26 10:21:01 +01:00
Joshua Tauberer
03bff5292b v0.50
v0.50 (September 25, 2020)
--------------------------

Setup:

* When upgrading from versions before v0.40, setup will now warn that ownCloud/Nextcloud data cannot be migrated rather than failing the installation.

Mail:

* An MTA-STS policy for incoming mail is now published (in DNS and over HTTPS) when the primary hostname and email address domain both have a signed TLS certificate installed, allowing senders to know that an encrypted connection should be enforced.
* The per-IP connection limit to the IMAP server has been doubled to allow more devices to connect at once, especially with multiple users behind a NAT.

DNS:

* autoconfig and autodiscover subdomains and CalDAV/CardDAV SRV records are no longer generated for domains that don't have user accounts since they are unnecessary.
* IPv6 addresses can now be specified for secondary DNS nameservers in the control panel.

TLS:

* TLS certificates are now provisioned in groups by parent domain to limit easy domain enumeration and make provisioning more resilient to errors for particular domains.

Control Panel:

* The control panel API is now fully documented at https://mailinabox.email/api-docs.html.
* User passwords can now have spaces.
* Status checks for automatic subdomains have been moved into the section for the parent domain.
* Typo fixed.

Web:

* The default web page served on fresh installations now adds the `noindex` meta tag.
* The HSTS header is revised to also be sent on non-success responses.
2020-09-25 07:43:30 -04:00
Joshua Tauberer
e891a9a3f3 Update CHANGELOG 2020-09-21 15:59:38 -04:00
Joshua Tauberer
51aedcf6c3 Drop the MTA-STS TLSRPT record unless set explicitly 2020-09-21 15:57:17 -04:00
b-k
853008ddcc
Be more forgiving of people who missed the train on upgrading NextCloud (#1813)
Co-authored-by: B <ben@klemens.org>
2020-09-21 15:45:58 -04:00
Felix Spöttel
7d6427904f Typo 2020-09-12 16:38:44 +02:00
Felix Spöttel
dcb93d071c Add TOTP secret to user_key hash
thanks @downtownallday
* this invalidates all user_keys after TOTP status is changed for user
* after changing TOTP state, a login is required
* due to the forced login, we can't and don't need to store the code used for setup in `mru_code`
2020-09-12 16:34:06 +02:00
Felix Spöttel
2ea97f0643 Do not log failed login attempts for MissingToken errors
* Due to the way that the /login UI works, this persists at least one failed login each time a user logs into the admin panel. This in turn triggers fail2ban at some point.
2020-09-06 13:08:44 +02:00
Felix Spöttel
4791c2fc62 Safeguard against empty mru_token column
* hmac.compare_digest() expects arguments of type string, make sure we don't pass None
 * Currently, this cannot happen but we might not want to store `mru_token` during setup
2020-09-06 13:03:54 +02:00
Felix Spöttel
49c333221a Use hmac.compare_digest() to compare mru_token 2020-09-06 12:54:45 +02:00
Felix Spöttel
481a333dc0 Address review feedback, thanks @hija 2020-09-04 20:28:15 +02:00
Felix Spöttel
b0df35eba0 conn.close() if mru_token update can't .commit() 2020-09-03 20:39:03 +02:00
Felix Spöttel
08ae3d2b7f Rename internal validate_two_factor_secret => validate_two_factor_secret 2020-09-03 19:48:54 +02:00
Felix Spöttel
7c4eb0fb70 Add sqlite migration 2020-09-03 19:39:29 +02:00
Felix Spöttel
ee01eae55e Decouple totp from users table by moving to totp_credentials table
* this allows implementation of other mfa schemes in the future (webauthn)
* also makes key management easier and enforces one totp credentials per user on db-level
2020-09-03 19:07:21 +02:00
Felix Spöttel
89b301afc7 Update OpenApi docs, rename /2fa/ => /mfa/ 2020-09-03 13:54:28 +02:00
Felix Spöttel
ce70f44c58 Extract TOTPStrategy class to totp.py
* this decouples `TOTP` validation and storage logic from `auth` and moves it to `totp`
* reduce `pyotp.validate#valid_window` from `2` to `1`
2020-09-03 11:19:19 +02:00
Felix Spöttel
6594e19a1f Autofocus otp input when logging in, update layout 2020-09-02 20:30:08 +02:00
Felix Spöttel
8597646a12 Update API route naming, update setup page
* Rename /two-factor-auth/ => /2fa/
* Nest totp routes under /2fa/totp/
* Update ids and methods in panel to allow for different setup types
2020-09-02 19:41:06 +02:00
Felix Spöttel
f205c48564 Use pyotp for validating TOTP codes
* also implements resynchronisation support via `pyotp`'s `valid_window option
2020-09-02 19:12:15 +02:00
Felix Spöttel
3c3683429b implement two factor check during login 2020-09-02 17:23:32 +02:00
Felix Spöttel
a7a66929aa add user interface for managing 2fa
* update user schema with 2fa columns
2020-09-02 16:48:23 +02:00
David Ferreira de Sousa Duque
a336931c95
Version release: v0.48.POWER.0 2020-08-26 23:13:41 +01:00
David Ferreira de Sousa Duque
d7d3561768 v0.48
Roundcube XSS vulnerability fixed.
 -----BEGIN PGP SIGNATURE-----
 
 iQFDBAABCgAtFiEEX0wOcxPM10RpOyrquSBB9MEL3YEFAl9GpkcPHGp0QG9jY2Ft
 cy5pbmZvAAoJELkgQfTBC92BoYAH/2NjdfN2d6f45uPq/X32bBAc6wfI7Cs9yCKp
 LOrAfPlmE0jRSm9ThATfZvaWci2r2IFhsFzQ9bWHpbIP5YD7mDD50I2uTnZa9BV4
 MsI40VXoh0BAgkWRqK60rTw0lQ9YGT+1TNLDEs1Y7vBjfTCOh4MMn4jUXkIEHDQg
 2pSHY1RUq7T0wRaHS+rTPDccotS/xCGg6uZJ+gSlvhRdxakAe9mo8139KD/4fjT8
 HK6igpwHsn3POg7mmJoSYXtScmWRYfnSV9kyfYyVyjhu5/uIowdICwFOzX7G7ruM
 yA/azBlyMs898e5jYFR1tQqQ1rVYVy/nqCQOiyJa34ngHGSi41U=
 =a9fn
 -----END PGP SIGNATURE-----
gpgsig -----BEGIN PGP SIGNATURE-----
 
 iQIzBAABCAAdFiEEAKK/toPAcMkE+dinLzJ3OKPArjoFAl9G3ZEACgkQLzJ3OKPA
 rjpEvQ//cG844Wv3samnABlRv2ZIjg6OXtjsE9OuN/O2exGhJ7wNhlJ5F3VyXP5Z
 Tufm2HNc3sg9lQUVmyvFbSx03f/tgRdykH2HDS55Q3Q9YdpmZJBGlqsoMN/GIfjU
 PDcLpN30XBt6S8qB/7d/U37ZW69OuFZLDRlwQZ66N/shxXkZSOp9U/9nH1Vf5OEU
 /L5RVtsi/baDnauXDJWpyNsLKKEB4jCrlEiVI437cN2Yr3Y/d1u4pf3zQOEQmAGV
 /A6fu2a9Kkc7IvcAKeoIvlyt+2gYw1zUDkBHf+LuSXGkxTt07L9Bjc7I6SY3DGo2
 QiEVOMDPiKWl+0UQ73w/lwCUS7FbzaG/Mj2+c1pJ85UaZYbU5ovjJLesk+UPIG8Q
 LmVCAHTw6QctZZi0BwP5epPk01zbeSBmRosT6b4l95G2sqh91CMNUjNRc7963yzB
 Z36hpaWSqpGKyEjma9XFvGi9Tfkg1JxblLjmPVqbyez7bpAMgSw5FBWU1zjxjOmi
 XOvGu/Fdu+gCre6IHfl8nTJNRc27UtJWTZjbQVl1OlbRx/h2QS/tKMpgN9VOrSV5
 koi4TuX+T4kSE2S+atzlPOQuDIOHfdlxaU2mlwtqVfHBjlwE7FEcdfIGa5Pl6Blj
 fFsDI7T6VGw2zup40vk/tRn5GY2KCPdp6rHhuCMA1PY6gerhhLw=
 =3X2X
 -----END PGP SIGNATURE-----

Merge upstream v0.48
2020-08-26 23:09:14 +01:00
Joshua Tauberer
0d72566c99 Merge v0.48 point release branch 2020-08-26 14:11:56 -04:00
Joshua Tauberer
62db58eaaf v0.48 2020-08-26 14:11:01 -04:00
Joshua Tauberer
891de8d6c3 Upgrade Roundcube to 1.4.8
Merges #1809
2020-08-26 14:10:04 -04:00
Richard Willis
62b9b1f15f
Add OpenAPI HTTP spec (#1804) 2020-08-22 15:44:19 -04:00
David Duque
03a1e57de6
Merge vagrant changes 2020-08-10 03:09:23 +01:00
David Duque
24c5d54f49
start.sh: Generate locales properly 2020-08-10 03:07:45 +01:00
David Duque
3d9f0e2135
Vagrant: Use libvirt/debian 2020-08-10 03:06:59 +01:00
David Duque
94da7bb088
status_checks.py: Properly terminate the process pools (#1795)
* Only spawn a thread pool when strictly needed

For --check-primary-hostname, the pool is not used.
When exiting, the other processes are left alive and will hang.

* Acquire pools with the 'with' statement
2020-08-09 11:42:39 -04:00
David Duque
ee22d4a4b9
Move advanced pages into separate dropdown menu 2020-08-06 19:15:15 +01:00
David Duque
0cf4ed9a24
Version bump 2020-07-30 15:43:48 +01:00
David Duque
1ba62c6112 v0.47
v0.47 (July 29, 2020)
 ---------------------
 
 Security fixes:
 
 * Roundcube is updated to version 1.4.7 fixing a cross-site scripting (XSS) vulnerability with HTML messages with malicious svg/na$
 * SSH connections are now rate-limited at the firewall level (in addition to fail2ban).
 -----BEGIN PGP SIGNATURE-----
 
 iQFDBAABCgAtFiEEX0wOcxPM10RpOyrquSBB9MEL3YEFAl8hh4APHGp0QG9jY2Ft
 cy5pbmZvAAoJELkgQfTBC92BD8EIAKuNEHxgL0C0kkpAhuTlVXuoNEH/2FF6hYS7
 7NqVrqOO1iVPGkGPhAh77CLpnvvJEhu9GeSWFhTrpI//5CvfafUQowmELClmDcYL
 yxHqgoHX9O0PAd+uCLgO3MdAzFMVLNbPmt/uPgEHufnrrQGIGieB2iGWnf9xnnpf
 wFSyQQnLofFpq7nH6qQvLNvh//zPQd7l/YV3ieEuT0dV4izg/Sr7Q5W6Zwn/q/ed
 Btp4CizRFRFTmulIEM8an+jSXMMvdVkut6WDcl6ct8LZLoWwtEkWVeru9IVu4n9L
 Lj8Bkt+8aRR6updnI/2tm0d7ZgFXWHc/+dfLCaK+aOlMD3qV9p0=
 =xsgn
 -----END PGP SIGNATURE-----
gpgsig -----BEGIN PGP SIGNATURE-----
 
 iQIzBAABCAAdFiEEAKK/toPAcMkE+dinLzJ3OKPArjoFAl8i0EQACgkQLzJ3OKPA
 rjopbg//T75ZyceGJVvDnzylhW65bIL8oUYiFLy/GOA39rmR1gjcwzHSaIP0kOtX
 HPpm6rhPhVUKo8pjuWSvLnnNfz9QfJ4b6SqAN2Zg/hiqFdMEShGZNqvSQVvLkfxi
 HHwa1C+TlRTD57HlVi9Y6TLX7YH65T9YmJol6KO30dGJRMIPssLg6K5k0Wf2Y2uG
 E+6tipkiTPcHEaKIHUPdi5xxTL/QHVn+c+C0nsiflX7i9vC6P30e3yNsOvpk3q7V
 XwD/bJfycUq8Qc5WhPsKoo287QY9XrkUco8vsVMDJJ1oCSIO1Ek5H/tgu2qB1QNJ
 EGtcAYr09Fi8+5PLhmbTgRRWJ6ez6SaNnxsh8W5FhRpymgujoe4ghMiuYCwfHW13
 ESB1KKZHGUiqP4nxHIgYyANrSP97qsZmVWUEQcwqhcP8BZY4NOzEsUKgIjTCTpVJ
 CbRUJlgQow7s/R76aH3Crb7xhbE+2eQPDgKQ6AwDySWbPTDd3T6MtL0Oe2MZS8Wg
 8mv02U+eqDfQ0TfD30vGIESARXJ1UJWfsLQzyyg7jBCTrIfSQt1IwFzXCASm78hs
 kHN0/gmXUULQq0FslKV/zrfOsNEzKX+sCwjOMG7RMlWVcEVkRyXFvcajBj72mvZl
 3kFOEqah8nErTStsP89Z+ltwfkVsWehu+vwP67NryRy4/B3y9fQ=
 =CTVK
 -----END PGP SIGNATURE-----

Merge upstream v0.47
2020-07-30 14:51:00 +01:00
Joshua Tauberer
65983b8ac7 Merge v0.47 point release branch 2020-07-29 10:27:06 -04:00
hija
56d0289ed9 v0.47 2020-07-29 10:24:56 -04:00
Marcus Bointon
f253c40012 [backport] Add rate limiting of SSH in the firewall (#1770)
See #1767. Backport of cfc8fb484c.
2020-07-29 10:24:23 -04:00
Joshua Tauberer
4bbe4af377 Update CHANGELOG 2020-07-29 10:23:02 -04:00
Hilko
2c34a6df2b Update roundcube to 1.4.7 2020-07-29 10:15:12 -04:00
Hilko
1098e2b48e
Add noindex to www_default meta tags (#1791) 2020-07-29 10:03:33 -04:00
Richard Willis
c50170b816
Update "Remove Alias" modal title (#1800) 2020-07-29 10:01:20 -04:00
Marcus Bointon
cd518e6820
Raise Dovecot per user connection limit (#1799) 2020-07-27 06:37:52 -04:00
David Duque
dd7899acca
Version bump 2020-07-26 01:03:28 +01:00
David Duque
5e597bb536
Update deprecated function from dnspython 2020-07-26 01:00:17 +01:00
David Duque
60911515fd
Support Ubuntu LTS point releases 2020-07-26 00:26:35 +01:00
David Duque
ac8c0ae762
Release v0.46.POWER.4 2020-07-22 12:45:18 +01:00
David Duque
16ae3038b3
Merge branch 'development' 2020-07-22 12:44:04 +01:00
David Duque
fc0bd12631
Acquire pools with the 'with' statement 2020-07-22 12:42:10 +01:00
David Duque
311e6c63e8
Render the 'Backup now' buttons even if there are already backups 2020-07-21 19:25:48 +01:00
David Duque
a0da88834c
Terminate the status checks process pool before exiting 2020-07-21 19:21:46 +01:00
David Duque
967409b157
Drop requirement for passwords to have no spaces (#1789) 2020-07-16 07:23:11 -04:00
David Duque
1b2711fc42
Add 'always' modifier to the HSTS add_header directive (#1790)
This will make it so that the HSTS header is sent regardless of the request status code (until this point it would only be sent if "the response code equals 200, 201, 206, 301, 302, 303, 307, or 308." - according to thttp://nginx.org/en/docs/http/ngx_http_headers_module.html#add_header)
2020-07-16 07:21:14 -04:00
David Duque
20b4f26e42
Use ubuntu/focal64 as main testbed 2020-07-15 15:28:47 +01:00
David Duque
c8fbe2dd5d
Determine the PHP version at runtime (instead of at setup-time) 2020-07-15 15:28:02 +01:00
David Duque
515a74ba11
Render the lsb_release at flask init time
Don't change the index.html file at setup time
2020-07-14 11:51:25 +01:00
David Duque
b562e7eefa
Hide the 'Create Backup' buttons when backups are turned off 2020-07-11 15:45:50 +01:00
David Duque
ccf60c7017
Backups: User-initiated and cron-initiated jobs will have the same lockname
So that some poor timing (initiating a backup when there's a cron-initiated backup)
doesn't screw everything up.
2020-07-11 09:16:32 +01:00
David Duque
e224b6b3b2
Update project status 2020-07-11 08:43:46 +01:00
David Duque
79e2398d71
Fix comment 2020-07-11 08:30:05 +01:00
David Duque
af9ef186b3
Add manual backup option 2020-07-10 15:48:37 +01:00
David Duque
e6102eacfb
AXFR Transfers (for secondary DNS servers): Allow IPv6 addresses (#1787) 2020-07-08 18:26:47 -04:00
David Duque
199c2c50ba
Backups: Fix backup target selector width 2020-07-08 19:32:24 +01:00
David Duque
4a85250242
Revert vagrantfile to upstream config 2020-07-08 19:31:35 +01:00
David Duque
dd017c44c7
Update ideas section and roadmap 2020-07-08 15:00:04 +01:00
John R. Supplee
e346ccfb02 v0.46
v0.46 (June 11, 2020)
 ---------------------
 
 Security fixes:
 
 * Roundcube is updated to version 1.4.6 (https://roundcube.net/news/2020/06/02/security-updates-1.4.5-and-1.3.12).
 -----BEGIN PGP SIGNATURE-----
 
 iQFDBAABCgAtFiEEX0wOcxPM10RpOyrquSBB9MEL3YEFAl7iW4MPHGp0QG9jY2Ft
 cy5pbmZvAAoJELkgQfTBC92B8awIAITrRfkfOFIfk7mbHY1giz/CJWP6+5EOV7J/
 a/1JOwRTccEy9Pjs1Tf/3VdamFnG6A5dzTNI/tPtqvVCT5cwy84mdCHgPFuZaLCs
 xBGdW8lf98a4sIP21GOXNi2mJlDqKog5ocGjgNJBw5mhCtY4A2IkjJ9S2X/bs1q0
 vAa1aBcNxaniTjLdTr80jVrzGzz9K6JtRowGDLz3YoJBxYCgXo/9CUTk3tOK+My2
 OnKVQCGX/23XoF5dYNtDmQBB3AwUfortBA30WNeipJ4VzgikcUQCyhrR3/wPr6P9
 wmVYmzv4Qw1bv/9ygijM4XLvgXx86LZPaFv+M82V7+Z3eIDW6QY=
 =8Sy4
 -----END PGP SIGNATURE-----

Merge tag 'v0.46' of https://github.com/mail-in-a-box/mailinabox

v0.46

v0.46 (June 11, 2020)
---------------------

Security fixes:

* Roundcube is updated to version 1.4.6
(https://roundcube.net/news/2020/06/02/security-updates-1.4.5-and-1.3.12).
2020-07-07 10:31:25 +02:00
David Duque
3dfdb9a309
Update Vagrantfile to pull from development branch 2020-07-03 19:01:16 +01:00
David Duque
1d4d03637f
Version bump 2020-06-29 09:47:38 +01:00
David Duque
b98111b4e1
Fix unassigned php version 2020-06-29 09:13:50 +01:00
David Duque
3876cbac8a
Version bump 2020-06-28 10:06:50 +01:00
David Duque
ffc7e8d77e
Add comments explaining 2020-06-28 10:05:25 +01:00
David Duque
7f305ee02e
Add /.well-known/mta-sts.txt to all nginx dotfiles 2020-06-28 09:57:28 +01:00
David Duque
fcb44dafa3
Let's encrypt certbot hotfix 2020-06-27 21:32:36 +01:00
David Duque
7af4ab0f4f
Version bump 2020-06-27 20:27:49 +01:00
David Duque
7864055490
Upgrade Nextcloud 2020-06-27 19:39:03 +01:00
David Duque
7b357fa71b Version bump (v0.46 rc) 2020-06-21 22:49:14 +01:00
David Duque
9a4cf4d7af Update dependencies 2020-06-21 16:02:17 +01:00
David Duque
022a11e159 Merge remote-tracking branch 'up/master' 2020-06-21 15:52:31 +01:00
David Duque
74554bcbf3 Version bump 2020-06-21 15:45:34 +01:00
David Duque
5d6c23cff9 Finalize php configuration 2020-06-21 15:18:46 +01:00
David Duque
0ccbf1b809 Only spawn a thread pool when strictly needed
For --check-primary-hostname, the pool is not used.
When exiting, the other processes are left alive and will hang.
2020-06-21 15:05:17 +01:00
Joshua Tauberer
6fd3195275 Fix MTA-STS policy id so it does not have invalid characters, fixes #1779 2020-06-12 13:09:11 -04:00
David Duque
d01069f7f2 Automatically agree to ToS on SSL provision 2020-06-12 09:27:08 +01:00
Joshua Tauberer
224242dfde Merge v0.46 point release branch 2020-06-11 12:25:49 -04:00
Joshua Tauberer
049bfb6f7f v0.46 2020-06-11 12:23:18 -04:00
Joshua Tauberer
12d60d102b Update Roundcube to 1.4.6
Fixes #1776
2020-06-11 12:21:17 -04:00
Joshua Tauberer
9db2fc7f05 In web proxies, add X-{Forwarded-{Host,Proto},Real-IP} and 'proxy_set_header Host' when there is a flag
Merges #1432, more or less.
2020-06-11 12:20:17 -04:00
Joshua Tauberer
e03a6541ce Don't make autoconfig/autodiscover subdomains and SRV records when the parent domain has no user accounts
These subdomains/records are for automatic configuration of mail clients, but if there are no user accounts on a domain, there is no need to publish a DNS record, provision a TLS certificate, or create an nginx server config block.
2020-06-11 12:20:17 -04:00
Faye Duxovni
41642f2f59 [backport] Fix roundcube error log file path in setup script (#1775) 2020-06-11 12:16:53 -04:00
Vasek Sraier
df9bb263dc
daily_tasks.sh: redirect stderr to stdout (#1768)
When the management commands fail, they can print something to the standard error output.
The administrator would never notice, because it wouldn't be send to him with the usual emails.
Fixes #1763
2020-06-07 09:56:45 -04:00
Faye Duxovni
339c330b4f
Fix roundcube error log file path in setup script (#1775) 2020-06-07 09:50:04 -04:00
Marcus Bointon
cfc8fb484c
Add rate limiting of SSH in the firewall (#1770)
See #1767.
2020-06-07 09:47:51 -04:00
Joshua Tauberer
bc1be9d70a readme fixes 2020-05-30 08:15:31 -04:00
Joshua Tauberer
3a4b8da8fd More for MTA-STS for incoming mail
* Create the mta_sts A/AAAA records even if there is no valid TLS certificate because we can't get a TLS certificate if we don't set up the domains.
* Make the policy id in the TXT record stable by using a hash of the policy file so that the DNS record doesn't change every day, which means no nightly notification and also it allows for longer caching by sending MTAs.
2020-05-30 08:04:09 -04:00
Joshua Tauberer
37dad9d4bb Provision certificates from Let's Encrypt grouped by DNS zone
Folks didn't want certificates exposing all of the domains hosted by the server (although this can already be found on the internet).

Additionally, if one domain fails (usually because of a misconfiguration), it would be nice if not everything fails. So grouping them helps with that.

Fixes #690.
2020-05-29 15:38:18 -04:00
Joshua Tauberer
b805f8695e Move status checks for www, autoconfig, autodiscover, and mta-sts to within the section for the parent domain
Since we're checking the MTA-STS policy, there's no need to check that the domain resolves etc. directly.
2020-05-29 15:38:13 -04:00
Joshua Tauberer
10bedad3a3 MTA-STS tweaks, add status check using postfix-mta-sts-resolver, change to enforce 2020-05-29 15:36:52 -04:00
A. Schippers
afc9f9686a
Publish MTA-STS policy for incoming mail (#1731)
Co-authored-by: Daniel Mabbett <triumph_2500@hotmail.com>
2020-05-29 15:30:07 -04:00
David Duque
8ca58798e4
Typo fix 2020-05-28 16:17:10 +01:00
David Duque
235ebe9a4a
Secondary nameservers: Allow IPv6 2020-05-28 15:47:43 +01:00
David Duque
211d3ff8a8
Fix os tag issues 2020-05-24 23:23:45 +01:00
John R. Supplee
332d2c3feb Add changes for new version to README 2020-05-17 18:19:11 +02:00
John R. Supplee
c152fe6312 v0.45 (May 16, 2020)
Security fixes:
 
 * Fix missing brute force login protection for Roundcube logins.
 
 Software updates:
 
 * Upgraded Roundcube from 1.4.2 to 1.4.4.
 * Upgraded Nextcloud from 17.0.2 to 17.0.6 (with Contacts from 3.1.6 to 3.3.0 and Calendar from 1.7.1 to v2.0.3)
 * Upgraded Z-Push to 2.5.2.
 
 System:
 
 * Nightly backups now occur on a random minute in the 3am hour (in the system time zone). The minute is chosen during Mail-in-a-Box installation/upgrade and remains the same until the next upgrade.
 * Fix for mail log statistics report on leap days.
 * Fix Mozilla autoconfig useGlobalPreferredServer setting.
 
 Web:
 
 * Add a new hidden feature to set nginx alias in www/custom.yaml.
 
 Setup:
 
 * Improved error handling.
 -----BEGIN PGP SIGNATURE-----
 
 iQFDBAABCgAtFiEEX0wOcxPM10RpOyrquSBB9MEL3YEFAl7AbCoPHGp0QG9jY2Ft
 cy5pbmZvAAoJELkgQfTBC92BjbEIAIwmIpgNCT+age/SsUhDY8pjnFQWXBCl1nwa
 RFN40Ev73DoBXUP+za4RE0eyCLIw5/laCwCjaESobiBTuc6boC1QU4abFUV5NfJQ
 P3AnQ2qXkrtcmIQX42ge4AGsL3vMVRtjZWb+bvut2SmLB8BI5w/9XsQAS59lqSz0
 kK6ShlDmFaToMgTQqwl0CW8a0vdjRca5Mq011xUZrvqTAm7ACQIvS6np4UYBGSNy
 bU8O1xWMJb0HlO7f+bWCDYr1I+nRS1xXMW9pKsE08YFwcRLa+C42QkDXDuS/o/zj
 EXBLGwYcB0DEu4wLLbih8xdbED2ZiMO2t6IHtbXPcoLtHo3Tv6I=
 =RUkr
 -----END PGP SIGNATURE-----

Merge tag 'v0.45' of https://github.com/mail-in-a-box/mailinabox

v0.45 (May 16, 2020)

Security fixes:

* Fix missing brute force login protection for Roundcube logins.

Software updates:

* Upgraded Roundcube from 1.4.2 to 1.4.4.
* Upgraded Nextcloud from 17.0.2 to 17.0.6 (with Contacts from 3.1.6 to 3.3.0 and Calendar from 1.7.1 to v2.0.3)
* Upgraded Z-Push to 2.5.2.

System:

* Nightly backups now occur on a random minute in the 3am hour (in the system time zone). The minute is chosen during Mail-in-a-Box installation/upgrade and remains the same until the next upgrade.
* Fix for mail log statistics report on leap days.
* Fix Mozilla autoconfig useGlobalPreferredServer setting.

Web:

* Add a new hidden feature to set nginx alias in www/custom.yaml.

Setup:

* Improved error handling.
2020-05-17 18:17:44 +02:00
David Duque
a51e968d31
Use pip3 2020-05-17 15:20:14 +01:00
David Duque
c95b91af5a
Force python3-pip 2020-05-17 15:14:43 +01:00
David Duque
117bdb7464
Update Nextcloud to the latest version
Nextcloud 17 doesn't support PHP 7.4 (and therefore Ubuntu 20.04 LTS)
2020-05-17 15:04:17 +01:00
David Duque
baa5d32dea
Make sure /etc/default/bind9 exists 2020-05-17 02:58:19 +01:00
David Duque
1513655bc4
Make sure that the OS in the admin panel matches the actual system OS 2020-05-17 02:45:35 +01:00
David Duque
959281c635
Version bump 2020-05-17 02:38:18 +01:00
David Duque
98b50ce333
Syntax function fix 2020-05-17 02:38:00 +01:00
David Duque
b66ade73c8
Begin support for Ubuntu LTS 2020-05-17 02:33:26 +01:00
Joshua Tauberer
7de8fc9bc0 v0.45 2020-05-16 06:45:23 -04:00
David Duque
de115fe01e Merge remote-tracking branch 'up/master' 2020-05-11 19:42:54 +01:00
yeuna92
c87b62b8c2
Fix path to Roundcube error log in fail2ban jails.conf (#1761) 2020-05-11 08:59:42 -04:00
clonejo
8fe33da85d Run nightly tasks on a random minute after 03:00 to avoid overload (#1754)
- The MIAB version check regularly fails at 03:00, presumably because a
  large portion of installations is checking mailinabox.email at the same
  time.
- At installation time, the time of the nightly clock is configured to
  run at a random minute after 03:00, but before 04:00.
- Users might expect the nightly tasks to be over at a certain time and
  run their own custom tasks afterwards. This could thus interfere with
  custom backup routines.
- This breaks reproducibility of the installation process.
- Users might also be surprised by the nightly task time changing after
  updating MIAB.
2020-05-10 19:54:45 -04:00
Joshua Tauberer
c202a5cbc6 Changlog entries 2020-05-10 19:46:25 -04:00
Joshua Tauberer
1353949e42 Upgrade Roundcube to 1.4.4, Nextcloud to 17.0.6, Z-Push to 2.5.2 2020-05-10 19:44:12 -04:00
Joshua Tauberer
c19f8c9ee6 Change Mozilla autoconfig useGlobalPreferredServer property to false
Fixes #1736.
2020-05-10 19:29:01 -04:00
David Duque
ad9979f9c6
Make the Show More link an actual button 2020-04-25 04:26:24 +01:00
David Duque
e75d89113a
Test 2020-04-25 04:17:55 +01:00
David Duque
7984d103a4
Test 2020-04-25 04:13:46 +01:00
David Duque
4309a6a875
Swap show-button order 2020-04-25 03:51:38 +01:00
David Duque
32e42f14fb
Do not apply custom nginx dotfiles to the default webroot 2020-04-24 17:03:13 +01:00
David Duque
d9567c0035
Use proper emojis for status checks 2020-04-24 15:50:01 +01:00
David Duque
7328990921
Installation instructions 2020-04-22 16:29:44 +01:00
David Duque
8ca34e18bd
Properly configure nsd, bump version 2020-04-22 16:25:12 +01:00
David Duque
3589ac2c06
Don't interfere with locales right away, bump version 2020-04-22 16:14:20 +01:00
David Duque
2d228c0520
Remove PPA installation process 2020-04-21 14:59:54 +01:00
David Duque
8ba078c04b
Attempt to fix some stuff and bump version 2020-04-21 14:48:16 +01:00
David Duque
372d5d9783
SMTP Relays: Wrap ternary operations correctly 2020-04-21 14:35:51 +01:00
David Duque
8aa1ea23c8
Use describe --tags when comparing versions 2020-04-21 14:26:51 +01:00
David Duque
2176d59727
Version check will now use the correct endpoint 2020-04-20 23:35:11 +01:00
David Duque
d98825f609
Update pre-flights, bump version 2020-04-20 23:31:18 +01:00
David Duque
ab9dbdf270
Default conf: don't repeat the upstream php conf 2020-04-20 20:01:19 +01:00
David Duque
502a4d2128
Uhhhh, yeah 2020-04-20 19:38:50 +01:00
David Duque
7ff5a336a6
Always assign default config to primary hostname 2020-04-20 19:35:20 +01:00
David Duque
c401625a01
Don't overwrite 2020-04-20 19:20:26 +01:00
David Duque
5f15c2e53b
Fix some stuff 2020-04-20 19:15:43 +01:00
David Duque
49da79cbd9
Oops (yet again) 2020-04-20 19:06:19 +01:00
David Duque
7f5a939e50
Throw failure reason (web update) 2020-04-20 18:59:02 +01:00
David Duque
3396bdbb22
Use get_web_root instead of raw indexing 2020-04-20 18:54:27 +01:00
David Duque
8c5ff2b523
Screwed up on the path 2020-04-20 18:28:13 +01:00
David Duque
b2c2d61867
Oops 2020-04-20 18:23:45 +01:00
David Duque
72070ee7bd
Create custom nginx files 2020-04-20 18:17:41 +01:00
David Duque
830248bf66
README update 2020-04-20 15:57:47 +01:00
David Duque
b6342d34d2
Start web templating work 2020-04-20 15:55:05 +01:00
David Duque
2ce037af5e
Modifications to nginx configuration files 2020-04-20 13:05:58 +01:00
David Duque
9a6a35cadc
Update version display 2020-04-20 00:43:20 +01:00
David Duque
ecb575e686
git describe --tags 2020-04-18 15:17:13 +01:00
David Duque
4ed014a50c
Add SMTP Relay status checks 2020-04-18 15:00:51 +01:00
David Duque
3bfdb0d77b
Version bump documentation 2020-04-18 14:45:42 +01:00
David Duque
a8be38fcc0
Refer to Debian instead of Ubuntu 2020-04-18 14:43:24 +01:00
David Duque
47622d5e0b
HTML Redirect 2020-04-18 14:40:07 +01:00
David Duque
ad3a78a300
relayhost, not relay_host, and key, not pass 2020-04-18 11:56:30 +01:00
David Duque
da7fe68daa
pls 2020-04-18 00:38:36 +01:00
David Duque
eee5dbf755
Is this it? 2020-04-18 00:22:10 +01:00
David Duque
8f247e3b70
Import other stuff 2020-04-18 00:13:13 +01:00
David Duque
10e4b79423
Fixing editconf input 2020-04-18 00:00:35 +01:00
David Duque
625eca8ea4
Swap tuple by a list 2020-04-17 23:48:39 +01:00
David Duque
858251045d
Cleanup 2020-04-17 23:39:52 +01:00
David Duque
52e9afcf2f
Just use the script directly 2020-04-17 22:59:25 +01:00
David Duque
c96c04b39d
I screwed up 2020-04-17 22:21:06 +01:00
David Duque
6f76ae68fd
Fix expression 2020-04-17 22:16:08 +01:00
David Duque
2c439abb64
Unpack argv 2020-04-17 20:53:49 +01:00
David Duque
b9dec64ea1 Merge branch 'master' of github.com:ddavness/mailinabox 2020-04-17 19:54:44 +01:00
David Duque
ca3d794c80
Sanity improvements 2020-04-17 19:54:34 +01:00
David Duque
60294a876d Allow editconf to be ran 2020-04-17 19:36:04 +01:00
David Duque
51a65a3a22
Fixes (again) 2020-04-17 19:16:31 +01:00
David Duque
02c2657569
Oops 2020-04-17 19:06:19 +01:00
David Duque
df8bacd0ed
Actually edit the files on Postfix's end 2020-04-17 19:05:05 +01:00
David Duque
c653f660bb
Move editconf into management 2020-04-17 18:54:13 +01:00
David Duque
25900758d1
I screwed up badly 2020-04-17 18:09:43 +01:00
David Duque
0e583b9e4f
Booleans are passed as strings 2020-04-17 18:06:16 +01:00
David Duque
6d8e2a5bb9
Cleanup 2020-04-17 10:45:47 +01:00
David Duque
9b6781685a
Move settings away from mailinabox.conf 2020-04-16 22:52:48 +01:00
David Duque
5e080bedb6
Syntax errors. 2020-04-16 22:21:33 +01:00
David Duque
430f6dab38
Actual implementation of the Relay setup daemon 2020-04-16 22:16:02 +01:00
David Duque
d9397a026b
SMTP Relay initialization 2020-04-16 20:19:14 +01:00
David Duque
7ffc889c08
Bump web dependencies (#1)
- Bootstrap: 3.3.7 -> 4.4.1
- - New admin panel style and respective corrections applied.

- JQuery: 2.1.4 -> 3.5.0
2020-04-16 19:52:01 +01:00
David Duque
785280c86b
Submission 2020-04-16 17:01:49 +01:00
David Duque
fcf5544fc8
WIP 2020-04-16 14:09:24 +01:00
David Duque
09b3c37885
Oops 2020-04-16 14:04:45 +01:00
David Duque
bf83bd6ff7
MiaB SMTP Daemon: Just return something for now 2020-04-16 12:56:27 +01:00
David Duque
03472788fd
WIP 2020-04-16 12:45:55 +01:00
David Duque
7b60c0850c Merge remote-tracking branch 'up/master' 2020-04-15 18:03:50 +01:00
David Duque
68768ed112
Fix attempt 2020-04-15 18:01:33 +01:00
David Duque
21196620b6
Code debug 2020-04-15 15:02:20 +01:00
David Duque
bb26a2d12c
Push script zone to the end of the document 2020-04-14 10:08:11 +01:00
David Duque
687721caf8
Load SMTP settings when entering the page 2020-04-14 10:03:44 +01:00
David Duque
14ee44e8e2
HTML JS Cleanup 2020-04-14 09:52:01 +01:00
Michael Becker
40b21c466d
Fypo fix in users.html (#1748) 2020-04-13 22:10:52 -04:00
David Duque
109d8735c7
Change admin panel footer 2020-04-13 19:32:38 +01:00
David Duque
ee688eb184
SMTP Relay Host stub progress 2020-04-13 18:36:56 +01:00
David Duque
c004e55c76
SMTP Relay Host stub progress 2020-04-13 18:29:14 +01:00
David Duque
6e462f6523
SMTP Relay Host stub progress 2020-04-13 18:21:33 +01:00
David Duque
51c288dcd5
SMTP Relay Host stub progress 2020-04-13 18:14:39 +01:00
David Duque
6393075f11
SMTP Relay Host stub progress 2020-04-13 18:09:04 +01:00
David Duque
9d23f67e8a
SMTP Relay Host stub progress 2020-04-13 18:05:49 +01:00
David Duque
10993b9154
SMTP Relay Host stub progress 2020-04-13 17:55:00 +01:00
David Duque
3278c8cf94
SMTP Relay Host stub progress 2020-04-13 16:18:26 +01:00
David Duque
aa62c6349e
SMTP Relay Host stub progress 2020-04-13 16:13:26 +01:00
David Duque
30221bdb11
SMTP Relay Host stub progress 2020-04-13 15:57:56 +01:00
David Duque
664267357a
SMTP Relay Host stub progress 2020-04-13 15:53:47 +01:00
David Duque
4a20d50eea
SMTP Relay Host stub 2020-04-13 15:34:14 +01:00
David Duque
cd4c478986
Add smtp relay html page 2020-04-13 01:24:36 +01:00
David Duque
4aa671c20b
Working on SMTP relays! 2020-04-13 01:18:19 +01:00
David Duque
974c9bba61
Fix status check colors, add SMTP relay stub 2020-04-13 01:16:23 +01:00
David Duque
0d17caccfe
Downgrade port 25 blockage error to warn; mention SMTP relays 2020-04-13 01:10:38 +01:00
David Duque
8d7f6bfb19
Change all occurrences of PHP 7.2 to PHP 7.3 2020-04-13 00:55:48 +01:00
David Duque
edb03b7862
Misc changes 2020-04-12 23:54:35 +01:00
David Duque
fa6e941e56
README update 2020-04-12 01:41:01 +01:00
David Duque
8667a574d4
Use local PHP version (ZPush) 2020-04-12 01:04:55 +01:00
David Duque
5f1343864c
Use local PHP version 2020-04-12 01:02:14 +01:00
David Duque
6cee029d15
Move php version to functions.sh 2020-04-12 00:56:55 +01:00
David Duque
5c93d698b4
Update PHP version (7.2 -> 7.3) 2020-04-12 00:50:31 +01:00
David Duque
0568bcc5d3
Early configuration of the nsd service 2020-04-12 00:44:19 +01:00
David Duque
435b01dda4 Merge remote-tracking branch 'up/master' 2020-04-11 19:32:10 +01:00
David Duque
9fb02090bf
Remove operations not applicable to Debian
Signed-off-by: David Duque <david.f.s.duque@tecnico.ulisboa.pt>
2020-04-11 19:24:20 +01:00
Stefan
f52749b403
Better return codes after errors in the setup scripts (#1741) 2020-04-11 14:18:44 -04:00
Sumit
d67e09f334
Allowing adding nginx aliases in www/custom.yaml (#1742)
with this nginx will keep on proxying requests and serve static content
instead of passing this responsibility to proxied server

Without this the one needs to run an additional server to server static
content on the proxied url
2020-04-11 14:17:46 -04:00
David Duque
fcff1f5d2c
Change environment to Debian 10 2020-04-11 19:12:40 +01:00
Daniel Davis
e224fc6656
Delete unused function apt_add_repository_to_unattended_upgrades (#1721)
The function apt_add_repository_to_unattended_upgrades is defined
but never called anywhere. It appears that automatic apt updates
are handled in system.sh where the file /etc/apt/apt.conf.d/02periodic
is created. The last call was removed in bbfa01f33a.

Co-authored-by: ddavis32 <dan@nthdegreesoftware.com>
2020-03-08 09:49:39 -04:00
Joshua Tauberer
5e47677f7a
Merge mail log script fixes for UTF-8 issue and Feb 29 issue (#1734) 2020-03-08 09:37:43 -04:00
Jarek Jurasz
db9637ce4f Fix Feb 29 issue #1733 2020-03-03 20:59:28 +01:00
Jarek Jurasz
f908bc364e mail_log.py reading forward #1593 2020-03-03 20:56:30 +01:00
John R. Supplee
9b96b93260 Merge v0.44
# Conflicts:
#	setup/bootstrap.sh
2020-03-02 21:54:27 +02:00
Joshua Tauberer
30c2c60f59 v0.44 2020-02-15 07:15:09 -05:00
Joshua Tauberer
ab5ce01bdd Some changelog entries 2020-01-22 03:36:02 -05:00
Joshua Tauberer
ddadb6c28a Roundcube 1.4.2 2020-01-22 03:25:53 -05:00
Joshua Tauberer
23be1031b8 Remove security.md's information about port 25 which is out of date 2020-01-22 03:25:30 -05:00
Michael Kroes
faee29ba8b Bump Nextcloud to 17.0.2 (#1702) 2020-01-22 03:06:17 -05:00
E.M. Makat
b86bf07d57 Fix spelling of 'guarantee' (#1703) 2020-01-22 02:58:40 -05:00
jvolkenant
e6294049bc Update Roundcube persistent_login plugin (#1712) 2020-01-22 02:58:04 -05:00
Joshua Tauberer
30885bcc8a Downgrade TLS settings for port 25, partially reverting f53b18ebb9
Port 25 now is aligned with Mozilla's "Old" recommendations at https://ssl-config.mozilla.org/#server=postfix&server-version=3.3.0&config=old&openssl-version=1.1.1.

See #1705
2020-01-20 14:52:23 -05:00
Bart
a67f90593d Replace dead link with archive.org link (#1698) 2019-12-19 18:33:36 -05:00
Joshua Tauberer
385340da46 install openssh-client which provides ssh-keygen and is not present on desktop Ubuntu by default 2019-12-12 11:27:39 -05:00
jvolkenant
0271e549bb Fix typo in InstallNextcloud calls (#1693) 2019-12-10 19:01:09 -05:00
Joshua Tauberer
f53b18ebb9 Upgrade TLS settings 2019-12-01 17:49:36 -05:00
Joshua Tauberer
8567a9b719 Fix upgrade issue broken by 802e7a1f4d 2019-12-01 17:44:12 -05:00
Vasek Sraier
ad9d732608 OpenDKIM canonicalization changed to relaxed for mail headers (#1620)
Because Mailman reformats headers it breaks DKIM signatures. SPF also does
not apply in mailing lists. This together causes DMARC to fail and mark the
email as invalid. This fixes DKIM signatures for Mailman-based mailing lists
and makes sure DMARC test is passed.
2019-12-01 16:24:38 -05:00
jvolkenant
aa15670dc2 Fixed multiple commented add_header entries in /etc/spamassassin/local.cf (#1641) 2019-12-01 16:23:02 -05:00
jvolkenant
81176c8e4b Fix to prevent multiple commented entries in dovecot conf (#1642) 2019-12-01 16:22:17 -05:00
Carl Reinke
960b5d5bbd Don't use ifquery to check interface state since it is no longer installed (#1689) 2019-12-01 16:21:38 -05:00
Carl Reinke
802e7a1f4d Copy systemd service files before linking to avoid issue with order of mounting filesystems (#1688) 2019-12-01 16:15:04 -05:00
Michael Kroes
52c68c6510 Implement Nextcloud php-fpm recommended performance tuning settings (#1679) 2019-12-01 16:13:33 -05:00
Michael Kroes
54b1ee9a3d Nextcloud 17 (#1676) 2019-12-01 16:11:00 -05:00
Francesco Montanari
6e3dee8b3b Upgrade RoundCube to 1.4.1 and set the default skin to elastic (#1673)
* Upgrade RoundCube to 1.4.0 and set the default skin to elastic
* Install php-ldap extension
* Remove smtp parameters that are now the default
2019-12-01 16:10:04 -05:00
Matthias Hähnel
cd62fd9826 Update usage hint in backup.py (#1662)
removed explicit call of the system python, cause the file has a shebang with the mail-in-a-box shipped python. 
for me the system python complaint, that it is missing some modules
2019-11-23 08:04:22 -05:00
Michael Kroes
91638c7fe0 Removed the postgrey option that specifies which whitelist file to use. This allows the usage of a .local verion (#1675) 2019-11-23 07:58:29 -05:00
Michael Kroes
ff8170d5ab Align nextcloud cron job with recommended settings (#1680) 2019-11-23 07:51:22 -05:00
John Supplee
8e94402282 Fix bug in displaying users when there is an archived user 2019-11-22 17:13:57 +02:00
Joshua Tauberer
f6f75f6fab Don't fail when resolving zone transfer IP addresses since a nameserver may not have an IPv6 address 2019-11-19 09:57:33 -05:00
Edwin Schaap
2f54f39f31 If xfr is subnet, do not create "notify" entry (#1672) 2019-11-10 11:58:22 -05:00
Victor
fa792f664e Use correct setting for .editorconfig indent_style (#1670) 2019-11-03 13:31:29 -05:00
Joshua Tauberer
b50dfb7f93 changelog entries 2019-11-02 15:57:14 -04:00
Dan Jensen
cde4e0caca Change SSL notification email subject (#1653)
Previously the notification email sent when a box's SSL certificate
is automatically updated said, "Error Provisioning TLS Certificate"
even when there was no error. This changes the subject line to "TLS
Certificate Provisioning Results", which is more accurate.
2019-11-02 15:29:05 -04:00
jvolkenant
df80b9fc71 Allow user_external for Nextcloud 16 (and eventually 17) (#1655) 2019-11-02 15:28:36 -04:00
notEvil
7558ffd4f3 Allow dns zone transfer from IPv6 (#1643) 2019-10-28 06:31:50 -04:00
Victor
50e9e8af30 Sort custom dns table based on fqdn, rtype, and value (#1651) 2019-10-28 06:29:40 -04:00
jvolkenant
ed02e2106b Update zpush to 2.5.1 (#1654) 2019-10-28 06:27:54 -04:00
John Supplee
0860a93e84 New release to remove extra features from the master branch 2019-10-11 12:43:32 +02:00
John Supplee
e04f358cc4 remove extra features from master branch 2019-10-11 12:40:50 +02:00
John Supplee
473d4616f2 update README for new release 2019-10-10 17:09:33 +02:00
John Supplee
3a23c8f7cf only show 'set quota' for non-archived mail boxes 2019-10-10 16:54:31 +02:00
John Supplee
e0626a4304 Merge branch 'master' of supplee.net:mailinabox-quota 2019-10-10 16:44:27 +02:00
Jeff Volkenant
24a567c3be Fix mailinabox-postgrey-whitelist cron job return code for file over 28 days
Merges #1639
2019-10-05 16:27:21 -04:00
Brendan Hide
70f05e9d52 Ensure the universe repository is enabled
A minimal Ubuntu server installation might not have universe enabled by
default. By adding it, we ensure we can install packages only available
in universe, such as python3-pip

Merges #1650.
2019-10-05 16:14:12 -04:00
Michael Kroes
889118aeb6 Upgraded Nextcloud to 16.0.5 (#1648)
* Upgraded Nextcloud to 16.0.5

* Improved Nextcloud upgrade detection
2019-10-05 16:12:00 -04:00
John Supplee
a05a33051e Increment quota version to 0.19-beta 2019-10-04 17:46:10 +02:00
John Supplee
0bef62743c Merge branch 'master' of github.com:jrsupplee/mailinabox 2019-10-04 17:37:18 +02:00
John Supplee
dbf29cf71a Update HTML API docs for mail users 2019-10-04 17:32:50 +02:00
Joshua Tauberer
a70ba94b0c add autoconfig domains before subtracting domains with overridden A records so that a custom DNS record can be used to suppress TLS certificate generation for those domains if needed 2019-09-10 07:11:16 -04:00
John R. Supplee
105a07e276 Update README 2019-09-02 18:40:52 -04:00
John R. Supplee
4686c8cd25 v0.43 (September 1, 2019)
-------------------------
 
 Security fixes:
 
 * A security issue was discovered in rsync backups. If you have enabled rsync backups, the file `id_rsa_miab` may have been copied to your backup destination. This file can be used to access your backup destination. If the file was copied to your backup destination, we recommend that you delete the file on your backup destination, delete `/root/.ssh/id_rsa_miab` on your Mail-in-a-Box, then re-run Mail-in-a-Box setup, and re-configure your SSH public key at your backup destination according to the instructions in the Mail-in-a-Box control panel.
 * Brute force attack prevention was missing for the managesieve service.
 
 Setup:
 
 * Nextcloud was not upgraded properly after restoring Mail-in-a-Box from a backup from v0.40 or earlier.
 
 Mail:
 
 * Upgraded Roundcube to 1.3.10.
 * Fetch an updated whitelist for greylisting on a monthly basis to reduce the number of delayed incoming emails.
 
 Control panel:
 
 * When using secondary DNS, it is now possible to specify a subnet range with the `xfr:` option.
 * Fixed an issue when the secondary DNS option is used and the secondary DNS hostname resolves to multiple IP addresses.
 * Fix a bug in how a backup configuration error is shown.
 -----BEGIN PGP SIGNATURE-----
 
 iQFDBAABCgAtFiEEX0wOcxPM10RpOyrquSBB9MEL3YEFAl1rrwIPHGp0QG9jY2Ft
 cy5pbmZvAAoJELkgQfTBC92BgckIALFnDFxhQ18MtClpi79+rnl1aA5DqbToCuI2
 MHIAOmxIVSavnd5MZZ3efXWAzIniEpbq0X+6Rlzas5lkreT1mHoJsKdkt0bOqy1a
 ZF2vT5UnUM9cwPHkU1ak/TaD9v97wbHpWWGwAK+/zTL6w1ReCVfQ2QzCzoDaY7xh
 OZFXE+YsaI7qZeG3Q4jfFr0IYDowLgjgBpdWvO71QKzWjIIvBNX1ZGt2r+cuKmQ5
 JOXIAR4fdri0p8dMd2sqq0FatBBCfjHDBykA/+GzJJDBX7MNoZsQT3bowrhj8XPS
 f5cKUKm7zlDsm02bfCtDD6nvYYUxvOdQx7yfdL8RYSdy71Chs20=
 =7M/i
 -----END PGP SIGNATURE-----

Merge tag 'v0.43' of https://github.com/mail-in-a-box/mailinabox

v0.43 (September 1, 2019)
-------------------------

Security fixes:

* A security issue was discovered in rsync backups. If you have enabled rsync backups, the file `id_rsa_miab` may have been copied to your backup destination. This file can be used to access your backup destination. If the file was copied to your backup destination, we recommend that you delete the file on your backup destination, delete `/root/.ssh/id_rsa_miab` on your Mail-in-a-Box, then re-run Mail-in-a-Box setup, and re-configure your SSH public key at your backup destination according to the instructions in the Mail-in-a-Box control panel.
* Brute force attack prevention was missing for the managesieve service.

Setup:

* Nextcloud was not upgraded properly after restoring Mail-in-a-Box from a backup from v0.40 or earlier.

Mail:

* Upgraded Roundcube to 1.3.10.
* Fetch an updated whitelist for greylisting on a monthly basis to reduce the number of delayed incoming emails.

Control panel:

* When using secondary DNS, it is now possible to specify a subnet range with the `xfr:` option.
* Fixed an issue when the secondary DNS option is used and the secondary DNS hostname resolves to multiple IP addresses.
* Fix a bug in how a backup configuration error is shown.
2019-09-02 18:39:23 -04:00
John R. Supplee
fa3c3236d8 v0.43 (September 1, 2019)
-------------------------
 
 Security fixes:
 
 * A security issue was discovered in rsync backups. If you have enabled rsync backups, the file `id_rsa_miab` may have been copied to your backup destination. This file can be used to access your backup destination. If the file was copied to your backup destination, we recommend that you delete the file on your backup destination, delete `/root/.ssh/id_rsa_miab` on your Mail-in-a-Box, then re-run Mail-in-a-Box setup, and re-configure your SSH public key at your backup destination according to the instructions in the Mail-in-a-Box control panel.
 * Brute force attack prevention was missing for the managesieve service.
 
 Setup:
 
 * Nextcloud was not upgraded properly after restoring Mail-in-a-Box from a backup from v0.40 or earlier.
 
 Mail:
 
 * Upgraded Roundcube to 1.3.10.
 * Fetch an updated whitelist for greylisting on a monthly basis to reduce the number of delayed incoming emails.
 
 Control panel:
 
 * When using secondary DNS, it is now possible to specify a subnet range with the `xfr:` option.
 * Fixed an issue when the secondary DNS option is used and the secondary DNS hostname resolves to multiple IP addresses.
 * Fix a bug in how a backup configuration error is shown.
 -----BEGIN PGP SIGNATURE-----
 
 iQFDBAABCgAtFiEEX0wOcxPM10RpOyrquSBB9MEL3YEFAl1rrwIPHGp0QG9jY2Ft
 cy5pbmZvAAoJELkgQfTBC92BgckIALFnDFxhQ18MtClpi79+rnl1aA5DqbToCuI2
 MHIAOmxIVSavnd5MZZ3efXWAzIniEpbq0X+6Rlzas5lkreT1mHoJsKdkt0bOqy1a
 ZF2vT5UnUM9cwPHkU1ak/TaD9v97wbHpWWGwAK+/zTL6w1ReCVfQ2QzCzoDaY7xh
 OZFXE+YsaI7qZeG3Q4jfFr0IYDowLgjgBpdWvO71QKzWjIIvBNX1ZGt2r+cuKmQ5
 JOXIAR4fdri0p8dMd2sqq0FatBBCfjHDBykA/+GzJJDBX7MNoZsQT3bowrhj8XPS
 f5cKUKm7zlDsm02bfCtDD6nvYYUxvOdQx7yfdL8RYSdy71Chs20=
 =7M/i
 -----END PGP SIGNATURE-----

Merge tag 'v0.43' of https://github.com/mail-in-a-box/mailinabox

v0.43 (September 1, 2019)
-------------------------

Security fixes:

* A security issue was discovered in rsync backups. If you have enabled
rsync backups, the file `id_rsa_miab` may have been copied to your
backup destination. This file can be used to access your backup
destination. If the file was copied to your backup destination, we
recommend that you delete the file on your backup destination, delete
`/root/.ssh/id_rsa_miab` on your Mail-in-a-Box, then re-run
Mail-in-a-Box setup, and re-configure your SSH public key at your backup
destination according to the instructions in the Mail-in-a-Box control
panel.
* Brute force attack prevention was missing for the managesieve service.

Setup:

* Nextcloud was not upgraded properly after restoring Mail-in-a-Box from
a backup from v0.40 or earlier.

Mail:

* Upgraded Roundcube to 1.3.10.
* Fetch an updated whitelist for greylisting on a monthly basis to
reduce the number of delayed incoming emails.

Control panel:

* When using secondary DNS, it is now possible to specify a subnet range
with the `xfr:` option.
* Fixed an issue when the secondary DNS option is used and the secondary
DNS hostname resolves to multiple IP addresses.
* Fix a bug in how a backup configuration error is shown.

5F4C0E7313CCD744693B2AEAB92041F4C10BDD81
2019-09-02 18:33:26 -04:00
Joshua Tauberer
9e29564f48 v0.43 2019-09-01 07:43:47 -04:00
Joshua Tauberer
5aeced5c2e add a test for fail2ban monitoring managesieve 2019-08-31 09:15:41 -04:00
Joshua Tauberer
46f64e0e0a fail2ban should watch for managesieve logins too, fixes #1622 2019-08-31 09:04:17 -04:00
Joshua Tauberer
4971b63501 changelog entries 2019-08-31 08:52:32 -04:00
Joshua Tauberer
3ff9817325 document the xfr: CIDR notation, fix spaces vs tabs and syntax error, broken by c7377e602d, #1616 2019-08-31 08:50:44 -04:00
jvolkenant
d6becddbe5 Change Nextcloud upgrade logic to look at STORAGE_ROOT's config.php version vs /usr/local's version.php version (#1632)
* Download and verify Nextcloud download before deleting old install directory
* Changed install logic to look at config.php and not version.php for database version number. When restoring from a backup, config.php in STORAGE_ROOT will hold the Nextcloud version that corresponds to the user's database and version.php in /usr/local won't even exist, so we were missing Nextcloud migration steps. In other cases they should be the same.
2019-08-31 08:50:36 -04:00
Michael Kroes
1d6793d124 Update the Postgrey whitelist to a newer version monthly (#1611)
Automatically update the Postgrey whitelist to a newer version once a month.
2019-08-31 08:38:41 -04:00
Kim Schulz
c7377e602d make it possible to use subnet addresses for axfr (#1616)
it is sometimes needed to be able to set axfr to more than just one ip address. This can be done with multiple xfr: in  the secondary dns input but if you need to add an entire subnet segment (xxx.xxx.xxx.0/yy) then it will not work.
With this patch it is now possible to use a subnet as input for xfr the same way as if it was an ip address.
2019-08-31 08:00:18 -04:00
Snacho
08021ea19f Fix an issue when Secondary NS has multiple A records (#1633)
If a custom secondary NS server has multiple A records status_checks.py will fail with a timeout and Web UI won't load.
2019-08-31 07:58:12 -04:00
cmharper
295d481603 Upgraded roundcube to 1.3.10 (#1634) 2019-08-31 07:55:38 -04:00
John R. Supplee
7636a7b40b Update README for v0.42b 2019-08-28 17:50:38 -04:00
John R. Supplee
32a0b0467f v0.42b (August 3, 2019)
-----------------------
 
 Changes:
 
 * Decreased the minimum supported RAM to 502 Mb.
 * Improved mail client autoconfiguration.
 * Added support for S3-compatible backup services besides Amazon S3.
 * Fixed the control panel login page to let LastPass save passwords.
 * Fixed an error in the user privileges API.
 * Silenced some spurrious messages.
 
 Software updates:
 
 * Upgraded Roundcube from 1.3.8 to 1.3.9.
 * Upgraded Nextcloud from 14.0.6 to 15.0.8 (with Contacts from 2.1.8 to 3.1.1 and Calendar from 1.6.4 to 1.6.5).
 * Upgraded Z-Push from 2.4.4 to 2.5.0.
 
 Note that v0.42 (July 4, 2019) was pulled shortly after it was released to fix a Nextcloud upgrade issue.
 -----BEGIN PGP SIGNATURE-----
 
 iQFDBAABCgAtFiEEX0wOcxPM10RpOyrquSBB9MEL3YEFAl1FrScPHGp0QG9jY2Ft
 cy5pbmZvAAoJELkgQfTBC92BCvgH/AieqUcy/ujFML4YeIKY/ThQNFdI6VUO0b4b
 QBzR9ixc9ctp6qV+wVfnL22xO4V2rcO/eMAKMc6AzOw6E97iWHtK/4L2fmRYXclw
 arln7LokFTczSp1J29ldRjCYMn3dOS1IvBRJe8JWvFNdQY6gGaGrhqV/nCIECOWP
 sJsJ/sPv2d4ZapbQtfqBh2WiLADNf1CSmDpkCXwS9Va+XstVeprIk1Kbsy8z5L8K
 0cFZWxiWTB/hgHD/BqUNxrzB5mS4fCyr9c9g+OIjZFeoF/olYPp4SsEFHPQ604YG
 qRupfjdG1bWHUpTT5lvG1AVIIPJPnvFk/Ctk1iQiYSU5BzbvKz8=
 =4QdN
 -----END PGP SIGNATURE-----

Merge tag 'v0.42b' of https://github.com/mail-in-a-box/mailinabox

v0.42b (August 3, 2019)
-----------------------

Changes:

* Decreased the minimum supported RAM to 502 Mb.
* Improved mail client autoconfiguration.
* Added support for S3-compatible backup services besides Amazon S3.
* Fixed the control panel login page to let LastPass save passwords.
* Fixed an error in the user privileges API.
* Silenced some spurrious messages.

Software updates:

* Upgraded Roundcube from 1.3.8 to 1.3.9.
* Upgraded Nextcloud from 14.0.6 to 15.0.8 (with Contacts from 2.1.8 to
3.1.1 and Calendar from 1.6.4 to 1.6.5).
* Upgraded Z-Push from 2.4.4 to 2.5.0.

Note that v0.42 (July 4, 2019) was pulled shortly after it was released
to fix a Nextcloud upgrade issue.

5F4C0E7313CCD744693B2AEAB92041F4C10BDD81
2019-08-28 17:26:00 -04:00
captainwasabi
c4cb828f65 Fix rsync backup options string: extraneous single quotes causing problems (#1629)
The resulting command had nested single quotes which doesn't work

I think this fixes all/most of the issues in #1627.  I am getting a full backup, then the next time it's run I get an incremental.  running from the CLI with --status looks good, --verify looks good, and --list looks good.
2019-08-13 05:57:05 -04:00
captainwasabi
0657f9e875 add proper check for DNS error in list_target_files (#1625)
The elif needed to check to see if the string was in the listing of results of the shell command.  As it was the conditional was just the string which always evaluates to true and was therefore giving a misleading error message.
2019-08-13 05:47:11 -04:00
Joshua Tauberer
e37768ca86 v0.42b 2019-08-03 11:49:32 -04:00
jvolkenant
bea5eb0dda Add interm upgrade step from Nextcloud 13 -> 14 (#1605) 2019-07-12 06:41:16 -04:00
jvolkenant
fd5b11823c Add AAAA records for autodiscover & autoconfig (#1606) 2019-07-10 06:28:37 -04:00
Joshua Tauberer
5fc1944f04 pull v0.42, go back to v0.41 2019-07-05 11:56:54 -04:00
Joshua Tauberer
39fd4ce16c v0.42 2019-07-04 21:34:55 -04:00
Joshua Tauberer
c0f4d5479f changelog updates 2019-06-16 11:40:40 -04:00
jvolkenant
193763f8f0 Update to Nextcloud 15.0.8, Contacts to 3.1.1, and Calendar to 1.6.5 (#1577)
* Update to Nextcloud 15.0.7, Contacts to 3.1.1, and Calendar to 1.6.5
* Enabled localhost-only insecure IMAP login for localhost Nextcloud auth
* Add package php-imagick and BigInt conversion
* added support for /cloud/oc[sm]-provider/ endpoint
2019-06-16 11:10:52 -04:00
jvolkenant
79759ea5a3 Upgrade Z-Push to 2.5.0 (#1581) 2019-06-16 11:07:45 -04:00
jvolkenant
6e5ceab0f8 hide virtualenv output (#1578) 2019-05-15 11:59:32 -07:00
jvolkenant
c6fa0d23df check that munin-cron is not running (via cron) when it is run in setup, fixes #660 (#1579) 2019-05-15 11:58:40 -07:00
cmharper
85e59245fd hide 'RTNETLINK answers: Network is unreachable' error message during setup if IPv6 is not available (#1576) 2019-05-15 11:57:06 -07:00
jvolkenant
4232a1205c fix dovecot message about SSLv2 not supported by OpenSSL (#1580) 2019-05-15 11:46:52 -07:00
Michael Heuberger
0d4c693792 Add missing login form method to keep LastPass happy (#1565) 2019-05-12 05:10:34 -07:00
Pascal Garber
77b2246010 Backup Amazon S3: Added support for custom endpoints (#1427) 2019-05-12 05:09:30 -07:00
jvolkenant
aff80ac58c Autodiscovery fix for additional hosted email domains, Fixes #941 (#1467) 2019-05-09 10:13:23 -07:00
John Supplee
e10bc30b4f Merge branch 'extended' into extended-fts 2019-04-30 13:59:21 +02:00
John Supplee
da3fd95b15 Merge branch 'extended' of supplee.net:mailinabox-quota into extended 2019-04-30 13:58:53 +02:00
John Supplee
9f59c056b8 go back to tomcat9.service.d 2019-04-20 13:18:54 +02:00
John Supplee
27f5085af7 fix path for tomcat9 solr conf 2019-04-20 12:45:33 +02:00
John Supplee
4e8e106ca4 Merge branch 'ubuntu_bionic_solr_fts' of https://github.com/jvolkenant/mailinabox into extended-fts 2019-04-20 12:42:42 +02:00
John Supplee
9e2d82b78f 18.04 moved to tomcat9 2019-04-20 11:59:24 +02:00
Jeff Volkenant
9386b4280c updated for tomcat9 and systemctl permission override 2019-04-18 09:29:42 -07:00
Jeff Volkenant
173aece6cd Merge branch 'master' of https://github.com/jvolkenant/mailinabox into ubuntu_bionic_solr_fts 2019-04-18 09:24:25 -07:00
John Supplee
f3572e51cf Merge branch 'extended' into extended-fts 2019-04-17 11:25:49 +02:00
John Supplee
102a6f6403 Merge branch 'master' of github.com:jrsupplee/mailinabox 2019-04-17 11:25:32 +02:00
John Supplee
c44eb9aaf5 Merge branch 'master' of github.com:jrsupplee/mailinabox into extended 2019-04-17 11:25:01 +02:00
John Supplee
3320432d59 Merge branch 'extended' into extended-fts 2019-04-17 11:23:44 +02:00
John Supplee
eb9cea03d6 Merge branch 'master' into extended 2019-04-17 11:23:30 +02:00
John Supplee
bac849bde8 Add check that IMAP quota service is running 2019-04-17 10:57:56 +02:00
John Supplee
0f7bbe0dde
Update README.md 2019-04-15 16:34:56 +02:00
just4t
25fec63a03 RAM limit to 502Mb to meet EC2 & Vultr 512Mb inst. (#1560)
AS told here: https://github.com/mail-in-a-box/mailinabox/pull/1534
2019-04-14 16:33:50 -04:00
dexbleeker
9b46637aff Update Roundcube to version 1.3.9 (#1546) 2019-04-14 14:19:21 -04:00
mbraem
fb25013334 user privileges is a set (#1551)
fixes #1540
2019-04-14 14:17:43 -04:00
John Supplee
a1536c7386 Merge branch 'extended' of supplee.net:mailinabox-quota into extended 2019-04-05 23:44:52 +02:00
John Supplee
57473ef39c fix problem with SSL certificate provisioning 2019-04-05 23:44:18 +02:00
John Supplee
39d55f2770 Merge branch 'extended' into extended-fts 2019-04-04 01:03:12 +02:00
John Supplee
67b2c0c0a4 Merge branch 'master' into extended 2019-04-04 01:02:35 +02:00
John Supplee
ac42628a94 Merge branch 'fts' into extended-fts 2019-04-04 00:01:44 +02:00
John Supplee
5d5a68f097 Merge branch 'ubuntu_bionic_solr_fts' of https://github.com/jvolkenant/mailinabox into fts 2019-04-04 00:00:40 +02:00
John Supplee
a6b9808e2e Merge branch 'master' into extended 2019-03-30 14:25:44 +02:00
John Supplee
e64595fc67 New version to tag with annotated tag 2019-03-30 14:23:38 +02:00
John Supplee
8fb4b092be Merge branch 'master' into extended 2019-03-30 14:04:00 +02:00
John Supplee
4c9548edaa change project status to beta 2019-03-30 13:58:54 +02:00
John Supplee
e8cb17c586 Fix problem postgrey configuration 2019-03-24 18:48:53 +02:00
John Supplee
0100801a24 bug fixes for options program 2019-03-09 16:05:17 +02:00
John Supplee
773276f4fe Fix variable name for POSTGREY 2019-03-09 14:53:26 +02:00
John Supplee
761bb054ce Merge branch 'extended' of supplee.net:mailinabox-quota into extended 2019-03-09 14:51:36 +02:00
John Supplee
6b207b6bb4 add python requirements 2019-03-09 14:45:31 +02:00
John Supplee
48f167876c Options dialog updates 2019-03-09 12:29:37 +02:00
John Supplee
99776c1513 Disable Postgrey service if it is not being used 2019-03-09 12:11:41 +02:00
John Supplee
bb96ee8269 Merge branch 'miab-config' into extended 2019-03-09 11:37:25 +02:00
John Supplee
d2d8586f21 Merge branch 'mailgraph' into extended 2019-03-09 11:11:13 +02:00
John Supplee
7a0c5ea910 Merge branch 'miab-config' into extended 2019-03-09 11:05:41 +02:00
John Supplee
075bdc7063 Merge branch 'miab-config' of supplee.net:mailinabox-quota into miab-config 2019-03-09 10:59:47 +02:00
John Supplee
2c50c1876e Merge branch 'master' into extended 2019-03-09 10:51:47 +02:00
John Supplee
a7f62f281b Initial work on dialog screen for options 2019-03-09 10:05:46 +02:00
John Supplee
36101208fe Merge branch 'spf-srs' of supplee.net:mailinabox-quota into miab-config 2019-03-08 22:06:20 +02:00
John Supplee
5167b3a623 Change variable name for Postgrey setup and add others 2019-03-08 17:18:24 +02:00
John Supplee
77143e6b24 Merge branch 'miab' into miab-config 2019-03-08 17:05:29 +02:00
John Supplee
ed68f6d64a fix variable reference in callback 2019-03-03 21:21:34 +02:00
John Supplee
7600e727c1 debugging of image display 2019-03-03 21:15:16 +02:00
John Supplee
e29e3a5cba fix QUERY_STRING to only have the image request 2019-03-03 19:23:58 +02:00
John Supplee
6883a60f5d load images as base64 2019-03-03 19:15:59 +02:00
John Supplee
dce4058705 process images returned from mailgraph 2019-03-03 00:34:41 +02:00
John Supplee
5b5087c9dd fix query params for mailgraph images 2019-03-02 23:33:45 +02:00
John Supplee
8967758413 Only load mailgraph images when the page is viewed 2019-03-02 23:18:44 +02:00
John Supplee
5ffa71999a work on the daemon for mailgraph 2019-03-02 20:47:54 +02:00
John Supplee
d41cdb844c remove nginx config for Mailgraph 2019-03-02 20:31:24 +02:00
John Supplee
fd239db7c1 integrate template 2019-03-02 20:22:08 +02:00
John Supplee
fefb5ebc33 more work on control panel integration 2019-03-02 20:16:39 +02:00
John Supplee
5615031ef8 initial work to integrate with control panel 2019-03-02 17:23:58 +02:00
John Supplee
8180707cc6 add missing semicolon to nginx conf 2019-03-02 15:43:30 +02:00
John Supplee
69a50ed954 fix error in nginx configuration 2019-03-02 15:39:19 +02:00
John Supplee
887e292748 Add changes for mailgraph webserver 2019-03-02 15:30:08 +02:00
John Supplee
68f4d1c426 add SRS support 2019-02-27 17:30:59 +02:00
John Supplee
176e8272a0 Merge branch 'spf' into spf-srs 2019-02-27 17:08:18 +02:00
John Supplee
adf7a31866 Update README and bump the version 2019-02-27 15:02:11 +02:00
John Supplee
4cbf05187c Merge branch 'master' of https://github.com/mail-in-a-box/mailinabox into devel 2019-02-27 12:52:41 +02:00
John Supplee
aa234a504e changes for SPF on incoming email 2019-02-27 12:48:34 +02:00
Joshua Tauberer
dd7a2aa8a6 v0.41 2019-02-26 18:17:50 -05:00
Joshua Tauberer
149552f79b systemctl link should use -f to avoid an error if a system service already exists with that name but points to a different file
https://discourse.mailinabox.email/t/new-error-failed-systemctl-link-conf-mailinabox-service/4626/2
2019-02-26 18:16:26 -05:00
Joshua Tauberer
adddd95e38 add lmtp_destination_recipient_limit=1 to work around spampd bug, see #1523 2019-02-25 13:20:57 -05:00
John Supplee
2b70277a35 Update README instructions for installing and upgrading 2019-02-15 16:08:06 +02:00
Ryan Stubbs
bad38840d8 Fix type on alias edit page (#1520) 2019-02-11 20:14:56 -05:00
John Supplee
7f8336e459 Fix bug with quota input that prevented adding users 2019-02-11 16:10:09 +02:00
John Supplee
514619b44a setup dns authentication for letsencrypt 2019-02-11 16:05:50 +02:00
John Supplee
fc1f211af5 initial work on extended configuration 2019-02-10 23:39:38 +02:00
Yoann Colin
10050aa601 Upgrade to NextCloud 14 (#1504)
* Upgraded Nextcloud from 13.0.6 to 14.0.6.
* Upgraded Contacts from 2.1.5 to 2.1.8.
* Upgraded Calendar from 1.6.1 to 1.6.4.
* Cleanup unsupported version upgrades: Since an upgrade to v0.30 is mandatory before moving upward, I removed the checks for Nextcloud prior version 12.
* Fix the storage root path.
* Add missing indices. Thx @yodax for your feedback.
2019-02-08 21:24:03 -05:00
John Supplee
ae6394c879 Fix instructions for quotas on the users page in control panel 2019-02-06 14:47:30 +02:00
John Supplee
53d1c1e4e9 Release new version v0.40-quota-0.15-alpha 2019-02-06 12:58:05 +02:00
John Supplee
4bed222162 Fix bug where quotas are not being recalculated 2019-02-06 12:53:46 +02:00
John Supplee
44a31733ac update latest version and update README 2019-02-05 13:36:53 +02:00
John Supplee
e44dd93778 mail setup fixes
* check for quota column before trying to add it
* force recalculation of quotas
2019-02-05 13:34:37 +02:00
John Supplee
19f204a9af Do not execute a dovecot reload on every quota update 2019-02-05 13:32:43 +02:00
John Supplee
d1378a5512 Add a thousands separator for message count 2019-02-05 01:48:38 +02:00
John Supplee
a60e1c274a Do not allow decimal points in quota sizes 2019-02-05 01:47:39 +02:00
John Supplee
686a074fab Force recalculation of a user's quota after setting a new value 2019-02-05 01:47:09 +02:00
John Supplee
937ef47c2a Remove default quota support from the TODOs 2019-02-01 21:51:49 +02:00
John Supplee
6964ed238a Show correct reference for updating versions 2019-02-01 19:01:59 +02:00
John Supplee
d51a32bcfe update latest version to v0.40-quota-0.13-alpha 2019-02-01 18:39:47 +02:00
John Supplee
2c6aea24cb Update README to reflect the latest changes 2019-02-01 18:38:00 +02:00
John Supplee
7874683618 Add percentage used and update tools/mail.py to set quotas 2019-02-01 15:36:27 +02:00
John Supplee
fa66585b6b fix typo 2019-02-01 01:35:55 +02:00
John Supplee
51f60c15bb Make the quota field narrower 2019-02-01 01:32:26 +02:00
John Supplee
9139b917cf Finally put the default quota load code in the right place 2019-02-01 01:21:44 +02:00
John Supplee
f64fffe6b8 move jQuery load back to original place and put quota load code in index.html 2019-02-01 01:05:28 +02:00
John Supplee
1a67c94db0 fix problem with jQuery being loaded too late 2019-02-01 00:56:25 +02:00
John Supplee
70c607e256 more default quota work 2019-01-31 23:58:10 +02:00
John Supplee
d1906bd055 Add support for a default quota value and allow setting quota when adding user 2019-01-31 22:57:04 +02:00
John Supplee
6de34b0464 Update README to have section for installation and upgrading 2019-01-31 12:28:32 +02:00
John Supplee
9a9e0116a1 Update to version v0.40-quota-0.12-alpha 2019-01-31 10:23:42 +02:00
John Supplee
f58745c04d Update version to v0.40-quota-0.11-alpha 2019-01-31 09:49:07 +02:00
Jeff Volkenant
1dee849498 Removed extra blankline 2019-01-30 11:33:43 -08:00
John Supplee
863e8895a0 Add the quota column to the users table 2019-01-30 17:14:48 +02:00
John Supplee
e40566967e Change up to date message 2019-01-30 15:35:17 +02:00
John Supplee
ede5e09ad0 make version checks from this repository 2019-01-30 15:31:15 +02:00
John Supplee
98e04bb11f Update README for first alpha release 2019-01-30 12:52:02 +02:00
John Supplee
ebb5896301 Merge remote-tracking branch 'origin/postfix-quota' 2019-01-30 12:38:01 +02:00
John Supplee
2bebd727c2 Fix README 2019-01-30 10:47:07 +02:00
John Supplee
74665d09e8 Merge branch 'master' into postfix-quota 2019-01-30 09:44:49 +02:00
John Supplee
fbf990092c Add progress to README 2019-01-30 09:44:23 +02:00
John Supplee
a23f186c65 initial test config 2019-01-30 08:48:04 +02:00
John Supplee
a8f02c1eb0 Fix problems with users that do not have maildirsize file 2019-01-30 00:01:16 +02:00
Jeff Volkenant
c302606de4 Extended timeout for php/roundcube for text searches that take a long time 2019-01-29 13:46:35 -08:00
John Supplee
8bd9cf38ab Use tabs for indentation 2019-01-29 23:31:56 +02:00
John Supplee
b3f9063ae7 comment out code generating errors
When the `maildirsize` file does not exist it causes the script to fail.
The IOError is not caught by the execpt
2019-01-29 19:41:54 +02:00
John Supplee
f68ef70b94 Calculate and display mailbox sizes in user list 2019-01-29 19:18:48 +02:00
John Supplee
c5c7de8b2b allow decimal points in quotas 2019-01-29 17:42:17 +02:00
John Supplee
3840443159 enable setting quotas from the command line 2019-01-29 13:51:18 +02:00
John Supplee
832d2ca375 Update README to note support for control panel 2019-01-28 23:38:04 +02:00
John Supplee
ccad47937e Add components to user interface for setting quotas 2019-01-28 23:27:03 +02:00
John Supplee
8b95fac8c5 remove unnecessary configuration files 2019-01-28 19:25:26 +02:00
John Supplee
e2d71d4636
Fix typo 2019-01-28 17:30:21 +02:00
John Supplee
b557e69313 display unlimited when quota is 0 2019-01-28 17:24:11 +02:00
John Supplee
dad22f7261 bug fix 2019-01-28 17:17:43 +02:00
John Supplee
587f33b6c0 bug fix for displaying quotas 2019-01-28 16:34:34 +02:00
John Supplee
3cd14bd4b8 Add display of quotas 2019-01-28 16:19:36 +02:00
John Supplee
d443135155 changes to users.sqlite
* add quota column
* modify users_query to return quota_rule
2019-01-28 11:23:49 +02:00
John Supplee
0b68bf8760 put roundcube config change in the right place 2019-01-28 09:38:32 +02:00
John Supplee
907c05299c Add quota support 2019-01-28 09:24:09 +02:00
jvolkenant
c60e3dc842 fail2ban ssh/ssh-ddos and sasl are now sshd and postfix-sasl (fixes #1453, merges #1454)
* fail2ban ssh/ssh-ddos and sasl are now sshd and postfix-sasl

* specified custom datepattern for miab-owncloud.conf
2019-01-18 09:40:51 -05:00
Jeff Volkenant
2fce29d775 Added Solr (Tomcat) to status_checks.py 2019-01-16 20:46:52 -08:00
Jeff Volkenant
2303ac3394 Force kickoff of Solr indexing at install time 2019-01-16 11:32:16 -08:00
Jeff Volkenant
4a23a522e1 added solr.sh to start.sh 2019-01-16 10:29:21 -08:00
Jeff Volkenant
a3d7e0dfae Adapted MIAB Solr install script from https://github.com/jkaberg/ for Ubuntu Bionic 2019-01-16 10:21:19 -08:00
Joshua Tauberer
c7659d9053 v0.40 2019-01-12 08:24:15 -05:00
Joshua Tauberer
cd3fb1b487 fix bootstrap.sh to not confuse the status checks about the latest version 2019-01-09 09:03:43 -05:00
Joshua Tauberer
29e77d25fc merge branch 'ubuntu_bionic' 2019-01-09 08:53:10 -05:00
Joshua Tauberer
e56c55efe8 write changelog summary for the Ubuntu 18.04 upgrade 2019-01-09 08:52:51 -05:00
Joshua Tauberer
8e0d9b9f21 update list of tls ciphers supported 2019-01-09 08:52:51 -05:00
Joshua Tauberer
6e60b47cb5 update bootstrap.sh script to detect the operating system and choose a different version tag depending on whether the box is running Ubuntu 14.04 or Ubuntu 18.04 2019-01-09 08:52:51 -05:00
Joshua Tauberer
a3add03706 Merge branch 'master' into ubuntu_bionic 2019-01-09 07:00:44 -05:00
Joshua Tauberer
7b592b1e99 v0.30 - the last Ubuntu 14.04 release 2019-01-09 06:31:56 -05:00
Joshua Tauberer
a67aa4cfd4 changelog 2019-01-09 06:17:27 -05:00
Dean Perry
31b743b164 Fix some more $DEFAULT_PUBLIC_IP issues (#1494) 2018-12-26 15:39:47 -05:00
jvolkenant
71f1c92b9e bash strict mode fixes (#1482) 2018-12-13 20:30:05 -05:00
EliterScripts
e80a1dd4b7 fix DEFAULT_PUBLIC_IP unbound variable error (#1488)
This will fix this error while installing:
setup/questions.sh: line 95: DEFAULT_PUBLIC_IP: unbound variable
2018-12-13 20:28:21 -05:00
jvolkenant
b7e9a90005 roundcube: upgrade carddav plugin to 3.0.3 & updated migrate.py (#1479)
* roundcube:  upgrade carddav plugin to 3.0.3 & updated migrate.py

* Check for db first and clear sessions to force re-login
2018-12-03 15:33:36 -05:00
Joshua Tauberer
0d4565e71d merge master branch 2018-12-02 18:19:15 -05:00
Joshua Tauberer
703a9376ef fix /etc /usr permissions for Scaleway, see #1438 2018-12-02 18:16:40 -05:00
Joshua Tauberer
b3b798adf2 changelog entries 2018-12-02 18:03:17 -05:00
Joshua Tauberer
bd54b41041 add missing rsyslog to apt install line
see #1438
2018-12-02 18:02:00 -05:00
Joshua Tauberer
a211ad422b add a note on the aliases page that aliases should not be used to forward to outside domains
fixes #1198
2018-12-02 18:02:00 -05:00
Joshua Tauberer
ef28a1defd show the Mail-in-a-Box version in the system status checks even when the new-version check is disabled
fixes #922
2018-12-02 18:02:00 -05:00
Joshua Tauberer
c5c413b447 remove user account mailbox size from the control panel because it takes way too long to compute on very large mailboxes
fixes #531
2018-12-02 18:02:00 -05:00
Joshua Tauberer
d2beb3919b document password character limitation
fixes #407
2018-12-02 18:02:00 -05:00
Achilleas Pipinellis
a7dded8182 Add a logfile entry to the NSD conf file (#1434)
Having a log file can help debugging when something goes wrong and
NSD doesn't fail or MiaB doesn't notify you.

See
https://discourse.mailinabox.email/t/dns-email-domain-becomes-inaccessible-every-few-hours/3770
2018-12-02 18:00:16 -05:00
jeff-h
000363492e Improve greylisting explanation. (#1447)
Hopefully this improves the accuracy of the greylisting description.
2018-12-02 17:58:26 -05:00
jeff-h
5be74dec6e Improve postgrey logging (#1448)
We can't presume the redelivery timeframe of the sending server. However, we do know the blacklist timeframe within which we will reject a redelivery.
2018-12-02 17:57:37 -05:00
Joshua Tauberer
9ddca42c91 add 'nameserver' to resolv.conf, fixes #1450 2018-11-30 10:46:54 -05:00
Joshua Tauberer
ff6d8fc672 remove the ppa directory since we're no longer supporting a PPA for Ubuntu 18.04 2018-11-30 10:46:54 -05:00
Joshua Tauberer
870b82637a fix some wrong variable names, fixes #1353 2018-11-30 10:46:54 -05:00
Joshua Tauberer
dc6458623d add a note on the aliases page that aliases should not be used to forward to outside domains
fixes #1198
2018-11-30 10:46:54 -05:00
Joshua Tauberer
60f9c9e3b7 show the Mail-in-a-Box version in the system status checks even when the new-version check is disabled
fixes #922
2018-11-30 10:46:54 -05:00
Joshua Tauberer
e5e0c64395 turn on bash strict mode to better catch setup errors
fixes #893
2018-11-30 10:46:54 -05:00
Joshua Tauberer
aa52f52d02 disable SMTP AUTH on port 25 to stop it accidentally being used for submission
fixes #830
2018-11-30 10:46:54 -05:00
Joshua Tauberer
b05b06c74a remove user account mailbox size from the control panel because it takes way too long to compute on very large mailboxes
fixes #531
2018-11-30 10:46:54 -05:00
Joshua Tauberer
7f8f4518e3 document password character limitation
fixes #407
2018-11-30 10:46:54 -05:00
Joshua Tauberer
86e2cfb6c8 remove old duplicity migration code from 2015, see 42322455 2018-11-30 10:46:54 -05:00
Holger Just
0335595e7e Update Roundcube to version 1.3.8 (#1475)
https://github.com/roundcube/roundcubemail/releases/tag/1.3.8
2018-11-25 10:40:21 -05:00
jvolkenant
8d5670068a fixes nginx warning about duplicate ssl configuration (#1460) 2018-10-25 15:18:21 -04:00
jvolkenant
c9b3d88108 Fixes #1437 - package python-virtualenv is now called just virtualenv (#1452) 2018-10-24 17:20:48 -04:00
Joshua Tauberer
16f38042ec v0.29 released, closes #1440 2018-10-24 16:12:25 -04:00
Joshua Tauberer
2f494e9a1c CHANGELOG fixes/updates 2018-10-24 16:09:59 -04:00
Joshua Tauberer
f739662392 duplicity started creating signature files with invalid filenames, fixes #1431 2018-10-13 16:16:30 -04:00
Michael Kroes
6eb9055275 Upgrade NextCloud to 13.06 (#1436) 2018-10-09 07:09:54 -04:00
Joshua Tauberer
3dbd6c994a update bind9 configuration 2018-10-03 14:28:43 -04:00
Joshua Tauberer
bc4bdca752 update reference to Ubuntu 14.04 to 18.04 in README.md and security.md and drop mentions of our custom packages that we no longer maintain 2018-10-03 13:00:15 -04:00
Joshua Tauberer
bbfa01f33a update to PHP 7.2
* drop the ondrej/php PPA since PHP 7.x is available directly from Ubuntu 18.04
* intall PHP 7.2 which is just the "php" package in Ubuntu 18.04
* some package names changed, some unnecessary packages are no longer provided
* update paths
2018-10-03 13:00:15 -04:00
Joshua Tauberer
f6a641ad23 remove some cleanup steps that are no longer needed since we aren't supporting upgrades of existing machines and, even if we did, we aren't supporting upgrades from really old versions of Mail-in-a-Box 2018-10-03 13:00:15 -04:00
Joshua Tauberer
51972fd129 fix some comments 2018-10-03 13:00:15 -04:00
Joshua Tauberer
bb43a2127c turn the x64/i686 architecture check into a warning since I'm not sure if we have any architecture requirements anymore, beyond what Ubuntu supports 2018-10-03 13:00:15 -04:00
Christopher A. DeFlumeri
d96613b8fe minimal changeset to get things working on 18.04
@joshdata squashed pull request #1398, removed some comments, and added these notes:

* The old init.d script for the management daemon is replaced with a systemd service.
* A systemd service configuration is added to configure permissions for munin on startup.
* nginx SSL settings are updated because nginx's options and defaults have changed, and we now enable http2.
* Automatic SSHFP record generation is updated to know that 22 is the default SSH daemon port, since it is no longer explicit in sshd_config.
* The dovecot-lucene package is dropped because the Mail-in-a-Box PPA where we built the package has not been updated for Ubuntu 18.04.
* The stock postgrey package is installed instead of the one from our PPA (which we no longer support), which loses the automatic whitelisting of DNSWL.org-whitelisted senders.
* Drop memcached and the status check for memcached, which we used to use with ownCloud long ago but are no longer installing.
* Other minor changes.
2018-10-03 13:00:06 -04:00
Joshua Tauberer
504a9b0abc certbot uses a new directory path for API v02 accounts and we should check that before creating a new account or else we'll try to create a new account on each setup run (which certbot just fails on) 2018-09-03 13:07:24 -04:00
Joshua Tauberer
842fbb3d72 auto-agree to Let's Encrypt's terms of service during setup
fixes #1409

This reverts commit 82844ca651 ("make certbot auto-agree to TOS if NONINTERACTIVE=1 env var is set (#1399)") and instead *always* auto-agree. If we don't auto-agree, certbot asks the user interactively, but our "curl | bash" setup line does not permit interactive prompts, so certbot failed to register and all certificate things were broken until the command was re-run interactively.
2018-09-03 13:06:34 -04:00
Joshua Tauberer
a5d5a073c7 update Z-Push to 2.4.4
Starting with 2.4, Z-Push no longer provides tarballs on their download server. The only options are getting the code from their git repository or using one of their distribution packages. Their Ubuntu 18.04 packaes don't seem to actually work in Ubuntu 18.04, so thinking ahead that's currently a bad choice. In 78d1c9be6e we switched from doing a git clone to using wget on their downloads server because of a problem with something related to stash.z-hub.io's SSL certificate. But wget also seems to work on their source code repository, so we can use that.
2018-09-02 11:29:44 -04:00
Joshua Tauberer
d4b122ee94 update to Nextcloud 13.0.5 2018-08-24 11:11:52 -04:00
Joshua Tauberer
052a1f3b26 update to Roundcube 1.3.7 2018-08-24 10:47:22 -04:00
Joshua Tauberer
180b054dbc small code cleanup testing if the utf8 locale is installed 2018-08-24 09:49:08 -04:00
Joshua Tauberer
cb162da5fe
Merge pull request #1412 from hlxnd/pr
Use ISO 8601 on backups table dates, fixes #1397
2018-08-05 15:16:05 -04:00
hlxnd
de9c556ad7 Add missing PHP end tag 2018-08-05 15:27:35 +02:00
hlxnd
f420294819 Use ISO 8601 on backups table dates. 2018-08-05 15:26:45 +02:00
Joshua Tauberer
738e0a6e17 v0.28 released, closes #1405 2018-07-30 11:14:38 -04:00
Pascal Garber
e0d46d1eb5 Use Nextcloud’s occ command to unlock the admin (#1406) 2018-07-25 15:37:09 -04:00
Joshua Tauberer
7f37abca05 add php7.0-curl to webmail.sh
see 7ee91f6ae6524d5f4f25acb4002220c4942633f5
see #1268
closes #1259
2018-07-22 09:19:36 -04:00
Joshua Tauberer
2f467556bd new ssl cert provisioning broke if a domain doesnt yet have a cert, fixes #1392 2018-07-19 11:40:49 -04:00
Joshua Tauberer
15583ec10d updated CHANGELOG 2018-07-19 11:27:37 -04:00
Nils Norman Haukås
78d1c9be6e failing z-push installation: replace git clone with wget_verify
git clone (which uses curl) underneath was failing. Curiously, the same
git clone command would work on my macos host machine.

From the screenshot it looks like curl was somehow not able to negotiate
the connection. Might have been a missing CA certificate for Comodo, but
I was not able to determine if that was the issue.

fixes #1393
closes #1387
closes #1400
2018-07-19 11:25:57 -04:00
dev9
b0b5d8e792 Fix .mobileconfig so CalDAV calendar works on Mac OS X (#1402)
The previous CalDAVPrincipalURL "/cloud/remote.php/caldav/calendars/" causes an error in OS X.

See: https://discourse.mailinabox.email/t/caldav-with-macos-10-12-2-does-not-work/1649 and other similar issues.

The correct CalDAVPrincipalURL: https://discourse.mailinabox.email/t/caldav-with-macos-10-12-2-does-not-work/1649 but it turns out you can just leave the key/value out completely and OS X/iOS are able to auto discover the correct URL.
2018-07-19 11:17:38 -04:00
Nils
82844ca651 make certbot auto-agree to TOS if NONINTERACTIVE=1 env var is set (#1399) 2018-07-15 11:24:15 -04:00
Joshua Tauberer
2a72c800f6 replace free_tls_certificates with certbot 2018-06-29 16:46:21 -04:00
Joshua Tauberer
8be23d5ef6 ssl_certificates: reuse query_dns function in status_checks and simplify calls by calling normalize_ip within query_dns 2018-06-29 16:46:21 -04:00
Joshua Tauberer
f9a0e39cc9 cryptography is now distributed as a wheel and no longer needs system development packages to be installed or pip/setuptools workarounds 2018-06-29 16:46:21 -04:00
Joshua Tauberer
0c0a079354 v0.27 2018-06-14 07:49:20 -04:00
Joshua Tauberer
42e86610ba changelog entry 2018-05-12 09:43:41 -04:00
yeah
7c62f4b8e9 Update Roundcube to 1.3.6 (#1376) 2018-04-17 11:54:24 -04:00
Joshua Tauberer
1eba7b0616 send the mail_log.py report to the box admin every Monday 2018-02-25 11:55:06 -05:00
Joshua Tauberer
9c7820f422 mail_log.py: include sent mail in the logins report in a new smtp column 2018-02-24 09:24:15 -05:00
Joshua Tauberer
87ec4e9f82 mail_log.py: refactor the dovecot login collector 2018-02-24 09:24:14 -05:00
Joshua Tauberer
08becf7fa3 the hidden feature for proxying web requests now sets X-Forwarded-For 2018-02-24 09:24:14 -05:00
Joshua Tauberer
5eb4a53de1 remove old tools/update-subresource-integrity.py script which isn't used now that we download all admin page remote assets during setup 2018-02-24 09:24:14 -05:00
Joshua Tauberer
598ade3f7a changelog entry 2018-02-24 09:24:09 -05:00
xetorixik
8f399df5bb Update Roundcube to 1.3.4 and Z-push to 2.3.9 (#1354) 2018-02-21 08:22:57 -05:00
Joshua Tauberer
ae73dc5d30 v0.26c 2018-02-13 10:46:02 -05:00
Joshua Tauberer
c409b2efd0 CHANGELOG entries 2018-02-13 10:44:07 -05:00
Joshua Tauberer
6961840c0e wrap wget in hide_output so that wget errors are shown
Our wget_verify function uses wget to download a file and then check
the file's hash. If wget fails, i.e. because of a 404 or other HTTP
or network error, we exited setup without displaying any output because
normally there are no errors and -q keeps the setup output clean.

Wrapping wget with our hide_output function, and dropping -q, captures
wget's output and shows it and exits setup just if wget fails.

see #1297
2018-02-13 10:38:10 -05:00
yeah
6162a9637c Add some development instructions to CONTRIBUTING.md (#1348) 2018-02-05 08:41:19 -05:00
Jan Schulz-Hofen
47c968e71b Upgrade Nextcloud from 12.0.3 to 12.0.5 2018-02-04 10:13:30 -05:00
Jan Schulz-Hofen
ed3e2aa712 Use new .tar.bz2 source files for ownCloud and fix upgrade paths 2018-02-04 10:13:30 -05:00
NatCC
fe597da7aa Update users.html (#1345)
Passwords must be eight characters long; when passwords are changed via the users page the dialog states that passwords need to be at least four characters but only eight or more are acceptable.
2018-02-03 17:49:11 -05:00
Joshua Tauberer
61e9888a85 Cdon't try to generate a CSR in the control panel until both the domain and country are selected
Fixes #1338.

See 0e9680fda63c33ace3f34ca7126617fb0efe8ffc, a52c56e571.
2018-01-28 09:08:24 -05:00
Joshua Tauberer
35fed8606e only spawn one process for the management daemon
In 0088fb4553 I changed the management daemon's startup
script from a symlink to a Python script to a bash script that activated the new virtualenv
and then launched Python. As a result, the init.d script that starts the daemon would
write the pid of bash to the pidfile, and when trying to kill it, it would kill bash but
not the Python process.

Using exec to start Python fixes this problem by making the Python process have the pid
that the init.d script knows about.

fixes #1339
2018-01-28 09:08:19 -05:00
Joshua Tauberer
ef6f121491 when generating a CSR in the control panel, don't set empty attributes
Same as in a52c56e571.

Fixes #1338.
2018-01-28 09:07:54 -05:00
Joshua Tauberer
ec3aab0eaa v0.26b 2018-01-25 09:27:17 -05:00
Joshua Tauberer
8c69b9e261 update CHANGELOG 2018-01-25 09:23:04 -05:00
Joshua Tauberer
e7150e3bc6 pin acme to v0.20, which is the last version compatible with free_tls_certificates
free_tls_certificates uses acme.jose, which in acme v0.21 was moved to a new Python package.

See #1328
2018-01-20 11:23:45 -05:00
Joshua Tauberer
8d6d84d87f run mailconfig.py's email address validator outside of the virtualenv during questions.sh
We don't have the virtualenv this early in setup.

Broken by 0088fb4553.

Fixes #1326.

See https://discourse.mailinabox.email/t/that-is-not-a-valid-email-error-during-mailinabox-installation/2793.
2018-01-20 10:59:37 -05:00
barrybingo
a6a1cc7ae0 Reduce munin-node log level to warning (#1330) 2018-01-19 12:00:44 -05:00
Joshua Tauberer
b5c0736d27 release v0.26 2018-01-18 17:10:23 -05:00
Joshua Tauberer
8ee7de6ff3 no need to do a second apt-get update after 'installing' the PHP7 PPA if the PPA was already installed 2018-01-15 13:28:18 -05:00
Joshua Tauberer
0088fb4553 install Python 3 packages in a virtualenv
The cryptography package has created all sorts of installation trouble over the last few years, probably because of mismatches between OS-installed packages and pip-installed packages. Using a virtualenv for all Python packages used by the management daemon should make sure everything is consistent.

See #1298, see #1264.
2018-01-15 13:27:04 -05:00
Joshua Tauberer
b2d103145f remove php5 packages from webmail.sh
The PHP5 packages have a dependency on (apache2 or php5-cgi or php5-fpm), and since removing php5-fpm apache2 started getting installed during setup, which caused a conflict with nginx of course.

These packages don't seem to be needed by Roundcube or Nextcloud --- Roundcube includes the ones it needs.

see #1264, #1298
2018-01-15 11:29:12 -05:00
Joshua Tauberer
fc9e279cec partial revert of 441bd350, accidentally uncommented something 2018-01-15 10:33:05 -05:00
yeah
257983d559 Fix typo in CHANGELOG.md (#1312) 2017-12-25 17:46:31 -05:00
Joshua Tauberer
e924459140 revert f25801e/#1233 - use Mozilla intermediate ciphers for IMAP/POP not modern ciphers
fixes #1300
2017-12-24 14:41:41 -05:00
Joshua Tauberer
441bd35053 update CHANGELOG 2017-12-23 18:01:41 -05:00
Michael Kroes
a0e603a3c6 Change z-push to use the git repository instead of the tar ball (#1305) 2017-12-23 17:51:18 -05:00
sam-banks
88604074d6 Bugfix for free command (#1278)
A quick fix - there's no "o" option for free.
2017-12-18 08:21:28 -05:00
yeah
d43111eb48 Add X-Spam-Score header to checked mail (#1292)
To enable users to do custom spam filtering based on score, it's helpful to render the actual spam score as a float in a separate header rather than as part of X-Spam-Status where it only appears in a comma separated list.
2017-12-18 08:17:47 -05:00
Jim Bailey
6729588d8c Changed temp_dir to /var/temp/roundcube to avoid loss on reboot. (#1302) 2017-12-18 08:12:45 -05:00
Joshua Tauberer
5f14eca67f merge v0.25 security release 2017-11-15 11:27:30 -05:00
Joshua Tauberer
8944cd7980 v0.25 2017-11-15 11:27:00 -05:00
yeah
2bbbc9dfa3 Update Roundcube to protect against CVE-2017-16651
See https://roundcube.net/news/2017/11/08/security-updates-1.3.3-1.2.7-and-1.1.10.

merges #1287
2017-11-15 11:14:21 -05:00
John Olten
544f155948 Add support for DNS wildcard [merges #1281] 2017-11-15 11:10:59 -05:00
Joshua Tauberer
f080eabb3a run apt-get autoremove after updating system packages
Old kernels can build up and some packages may not be needed anymore.

See https://discourse.mailinabox.email/t/storage-space-decreasing/2525/5.
2017-11-15 11:05:43 -05:00
Jānis (Yannis)
7bf377eed1 use RSASHA256 for .lv domains DNSSEC (#1277) 2017-10-31 18:01:47 -04:00
Nicolas North
cd554cf480 document the "local" alias pointing to this box in Custom DNS (#1261) 2017-10-20 17:20:21 -04:00
Michael Kroes
e5448405ae add php7.0-mbstring to webmail.sh (#1268) 2017-10-15 07:53:01 -04:00
Tristan Hill
a7eff8fb35 turn off apt verbose in unattended upgrades (#1255) 2017-10-06 08:16:40 -04:00
Fabian Bucher
341aa8695a update F-Droid DAVdroid link (#1253)
the information about the invalid link comes from here -> https://discourse.mailinabox.email/t/admin-sync-guide-contacts-and-calendar-davdroid-3-69-free-here/2528
2017-10-04 17:47:15 -04:00
Joshua Tauberer
5efdd72f41 update TLS test to record changes in the ciphers we offer on the open ports 2017-10-03 12:01:10 -04:00
Joshua Tauberer
f25801e88d Merge #1233 - Limit Dovecot ciphers to the Mozilla modern set 2017-10-03 11:55:16 -04:00
Joshua Tauberer
cc7be13098 update nginx cipher list to Mozilla's current intermediate ciphers and update HSTS header to be six months
* The Mozilla recommendations must have been updated in the last few years.
* The HSTS header must have >=6 months to get an A+ at ssllabs.com/ssltest.
2017-10-03 11:47:32 -04:00
Joshua Tauberer
2556e3fbc2 HSTS header does not belong here, will result in multiple headers 2017-10-03 11:38:15 -04:00
Joshua Tauberer
00898b2ff5 v0.24 2017-10-03 10:49:04 -04:00
Joshua Tauberer
35b8a149d8 fix dns regex: underscores are allowed in domain names even though they are not allowed in hostnames 2017-09-22 12:31:49 -04:00
Joshua Tauberer
d0423afd18 Nextcloud install shouldn't fail if php-fpm isn't already running 2017-09-22 11:10:48 -04:00
Joshua Tauberer
edf42df835 update Roundcube (1.3.1), persistent login plugin, Z-Push (2.3.8), and Nextcloud (12.0.3) 2017-09-22 11:10:40 -04:00
Joshua Tauberer
734745a4a6 Nextcloud 12.0.2, fix Nextcloud 12 upgrades seeing the wrong version
Nextcloud 12 adds a new OC_VersionCanBeUpgradedFrom field to /usr/local/lib/owncloud/version.php which lists
prior NC/OC version numbers, which confuses our check for what the installed version is. Make our regex more strict.

merges #1238
2017-09-01 07:58:07 -04:00
dofl
dbebaba8b9 switch PHP's process manager to on demand
merges #1216
2017-08-30 13:39:25 -04:00
Joshua Tauberer
cb765dfe2a changelog entries 2017-08-30 13:11:58 -04:00
Lloyd Smart
81258e2189 Implement upstream issue #1228 for stronger dh parameters in Dovecot. (#1232) 2017-08-30 13:04:22 -04:00
Lloyd Smart
4dd4b4232a Limited ciphers to the Mozilla modern set from https://mozilla.github.io/server-side-tls/ssl-config-generator/ as requested in issue #1228. 2017-08-29 15:02:58 +01:00
Marius Blüm
48ff664ee9 Remove the ? from "Log out" (#1231)
Signed-off-by: Marius Blüm <marius@lineone.io>
2017-08-23 19:46:45 -04:00
Michael Kroes
a52c56e571 only set the CN field when generating initial CSR to prevent issues with the php7 ppa version of openssl (#1223)
OpenSSL 1.1.0f now validates the other subject fields and rejects the empty string (for the country?) because it isn't two characters.
2017-07-30 08:11:39 -04:00
Jon Hermansen
6ace97e482 update PPA build URL for postgrey 1.35. Fixes #1211 (#1212) 2017-07-21 15:13:57 -04:00
Git Repository
19a928e4ec [Issue #1159] Remove any +tag name in email alias before checking privileges (#1181)
* [Issue #1159] Remove any +tag name in email alias before checking privileges

* Move priprivileged email check after the conversion to unicode so only IDNA serves as input
2017-07-21 11:10:16 -04:00
Michael Kroes
78f2fe213e Secondary name server could not be set (#1209) 2017-07-21 08:20:37 -04:00
Michael Kroes
a16855ecf0 Backup script should now stop php7.0-fpm instead of php5-fpm (#1206) 2017-07-17 09:45:40 -04:00
yodax
d773140502 Update to Nextcloud 12 using PHP7
* Install PHP7 via a PPA, enable unattended upgrades for the PPA, and switch all of our PHP configuration to the PHP7 install.
* Keep installing PHP5 for ownCloud/Nextcloud packages because we need it to possibly run transitional updates to ownCloud/Nextcloud versions less than 12. But replace PHP5 packages with PHP7 packages elsewhere.
* Update to Nextcloud 12 which requires PHP7, with a transitional upgrade to Nextcloud 11.0.3.
* Disable TLS cert validation by Roundcube when connecting to localhost IMAP and SMTP. Validation became the default in PHP7 but we don't necessarily have a (non-self-)signed certificate and it definitely isn't valid for the IP address 127.0.0.1.

Merges #1140
2017-07-14 06:48:22 -04:00
Michael Kroes
2c324d0bc9 web_domains should also normalize ipv6 addresses (#1201) 2017-07-13 07:16:12 -04:00
Joshua Tauberer
2bd6cc4d6b update to Z-Push 2.3.7 2017-07-10 18:01:21 -04:00
Joshua Tauberer
b11157e0b6 updated to Roundcube 1.3, but unfortunately dropping the vacation plugin
Switched to the -complete download which has vendored assets. See https://github.com/mail-in-a-box/mailinabox/pull/1140.
2017-07-10 17:31:59 -04:00
François Deppierraz
46ba62b7b1 Add support for NS records in custom domains (#1177) 2017-06-11 07:56:30 -04:00
Joshua Tauberer
4c36d6e6c9 release v0.23a 2017-05-31 07:42:18 -04:00
Michael Kroes
e49c99890b fetch whole bootstrap - fixes missing icons in admin (#1185) 2017-05-31 07:36:17 -04:00
Joshua Tauberer
a13fd90347 v0.23 2017-05-30 06:50:42 -04:00
Git Repository
18f1689f45 changed the location we store the web-assets for the admin pages to /usr/local/mailinabox (#1179) 2017-05-23 19:22:53 -04:00
Git Repository
8234a5a9f4 download jQuery and Bootstrap during setup and serve locally so that we don't rely on a CDN which is blocked in some parts of the world (#1167) (#1171) 2017-05-08 07:25:16 -04:00
Michael Kroes
1d9f9ea617 Fix two typos in setup/owncloud.sh regarding the setting of the hostname (#1172) 2017-05-08 07:23:59 -04:00
Michael Kroes
fbb38c3881 Add changelog for custom dns CAA records (#1173) 2017-05-08 07:23:12 -04:00
Git Repository
2caddb41eb #1161 Move the config line for mail_domain to always reset the PRIMARY_HOST (#1163) 2017-05-06 08:18:50 -04:00
Michael Kroes
d2b7204319 Add support for adding a custom "CAA" DNS record (#1155) 2017-04-30 08:58:00 -04:00
Michael Kroes
68ebca8a15 Update Z-Push to 2.3.6 (#1166) 2017-04-30 07:24:36 -04:00
Joshua Tauberer
9c9dcdbf0a update README to link to http://z-push.org/ now that we are on the main line 2017-04-24 17:34:53 -04:00
Joshua Tauberer
0c4c2e51bb bump to Nextcloud 10.0.5 2017-04-24 17:31:54 -04:00
Joshua Tauberer
828512b95a changelog entries 2017-04-17 07:51:01 -04:00
Joshua Tauberer
add985ce5d letencrypt now supports idna, remove the check/block 2017-04-17 07:45:08 -04:00
Michael Kroes
416dbebf45 update z-push to 2.3.5 on the upstream repository z-push.org (#1153) 2017-04-17 07:42:44 -04:00
Git Repository
2a046a22f4 changed roundcube theme to 'larry' (#1138)
Updated the setup file to use roundcube's 'larry' theme as the default.
2017-04-17 07:29:50 -04:00
yodax
b66f12dd4c Fix rsync backup. The path was not append properly 2017-04-17 07:25:47 -04:00
yodax
6e04eb490f Add check to prevent division by zero during backup status 2017-04-17 07:25:47 -04:00
Michael Kroes
cd39c2b53f Merge pull request #1151 from phol/master
Corrected typo in setup/dns.sh
2017-04-10 18:52:38 +02:00
Pieter
5da168466d Corrected typo in setup/dns.sh 2017-04-10 18:37:09 +02:00
Joas Schilling
a5f39784dd remove nginx error pages for nextcloud (#1141)
They are known to cause troubles, for more information see
https://github.com/nextcloud/server/issues/3847
2017-04-04 07:42:50 -04:00
Michael Kroes
a072730fb8 Wrap normalize_ip in try..except (#1139)
closes #1134
2017-04-03 16:53:53 -04:00
Joshua Tauberer
00c61dbcdd changelog entry for migration to Nextcloud 2017-04-02 07:53:56 -04:00
Joshua Tauberer
10bf40250b merge #1121 - migration from ownCloud to Nextcloud
branch 'nextcloud' of https://github.com/yeah/mailinabox
2017-04-02 07:47:31 -04:00
Joshua Tauberer
453091f1fb v0.22 released 2017-04-02 07:34:14 -04:00
Jan Schulz-Hofen
48e0f39179 Rename ownCloud to Nextcloud in safe places
e.g. code comments and user-facing prompts/outputs which can be safely changed without risking to break anything
2017-04-02 11:19:21 +02:00
Jan Schulz-Hofen
bb641cdfba Move from ownCloud to Nextcloud 2017-03-28 11:16:04 +07:00
Joshua Tauberer
255a65ac98 suppress rmcarddav's php version check
Since it says "RCMCardDAV requires at least PHP 5.6.18. Older versions might work", let's hope for the best.

Also hiding its preferences panel in settings since if it doesn't work, we don't want folks using it for anything but connecting to ownCloud contacts.
2017-03-27 08:18:05 -04:00
yeah
c7badb80d1 Set default user password length to 8 in non-interactive setups (#1123)
To comply with #1098 and avoid failed setups while testing with Vagrant
2017-03-26 13:23:34 -04:00
Joshua Tauberer
653cb7ce10 roundcube 1.2.4, persistent login plugin 2017-03-26 09:50:00 -04:00
Joshua Tauberer
d7d8964afc changlog entries 2017-03-26 09:31:35 -04:00
yeah
6c3696a54a Upgrade ownCloud to 9.1.4 to address security vulnerabilities, refs #1111 (#1120)
* Move variable assignment up and do not use call arguments directly

* Upgrade ownCloud to latest patch release 9.1.4

also move owncloud hash to its own variable
2017-03-26 09:20:27 -04:00
Rinze de Laat
9c9cae2096 Added an alternative mail log scanning script for use from the command line (and monitoring, at a later stage)
merges #970
2017-03-26 09:13:35 -04:00
Théo Segonds
423f1907d0 Fix zpush compatibility list link (#1076) 2017-03-26 09:09:00 -04:00
Sean Watson
86621392f6 support SSHFP records for custom domains (#1114) 2017-03-09 09:05:52 -05:00
Sean Watson
368b9c50d0 add DSA and ED25519 SSHFP records if those keys are present (#1078) 2017-03-01 08:02:41 -05:00
Jan Schulz-Hofen
3830facf78 set dovecot vsz_limit to 1/3 of available memory (#1096)
The `default_vsz_limit` is the maximum amount of virtual memory that can be allocated. It should be set *reasonably high* to avoid allocation issues with larger mailboxes. We're setting it to 1/3 of the total available memory (physical mem + swap) to be sure.

See here for discussion:
- https://www.dovecot.org/list/dovecot/2012-August/137569.html
- https://www.dovecot.org/list/dovecot/2011-December/132455.html
2017-03-01 07:59:48 -05:00
Manuel
d4baac2363 at the end of setup show SHA256 tls cert hash instead of SHA1 hash (#1108) 2017-03-01 07:57:03 -05:00
NatCC
f88c907a29 Update jails.conf - SSH fail2ban jail (#1105)
SSH fail2ban jail is not enabled by default and so the jail does not load.
2017-02-21 09:32:28 -05:00
Ian Beringer
89222d519a Fix date delta display for deltas greater than 1 year (#1099) 2017-02-15 18:24:32 -05:00
Dominik Murzynowski
36bef2ee16 Change password min-length to 8 characters (#1098) 2017-02-14 14:24:59 -05:00
Norman S
f6b20a810f Enforce pip to use python 2.7 for boto (#1093) 2017-02-10 09:44:40 -05:00
Norman S
f2ff14100e Change password min-length to four characters (#1094)
in order to correlate with the management interface.
2017-02-10 09:43:11 -05:00
Joshua Tauberer
2c86fa3755 merge v0.21c hot fix release 2017-02-01 11:26:32 -05:00
Joshua Tauberer
3c05fc94ff v0.21c 2017-02-01 11:01:11 -05:00
Joshua Tauberer
2e00530944 upgrade acme package 2017-02-01 11:01:11 -05:00
Joshua Tauberer
32d6728dc9 fix pip breaking due to setuptools/pip/cryptography problem
pip<6.1 + setuptools>=34 have a problem with packages that
try to update setuptools during installation, like cryptography.
See https://github.com/pypa/pip/issues/4253. The Ubuntu 14.04
package versions are pip 1.5.4 and setuptools 3.3. When we
install cryptography under those versions, it tries to update
setuptools to version 34, which became available about 10 days
ago, and then pip gets permanently broken with errors like
"ImportError: No module named 'packaging'".

The easiest work-around on systems that aren't already broken is
to upgrade pip and setuptools individually before we install any
package that tries to update setuptools.

Also try to detect a broken system and forcibly remove setuptools
first before trying to install/upgrade pip.

fixes #1080, fixes #1081, fixes #1086
see #1083
see https://discourse.mailinabox.email/t/error-with-pip-and-python/1880
see https://discourse.mailinabox.email/t/error-installing-mib/1875
2017-02-01 10:29:28 -05:00
wsteitz
a3c71fe14f move unzip installation from owncload to system setup (#1077) 2017-01-22 10:37:54 -05:00
Joshua Tauberer
a24977a96e normalize_ip for ipv6 still not correct, was broken if box has no IPv6 address 2017-01-18 07:51:59 -05:00
Joshua Tauberer
e694f57673 changelog entries 2017-01-15 11:23:59 -05:00
Joshua Tauberer
cd59de6314 update roundcube to 1.2.3 2017-01-15 11:17:17 -05:00
Joshua Tauberer
a081d04082 move the custom exclusive process code from utils.py into a new python package named exclusiveprocess 2017-01-15 11:02:23 -05:00
Bill Cromie
09577816f8 adds optional vagrant-cachier if you have the plugin installed (#1028) 2017-01-15 10:47:36 -05:00
Bill Cromie
2647febbf5 cardav plugin for roundcube (#1029) 2017-01-15 10:46:33 -05:00
guyzmo
bd0635728c added editorconfig setup (#1037) 2017-01-15 10:44:13 -05:00
Jonathan Chun
584cfe42c4 compare IPv6 addresses correctly with normalization (#1052) 2017-01-15 10:41:12 -05:00
Michael Kroes
41601a592f Improve error handling when doing update checks (#1065)
* Added an error message to handle exceptions when the setup script is trying to determine the latest Miab version
2017-01-15 10:35:33 -05:00
Bill Cromie
18c253eeda adding a fully qualified domain name for the hostname and ignoring the .vagrant dir (#1027) 2016-12-20 16:32:06 -05:00
guyzmo
34d58fb720 Fix/rsync issues (#1036)
* Fixed issue with relative path for rsync relative names

Actually using the parsed URL `path` part, instead of doing a lousy split().
Renamed the `p` variable into something more sensible (`target`).

Fixes: #1019

* Added more verbose error messages upon rsync failures

fixes #1033

* Added command to test file listing
2016-12-17 09:29:48 -05:00
Joshua Tauberer
99d0afd650 secondary nameserver check fails if domain has custom DNS (round-robin) multiple A records
fixes #834
2016-12-07 07:02:52 -05:00
Joshua Tauberer
cd717ec94e nightly TLS certificate provisioning should omit warnings about domains it cant provision for 2016-12-07 07:02:52 -05:00
Joshua Tauberer
0b7f477b96 v0.21b
-----BEGIN PGP SIGNATURE-----
 Version: GnuPG v1
 
 iQEcBAABAgAGBQJYRevlAAoJELkgQfTBC92BqS8IALs7LHQAujNvFYnAN5OqJV0y
 3RW5rB1Y0EROWH3YDZBEk6tmZ4+rtF3U6q2aHxiJ0BwsJW9KlO8rlqVUFRdEW+US
 BZVhBhBrZLBxmnkS8JF5rCA+xcga1iIL7uSALAikpJK3PxtmMgjEvt9jc2e1VXDC
 isHMSCHCLzBVx29RFfWf+3LmSkmg5UWMCNjxKLLJcalV/hIRg+zFT7zdJdaKnI7Z
 cj56o+j75sLm+4ftM+n05Q9iTw5yGg2Qqx7bJCDpWEGJClkeuy9n//X5a63XlWbk
 IIj3YMAUZj+5wZ7zneN6A9MUMp24OLOztQRWzn7XJR18gSFkdg7RZgWA0eERC1g=
 =tkCm
 -----END PGP SIGNATURE-----

merge hot-fix release v0.21b
2016-12-05 17:36:32 -05:00
Joshua Tauberer
ab2367e98a v0.21b 2016-12-05 17:36:11 -05:00
Corey Hinshaw
384c3b5e3d Change ownership of roundcube DB after running migrations (#1024)
* Fix #1023 by changing ownership of roundcube DB after running migrations

* Set mode of roundcube sqlite database during setup
2016-12-05 17:32:31 -05:00
Corey Hinshaw
d91368c478 Change ownership of roundcube DB after running migrations (#1024)
* Fix #1023 by changing ownership of roundcube DB after running migrations

* Set mode of roundcube sqlite database during setup
2016-12-05 17:31:20 -05:00
wsteitz
61105b1ec3 remove all references to justtesting.email (#1003) (#1005) 2016-11-30 12:55:18 -05:00
Leo Koppelkamm
b6f90e10c1 Allow larger messages to be checked by SpamAssassin (#1006)
Additionally, add the spam report headers to all emails, in order to make it easier to debug false negatives.
2016-11-30 12:55:03 -05:00
Michael Kroes
3af5e55035 Upgrade to ownCloud 9.1.2 (#1010)
* Update owncloud to 9.1.2

* Upgrade to ownCloud 9.1.2 from 9.1.1 would fail because the guid of 9.1.1 matched with the regex for the version of 8.x
2016-11-30 12:54:27 -05:00
Joshua Tauberer
e03b071e8b missed changelog header 2016-11-30 12:50:38 -05:00
Joshua Tauberer
df93d82d0f v0.21 released 2016-11-30 12:42:24 -05:00
Christian Koptein
59913a5e4c Added Hacker-News Reference Nov 2016 (#1014) 2016-11-28 07:24:57 -05:00
Michael Kroes
c3605f6211 Check if update-manager release-upgrades configuration file is present before editing (#996) 2016-11-13 17:36:33 -05:00
Joshua Tauberer
96b3a29800 rsync backup broke other things 2016-11-12 09:59:06 -05:00
Joshua Tauberer
abb6a1a070 changelog entries 2016-11-12 09:34:52 -05:00
guyzmo
041b5f883f Support for rsync+ssh backup target (#678)
* Added support for backup to a remote server using rsync

* updated web interface to get data from user
* added way to list files from server

It’s not using the “username” field of the yaml configuration
file to minimise the amount of patches needed. So the username
is actually sorted within the rsync URL.

Signed-off-by: Bernard `Guyzmo` Pratz <guyzmo+github@m0g.net>

* Added ssh key generation upon installation for root user.

Signed-off-by: Bernard `Guyzmo` Pratz <guyzmo+github@m0g.net>

* Removed stale blank lines, and fixed typo

Signed-off-by: Bernard `Guyzmo` Pratz <guyzmo+github@m0g.net>

* fix backup-location lines, by switching it from id to class

* Various web UI fixes

- fixed user field being shadowed ;
- fixed settings reading comparaison ;
- fixed forgotten min-age field.

Signed-off-by: Bernard `Guyzmo` Pratz <guyzmo+github@m0g.net>

* Added SSH Public Key shown on the web interface UI

Signed-off-by: Bernard `Guyzmo` Pratz <guyzmo+github@m0g.net>

* trailing spaces.

Signed-off-by: Bernard `Guyzmo` Pratz <guyzmo+github@m0g.net>

* fixed the extraneous environment

Signed-off-by: Bernard `Guyzmo` Pratz <guyzmo+github@m0g.net>

* Updated key setup

- made key lower in bits, but stronger (using -a option),
- made ssh-keygen run in background using nohup,
- added independent key file, as id_rsa_miab,
- added ssh-options to all duplicity calls to use the id_rsa_miab keyfile,
- changed path to the public key display

Signed-off-by: Bernard `Guyzmo` Pratz <guyzmo+github@m0g.net>

* added rsync options for ssh identity support

Signed-off-by: Bernard `Guyzmo` Pratz <guyzmo+github@m0g.net>

* removed strict host checking for all backup operations

Signed-off-by: Bernard `Guyzmo` Pratz <guyzmo+github@m0g.net>

* Remove nohup from ssh-keygen so errors aren't hidden. Also only generate a key if none exists yet

* Add trailing slash when checking a remote backup. Also check if we actually can read the remote size

* Factorisation of the repeated rsync/ssh options

cf https://github.com/mail-in-a-box/mailinabox/pull/678#discussion_r81478919

* Updated message SSH key creation

https://github.com/mail-in-a-box/mailinabox/pull/678#discussion_r81478886
2016-11-12 09:28:55 -05:00
yodax
3b78a8d9d6 If ufw isn't installed on the machine the status checks shouldn't fail 2016-11-12 09:25:34 -05:00
Scott Bronson
6ea1a06a12 suppress Ubuntu's upgrade prompts (#992)
On every login we're notified:

  New release '16.04.1 LTS' available.
  Run 'do-release-upgrade' to upgrade to it.

Disable this so that an eager yet inattentive admin
doesn't accidentally follow these instructions.
2016-11-08 21:41:02 -05:00
Michael Kroes
2b00478b8b Check if apc is disabled during ownCloud setup, if so enable it (#983) 2016-10-24 07:59:34 -04:00
Michael Kroes
155bcfc654 Add ownCloud 9.1.1 to the changelog (#984) 2016-10-21 10:12:46 -04:00
Tristan Hill
4b07a6aa8f disable nested checker checks (#972)
fixes #967
2016-10-18 14:15:33 -04:00
Michael Kroes
2151d81453 update to ownCloud 9.1.1 (with intermediate upgrades) (#894)
[this is a squashed merge from-]

* Install owncoud 9.1 and provide an upgrade path from 8.2. This also disables memcached and goes with apc. The upgrade fails with memcached.

* Remove php apc setting

* Add dav migrations for each user

* Add some comments to the code

* When upgrading owncloud from 8.2.3 to 9.1.0 the backup of 8.2.3 was overwritten when going from 9.0 to 9.1

* Add upgrade path from 8.1.1. Only do an upgrade check if owncloud was previously installed.

* Stop php5-fpm before owncloud upgrade to prevent database locks

* Fix fail2ban tests for owncloud 9

* When upgrading owncloud copy the database to the user-data/owncloud-backup directory

* Remove not need unzip directives during owncloud extraction. Directory is removed beforehand so a normal extraction is fine

* Improve backup of owncloud installation and provide a post installation restore script. Update the owncloud version number to 9.1.1. Update the calendar and contacts apps to the latest versions

* Separate the ownCloud upgrades visually in the console output.
2016-10-18 06:04:13 -04:00
Michael Kroes
fd6226187a lower memory requirements to 512MB, display a warning if system memory is below 768MB. (#952) 2016-10-15 15:41:25 -04:00
rxcomm
bbe27df413 SSHFP record creation should scan nonstandard SSH port if necessary (#974)
* sshfp records from nonstandard ports

If port 22 is not open, dns_update.py will not create SSHFP records
because it only scans port 22 for keys. This commit modifies
dns_update.py to parse the sshd_config file for open ports, and
then obtains keys from one of them (even if port 22 is not open).

* modified test of s per JoshData request

* edit CHANGELOG per JoshData

* fix typo
2016-10-15 15:36:13 -04:00
Michael Kroes
a658abc95f Fix status checks for ufw when the system doesn't support iptables (#961) 2016-10-08 14:35:19 -04:00
Joshua Tauberer
9331dbc519 merge z-push-from-name #940 2016-10-08 14:32:57 -04:00
Steve Gregg
8b5eba21c0 Correct typo of "PRIORITY" in the template (#965) 2016-10-05 18:43:50 -04:00
yodax
da5497cd1c Update changelog entries 2016-09-28 08:37:24 +02:00
Michael Kroes
a27ec68467 Merge pull request #951 from MariusBluem/remove-certificate-providers
Remove Certificate Providers / Fix #950
2016-09-28 08:33:11 +02:00
Marius Blüm
3ac4b8aca8
Remove Certificate Providers / Fix #950
Signed-off-by: Marius Blüm <marius@lineone.io>
2016-09-27 15:06:50 +02:00
Michael Kroes
02feeafe6a change bayes_file_mode to world writable (merges #931)
fixes #534, again, hopefully
2016-09-23 15:14:21 -04:00
Marius Blüm
5f0376bfbf Fix typo in alias-page, fixes #943 (merges #949)
Signed-off-by: Marius Blüm <marius@lineone.io>
2016-09-23 15:11:37 -04:00
Joshua Tauberer
4e4fe90fc7 v0.20 2016-09-23 07:49:13 -04:00
Joshua Tauberer
3cd5a6eee7 changelog entries 2016-09-23 07:46:01 -04:00
Joshua Tauberer
c26bc841a2 more for dnspython exception with IPv6 addresses
fixes #945, corrects prev commit (#947) in case of multiple AAAA records, adds changelog
2016-09-23 07:41:24 -04:00
Mathis Hoffmann
163daea41c dnspython exception with IPv6 addresses
see #945, merges #947
2016-09-23 07:35:53 -04:00
Corey Hinshaw
d8316119eb Use Roundcube identities to populate Z-Push From name 2016-09-19 11:10:44 -04:00
Scott Bronson
102b2d46ab typo fix: seconday -> secondary (#939) 2016-09-18 08:10:49 -04:00
Joshua Tauberer
58541c467f merge #936 - fix wonky free disk space messages - from cmsirbu/master
fix status_checks.py free disk space reporting, fixes #932
2016-09-16 07:31:57 -04:00
cs@twoflower
00bd23eb04 fix status_checks.py free disk space reporting #932 2016-09-15 17:01:21 +01:00
Joshua Tauberer
d73d1c6900 changelog typos 2016-08-24 07:47:55 -04:00
Joshua Tauberer
fc0abd5b4d confirm that fail2ban is protecting pop3s, closes #629 2016-08-22 19:18:23 -04:00
Joshua Tauberer
27b4edfc76 v0.19b
-----BEGIN PGP SIGNATURE-----
 Version: GnuPG v1
 
 iQEcBAABAgAGBQJXuHvJAAoJELkgQfTBC92B2IsIAJl+tQkkVp5cu4zuSLOpHj73
 LFGGCrGTSMwuyNbnklkLmLIfRxlmNfHNfQqHYhxJQq7JVLuDRJS2rTJnSWGg4PuE
 vyrjOEFNNqFp9cy00j6NMUUcJa4kte4cvMg3Sonz7JkVwS3fxp7hSgZknYOjlLvh
 R/FmrqVhpDtTZRtMjcQaCtCTWUEETYFLsJZ2iZkIlpGhoxPGEhKZquNrT0s3qrNv
 Rwf6O3i9RIS/bOu2lWI+ymdStPVJnn+deRTBWPpsxXdNC/NG9+gWiqGgRnjTBbMO
 uzH1hYct+J6TWeNpesECfMMjTOZ+T7yrRJc1s9ThuLokyAlo9yf4E5YFziZ0hi4=
 =JxNp
 -----END PGP SIGNATURE-----

merge v0.19b hot fix release
2016-08-20 11:50:26 -04:00
Joshua Tauberer
ba75ff7820 v0.19b 2016-08-20 11:48:08 -04:00
Joshua Tauberer
a14b17794b simplify how munin-cgi-graph is called to reduce the attack surface area
Seems like if REQUEST_METHOD is set to GET, then we can drop two redundant ways the query string is given. munin-cgi-graph itself reads the environment variables only, but its calls to Perl's CGI::param will look at the command line if REQUEST_METHOD is not used, otherwise it uses environment variables like CGI used to work.

Since this is all behind admin auth anyway, there isn't a public vulnerability. #914 was opened without comment which lead me to notice the redundancy and worry about a vulnerability, before I realized this is admin-only anyway.

The vulnerability was created by 6d6f3ea391.

See #914.

This is the v0.19b hotfix commit.
2016-08-20 11:47:44 -04:00
Joshua Tauberer
35a360ef0b simplify how munin-cgi-graph is called to reduce the attack surface area
Seems like if REQUEST_METHOD is set to GET, then we can drop two redundant ways the query string is given. munin-cgi-graph itself reads the environment variables only, but its calls to Perl's CGI::param will look at the command line if REQUEST_METHOD is not used, otherwise it uses environment variables like CGI used to work.

Since this is all behind admin auth anyway, there isn't a public vulnerability. #914 was opened without comment which lead me to notice the redundancy and worry about a vulnerability, before I realized this is admin-only anyway.
2016-08-19 12:42:43 -04:00
Joshua Tauberer
86457e5bc4 merge: fail2ban broke, released v0.19a 2016-08-18 08:39:31 -04:00
Joshua Tauberer
7c9f3e0b23 v0.19a 2016-08-18 08:36:28 -04:00
Joshua Tauberer
83d8dbca3e fail2ban won't start until the roundcube log file is created
fixes #911
2016-08-18 08:32:14 -04:00
Joshua Tauberer
8cf2e468bd [merge #900] Adding a Code of Conduct
Merge pull request #900 from mail-in-a-box/code_of_conduct
2016-08-15 20:10:37 -04:00
Joshua Tauberer
440a545010 some improvements suggested by the community 2016-08-15 20:09:05 -04:00
Marius Blüm
942bcfc7c5 Update Bootstrap to 3.3.7 (#909)
Signed-off-by: Marius Blüm <marius@lineone.io>
2016-08-15 18:06:12 -04:00
ReadmeCritic
4f2d16a31d Update README URLs based on HTTP redirects (#908) 2016-08-15 11:07:09 -04:00
Joshua Tauberer
e9368de462 [merge #902] Upgrade ownCloud from 8.2.3 to 8.2.7
Merge https://github.com/mar1u5/mailinabox

fixes #901
2016-08-13 17:36:08 -04:00
Joshua Tauberer
cdd0a821eb v0.19
closes #898
2016-08-13 17:27:10 -04:00
Marius Blüm
6f165d0aeb
Update Changelog
Signed-off-by: Marius Blüm <marius@lineone.io>
2016-08-09 00:58:10 +02:00
Marius Blüm
6c22c0533e
Upgrade ownCloud from 8.2.3 to 8.2.7
Signed-off-by: Marius Blüm <marius@lineone.io>
2016-08-09 00:53:15 +02:00
Joshua Tauberer
d38b732b0a add a Code of Conduct 2016-08-08 08:19:42 -04:00
Joshua Tauberer
81b5af6b64 document fail2ban filters in security.md 2016-08-08 07:55:46 -04:00
Joshua Tauberer
fc5cc9753b roundcube 1.2.1 2016-08-08 07:32:02 -04:00
Joshua Tauberer
1aca6fe08f some minor tweaks to the new users/aliases API documentation 2016-08-08 07:28:10 -04:00
Joshua Tauberer
cf3e1cd595 add SRV records for CardDAV/CalDAV
DavDroid's latest version's account configuration no longer just asked for a hostname. Its email address & password configuration mode did not work without a SRV record.
2016-07-31 20:53:57 -04:00
Joshua Tauberer
b044dda28f put the ufw status checks in the network section, add a punctuation mark, add changelog entry 2016-07-29 09:23:36 -04:00
Joshua Tauberer
f66f39b61d Merge branch 'ufw_status_check' of https://github.com/yodax/mailinabox 2016-07-29 09:16:22 -04:00
Joshua Tauberer
6de7d59f14 changelog entries 2016-07-29 09:12:01 -04:00
Michael Kroes
9c8f2e75fc allow i686 as a supported architecture
This is checked during preflight. See https://github.com/mail-in-a-box/mailinabox/issues/885 (#889)
2016-07-29 09:07:16 -04:00
Joshua Tauberer
cbc4bf553d Merge pull request #880 from schlypel/master
Added information about API endpoints
2016-07-29 09:04:27 -04:00
Michael Kroes
4e3cfead46 Add HSTS to the control panel headers (#879) 2016-07-29 09:01:40 -04:00
Joshua Tauberer
8844a9185f Merge pull request #798 from mail-in-a-box/fail2banjails
add fail2ban jails for ownCloud, postfix submission, roundcube, and the Mail-in-a-Box management daemon
2016-07-29 08:52:44 -04:00
schlypel
3249a55f3a added API info to users page template 2016-06-29 13:35:42 +02:00
schlypel
b58fb54725 added API info to aliases page template 2016-06-29 13:34:54 +02:00
Joshua Tauberer
82903cd09e Merge pull request #857 from biermeester/master
Small extension to mail log management script
2016-06-27 06:17:16 -04:00
Michael Kroes
fb14e30feb Remove owncloud log configuration from initial setup and only apply it during the configuration updates. This applies to both the timezone and the log format 2016-06-27 06:03:24 -04:00
Michael Kroes
d9ac321f25 Owncloud needs more time to detect blocks. It doesn't respond as fast as the other services. Also owncloud logs UTC (since latest update) even though the timezone is not UTC. Also to detect a block, we get a timeout instead of a refused) 2016-06-27 06:03:19 -04:00
Michael Kroes
bf5e9200f8 Update owncloud url to use webdav and increase http timeout 2016-06-27 06:03:14 -04:00
Joshua Tauberer
5f5f00af4a for DANE, the smtp_tls_mandatory_protocols setting seems like it also needs to be set (unlike the cipher settings, this isn't documented to be in addition to the non-mandatory setting) 2016-06-12 09:11:55 -04:00
Joshua Tauberer
6b73bb5d80 outbound SMTP connections should use the same TLS settings as inbound: drop SSLv2, SSLv3, anonymous ciphers, RC4 2016-06-12 09:11:54 -04:00
Joshua Tauberer
3055f9a79c drop SSLv3, RC4 ciphers from SMTP port 25
Per http://googleappsupdates.blogspot.ro/2016/05/disabling-support-for-sslv3-and-rc4-for.html, Google is about to do the same.

fixes #611
2016-06-12 09:11:50 -04:00
Rinze
1c84e0aeb6 Added received mail count to hourly activity overview in mail log management script 2016-06-10 13:08:57 +02:00
Rinze
ae1b56d23f Added POP3 support to mail log management script 2016-06-10 11:19:03 +02:00
Rinze
946cd63e8e Mail log management script cleanup 2016-06-10 10:32:32 +02:00
Michael Kroes
01fa8cf72c add fail2ban jails for ownCloud, postfix submission, roundcube, and the Mail-in-a-Box management daemon
(tests squashed into this commit by josh)
2016-06-06 09:13:10 -04:00
Chris Blankenship
fac8477ba1 Configured Dovecot to log into its own logfile 2016-06-06 08:21:44 -04:00
aspdye
61744095a8 Update Roundcube to 1.2.0
closes #840
2016-06-06 07:32:54 -04:00
Joshua Tauberer
d5b38a27e6 run roundcube's database migration script on every update
There hasn't been a sqlite migration yet, since Mail-in-a-Box's creation, but with Roundcube 1.2 there will be.
2016-06-06 07:28:12 -04:00
Joshua Tauberer
6666d28c44 v0.18c 2016-06-02 15:47:45 -04:00
Joshua Tauberer
66675ff2e9 Dovecot LMTP accepted all mail regardless of whether destination was a user, broken by ae8cd4ef, fixes #852
In the earlier commit, I added a Dovecot userdb lookup. Without a userdb lookup, Dovecot would use the password db for user lookups. With a userdb lookup we can support iterating over users.

But I forgot the WHERE clause in the query, resulting in every incoming message being accepted if the user database contained any users at all. Since the mailbox path template is the same for all users, mail was delivered correctly except that mail that should have been rejected was delivered too.
2016-06-02 08:05:34 -04:00
Joshua Tauberer
9ee2d946b7 Merge pull request #821 from m4rcs/before-backup
Added a pre-backup script to complement post-backup script.
2016-05-17 19:48:14 -04:00
Arnaud
ff7d4196a6 target to blank for munin link in tempalte (#822)
adding :
target="_blank"
to 
<li><a href="/admin/munin">Munin Monitoring</a></li> on line 96
Why ?
Because when you click on munin link, and follow links, you lose your index, or click back many times...
So i propose my pull request.
Et voilà ^^
2016-05-17 19:46:45 -04:00
aspdye
490b36d86c Fix #819 (#823) 2016-05-17 19:46:10 -04:00
Joshua Tauberer
867d9c4669 v0.18b 2016-05-16 07:17:20 -04:00
Joshua Tauberer
1ad5892acd can't change roundcube's default_host setting, partially reverts 6d259a6e12
The default_host setting is a part of the internal username key. We can't change that without causing Roundcube to create new internal user accounts.
2016-05-16 07:14:45 -04:00
Joshua Tauberer
94b7c80792 v0.18 2016-05-15 20:41:31 -04:00
Marc Schiller
69bd137b4e Added a pre-backup script to complement post-backup script. 2016-05-11 10:11:16 +02:00
Joshua Tauberer
ae8cd4efdf support 'dovecot -A' iteration of all users 2016-05-06 09:16:48 -04:00
Joshua Tauberer
6d259a6e12 use "127.0.0.1" throughout rather than mixing use of an IP address and "localhost"
On some machines localhost is defined as something other than 127.0.0.1, and if we mix "127.0.0.1" and "localhost" then some connections won't be to to the address a service is actually running on.

This was the case with DKIM: It was running on "localhost" but Postfix was connecting to it at 127.0.0.1. (https://discourse.mailinabox.email/t/opendkim-is-not-running-port-8891/1188/12.)

I suppose "localhost" could be an alias to an IPv6 address? We don't really want local services binding on IPv6, so use "127.0.0.1" to be explicit and don't use "localhost" to be sure we get an IPv4 address.

Fixes #797
2016-05-06 09:10:38 -04:00
Joshua Tauberer
e7fffc66c7 changelog tweaks, fixes #805 2016-05-06 08:51:53 -04:00
aspdye
8548ede638 Merge pull #806 - Update Roundcube to 1.1.5 2016-04-24 06:31:28 -04:00
Joshua Tauberer
6eeb107ee3 Merge #795 - Upgrade Bootstrap 3.3.5 to 3.3.6 2016-04-24 06:27:50 -04:00
Joshua Tauberer
31eefa18da Merge #793 - Hostname as Roundcube Name 2016-04-23 13:45:09 -04:00
Joshua Tauberer
20adbb51cb Merge #804 - Make clear that Let's Encrypt is reccomended! 2016-04-23 09:51:44 -04:00
aspdye
79a39d86f9 reseller -> provider 2016-04-23 15:18:21 +02:00
aspdye
0ebf33e9df Make clear that Let's Encrypt is reccomended! 2016-04-23 11:35:02 +02:00
Joshua Tauberer
d3818d1db6 changelog entries 2016-04-13 18:42:53 -04:00
aspdye
f65d9d3196 Upgrade Bootstrap 3.3.5 to 3.3.6 2016-04-09 13:27:27 +02:00
aspdye
74fea6b93e Hostname as Roundcube Name 2016-04-09 10:23:20 +02:00
Joshua Tauberer
7a935d8385 Merge #791 - Add ownCloud 8.2.3 update to changelog 2016-04-08 08:20:06 -04:00
aspdye
7e0f534aea Add ownCloud update to changelog 2016-04-08 14:04:15 +02:00
Michael Kroes
736b3de221 Improve matching of ufw output. Reuse network service list. Improve messages 2016-04-07 16:03:28 +02:00
Joshua Tauberer
5628f8eecb Merge #773 - Set the hostname of the box during the setup 2016-04-07 09:44:39 -04:00
Joshua Tauberer
9cc5160c38 Merge #789 - Update to ownCloud v8.2.3 2016-04-07 09:32:24 -04:00
Michael Kroes
42f2e983e5 Merge branch 'master' into ufw_status_check 2016-04-07 15:13:59 +02:00
Michael Kroes
bc40134b7b Remove comment about loopback interface 2016-04-07 10:55:20 +02:00
Michael Kroes
3649ba1ce9 Merge branch 'master' into hostname 2016-04-07 10:54:53 +02:00
kurt89523
22395bdb8b Update to ownCloud v8.2.3 2016-04-06 17:31:59 -07:00
Joshua Tauberer
30c89be982 merge #771 - stop fail2ban recidive emails
The emails were not deliverable anyway.
2016-04-06 19:03:44 -04:00
Joshua Tauberer
853b641d1b Merge #787 - Add SRV record to the Custom DNS page 2016-04-05 07:17:12 -04:00
msgerbs
703a963ae5 Add SRV record to the Custom DNS page
Add SRV to the drop-down to add a custom DNS zone. I made this change on my up-to-date install and it worked without any issues.
2016-04-05 00:54:26 -05:00
Michael Kroes
c9f30e8059 Add status checks for ufw 2016-04-02 13:41:16 +02:00
Joshua Tauberer
1a1d125b31 v0.17c
-----BEGIN PGP SIGNATURE-----
 Version: GnuPG v1
 
 iQEcBAABAgAGBQJW/mJqAAoJELkgQfTBC92B/F8H/2s6wKhzzeoqkhLU2nvYJh0B
 Q1d0SbtdQWIWrTQbcjIR3aGYwJzJ+HC7rylrwS4lB2ugpJBA0MnfD+ktwbe/EyDa
 pN6WLlmnXyAw28//ubq0FQqC8Gawsj4WMfmSEw/XuDShik8XJmU7QUEnewClJ7So
 ko4eVp9KL8MU3Rj/DebhyoW0EjpB/qrJvLSqtj4KCxKYES9J8nUVBFVRDL48yNx4
 2KTIjqreGZmtW0/wxPnganMeV6DZn3B6vBmqOYYvw7bf6r/cY0ZkNK/ENlo+ntJD
 3jFKki4TJChhGVWH5T4Tw2bys4Cua1+SA3cleNRH1rYSvRWyOCwK+LS4YBJHYp4=
 =umMp
 -----END PGP SIGNATURE-----

merge hotfix release tag 'v0.17c' into master

The hotfixes were all already applied to master in original PRs. This merge merely brings over the CHANGELOG and the updated install instructions (v0.17b=>v0.17c), including to bootstrap.sh which is what triggers v0.17c being the latest release.
2016-04-01 08:00:10 -04:00
Joshua Tauberer
86881c0107 v0.17c 2016-04-01 07:58:28 -04:00
Joshua Tauberer
e65c77588e hotfix merge #776 - some owncloud paths were improperly exposed over http 2016-04-01 07:58:24 -04:00
Joshua Tauberer
3843f63416 hotfix merge #772 - yodax/generic-login-message
Make control panel login failed messages generic - don't reveal if an email address has an account on the system.
2016-03-31 10:46:38 -04:00
Joshua Tauberer
703e6795e8 hotfix merge #769 - update the Roundcube html5_notifier plugin from version 0.6 to 0.6.2
fixes Roundcube getting stuck for some people, hopefully fixes #693
2016-03-31 10:46:34 -04:00
Joshua Tauberer
b3223136f4 hotfix - install roundcube from our own mirror, hosted in Josh's AWS S3 account, because sourceforge is down all the time
fixes #750, see #701, see #370

was df92a10eba
2016-03-31 10:35:48 -04:00
Joshua Tauberer
aa1fdaddaf hotfix merge #755 - Prevent click jacking of the management interface 2016-03-31 10:34:52 -04:00
Joshua Tauberer
7fa9baf308 hotfix merge #744 - Fix for putty Line Drawing issues 2016-03-31 10:33:42 -04:00
Joshua Tauberer
36d51bbde0 merge #776 - some owncloud paths were improperly exposed over http 2016-03-31 10:20:21 -04:00
Joshua Tauberer
eb8cfaab75 changelog entry for html5_notifier bump 2016-03-31 10:20:13 -04:00
Tibor Blaho
c5e8a975cd Fix denied ownCloud nginx locations 2016-03-31 00:07:48 +02:00
Michael Kroes
3210ccdcac Don't set the hostname on the loopback 127.0.1.1 2016-03-26 15:41:20 +01:00
Joshua Tauberer
252c35c66e Merge pull request #772 from yodax/generic-login-message
Make control panel login failed messages generic - don't reveal if an email address has an account on the system.
2016-03-26 09:22:02 -04:00
Michael Kroes
c910a58f07 Set the hostname of the box during the setup 2016-03-26 14:15:28 +01:00
Michael Kroes
f292e8fc5b Add generic login failed message 2016-03-26 14:06:43 +01:00
Michael Kroes
4d7229ccb0 Add documentation on why the notification was removed from the recidive jail 2016-03-26 13:37:33 +01:00
Joshua Tauberer
1e1c3cbd00 Merge pull request #768 from yodax/web
Static web hosting, instructions weren't rendered
2016-03-26 07:46:49 -04:00
Joshua Tauberer
611e9cc84d merges #769 - update the Roundcube html5_notifier plugin from version 0.6 to 0.6.2
fixes Roundcube getting stuck for some people, hopefully fixes #693
2016-03-26 07:43:22 -04:00
Michael Kroes
454a2b167b Stop fail2ban recidive from sending emails, like all other jails 2016-03-26 09:04:51 +01:00
david
f6e0af124f updated html5_notifier version to 0.6.2 in setup 2016-03-25 20:16:51 +01:00
Michael Kroes
d7d8bda0a4 Instructions on how to create a web site for a domain weren't rendered. Users would miss the step about manually creating the directory to put files in there and wouldn't see anything happen 2016-03-25 13:37:55 +01:00
Joshua Tauberer
df92a10eba install roundcube from our own mirror, hosted in Josh's AWS S3 account, because sourceforge is down all the time
fixes #750, see #701, see #370
2016-03-23 17:31:24 -04:00
Joshua Tauberer
74a0359cec Merge pull request #763 from Neopallium/master
Fix creation of custom MX records.
2016-03-23 17:22:42 -04:00
Joshua Tauberer
336b95b3d5 Merge pull request #756 from yodax/preflight_arm
Add a preflight check for supported architecture
2016-03-23 17:19:21 -04:00
Joshua Tauberer
56591abbc2 merge #766 - Configure bayes_file_mode in spamassassin/local.cf 2016-03-23 17:17:30 -04:00
Joshua Tauberer
313a86d0fa add changelog entry for bayes file permissions 2016-03-23 17:16:50 -04:00
Joshua Tauberer
083e3cf755 merge #757 (squashed) - add swap space to low-memory systems 2016-03-23 17:07:40 -04:00
Michael Kroes
696bbe4e82 Add a swap file to the system if system memory is less than 2GB, 5GB of free disk space is available, and if no swap file yet exists 2016-03-23 17:07:04 -04:00
Joshua Tauberer
3d4cabbcd5 merge #755 - Prevent click jacking of the management interface 2016-03-23 16:53:48 -04:00
Joshua Tauberer
cdedaed3b0 merge #744 - Fix for putty Line Drawing issues 2016-03-23 16:51:01 -04:00
Joshua Tauberer
c01f903413 edit NCURSES_NO_UTF8_ACS's comment, add changelog entry 2016-03-23 16:50:27 -04:00
Joshua Tauberer
5edefbec27 merge #735 - Allow a server to be rebooted when a reboot is required 2016-03-23 16:39:40 -04:00
Joshua Tauberer
67555679bd move the reboot button, fix grammar, refactor check for DRY, add changelog entry 2016-03-23 16:37:15 -04:00
Joshua Tauberer
546d6f0026 merge #674 - Support munin's cgi dynazoom 2016-03-23 16:10:30 -04:00
Joshua Tauberer
bd86d44c8b simplify the munin_cgi wrapper / add changelog entry 2016-03-23 16:09:19 -04:00
Robert G. Jakabosky
72fcb005b2 Check MX priority. 2016-03-22 03:07:14 +08:00
Robert G. Jakabosky
84638ab11e Fix creation of custom MX records. 2016-03-21 21:12:08 +08:00
yodax
84f4509b48 Configure bayes_file_mode in spamassassin/local.cf 2016-03-20 05:55:58 +01:00
Michael Kroes
35a593af13 Improve preflight message 2016-03-14 07:14:09 +01:00
Michael Kroes
f69d6e9015 Add a preflight check for supported architecture 2016-03-14 07:00:33 +01:00
Michael Kroes
44705a32b7 Never allow admin panel to be inside a frame, use both modern and old headers. Also set no content sniffing 2016-03-13 18:40:02 +01:00
Michael Kroes
e343061cf4 Prevent clickjacking of management interface 2016-03-13 18:23:10 +01:00
Joshua Tauberer
65add24e2a Merge pull request #751 from yodax/wgetrc
Add a preflight check for ~/.wgetrc
2016-03-11 10:29:24 -05:00
Michael Kroes
33a9fb6aa2 Add a better message 2016-03-11 15:14:37 +01:00
Michael Kroes
0bc5d20e8f Add check for user overrides to wgetrc 2016-03-11 15:10:31 +01:00
Joshua Tauberer
49ea9cddd1 ssl_certificates: also forgot to catch free_tls_certificates.client.RateLimited 2016-03-06 14:39:34 -05:00
c0h1b4
6a48cdcdf3 Fix for putty Line Drawing issues
Fix for putty (Windows) Line Drawing characters to be shown correctly.
2016-03-01 10:40:39 -03:00
Joshua Tauberer
f78f039776 v0.17b (March 1, 2016)
----------------------
 
 ownCloud moved their source code to a new location, breaking our installation script.
 -----BEGIN PGP SIGNATURE-----
 Version: GnuPG v1
 
 iQEcBAABAgAGBQJW1YnLAAoJELkgQfTBC92BZg8H/RVfJRrhkM6B0UocYOmp2LQB
 u6b/KxSM5JbBq9BTyQTAkQ0Dj6caWpBwAATGgxvyAYt0Du/QTx/ycfgenkP2DWcI
 UYm+AkV2Eg9BiXpFqG33vJ/aWjATB+S3XM+77jMbeMb4kK+PfOHguvmxnsS4x8HY
 h+unkXPy8fr1rATaOGg32fTZrKkVWAriHFfJqy3kkoTHS+9fj7MVGbTBPntPaC46
 /42IsCFiQsNdz6QeHmXzFnP6AuqmkbcN9dpsFe36qfSfwVhNqC776lxxP+r1nK3p
 AWs/q5VQ4V1ght3MzDch9dOlmGuqBVB1WkGdQVV7RemYrdNi5S1h+dXXUDiNXY8=
 =C7LW
 -----END PGP SIGNATURE-----

merge point release v0.17b

ownCloud moved their source code to a new location, breaking our installation script.
2016-03-01 07:24:06 -05:00
Joshua Tauberer
d881487d68 v0.17b 2016-03-01 07:23:20 -05:00
Joshua Tauberer
33d07b2b54 ownCloud moved their source code to a new location, breaking our installation script.
Fixes #741.
2016-03-01 07:23:16 -05:00
Joshua Tauberer
3bbec18ac6 Merge pull request #734 from yodax/dynamicpool
Create a temporary multiprocessing pool
2016-02-28 12:39:11 -05:00
Joshua Tauberer
fc5c198646 Merge pull request #728 from yodax/noexec
Add check to preflight for exec on tmp
2016-02-28 12:38:43 -05:00
Joshua Tauberer
2be373fd06 Merge pull request #727 from yodax/userlist
Allow files in /home/user-data/mail/mailboxes
2016-02-28 12:33:38 -05:00
Michael Kroes
b71ad85e9f Restore an empty line 2016-02-26 09:51:22 +01:00
Michael Kroes
86d3e9da86 Merge branch 'master' into reboot 2016-02-26 09:49:03 +01:00
yodax
f53d3bc390 Merge branch 'master' into dynamicpool 2016-02-26 09:20:23 +01:00
Joshua Tauberer
f9ca440ce8 v0.17 2016-02-25 18:36:14 -05:00
Michael Kroes
8ea2f5a766 Allow a server to be rebooted when a reboot is required 2016-02-25 21:56:27 +01:00
yodax
6c1357e16c Merge branch 'master' into dynamicpool 2016-02-23 17:01:13 +01:00
Joshua Tauberer
d880f088be fix changelog description of a bug, see #725 2016-02-23 10:24:26 -05:00
Joshua Tauberer
5cabfd591b (re-fix) mail sent from an address on a subdomain of a domain hosted by the box (a non-zone domain) would never be DKIM-signed because only zones were included in the openDKIM configuration, mistakenly
This was originally fixed in 143bbf37f4 (February 16, 2015). Then I broke it in 7a93d219ef (November 2015) while doing some refactoring ahead of v0.15.
2016-02-23 10:16:04 -05:00
yodax
721730f0e8 Create a temporary multiprocessing pool 2016-02-23 06:32:01 +01:00
Joshua Tauberer
af80849857 Merge pull request #732 from yodax/memory
Reduce percentages for required free memory checks
2016-02-22 15:02:50 -05:00
yodax
7a191e67b8 Add a changelog entry 2016-02-22 21:01:33 +01:00
Joshua Tauberer
4b2e48f2c0 Merge pull request #726 from yodax/login
When previous panel was login, move to system_status
2016-02-22 14:44:23 -05:00
Joshua Tauberer
eb545d7941 Merge pull request #733 from yodax/daemons
Reduce number of processes in the pool to 5
2016-02-22 14:42:20 -05:00
yodax
a2e6e81697 Add a changelog entry 2016-02-22 19:14:46 +01:00
yodax
1b24e2cbaf Reduce percentages for required memory checks 2016-02-22 17:49:19 +01:00
yodax
0843159fb4 Reduce number of processes in the pool to 5 2016-02-22 17:38:30 +01:00
Michael Kroes
a7e60af93f Update comments 2016-02-21 12:47:09 -05:00
Michael Kroes
42f879687f Add check to preflight for exec on tmp 2016-02-21 12:43:04 -05:00
yodax
057903a303 Allow files in /home/user-data/mail/mailboxes 2016-02-21 13:49:07 +01:00
yodax
b8e99c30a2 When previous panel was login, move to system_status 2016-02-20 18:42:28 +01:00
Joshua Tauberer
3d933c16d0 Merge pull request #718 from shakaran/patch-1
Fix small typo in comments
2016-02-18 17:49:35 -05:00
Ángel Guzmán Maeso
e785886447 Fix small typo in comments 2016-02-18 15:38:33 +01:00
Joshua Tauberer
23ecff04b8 the logic in 4ed23f44e6 for taking backups more often was partly backward 2016-02-18 07:50:59 -05:00
Joshua Tauberer
a0bae5db5c update changelog 2016-02-18 07:18:51 -05:00
Joshua Tauberer
86368ed165 clean up apt_install lines and comments in setup/management.sh 2016-02-18 06:59:38 -05:00
Joshua Tauberer
5e4c0ed825 Revert "install boto (py2) via the package manager, not pip (used by duplicity)"
This reverts commit b32cb6229b.

Fixes #627. Fixes #653. Closes #714.
2016-02-18 06:54:23 -05:00
Joshua Tauberer
ffa9dc5d67 Merge pull request #716 from pra85/patch-1
Fix a typo in Readme
2016-02-18 06:44:25 -05:00
Prayag Verma
43cb6c4995 Fix a typo in Readme
`matchs` → `matches`
2016-02-18 09:47:47 +05:30
Joshua Tauberer
36cb2ef41d missing elif 2016-02-16 09:11:54 -05:00
Joshua Tauberer
098e250cc4 bump free_tls_certificates, fixes #695, if a challenge fails dont cache it permanently (or at all) 2016-02-16 09:08:58 -05:00
Joshua Tauberer
3d5a35b184 typo 2016-02-15 18:47:19 -05:00
Joshua Tauberer
87d3f2641d merge #685 - tweak postfix mail queue/warn/bounce times 2016-02-15 18:44:56 -05:00
Joshua Tauberer
c6c75c5a17 document the default values for delay_warning_time, maximal_queue_lifetime, bounce_queue_lifetime 2016-02-15 18:38:55 -05:00
Joshua Tauberer
1ba44b02d4 forgot to catch free_tls_certificates.client.ChallengeFailed
Provisioning could crash if, e.g., the DNS we see is different from the DNS Let's Encrypt sees.

see #695, probably fixes it
2016-02-15 18:22:16 -05:00
Joshua Tauberer
6fd4cd85ca Merge pull request #712 from s4wny/patch-2
Added a warning to the installation / setup script
2016-02-14 14:29:13 -05:00
Sony?
6182347641 spelling box->Box 2016-02-14 20:24:00 +01:00
Sony?
401b0526a3 Added a warning to the installation / setup script
See pull request #638 and issue #635 for more information.
2016-02-14 19:40:43 +01:00
Joshua Tauberer
2f24328608 before the user agrees to Let's Encrypt's ToS the admin could get a nightly email with weird interactive text
Made a mistake refactoring the headless variable earlier.

fixes #696
2016-02-13 12:38:16 -05:00
Joshua Tauberer
8ea42847da nightly status checks could fail if any domains had non-ASCII characters
https://discourse.mailinabox.email/t/status-check-emails-empty-after-upgrading-to-v0-16/1082/3

A user on that thread suggests an alternate solution, adding `PYTHONIOENCODING=utf-8` to `/etc/environment`. Python docs say that affects stdin/out/err. But we also use these environment variables elsewhere to ensure that config files we read/write are opened with UTF8 too. Maybe all that can be simplified too.
2016-02-13 11:51:06 -05:00
Joshua Tauberer
4ed23f44e6 take a full backup more often so we don't keep backups around for so long 2016-02-05 11:08:33 -05:00
Joshua Tauberer
178527dab1 convert the backup increment time to the local timezone, fixes #700
Duplicity gives times in UTC. We were assuming times were in local time.
2016-02-05 08:58:07 -05:00
Joshua Tauberer
f5c376dca8 Merge pull request #699 from BastianPoe/patch-1
Fix: Correct IP is reported when using custom DNS
2016-02-04 15:42:10 -05:00
Wolf-Bastian Pöttner
239eac662c Fix: Correct IP is reported when using custom DNS
Fix bug that reports wrong ip, when custom DNS is enabled
2016-02-04 21:32:11 +01:00
Joshua Tauberer
4e18f66db6 tls control panel: only show integral seconds while waiting the requested time from Lets Encrypt, in case we got back a non-integral number of seconds to wait 2016-02-03 08:21:22 -05:00
Joshua Tauberer
77937df955 bind postfix to the right network interface when sending outbound mail so that SPF checks on the receiving end will pass
fixes #3 (again)
2016-02-01 12:36:52 -05:00
Joshua Tauberer
4db8efa0df bump Roundcube to 1.1.4 2016-02-01 12:31:42 -05:00
Joshua Tauberer
66c80bd16a Merge pull request #688 from OmgImAlexis/patch-1
fixed typo
2016-01-30 19:31:47 -05:00
X O
5895aeecd7 fixed typo 2016-01-31 11:01:00 +10:30
Joshua Tauberer
83ffc99b9c change the public URL of bootstrap.sh to setup.sh 2016-01-30 11:19:51 -05:00
Joshua Tauberer
3615772b2d v0.16 2016-01-30 11:15:14 -05:00
Joshua Tauberer
78729bd277 update CHANGELOG 2016-01-27 20:23:41 -05:00
dofl
85a9a1608c Update mail-postfix.sh 2016-01-21 16:05:43 +01:00
dofl
2e693f7011 Update mail-postfix.sh
Updated according to Josh's latest reaction. Sounds good.
2016-01-21 08:38:39 +01:00
dofl
6f0220da4b Update mail-postfix.sh
Same result as maximal_queue_lifetime and bounce_queue_lifetime, but complies with rfc2821.
2016-01-20 15:34:22 +01:00
dofl
09a45b4397 Update mail-postfix.sh
The default timeout for Postfix's maximal_queue_lifetime and bounce_queue_lifetime is 5 days. This is way too long if you expect someone to have an answer and after 5 days you'll get the message that it's not delivered. This disrupts communication. It would be more responsive if the user got the 'can't deliver' error after 24 hours.
2016-01-20 13:25:41 +01:00
mike
6b408ef824 Use utils.shell instead of subprocess.Popen 2016-01-14 10:24:04 -05:00
Joshua Tauberer
62b6117638 Merge pull request #675 from jeroenj/patch-1
Removes border and rounded corners from navbar
2016-01-14 09:53:36 -05:00
Jeroen Jacobs
70111dafbc Removes border and rounded corners from navbar 2016-01-14 15:48:39 +01:00
Joshua Tauberer
faaa74c3a7 tls: hide extra reasons why domains aren't getting a new certificate during setup 2016-01-14 07:21:08 -05:00
mike
8932aaf4ef needed libcgi-fast-perl and chown log files 2016-01-13 23:55:45 -05:00
mike
6d6f3ea391 Added ability to use munin's dynazoom 2016-01-13 22:20:33 -05:00
Joshua Tauberer
2ad7d0830e add exception handling for what_version_is_this, fixes #659 2016-01-09 09:23:07 -05:00
Joshua Tauberer
5045e206c2 roundcube file ownership should not preserve uid/gid from the release tarball, tar (when run as root) should always extract using --no-same-owner, fixes #667 2016-01-09 09:17:45 -05:00
Joshua Tauberer
07f9228694 Merge branch 'letsencrypt' for automatic provisioning of TLS certificates from Let's Encrypt 2016-01-09 08:58:35 -05:00
Joshua Tauberer
50b5b91216 v0.15a
Sending mail through Exchange/ActiveSync (Z-Push) had been broken since v0.14. This is now fixed.
 -----BEGIN PGP SIGNATURE-----
 Version: GnuPG v1
 
 iQEcBAABAgAGBQJWkQ9rAAoJELkgQfTBC92BPjQH/ibFBZHgma7C53Q4X9iPfUk2
 dPK75rNBx06d6yW4LNAYuWVnNO1Mb0khb2k2UPwg4noImYWqsNS9hXih7C6oPHiK
 Szz4ubCc5MEqnmhxiNkzLdIBvsyKmz8IfmCl+LCXu8uk0Fb+pB6zbSdAGxjtaSPL
 itCwz8+ApTC4bl1CoNYPn2zudHmHNeC7L6INYdb+xbtc/Tz6mO/xMaBVPDiKeq9P
 LqLTOXiJNENz7vKSZytlWGOTdtSTZqwd7JBXuBg0QFz5C9yg8EV4LWB9wOm5aTIf
 Fol3WK5ZHA5YeihZKmZSjz9+p4iwv5hqR5osKL2n46LeVHyafESl+QnZSXRlDjE=
 =i3ei
 -----END PGP SIGNATURE-----

Merge release branch v0.15a

v0.15a

Sending mail through Exchange/ActiveSync (Z-Push) had been broken since v0.14. This is now fixed.
2016-01-09 08:48:37 -05:00
Joshua Tauberer
72bfc0915c v0.15a (January 9, 2016)
Sending mail through Exchange/ActiveSync (Z-Push) had been broken since v0.14. This is now fixed.
2016-01-09 08:44:51 -05:00
Bernard `Guyzmo` Pratz
a7d7a9adbd Fixing issue making it impossible to send mail from Z-Push
* added IMAP_SMTP_METHOD to z_push/backend_imap
 * reverting that line accidentally deleted in commit 5055ef
 * cf pull request GH-580 that commit is part of

Signed-off-by: Bernard `Guyzmo` Pratz <guyzmo+github@m0g.net>
2016-01-09 08:42:18 -05:00
Joshua Tauberer
413af1fe67 Merge pull request #668 from guyzmo/z-push-smtp-fix
Fixing issue making it impossible to send mail from Z-Push
2016-01-09 08:41:59 -05:00
Bernard `Guyzmo` Pratz
b09cbb0ca4 Fixing issue making it impossible to send mail from Z-Push
* added IMAP_SMTP_METHOD to z_push/backend_imap
 * reverting that line accidentally deleted in commit 5055ef
 * cf pull request GH-580 that commit is part of

Signed-off-by: Bernard `Guyzmo` Pratz <guyzmo+github@m0g.net>
2016-01-08 16:43:09 +00:00
Joshua Tauberer
5b135738b4 Merge pull request #661 from baltoche/master
Gandi tells me that .be TLD only supports algorithms 8 and 10 for DNSSEC
2016-01-05 11:10:21 -05:00
baltoche
36e5772a8e Update dns_update.py 2016-01-05 16:56:16 +01:00
Joshua Tauberer
2b9fb9643d changelog entries for lets encrypt 2016-01-04 18:43:17 -05:00
Joshua Tauberer
2882e63dd8 second part of provisioning tls certificates from the control panel 2016-01-04 18:43:17 -05:00
Joshua Tauberer
812ef024ef status checks: check that the non-primary domains also resolve over IPv6, if configured 2016-01-04 18:43:17 -05:00
Joshua Tauberer
40cdc5aa30 status checks: if a domain's DNS isnt working dont check the TLS certificate because we cant automatically provision one now anyway 2016-01-04 18:43:17 -05:00
Joshua Tauberer
b8d6226a9a when provisioning tls certs from the command line, specify domain names as command line arguments to force getting certs for those domains 2016-01-04 18:43:17 -05:00
Joshua Tauberer
bac15d3919 provision tls certificates from the control panel 2016-01-04 18:43:16 -05:00
Joshua Tauberer
4b4f670adf s/SSL/TLS/ in user-visible text throughout the project 2016-01-04 18:43:16 -05:00
Joshua Tauberer
b1b57f9bfd don't try to get certs for IDNA domains and report all reasons for not fetching a certificate
fixes #646
2016-01-04 18:43:16 -05:00
Joshua Tauberer
b6933a73fa provision and install free SSL certificates from Let's Encrypt 2016-01-04 18:43:16 -05:00
Joshua Tauberer
5033042b8c backups: email the administrator when there's a problem
Refactor by moving the email-the-admin code out of the status checks and into a new separate tool.

This is why I suppressed non-error output of the backups last commit - so it doesn't send a daily email.
2016-01-04 18:43:02 -05:00
Joshua Tauberer
89a46089ee backups: suppress all output except errors 2016-01-04 18:43:02 -05:00
Joshua Tauberer
e288d7730b backups: trap an error that occurs as early as getting the current backup status 2016-01-04 18:43:02 -05:00
Joshua Tauberer
5ae75e723c Merge pull request #654 from ChlorideCull/cull-fix-typo
Fix miscellaneous typos in comments
2016-01-03 10:53:25 -05:00
Chloride Cull
d6d5009d23 Fix typos in questions.sh
sed s/supress/suppress/g
sed s/depencies/dependencies/g
2016-01-03 16:48:23 +01:00
Joshua Tauberer
06a0e7f3fe merge #584 - Add checks to the management interface to report memory usage 2016-01-01 18:13:21 -05:00
Joshua Tauberer
a9cd72bbf9 tighten the status text strings for free memory, add changelog entry 2016-01-01 18:12:36 -05:00
Joshua Tauberer
f184a74fa0 merge #647 - open the port for Sieve 2016-01-01 17:53:40 -05:00
Joshua Tauberer
682b1dea5e changelog/status checks updated for opening the sieve port 2016-01-01 17:53:05 -05:00
Joshua Tauberer
3fbbf56986 v0.15 (January 1, 2016)
-----------------------

Mail:

* Updated Roundcube to version 1.1.3.
* Auto-create aliases for abuse@, as required by RFC2142.
* The DANE TLSA record is changed to use the certificate subject public key rather than the whole certificate, which means the record remains valid after certificate changes (so long as the private key remains the same, which it does for us).

Control panel:

* When IPv6 is enabled, check that system services are accessible over IPv6 too, that the box's hostname resolves over IPv6, and that reverse DNS is setup correctly for IPv6.
* Explanatory text for setting up secondary nameserver is added/fixed.
* DNS checks now have a timeout in case a DNS server is not responding, so the checks don't stall indefinitely.
* Better messages if external DNS is used and, weirdly, custom secondary nameservers are set.
* Add POP to the mail client settings documentation.
* The box's IP address is added to the fail2ban whitelist so that the status checks don't trigger the machine banning itself, which results in the status checks showing services down even though they are running.
* For SSL certificates, rather than asking you what country you are in during setup, ask at the time a CSR is generated. The default system self-signed certificate now omits a country in the subject (it was never needed). The CSR_COUNTRY Mail-in-a-Box setting is dropped entirely.

System:

* Nightly backups and system status checks are now moved to 3am in the system's timezone.
* fail2ban's recidive jail is now active, which guards against persistent brute force login attacks over long periods of time.
* Setup (first run only) now asks for your timezone to set the system time.
* The Exchange/ActiveSync server is now taken offline during nightly backups (along with SMTP and IMAP).
* The machine's random number generator (/dev/urandom) is now seeded with Ubuntu Pollinate and a blocking read on /dev/random.
* DNSSEC key generation during install now uses /dev/urandom (instead of /dev/random), which is faster.
* The $STORAGE_ROOT/ssl directory is flattened by a migration script and the system SSL certificate path is now a symlink to the actual certificate.
* If ownCloud sends out email, it will use the box's administrative address now (admin@yourboxname).
* Z-Push (Exchange/ActiveSync) logs now exclude warnings and are now rotated to save disk space.
* Fix pip command that might have not installed all necessary Python packages.
* The control panel and backup would not work on Google Compute Engine because GCE installs a conflicting boto package.
* Added a new command `management/backup.py --restore` to restore files from a backup to a target directory (command line arguments are passed to `duplicity restore`).
2016-01-01 17:47:18 -05:00
Ralph J.Mayer
2835d9b468 Merge pull request #1 from TabTwo/TabTwo-patch-1
Allow remote client for Sieve
2015-12-31 18:23:30 +01:00
Ralph J.Mayer
afd401c3d4 Allow remote client for Sieve 2015-12-31 18:22:31 +01:00
Joshua Tauberer
362bc060f6 fix merge mistake (4305a71916) 2015-12-26 14:12:15 -05:00
Joshua Tauberer
c8fef45362 v0.15-rc1 2015-12-26 14:01:00 -05:00
Joshua Tauberer
8d19eade85 clarify the backup days option, fixes #570 2015-12-26 12:04:26 -05:00
Joshua Tauberer
d53332b7cf drop the CSR_COUNTRY setting and ask within the control panel 2015-12-26 11:48:23 -05:00
Joshua Tauberer
392d33b902 change DANE TLSA record to hash the subject public key rather than the whole certificate, which means it is good for any certificate tied to the same private key
Better for short-lived certificates. This is especially in preparation to using certificates from Let's Encrypt.

see #268
2015-12-26 11:01:46 -05:00
Joshua Tauberer
4305a71916 merge #587 - move backup and nightly status checks to 3am in system time
previously these were run in a cron.daily script which per crontab is run at 6:25 am local time
2015-12-26 08:42:58 -05:00
Joshua Tauberer
a4d8e12fd7 clean up the backup time patch: dont choose timezone here, move status checks into the same 3am script 2015-12-26 08:41:37 -05:00
Joshua Tauberer
3cb5e109a3 update changelog entries 2015-12-26 08:25:47 -05:00
Joshua Tauberer
e4a4b47fac setup now asks for and sets the system timezone
closes #294
see #328
maybe related to #235
2015-12-26 08:08:08 -05:00
BuildTools
8a35905d2e add timezone selection 2015-12-23 17:29:13 -05:00
Joshua Tauberer
dbf4729109 add management/backup.py --restore 2015-12-23 12:53:38 +00:00
Joshua Tauberer
86b9ef496c Merge pull request #636 from bronson/doc-mites
tiny tweaks to make the bash docs slightly more readable
2015-12-23 07:29:39 -05:00
Scott Bronson
6336cc6452 tiny tweaks to make the bash slightly more readable 2015-12-22 12:33:26 -08:00
Joshua Tauberer
bc79319864 Merge pull request #494 from anoma/fail2ban-recidive
Activate FAIL2BAN recidive jail
2015-12-22 08:11:19 -05:00
Joshua Tauberer
62e88cff54 merge #624 document POP client settings in the control panel 2015-12-12 08:46:52 -05:00
Joshua Tauberer
6e6c993724 reword POP documentation, add to changelog/readme 2015-12-12 08:46:18 -05:00
Marius
f8b4e3775d Update mail-guide.html (POP3) 2015-12-12 08:41:13 -05:00
Joshua Tauberer
fad69f85fa Merge pull request #605 from ariejan/feature/604-add-rfc2142-mail-aliases
Add alias for abuse@
2015-12-07 15:56:51 -05:00
Joshua Tauberer
0029811de2 Merge pull request #621 from bronson/nobind9
don't install bind9-host when setting hostname
2015-12-07 15:52:47 -05:00
Scott Bronson
fe9ed3f70d don't install bind9-host when setting hostname
also remove an incorrect comment
2015-12-07 10:21:51 -08:00
Ariejan de Vroom
aedfe62bb0 Add alias for abuse@ 2015-12-07 16:31:58 +01:00
Joshua Tauberer
c4f00626ef status checks: check that PRIMARY_HOSTNAME's AAAA record is working 2015-12-07 09:08:00 -05:00
Joshua Tauberer
fdad83a1bb status checks: check IPv6 reverse DNS 2015-12-07 08:58:48 -05:00
Joshua Tauberer
20e11bbab3 fail2ban: whitelist our machine's public ip address so status checks dont cause bans of the machine itself 2015-12-07 08:45:59 -05:00
Joshua Tauberer
5bbe9f9a04 status checks: when ipv6 is enabled, check that services are accessible over ipv6 too 2015-12-07 08:37:04 -05:00
Joshua Tauberer
7a93d219ef some cleanup in dns_update.py 2015-11-29 14:59:35 +00:00
Joshua Tauberer
808522d895 merge functions get_web_domains and get_default_www_redirects 2015-11-29 14:46:08 +00:00
Joshua Tauberer
be9efe0273 ensure malformed ssl certificate can't cause it to be written to an arbitrary path 2015-11-29 14:04:37 +00:00
Joshua Tauberer
766b98c4ad refactor: move SSL-related management functions into a new module ssl_certificates.py 2015-11-29 13:59:22 +00:00
Joshua Tauberer
c422543fdd make the system SSL certificate a symlink so we never have to replace a certificate file, and flatten the directory structure of user-installed certificates 2015-11-29 02:02:01 +00:00
Joshua Tauberer
cf33be4596 fix boto 2 conflict on Google Compute Engine instances
GCE installs some Python-2-only boto plugin that conflicts with boto running under Python 3. It gives a SyntaxError in /usr/share/google/boto/boto_plugins/compute_auth.py (https://github.com/GoogleCloudPlatform/compute-image-packages).

Disabling boto's default configuration file prior to importing boto so that GCE's plugin is not loaded.

See https://discourse.mailinabox.email/t/500-internal-server-error-for-admin/942.
2015-11-26 14:51:44 +00:00
Joshua Tauberer
161d096139 add a way to dump backup status from the command line 2015-11-26 14:34:07 +00:00
Joshua Tauberer
b32cb6229b install boto (py2) via the package manager, not pip (used by duplicity) 2015-11-26 14:20:59 +00:00
Joshua Tauberer
3dd5fff110 Merge pull request #602 from NurdTurd/patch-2
Typo on 'weirdly'
2015-11-20 14:54:39 -05:00
Sheldon Rupp
398a66dd4a Typo on 'weirdly' 2015-11-20 20:46:28 +01:00
Michael Kroes
59f8aa1c31 Add checks to the management interface to report memory usage 2015-11-20 01:48:59 -05:00
Joshua Tauberer
cfba97e104 updated changelog 2015-11-19 07:01:05 -05:00
Joshua Tauberer
bbf78716fd during setup suppress the status line about generating an SSL certificate if we already have it 2015-11-19 07:00:33 -05:00
Joshua Tauberer
b9820641aa when generating the initial self-signed cert, dont keep the CSR - it has no use after this step 2015-11-19 07:00:33 -05:00
Joshua Tauberer
8c00556bab use /dev/urandom for roundcube/owncloud key generation, see #596, partially reverts #115 (69f0e1d07a) 2015-11-19 07:00:33 -05:00
Joshua Tauberer
16d148a8a9 use /dev/urandom for DNSSEC key generation, fixes #596, partially reverts #115 (69f0e1d07a) 2015-11-19 07:00:33 -05:00
Joshua Tauberer
e8264e9b6a ensure /dev/urandom is seeded with a blocking call to /dev/random and using Ubuntu's pollinate servers 2015-11-19 07:00:33 -05:00
Joshua Tauberer
4f2b223070 add comments about how openssl generates random numbers for genrsa and what could create a perfect storm to make the key not random
see #596
2015-11-19 07:00:32 -05:00
Joshua Tauberer
05e128cafb the >'s in pip install package names might be interpreted as shell redirects and was creating files name '=1.0.0' '=2.0.0' and '=1.0.2' (I'm not sure how this was ever working) 2015-11-19 07:00:32 -05:00
Joshua Tauberer
59e9952a61 the explanatory text for setting up secondary nameservers was hidden until a secondary nameserver is added, so that wasn't helpful 2015-11-19 07:00:32 -05:00
Joshua Tauberer
2f9fd09b2f Merge pull request #599 from nstanke/loglevel
Change Z-Push log level to error
2015-11-18 17:42:58 -05:00
Norman Stanke
ec20d657ba Change Z-Push log level to error 2015-11-18 21:39:17 +01:00
Joshua Tauberer
34ba279b0a Merge pull request #598 from yodax/zpush-log-rotate
Add log rotation to z-push
2015-11-18 07:25:24 -05:00
yodax
c28065cc56 Add log rotation to z-push 2015-11-17 09:27:05 -05:00
Joshua Tauberer
04960d0b98 Merge pull request #597 from yodax/backup-stop-phpfpm
During the backup you will get login failures which will confuse iOS
2015-11-17 08:14:23 -05:00
yodax
280de022cb Change order in which service stop 2015-11-17 05:22:42 -05:00
yodax
fa1cad7fb2 During the backup you will get login failures which will confuse iOS, so it is better to stop php-fpm as well 2015-11-17 02:57:14 -05:00
Joshua Tauberer
1926bfa1c5 all DNS queries should have a timeout, fixes #591 2015-11-11 12:25:55 +00:00
Joshua Tauberer
2b351208e0 Merge pull request #589 from NurdTurd/patch-1
Change 'Wosign' to 'WoSign'
2015-11-08 15:59:12 -05:00
Sheldon Rupp
96b02e68ee Change 'Wosign' to 'WoSign' 2015-11-08 21:31:43 +01:00
BuildTools
82f4f8b2eb delete /etc/cron.daily/mailinabox-backup 2015-11-06 07:55:48 -05:00
BuildTools
d703b0a2a1 change from /etc/cron.daily to /etc/cron.d 2015-11-06 07:47:40 -05:00
Joshua Tauberer
2e3796c4f7 set owncloud's email from address, fixes #586 2015-11-05 11:20:16 +00:00
Joshua Tauberer
ac238b9d28 dont run secondary nameserver checks if the zone's nameservers aren't correct to begin with, possibly because the user is using external DNS, see #582 2015-11-05 11:09:15 +00:00
Joshua Tauberer
25e6fa53c2 update Roundcube to 1.1.3 2015-11-05 11:03:34 +00:00
Joshua Tauberer
57b4c685df v0.14
v0.14 (November 4, 2015)
------------------------

Mail:

* Spamassassin's network-based tests (Pyzor, others) and DKIM tests are now enabled. (Pyzor had always been installed but was not active due to a misconfiguration.)
* Moving spam out of the Spam folder and into Trash would incorrectly train Spamassassin that those messages were not spam.
* Automatically create the Sent and Archive folders for new users.
* The HTML5_Notifier plugin for Roundcube is now included, which when turned on in Roundcube settings provides desktop notifications for new mail.
* The Exchange/ActiveSync backend Z-Push has been updated to fix a problem with CC'd emails not being sent to the CC recipients.

Calender/Contacts:

* CalDAV/CardDAV and Exchange/ActiveSync for calendar/contacts wasn't working in some network configurations.

Web:

* When a new domain is added to the box, rather than applying a new self-signed certificate for that domain, the SSL certificate for the box's primary hostname will be used instead.
* If a custom DNS record is set on a domain or 'www'+domain, web would not be served for that domain. If the custom DNS record is just the box's IP address, that's a configuration mistake, but allow it and let web continue to be served.
* Accommodate really long domain names by increasing an nginx setting.

Control panel:

* Added an option to check for new Mail-in-a-Box versions within status checks. It is off by default so that boxes don't "phone home" without permission.
* Added a random password generator on the users page to simplify creating new accounts.
* When S3 backup credentials are set, the credentials are now no longer ever sent back from the box to the client, for better security.
* Fixed the jumpiness when a modal is displayed.
* Focus is put into the login form fields when the login form is displayed.
* Status checks now include a warning if a custom DNS record has been set on a domain that would normally serve web and as a result that domain no longer is serving web.
* Status checks now check that secondary nameservers, if specified, are actually serving the domains.
* Some errors in the control panel when there is invalid data in the database or an improperly named archived user account have been suppressed.
* Added subresource integrity attributes to all remotely-sourced resources (i.e. via CDNs) to guard against CDNs being used as an attack vector.

System:

* Tweaks to fail2ban settings.
* Fixed a spurrious warning while installing munin.
2015-11-04 17:56:31 -05:00
Joshua Tauberer
3fd1279e7d ...but then also have to compare against the intended IP address, which might have a custom override, see #582 2015-11-03 12:06:03 +00:00
Joshua Tauberer
3bc38c89ab secondary NS status checks in 3b91bc2c0a should not be skipped if the target IP address has been modified by a custom record
see #582
2015-11-03 06:48:04 -05:00
Joshua Tauberer
4995cebc38 add additional comments explaining why the IMAP special folders are set up as they are 2015-11-01 07:30:15 -05:00
Joshua Tauberer
720157e8a3 update changelog 2015-10-31 19:20:56 +00:00
Joshua Tauberer
8d9eb022d1 bump HTML5_Notifier version, include its version in the check for whether we need to update Roundcube 2015-10-31 19:06:56 +00:00
Joshua Tauberer
5e2eb51879 merge: add roudcube html5_notifier plugin, #550
Merge branch 'patch-1' of https://github.com/Hoekynl/mailinabox
2015-10-31 18:59:53 +00:00
Joshua Tauberer
d0062b7de4 Merge pull request #572 from OmgImAlexis/patch-1
Added wosign as a suggested free SSL provider.
2015-10-31 14:57:13 -04:00
Joshua Tauberer
6307503cda Merge pull request #580 from yodax/z-push-update
Update z-push to latest version
2015-10-31 14:52:46 -04:00
Michael Kroes
9b1e04b1e8 Merge remote-tracking branch 'upstream/master' into z-push-update 2015-10-31 03:08:54 -04:00
Michael Kroes
24f1dbc0bb PHP version has a bug that needs a workaround in z-push 2015-10-27 16:42:58 -04:00
Joshua Tauberer
5d158c524d Merge pull request #581 from yodax/default-archive-folder
For a new user create the archive folder
2015-10-27 08:15:50 -04:00
Michael Kroes
fd9287a0fd Add Archive folder to comment in mail-dovecot.sh 2015-10-27 07:58:07 -04:00
Michael Kroes
90836eff5b For a new user create the archive folder 2015-10-27 02:20:00 -04:00
Michael Kroes
914cf68651 Remove default comments from imap config 2015-10-25 13:26:38 -04:00
Michael Kroes
4db82d3d09 Caldav doesnt support sync tokens 2015-10-25 13:19:22 -04:00
Michael Kroes
5055ef060d Change configuration options for new version of z-push 2015-10-25 08:29:57 -04:00
Michael Kroes
35088a7cac Update Z-Push version to 80cbe53de4ab8dd598d1f2af6f0a23fa396c529a 2015-10-25 07:25:24 -04:00
Joshua Tauberer
f046031b26 nginx-ssl.conf changes were partially incorrect, partial revert of 834c42bc50
My own /etc/nginx/nginx.conf was messed up, so what I thought were Ubuntu 14.04 defaults weren't, and we lost the ssl_protocols and ssl_prefer_server_ciphers settings. This puts those back.

https://discourse.mailinabox.email/t/dev-master-version-reported-as-poodle-attack-vulnerable-by-ssllabs/898
2015-10-24 11:36:18 +00:00
Joshua Tauberer
3b91bc2c0a if secondary nameservers are given, status checks now check they are serving the right info 2015-10-22 10:58:36 +00:00
Joshua Tauberer
4c4babd9e7 experimentally scanning the mail log to see if we can infer a good time to take a backup 2015-10-22 10:35:14 +00:00
Joshua Tauberer
53dc53bf8f changelog entries 2015-10-18 12:10:57 +00:00
Joshua Tauberer
274e5ca676 let dovecot automatically create mailbox folders rather than doing it manually in the management daemon, fixes #554 2015-10-18 11:55:27 +00:00
Joshua Tauberer
5e7b7835b7 Merge pull request #573 from ptimof/master
Added 'Sent' folder when creating user.
2015-10-12 10:05:52 -04:00
Peter Timofejew
1bdfdbee89 Added 'Sent' folder when creating user. 2015-10-12 09:43:35 -04:00
X O
ebffaab16a Added wosign as a suggest free SSL provider. 2015-10-11 11:33:18 +10:30
Joshua Tauberer
d6d4085809 munin setup may show '/bin/rm: missing operand', fixes #527 2015-10-10 16:48:49 +00:00
Joshua Tauberer
2a44b0cafb the new SSL certs routine requires cryptography>=1.0.2 to make RSAPublicNumbers hashable
an earlier problem about --upgrade (de34d0d337) seemed to be just a local problem on my box, so going back to unpinned >= requirement specs

https://discourse.mailinabox.email/t/upgrade-to-v0-13b-broke-admin/876
2015-10-08 12:24:22 +00:00
Joshua Tauberer
834c42bc50 move nginx-ssl to be a global configuration file rather than including it into each server block 2015-09-27 17:13:11 +00:00
Joshua Tauberer
6c8ee1862a use subresource integrity attributes to guard against CDNs being used as an attack vector; drop external resources that we can't protect this way (fonts); fixes #234 2015-09-18 19:04:28 +00:00
Joshua Tauberer
787beab63f choose the best SSL cert from among the installed certificates; use the server certificate instead of self-signed certificates
For HTTPS for the non-primary domains, instead of selecting an SSL certificate by expecting it to be in a directory named after the domain name (with special-case lookups
for www domains, and reusing the server certificate where possible), now scan all of the certificates that have been installed and just pick the best to use for each domain.

If no certificate is available, don't create a self-signed certificate anymore. This wasn't ever really necessary. Instead just use the server certificate.
2015-09-18 13:25:18 +00:00
Joshua Tauberer
58349a9410 when updating DNS, clear the local DNS cache 2015-09-18 13:00:53 +00:00
Joshua Tauberer
93c2258d23 let the HSTS header be controlled by the management daemon so some domains can choose to enable preload 2015-09-08 21:20:50 +00:00
Joshua Tauberer
bd7a4dedc1 Merge pull request #551 from anoma/master
Revert two FAIL2BAN SSH jail changes
2015-09-07 06:49:48 -04:00
anoma
ae3ae0b5ba Revert to default FAIL2BAN findtime for SSH jail
I propose that the default 600s/10minute find time is a better test duration for this ban. The altered 120s findtime sounds reasonable until you consider that attackers can simply throttle to 3 attempts per minute and never be banned.

The remaining non default jail settings of maxretry = 7 and bantime = 3600 I believe are good.
2015-09-07 08:36:59 +01:00
anoma
42d657eb54 Unnecessary config item, inherited from default jail.conf 2015-09-07 08:28:54 +01:00
Joshua Tauberer
d60d73b7e0 status checks: dont error if there's a domain that dns_update hasn't been run yet on 2015-09-06 13:27:35 +00:00
Joshua Tauberer
6704da1446 silence errors in the admin if there is an invalid domain name in the database
see #531
2015-09-06 13:27:28 +00:00
Hoekynl
d24a2f7cab Updated, mistype.
Removed :$HTML5_NOTIFIER_VERSION, which breaks it
2015-09-06 10:22:08 +02:00
Hoekynl
ed31002cc6 Added commit version hash. Working now.
Added HTML5_NOTIFIER_VERSION
Updated git_clone to work.

Tested and working.
2015-09-06 10:20:36 +02:00
Hoekynl
f8ac896795 Include html5_notifier by default
Include the roundcube plugin html_notifier by default
2015-09-05 23:33:19 +02:00
Joshua Tauberer
3e96de26dd server_names_hash_bucket_size=128 now, see #93 2015-09-05 20:24:17 +00:00
Joshua Tauberer
4f6fa40dbd warn in status checks if a custom DNS record has been set on a domain that would normally serve web and as a result that domain no longer is serving web 2015-09-05 20:07:51 +00:00
Joshua Tauberer
104b804059 if a custom DNS record exists for a web-serving domain and the record is just the box's IP address, don't skip this domain for serving web 2015-09-05 20:07:51 +00:00
Joshua Tauberer
c545e46ebe Merge pull request #548 from NurdTurd/patch-1
Typo
2015-09-05 15:30:25 -04:00
Sheldon Rupp
52a216fbcb Typo
Change KB to MB due to typo.
2015-09-05 21:29:24 +02:00
Joshua Tauberer
2c29d59895 Merge pull request #478 from kri3v/patch-1
Added more bantime and lowered max retry attempts
2015-09-05 11:42:36 -04:00
Joshua Tauberer
de34d0d337 pin pip versions of email_validator and cryptography so pip doesn't keep reinstalling them each upgrade even if nothing changed (and the ceffi depedency installation can be very slow and is prone to break under low memory) 2015-09-05 12:35:01 +00:00
Joshua Tauberer
2bb7a6fc27 changelog entries 2015-09-05 08:01:59 -04:00
Joshua Tauberer
1b84292c56 Merge pull request #544 from 0xFelix/master
Fix DKIM validation and spamassassin DNS/Pyzor checks
2015-09-05 06:59:00 -04:00
Felix
18efae9703 Remove direct dependencies as they get installed automatically 2015-09-05 09:08:47 +02:00
Joshua Tauberer
4b6d86ef89 trim the instructions at the end of an upgrade about the DNS-broken control panel login 2015-09-04 18:49:32 -04:00
Joshua Tauberer
75a75a6f84 admin: rename my ajax javascript function to ajax_with_indicator; see 79c57c2303 2015-09-04 18:40:56 -04:00
Joshua Tauberer
2e99589336 admin: fix jumpyness when a modal is shown (move overflow-y to body; make the navbar not fixed to top) 2015-09-04 22:21:10 +00:00
Joshua Tauberer
188b21dd36 bump bootstrap to 3.3.5 and jquery to 1.11.3 on the admin 2015-09-04 22:13:56 +00:00
Joshua Tauberer
0cf56e0aad add a random password generator to the users page of the admin 2015-09-04 22:12:07 +00:00
Felix
bd7728ac94 Add documentation for additional packages, remove unneeded package libcrypt-openssl-random-perl 2015-09-04 15:45:47 +02:00
Felix
b6f7a10569 Add missing dependencies for DKIM validation 2015-09-04 09:25:49 +02:00
Felix
53a9fc0e48 Set 'LOCALONLY' to 0 in /etc/default/spampd 2015-09-04 09:18:12 +02:00
Joshua Tauberer
b05af6eecb v0.13b
ownCloud 8.1.1 trusted_domains autoconfiguration fix.
 -----BEGIN PGP SIGNATURE-----
 Version: GnuPG v1
 
 iQEcBAABAgAGBQJV43ODAAoJELkgQfTBC92BAMEH/3DbsticgFhbPzMsCcmcjxkg
 1Dxw4e8YRgMPp3xuq4/5we6bL/KXSxioFc1488jfiLhAe6fHZGmSi4p6L8twnsxD
 exUd/pHZ8L1SC953JhBXLUWYfAQ/ozEZ8bNPVJ4NLx5T58FPWBSRouQHHZTMc/z1
 Pduc6RjZQ3o1dmTzbwt5hB/ZS61CFV2V9cr+aKmFSDKh7/qzBSaqGfiTOsWI43GE
 JfCN6hwnCUvvkGfaYmxJSY/emgiJETLkQCv0e1kZs5MfojkFUspqvmTQViE2HI4f
 y5FWmPXvhoHuMIgH0q0Rrw0xchXW44fJbK4SnT50z7do8F7KmSX6ztw5oxux/U0=
 =kcFy
 -----END PGP SIGNATURE-----

v0.13b - release & merge side-branch

ownCloud 8.1.1 trusted_domains autoconfiguration fix.
2015-08-30 17:21:36 -04:00
Joshua Tauberer
571171a0c6 ownCloud 8.1.1's autoconfig resets trusted_domains / update trusted_domains if PRIMARY_HOSTNAME changes
Seems like ownCloud 8.1.1 now doesn't play nice with trusted_domains. Whatever is put in ahead of time gets reset to an array containing 'localhost' only, probably because we invoke autoconfiguration from the command line where it doesn't know the hostname it's being accessed from. We now set this value after running autoconfig.

This has the added benefit of also fixing the problem that if PRIMARY_HOSTNAME changes, trusted_domains wasn't updated. Now it is. Fixes #503.

See #514.
2015-08-30 17:19:38 -04:00
Joshua Tauberer
c5082498ab utils.py can't import non-standard modules because it is imported by migrate.py, which is run before anything is installed
closes #540
2015-08-30 13:50:34 -04:00
Joshua Tauberer
d19c215bf1 Merge pull request #537 from elwebmaster/patch-1
Update nginx-primaryonly.conf
2015-08-28 15:10:49 -04:00
Stefan Dimitrov
42dd46e305 Update nginx-primaryonly.conf
Nginx should be connecting over the local interface, not to the IP the resolver gives it. Elsewhere in this file proxy_pass uses 127.0.0.1 as it should.
2015-08-28 15:07:47 -04:00
Joshua Tauberer
a6496949f8 Merge pull request #536 from badsyntax/external-dns-txt-record-limit-info
Added a note about TXT record length limitations and how to construct the records to bypass the limitation
2015-08-28 15:00:23 -04:00
Richard Willis
ab59323813 Added a note about TXT record length limitations and how to construct the records to bypass the limitation 2015-08-28 15:50:02 +02:00
Joshua Tauberer
a56a9dc6a1 add Mail-in-a-Box version check to status checks
closes #502
2015-08-28 12:34:02 +00:00
Joshua Tauberer
bc790ea581 backups: make the instructions about the backup password file more prominent 2015-08-28 12:33:07 +00:00
Joshua Tauberer
dbfd158388 dont refresh the backup page when there's an error saving the config 2015-08-28 12:33:07 +00:00
Joshua Tauberer
2b1f7da654 S3 credentials for backup should not be displayed in the control panel, fixes #529 2015-08-28 12:33:07 +00:00
Joshua Tauberer
0c9d431a3f major cleanup to adding new version check to the status checks 2015-08-28 12:29:55 +00:00
Norman Stanke
1a525df8ad Add Mail-in-a-Box version status check. 2015-08-28 11:55:21 +00:00
Joshua Tauberer
ef1779ba80 Merge pull request #523 from derekrspencer/master
Fix antispam-plugin config problem in #520
2015-08-28 07:51:02 -04:00
Joshua Tauberer
d4e9938e3f Merge pull request #533 from badsyntax/login-form-focus
Focus on fields in the login form
2015-08-27 16:20:23 -04:00
Richard Willis
f26c0b71d2 Focus on fields in the login form
This just makes life a little easier...

Squashed the following commits:

* Use $.trim() for better browser support
2015-08-27 22:17:13 +02:00
Joshua Tauberer
b2dfdc386a Merge pull request #528 from phareous/master
Allow global sieve scripts for before or after user sieve scripts. Th…
2015-08-26 18:03:47 -04:00
Michael Long
732a6922de Allow global sieve scripts for before or after user sieve scripts. This allows defining custom system-wide sieve rules. 2015-08-24 19:55:34 -04:00
Joshua Tauberer
9501a2209e Merge pull request #526 from nstanke/v0.13_readme
v0.13a README
2015-08-24 15:27:15 -04:00
Norman Stanke
8c83171a28 v0.13a 2015-08-24 21:21:52 +02:00
Joshua Tauberer
289936db7a 0.13a (August 23, 2015)
Work-around for ownCloud 8.1.1 upgrade bug and tweaking munin's setup.

v0.13a (August 23, 2015)
------------------------

Note: v0.13 (no 'a', August 19, 2015) was pulled immediately due to an ownCloud bug that prevented upgrades. v0.13a works around that problem.

Mail:

* Outbound mail headers (the Recieved: header) are tweaked to possibly improve deliverability.
* Some MIME messages would hang Roundcube due to a missing package.
* The users permitted to send as an alias can now be different from where an alias forwards to.

DNS:

* The secondary nameservers option in the control panel now accepts more than one nameserver and a special xfr:IP format to specify zone-transfer-only IP addresses.
* A TLSA record is added for HTTPS for DNSSEC-aware clients that support it.

System:

* Backups can now be turned off, or stored in Amazon S3, through new control panel options.
* Munin was not working on machines confused about their hostname and had lots of errors related to PANGO, NTP peers and network interfaces that were not up.
* ownCloud updated to version 8.1.1 (with upgrade work-around), its memcached caching enabled.
* When upgrading, network checks like blocked port 25 are now skipped.
* Tweaks to the intrusion detection rules for IMAP.
* Mail-in-a-Box's setup is a lot quieter, hiding lots of irrelevant messages.

Control panel:

* SSL certificate checks were failing on OVH/OpenVZ servers due to missing /dev/stdin.
* Improve the sort order of the domains in the status checks.
* Some links in the control panel were only working in Chrome.
2015-08-23 12:52:43 -04:00
Joshua Tauberer
6c71abbb09 suppress PANGO warning when running munin during setup 2015-08-23 12:42:39 -04:00
Joshua Tauberer
841181ed55 ownCloud 8.1.1 broke upgrades: do "occ upgrade" twice
Per https://github.com/owncloud/core/issues/18224 and https://discourse.mailinabox.email/t/v13-upgrade-issue-with-calendar/757/10, upgrades from anything to ownCloud 8.1.1 were broken. But the workaround is to run the upgrade step twice.
2015-08-23 12:37:25 -04:00
Joshua Tauberer
c3821e9165 munin: configure the plugins better
* let munin choose what it wants to monitor, it may need an update periodically
* deactivate monitoring of NTP peers (this makes no sense)
* deactivate monitoring of network interfaces that aren't up (also makes no sense)
* we get errors in munin-node.log if we don't create /var/lib/munin-node/plugin-state/
2015-08-23 16:04:37 +00:00
derekrspencer
d9a4085efa Fix antispam-plugin config problem in #520
The antispam-plugin configuration is not ignoring deleted email (mail sent to a trash folder). This causes a big problem because if someone decides to "clean up" their Spam folder by deleting all of the emails, then sa-learn --ham is run on what is usually correctly caught spam messages. This causes big problems with the accuracy of the bayes scoring! It should really only be learning ham if someone drags the email to a non-Trash folder.

I ran "sa-learn --dbpath /home/user-data/mail/spamassassin --dump magic" before these changes and saw the nham counter increment with every message deleted from Spam.  With this new config sa-learn --ham is not run when email is deleted from Spam so the bayes database is not incorrectly trained.
2015-08-19 21:50:32 -07:00
Joshua Tauberer
6b35d8402c pulling v0.13 2015-08-19 17:58:47 -04:00
Joshua Tauberer
3b6f7250ee v0.13
v0.13 (August 19, 2015)
-----------------------

Mail:

* Outbound mail headers (the Recieved: header) are tweaked to possibly improve deliverability.
* Some MIME messages would hang Roundcube due to a missing package.
* The users permitted to send as an alias can now be different from where an alias forwards to.

DNS:

* The secondary nameservers option in the control panel now accepts more than one nameserver and a special xfr:IP format to specify zone-transfer-only IP addresses.
* A TLSA record is added for HTTPS for DNSSEC-aware clients that support it.

System:

* Backups can now be turned off, or stored in Amazon S3, through new control panel options.
* Munin was not working on machines confused about their hostname.
* ownCloud updated to version 8.1.1, its memcached caching enabled.
* When upgrading, network checks like blocked port 25 are now skipped.
* Tweaks to the intrusion detection rules for IMAP.
* Mail-in-a-Box's setup is a lot quieter, hiding lots of irrelevant messages.

Control panel:

* SSL certificate checks were failing on OVH/OpenVZ servers due to missing /dev/stdin.
* Improve the sort order of the domains in the status checks.
* Some links in the control panel were only working in Chrome.
2015-08-19 16:37:18 -04:00
Joshua Tauberer
c6bf061b26 changelog entries 2015-08-19 16:34:39 -04:00
Joshua Tauberer
a8074ae3e4 suppress some status output regarding new automatic aliases on first installation 2015-08-19 16:30:32 -04:00
Joshua Tauberer
cfc4e6b48b automatic administrator aliases are probably not bidirectional because the administrator@ address is an alias and not a user 2015-08-19 16:06:09 -04:00
Joshua Tauberer
73fbcd7fa3 silence all of the installing/already installed package messages on installation
Querying dpkg for each package is slow, and we have way too much output on installation because of it.
2015-08-19 15:58:35 -04:00
Joshua Tauberer
79317ed67e dont run ownCloud's upgrade before it's been installed to remove a noisy message 2015-08-19 15:43:34 -04:00
Joshua Tauberer
1b26632bb4 readme tweaks 2015-08-17 08:20:31 -04:00
Joshua Tauberer
5b415c6895 tweak security.md for new alias permitted_senders controls 2015-08-17 08:18:32 -04:00
Joshua Tauberer
078f3bff70 ownCloud set overwrite.cli.url setting 2015-08-16 23:47:51 +00:00
Joshua Tauberer
d02f800d1b ownCloud: enable memcached properly 2015-08-16 23:46:23 +00:00
Joshua Tauberer
83d3643e8f changelog entries 2015-08-15 15:58:27 +00:00
Joshua Tauberer
6d35ae56d3 Merge pull request #517 from fxaguessy/master
fix problem with certificate verification on OpenVZ servers
2015-08-15 11:55:30 -04:00
root
39270a8e35 fix problem with certificate verification on OpenVZ servers 2015-08-15 17:32:40 +02:00
Joshua Tauberer
8c08f957cd bidirectional alias controls: a new permitted_senders column in the aliases table allows setting who can send as an address independently of where the address forwards to
But the default permitted senders are the same as the addresses the alias forwards to.

Merge branch 'dhpiggott-bidirectional-alias-controls'
2015-08-14 23:09:22 +00:00
Joshua Tauberer
5924d0fe0d various cleanup related to the new permitted_senders column for aliases 2015-08-14 23:05:08 +00:00
Joshua Tauberer
c23a34d475 tweak README 2015-08-14 17:11:32 -04:00
Joshua Tauberer
1bd3a725ca use the right version of the ownCloud apps
Contacts and Calendar are now versioned separately from ownCloud core.

Contacts appears to be in a v0.4.x.x series. This is the hash of v0.4.0.0.

Per https://github.com/owncloud/calendar/issues/892, Calendar is in a 0.7.x series. This is the hash for 0.7.2.
2015-08-12 19:05:55 +00:00
Joshua Tauberer
848dea83ab additional error handling for backups with an invalid target 2015-08-12 11:19:59 +00:00
Joshua Tauberer
bded529535 changelog entries 2015-08-12 11:10:36 +00:00
Joshua Tauberer
ec665a97db ownCloud 8.1.1
Only core. The apps don't seem to have been updated.
2015-08-12 11:07:30 +00:00
Joshua Tauberer
fc536b6ff3 Merge pull request #512 from ponychicken/backup-fixes
S3 backups fail if no prefix is used
2015-08-11 07:59:07 -04:00
Leo Koppelkamm
f96bef43cc If no prefix is specified, set the path to '', otherwise boto won't list the files 2015-08-11 13:54:30 +02:00
Joshua Tauberer
f4e8ee0af9 html errors in the backup template, my bad 2015-08-09 20:34:08 +00:00
Joshua Tauberer
c2e5f5cb2b merge - duplicity configuration in the control panel and disabling backups
Merges branch 'ponychicken-backup'.
2015-08-09 20:16:50 +00:00
Joshua Tauberer
9ca116d545 add an option to disable backups 2015-08-09 20:15:43 +00:00
Joshua Tauberer
cdd3a64638 after-backup was run with the wrong environment 2015-08-09 20:08:33 +00:00
Joshua Tauberer
99e51f8a52 use boto to get actual file sizes of backup files when S3 is used 2015-08-09 20:08:33 +00:00
Joshua Tauberer
3b4b57c081 switching between backup options in the admin wasn't working at all
* going from s3 to file target wasn't working
* use 'local' in the config instead of a file: url, for the local target, so it is not path-specific
* break out the S3 fields since users can't be expected to know how to form a URL
* use boto to generate a list of S3 hosts
* use boto to validate that the user input for s3 is valid
* fix lots of html errors in the backup admin
2015-08-09 20:08:33 +00:00
Joshua Tauberer
c7f8ead496 clean up the new backup configuration panel 2015-08-09 20:08:30 +00:00
Joshua Tauberer
3f15879578 remove global variables in backup.py 2015-08-09 17:54:46 +00:00
Leo Koppelkamm
1cdd205eb7 Missed one max_age 2015-07-28 20:58:39 +02:00
Leo Koppelkamm
77099b3bce Reword backup min_time label 2015-07-28 00:42:00 +02:00
Leo Koppelkamm
0d8a4099c1 Add placeholder attribute; use input instead of textarea 2015-07-28 00:37:48 +02:00
Leo Koppelkamm
606cf6a941 Fix API typo 2015-07-28 00:34:26 +02:00
Leo Koppelkamm
ba9065cada Don't write collection_status output to file but parse it directly 2015-07-27 22:30:22 +02:00
Leo Koppelkamm
e693802091 Rename max_age to min_age
Also clarify a comment and remove an unneeded type check
2015-07-27 22:18:19 +02:00
Leo Koppelkamm
fa0dd684da Add archive-dir argument to collection-status 2015-07-27 22:13:28 +02:00
Leo Koppelkamm
43fb7fe635 Remove unused variable 2015-07-27 22:11:43 +02:00
Leo Koppelkamm
91e4ea6e2f Infer target_type from url 2015-07-27 22:09:58 +02:00
Leo Koppelkamm
1e3e34f15f Make backup API RESTful 2015-07-27 22:00:36 +02:00
Leo Koppelkamm
96fb0f78f7 Add comment regarding the use of pip instead of pip3 2015-07-27 21:56:08 +02:00
Leo Koppelkamm
2e6c410336 Make backups more configurable
Backup location and maximum age can now be configured in the admin panel.
For now only S3 is supported, but adding other duplicity supported backends should be straightforward.
2015-07-27 21:53:34 +02:00
Joshua Tauberer
0293e04311 fix control panel links, broken in Firefox (worked in Chrome)
see https://discourse.mailinabox.email/t/bug-present-for-ages/694/3
2015-07-25 14:12:45 +00:00
Joshua Tauberer
8c066764d9 munin-node gets confused about the machine's name in some cases
Some users report munin is broken because munin and munin-node disagree about the name of the machine. I think this occurs if hostname (used by munin-node) reports a different name than PRIMARY_HOSTNAME (which we put in the munin config).

Hard-code PRIMARY_HOSTNAME in munin-node.conf.

Fixes #474.
See https://discourse.mailinabox.email/t/404-not-found-on-admin-munin/623/24.
2015-07-22 21:03:55 +00:00
Joshua Tauberer
1900e512f2 improve the sort order of domains - siblings to the primary hostname were not sorted right 2015-07-21 11:25:11 +00:00
Joshua Tauberer
d0ccde7b48 changelog entries 2015-07-21 06:50:00 -04:00
Joshua Tauberer
1e261e347a missing dependency php-mail-mimedecode for roundcube, fixes #447 2015-07-21 10:25:10 +00:00
Joshua Tauberer
2cb4cdc645 dont run network checks during upgrades since this is a bad reason to block an upgrade from going through 2015-07-21 06:21:56 -04:00
David Piggott
123ac4fd33 s/email/address/ in aliases UI variable names
This makes the frontend consistent with the backend.
2015-07-20 12:51:57 +01:00
David Piggott
423bb8e317 Fix remove-alias button breakage 2015-07-20 12:51:57 +01:00
David Piggott
e6ff280984 Store and set alias receivers and senders separately for maximum control 2015-07-20 12:51:57 +01:00
David Piggott
3fdfad27cd Add support for bidirectional mail alias controls
This is an extension of #427. Building on that change it adds support in the
aliases table for flagging aliases as:
 1. Applicable to inbound and outbound mail.
 2. Applicable to inbound mail only.
 3. Applicable to outbound mail only.
 4. Disabled.

The aliases UI is also updated to allow administrators to set the direction of
each alias.

Using this extra information, the sqlite queries executed by Postfix are
updated so only the relevant alias types are checked.

The goal and result of this change is that outbound-only catch-all aliases can
now be defined (in fact catch-all aliases of any type can be defined).

This allow us to continue supporting relaying as described at
https://mailinabox.email/advanced-configuration.html#relay
without requiring that administrators either create regular aliases for each
outbound *relay* address, or that they create a catch-all alias and then face a
flood of spam.

I have tested the code as it is in this commit and fixed every issue I found,
so in that regard the change is complete. However I see room for improvement
in terms of updating terminology to make the UI etc. easier to understand.
I'll make those changes as subsequent commits so that this tested checkpoint is
not lost, but also so they can be rejected independently of the actual change
if not wanted.
2015-07-20 12:51:57 +01:00
Joshua Tauberer
d3bbc0ec95 bug in new secondary nameservers
forgot a 'continue' statement
see 216acb0eeb
fixes #497
2015-07-20 11:25:16 +00:00
Joshua Tauberer
e54608c282 fix occ upgrade to not bail when occ returns 'ownCloud is already latest version' exit code 3, see #496 2015-07-19 13:06:38 +00:00
Joshua Tauberer
9b9a40ddd7 v0.12c
remove live dependency on Sourceforge
 -----BEGIN PGP SIGNATURE-----
 Version: GnuPG v1
 
 iQEcBAABAgAGBQJVq5kxAAoJELkgQfTBC92B9uMIALcrGjq7weaL3qRHYRoeVs5C
 /Ov1Lg9QY7PGRl3HtBmFvw50E3coxFCFBfEycK0D9Rue6xF2PHyg8n0DvX5Q2wSD
 A9EWAv27ZPoup8/ggv970lTZSpJzseJs1Km0QeOaapfgzPFFtDDwUbkV8sHQxXi4
 KCFzmlE72rmvsley/u3IlS/dCb07QdLhdIa/ZJYxSIMJdvMqj0enefBOELoeomYC
 ZoNzzzB08eCiyTVd6BTFPBz6CWI6yW203JWoQsSjaz9qEB/N6m9u/PrHBT8VPIRM
 Q/a4gn598eAzcGEjub3ZYmJlnbBSlhvczfljmYgNcgizy/SwByaA1AaAemdwI5s=
 =2FnK
 -----END PGP SIGNATURE-----

Merge tag 'v0.12c'

v0.12c

remove live dependency on Sourceforge

everything was already on master
2015-07-19 08:34:16 -04:00
Joshua Tauberer
1b00184c89 v0.12c release to work-around Sourceforge outage 2015-07-19 08:30:03 -04:00
Joshua Tauberer
e11825392d use a temporary mirror for roundcube while Sourceforge is recovering from an outage https://twitter.com/sfnet_ops/status/622171668497076224 2015-07-19 08:25:04 -04:00
Joshua Tauberer
1a995d9e26 forgot to create the pyzor home_dir in 3f606feea3 2015-07-19 08:25:04 -04:00
Joshua Tauberer
53d4820d74 hard-code pyzor sevice URL because 'pyzor discover' is failing because Sourceforge is offline, fixes #496 2015-07-19 08:25:04 -04:00
Joshua Tauberer
40a5fa46d1 use a temporary mirror for roundcube while Sourceforge is recovering from an outage https://twitter.com/sfnet_ops/status/622171668497076224 2015-07-17 20:27:59 -04:00
Joshua Tauberer
05e33edb0d forgot to create the pyzor home_dir in 3f606feea3 2015-07-17 20:26:36 -04:00
Joshua Tauberer
76dba1a521 the ownCloud upgrade must be run after apps are (re-)enabled after an upgrade 2015-07-17 11:44:28 +00:00
Joshua Tauberer
f7298a45bd update to ownCloud 8.1.0 2015-07-17 11:44:28 +00:00
Joshua Tauberer
3f606feea3 hard-code pyzor sevice URL because 'pyzor discover' is failing because Sourceforge is offline, fixes #496 2015-07-17 11:44:28 +00:00
Joshua Tauberer
541d9252f6 allow PEM files to have non-Unix line endings 2015-07-17 11:44:28 +00:00
Joshua Tauberer
cbbbb117e0 Merge pull request #492 from PortableTech/tlsa
Add TLSA record for SSL connections.
2015-07-13 09:15:12 -04:00
PortableTech
415f95b792 Add TLSA record for HTTPS connections.
While not widely supported, there are some browser addons that can
validate DNSSEC and TLSA for additional out-of-band verification of
certificates when browsing the web.  Costs nothing to implement and
might improve security in some situations.
2015-07-13 09:12:13 -04:00
Joshua Tauberer
5f17abc856 Merge pull request #463 from PortableTech/master
outgoing_mail_header_filters use local hostname and ip
2015-07-11 17:21:55 -04:00
Joshua Tauberer
a91995f0a7 Merge pull request #486 from anoma/fail2ban-organise
Optimise FAIL2BAN jail.local
2015-07-11 17:20:34 -04:00
Joshua Tauberer
216acb0eeb merge #476 - multiple secondary NS servers and zone xfr-only servers
closes #476
2015-07-10 15:42:44 +00:00
Joshua Tauberer
5dd5fc4a1c clean up multiple secondary nameservers and zone xfr ip addresses 2015-07-10 15:42:33 +00:00
Brian Bustin
09133c8f59 Initial backend changes to make it possible to have one or more secondary name servers 2015-07-10 14:59:38 +00:00
Joshua Tauberer
d08a3095a9 tweak security.md 2015-07-09 13:30:25 -04:00
anoma
593fd242bf Activate FAIL2BAN recidive jail
Recidive can be thought of as FAIL2BAN checking itself. This setup will monitor the FAIL2BAN log and if 10 bans are seen within one day activate a week long ban and email the mail in a box admin that it has been applied . These bans survive FAIL2BAN service restarts so are much stronger which obviously means we need to be careful with them.

Our current settings are relatively safe and definitely not easy to trigger by mistake e.g to activate a recidive IP jail by failed SSH logins a user would have to fail logging into SSH  6 times in 10 minutes, get banned, wait for the ban to expire and then repeat this process 9 further times within a 24 hour period.

The default maxretry of 5 is much saner but that can be applied once users are happy with this jail. I have been running a stronger version of this for months and it does a very good job of ejecting persistent abusers.
2015-07-07 12:37:42 +01:00
anoma
e591d9082f Ultra safe dovecot findtime and maxretry settings
Explicitly set the timings and counts for the dovecot jail rather than change the global [DEFAULT] and inherit it for this one jail. These settings are far too safe so a future PR should increase security here.
2015-07-06 13:44:53 +01:00
anoma
b6f26c0f1e Revert to defaults FAIL2BAN findtime and maxretry
Reverts the remaining FAIL2BAN settings to default: findtime 600 and maxretry 3. As jail settings override default settings this was hardly being used anyway so it is better to explicitly set it per jail as and when required.
2015-07-06 13:42:41 +01:00
Joshua Tauberer
34b7638342 v0.12b 2015-07-04 11:31:51 -04:00
Joshua Tauberer
acd91665b5 setting an alias to forward to two or more addresses was broken since aa33428311
fixes #482
2015-07-04 15:28:45 +00:00
Joshua Tauberer
b503ea1cf7 v0.12
--------------------

This is a minor update to v0.11, which was a major update. Please read v0.11's advisories.

* The administrator@ alias was incorrectly created starting with v0.11. If your first install was v0.11, check that the administrator@ alias forwards mail to you.
* Intrusion detection rules (fail2ban) are relaxed (i.e. less is blocked).
* SSL certificates could not be installed for the new automatic 'www.' redirect domains.
* PHP's default character encoding is changed from no default to UTF8. The effect of this change is unclear but should prevent possible future text conversion issues.
* User-installed SSL private keys in the BEGIN PRIVATE KEY format were not accepted.
* SSL certificates with SAN domains with IDNA encoding were broken in v0.11.
* Some IDNA functionality was using IDNA 2003 rather than IDNA 2008.
2015-07-03 10:34:33 -04:00
Joshua Tauberer
091c2e45bf always attempt to upgrade pip packages during setup 2015-07-03 14:25:41 +00:00
Joshua Tauberer
0a78d1d2fa update changelog 2015-07-03 14:15:14 +00:00
Joshua Tauberer
ff4780d5fb better error handling of invalid PEM files 2015-07-03 14:00:59 +00:00
PortableTech
07beef3db2 outgoing_mail_header_filters use local hostname and ip
Modify outgoing_mail_header_filters and mail-postfix.sh
files to result in the primary hostname, and the public
ip of the server showing in the first mail header route
instead of unknown and 127.0.0.1.  This could help lower
the spam score of mail sent from your server to some
public mail services.
2015-07-02 16:04:56 -04:00
Joshua Tauberer
0924f8ca7a allow for PEM private keys in the 'BEGIN PRIVATE KEY' format too
see https://discourse.mailinabox.email/t/another-upgrade-failure/630/5
2015-07-02 15:37:26 -04:00
Joshua Tauberer
6302aa6c12 Merge pull request #479 from hnk/patch-1
update docstring to clarify usage of -c option
2015-07-02 13:44:03 -04:00
Hnk Reno
da4d9ff607 update docstring to clarify usage of -c option 2015-07-02 19:27:05 +02:00
kri3v
dd0bdef640 Added more bantime and lowered max retry attempts
Ban time was too low for preventing ssh brute force attacks, this change also allows to keep the auth.log more clean and avoid wasting cpu and i/o on this. 

Bots eventually will flag your IP as secure and move along.
2015-07-02 12:55:43 -03:00
Joshua Tauberer
e57e08088a the control panel would not allow installing a certificate for a www redirect domain, fixes #475 2015-07-02 10:53:54 +00:00
Joshua Tauberer
5e43c394d5 Merge pull request #477 from anoma/master
cleanup and harden of fail2ban
2015-07-02 06:22:57 -04:00
anoma
b2eaaeca4b Revert to default 6 ssh/ddos login attempts
No legitimate admin will require 20 login attempts. The default 6 is a sane middle ground especially since in 10 minutes they can try again  or immediately from another IP anyway.
2015-07-02 10:23:48 +01:00
anoma
e2d9a523c3 Cleanup blank lines, comments and whitespace to make it easier to follow 2015-07-02 10:19:37 +01:00
anoma
11df1e4680 Unnecessary config item, inherited from default jail.conf 2015-07-02 10:10:50 +01:00
anoma
53d5542402 Revert to default 600 second ban time
A 60 second/1 minute ban time is not long enough to counter brute force attacks which is the main purpose of fail2ban for mail in a box. The default bantime of 10 minutes is still sane and I think we have proven fail2ban is reliable enough not to cause problems in general. It is not worth sacrificing security for the rare case where an admin locks themselves out for 10 minutes.
2015-07-02 10:08:50 +01:00
anoma
bfda3f40b9 Unnecessary config item, inherited from default jail.conf 2015-07-02 09:55:59 +01:00
Joshua Tauberer
c0ddceb2bd Merge pull request #471 from hnk/patch-1
Set PHPs default charset to UTF-8, since we use it. Closes #367.
2015-06-30 12:00:27 -04:00
Joshua Tauberer
42a506231b don't automatically create the administrator@ alias (e.g. on first user creation) because we dont know what it should be an alias to (leave this to be resolved manually), fixes #470
Was broken by 462a79cf47.
2015-06-30 09:16:22 -04:00
Joshua Tauberer
e3252f53da idna domains in certificate subject alternative names were not handled correctly after switching to cryptography package 2015-06-30 13:09:18 +00:00
Joshua Tauberer
aa33428311 some IDNA functionality was still using Python's built-in IDNA 2003 encoder rather than the idna package's IDNA 2008 encoder 2015-06-30 13:09:18 +00:00
Hnk Reno
ca5d228be6 Set PHPs default charset to UTF-8, since we use it. Closes #367. 2015-06-30 11:31:43 +02:00
Joshua Tauberer
f89a98c78a v0.11b to fix missing package for apt-add-repository 2015-06-29 21:52:47 -04:00
Joshua Tauberer
a3087d8815 must install software-properties-common to have add-apt-repository 2015-06-29 21:47:54 -04:00
Joshua Tauberer
23d2df7a93 v0.11
---------------------

Advisories:
* Users can no longer spoof arbitrary email addresses in outbound mail. When sending mail, the email address configured in your mail client must match the SMTP login username being used, or the email address must be an alias with the SMTP login username listed as one of the alias's targets.
* This update replaces your DKIM signing key with a stronger key. Because of DNS caching/propagation, mail sent within a few hours after this update could be marked as spam by recipients. If you use External DNS, you will need to update your DNS records.
* The box will now install software from a new Mail-in-a-Box PPA on Launchpad.net, where we are distributing two of our own packages: a patched postgrey and dovecot-lucene.

Mail:
* Greylisting will now let some reputable senders pass through immediately.
* Searching mail (via IMAP) will now be much faster using the dovecot lucene full text search plugin.
* Users can no longer spoof arbitrary email addresses in outbound mail (see above).
* Fix for deleting admin@ and postmaster@ addresses.
* Roundcube is updated to version 1.1.2, plugins updated.
* Exchange/ActiveSync autoconfiguration was not working on all devices (e.g. iPhone) because of a case-sensitive URL.
* The DKIM signing key has been increased to 2048 bits, from 1024, replacing the existing key.

Web:
* 'www' subdomains now automatically redirect to their parent domain (but you'll need to install an SSL certificate).
* OCSP no longer uses Google Public DNS.
* The installed PHP version is no longer exposed through HTTP response headers, for better security.

DNS:
* Default IPv6 AAAA records were missing since version 0.09.

Control panel:
* Resetting a user's password now forces them to log in again everywhere.
* Status checks were not working if an ssh server was not installed.
* SSL certificate validation now uses the Python cryptography module in some places where openssl was used.
* There is a new tab to show the installed version of Mail-in-a-Box and to fetch the latest released version.

System:
* The munin system monitoring tool is now installed and accessible at /admin/munin.
* ownCloud updated to version 8.0.4. The ownCloud installation step now is reslient to download problems. The ownCloud configuration file is now stored in STORAGE_ROOT to fix loss of data when moving STORAGE_ROOT to a new machine.
* The setup scripts now run `apt-get update` prior to installing anything to ensure the apt database is in sync with the packages actually available.
2015-06-29 20:58:35 -04:00
Joshua Tauberer
1cd97d46a2 press hit 2015-06-27 10:10:33 -04:00
Joshua Tauberer
53f84a8092 set ssl_stapling_verify back to on, reverts part of 47de93961e
The sslmate guidance changed. See #458.
2015-06-27 07:14:16 -04:00
Joshua Tauberer
6441de63ba typo in security.md 2015-06-26 11:38:40 -04:00
Joshua Tauberer
b2553aea33 note the new sender email address spoofing restriction at the top of the changelog 2015-06-26 11:36:10 -04:00
Joshua Tauberer
5ef1cfbdc7 forgot new version.html template file 2015-06-25 17:43:50 +00:00
Joshua Tauberer
7527b4dc27 show the Mail-in-a-Box version in the control panel and a button to ping the MiaB website for the latest version
fixes #441
2015-06-25 13:43:11 +00:00
Joshua Tauberer
1367816b04 merge #451 - Increase DKIM key length to 2048 2015-06-25 13:07:29 +00:00
Joshua Tauberer
299a2315c1 dkim 2048 bits - migration and zone file generation changes
* Add a migration to delete any existing DKIM key so that existing machines get a fresh 2048-bit key. (Sadly we don't support key rotation so the change is immediate.)
* Because the DNS record for a 2048-bit key is so much longer, the way we read OpenDKIM's DNS record text file had to be modified to combine an arbitrary number of TXT record quoted ("...") strings.
* When writing out the TXT record value, the string must be split into quoted ("...") strings with a maximum length of 255 bytes each, per the DNS spec.
* Added a changelog entry.
2015-06-25 13:06:29 +00:00
Joshua Tauberer
9a6aea6940 changelog entry for z-push autodiscover case insensitivity 2015-06-25 12:32:07 +00:00
Joshua Tauberer
98cd04cccf Merge pull request #452 from m4rcs/master
Z-Push autoconfiguration fails due to URL case sensitivity
2015-06-25 08:28:44 -04:00
Marc Schiller
0cc20cbb97 Fixed a bug where autoconfiguration for Z-Push fails due to case of URL. 2015-06-25 11:56:33 +02:00
PortableTech
ef6a17d4a6 Increase DKIM key length to 2048
Currently MiaB creates 1024 bit keys which is seen as a minimum standard
by several providers such as Google who already uses a 2048 bit key.
Increasing the keysize beyond 2048 is an issue as it often goes beyond
supported DNS record sizes.
2015-06-24 18:49:19 -04:00
Joshua Tauberer
17a149947a other CHANGELOG updates 2015-06-24 18:16:25 -04:00
Joshua Tauberer
a2c50ae967 note the new SMTP mail from restriction in the changelog and security guide 2015-06-24 18:12:41 -04:00
Joshua Tauberer
13958ba4df Merge pull request #427 from pichak/add-sender-login-mismatch
Reject outgoing mail if MAIL FROM (envelope sender) does not match login name or is not an alias that directs mail (directly) to login name.
2015-06-24 18:03:03 -04:00
Joshua Tauberer
8eb71483f3 Merge pull request #450 from agriffaut/patch-1
ownCloud breaks if download fails (Issue #449)
2015-06-24 08:11:30 -04:00
aLeX
d8e30883fa Issue #449
If the downloaded file doesn't pass hash verification, the script exits and leaves a broken system
Just make hash verification before moving owncloud directory
2015-06-24 14:06:01 +02:00
Joshua Tauberer
47acbbf332 bump to latest version of my email_validator library 2015-06-23 16:43:35 -04:00
Joshua Tauberer
dece359c90 validate certificates using the cryptography python package as much as possible, shelling out to openssl just once instead of four times per certificate
* Use `cryptography` instead of parsing openssl's output.
* When checking if we can reuse the primary domain certificate or a www-parent-domain certificate for a domain, avoid shelling out to openssl entirely.
2015-06-21 14:53:37 +00:00
Joshua Tauberer
6a9eb4e367 improve inline documentation for the virtual-alias-maps query 2015-06-21 08:22:33 -04:00
Morteza Milani
fc03ce9b2f Fix login map. Now includes both emails and aliases 2015-06-20 03:27:18 -07:00
Toilal
ce17c12ca2 Use netcat to check if mailinabox webservice is available
[JT added installing netcat-openbsd in system.sh]
2015-06-18 08:04:46 -04:00
Joshua Tauberer
5edaeb8c7b add a new autoconfiguration option PRIMARY_HOSTNAME=auto to simply grab the hostname from reverse DNS
drawn from 5b23a06a74.
2015-06-18 07:46:09 -04:00
Joshua Tauberer
3a28d1b073 showing the Mail-in-a-Box version using git describe was broken since dd6a8d99 2015-06-18 07:45:55 -04:00
Joshua Tauberer
6f2226bfcd move more of start.sh into questions.sh to keep start.sh cleaner and encapsulate all of the variable setting in a single script
Based on 5b23a06a74.
2015-06-18 07:38:18 -04:00
Joshua Tauberer
97cd4c64ad don't expose PHP version in the X-Powered-By header, closes #439, fixes #433 2015-06-18 11:12:03 +00:00
Joshua Tauberer
43d50d0667 Merge pull request #445 from bizonix/patch-1
fix wrong redirect for automatic www subdomain redirects
2015-06-18 07:05:01 -04:00
Joshua Tauberer
6258a7f311 status checks were broken if sshd was not present, fixes #444 2015-06-18 11:01:11 +00:00
Joshua Tauberer
ab36cc8968 whitespace=>tabs 2015-06-18 10:54:51 +00:00
bizonix
33b71c6b3c fix wrong redirect
$ curl -I https://www.site.co.il/static/images/1.png?a=b | grep Location
Location: https://site.co.il?a=b
but should be something like 
Location: https://site.co.il/static/images/1.png?a=b
2015-06-18 01:48:15 +03:00
Joshua Tauberer
34e821c102 Roundcube 1.1.2 2015-06-17 11:00:15 +00:00
Joshua Tauberer
2af557139d default IPv6 AAAA records were missing
This was broken by the ability to have multiple TXT records in 9f1d633ae4.
2015-06-17 06:47:22 -04:00
Joshua Tauberer
9e0dcd8718 security.md: add a section on DNSSEC specifically 2015-06-15 10:24:16 -04:00
Joshua Tauberer
be2b5a62de ownCloud updated to version 8.0.4 2015-06-14 16:04:07 +00:00
Joshua Tauberer
0cbba71c72 merge #429 - Move OwnCloud's config to Storage Root 2015-06-14 15:48:09 +00:00
Joshua Tauberer
d28563fb45 tweak the ownCloud config location migration (no need for third ln) 2015-06-14 15:42:32 +00:00
Norman Stanke
38632f0f90 Move OwnCloud's config to Storage Root 2015-06-12 14:53:02 +02:00
Joshua Tauberer
0754ce01b1 questions.sh needs to apt-get update before it does an apt-get install, see #431, see #438 2015-06-10 09:43:22 -04:00
Joshua Tauberer
1ef455d37d bootstrap.sh needs to apt-get update before it does an apt-get install, fixes #431 2015-06-10 09:33:47 -04:00
Joshua Tauberer
d152603abd changelog entries and mention our forks of postgrey and dovecot in the README 2015-06-10 09:27:29 -04:00
Joshua Tauberer
9e125aec00 Merge pull request #436 from bizonix/patch-1
fix loop redirecting
2015-06-07 16:30:58 -04:00
bizonix
2c90c267bd fix loop redirecting
server is redirecting the request for this address in a way that will never complete
2015-06-07 21:50:41 +03:00
Joshua Tauberer
47de93961e OCSP improvements
* Set ssl_stapling_verify to off per https://sslmate.com/blog/post/ocsp_stapling_in_apache_and_nginx ('on' has no security benefits).
* Set resolver to 127.0.0.1, instead of Google Public DNS, because we might as well use our local nameserver anyway.
* Remove the commented line which per the link above would never be necessary anyway.

OCSP seems to work just fine after these changes.
2015-06-06 23:24:09 +00:00
Joshua Tauberer
1990f32ca4 typo, fixes #435 2015-06-06 13:22:50 +00:00
Joshua Tauberer
807939c0e4 make the +tag address tips clearer 2015-06-06 13:02:23 +00:00
Joshua Tauberer
a1c7bf0883 add munin to readme 2015-06-06 12:55:13 +00:00
Joshua Tauberer
5008cc603e merge - munin system monitoring 2015-06-06 12:52:22 +00:00
Joshua Tauberer
9857db96cd add a link to the /admin/munin page from the control panel nav bar 2015-06-06 12:52:16 +00:00
Joshua Tauberer
e9e6d94e3b the control panel auth hmac message should also include the user's password so that resetting a password in the database forces that user to log in to the control panel again; also use a sha256 hmac 2015-06-06 12:38:19 +00:00
Joshua Tauberer
462a79cf47 fix what counts as a required alias, fixes #434 2015-06-06 12:12:10 +00:00
Joshua Tauberer
f792deeebd when the undocumented custom web settings has a redirect or proxy at the root of a domain, use a minimal nginx config template (same as the new default www redirects) 2015-06-04 12:32:00 +00:00
Joshua Tauberer
95173bb327 provide redirects from www subdomains of zones to their parent domain
* Split the nginx templates again so we have just the part needed to make a domain do a redirect separate from the rest.
* Add server blocks to the nginx config for these domains.
* List these domains in the SSL certificate install admin panel.
* Generate default 'www' records just for domains we provide default redirects for.

Fixes #321.
2015-06-04 12:19:01 +00:00
Joshua Tauberer
1d09e2406b refactor how the nginx config file is assembled
This doesn't change anything. Just preparation for the next commit.
2015-06-04 12:19:01 +00:00
Joshua Tauberer
c9add7a8bf if a user sets a custom A record on PRIMARY_HOSTNAME, which is ignored anyway, don't let that cause PRIMARY_HOSTNAME from being dropped from nginx.conf
Could be related to https://discourse.mailinabox.email/t/nginx-lost-admin-record-after-install-ssl-cert-problem/528.
2015-06-04 12:19:01 +00:00
Joshua Tauberer
e4caed9277 add a note in the setup script about the use of our postgrey fork and dnswl's license terms 2015-06-03 16:28:20 -04:00
Joshua Tauberer
1760eaa601 merge #406 - dovecot-lucene & packaging 2015-06-03 15:51:16 -04:00
Joshua Tauberer
b25ce67fe1 bring the postgrey patches into this repository rather than maintaining them in a separate postgrey fork repository 2015-06-03 15:50:25 -04:00
Joshua Tauberer
b23ba6f75e simplify build/setup of dovecot-lucene package 2015-06-03 15:48:35 -04:00
Morteza Milani
cf904a05cc Reject outgoing mail if FROM does not match Login 2015-06-01 21:26:01 -07:00
Joshua Tauberer
47a5a44b9e v0.10
* SMTP Submission (port 587) began offering the insecure SSLv3 protocol due to a misconfiguration in the previous version.
* Roundcube now allows persistent logins using Roundcube-Persistent-Login-Plugin.
* ownCloud is updated to version 8.0.3.
* SPF records for non-mail domains were tightened.
* The minimum greylisting delay has been reduced from 5 minutes to 3 minutes.
* Users and aliases weren't working if they were entered with any uppercase letters. Now only lowercase is allowed.
* After installing an SSL certificate from the control panel, the page wasn't being refreshed.
* Backups broke if the box's hostname was changed after installation.
* Dotfiles (i.e. .svn) stored in ownCloud Files were not accessible from ownCloud's mobile/desktop clients.
* Fix broken install on OVH VPS's.
2015-06-01 18:05:41 -04:00
Joshua Tauberer
a0e6c7ceb6 fix downloading dotfiles through ownCloud's webdav
fixes #414
2015-05-30 18:03:37 +00:00
Joshua Tauberer
49aa367ffa merge #422 - Add persistent login functionality to roundcube 2015-05-30 14:07:50 +00:00
Joshua Tauberer
83b36f2c3a simplify the roundcube updating logic, changelog entry for roundcube persistent login 2015-05-30 14:07:36 +00:00
Joshua Tauberer
2b341d884f merge #396 - allow the backup process to work after a hostname change 2015-05-30 13:55:08 +00:00
Joshua Tauberer
141a09b31e changelog, comments for duplicity --allow-source-mismatch 2015-05-30 13:46:39 +00:00
Joshua Tauberer
6378ec4bbd Merge pull request #423 from BrianZachary/master
Update README.md
2015-05-29 16:53:38 -04:00
BrianZachary
603fb1c698 Update README.md
Added latest front page appearance of Mail-In-A-Box to README.md
2015-05-29 16:43:14 -04:00
Joaquin Bravo
67b4ea947b Add persistent login functionality to roundcube 2015-05-29 14:49:40 -05:00
Joshua Tauberer
4075b7c78a Merge pull request #421 from samrobotmesh/patch-1
Echange -> Exchange
2015-05-29 10:59:25 -04:00
Sam
6499eba0cb Echange -> Exchange 2015-05-29 07:36:53 -07:00
Joshua Tauberer
980626aa40 Merge branch 'postgrey/delay/clean' of https://github.com/Xoib/mailinabox
Closes #413.
2015-05-29 13:00:51 +00:00
Eric Mill
3f329bc1a8 fix typos 2015-05-29 01:38:42 -04:00
Joshua Tauberer
69de67b1c2 link security.md from the readme 2015-05-28 21:41:23 -04:00
Joshua Tauberer
7158f9a8d9 security.md: add links to appropriate source files in various places to make it easier to inspect the code to verify the statements; unfortunately line numbers will drift but it would be nice if we could link right to line numbers 2015-05-28 21:39:50 -04:00
Joshua Tauberer
bb75bd7167 more security details 2015-05-28 21:39:50 -04:00
Joshua Tauberer
4fa58169f1 after installing an SSL certificate from the control panel the page wasn't being refreshed, broken in ec73c171c7 2015-05-28 18:45:53 +00:00
Joshua Tauberer
564040897f Merge pull request #420 from dhpiggott/increase-dmarc-and-spf-strictness
Make SPF forbid any outbound mail from non-mail domains
2015-05-28 13:17:14 -04:00
David Piggott
f78bbab289 Make SPF forbid any outbound mail from non-mail domains 2015-05-28 18:11:44 +01:00
Joshua Tauberer
d3c82d7363 Merge pull request #419 from dhpiggott/improve-dmarc-and-spf-descriptions
Improve DMARC and SPF record descriptions
2015-05-28 13:06:44 -04:00
David Piggott
7b9b978a6d Improve DMARC and SPF record descriptions 2015-05-28 16:34:58 +01:00
Joshua Tauberer
45d47818ca add changelog entry for 4f98d470a0 2015-05-28 13:12:57 +00:00
Joshua Tauberer
202c4a948b our users/aliases database is case sensitive - force new users/aliases to lowercase
Unfortunately our users/aliases database is case sensitive. (Perhaps I should have defined the columns with COLLATE NOCASE, see https://www.sqlite.org/datatype3.html.) Postfix always queries the tables in lowecase, so mail delivery would fail if a user or alias were defined with any capital letters. It would have also been possible to add multiple euqivalent addresses into the database with different case.

This commit rejects new mail users that have capital letters and forces new aliases to lowecase. I prefer to reject rather than casefold user accounts so that the login credentials the user gave are exactly what goes into the database.

https://discourse.mailinabox.email/t/recipient-address-rejected-user-unknown-in-virtual-mailbox-table/512/4
2015-05-28 13:11:30 +00:00
Joshua Tauberer
b5269bb28e Merge pull request #418 from dhpiggott/aliases-template-tweak
Use lowercase h for consistency in aliases template - it reads better…
2015-05-28 08:56:44 -04:00
David Piggott
d6c5f09a1a Use lowercase h for consistency in aliases template - it reads better (IMO!)
This also includes fixes for a typo and some whitespace inconsistencies in
mailconfig.py. In fact the capitalisation change and those fixes are the
remnants of a patch I had been running that changed the default aliases - it
was through developing it that I found the issues.

(I wanted to bring the number of patches I apply before deploying to zero and
in the case of this one I've come to view the way MIAB already is as superior,
so I've undone the core of my patch and these tiny issues are all that remain).
2015-05-28 13:46:15 +01:00
Xoib
11546b97bb softer the greylisting delay restriction
A lot of legit mail servers try again between 200 and 285 seconds, then
3 hours later. Why? RFC is not strict about retry timer so postfix and
other MTA have their own intervals. To fix the problem of receiving
these e-mail really latter, I reduced the delay of postgrey to
180 seconds (default is 300 seconds).
2015-05-26 16:10:14 +02:00
Joshua Tauberer
cac6a251cc Merge pull request #411 from nstanke/munin
remove unnecessary source call
2015-05-26 07:12:54 -04:00
Norman Stanke
31d26a7bad remove unnecessary source call 2015-05-26 13:06:50 +02:00
Joshua Tauberer
a9ed9ae936 more work on munin
* install the munin-node package
* don't install munin-plugins-extra (if the user wants it they can add it)
* expose the munin www directory via the management daemon so that it can handle authorization, rather than manintaining a separate password file
2015-05-25 17:03:52 +00:00
Joshua Tauberer
a9892efe38 Merge branch 'master' of https://github.com/nstanke/mailinabox into munin 2015-05-25 16:03:45 +00:00
Brock Tice
f02e0a3ccb Fixed comment that still referenced solr. 2015-05-25 08:50:41 -06:00
Joshua Tauberer
d6f26609fc Merge pull request #410 from stevesbrain/master
Fixing minor misspelling of the word: encrypted
2015-05-24 22:12:16 -04:00
StevesMonkey
05438d047d Fixing minor misspelling of the word: encrypted 2015-05-25 10:15:57 +09:30
Brock Tice
32f5632620 Switch to official PPA 2015-05-23 20:09:50 -04:00
Brock Tice
005cc08b40 Modified makefile to work with updated debian-lucene patch, use official locations.
Fixed conflicting edits to ppa/Makefile due to cherry-pick.
2015-05-23 20:09:06 -04:00
Brock Tice
6a659fe10d Fixed patches for pull request.
Resolved conflict between two patches.
2015-05-23 20:07:33 -04:00
Brock Tice
6941ca2f63 Added apt-get update before installing dovecot-lucene 2015-05-23 20:01:58 -04:00
Brock Tice
e4eba49c1b Added lucene.sh to start script. 2015-05-23 20:01:45 -04:00
Brock Tice
f289439d1d Adapted Jonty's original solr.sh script to instead set up lucene full-text search in dovecot. 2015-05-23 20:01:32 -04:00
Joshua Tauberer
a5ef64919a ppa: build the dovecot-lucene package 2015-05-23 14:03:14 -04:00
Joshua Tauberer
e132125cf3 ppa: move build to /tmp 2015-05-23 12:55:55 -04:00
Joshua Tauberer
01b5512ac7 add @Jonty's .deb patch for building dovecot lucene 2015-05-23 12:14:31 -04:00
Joshua Tauberer
a0c7e63d78 best guess at what clients are supported by the tls settings used 2015-05-22 17:36:55 -04:00
Joshua Tauberer
8ba5f2ffa7 add security.md and clean up README 2015-05-22 16:53:13 -04:00
Joshua Tauberer
2c44333679 compare tls ciphers against Mozilla's recommendations 2015-05-20 19:41:04 -04:00
Joshua Tauberer
610be9cf17 record current TLS settings from my box 2015-05-20 18:31:46 -04:00
Joshua Tauberer
eb5e8fe388 the switch of smtpd_tls_security_level may to encrypt for submission broke smtpd_tls_protocols
The submission port began offering SSLv3.

With `encrypt`, the smtpd_tls_protocols option is ignored and smtpd_tls_mandatory_protocols must be set instead.

see e39b777abc
2015-05-20 22:27:11 +00:00
Joshua Tauberer
c999c6082f tweak unhackable language, see #402 2015-05-19 11:18:53 -04:00
Joshua Tauberer
3b86b3fe66 bump to email_validator 0.1.0-rc5 2015-05-19 08:37:17 -04:00
Joshua Tauberer
0a71dca825 add preliminary build tools for a Mail-in-a-Box PPA
Starting with my dnswl.org modifications to postgrey.
2015-05-18 22:15:09 -04:00
Joshua Tauberer
4f98d470a0 '/dev/stdout' does not exist on some systems (!)
The OVH VPS provider creates systems without /dev/stdout. I have never seen that before. But fine. We were passing it as a command line option to `openssl req`, but outputting to stdout is the default so it's not necessary to specify /dev/stdout.

Fixes #277. Also https://discourse.mailinabox.email/t/500-internal-server-error/475/10.
2015-05-16 13:34:47 +00:00
Joshua Tauberer
57abae3999 if the main ssl cert is expiring soon, the end of setup would display the control panel instructions as if the cert were self-signed 2015-05-14 19:16:31 +00:00
Xoib
202e49a897 allow the backup process to work after a hostname change 2015-05-13 13:52:23 +02:00
Joshua Tauberer
13093f1732 update to ownCloud 8.0.3
see #375
2015-05-11 13:00:40 +00:00
Joshua Tauberer
837d327c1e v0.09
=====

May 8, 2015

Mail:

* Spam checking is now performed on messages larger than the previous limit of 64KB.
* POP3S is now enabled (port 995).
* Roundcube is updated to version 1.1.1.
* Minor security improvements (more mail headers with user agent info are anonymized; crypto settings were tightened).

ownCloud:

* Downloading files you uploaded to ownCloud broke because of a change in ownCloud 8.

DNS:

* Internationalized Domain Names (IDNs) should now work in email. If you had custom DNS or custom web settings for internationalized domains, check that they are still working.
* It is now possible to set multiple TXT and other types of records on the same domain in the control panel.
* The custom DNS API was completely rewritten to support setting multiple records of the same type on a domain. Any existing client code using the DNS API will have to be rewritten. (Existing code will just get 404s back.)
* On some systems the `nsd` service failed to start if network inferfaces were not ready.

System / Control Panel:

* In order to guard against misconfiguration that can lead to domain control validation hijacking, email addresses that begin with admin, administrator, postmaster, hostmaster, and webmaster can no longer be used for (new) mail user accounts, and aliases for these addresses may direct mail only to the box's administrator(s).
* Backups now use duplicity's built-in gpg symmetric AES256 encryption rather than my home-brewed encryption. Old backups will be incorporated inside the first backup after this update but then deleted from disk (i.e. your backups from the previous few days will be backed up).
* There was a race condition between backups and the new nightly status checks.
* The control panel would sometimes lock up with an unnecessary loading indicator.
* You can no longer delete your own account from the control panel.

Setup:

* All Mail-in-a-Box release tags are now signed on github, instructions for verifying the signature are added to the README, and the integrity of some packages downloaded during setup is now verified against a SHA1 hash stored in the tag itself.
* Bugs in first user account creation were fixed.
2015-05-08 08:10:39 -04:00
Joshua Tauberer
e39b777abc require TLS on SMTP submission (port 587) to prevent accidental client misconfiguration, although this has no other practical consequences since without TLS clients couldn't authenticate anyway 2015-05-06 00:25:03 +00:00
Joshua Tauberer
7ca42489ae drop legacy, export-grade, and anonymous ciphers from SMTP (port 25, opportunistic)
Even though SMTP (on port 25) is typically opportunistic and a MitM attack can't be prevented, we may as well only offer ciphers that provide some level of security. If a client is so old or misconfigured that it doesn't support newer ciphers, it should hopefully fall back to a non-TLS connection.

Postfix's default was basically anything goes (anonymous and 40-bit ciphers!). Google's MTA's only offer ciphers at 112 bits at greater, and this change approximates that with Postfix's "medium" setting.

Fixes #371
2015-05-05 23:50:07 +00:00
Joshua Tauberer
8c6363f792 bad ciphers were allowed in smtp submssion
This disallows aNULL and other bad ciphers in the Postfix submission server.

I missed an option in 45e93f7dcc recommended by the blog post I was reading.

Fixes #389.
2015-05-05 23:14:59 +00:00
Joshua Tauberer
cbb7f29f96 add 'ip-transparent: yes' to nsd.conf
https://discourse.mailinabox.email/t/nsd-service-not-started-at-startup-dns-not-working/449
2015-05-04 11:24:40 +00:00
Joshua Tauberer
8886c9b6bc move the server: block of nsd.conf out of the management daemon and into the setup scripts 2015-05-04 11:24:40 +00:00
Joshua Tauberer
a07de38e80 remove workaround for buggy nsd installation
Prior to nsd 4.0.1-1ubuntu0.1, we had to create the nsd user before installing the nsd package.

This was our issue #25 (see 4e6037c0e1, c7e1e29d) and I reported it upstream at https://bugs.launchpad.net/ubuntu/+source/nsd/+bug/1311886. The new package was published by Ubuntu on 2015-01-15 so this work-around is no longer needed.
2015-05-04 11:24:40 +00:00
Joshua Tauberer
1f08997a9e need my new email_validator library during questions 2015-05-03 11:02:23 -04:00
Joshua Tauberer
1b2d07d81d update CHANGELOG 2015-05-03 14:33:22 +00:00
Joshua Tauberer
fc32cf5bcc permit the first user account to be a domain control validation address because a) it will necessarily be an admin and b) the user doesn't know the rules yet 2015-05-03 14:21:36 +00:00
Joshua Tauberer
ce94ef38b2 anonymize X-Pgp-Agent, Mime-Version outgoing mail headers; fixes #342
I don't have a mail client that sets Mime-Version with a user agent string so I couldn't really test.
2015-05-03 14:03:59 +00:00
Joshua Tauberer
1e9c587b92 rewrite the DNS API to permit setting multiple records of the same type on the same domain
e.g. multiple TXT records

fixes #333
2015-05-03 13:43:38 +00:00
Joshua Tauberer
9f1d633ae4 re-do the custom DNS get/set routines so it is possible to store more than one record for a qname-rtype pair, like multiple TXT records 2015-05-03 13:43:38 +00:00
Joshua Tauberer
f01189631a management api: make json responses nicely formatted
Better while debugging.
2015-05-03 13:43:38 +00:00
Joshua Tauberer
542877ee46 use the font-awesome .fa-spinner.fa-pulse classes for the AJAX loading indicator, rather than the static glyphicon-time icon 2015-05-03 13:43:38 +00:00
Joshua Tauberer
f1760b516d control panel: sometimes the ajax loading modal would show after operations were already done
Needed to add the clearQueue flag to jQuery's stop() method
2015-05-03 13:43:38 +00:00
Joshua Tauberer
f0143fd6c9 bump version of my email_validator library 2015-04-29 21:18:14 +00:00
Joshua Tauberer
febfa72d60 race condition between backups and status checks - connection refused
At the end of the backup, wait a bit for dovecot and postfix to finish restarting.

Hopefully fixes #381.
2015-04-29 21:06:38 +00:00
Joshua Tauberer
c03e00035f prevent archiving of the user's own account because they'll lose access to the control panel 2015-04-28 07:17:21 -04:00
Joshua Tauberer
2f8866ef32 if there are no users at all the warning on the control panel login screen was incorrect 2015-04-28 07:17:21 -04:00
Joshua Tauberer
f98afac6df if you make an API call with a user-specific API key (e.g. from control panel) but your account no longer exists on the system, there was an unhandled error
see 1039a08be6
2015-04-28 07:17:21 -04:00
Joshua Tauberer
5efd5abbe4 move the email address syntax validation for users and aliases into my new email_validator library (https://github.com/JoshData/python-email-validator) 2015-04-21 14:43:12 +00:00
Joshua Tauberer
6bb8f5d889 ownCloud 8 busted MOD_X_ACCEL_REDIRECT_ENABLED
see https://github.com/owncloud/core/issues/14976

We will need to update when ownCloud makes this better with MOD_X_ACCEL_REDIRECT_PREFIX.

See https://discourse.mailinabox.email/t/owncloud-can-not-read-uploaded-data/428.
2015-04-20 22:18:45 +00:00
Joshua Tauberer
35f4a49d10 my html5 stub was wrong; 8c3aed2846 2015-04-19 13:21:38 +00:00
Joshua Tauberer
a31d713fcc stricter validation of the domain parts of email addresses: only letters, numbers, and hyphens, and the TLD ends with a letter 2015-04-19 13:06:11 +00:00
Joshua Tauberer
6f38f7afc3 update CHANGELOG 2015-04-16 12:02:17 +00:00
Joshua Tauberer
e514ca0009 bump to Roundcube 1.1.1 2015-04-16 11:45:35 +00:00
Joshua Tauberer
8c3aed2846 update the control panel html template to my latest html5 stub
jquery 1.11.1, bootstrap 3.3.0, better accessibility, see https://github.com/JoshData/html5-stub
2015-04-11 15:40:19 -04:00
Joshua Tauberer
2cab9d5514 editconf.py: better error message if command line arguments are not valid 2015-04-11 15:25:11 -04:00
Joshua Tauberer
c38bdbb0c5 mistake in 31eec9fa1c #300 2015-04-11 15:24:15 -04:00
Joshua Tauberer
2a1704a0dc check that the downloaded ownCloud and roundcube files match a known SHA1 hash 2015-04-11 15:21:38 -04:00
Joshua Tauberer
36168b4609 add a 'backup --verify' command to run duplicity's verify command to check that the backup files are OK 2015-04-11 18:43:46 +00:00
Joshua Tauberer
bd498def76 backups now use duplicity's built-in gpg symmetric encryption
Merge branch 'dhpiggott-gpg-encrypt-backups'
2015-04-11 18:33:57 +00:00
Joshua Tauberer
d8279c48ac new backup method tweaks
* use the AES256 cipher, be explicit that only the first line of secret_key.txt is used, and sanity check that the passphrase is long enough
* change overship of the encrypted files to the user-data user
* simplify variable names in management/backup.py
* although I appreciate long comments I am trimming the commentary about the backup migration
* revise the control panel template to not refer to the old unencrypted files
* add CHANGELOG entry
2015-04-11 18:32:22 +00:00
David Piggott
4232245546 Use built in duplicity encryption (GPG) for backups, closes #362, closes #363
[Josh merged some subsequent commits:]

* Guard via idempotency against termination between migration operations
* Final corrections and tweaks
* Pass passphrase through to all duplicity calls

Empirical evidence (a failed cron job) shows that cleanup requires the
passphrase (so it presumably needs to decrypt metadata), and though
remove-older-than has been working fine without it, it won't do any harm
to set it in case that changes or there are any special cases.

* Add back the archive-dir override but locate it at STORAGE_ROOT/backup/cache
2015-04-11 17:51:44 +00:00
Joshua Tauberer
2d1186e55d increase spampd maximum message size from 64KB to 500KB, matching the spamc default
see https://discourse.mailinabox.email/t/allow-spamassassin-to-scan-emails-larger-than-250kb/391
2015-04-09 14:46:02 +00:00
Joshua Tauberer
072aeca1be prevent accidental domain control validation hijacking by limiting use of admin@ etc. addresses in users/aliases 2015-04-09 14:46:02 +00:00
Joshua Tauberer
cb656f9ef4 in status checks replace '=>' with a Unicode arrow and tweak how aliases are reported 2015-04-09 14:46:02 +00:00
Joshua Tauberer
322a5779f1 store IDNs (internationalized domain names) in IDNA (ASCII) in our database, not in Unicode
I changed my mind. In 1bf8f1991f I allowed Unicode domain names to go into the database. I thought that was nice because it's what the user *means*. But it's not how the web works. Web and DNS were working, but mail wasn't. Postfix (as shipped with Ubuntu 14.04 without support for SMTPUTF8) exists in an ASCII-only world. When it goes to the users/aliases table, it queries in ASCII (IDNA) only and had no hope of delivering mail if the domain was in full Unicode in the database. I was thinking ahead to SMTPUTF8, where we *could* put Unicode in the database (though that would prevent IDNA-encoded addressing from being deliverable) not realizing it isn't well supported yet anyway.

It's IDNA that goes on the wire in most places anyway (SMTP without SMTPUTF8 (and therefore how Postfix queries our users/aliases tables), DNS zone files, nginx config, CSR 'CN' field, X509 Common Name and Subject Alternative Names fields), so we should really be talking in terms of IDNA (i.e. ASCII).

This partially reverts commit 1bf8f1991f, where I added a lot of Unicode=>IDNA conversions when writing configuration files. Instead I'm doing Unicode=>IDNA before email addresses get into the users/aliases table. Now we assume the database uses IDNA-encoded ASCII domain names. When adding/removing aliases, addresses are converted to ASCII (w/ IDNA). User accounts must be ASCII-only anyway because of Dovecot's auth limitations, so we don't do any IDNA conversion (don't want to change the user's login info behind their back!). The aliases control panel page converts domains back to Unicode for display to be nice. The status checks converts the domains to Unicode just for the output headings.

A migration is added to convert existing aliases with Unicode domains into IDNA. Any custom DNS or web settings with Unicode may need to be changed.

Future support for SMTPUTF8 will probably need to add columns in the users/aliases table so that it lists both IDNA and Unicode forms.
2015-04-09 14:46:02 +00:00
Joshua Tauberer
e41df28bf2 if a migration fails, dont continue setup 2015-04-09 14:46:02 +00:00
Joshua Tauberer
d3239b49ce Merge pull request #372 from hnk/fix-spaces_in_password
fix(read_password): regex check for spaces, quotes
2015-04-09 09:08:31 -04:00
Joshua Tauberer
d11be61d94 Add POP3S support (merge w/ adjustments)
* Add pop3s to the ufw firewall rules.
* Updated some comments.
* Updated CHANGELOG.

Merge branch 'master' of https://github.com/pichak/mailinabox
2015-04-09 08:19:20 -04:00
Morteza Milani
916063a79b Better documentation for POP3 settings, UIDL.
UIDL assigns a unique string to each email. This allows emails to
be left on the server after a client downloads them.
2015-04-08 21:32:14 -07:00
Joshua Tauberer
5aa0bf2d14 add instructions for verifying the signed tags to the README 2015-04-01 10:38:09 -04:00
Joshua Tauberer
f3ad6b4acc Version 0.08
CHANGELOG
=========

v0.08 (April 1, 2015)
---------------------

Mail:

* The Roundcube vacation_sieve plugin by @arodier is now installed to make it easier to set vacation auto-reply messages from within Roundcube.
* Authentication-Results headers for DMARC, added in v0.07, were mistakenly added for outbound mail --- that's now removed.
* The Trash folder is now created automatically for new mail accounts, addressing a Roundcube error.

DNS:

* Custom DNS TXT records were not always working and they can now override the default SPF, DKIM, and DMARC records.

System:

* ownCloud updated to version 8.0.2.
* Brute-force SSH and IMAP login attempts are now prevented by properly configuring fail2ban.
* Status checks are run each night and any changes from night to night are emailed to the box administrator (the first user account).

Control panel:

* The new check that system services are running mistakenly checked that the Dovecot Managesieve service is publicly accessible. Although the service binds to the public network interface we don't open the port in ufw. On some machines it seems that ufw blocks the connection from the status checks (which seems correct) and on some machines (mine) it doesn't, which is why I didn't notice the problem.
* The current backup chain will now try to predict how many days until it is deleted (always at least 3 days after the next full backup).
* The list of aliases that forward to a user are removed from the Mail Users page because when there are many alises it is slow and times-out.
* Some status check errors are turned into warnings, especially those that might not apply if External DNS is used.
2015-04-01 10:14:34 -04:00
Joshua Tauberer
ec039719de prevent caching of ajax responses in the control panel
GET requests might be cached. Definitely happens on Internet Explorer. Makes it look like the user is getting unauthorized access.

See https://discourse.mailinabox.email/t/fresh-install-can-login-to-webmail-but-not-admin/394/4.
2015-03-31 14:52:11 +00:00
Sascha Reynolds
6989df0af3 fix(read_password): regex check for spaces, quotes
* Passwords must be at least four characters. So we need to check them
here to ensure that first user creation works during initial setup
* Change quotes to match rest of code
2015-03-30 19:59:07 +02:00
Joshua Tauberer
14b16b2f36 allow custom DNS TXT records for SPF, DKIM, and DMARC to override the ones we want to set
fixes #323
fixes #324
2015-03-30 01:20:03 +00:00
Joshua Tauberer
cbc7e280d6 set the SPF record after custom DNS records so that the SPF record doesn't prevent all custom TXT records from coming in 2015-03-30 01:18:05 +00:00
Joshua Tauberer
f4fa9c93a0 Merge pull request #366 from hnk/hnk-patch-read_password
Change read_password() logic to catch short passwords
2015-03-29 14:12:07 -04:00
Hnk Reno
6c64723d7c Change read_password() logic to better catch improper passwords
Currently read_password does not verify password length. But further down the chain, passwords are checked to make sure they are longer than four characters.

If during initial setup, the user enters a password that is shorter than four characters, this will not be caught here, but when the script actually calls management/mailconfig.py to add the user, it will fail without a chance to correct the short password.

The setup script will then continue without an inital user being created and this will confuse users.
2015-03-29 18:54:37 +02:00
Joshua Tauberer
3d21f2223e status checks: turn missing DNSSEC into a warning instead of an error; omit an error about missing TLSA if DNSSEC isn't in use; if DNSSEC is in use, make a missing TLSA record a warning instead of an error 2015-03-28 11:24:05 -04:00
Joshua Tauberer
710a69b812 turn some nameserver status check errors into warnings if the domain resolves correctly since the user might be using External DNS, closes #330 2015-03-28 11:23:59 -04:00
Joshua Tauberer
dd6a8d9998 upgrade to ownCloud 8.0.2
The contacts and calendar apps are now maintained outside of ownCloud core, so we now pull them in from github tags and must enable them explicitly.
2015-03-28 11:08:57 -04:00
Joshua Tauberer
9f32e5af0a the install of roundcube vacation_sieve requires that we install git
see a8669197dd
2015-03-28 09:54:52 -04:00
Joshua Tauberer
298e19598b small bug in the new system status checks show-changes command
see 4d22fb9b2a

fixes #360
2015-03-22 14:03:12 +00:00
Joshua Tauberer
680191d7cb drop the list of aliases from the users control panel page because with more than 50 aliases it seems to be so slow it times out
see https://discourse.mailinabox.email/t/small-bug-in-admin-panel-when-49-aliases/378
2015-03-22 13:59:05 +00:00
Joshua Tauberer
81d6d69b85 update CHANGELOG 2015-03-22 13:58:24 +00:00
Joshua Tauberer
6df72bf4ac create the Trash folder on new user creation (fixes #359) 2015-03-22 13:33:17 +00:00
Joshua Tauberer
01f2451349 provide a better error message when creating a user account with non-ASCII characters 2015-03-22 12:33:06 +00:00
Joshua Tauberer
dcd971d079 the opendmarc miter should run on incoming mail only
I added OpenDMARC's milter in fba4d4702e. But this started
setting Authentication-Results headers on outbound mail with failures. Not sure why it
fails at that point, but it shouldn't be set at all. The failure might cause recipients
to junk the mail. See #358.

This commit removes the milter from the SMTP submission (port 587) listener.
2015-03-21 16:14:01 +00:00
Joshua Tauberer
4d22fb9b2a run status checks each night and email the administrator with the changes from the previous day's results 2015-03-21 16:02:42 +00:00
Joshua Tauberer
c18d58b13f backups: predict when the next backup will occur 2015-03-21 15:22:45 +00:00
Joshua Tauberer
b539c2df70 Merge pull request #347 from Toilal/feat/start-enhancements
If the migration file is missing but the storage directory exists, assume this is a fresh directory -- don't bother trying to migrate, and do write the migration file with the current migration ID.
2015-03-19 11:57:24 -04:00
Toilal
64fdb4ddc1 Behave nicely when mailinabox.version file is missing 2015-03-09 08:54:32 +01:00
Joshua Tauberer
a8669197dd added Roundcube plugin vacation_sieve
Merge branch 'master' of https://github.com/zealot128-os/mailinabox

Closes #334
2015-03-08 19:15:20 +00:00
Joshua Tauberer
2412c92772 enable fail2ban for ssh and dovecot
Merge branch 'master' of https://github.com/h8h/mailinabox

see #353, #319
2015-03-08 18:40:17 +00:00
Joshua Tauberer
7c0ca42145 status checks: don't check that dovecot-sieve is publicly accessible 2015-03-08 18:35:33 +00:00
H8H
c443524ee2 Configure fail2ban jails to prevent dumb brute-force attacks against postfix, dovecot and ssh. See #319 2015-03-08 01:13:55 +01:00
Joshua Tauberer
e2fa01e0cf Merge pull request #348 from benschumacher/master
Update MX records using DNS Update API / Management UI
2015-03-04 13:42:02 -05:00
Ben Schumacher
6558f05d1d Give the DNS update tool the ability to customize MX records. Useful if you want a subdomain to send mail to another host. 2015-03-04 13:32:35 -05:00
Joshua Tauberer
1be0f39be0 prep for v0.07 tag 2015-02-28 17:09:12 -05:00
Joshua Tauberer
d01001f2a5 some more CHANGELOG entries 2015-02-28 17:06:09 -05:00
Joshua Tauberer
7c85694d60 Merge pull request #332 from mathuin/better-mx-check
Changed MX check to respect priorities other than 10.
2015-02-23 07:56:24 -05:00
Jack Twilley
b2fcd4c9e5 Now supports domains with multiple MX records.
The status check on MX records now correctly handles domains with
multiple MX records.
2015-02-22 17:05:09 -08:00
Stefan Wienert
ba8123f08a reduced diff noise 2015-02-21 16:06:56 +01:00
Stefan Wienert
e2879a8eb1 made the setup repeatable 2015-02-21 16:05:47 +01:00
Stefan Wienert
eab8652225 added vacation_sieve plugin for Roundcube 2015-02-21 16:01:27 +01:00
Jack Twilley
ead6f96513 Changed MX check to respect priorities other than 10.
Reordered the if a little, added some string parsing, and modified the
OK text to include a warning.
2015-02-20 11:29:28 -08:00
Joshua Tauberer
7ec662c83f status checks: use a worker pool that lives across flask requests, see #327 2015-02-18 16:42:33 +00:00
Joshua Tauberer
348d2b8701 Merge pull request #326 from dhpiggott/custom-dns-filter-secondary-nameserver
Do not show '_secondary_nameserver' in Custom DNS table
2015-02-17 08:31:34 -05:00
David Piggott
12f0dcb23b Do not show '_secondary_nameserver' in Custom DNS table
It's redundant and potentially confusing, as any secondary NS shows in "Using a
Secondary Nameserver".
2015-02-17 13:28:48 +00:00
Joshua Tauberer
449a538e6b if a CNAME is set for a domain, don't create a website for that domain (just like A/AAAA records) 2015-02-17 00:48:26 +00:00
Joshua Tauberer
3c50c9a18b when serving a 'www.' domain, check if the parent domain's ssl certificate can be used besides checking PRIMARY_HOSTNAME
Removing buy_certificate.py which is not working and I don't want to update its call signatures.
2015-02-17 00:42:25 +00:00
Joshua Tauberer
3c10ec70a5 update comment 2015-02-17 00:08:04 +00:00
Joshua Tauberer
1a59f343c0 adding entries to the CHANGELOG 2015-02-16 23:58:17 +00:00
Joshua Tauberer
fba4d4702e install opendmarc to add Authentication-Results headers for DMARC too 2015-02-16 23:17:44 +00:00
Joshua Tauberer
143bbf37f4 all mail domains, not just (top-level) zones, must have an entry in the opendkim key tables so that such outgoing mail gets signed
If you had both x.y.com and y.com configured here, x.y.com mail would not get DKIM-signed.
2015-02-16 18:13:51 -05:00
Joshua Tauberer
fd3ad267ba if a domain has a catch-all or domain alias then we no longer force the creation of postmaster@ and so we should not be checking for its existence in the status checks
see 85a40da83c
2015-02-15 19:07:10 -05:00
Joshua Tauberer
330583f71d status checks: if a service isn't available publicly, check if it is available on the loopback interface to distinguish not running from not accessible 2015-02-13 09:30:25 -05:00
Joshua Tauberer
d775f90f0c prevent apt from asking the user any questions
Add additional options to really prevent apt from asking questions, which causes setup to hang because stdin/out have been redirected.

fixes #270, #291
2015-02-13 13:41:52 +00:00
Joshua Tauberer
e096144713 Outlook 2007 or later on Windows 7 and later
fixes #308
2015-02-13 13:29:01 +00:00
Joshua Tauberer
7ce30ba888 roundcube 1.1.0 2015-02-13 13:22:46 +00:00
Joshua Tauberer
6a3ec1d874 updating CHANGELOG 2015-02-13 13:20:55 +00:00
Joshua Tauberer
575d3a66c6 more on being smarter about waiting for the management daemon to start
cc333b3965 worked for fresh systems, but if the system already had the daemon running the api.key file would already exist and the test would pass to early. Now removing the file first.

fixes #322
2015-02-13 13:11:03 +00:00
Joshua Tauberer
cc333b3965 be smarter about waiting for the management daemon to start before accessing it 2015-02-10 10:03:07 -05:00
Joshua Tauberer
351758b3bd typo
typo in "roudcube"
2015-02-10 09:27:36 -05:00
Joshua Tauberer
94053d8432 Merge pull request #317 from bizonix/master
Disable viewing dotfiles (.htaccess, .svn, .git, etc.)
2015-02-09 12:53:32 -05:00
BiZoNiX
e14b2826e0 Disable viewing dotfiles (.htaccess, .svn, .git, etc.) 2015-02-09 19:41:42 +02:00
Joshua Tauberer
150611123a typo/text tweak 2015-02-05 09:17:48 -05:00
Joshua Tauberer
abfc17ee62 web admin: simplify the instructions for creating a separate web directory for particular sites by moving it into a modal 2015-02-05 09:12:55 -05:00
Joshua Tauberer
97be9c94b9 if the user has set a http proxy or redirect on the root path of a domain, using custom.yaml, skip the domain from the static hosting panel because it wont be serving any static files 2015-02-05 08:55:57 -05:00
Joshua Tauberer
21b00e8fbb if a custom A record is set, dont put in a default AAAA record pointing to the box because it will probably be wrong --- the user should either set an AAAA record or let the domain not resolve on IPv6 2015-02-03 21:51:19 -05:00
Joshua Tauberer
01636c2e4b Merge branch 'h8h-master'
I squashed some commits together and modified the commit message...
2015-02-03 23:54:17 +00:00
H8H
005315cd29 removed hardcoded /home directory to apply the existing configuration options for STORAGE_USER/ROOT if they exist
Highest priority: the pre set STORAGE_ROOT/USER, midmost priority: the config settings, lowest priority: the default one.

fixes #309; closes #311
2015-02-03 23:52:02 +00:00
Ian Beringer
20d20df829 allow for non-standard ssh port in status check
closes #313
2015-02-01 23:06:56 +00:00
Joshua Tauberer
f945a1bc6b Merge pull request #312 from ikarus23/master
hide nginx version an OS information for better privacy
2015-02-01 14:25:39 -05:00
ikarus
3a09b04786 hide nginx version an OS information for better privacy. 2015-02-01 20:13:03 +01:00
Joshua Tauberer
82e752395b Merge pull request #310 from ikarus23/master
do better redirection from http to https
2015-01-31 19:58:31 -05:00
ikarus
e330abd587 do better redirection from http to https
Redirect using the 'return' directive and the built-in
variable '$request_uri' to avoid any capturing, matching
or evaluation of regular expressions.

It's best practice. See: http://wiki.nginx.org/Pitfalls#Taxing_Rewrites
2015-02-01 01:32:07 +01:00
Joshua Tauberer
16422b4055 adding items to the CHANGELOG 2015-01-31 21:36:37 +00:00
Joshua Tauberer
b9ca74c915 implement Mozilla (e.g. Thunderbird) autoconfiguration file
fixes #241
2015-01-31 21:33:18 +00:00
Joshua Tauberer
7e05d7478f run status checks asynchronously so that they finish faster, since many checks are waiting on network replies and ought not to block the whole thing 2015-01-31 20:42:43 +00:00
Joshua Tauberer
8fd98d7db3 status checks: s/env['out']/output/ 2015-01-31 20:42:43 +00:00
Joshua Tauberer
1039a08be6 /admin login now issues a user-specific key for future calls (rather than providing the system-wide API key or passing the password on each request) 2015-01-31 20:42:43 +00:00
Joshua Tauberer
023b38df50 split management daemon authorization from authentication and use 'doveadm pw' rather than 'doveadm auth test' so that it is decoupled from dovecot's login mechanism
This was done to pave the way for two-factor authentication, but that's still a ways off.
2015-01-31 20:41:41 +00:00
Joshua Tauberer
3187053b3a dont save the CSR generated to make self-signed certificates for non-primary domains (it has no value and might be confusing) 2015-01-31 13:27:06 +00:00
Joshua Tauberer
a3e526e818 Merge pull request #307 from pierreozoux/master
Typo
2015-01-29 12:52:37 -05:00
pierreozoux
f6d4621834 Typo 2015-01-29 17:03:20 +00:00
Joshua Tauberer
d075113c1a Merge pull request #306 from dhpiggott/fix-backup-typos
Fix typos in backup status template
2015-01-29 08:22:44 -05:00
David Piggott
63f2abd923 Fix typos in backup status template 2015-01-29 09:25:12 +00:00
Norman
f78cff225b Add Munin
removed testing source

fixed typo & dns

oh cat

more fixes

forgot root

more nginx stuff

nginx munin.conf fix

more fixes

set dns record
2015-01-28 21:42:16 +01:00
Morteza Milani
31eec9fa1c Add POP3s support 2015-01-25 23:37:01 -08:00
Joshua Tauberer
624cc7876a Merge pull request #297 from kurthuwig/fix_typo
Fix typo in mail-guide.html
2015-01-21 08:52:39 -05:00
Kurt Huwig
d3059c810f Fix typo in mail-guide.html
Sercurity -> Security
2015-01-21 08:23:26 +01:00
Joshua Tauberer
85a40da83c catch-all aiases and domain aliases should not require postmaster@ and admin@ aliases because they'll forward anyway 2015-01-19 23:32:36 +00:00
Joshua Tauberer
1bf8f1991f internationalized domain names (DNS, web, CSRs, normalize to Unicode in database, prohibit non-ASCII characters in user account names)
* For non-ASCII domain names, we will keep the Unicode encoding in our users/aliases table. This is nice for the user and also simplifies things like sorting domain names (using Unicode lexicographic order is good, using ASCII lexicogrpahic order on IDNA is confusing).
* Write nsd config, nsd zone files, nginx config, and SSL CSRs with domains in IDNA-encoded ASCII.
* When checking SSL certificates, treat the CN and SANs as IDNA.
* Since Chrome has an interesting feature of converting Unicode to IDNA in <input type="email"> form fields, we'll also forcibly convert IDNA to Unicode in the domain part of email addresses before saving email addresses in the users/aliases tables so that the table is normalized to Unicode.
* Don't allow non-ASCII characters in user account email addresses. Dovecot gets confused when querying the Sqlite database (which we observed even for non-word ASCII characters too, so it may not be related to the character encoding).
2015-01-19 23:31:55 +00:00
Joshua Tauberer
d155aa8745 if all system services are running, say so in the status checks rather than being totally silent 2015-01-19 22:04:25 +00:00
Joshua Tauberer
c0bfd6d15f bring CHANGELOG up to date 2015-01-19 22:04:25 +00:00
Joshua Tauberer
24cc108147 if a custom CNAME record is set, don't add a default A/AAAA record, e.g. for 'www'
see https://discourse.mailinabox.email/t/multiple-domains-in-mail-in-a-box-with-the-domains-being-hosted-elsewhere/56/18
2015-01-19 22:04:21 +00:00
Joshua Tauberer
b02d7d990e install cron in case it isn't already installed 2015-01-11 20:00:11 +00:00
Joshua Tauberer
87f82addbc preflight memory check: units problems
/proc/meminfo reports kibibytes. Lower the minimum memory requirement so that 768 MB (not MiB) also is allowed.

Report the detected memory in MB (not KiB), to be clearer.

Fixes #289.
2015-01-11 14:13:35 +00:00
Joshua Tauberer
09713e8eab status checks: check that system services are running
If bind9 isn't running, dont proceed with other checks because we can't do DNS checks. Even though we skip, add error handling so that a failed call to rndc doesn't crash and that a timeout in a DNS check doesn't crash the status checks.
2015-01-11 14:13:35 +00:00
Joshua Tauberer
0aa3941832 release v0.06 2015-01-04 15:18:13 -05:00
Joshua Tauberer
fea77e41df Owncloud doesnt't support CARDDAV_SUPPORTS_SYNC
partialy reverts 93a722f; closes #287
2015-01-04 16:04:32 +00:00
Joshua Tauberer
74ef9ab7c5 Merge pull request #288 from fjuan/srv_doc
explain how to add SRV records to DNS zonefile using the API
2015-01-04 09:19:24 -05:00
Francisco de Juan
6499c82d7f explain how to add SRV records to DNS zonefile using the API 2015-01-04 10:23:34 +01:00
Joshua Tauberer
80e97feee2 update CHANGELOG 2015-01-02 23:47:19 +00:00
Joshua Tauberer
fddab5d432 allow the dns api to set srv records
see https://discourse.mailinabox.email/t/create-srv-record-at-the-dns-server/225
2015-01-02 23:39:09 +00:00
Joshua Tauberer
c4e4805160 ensure postfix/postgrey agree on whether to communicate with ipv4 or ipv6
see https://discourse.mailinabox.email/t/postgrey-and-ipv6/227
2015-01-02 23:37:16 +00:00
Joshua Tauberer
c75950125d set dovecot default_process_limit and fs.inotify.max_user_instances to better defaults
See https://discourse.mailinabox.email/t/mailserver-limits/228.
2015-01-02 23:25:52 +00:00
Joshua Tauberer
f141af4b61 status checks: dont die if openssh-server isn't installed
see https://discourse.mailinabox.email/t/local-dns-is-not-working-was-unable-to-check-system-status/165/39
2015-01-02 22:59:29 +00:00
Joshua Tauberer
3d8ea0e6ed mail log scanner: dont assume lines are utf8 2015-01-02 22:49:25 +00:00
H8H
6efeff6fce [Z-Push] Owncloud doesnt't support CARDDAV_SUPPORTS_SYNC, so set it to false 2014-12-29 16:35:47 +01:00
Joshua Tauberer
399f9d9bdf in status checks, clear bind9 cache using rndc rather than restarting bind9 2014-12-26 13:22:14 +00:00
Joshua Tauberer
2b76fd299e admin: ensure multiple concurrent api calls dont confuse the ajax loading indicator (track number of open requets, stop fade animation when it is time to hide) 2014-12-21 22:47:11 +00:00
Joshua Tauberer
90592bb157 add a control panel for setting custom dns records so that we dont have to use the api manually 2014-12-21 11:31:24 -05:00
Joshua Tauberer
5cf38b950a bump ownCloud to 7.0.4; fixes #283 2014-12-12 01:00:35 +00:00
Joshua Tauberer
3bc5361491 Merge pull request #282 from m4rcs/master
Fix NS status check - should sort on both ends of the comparison
2014-12-09 11:17:30 -05:00
Marc Schiller
c3a7e3413b Fixed a small status check bug, where secondary dns server check fails misleadingly. 2014-12-09 12:40:32 +01:00
Joshua Tauberer
d390bfb215 indicate in the admin when a multi-domain or wildcard certificate is in use 2014-12-05 14:43:52 -05:00
Joshua Tauberer
ceba53f1c4 explain how to install a multi-domain or wildcard ssl cert; if one is installed, the Replace Cert button in the admin for non-primary domains should not replace the cert on the primary domain 2014-12-05 14:25:14 -05:00
Joshua Tauberer
be59bcd47d for .fund domains use RSASHA256 DNSSEC keys 2014-12-05 12:03:21 -05:00
Joshua Tauberer
cfe0fa912a add a 'redirects' feature in web/custom.yaml 2014-12-05 12:03:21 -05:00
Joshua Tauberer
31d6128a2b nginx: explicitly listen on both ipv4 and ipv6 (works even if ipv6 isn't present) 2014-11-30 14:41:30 +00:00
Joshua Tauberer
82cf5b72e4 simplify some output in the work-in-progress mail log scanner 2014-11-30 14:41:30 +00:00
Joshua Tauberer
8ec8c42441 Merge pull request #275 from grepwood/master
add `apt-get install sudo`
2014-11-25 10:39:13 -05:00
Michael Dec
7e36e1fd90 added sudo to the list
not all setups have it and the miab installer depends on it
2014-11-25 15:36:34 +00:00
Joshua Tauberer
a7710e9058 dns.resolver.query treats hostnames as relative names if they don't end in a period
Relative hostnames have a fall-back lookup with the machine's hostname appended, which makes no sense. Add a period, e.g. "my.hostname.com" => "my.hostname.com.", to prevent that.

This caused false positive Spamhaus checks. Fixes #185.
2014-11-21 15:16:59 +00:00
Joshua Tauberer
3133dcd5a3 release 0.05 2014-11-18 16:52:02 +00:00
Joshua Tauberer
057c1dd913 recommend IMAP/SMTP for everyone 2014-11-18 16:47:42 +00:00
Joshua Tauberer
06f2477cfd the new iOS configuration profile also is used on OS X 10.10.1, see #261 2014-11-18 16:32:37 +00:00
Joshua Tauberer
1abc8ed469 add CHANGELOG entries for recent work 2014-11-14 14:13:24 +00:00
Joshua Tauberer
cdaa2c847d [merge] iOS Mobile Configuration Profile 2014-11-14 13:56:18 +00:00
Joshua Tauberer
b04addda9a move the mobileconfig into the conf directory as a plain XML file and handle substitutions and copying to /var in web.sh 2014-11-14 13:52:29 +00:00
Joshua Tauberer
7e7abf3b53 support "domain aliases" (@domain => @domain aliases)
This seemed to already be technically supported but the validation is now stricter and the admin is more helpful:

* Postfix seems to allow @domain.tld as an alias destination address but only if it is the only destination address (see the virtual man page).
 * Allow @domain.tld if it is the whole destination address string.
 * Otherwise, do not allow email addresses without local parts in the destination.
* In the admin, add a third tab for making it clear how to add a domain alias.

closes #265
2014-11-14 13:35:58 +00:00
Joshua Tauberer
9b9f5abf8f update to ownCloud 7.0.3 2014-11-14 13:35:58 +00:00
Norman
7db80458dd fix description 2014-11-06 15:42:22 +01:00
Norman
5775cab175 various fixes 2014-11-06 15:33:08 +01:00
Norman
c872e6a9f0 iOS Configuration Profile
change name

removed .vagrant

fix guide layout
2014-11-05 18:42:04 +01:00
Joshua Tauberer
995b7c4d2b Merge pull request #260 from fpgaminer/master
Fix typo in zpush.sh comment
2014-11-05 06:43:46 -05:00
fpgaminer
f797eecaca Fix typo in zpush.sh comment 2014-11-04 19:53:24 -08:00
Joshua Tauberer
de0ccd0632 [merge] Disable encapsulation of spam and marking of it as seen
is #254 plus a longer comment, fixes #243
2014-10-31 12:15:58 +00:00
David Piggott
be9d97902f Disable encapsulation of spam and marking of it as seen 2014-10-28 15:15:21 +00:00
Joshua Tauberer
20c5471a89 expose the ownCloud API, fixes #240, fixes #242 2014-10-28 12:05:07 +00:00
Joshua Tauberer
ec73c171c7 when installing a ssl cert for the primary hostname, dns, postfix, and dovecot all need to be updated/kicked
see https://discourse.mailinabox.email/t/there-is-a-problem-with-the-ssl-certificate/144/4
2014-10-28 11:38:04 +00:00
Joshua Tauberer
f9acf0adec better errors for ssl certificates 2014-10-24 21:30:33 +00:00
Joshua Tauberer
8b65c11cdf the namecheap link was bad 2014-10-23 17:17:26 +00:00
Joshua Tauberer
34fca29dd3 fix the animated scroll target on the ssl panel to scroll so that the header is actually visible and not covered by the nav bar 2014-10-23 17:10:21 +00:00
Joshua Tauberer
b75fbf22ca clear the local dns cache each time the status checks are run by restarting bind9 2014-10-23 17:06:33 +00:00
Joshua Tauberer
d790cae0e2 DNSSEC: use RSASHA256 for the .guide tld too 2014-10-23 17:03:23 +00:00
Joshua Tauberer
a68703dfb3 add a tool for me to see how many installations are happening by scanning for accesses to bootstrap.sh 2014-10-21 14:02:19 +00:00
Joshua Tauberer
f35b2081a1 s/os.rename/shutil.move/ so that the file can be moved across filesystem boundaries, fxies #246 2014-10-21 11:45:14 +00:00
David Piggott
f0508d8cc9 Improve wrapping of external DNS value column to prevent layout overflow
see #244

Conflicts:
	management/templates/external-dns.html
2014-10-21 11:33:42 +00:00
Joshua Tauberer
47dd59c2a7 admin mail guide: use bootstrap .panel to style the tips
also give more space for the login settings and less space to the tips
2014-10-21 11:17:49 +00:00
Joshua Tauberer
c2fe1bc2e3 document +tag addresses in the mail guide 2014-10-21 11:17:49 +00:00
Joshua Tauberer
cce1184090 admin: change the css class name around the panels to not invoke the bootstrap 'panel' css 2014-10-21 11:17:49 +00:00
Joshua Tauberer
1adb1d8307 admin: there is no need to make each panel a separate bootstrap container
* also fixes the footer alignment to be within a container rather than a container-fluid
* this changed the width of the login form slightly, so am cleaning that up too

see #244
2014-10-21 11:17:28 +00:00
Joshua Tauberer
c2174e10a6 some admin pages had a container within a container
see #244
2014-10-21 11:17:15 +00:00
Joshua Tauberer
18283c7df0 Merge pull request #248 from dhpiggott/bash-fix
Add shebangs to enable running dkim and webmail scripts separately
2014-10-20 16:35:57 -04:00
David Piggott
3ff74c8dc5 Add source line so dkim should actually work when run separately 2014-10-20 21:33:20 +01:00
David Piggott
e997114d6e Add shebangs to enable running dkim and webmail scripts separately 2014-10-20 21:26:14 +01:00
Joshua Tauberer
e9aecba4df update to roundcube 1.0.3, and really update
Updating existing installed was broken. The new roundcube would be copied into a subdirectory of /usr/local/lib/roundcubemail.

Also fixes the image thumbnail issue raised on the forum (https://discourse.mailinabox.email/t/roundcube-thumbnails-not-showing/136), see http://trac.roundcube.net/wiki/Changelog.
2014-10-20 12:48:15 +00:00
Joshua Tauberer
6585384daa bring the max outgoing mail size via webmail and z-push in line with the limit set in postfix: 128 MB
The limit was previously the nginx default (2MB?).

fixes #236
2014-10-16 22:11:10 +00:00
Joshua Tauberer
6bc821676c add the sa-learn permissions changes to the CHANGELOG 2014-10-16 22:11:10 +00:00
Joshua Tauberer
f38ef0223d update version number in CHANGELOG 2014-10-15 12:36:34 -04:00
Joshua Tauberer
8902e9d1fc bump bootstrap to incoming v0.04 tag 2014-10-15 12:33:20 -04:00
Joshua Tauberer
86a5394f07 fix control panel when no backup has been made yet 2014-10-15 12:31:08 -04:00
Joshua Tauberer
df5df18820 fixes for bootstrap.sh for upgrading
* `git fetch` wasn't done right for shallow clones
* the test for whether mailinabox has already been cloned wasn't looking at the right directory if the script was not run from $HOME
2014-10-15 12:22:48 -04:00
Joshua Tauberer
0b5bf602aa various improvements in bash comments 2014-10-15 11:46:20 -04:00
Joshua Tauberer
06e074bd32 disable SSLv3 in dovecot now that it is known to be insecure (POODLE)
SSLv3 is already disabled in Postfix (45e93f7dcc) and Nginx (51dd2ed70b).
2014-10-15 15:39:05 +00:00
Joshua Tauberer
b5b3fca137 report free disk space in the admin 2014-10-13 14:12:16 +00:00
Joshua Tauberer
048e35a80f fix display of backups that are past due to be reaped 2014-10-13 14:12:16 +00:00
Joshua Tauberer
fb3045f456 retain backups only for 3 days; beyond that the user is responsible for copying files off of the machine 2014-10-13 14:12:11 +00:00
Joshua Tauberer
495790d81d still didn't get the permissions right, chmod must follow sa-learn's initial creation of files
see #231, #201, b26abc947e, 7ca54a2bfb, dfe0a9f187
2014-10-12 18:05:04 +00:00
Joshua Tauberer
dfe0a9f187 clean up setup/spamassassin.sh 2014-10-12 17:57:04 +00:00
Joshua Tauberer
7ca54a2bfb give dovecot antispam plugin's sa-learn-pipe script permission to write to the bayes files
see #231, #201, b26abc947e.
2014-10-12 17:57:04 +00:00
Joshua Tauberer
a81bf93fdc preparing changelog for next release 2014-10-12 08:47:54 -04:00
Joshua Tauberer
54d01746fe Merge pull request #233 from h8h/patch-3
Jump to the panel_aliases anchor (top) to directly edit the selected alias
2014-10-11 19:30:40 -04:00
h8h
57f8ee0b09 Smoothly scroll to alias edit form. 2014-10-11 21:52:00 +02:00
h8h
64220292f1 Jump to the panel_aliases anchor (top) to directly edit the selected alias 2014-10-11 19:56:36 +02:00
Joshua Tauberer
82851d6d2d suppress "Something went wrong, sorry." when the management daemon's api key has changed 2014-10-11 17:06:22 +00:00
Joshua Tauberer
2f952a7915 delay an ajax call to see if this fixes the problem of the loading indicator not going away after showing the user a panel after login 2014-10-11 17:06:22 +00:00
Joshua Tauberer
f9eceec746 Merge pull request #232 from dhpiggott/spamassassin-permissions-fix
Change owner of spamassassin directory from mail to spampd, closes #231
2014-10-11 13:06:12 -04:00
David Piggott
b26abc947e Change owner of spamassassin directory from mail to spampd, closes #231 2014-10-11 18:00:22 +01:00
Joshua Tauberer
b78eae1351 Merge pull request #230 from dhpiggott/dns-update-fix
Pass additional_records to recursive build_zone calls, closes #229
2014-10-11 12:27:30 -04:00
David Piggott
ca57560f11 Pass additional_records to recursive build_zone calls, closes #229
The problem was that custom records defined for a subdomain where implicit
records are otherwise defined (e.g. A/AAAA records for the root) were ignored.

Though additional_records for a subdomain are processed in the base call to
build_zone (the call for the parent domain), and so custom records that don't
override implicits were working fine, those that overrode implicits were
ignored.

This was because the recursive call to build_zone for the subdomain creates the
implicit records (including A/AAAA records for the root), and so by relying on
the base call to add the additional_records fails because has_rec returned
true.

Adding a subdomain's additional_records in the child call works because has_rec
returns false when testing whether to add an e.g. A/AAAA override for the root,
as the defaults have not yet been added.
2014-10-11 17:04:35 +01:00
Joshua Tauberer
17331e7d82 adding a really slick ssl certificate installation form in the control panel 2014-10-10 15:49:14 +00:00
Joshua Tauberer
5130b279d8 management/mail_log.py also include the previously rotated log file 2014-10-10 13:59:50 +00:00
Joshua Tauberer
aac6e49b94 spelling typo 2014-10-10 13:50:44 +00:00
Joshua Tauberer
2f4eccd9a9 add 'source /etc/mailinabox.conf' to dns.sh so it can be run separately 2014-10-08 12:48:43 +00:00
Joshua Tauberer
ac49912b39 recommend DAVdroid
see http://discourse.mailinabox.email/t/recommend-a-different-android-carddav-and-caldav-android/102/1
2014-10-07 20:53:37 +00:00
Joshua Tauberer
0441a2e2e3 make a self-signed certificate on a non-primary domain a warning rather than an error, fixes #95 2014-10-07 20:41:07 +00:00
Joshua Tauberer
8566b78202 drop webfinger, see #95 2014-10-07 20:30:36 +00:00
Joshua Tauberer
06a8ce1c9d in the admin, show user mailbox sizes, fixes #210 2014-10-07 20:24:11 +00:00
Joshua Tauberer
443b084a17 in the admin, group aliases by domain, fixes #211 2014-10-07 19:47:46 +00:00
Joshua Tauberer
990649af2d in the admin, group users by domain, fixes 209 2014-10-07 19:47:43 +00:00
Joshua Tauberer
6f4d29a410 tweak the new web instructions 2014-10-07 16:17:45 +00:00
Joshua Tauberer
6ab29c3244 add instructions for static web hosting into the control panel 2014-10-07 16:05:42 +00:00
Joshua Tauberer
bf9b770255 sort SSHFP records so that DNS updates don't trigger spurrious zone changes 2014-10-07 15:15:22 +00:00
Joshua Tauberer
9210ebdb9f control panel tweaks 2014-10-07 15:12:35 +00:00
Joshua Tauberer
a56bb984d6 handle catastrophically bad certificates rather than raising an exception 2014-10-07 14:58:21 +00:00
Joshua Tauberer
7d1c0b3834 show SSL certificate expiration info in the control panel even long before certificates expire 2014-10-07 14:49:36 +00:00
Joshua Tauberer
20892b5d5b status check on ns records should now take into account that secondary dns may be customized, see #223 2014-10-05 18:42:52 +00:00
Joshua Tauberer
4cf53cd8ee backup status relativedelta was displaying wrong for deltas greater than 1 month 2014-10-05 18:23:29 +00:00
Joshua Tauberer
711db9352c bootstrap: apt was mangling stdin
When executed "cat bootstrap.sh | bash", apt-get mangled stdin. The script would terminate at the end of the if block containing apt-get (that seems to be as much as bash read from the pipe) and the remainder of the script was output to the console. This was very weird.

Ensuring that apt-get and git have their stdins redirected from /dev/null seems to fix the problem.

see #224
2014-10-05 13:40:12 -04:00
Joshua Tauberer
f42a1c5a74 allow overriding the second nameserver with a secondary/slave server
fixes #151
fixes #223
2014-10-05 14:53:42 +00:00
Joshua Tauberer
092c842a87 split external/custom dns into separate pages in the admin 2014-10-05 13:38:23 +00:00
Joshua Tauberer
d9ecc50119 since the management server binds to 127.0.0.1, must use that and not 'localhost' to connect to it because 'localhost' resolves to the IPv6 ::1 when it is available, see #224 2014-10-05 09:01:26 -04:00
Joshua Tauberer
7c2092d48f remove apache before installing nginx, see #224 2014-10-05 09:01:20 -04:00
Joshua Tauberer
5fd107cae5 more work on making the bash scripts readable 2014-10-04 17:57:26 -04:00
Joshua Tauberer
db0967446b remove unnecessary sudos 2014-10-04 14:06:08 -04:00
Joshua Tauberer
2ff5038c84 replace '.' with 'source' 2014-10-04 14:05:06 -04:00
Joshua Tauberer
4ae76aa2dd dnssec: use RSASHA256 keys for .email domains 2014-10-04 17:29:42 +00:00
h8h
ba33669a62 generate the locales before change to it.
For my german box changing the locale failed:
´´´´/bin/sh: warning: setlocale: LC_ALL: cannot change locale (en_US.UTF-8)
/bin/sh: warning: setlocale: LC_ALL: cannot change locale (en_US.UTF-8)
/bin/sh: warning: setlocale: LC_ALL: cannot change locale (en_US.UTF-8)
/bin/sh: warning: setlocale: LC_ALL: cannot change locale (en_US.UTF-8)
setup/functions.sh: line 6: warning: setlocale: LC_ALL: cannot change locale (en_US.UTF-8)´´´´

see #206 and 4e6d572de9
closes #220
commit modified by joshdata
2014-10-02 11:05:42 +00:00
Joshua Tauberer
779d921410 status checks: put DNSSEC tests in a better order w.r.t. other tests
* If the PRIMARY_HOSTNAME is in a zone with a DS record set at the registrar, show any DNSSEC failure (but only a failure) immediately since it is probably the cause of other DNS errors displayed later.
* For zones, if a DS record is set at the register, do the DNSSEC test first because even the NS test will fail if DNSSEC is improperly configure.
* But if a DS record is not set, the this is just a suggestion to configure DNSSEC so offer the suggestion last --- after mail and web checks.

see https://discourse.mailinabox.email/t/dns-nameserver-gandi-glue-records-issues/105/3
2014-10-01 12:13:11 +00:00
jkaberg
68efef1164 dont log robots.txt and favicon.ico. we should REALLY consider creating seperate include files for *all* of our "apps", this is getting messy.. 2014-09-27 17:04:05 +00:00
Joshua Tauberer
6ecada7eed Merge commit '93a722f' 2014-09-27 16:56:38 +00:00
Joshua Tauberer
94c4352f45 Merge branch 'jmar71n-master' - site-wide bayesean spam filtering 2014-09-27 16:18:55 +00:00
Joshua Tauberer
6dd6353d41 move sa-learn-pipe.sh from /usr to /usr/local 2014-09-27 16:18:40 +00:00
Joshua Tauberer
d06bfa6c1b tweak the site-wide bayesian spam filtering config 2014-09-27 16:18:36 +00:00
Joshua Tauberer
5c7ba2a4c7 preliminary work on a mail.log scanner to report things in the control panel 2014-09-27 13:33:13 +00:00
Joshua Tauberer
e9cc3fdaab make mail instructions clearer and describe greylisting, DMARC policy 2014-09-27 13:32:22 +00:00
Joshua Tauberer
8bd37ea53c add catch-alls to the admin again with nicer instructions 2014-09-27 13:32:22 +00:00
Joshua Tauberer
698ae03505 catch-all addresses should not have precedence over mail users
Aliases have precedence over mail users. A catch-all address would grab mail intended for a mail user and send it elsewhere. This adds some SQL hackery to create dummy aliases for all mail users.

fixes #200
closes #214 another way
2014-09-27 13:32:10 +00:00
Joshua Tauberer
a4c70f7a92 revert dovecot part of 39bca053ed because dovecot started behaving weird and I don't have time to debug it 2014-09-26 22:41:59 +00:00
Joshua Tauberer
39bca053ed add 2048 bits of DH params for nginx, postfix, dovecot
nginx/postfix use a new pre-generated dh2048.pem file. dovecot generates the bits on its own.

ssllabs.com reports that TLS_DHE ciphers went from 1024 to 2048 bits as expected. The ECDHE ciphers remain at 256 bits --- no idea what that really means. (This tests nginx only. I haven't tested postfix/dovecot.)

see https://discourse.mailinabox.email/t/fips-ready-for-ssl-dhec-key-exchange/76/3
2014-09-26 22:09:22 +00:00
Joshua Tauberer
c2eb8e5330 typo in roundcube download URL
see 8e0967dd8e (commitcomment-7940724)
2014-09-26 14:26:45 +00:00
Joshua Tauberer
ab47144ae3 add strict SPF and DMARC records to any subdomains (including custom records) that do not have SPF/DMARC set
closes #208
2014-09-26 14:01:03 +00:00
Joshua Tauberer
9b6f9859d1 dns_update: assume DKIM is present 2014-09-26 14:01:03 +00:00
Joshua Tauberer
4e6d572de9 ensure Python operates in UTF-8 with a consistent locale for all users
fixes #206 (hopefully)
2014-09-26 08:26:09 -04:00
Joshua Tauberer
145186a6b6 link to Modoboa in README 2014-09-26 08:20:13 -04:00
Joshua Tauberer
5714b3c6b7 bump bootstrap.sh to incoming 0.03 tag 2014-09-24 12:48:15 +00:00
Joshua Tauberer
8e0967dd8e if an earlier version of roundcube had already been installed, update to our target version
fixes #195
2014-09-24 12:46:51 +00:00
Joshua Tauberer
5a89f3c633 don't allow catch-all addresses in the admin because they take precedence over mail users and that's counter-intuitive
For now use the command-line tools/mail.py if you need it.

see #200

Revert "Changed incomming-email-input to type text"

This reverts commit 9631fab7b2.
2014-09-24 12:36:47 +00:00
Joshua Tauberer
ed8fb2d06d the latest z-push introduces a new/second USE_FULLEMAIL_FOR_LOGIN parameter
see http://discourse.mailinabox.email/t/activesync-z-push-not-working/94/3
2014-09-24 12:24:35 +00:00
Joshua Tauberer
8c8d9304ac lock z-push to a particular upstream version by fmbiete/Z-Push-contrib commit hash 2014-09-24 12:20:10 +00:00
jmar71n
b5bb12d0d2 enable site-wide bayesian filtering
Create directory in $STORAGE_ROOT for bayes database.

Added --username arg to sa-learn as the user mail does not have permission to edit files in $STORAGE_ROOT. There is probably a better solution to this...
2014-09-20 16:07:30 +01:00
jkaberg
93a722f85b ownCloud (witch is based on SabreDAV) supports sync 2014-09-10 21:22:56 +02:00
121 changed files with 23079 additions and 4163 deletions

26
.editorconfig Normal file
View file

@ -0,0 +1,26 @@
# EditorConfig helps developers define and maintain consistent
# coding styles between different editors and IDEs
# editorconfig.org
root = true
[Vagrantfile]
indent_style = space
indent_size = 2
[*.rb]
indent_style = space
indent_size = 2
[**.html]
indent_style = tab
[**.py]
indent_style = tab
[**]
indent_style = tab
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

5
.gitattributes vendored Normal file
View file

@ -0,0 +1,5 @@
# All text should use Unix-style Line-endings
* text eol=lf
# Except mta-sts.txt (RFC 8461)
mta-sts.txt text eol=crlf

1
.github/FUNDING.yml vendored Normal file
View file

@ -0,0 +1 @@
ko_fi: davness

3
.gitignore vendored
View file

@ -4,3 +4,6 @@ management/__pycache__/
tools/__pycache__/
externals/
.env
.vagrant
api/docs/api-docs.html
mailinabox-ca.crt

3
.style.yapf Normal file
View file

@ -0,0 +1,3 @@
[style]
based_on_style = pep8
use_tabs = True

1245
CHANGELOG.md Normal file

File diff suppressed because it is too large Load diff

48
CODE_OF_CONDUCT.md Normal file
View file

@ -0,0 +1,48 @@
# Mail-in-a-Box Code of Conduct
Mail-in-a-Box is an open source community project about working, as a group, to empower ourselves and others to have control over our own digital communications. Just as we hope to increase technological diversity on the Internet through decentralization, we also believe that diverse viewpoints and voices among our community members foster innovation and creative solutions to the challenges we face.
We are committed to providing a safe, welcoming, and harrassment-free space for collaboration, for everyone, without regard to age, disability, economic situation, ethnicity, gender identity and expression, language fluency, level of knowledge or experience, nationality, personal appearance, race, religion, sexual identity and orientation, or any other attribute. Community comes first. This policy supersedes all other project goals.
The maintainers of Mail-in-a-Box share the dual responsibility of leading by example and enforcing these policies as necessary to maintain an open and welcoming environment. All community members should be excellent to each other.
## Scope
This Code of Conduct applies to all places where Mail-in-a-Box community activity is ocurring, including on GitHub, in discussion forums, on Slack, on social media, and in real life. The Code of Conduct applies not only on websites/at events run by the Mail-in-a-Box community (e.g. our GitHub organization, our Slack team) but also at any other location where the Mail-in-a-Box community is present (e.g. in issues of other GitHub organizations where Mail-in-a-Box community members are discussing problems related to Mail-in-a-Box, or real-life professional conferences), or whenever a Mail-in-a-Box community member is representing Mail-in-a-Box to the public at large or acting on behalf of Mail-in-a-Box.
This code does not apply to activity on a server running Mail-in-a-Box software, unless your server is hosting a service for the Mail-in-a-Box community at large.
## Our Standards
Examples of behavior that contributes to creating a positive environment include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Showing empathy towards other community members
* Making room for new and quieter voices
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or advances
* Trolling, insulting/derogatory/unwelcome comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic address, without explicit permission
* Aggressive and micro-aggressive behavior, such as unconstructive criticism, providing corrections that do not improve the conversation (sometimes referred to as "well actually"s), repeatedly interrupting or talking over someone else, feigning surprise at someone's lack of knowledge or awareness about a topic, or subtle prejudice (for example, comments like "That's so easy my grandmother could do it.", which is prejudicial toward grandmothers).
* Other conduct which could reasonably be considered inappropriate in a professional setting
* Retaliating against anyone who reports a violation of this code.
We will not tolerate harassment. Harassment is any unwelcome or hostile behavior towards another person for any reason. This includes, but is not limited to, offensive verbal comments related to personal characteristics or choices, sexual images or comments, deliberate intimidation, bullying, stalking, following, harassing photography or recording, sustained disruption of discussion or events, nonconsensual publication of private comments, inappropriate physical contact, or unwelcome sexual attention. Conduct need not be intentional to be harassment.
## Enforcement
We will remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not consistent with this Code of Conduct. We may ban, temporarily or permanently, any contributor for violating this code, when appropriate.
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project lead, [Joshua Tauberer](https://razor.occams.info/). All reports will be treated confidentially, impartially, consistently, and swiftly.
Because the need for confidentiality for all parties involved in an enforcement action outweighs the goals of openness, limited information will be shared with the Mail-in-a-Box community regarding enforcement actions that have taken place.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant, version 1.4](http://contributor-covenant.org/version/1/4) and the code of conduct of [Code for DC](http://codefordc.org/resources/codeofconduct.html).

View file

@ -1,3 +1,50 @@
# Contributing
Mail-in-a-Box is an open source project. Your contributions and pull requests are welcome.
## Development
To start developing Mail-in-a-Box, [clone the repository](https://github.com/mail-in-a-box/mailinabox) and familiarize yourself with the code.
$ git clone https://github.com/mail-in-a-box/mailinabox
### Vagrant and VirtualBox
We recommend you use [Vagrant](https://www.vagrantup.com/intro/getting-started/install.html) and [VirtualBox](https://www.virtualbox.org/wiki/Downloads) for development. Please install them first.
With Vagrant set up, the following should boot up Mail-in-a-Box inside a virtual machine:
$ vagrant up --provision
_If you're seeing an error message about your *IP address being listed in the Spamhaus Block List*, simply uncomment the `export SKIP_NETWORK_CHECKS=1` line in `Vagrantfile`. It's normal, you're probably using a dynamic IP address assigned by your Internet providerthey're almost all listed._
### Modifying your `hosts` file
After a while, Mail-in-a-Box will be available at `192.168.56.4` (unless you changed that in your `Vagrantfile`). To be able to use the web-based bits, we recommend to add a hostname to your `hosts` file:
$ echo "192.168.56.4 mailinabox.lan" | sudo tee -a /etc/hosts
You should now be able to navigate to https://mailinabox.lan/admin using your browser. There should be an initial admin user with the name `me@mailinabox.lan` and the password `12345678`.
### Making changes
Your working copy of Mail-in-a-Box will be mounted inside your VM at `/vagrant`. Any change you make locally will appear inside your VM automatically.
Running `vagrant up --provision` again will repeat the installation with your modifications.
Alternatively, you can also ssh into the VM using:
$ vagrant ssh
Once inside the VM, you can re-run individual parts of the setup like in this example:
vm$ cd /vagrant
vm$ sudo setup/owncloud.sh # replace with script you'd like to re-run
### Tests
Mail-in-a-Box needs more tests. If you're still looking for a way to help out, writing and contributing tests would be a great start!
## Public domain
This project is in the public domain. Copyright and related rights in the work worldwide are waived through the [CC0 1.0 Universal public domain dedication][CC0]. See the LICENSE file in this directory.
@ -5,3 +52,7 @@ This project is in the public domain. Copyright and related rights in the work w
All contributions to this project must be released under the same CC0 wavier. By submitting a pull request or patch, you are agreeing to comply with this waiver of copyright interest.
[CC0]: http://creativecommons.org/publicdomain/zero/1.0/
## Code of Conduct
This project has a [Code of Conduct](CODE_OF_CONDUCT.md). Please review it when joining our community.

112
README.md
View file

@ -1,43 +1,101 @@
Mail-in-a-Box
=============
# Power Mail-in-a-Box
## **[Installation](#installation)** (current version: v60.5)
## **[Upgrading Quick Start](#upgrading)**
By [@JoshData](https://github.com/JoshData) and [contributors](https://github.com/mail-in-a-box/mailinabox/graphs/contributors).
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/davness)
Mail-in-a-Box helps individuals take back control of their email by defining a one-click, easy-to-deploy SMTP+everything else server: a mail server in a box.
Power Mail-in-a-Box (a fork of [Mail-in-a-Box](https://mailinabox.email/)) is a complete pre-configured mail appliance, quickly deployable in a matter of minutes.
**Please see [https://mailinabox.email](https://mailinabox.email) for the project's website and setup guide!**
It's main difference to the main project is focused on ad-hoc, advanced features. While Mail-in-a-Box caters to beginners by providing sane configurations, Power Mail-in-a-Box also attempts to cater to advanced users that want deeper levels of customization.
* * *
## Features
- Configure Power Mail-in-a-Box to use an external SMTP relay;
- Brand new admin panel (with up-to-date libraries);
- Perform backups right away from the admin panel;
- Account quotas support (thanks to **@[jrsupplee](https://github.com/jrsupplee/mailinabox)**!)
- Customize TTL's for custom DNS records;
- Publish OpenPGP keys authoritatively via a WKD server;
- - **In the future:** Allow usage of OpenPGP keys to encrypt backups;
- Per-domain nginx configuration;
I am trying to:
## Goals
- **Easy of use** - deployment shouldn't take too many technical details to understand. Power Mail-in-a-Box already comes with default configurations which should be good for most users.
- **Privacy, security and independence** - keeping your mail and data safe from advertisers and prying eyes.
- **Accessible customizability** - bring the features closer to the people instead of tucking them away in configuration files.
- **Customizability potential** - allow for deep customization by power users.
- **Concentration** - all the services you need in just one box.
- **Support** - support a wide range of operating systems when possible, without compromising the codebase as a whole.
- **Lightweight** - should be able to run even with very limited resources.
* Make deploying a good mail server easy.
* Promote [decentralization](http://redecentralize.org/), innovation, and privacy on the web.
* Have automated, auditable, and [idempotent](http://sharknet.us/2014/02/01/automated-configuration-management-challenges-with-idempotency/) configuration.
* **Not** be a mail server that the NSA cannot hack.
* **Not** be customizable by power users.
## Non-goals
- **Scalability** - this appliance is geared towards individuals and small/mid-sized organizations. If your use case is mission-critical it probably is a better idea to shop for a product that provides support.
- **Portability** - I didn't figure out yet a way to easily transition from Mail-in-a-Box to Power Mail-in-a-Box.
The long-term goal is to have this be a one-click email appliance with *no* user-configurable setup options.
# Minimum Pre-requisites
The machine this appliance will be installed on needs to have the following specs (or better). Most cloud providers are able to provide VM's that satisfy these specs at relatively low cost.
For more background, see [The Rationale](https://github.com/mail-in-a-box/mailinabox/wiki).
<small>_These specs depend on the number of users being served and/or amount of traffic_</small>
- 1 CPU core;
- 512MB of RAM (**at least 1GB** is recommended);
- 10GB of disk;
- **One of the following operating systems:**
- - Debian GNU/Linux 11 (Bullseye)
- - Ubuntu LTS 20.04 (Focal Fossa)
- - Ubuntu LTS 22.04 (Jammy Jellyfish)
This setup is what has been powering my own personal email since September 2013.
## Legacy Support
The following distributions are no longer supported for the latest version, but they used to be supported at a earlier time:
- **Debian 10 (Buster)** <= **v56.5**
The Box
-------
<small>_These network requirements are usually not provided by residential ISP's. They are not **strictly required** for Power Mail-in-a-Box to install, but it will take more work to get it running as intended._</small>
- Static, public IPv4 (most residential connections **do not** provide static addresses);
- - If the machine is behind a NAT, manual configuration might be required.
- Reverse DNS for that IPv4 address (**Caution:** some cloud providers do not provide this);
- You should be able to edit the firewall for that address. **In particular, outbound port 25 should not be blocked.**
Mail-in-a-Box turns a fresh Ubuntu 14.04 LTS 64-bit machine into a working mail server, including SMTP ([postfix](http://www.postfix.org/)), IMAP ([dovecot](http://dovecot.org/)), Exchange ActiveSync ([z-push](https://github.com/fmbiete/Z-Push-contrib)), webmail ([Roundcube](http://roundcube.net/)), spam filtering ([spamassassin](https://spamassassin.apache.org/)), greylisting ([postgrey](http://postgrey.schweikert.ch/)), CardDAV/CalDAV ([ownCloud](http://owncloud.org/)), DNS, [SPF](https://en.wikipedia.org/wiki/Sender_Policy_Framework), DKIM ([OpenDKIM](http://www.opendkim.org/)), [DMARC](https://en.wikipedia.org/wiki/DMARC), [DNSSEC](https://en.wikipedia.org/wiki/DNSSEC), [DANE TLSA](https://en.wikipedia.org/wiki/DNS-based_Authentication_of_Named_Entities), [SSHFP](https://tools.ietf.org/html/rfc4255), and basic system services like a firewall, intrusion protection, and setting the system clock.
# Firewall
If the machine is behind an external firewall or NAT, the following **inbound ports SHOULD** be open to external traffic:
The Acknowledgements
--------------------
- `25/tcp`
- `53/tcp`
- `53/udp`
- `80/tcp`
- `443/tcp`
- `465/tcp`
- `587/tcp`
- `993/tcp`
- `995/tcp`
- `4190/tcp`
This project was inspired in part by the ["NSA-proof your email in 2 hours"](http://sealedabstract.com/code/nsa-proof-your-e-mail-in-2-hours/) blog post by Drew Crawford, [Sovereign](https://github.com/al3x/sovereign) by Alex Payne, and conversations with <a href="http://twitter.com/shevski" target="_blank">@shevski</a>, <a href="https://github.com/konklone" target="_blank">@konklone</a>, and <a href="https://github.com/gregelin" target="_blank">@GregElin</a>.
# Installation
Mail-in-a-Box is similar to [iRedMail](http://www.iredmail.org/).
1. Power Mail-in-a-Box uses `ufw` to configure it's internal firewall. If your cloud provider requires you to use another tool (usually it does not, but <small>\*cough\* _Oracle Cloud_ \*cough\*</small>), you can follow [these instructions](https://github.com/ddavness/power-mailinabox/discussions/21).
The History
-----------
2. Make sure `curl` is installed and locales are configured correctly - you'll want to make sure the primary locale is set to `en_US.UTF-8`:
```
sudo apt install curl locales
sudo dpkg-reconfigure locales
```
* In 2007 I wrote a relatively popular Mozilla Thunderbird extension that added client-side SPF and DKIM checks to mail to warn users about possible phishing: [add-on page](https://addons.mozilla.org/en-us/thunderbird/addon/sender-verification-anti-phish/), [source](https://github.com/JoshData/thunderbird-spf).
* Mail-in-a-Box was a semifinalist in the 2014 [Knight News Challenge](https://www.newschallenge.org/challenge/2014/submissions/mail-in-a-box), but it was not selected as a winner.
* Mail-in-a-Box hit the front page of Hacker News in [April](https://news.ycombinator.com/item?id=7634514) and [September](https://news.ycombinator.com/item?id=8276171) 2014.
3. Run the following command, and then follow the instructions that appear on the screen:
```
curl -L https://power-mailinabox.net/setup.sh | sudo bash
```
# Upgrading
To upgrade an existing box to the latest version, run the same command as you do to perform a new installation:
```
curl -L https://power-mailinabox.net/setup.sh | sudo bash
```
## Installing or upgrading to a different version
If for some reason you wish to install a different version (for example, an older version for a workaround, or a beta/release candidate version for testing), you can use the following command.
```
curl -L https://power-mailinabox.net/<VERSION>/setup.sh | sudo bash
```
Where `<VERSION>` is the version you want to install. (**Example:** `v60.0`).
> ⚠️ **Downgrading might not always be possible and is not supported!** Make sure you know what you're doing before doing so.

83
Vagrantfile vendored
View file

@ -1,32 +1,75 @@
# -*- mode: ruby -*-
# vi: set ft=ruby :
ip = 2
machines = [
{
'iso' => "debian/buster64",
'host' => "buster"
},
{
'iso' => "generic/ubuntu2004",
'host' => "focal"
},
{
'iso' => "debian/bullseye64",
'host' => "bullseye"
},
{
'iso' => "generic/ubuntu2204",
'host' => "jammy"
},
]
Vagrant.configure("2") do |config|
config.vm.box = "ubuntu14.04"
config.vm.box_url = "http://cloud-images.ubuntu.com/vagrant/trusty/current/trusty-server-cloudimg-amd64-vagrant-disk1.box"
config.vm.provider :virtualbox do |vb|
vb.customize ["modifyvm", :id, "--cpus", 1, "--memory", 768]
end
config.vm.provider :libvirt do |v|
v.memory = 768
v.cpus = 1
v.nested = true
end
config.vm.provider :kvm do |kvm|
kvm.memory_size = '768m'
end
# 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
# the machine's box will let anyone log into it. So instead we'll put the
# machine on a private network.
config.vm.hostname = "mailinabox"
config.vm.network "private_network", ip: "192.168.50.4"
config.vm.synced_folder ".", "/vagrant", nfs_version: "4"
config.vm.provision :shell, :inline => <<-SH
# Set environment variables so that the setup script does
# not ask any questions during provisioning. We'll let the
# machine figure out its own public IP and it'll take a
# subdomain on our justtesting.email domain so we can get
# started quickly.
export NONINTERACTIVE=1
export PUBLIC_IP=auto
export PUBLIC_IPV6=auto
export PRIMARY_HOSTNAME=auto-easy
export CSR_COUNTRY=US
#export SKIP_NETWORK_CHECKS=1
(0..(machines.size - 1)).each do |n|
node = machines[n]
config.vm.define node['host'] do |m|
m.vm.box = node['iso']
m.vm.hostname = "#{node['host']}.mailinabox.lan"
m.vm.network "private_network", ip: "192.168.168.#{ip+n}"
# Start the setup script.
cd /vagrant
setup/start.sh
SH
m.vm.provision "shell", :inline => <<-SH
git config --global --add safe.directory /vagrant
# Set environment variables so that the setup script does
# not ask any questions during provisioning. We'll let the
# machine figure out its own public IP.
export NONINTERACTIVE=1
export PUBLIC_IP=192.168.168.#{ip+n}
export PUBLIC_IPV6=auto
export PRIVATE_IP=192.168.168.#{ip+n}
export PRIMARY_HOSTNAME=\"#{node['host']}.mailinabox.lan\"
export SKIP_NETWORK_CHECKS=1
# Start the setup script.
cd /vagrant
setup/start.sh
# After setup is done, fully open the ssh ports again
ufw allow ssh
SH
m.vm.provision "shell", run: "always", :inline => <<-SH
service mailinabox restart
SH
end
end
end

23
api/docs/generate-docs.sh Executable file
View file

@ -0,0 +1,23 @@
#!/usr/bin/env sh
# Requirements:
# - Node.js
# - redoc-cli (`npm install redoc-cli -g`)
redoc-cli bundle ../mailinabox.yml \
-t template.hbs \
-o api-docs.html \
--templateOptions.metaDescription="Mail-in-a-Box HTTP API" \
--title="Mail-in-a-Box HTTP API" \
--options.expandSingleSchemaField \
--options.hideSingleRequestSampleTab \
--options.jsonSampleExpandLevel=10 \
--options.hideDownloadButton \
--options.theme.logo.maxHeight=180px \
--options.theme.logo.maxWidth=180px \
--options.theme.colors.primary.main="#C52" \
--options.theme.typography.fontSize=16px \
--options.theme.typography.fontFamily="Raleway, sans-serif" \
--options.theme.typography.headings.fontFamily="Ubuntu, Arial, sans-serif" \
--options.theme.typography.code.fontSize=15px \
--options.theme.typography.code.fontFamily='"Source Code Pro", monospace'

31
api/docs/template.hbs Normal file
View file

@ -0,0 +1,31 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf8" />
<title>{{title}}</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="{{templateOptions.metaDescription}}" />
<link rel="icon" type="image/png" href="https://mailinabox.email/static/logo_small.png">
<link rel="apple-touch-icon" type="image/png" href="https://mailinabox.email/static/logo_small.png">
<link href="https://fonts.googleapis.com/css?family=Raleway:400,700" rel="stylesheet" />
<link href="https://fonts.googleapis.com/css?family=Ubuntu:300" rel="stylesheet" />
<link href="https://fonts.googleapis.com/css?family=Source+Code+Pro:500" rel="stylesheet" />
<style>
body {
margin: 0;
padding: 0;
}
h1 {
color: #000 !important;
}
</style>
{{{redocHead}}}
</head>
<body>
{{{redocHTML}}}
</body>
</html>

3424
api/mailinabox.yml Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,63 @@
## NOTE: This file is automatically generated by Mail-in-a-Box.
## Do not edit this file. It is continually updated by
## Mail-in-a-Box and your changes will be lost.
##
## Mail-in-a-Box machines are not meant to be modified.
## If you modify any system configuration you are on
## your own --- please do not ask for help from us.
namespace inbox {
# Automatically create & subscribe some folders.
# * Create and subscribe the INBOX folder.
# * Our sieve rule for spam expects that the Spam folder exists.
# * Z-Push must be configured with the same settings in conf/zpush/backend_imap.php (#580).
# MUA notes:
# * Roundcube will show an error if the user tries to delete a message before the Trash folder exists (#359).
# * K-9 mail will poll every 90 seconds if a Drafts folder does not exist.
# * Apple's OS X Mail app will create 'Sent Messages' if it doesn't see a folder with the \Sent flag (#571, #573) and won't be able to archive messages unless 'Archive' exists (#581).
# * Thunderbird's default in its UI is 'Archives' (plural) but it will configure new accounts to use whatever we say here (#581).
# auto:
# 'create' will automatically create this mailbox.
# 'subscribe' will both create and subscribe to the mailbox.
# special_use is a space separated list of IMAP SPECIAL-USE
# attributes as specified by RFC 6154:
# \All \Archive \Drafts \Flagged \Junk \Sent \Trash
mailbox INBOX {
auto = subscribe
}
mailbox Spam {
special_use = \Junk
auto = subscribe
}
mailbox Drafts {
special_use = \Drafts
auto = subscribe
}
mailbox Sent {
special_use = \Sent
auto = subscribe
}
mailbox Trash {
special_use = \Trash
auto = subscribe
}
mailbox Archive {
special_use = \Archive
auto = subscribe
}
# dovevot's standard mailboxes configuration file marks two sent folders
# with the \Sent attribute, just in case clients don't agree about which
# they're using. We'll keep that, plus add Junk as an alterative for Spam.
# These are not auto-created.
mailbox "Sent Messages" {
special_use = \Sent
}
mailbox Junk {
special_use = \Junk
}
}

View file

@ -0,0 +1,94 @@
##
## IMAP specific settings
##
# If nothing happens for this long while client is IDLEing, move the connection
# to imap-hibernate process and close the old imap process. This saves memory,
# because connections use very little memory in imap-hibernate process. The
# downside is that recreating the imap process back uses some resources.
#imap_hibernate_timeout = 0
# Maximum IMAP command line length. Some clients generate very long command
# lines with huge mailboxes, so you may need to raise this if you get
# "Too long argument" or "IMAP command line too large" errors often.
#imap_max_line_length = 64k
# IMAP logout format string:
# %i - total number of bytes read from client
# %o - total number of bytes sent to client
# %{fetch_hdr_count} - Number of mails with mail header data sent to client
# %{fetch_hdr_bytes} - Number of bytes with mail header data sent to client
# %{fetch_body_count} - Number of mails with mail body data sent to client
# %{fetch_body_bytes} - Number of bytes with mail body data sent to client
# %{deleted} - Number of mails where client added \Deleted flag
# %{expunged} - Number of mails that client expunged, which does not
# include automatically expunged mails
# %{autoexpunged} - Number of mails that were automatically expunged after
# client disconnected
# %{trashed} - Number of mails that client copied/moved to the
# special_use=\Trash mailbox.
# %{appended} - Number of mails saved during the session
#imap_logout_format = in=%i out=%o
# Override the IMAP CAPABILITY response. If the value begins with '+',
# add the given capabilities on top of the defaults (e.g. +XFOO XBAR).
#imap_capability =
# How long to wait between "OK Still here" notifications when client is
# IDLEing.
#imap_idle_notify_interval = 2 mins
imap_idle_notify_interval=4 mins
# ID field names and values to send to clients. Using * as the value makes
# Dovecot use the default value. The following fields have default values
# currently: name, version, os, os-version, support-url, support-email.
#imap_id_send =
# ID fields sent by client to log. * means everything.
#imap_id_log =
# Workarounds for various client bugs:
# delay-newmail:
# Send EXISTS/RECENT new mail notifications only when replying to NOOP
# and CHECK commands. Some clients ignore them otherwise, for example OSX
# Mail (<v2.1). Outlook Express breaks more badly though, without this it
# may show user "Message no longer in server" errors. Note that OE6 still
# breaks even with this workaround if synchronization is set to
# "Headers Only".
# tb-extra-mailbox-sep:
# Thunderbird gets somehow confused with LAYOUT=fs (mbox and dbox) and
# adds extra '/' suffixes to mailbox names. This option causes Dovecot to
# ignore the extra '/' instead of treating it as invalid mailbox name.
# tb-lsub-flags:
# Show \Noselect flags for LSUB replies with LAYOUT=fs (e.g. mbox).
# This makes Thunderbird realize they aren't selectable and show them
# greyed out, instead of only later giving "not selectable" popup error.
#
# The list is space-separated.
#imap_client_workarounds =
# Host allowed in URLAUTH URLs sent by client. "*" allows all.
#imap_urlauth_host =
# What happens when FETCH fails due to some internal error:
# disconnect-immediately:
# The FETCH is aborted immediately and the IMAP client is disconnected.
# disconnect-after:
# The FETCH runs for all the requested mails returning as much data as
# possible. The client is finally disconnected without a tagged reply.
# no-after:
# Same as disconnect-after, but tagged NO reply is sent instead of
# disconnecting the client. If the client attempts to FETCH the same failed
# mail more than once, the client is disconnected. This is to avoid clients
# from going into infinite loops trying to FETCH a broken mail.
#imap_fetch_failure = disconnect-immediately
mail_plugins = $mail_plugins quota
protocol imap {
# Space separated list of plugins to load (default is global mail_plugins).
mail_plugins = $mail_plugins antispam imap_quota
# Maximum number of IMAP connections allowed for a user from each IP address.
# NOTE: The username is compared case-sensitively.
#mail_max_userip_connections = 10
}

View file

@ -0,0 +1,98 @@
##
## Quota configuration.
##
# Note that you also have to enable quota plugin in mail_plugins setting.
# <doc/wiki/Quota.txt>
##
## Quota limits
##
# Quota limits are set using "quota_rule" parameters. To get per-user quota
# limits, you can set/override them by returning "quota_rule" extra field
# from userdb. It's also possible to give mailbox-specific limits, for example
# to give additional 100 MB when saving to Trash:
plugin {
#quota_rule = *:storage=1G
#quota_rule2 = Trash:storage=+100M
# LDA/LMTP allows saving the last mail to bring user from under quota to
# over quota, if the quota doesn't grow too high. Default is to allow as
# long as quota will stay under 10% above the limit. Also allowed e.g. 10M.
#quota_grace = 10%%
# Quota plugin can also limit the maximum accepted mail size.
#quota_max_mail_size = 100M
}
##
## Quota warnings
##
# You can execute a given command when user exceeds a specified quota limit.
# Each quota root has separate limits. Only the command for the first
# exceeded limit is excecuted, so put the highest limit first.
# The commands are executed via script service by connecting to the named
# UNIX socket (quota-warning below).
# Note that % needs to be escaped as %%, otherwise "% " expands to empty.
plugin {
#quota_warning = storage=95%% quota-warning 95 %u
#quota_warning2 = storage=80%% quota-warning 80 %u
}
# Example quota-warning service. The unix listener's permissions should be
# set in a way that mail processes can connect to it. Below example assumes
# that mail processes run as vmail user. If you use mode=0666, all system users
# can generate quota warnings to anyone.
#service quota-warning {
# executable = script /usr/local/bin/quota-warning.sh
# user = dovecot
# unix_listener quota-warning {
# user = vmail
# }
#}
##
## Quota backends
##
# Multiple backends are supported:
# dirsize: Find and sum all the files found from mail directory.
# Extremely SLOW with Maildir. It'll eat your CPU and disk I/O.
# dict: Keep quota stored in dictionary (eg. SQL)
# maildir: Maildir++ quota
# fs: Read-only support for filesystem quota
plugin {
quota = maildir
quota_grace = 10%
quota_status_success = DUNNO
quota_status_nouser = DUNNO
quota_status_overquota = "522 5.2.2 Mailbox is full"
#quota = dirsize:User quota
#quota = maildir:User quota
#quota = dict:User quota::proxy::quota
#quota = fs:User quota
}
service quota-status {
executable = quota-status -p postfix
inet_listener {
port = 12340
}
}
# Multiple quota roots are also possible, for example this gives each user
# their own 100MB quota and one shared 1GB quota within the domain:
plugin {
#quota = dict:user::proxy::quota
#quota2 = dict:domain:%d:proxy::quota_domain
#quota_rule = *:storage=102400
#quota2_rule = *:storage=1048576
}

View file

@ -0,0 +1,22 @@
# Fail2Ban filter Dovecot authentication and pop3/imap/managesieve server
# For Mail-in-a-Box
[INCLUDES]
before = common.conf
[Definition]
_daemon = (auth|dovecot(-auth)?|auth-worker)
failregex = ^%(__prefix_line)s(pop3|imap|managesieve)-login: (Info: )?(Aborted login|Disconnected)(: Inactivity)? \(((no auth attempts|auth failed, \d+ attempts)( in \d+ secs)?|tried to use (disabled|disallowed) \S+ auth)\):( user=<\S*>,)?( method=\S+,)? rip=<HOST>, lip=(\d{1,3}\.){3}\d{1,3}(, TLS( handshaking)?(: Disconnected)?)?(, session=<\S+>)?\s*$
ignoreregex =
# DEV Notes:
# * the first regex is essentially a copy of pam-generic.conf
# * Probably doesn't do dovecot sql/ldap backends properly
#
# Author: Martin Waschbuesch
# Daniel Black (rewrote with begin and end anchors)
# Mail-in-a-Box (swapped session=...)

View file

@ -0,0 +1,12 @@
# Fail2Ban filter Mail-in-a-Box management daemon
[INCLUDES]
before = common.conf
[Definition]
_daemon = mailinabox
failregex = Mail-in-a-Box Management Daemon: Failed login attempt from ip <HOST> - timestamp .*
ignoreregex =

View file

@ -0,0 +1,7 @@
[INCLUDES]
before = common.conf
[Definition]
failregex=<HOST> - .*GET /admin/munin/.* HTTP/1.1\" 401.*
ignoreregex =

View file

@ -0,0 +1,8 @@
[INCLUDES]
before = common.conf
[Definition]
datepattern = %%Y-%%m-%%d %%H:%%M:%%S
failregex=Login failed: .*Remote IP: '<HOST>[\)']
ignoreregex =

View file

@ -0,0 +1,7 @@
[INCLUDES]
before = common.conf
[Definition]
failregex=postfix/submission/smtpd.*warning.*\[<HOST>\]: .* authentication (failed|aborted)
ignoreregex =

View file

@ -0,0 +1,9 @@
[INCLUDES]
before = common.conf
[Definition]
failregex = IMAP Error: Login failed for .*? from <HOST>\. AUTHENTICATE.*
ignoreregex =

86
conf/fail2ban/jails.conf Normal file
View file

@ -0,0 +1,86 @@
# Fail2Ban configuration file for Mail-in-a-Box. Do not edit.
# This file is re-generated on updates.
[DEFAULT]
# Whitelist our own IP addresses. 127.0.0.1/8 is the default. But our status checks
# ping services over the public interface so we should whitelist that address of
# ours too. The string is substituted during installation.
ignoreip = 127.0.0.1/8 PUBLIC_IP ::1 PUBLIC_IPV6
[dovecot]
enabled = true
filter = dovecotimap
logpath = /var/log/mail.log
findtime = 30
maxretry = 20
[miab-management]
enabled = true
filter = miab-management-daemon
port = http,https
logpath = /var/log/syslog
maxretry = 20
findtime = 30
[miab-munin]
enabled = true
port = http,https
filter = miab-munin
logpath = /var/log/nginx/access.log
maxretry = 20
findtime = 30
[miab-owncloud]
enabled = true
port = http,https
filter = miab-owncloud
logpath = STORAGE_ROOT/owncloud/nextcloud.log
maxretry = 20
findtime = 120
[miab-postfix465]
enabled = true
port = 465
filter = miab-postfix-submission
logpath = /var/log/mail.log
maxretry = 20
findtime = 30
[miab-postfix587]
enabled = true
port = 587
filter = miab-postfix-submission
logpath = /var/log/mail.log
maxretry = 20
findtime = 30
[miab-roundcube]
enabled = true
port = http,https
filter = miab-roundcube
logpath = /var/log/roundcubemail/errors.log
maxretry = 20
findtime = 30
[recidive]
enabled = true
maxretry = 10
action = iptables-allports[name=recidive]
# In the recidive section of jail.conf the action contains:
#
# action = iptables-allports[name=recidive]
# sendmail-whois-lines[name=recidive, logpath=/var/log/fail2ban.log]
#
# The last line on the action will sent an email to the configured address. This mail will
# notify the administrator that someone has been repeatedly triggering one of the other jails.
# By default we don't configure this address and no action is required from the admin anyway.
# So the notification is ommited. This will prevent message appearing in the mail.log that mail
# can't be delivered to fail2ban@$HOSTNAME.
[postfix-sasl]
enabled = true
[sshd]
enabled = true
maxretry = 7
bantime = 3600

126
conf/ios-profile.xml Normal file
View file

@ -0,0 +1,126 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<!--
iOS/OS X Configuration Profile
Mobileconfig for iOS/OS X users to setup IMAP, SMTP, Contacts & Calendar
https://developer.apple.com/library/ios/featuredarticles/iPhoneConfigurationProfileRef/Introduction/Introduction.html
-->
<plist version="1.0">
<dict>
<key>PayloadContent</key>
<array>
<dict>
<key>CalDAVAccountDescription</key>
<string>PRIMARY_HOSTNAME calendar</string>
<key>CalDAVHostName</key>
<string>PRIMARY_HOSTNAME</string>
<key>CalDAVPort</key>
<real>443</real>
<key>CalDAVUseSSL</key>
<true/>
<key>PayloadDescription</key>
<string>PRIMARY_HOSTNAME (Mail-in-a-Box)</string>
<key>PayloadDisplayName</key>
<string>PRIMARY_HOSTNAME calendar</string>
<key>PayloadIdentifier</key>
<string>email.mailinabox.mobileconfig.PRIMARY_HOSTNAME.CalDAV</string>
<key>PayloadOrganization</key>
<string></string>
<key>PayloadType</key>
<string>com.apple.caldav.account</string>
<key>PayloadUUID</key>
<string>UUID1</string>
<key>PayloadVersion</key>
<integer>1</integer>
</dict>
<dict>
<key>EmailAccountDescription</key>
<string>PRIMARY_HOSTNAME mail</string>
<key>EmailAccountType</key>
<string>EmailTypeIMAP</string>
<key>IncomingMailServerAuthentication</key>
<string>EmailAuthPassword</string>
<key>IncomingMailServerHostName</key>
<string>PRIMARY_HOSTNAME</string>
<key>IncomingMailServerPortNumber</key>
<integer>993</integer>
<key>IncomingMailServerUseSSL</key>
<true/>
<key>OutgoingMailServerAuthentication</key>
<string>EmailAuthPassword</string>
<key>OutgoingMailServerHostName</key>
<string>PRIMARY_HOSTNAME</string>
<key>OutgoingMailServerPortNumber</key>
<integer>465</integer>
<key>OutgoingMailServerUseSSL</key>
<true/>
<key>OutgoingPasswordSameAsIncomingPassword</key>
<true/>
<key>PayloadDescription</key>
<string>PRIMARY_HOSTNAME (Mail-in-a-Box)</string>
<key>PayloadDisplayName</key>
<string>PRIMARY_HOSTNAME mail</string>
<key>PayloadIdentifier</key>
<string>email.mailinabox.mobileconfig.PRIMARY_HOSTNAME.E-Mail</string>
<key>PayloadOrganization</key>
<string></string>
<key>PayloadType</key>
<string>com.apple.mail.managed</string>
<key>PayloadUUID</key>
<string>UUID2</string>
<key>PayloadVersion</key>
<integer>1</integer>
<key>PreventAppSheet</key>
<false/>
<key>PreventMove</key>
<false/>
<key>SMIMEEnabled</key>
<false/>
</dict>
<dict>
<key>CardDAVAccountDescription</key>
<string>PRIMARY_HOSTNAME contacts</string>
<key>CardDAVHostName</key>
<string>PRIMARY_HOSTNAME</string>
<key>CardDAVPort</key>
<integer>443</integer>
<key>CardDAVPrincipalURL</key>
<string>/cloud/remote.php/carddav/addressbooks/</string>
<key>CardDAVUseSSL</key>
<true/>
<key>PayloadDescription</key>
<string>PRIMARY_HOSTNAME (Mail-in-a-Box)</string>
<key>PayloadDisplayName</key>
<string>PRIMARY_HOSTNAME contacts</string>
<key>PayloadIdentifier</key>
<string>email.mailinabox.mobileconfig.PRIMARY_HOSTNAME.carddav</string>
<key>PayloadOrganization</key>
<string></string>
<key>PayloadType</key>
<string>com.apple.carddav.account</string>
<key>PayloadUUID</key>
<string>UUID3</string>
<key>PayloadVersion</key>
<integer>1</integer>
</dict>
</array>
<key>PayloadDescription</key>
<string>PRIMARY_HOSTNAME (Mail-in-a-Box)</string>
<key>PayloadDisplayName</key>
<string>PRIMARY_HOSTNAME</string>
<key>PayloadIdentifier</key>
<string>email.mailinabox.mobileconfig.PRIMARY_HOSTNAME</string>
<key>PayloadOrganization</key>
<string></string>
<key>PayloadRemovalDisallowed</key>
<false/>
<key>PayloadType</key>
<string>Configuration</string>
<key>PayloadUUID</key>
<string>UUID4</string>
<key>PayloadVersion</key>
<integer>1</integer>
</dict>
</plist>

11
conf/mailinabox.service Normal file
View file

@ -0,0 +1,11 @@
[Unit]
Description=Mail-in-a-Box System Management Service
After=multi-user.target
[Service]
Type=idle
IgnoreSIGPIPE=False
ExecStart=/usr/local/lib/mailinabox/start
[Install]
WantedBy=multi-user.target

View file

@ -1,125 +0,0 @@
#! /bin/sh
### BEGIN INIT INFO
# Provides: mailinabox
# Required-Start: $all
# Required-Stop: $all
# Default-Start: 2 3 4 5
# Default-Stop: 0 1 6
# Short-Description: Start and stop the Mail-in-a-Box management daemon.
# Description: Start and stop the Mail-in-a-Box management daemon.
### END INIT INFO
# Adapted from http://blog.codefront.net/2007/06/11/nginx-php-and-a-php-fastcgi-daemon-init-script/
PATH=/sbin:/usr/sbin:/bin:/usr/bin
DESC="Mail-in-a-Box Management Daemon"
NAME=mailinabox
DAEMON=/usr/local/bin/mailinabox-daemon
PIDFILE=/var/run/$NAME.pid
SCRIPTNAME=/etc/init.d/$NAME
# Exit if the package is not installed
[ -x "$DAEMON" ] || exit 0
# Set defaults.
START=yes
EXEC_AS_USER=root
# Read configuration variable file if it is present
[ -r /etc/default/$NAME ] && . /etc/default/$NAME
# Load the VERBOSE setting and other rcS variables
. /lib/init/vars.sh
# Define LSB log_* functions.
# Depend on lsb-base (>= 3.0-6) to ensure that this file is present.
. /lib/lsb/init-functions
# If the daemon is not enabled, give the user a warning and then exit,
# unless we are stopping the daemon
if [ "$START" != "yes" -a "$1" != "stop" ]; then
log_warning_msg "To enable $NAME, edit /etc/default/$NAME and set START=yes"
exit 0
fi
# Process configuration
#export ...
DAEMON_ARGS=""
do_start()
{
# Return
# 0 if daemon has been started
# 1 if daemon was already running
# 2 if daemon could not be started
start-stop-daemon --start --quiet --pidfile $PIDFILE --exec $DAEMON --test > /dev/null \
|| return 1
start-stop-daemon --start --quiet --pidfile $PIDFILE --exec $DAEMON \
--background --make-pidfile --chuid $EXEC_AS_USER --startas $DAEMON -- \
$DAEMON_ARGS \
|| return 2
}
do_stop()
{
# Return
# 0 if daemon has been stopped
# 1 if daemon was already stopped
# 2 if daemon could not be stopped
# other if a failure occurred
start-stop-daemon --stop --quiet --retry=TERM/30/KILL/5 --pidfile $PIDFILE > /dev/null # --name $DAEMON
RETVAL="$?"
[ "$RETVAL" = 2 ] && return 2
# Wait for children to finish too if this is a daemon that forks
# and if the daemon is only ever run from this initscript.
# If the above conditions are not satisfied then add some other code
# that waits for the process to drop all resources that could be
# needed by services started subsequently. A last resort is to
# sleep for some time.
start-stop-daemon --stop --quiet --oknodo --retry=0/30/KILL/5 --exec $DAEMON
[ "$?" = 2 ] && return 2
# Many daemons don't delete their pidfiles when they exit.
rm -f $PIDFILE
return "$RETVAL"
}
case "$1" in
start)
[ "$VERBOSE" != no ] && log_daemon_msg "Starting $DESC" "$NAME"
do_start
case "$?" in
0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;;
2) [ "$VERBOSE" != no ] && log_end_msg 1 ;;
esac
;;
stop)
[ "$VERBOSE" != no ] && log_daemon_msg "Stopping $DESC" "$NAME"
do_stop
case "$?" in
0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;;
2) [ "$VERBOSE" != no ] && log_end_msg 1 ;;
esac
;;
restart|force-reload)
log_daemon_msg "Restarting $DESC" "$NAME"
do_stop
case "$?" in
0|1)
do_start
case "$?" in
0) log_end_msg 0 ;;
1) log_end_msg 1 ;; # Old process is still running
*) log_end_msg 1 ;; # Failed to start
esac
;;
*)
# Failed to stop
log_end_msg 1
;;
esac
;;
*)
echo "Usage: $SCRIPTNAME {start|stop|restart|force-reload}" >&2
exit 3
;;
esac

View file

@ -0,0 +1,44 @@
<?xml version="1.0"?>
<clientConfig version="1.1">
<emailProvider id="PRIMARY_HOSTNAME">
<domain>PRIMARY_HOSTNAME</domain>
<displayName>PRIMARY_HOSTNAME (Mail-in-a-Box)</displayName>
<displayShortName>PRIMARY_HOSTNAME</displayShortName>
<incomingServer type="imap">
<hostname>PRIMARY_HOSTNAME</hostname>
<port>993</port>
<socketType>SSL</socketType>
<username>%EMAILADDRESS%</username>
<authentication>password-cleartext</authentication>
</incomingServer>
<outgoingServer type="smtp">
<hostname>PRIMARY_HOSTNAME</hostname>
<port>465</port>
<socketType>SSL</socketType>
<username>%EMAILADDRESS%</username>
<authentication>password-cleartext</authentication>
<addThisServer>true</addThisServer>
<useGlobalPreferredServer>false</useGlobalPreferredServer>
</outgoingServer>
<documentation url="https://PRIMARY_HOSTNAME/">
<descr lang="en">PRIMARY_HOSTNAME website.</descr>
</documentation>
</emailProvider>
<webMail>
<loginPage url="https://PRIMARY_HOSTNAME/mail/" />
<loginPageInfo url="https://PRIMARY_HOSTNAME/mail/" >
<username>%EMAILADDRESS%</username>
<usernameField id="rcmloginuser" name="_user" />
<passwordField id="rcmloginpwd" name="_pass" />
<loginButton id="rcmloginsubmit" />
</loginPageInfo>
</webMail>
<clientConfigUpdate url="https://PRIMARY_HOSTNAME/.well-known/autoconfig/mail/config-v1.1.xml" />
</clientConfig>

4
conf/mta-sts.txt Normal file
View file

@ -0,0 +1,4 @@
version: STSv1
mode: MODE
mx: PRIMARY_HOSTNAME
max_age: 604800

10
conf/munin.service Normal file
View file

@ -0,0 +1,10 @@
[Unit]
Description=Munin System Monitoring Startup Script
After=multi-user.target
[Service]
Type=idle
ExecStart=/usr/local/lib/mailinabox/munin_start.sh
[Install]
WantedBy=multi-user.target

View file

@ -0,0 +1,58 @@
root $ROOT;
# ADDITIONAL DIRECTIVES HERE
location = /robots.txt {
log_not_found off;
access_log off;
}
location = /favicon.ico {
log_not_found off;
access_log off;
}
location = /mailinabox.mobileconfig {
alias /var/lib/mailinabox/mobileconfig.xml;
}
location = /.well-known/autoconfig/mail/config-v1.1.xml {
alias /var/lib/mailinabox/mozilla-autoconfig.xml;
}
location = /mail/config-v1.1.xml {
alias /var/lib/mailinabox/mozilla-autoconfig.xml;
}
location = /.well-known/mta-sts.txt {
alias /var/lib/mailinabox/mta-sts.txt;
}
# Z-Push (Microsoft Exchange ActiveSync)
location /Microsoft-Server-ActiveSync {
include /etc/nginx/fastcgi_params;
fastcgi_param SCRIPT_FILENAME /usr/local/lib/z-push/index.php;
fastcgi_param PHP_VALUE "include_path=.:/usr/share/php:/usr/share/pear:/usr/share/awl/inc";
fastcgi_read_timeout 630;
fastcgi_pass php-default;
# Outgoing mail also goes through this endpoint, so increase the maximum
# file upload limit to match the corresponding Postfix limit.
client_max_body_size 128M;
}
location ~* ^/autodiscover/autodiscover.xml$ {
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME /usr/local/lib/z-push/autodiscover/autodiscover.php;
fastcgi_param PHP_VALUE "include_path=.:/usr/share/php:/usr/share/pear:/usr/share/awl/inc";
fastcgi_pass php-default;
}
# Disable viewing dotfiles (.htaccess, .svn, .git, etc.)
# This block is placed at the end. Nginx's precedence rules means this block
# takes precedence over all non-regex matches and only regex matches that
# come after it (i.e. none of those, since this is the last one.) That means
# we're blocking dotfiles in the static hosted sites but not the FastCGI-
# handled locations for Nextcloud (which serves user-uploaded files that might
# have this pattern, see #414) or some of the other services.
location ~ /\.(ht|svn|git|hg|bzr) {
log_not_found off;
access_log off;
deny all;
}

View file

@ -0,0 +1,15 @@
# ADDITIONAL DIRECTIVES HERE
# WKD Locations
location ~ ^/.well-known/openpgpkey/(?<domain>.+)/policy$ {
add_header Access-Control-Allow-origin * always;
if (-d /var/lib/mailinabox/wkd/$domain/) {
return 204;
}
return 404;
}
location ~ ^/.well-known/openpgpkey/(?<domain>.+)/hu/(?<keyid>[ybndrfg8ejkmcpqxot1uwisza345h769]+)$ {
add_header Access-Control-Allow-Origin * always;
alias /var/lib/mailinabox/wkd/$domain/$keyid;
}

View file

@ -1,49 +1,100 @@
# ADDITIONAL DIRECTIVES HERE
# Control Panel
# Proxy /admin to our Python based control panel daemon. It is
# listening on IPv4 only so use an IP address and not 'localhost'.
location /admin/assets {
alias /usr/local/lib/mailinabox/vendor/assets;
}
rewrite ^/admin$ /admin/;
rewrite ^/admin/munin$ /admin/munin/ redirect;
location /admin/ {
proxy_pass http://localhost:10222/;
proxy_pass http://127.0.0.1:10222/;
proxy_set_header X-Forwarded-For $remote_addr;
add_header X-Frame-Options "DENY";
add_header X-Content-Type-Options nosniff;
add_header Content-Security-Policy "frame-ancestors 'none';";
}
# ownCloud configuration.
# Roundcube Webmail configuration.
rewrite ^/mail$ /mail/ redirect;
rewrite ^/mail/$ /mail/index.php;
location /mail/ {
index index.php;
alias /usr/local/lib/roundcubemail/;
}
location ~ /mail/config/.* {
# A ~-style location is needed to give this precedence over the next block.
return 403;
}
location ~ /mail/.*\.php {
# note: ~ has precendence over a regular location block
include fastcgi_params;
fastcgi_split_path_info ^/mail(/.*)()$;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME /usr/local/lib/roundcubemail/$fastcgi_script_name;
fastcgi_pass php-default;
# Outgoing mail also goes through this endpoint, so increase the maximum
# file upload limit to match the corresponding Postfix limit.
client_max_body_size 128M;
}
# Nextcloud configuration.
rewrite ^/cloud$ /cloud/ redirect;
rewrite ^/cloud/$ /cloud/index.php;
rewrite ^/cloud/(contacts|calendar|files)$ /cloud/index.php/apps/$1/ redirect;
rewrite ^(/cloud/core/doc/[^\/]+/)$ $1/index.html;
rewrite ^(/cloud/oc[sm]-provider)/$ $1/index.php redirect;
location /cloud/ {
alias /usr/local/lib/owncloud/;
location ~ ^/(data|config|\.ht|db_structure\.xml|README) {
location ~ ^/cloud/(build|tests|config|lib|3rdparty|templates|data|README)/ {
deny all;
}
}
location ~ ^/cloud/(?:\.|autotest|occ|issue|indie|db_|console) {
deny all;
}
# Enable paths for service and cloud federation discovery
# Resolves warning in Nextcloud Settings panel
location ~ ^/cloud/(oc[sm]-provider)?/([^/]+\.php)$ {
index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME /usr/local/lib/owncloud/$1/$2;
fastcgi_pass php-default;
}
}
location ~ ^(/cloud)(/[^/]+\.php)(/.*)?$ {
location ~ ^(/cloud)((?:/ocs)?/[^/]+\.php)(/.*)?$ {
# note: ~ has precendence over a regular location block
# Accept URLs like:
# /cloud/index.php/apps/files/
# /cloud/index.php/apps/files/ajax/scan.php (it's really index.php; see 6fdef379adfdeac86cc2220209bdf4eb9562268d)
# /cloud/ocs/v1.php/apps/files_sharing/api/v1 (see #240)
# /cloud/remote.php/webdav/yourfilehere...
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME /usr/local/lib/owncloud/$2;
fastcgi_param SCRIPT_NAME $1$2;
fastcgi_param PATH_INFO $3;
fastcgi_param MOD_X_ACCEL_REDIRECT_ENABLED on;
fastcgi_param MOD_X_ACCEL_REDIRECT_PREFIX /owncloud-xaccel;
fastcgi_read_timeout 630;
fastcgi_pass php-fpm;
error_page 403 /cloud/core/templates/403.php;
error_page 404 /cloud/core/templates/404.php;
fastcgi_pass php-default;
client_max_body_size 1G;
fastcgi_buffers 64 4K;
}
location ^~ /cloud/data {
# In order to support MOD_X_ACCEL_REDIRECT_ENABLED, we need to expose
# the data directory but only allow 'internal' redirects within nginx
# so that this is not exposed to the world.
location ^~ /owncloud-xaccel/ {
# This directory is for MOD_X_ACCEL_REDIRECT_ENABLED. Nextcloud sends the full file
# path on disk as a subdirectory under this virtual path.
# We must only allow 'internal' redirects within nginx so that the filesystem
# is not exposed to the world.
internal;
alias $STORAGE_ROOT/owncloud;
alias /;
}
location ~ ^/((caldav|carddav|webdav).*)$ {
# Z-Push doesn't like getting a redirect, and a plain rewrite didn't work either.
# Properly proxying like this seems to work fine.
proxy_pass https://$HOSTNAME/cloud/remote.php/$1;
proxy_pass https://127.0.0.1/cloud/remote.php/$1;
}
rewrite ^/.well-known/host-meta /cloud/public.php?service=host-meta last;
rewrite ^/.well-known/host-meta.json /cloud/public.php?service=host-meta-json last;
rewrite ^/.well-known/carddav /cloud/remote.php/carddav/ redirect;
rewrite ^/.well-known/caldav /cloud/remote.php/caldav/ redirect;

View file

@ -1,75 +1,20 @@
# from: https://gist.github.com/konklone/6532544
###################################################################################
# We track the Mozilla "intermediate" compatibility TLS recommendations.
# Note that these settings are repeated in the SMTP and IMAP configuration.
# ssl_protocols has moved to nginx.conf in bionic, check there for enabled protocols.
ssl_ciphers 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_dhparam STORAGE_ROOT/ssl/dh2048.pem;
# Basically the nginx configuration I use at konklone.com.
# I check it using https://www.ssllabs.com/ssltest/analyze.html?d=konklone.com
#
# To provide feedback, please tweet at @konklone or email eric@konklone.com.
# Comments on gists don't notify the author.
#
# Thanks to WubTheCaptain (https://wubthecaptain.eu) for his help and ciphersuites.
# Thanks to Ilya Grigorik (https://www.igvita.com) for constant inspiration.
# Path to certificate and private key.
# The .crt may omit the root CA cert, if it's a standard CA that ships with clients.
#ssl_certificate /path/to/unified.crt;
#ssl_certificate_key /path/to/my-private-decrypted.key;
# Tell browsers to require SSL (warning: difficult to change your mind)
add_header Strict-Transport-Security max-age=31536000;
# Prefer certain ciphersuites, to enforce Forward Secrecy and avoid known vulnerabilities.
#
# Forces forward secrecy in all browsers and clients that can use TLS,
# but with a small exception (DES-CBC3-SHA) for IE8/XP users.
#
# Reference client: https://www.ssllabs.com/ssltest/analyze.html
ssl_prefer_server_ciphers on;
ssl_ciphers 'kEECDH+ECDSA+AES128 kEECDH+ECDSA+AES256 kEECDH+AES128 kEECDH+AES256 kEDH+AES128 kEDH+AES256 DES-CBC3-SHA +SHA !aNULL !eNULL !LOW !MD5 !EXP !DSS !PSK !SRP !kECDH !CAMELLIA !RC4 !SEED';
# Cut out (the old, broken) SSLv3 entirely.
# This **excludes IE6 users** and (apparently) Yandexbot.
# Just comment out if you need to support IE6, bless your soul.
ssl_protocols TLSv1.2 TLSv1.1 TLSv1;
# Turn on session resumption, using a 10 min cache shared across nginx processes,
# as recommended by http://nginx.org/en/docs/http/configuring_https_servers.html
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
keepalive_timeout 70;
ssl_session_cache shared:SSL:50m;
ssl_session_timeout 1d;
# Buffer size of 1400 bytes fits in one MTU.
# nginx 1.5.9+ ONLY
#ssl_buffer_size 1400;
ssl_buffer_size 1400;
# SPDY header compression (0 for none, 9 for slow/heavy compression). Preferred is 6.
#
# BUT: header compression is flawed and vulnerable in SPDY versions 1 - 3.
# Disable with 0, until using a version of nginx with SPDY 4.
spdy_headers_comp 0;
# Now let's really get fancy, and pre-generate a 2048 bit random parameter
# for DH elliptic curves. If not created and specified, default is only 1024 bits.
#
# Generated by OpenSSL with the following command:
# openssl dhparam -outform pem -out dhparam2048.pem 2048
#
# Note: raising the bits to 2048 excludes Java 6 clients. Comment out if a problem.
#ssl_dhparam /path/to/dhparam2048.pem;
# OCSP stapling - means nginx will poll the CA for signed OCSP responses,
# and send them to clients so clients don't make their own OCSP calls.
# http://en.wikipedia.org/wiki/OCSP_stapling
#
# while the ssl_certificate above may omit the root cert if the CA is trusted,
# ssl_trusted_certificate below must point to a chain of **all** certs
# in the trust path - (your cert, intermediary certs, root cert)
#
# 8.8.8.8 and 8.8.4.4 below are Google's public IPv4 DNS servers.
# nginx will use them to talk to the CA.
ssl_stapling on;
ssl_stapling_verify on;
resolver 8.8.8.8 8.8.4.4 valid=86400;
resolver 127.0.0.1 valid=86400;
resolver_timeout 10;
#ssl_trusted_certificate /path/to/all-certs-in-chain.crt;
# h/t https://gist.github.com/konklone/6532544

View file

@ -6,7 +6,11 @@
## If you modify any system configuration you are on
## your own --- please do not ask for help from us.
upstream php-fpm {
server unix:/var/run/php5-fpm.sock;
# This socket is reserved for Mail-in-a-Box-specific services
upstream php-default {
server unix:/var/run/php/php-default.sock;
}
upstream php-fpm {
server unix:/var/run/php/php{{phpver}}-fpm.sock;
}

View file

@ -1,73 +1,47 @@
## $HOSTNAME
# Redirect all HTTP to HTTPS.
# Redirect all HTTP to HTTPS *except* the ACME challenges (Let's Encrypt TLS certificate
# domain validation challenges) path, which must be served over HTTP per the ACME spec
# (due to some Apache vulnerability).
server {
listen 80;
listen [::]:80;
server_name $HOSTNAME;
root /tmp/invalid-path-nothing-here;
rewrite ^/(.*)$ https://$HOSTNAME/$1 permanent;
# Improve privacy: Hide version an OS information on
# error pages and in the "Server" HTTP-Header.
server_tokens off;
location / {
# Redirect using the 'return' directive and the built-in
# variable '$request_uri' to avoid any capturing, matching
# or evaluation of regular expressions.
return 301 https://$HOSTNAME$request_uri;
}
location /.well-known/acme-challenge/ {
# This path must be served over HTTP for ACME domain validation.
# We map this to a special path where our TLS cert provisioning
# tool knows to store challenge response files.
alias $STORAGE_ROOT/ssl/lets_encrypt/webroot/.well-known/acme-challenge/;
}
}
# The secure HTTPS server.
server {
listen 443 ssl;
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name $HOSTNAME;
# Improve privacy: Hide version an OS information on
# error pages and in the "Server" HTTP-Header.
server_tokens off;
ssl_certificate $SSL_CERTIFICATE;
ssl_certificate_key $SSL_KEY;
include /etc/nginx/nginx-ssl.conf;
# Expose this directory as static files.
root $ROOT;
index index.html index.htm;
# Roundcube Webmail configuration.
rewrite ^/mail$ /mail/ redirect;
rewrite ^/mail/$ /mail/index.php;
location /mail/ {
index index.php;
alias /usr/local/lib/roundcubemail/;
}
location ~ /mail/config/.* {
# A ~-style location is needed to give this precedence over the next block.
return 403;
}
location ~ /mail/.*\.php {
# note: ~ has precendence over a regular location block
include fastcgi_params;
fastcgi_split_path_info ^/mail(/.*)()$;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME /usr/local/lib/roundcubemail/$fastcgi_script_name;
fastcgi_pass php-fpm;
client_max_body_size 20M;
}
# Webfinger configuration.
location = /.well-known/webfinger {
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME /usr/local/bin/mailinabox-webfinger.php;
fastcgi_pass php-fpm;
}
# Z-Push (Microsoft Exchange ActiveSync)
location /Microsoft-Server-ActiveSync {
include /etc/nginx/fastcgi_params;
fastcgi_param SCRIPT_FILENAME /usr/local/lib/z-push/index.php;
fastcgi_param PHP_VALUE "include_path=.:/usr/share/php:/usr/share/pear:/usr/share/awl/inc";
fastcgi_read_timeout 630;
fastcgi_pass php-fpm;
}
location /autodiscover/autodiscover.xml {
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME /usr/local/lib/z-push/autodiscover/autodiscover.php;
fastcgi_param PHP_VALUE "include_path=.:/usr/share/php:/usr/share/pear:/usr/share/awl/inc";
fastcgi_pass php-fpm;
}
# ADDITIONAL DIRECTIVES HERE
}

View file

@ -1,10 +1,14 @@
# Remove the first line of the Received: header. Note that we cannot fully remove the Received: header
# because OpenDKIM requires that a header be present when signing outbound mail. The first line is
# where the user's home IP address would be.
/^\s*Received:[^\n]*(.*)/ REPLACE Received: from authenticated-user (unknown [127.0.0.1])$1
/^\s*Received:[^\n]*(.*)/ REPLACE Received: from authenticated-user (PRIMARY_HOSTNAME [PUBLIC_IP])$1
# Remove other typically private information.
/^\s*User-Agent:/ IGNORE
/^\s*X-Enigmail:/ IGNORE
/^\s*X-Mailer:/ IGNORE
/^\s*X-Originating-IP:/ IGNORE
/^\s*X-Pgp-Agent:/ IGNORE
# The Mime-Version header can leak the user agent too, e.g. in Mime-Version: 1.0 (Mac OS X Mail 8.1 \(2010.6\)).
/^\s*(Mime-Version:\s*[0-9\.]+)\s.+/ REPLACE $1

View file

@ -1,7 +1,6 @@
require ["regex", "fileinto", "imap4flags"];
if allof (header :regex "X-Spam-Status" "^Yes") {
setflag "\\Seen";
fileinto "Spam";
stop;
}

View file

@ -1,9 +1,106 @@
<html>
<head>
<title>this is a mail-in-a-box</title>
</head>
<body>
<h1>this is a mail-in-a-box</h1>
<p>take control of your email at <a href="https://mailinabox.email/">https://mailinabox.email/</a></p>
</body>
<head>
<title>Power Mail-in-a-Box</title>
<meta charset="utf-8" name="robots" content="noindex">
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
position: relative;
min-height: 100%;
}
#content {
padding-bottom: 3.5rem;
}
p,
ul,
li {
font-size: 15pt
}
th,
td {
font-size: 20pt;
width: 30%;
}
#small td {
font-size: 12.5pt;
}
footer {
position: absolute;
bottom: 0;
width: 100%;
height: 3.5rem;
background-color: #ddd;
}
footer p {
font-size: 10pt;
}
</style>
</head>
<body>
<div id=content>
<h1 align=center>It's working! 🎉🎉🎉</h1>
<p align=center>It is running! Now you can begin your <b>very</b> awesome thing!
<p>
<h2 align=center>What is this Power Mail-in-a-Box able to do for you?</h2>
<ul>
<li>Send and receive mail (duh);</li>
<li>Be your <a href="https://wiki.gnupg.org/WKD">Web Key Directory</a> server, if you're into GnuPG and all
that jazz. No need to rely on public keyservers!</li>
<li>Also be your web server - if you want:</li>
<ul>
<li>Static sites? No problem!</li>
<li>Front-end + back-end? No problem!</li>
<li>PHP? <b>Did I stutter?</b> We got you!</li>
<li>How many sites can I host at the same time? How many you want (provided your server can handle all
of them, of course)</li>
</ul>
</ul>
<h2 align=center>Let's begin, then?</h2>
<p align=center>This Power Mail-in-a-Box was configured with the name <code>{{PRIMARY_HOSTNAME}}</code>.</p>
<table align=center>
<thead>
<th>To access the admin panel...</th>
<th>To access the webmail...</th>
<th>To access NextCloud...</th>
</thead>
<tbody>
<tr>
<td align=center><b><a href="https://{{PRIMARY_HOSTNAME}}/admin">Go here</a></b></td>
<td align=center><b><a href="https://{{PRIMARY_HOSTNAME}}/mail">Go here</a></b></td>
<td align=center><b><a href="https://{{PRIMARY_HOSTNAME}}/cloud">Go here</a></b></td>
</tr>
<tr id=small>
<td align=center><code>{{PRIMARY_HOSTNAME}}/admin</code></td>
<td align=center><code>{{PRIMARY_HOSTNAME}}/mail</code></td>
<td align=center><code>{{PRIMARY_HOSTNAME}}/cloud</code></td>
</tr>
</tbody>
</table>
<br><br>
<h2 align=center>Oh and by the way: thank you for sticking by!</h2>
<p align=center>This project is maintained <a href="https://power-mailinabox.net">on GitHub</a>.
Feel free to take a peek and maybe leave a star. It is very much appreciated! ❤</p>
<p align=center>Also feel free report any issues you may find, or leave your ideas.</p>
</div>
<footer>
<p align=center>
Unless you moved the folder elsewhere, or you manually added this file, it is located at
<code>{{STORAGE_ROOT}}/www/default/index.html</code> on the server - feel free to remove it whenever you
want.
</p>
</footer>
</body>
</html>

View file

@ -5,11 +5,12 @@
* Descr : Autodiscover configuration file
************************************************/
define('TIMEZONE', '');
// Defines the base path on the server
define('BASE_PATH', dirname($_SERVER['SCRIPT_FILENAME']). '/');
// The Z-Push server location for the autodiscover response
define('SERVERURL', 'https://PRIMARY_HOSTNAME/Microsoft-Server-ActiveSync');
define('ZPUSH_HOST', 'PRIMARY_HOSTNAME');
define('USE_FULLEMAIL_FOR_LOGIN', true);
@ -18,6 +19,7 @@ define('LOGFILE', LOGFILEDIR . 'autodiscover.log');
define('LOGERRORFILE', LOGFILEDIR . 'autodiscover-error.log');
define('LOGLEVEL', LOGLEVEL_INFO);
define('LOGUSERLEVEL', LOGLEVEL);
$specialLogUsers = array();
// the backend data provider
define('BACKEND_PROVIDER', 'BackendCombined');

View file

@ -5,14 +5,12 @@
* Descr : CalDAV backend configuration file
************************************************/
define('CALDAV_SERVER', 'https://localhost');
define('CALDAV_PROTOCOL', 'https');
define('CALDAV_SERVER', '127.0.0.1');
define('CALDAV_PORT', '443');
define('CALDAV_PATH', '/caldav/calendars/%u/');
define('CALDAV_PERSONAL', '');
// If the CalDAV server supports the sync-collection operation
// DAViCal and SOGo support it
// Setting this to false will work with most servers, but it will be slower
define('CALDAV_PERSONAL', 'PRINCIPAL');
define('CALDAV_SUPPORTS_SYNC', false);
define('CALDAV_MAX_SYNC_PERIOD', 2147483647);
?>

View file

@ -7,23 +7,17 @@
define('CARDDAV_PROTOCOL', 'https'); /* http or https */
define('CARDDAV_SERVER', 'localhost');
define('CARDDAV_SERVER', '127.0.0.1');
define('CARDDAV_PORT', '443');
define('CARDDAV_PATH', '/carddav/addressbooks/%u/');
define('CARDDAV_DEFAULT_PATH', '/carddav/addressbooks/%u/contacts/'); /* subdirectory of the main path */
define('CARDDAV_GAL_PATH', ''); /* readonly, searchable, not syncd */
define('CARDDAV_GAL_MIN_LENGTH', 5);
define('CARDDAV_CONTACTS_FOLDER_NAME', '%u Addressbook');
// If the CardDAV server supports the sync-collection operation
// DAViCal supports it, but SabreDav, Owncloud, SOGo don't
// Setting this to false will work with most servers, but it will be slower: 1 petition for the href of vcards, and 1 petition for each vcard
define('CARDDAV_SUPPORTS_SYNC', false);
// If the CardDAV server supports the FN attribute for searches
// DAViCal supports it, but SabreDav, Owncloud and SOGo don't
// DAViCal supports it, but SabreDav, Nextcloud and SOGo don't
// Setting this to true will search by FN. If false will search by sn, givenName and email
// It's safe to leave it as false
define('CARDDAV_SUPPORTS_FN_SEARCH', false);

View file

@ -5,19 +5,37 @@
* Descr : IMAP backend configuration file
************************************************/
define('IMAP_SERVER', 'localhost');
define('IMAP_SERVER', '127.0.0.1');
define('IMAP_PORT', 993);
define('IMAP_OPTIONS', '/ssl/norsh/novalidate-cert');
define('IMAP_DEFAULTFROM', '');
define('IMAP_DEFAULTFROM', 'sql');
// not used
define('IMAP_FROM_SQL_DSN', '');
define('SYSTEM_MIME_TYPES_MAPPING', '/etc/mime.types');
define('IMAP_AUTOSEEN_ON_DELETE', false);
define('IMAP_FOLDER_CONFIGURED', true);
define('IMAP_FOLDER_PREFIX', '');
define('IMAP_FOLDER_PREFIX_IN_INBOX', false);
// see our conf/dovecot-mailboxes.conf file for IMAP special flags settings
define('IMAP_FOLDER_INBOX', 'INBOX');
define('IMAP_FOLDER_SENT', 'SENT');
define('IMAP_FOLDER_DRAFT', 'DRAFTS');
define('IMAP_FOLDER_TRASH', 'TRASH');
define('IMAP_FOLDER_SPAM', 'SPAM');
define('IMAP_FOLDER_ARCHIVE', 'ARCHIVE');
define('IMAP_INLINE_FORWARD', true);
define('IMAP_EXCLUDED_FOLDERS', '');
define('IMAP_FROM_SQL_DSN', 'sqlite:STORAGE_ROOT/mail/roundcube/roundcube.sqlite');
define('IMAP_FROM_SQL_USER', '');
define('IMAP_FROM_SQL_PASSWORD', '');
define('IMAP_FROM_SQL_OPTIONS', serialize(array(PDO::ATTR_PERSISTENT => true)));
define('IMAP_FROM_SQL_QUERY', "select first_name, last_name, mail_address from users where mail_address = '#username@#domain'");
define('IMAP_FROM_SQL_FIELDS', serialize(array('first_name', 'last_name', 'mail_address')));
define('IMAP_FROM_SQL_FROM', '#first_name #last_name <#mail_address>');
define('IMAP_FROM_SQL_QUERY', "SELECT name, email FROM identities i INNER JOIN users u ON i.user_id = u.user_id WHERE u.username = '#username' AND i.standard = 1 AND i.del = 0 AND i.name <> ''");
define('IMAP_FROM_SQL_FIELDS', serialize(array('name', 'email')));
define('IMAP_FROM_SQL_FROM', '#name <#email>');
define('IMAP_FROM_SQL_FULLNAME', '#name');
// not used
define('IMAP_FROM_LDAP_SERVER', '');
define('IMAP_FROM_LDAP_SERVER_PORT', '389');
define('IMAP_FROM_LDAP_USER', 'cn=zpush,ou=servers,dc=zpush,dc=org');
@ -26,17 +44,14 @@ define('IMAP_FROM_LDAP_BASE', 'dc=zpush,dc=org');
define('IMAP_FROM_LDAP_QUERY', '(mail=#username@#domain)');
define('IMAP_FROM_LDAP_FIELDS', serialize(array('givenname', 'sn', 'mail')));
define('IMAP_FROM_LDAP_FROM', '#givenname #sn <#mail>');
define('IMAP_FROM_LDAP_FULLNAME', '#givenname #sn');
// copy outgoing mail to this folder. If not set z-push will try the default folders
define('IMAP_SENTFOLDER', '');
define('IMAP_INLINE_FORWARD', true);
define('IMAP_EXCLUDED_FOLDERS', '');
define('IMAP_SMTP_METHOD', 'sendmail');
global $imap_smtp_params;
$imap_smtp_params = array('host' => 'ssl://localhost', 'port' => 587, 'auth' => true, 'username' => 'imap_username', 'password' => 'imap_password');
$imap_smtp_params = array('host' => 'ssl://127.0.0.1', 'port' => 465, 'auth' => true, 'username' => 'imap_username', 'password' => 'imap_password');
define('MAIL_MIMEPART_CRLF', "\r\n");
define('IMAP_MEETING_USE_CALDAV', true);
?>

View file

@ -1,109 +1,180 @@
import base64, os, os.path
import base64
import os
import os.path
import hmac
import json
import secrets
from datetime import timedelta
from flask import make_response
from expiringdict import ExpiringDict
import utils
from mailconfig import get_mail_user_privileges
from mailconfig import get_mail_password, get_mail_user_privileges
from mfa import get_hash_mfa_state, validate_auth_mfa
DEFAULT_KEY_PATH = '/var/lib/mailinabox/api.key'
DEFAULT_KEY_PATH = '/var/lib/mailinabox/api.key'
DEFAULT_AUTH_REALM = 'Mail-in-a-Box Management Server'
class KeyAuthService:
"""Generate an API key for authenticating clients
Clients must read the key from the key file and send the key with all HTTP
requests. The key is passed as the username field in the standard HTTP
Basic Auth header.
"""
class AuthService:
def __init__(self):
self.auth_realm = DEFAULT_AUTH_REALM
self.key = self._generate_key()
self.key_path = DEFAULT_KEY_PATH
self.max_session_duration = timedelta(days=2)
def write_key(self):
"""Write key to file so authorized clients can get the key
self.init_system_api_key()
self.sessions = ExpiringDict(
max_len=64,
max_age_seconds=self.max_session_duration.total_seconds())
The key file is created with mode 0640 so that additional users can be
authorized to access the API by granting group/ACL read permissions on
the key file.
"""
def create_file_with_mode(path, mode):
# Based on answer by A-B-B: http://stackoverflow.com/a/15015748
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)
def init_system_api_key(self):
"""Write an API key to a local file so local processes can use the API"""
os.makedirs(os.path.dirname(self.key_path), exist_ok=True)
with open(self.key_path, 'r') as file:
self.key = file.read()
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):
"""Test if the HTTP Authorization header's username matches the system key, a session key,
or if the username/password passed in the header matches a local user.
Returns a tuple of the user's email address and list of user privileges (e.g.
('my@email', []) or ('my@email', ['admin']); raises a ValueError on login failure.
If the user used the system API key, the user's email is returned as None since
this key is not associated with a user."""
def is_authenticated(self, request, env):
"""Test if the client key passed in HTTP Authorization header matches the service key
or if the or username/password passed in the header matches an administrator user.
Returns 'OK' if the key is good or the user is an administrator, otherwise an error message."""
def parse_http_authorization_basic(header):
def decode(s):
return base64.b64decode(s.encode('ascii')).decode('ascii')
def decode(s):
return base64.b64decode(s.encode('ascii')).decode('ascii')
def parse_basic_auth(header):
if " " not in header:
return None, None
scheme, credentials = header.split(maxsplit=1)
if scheme != 'Basic':
return None, None
credentials = decode(credentials)
if ":" not in credentials:
return None, None
username, password = credentials.split(':', maxsplit=1)
return username, password
header = request.headers.get('Authorization')
if not header:
return "No authorization header provided."
username, password = parse_basic_auth(header)
username, password = parse_http_authorization_basic(
request.headers.get('Authorization', ''))
if username in (None, ""):
return "Authorization header invalid."
elif username == self.key:
return "OK"
raise ValueError("Authorization header invalid.")
if username.strip() == "" and password.strip() == "":
raise ValueError(
"No email address, password, session key, or API key provided."
)
# If user passed the system API key, grant administrative privs. This key
# is not associated with a user.
if username == self.key and not login_only:
return (None, ["admin"])
# If the password corresponds with a session token for the user, grant access for that user.
if self.get_session(username, password, "login",
env) and not login_only:
sessionid = password
session = self.sessions[sessionid]
if logout:
# Clear the session.
del self.sessions[sessionid]
else:
# Re-up the session so that it does not expire.
self.sessions[sessionid] = session
# If no password was given, but a username was given, we're missing some information.
elif password.strip() == "":
raise ValueError("Enter a password.")
else:
return self.check_imap_login( username, password, env)
# The user is trying to log in with a username and a password
# (and possibly a MFA token). On failure, an exception is raised.
self.check_user_auth(username, password, request, env)
def check_imap_login(self, email, pw, env):
# Validate a user's credentials.
# Get privileges for authorization. This call should never fail because by this
# point we know the email address is a valid user --- unless the user has been
# deleted after the session was granted. On error the call will return a tuple
# of an error message and an HTTP status code.
privs = get_mail_user_privileges(username, env)
if isinstance(privs, tuple):
raise ValueError(privs[0])
# Sanity check.
if email == "" or pw == "":
return "Enter an email address and password."
# Return the authorization information.
return (username, privs)
def check_user_auth(self, email, pw, request, env):
# Validate a user's login email address and password. If MFA is enabled,
# check the MFA token in the X-Auth-Token header.
#
# On login failure, raises a ValueError with a login error message. On
# success, nothing is returned.
# Authenticate.
try:
# Use doveadm to check credentials. doveadm will return
# Get the hashed password of the user. Raise a ValueError if the
# email address does not correspond to a user. But wrap it in the
# same exception as if a password fails so we don't easily reveal
# if an email address is valid.
pw_hash = get_mail_password(email, env)
# Use 'doveadm pw' to check credentials. doveadm will return
# a non-zero exit status if the credentials are no good,
# and check_call will raise an exception in that case.
utils.shell('check_call', [
"/usr/bin/doveadm",
"auth", "test",
email, pw
])
"pw",
"-p",
pw,
"-t",
pw_hash,
])
except:
# Login failed.
return "Invalid email address or password."
raise ValueError("Incorrect email address or password.")
# Authorize.
# (This call should never fail on a valid user.)
privs = get_mail_user_privileges(email, env)
if isinstance(privs, tuple): raise Exception("Error getting privileges.")
if "admin" not in privs:
return "You are not an administrator for this system."
# If MFA is enabled, check that MFA passes.
status, hints = validate_auth_mfa(email, request, env)
if not status:
# Login valid. Hints may have more info.
raise ValueError(",".join(hints))
return "OK"
def create_user_password_state_token(self, email, env):
# Create a token that changes if the user's password or MFA options change
# so that sessions become invalid if any of that information changes.
msg = get_mail_password(email, env).encode("utf8")
def _generate_key(self):
raw_key = os.urandom(32)
return base64.b64encode(raw_key).decode('ascii')
# Add to the message the current MFA state, which is a list of MFA information.
# Turn it into a string stably.
msg += b" " + json.dumps(get_hash_mfa_state(email, env),
sort_keys=True).encode("utf8")
# Make a HMAC using the system API key as a hash key.
hash_key = self.key.encode('ascii')
return hmac.new(hash_key, msg, digestmod="sha256").hexdigest()
def create_session_key(self, username, env, type=None):
# Create a new session.
token = secrets.token_hex(32)
self.sessions[token] = {
"email": username,
"password_token":
self.create_user_password_state_token(username, env),
"type": type,
}
return token
def get_session(self, user_email, session_key, session_type, env):
if session_key not in self.sessions:
return None
session = self.sessions[session_key]
if session_type == "login" and session["email"] != user_email:
return None
if session["type"] != session_type:
return None
if session["password_token"] != self.create_user_password_state_token(
session["email"], env):
return None
return session

View file

@ -1,95 +1,190 @@
#!/usr/bin/python3
#!/usr/local/lib/mailinabox/env/bin/python
# This script performs a backup of all user data:
# 1) System services are stopped while a copy of user data is made.
# 2) An incremental backup is made using duplicity into the
# directory STORAGE_ROOT/backup/duplicity.
# 3) The stopped services are restarted.
# 4) The backup files are encrypted with a long password (stored in
# backup/secret_key.txt) to STORAGE_ROOT/backup/encrypted.
# 5) STORAGE_ROOT/backup/after-backup is executd if it exists.
# 1) System services are stopped.
# 2) STORAGE_ROOT/backup/before-backup is executed if it exists.
# 3) An incremental encrypted backup is made using duplicity.
# 4) The stopped services are restarted.
# 5) STORAGE_ROOT/backup/after-backup is executed if it exists.
import os, os.path, shutil, glob, re, datetime
import dateutil.parser, dateutil.relativedelta, dateutil.tz
import os
import os.path
import shutil
import glob
import re
import datetime
import sys
import dateutil.parser
import dateutil.relativedelta
import dateutil.tz
import rtyaml
from exclusiveprocess import Lock, CannotAcquireLock
from utils import exclusive_process, load_environment, shell
# destroy backups when the most recent increment in the chain
# that depends on it is this many days old.
keep_backups_for_days = 14
from utils import load_environment, shell, wait_for_service, get_php_version
def backup_status(env):
# What is the current status of backups?
# Loop through all of the files in STORAGE_ROOT/backup/duplicity to
# get a list of all of the backups taken and sum up file sizes to
# see how large the storage is.
# If backups are disabled, return no status.
config = get_backup_config(env)
if config["target"] == "off":
return {}
# Query duplicity to get a list of all full and incremental
# backups available.
backups = {}
now = datetime.datetime.now(dateutil.tz.tzlocal())
def reldate(date, ref):
backup_root = os.path.join(env["STORAGE_ROOT"], 'backup')
backup_cache_dir = os.path.join(backup_root, 'cache')
def reldate(date, ref, clip):
if ref < date:
return clip
rd = dateutil.relativedelta.relativedelta(ref, date)
if rd.days >= 7: return "%d days" % rd.days
if rd.days > 1: return "%d days, %d hours" % (rd.days, rd.hours)
if rd.days == 1: return "%d day, %d hours" % (rd.days, rd.hours)
if rd.years > 1:
return "%d years, %d months" % (rd.years, rd.months)
if rd.years == 1:
return "%d year, %d months" % (rd.years, rd.months)
if rd.months > 1:
return "%d months, %d days" % (rd.months, rd.days)
if rd.months == 1:
return "%d month, %d days" % (rd.months, rd.days)
if rd.days >= 7:
return "%d days" % rd.days
if rd.days > 1:
return "%d days, %d hours" % (rd.days, rd.hours)
if rd.days == 1:
return "%d day, %d hours" % (rd.days, rd.hours)
return "%d hours, %d minutes" % (rd.hours, rd.minutes)
backups = { }
basedir = os.path.join(env['STORAGE_ROOT'], 'backup/duplicity/')
encdir = os.path.join(env['STORAGE_ROOT'], 'backup/encrypted/')
for fn in os.listdir(basedir):
m = re.match(r"duplicity-(full|full-signatures|(inc|new-signatures)\.(?P<incbase>\d+T\d+Z)\.to)\.(?P<date>\d+T\d+Z)\.", fn)
if not m: raise ValueError(fn)
# Get duplicity collection status and parse for a list of backups.
def parse_line(line):
keys = line.strip().split()
date = dateutil.parser.parse(keys[1]).astimezone(dateutil.tz.tzlocal())
return {
"date": keys[1],
"date_str": date.strftime("%Y-%m-%d %X") + " " + now.tzname(),
"date_delta": reldate(date, now, "the future?"),
"full": keys[0] == "full",
"size": 0, # collection-status doesn't give us the size
# number of archive volumes for this backup (not really helpful)
"volumes": int(keys[2]),
}
code, collection_status = shell('check_output', [
"/usr/local/bin/duplicity",
"collection-status",
"--archive-dir", backup_cache_dir,
"--gpg-options", "--cipher-algo=AES256",
"--log-fd", "1",
get_duplicity_target_url(config),
] + get_duplicity_additional_args(env),
get_duplicity_env_vars(env),
trap=True)
if code != 0:
# Command failed. This is likely due to an improperly configured remote
# destination for the backups or the last backup job terminated unexpectedly.
raise Exception("Something is wrong with the backup: " +
collection_status)
for line in collection_status.split('\n'):
if line.startswith(" full") or line.startswith(" inc"):
backup = parse_line(line)
backups[backup["date"]] = backup
# Look at the target directly to get the sizes of each of the backups. There is more than one file per backup.
# Starting with duplicity in Ubuntu 18.04, "signatures" files have dates in their
# filenames that are a few seconds off the backup date and so don't line up
# with the list of backups we have. Track unmatched files so we know how much other
# space is used for those.
unmatched_file_size = 0
for fn, size in list_target_files(config):
m = re.match(
r"duplicity-(full|full-signatures|(inc|new-signatures)\.(?P<incbase>\d+T\d+Z)\.to)\.(?P<date>\d+T\d+Z)\.",
fn)
if not m:
continue # not a part of a current backup chain
key = m.group("date")
if key not in backups:
date = dateutil.parser.parse(m.group("date"))
backups[key] = {
"date": m.group("date"),
"date_str": date.strftime("%x %X"),
"date_delta": reldate(date, now),
"full": m.group("incbase") is None,
"previous": m.group("incbase"),
"size": 0,
"encsize": 0,
}
backups[key]["size"] += os.path.getsize(os.path.join(basedir, fn))
# Also check encrypted size.
encfn = os.path.join(encdir, fn + ".enc")
if os.path.exists(encfn):
backups[key]["encsize"] += os.path.getsize(encfn)
if key in backups:
backups[key]["size"] += size
else:
unmatched_file_size += size
# Ensure the rows are sorted reverse chronologically.
# This is relied on by should_force_full() and the next step.
backups = sorted(backups.values(), key = lambda b : b["date"], reverse=True)
backups = sorted(backups.values(), key=lambda b: b["date"], reverse=True)
# When will a backup be deleted?
saw_full = False
# Get the average size of incremental backups, the size of the
# most recent full backup, and the date of the most recent
# backup and the most recent full backup.
incremental_count = 0
incremental_size = 0
first_date = None
first_full_size = None
first_full_date = None
for bak in backups:
if first_date is None:
first_date = dateutil.parser.parse(bak["date"])
if bak["full"]:
first_full_size = bak["size"]
first_full_date = dateutil.parser.parse(bak["date"])
break
incremental_count += 1
incremental_size += bak["size"]
# When will the most recent backup be deleted? It won't be deleted if the next
# backup is incremental, because the increments rely on all past increments.
# So first guess how many more incremental backups will occur until the next
# full backup. That full backup frees up this one to be deleted. But, the backup
# must also be at least min_age_in_days old too.
deleted_in = None
days_ago = now - datetime.timedelta(days=keep_backups_for_days)
if incremental_count > 0 and incremental_size > 0 and first_full_size is not None:
# How many days until the next incremental backup? First, the part of
# the algorithm based on increment sizes:
est_days_to_next_full = (.5 * first_full_size - incremental_size) / (
incremental_size / incremental_count)
est_time_of_next_full = first_date + \
datetime.timedelta(days=est_days_to_next_full)
# ...And then the part of the algorithm based on full backup age:
est_time_of_next_full = min(
est_time_of_next_full, first_full_date +
datetime.timedelta(days=config["min_age_in_days"] * 10 + 1))
# It still can't be deleted until it's old enough.
est_deleted_on = max(
est_time_of_next_full,
first_date + datetime.timedelta(days=config["min_age_in_days"]))
deleted_in = "approx. %d days" % round(
(est_deleted_on - now).total_seconds() / 60 / 60 / 24 + .5)
# When will a backup be deleted? Set the deleted_in field of each backup.
saw_full = False
for bak in backups:
if deleted_in:
# Subsequent backups are deleted when the most recent increment
# in the chain would be deleted.
# The most recent increment in a chain and all of the previous backups
# it relies on are deleted at the same time.
bak["deleted_in"] = deleted_in
if bak["full"]:
# Reset when we get to a full backup. A new chain start next.
# Reset when we get to a full backup. A new chain start *next*.
saw_full = True
deleted_in = None
elif saw_full and not deleted_in:
# Mark deleted_in only on the first increment after a full backup.
deleted_in = reldate(days_ago, dateutil.parser.parse(bak["date"]))
# We're now on backups prior to the most recent full backup. These are
# free to be deleted as soon as they are min_age_in_days old.
deleted_in = reldate(
now,
dateutil.parser.parse(bak["date"]) +
datetime.timedelta(days=config["min_age_in_days"]),
"on next daily backup")
bak["deleted_in"] = deleted_in
return {
"directory": basedir,
"encpwfile": os.path.join(env['STORAGE_ROOT'], 'backup/secret_key.txt'),
"encdirectory": encdir,
"tz": now.tzname(),
"backups": backups,
"unmatched_file_size": unmatched_file_size,
}
def should_force_full(env):
def should_force_full(config, env):
# Force a full backup when the total size of the increments
# since the last full backup is greater than half the size
# of that full backup.
@ -101,108 +196,491 @@ def should_force_full(env):
inc_size += bak["size"]
else:
# ...until we reach the most recent full backup.
# Return if we should to a full backup.
return inc_size > .5*bak["size"]
# Return if we should to a full backup, which is based
# on the size of the increments relative to the full
# backup, as well as the age of the full backup.
if inc_size > .5 * bak["size"]:
return True
if dateutil.parser.parse(bak["date"]) + datetime.timedelta(
days=config["min_age_in_days"] * 10 +
1) < datetime.datetime.now(dateutil.tz.tzlocal()):
return True
return False
else:
# If we got here there are no (full) backups, so make one.
# (I love for/else blocks. Here it's just to show off.)
return True
def perform_backup(full_backup):
def get_passphrase(env):
# Get the encryption passphrase. secret_key.txt is 2048 random
# bits base64-encoded and with line breaks every 65 characters.
# gpg will only take the first line of text, so sanity check that
# that line is long enough to be a reasonable passphrase. It
# only needs to be 43 base64-characters to match AES256's key
# length of 32 bytes.
backup_root = os.path.join(env["STORAGE_ROOT"], 'backup')
with open(os.path.join(backup_root, 'secret_key.txt')) as f:
passphrase = f.readline().strip()
if len(passphrase) < 43:
raise Exception("secret_key.txt's first line is too short!")
return passphrase
def get_duplicity_target_url(config):
target = config["target"]
if get_target_type(config) == "s3":
from urllib.parse import urlsplit, urlunsplit
target = list(urlsplit(target))
# Although we store the S3 hostname in the target URL,
# duplicity no longer accepts it in the target URL. The hostname in
# the target URL must be the bucket name. The hostname is passed
# via get_duplicity_additional_args. Move the first part of the
# path (the bucket name) into the hostname URL component, and leave
# the rest for the path.
target_bucket = target[2].lstrip('/').split('/', 1)
target[1] = target_bucket[0]
target[2] = target_bucket[1] if len(target_bucket) > 1 else ''
target = urlunsplit(target)
return target
def get_duplicity_additional_args(env):
config = get_backup_config(env)
port = 0
try:
port = int(config["target_rsync_port"])
except Exception:
port = 22
if get_target_type(config) == 'rsync':
return [
f"--ssh-options= -i /root/.ssh/id_rsa_miab -p {port}",
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':
# See note about hostname in get_duplicity_target_url.
from urllib.parse import urlsplit, urlunsplit
target = urlsplit(config["target"])
endpoint_url = urlunsplit(("https", target.netloc, '', '', ''))
return ["--s3-endpoint-url", endpoint_url]
return []
def get_duplicity_env_vars(env):
config = get_backup_config(env)
env = {"PASSPHRASE": get_passphrase(env)}
if get_target_type(config) == 's3':
env["AWS_ACCESS_KEY_ID"] = config["target_user"]
env["AWS_SECRET_ACCESS_KEY"] = config["target_pass"]
return env
def get_target_type(config):
protocol = config["target"].split(":")[0]
return protocol
def perform_backup(full_backup, user_initiated=False):
env = load_environment()
php_fpm = f"php{get_php_version()}-fpm"
exclusive_process("backup")
# Create an global exclusive lock so that the backup script
# cannot be run more than one.
lock = Lock(name="mailinabox_backup_daemon", die=(not user_initiated))
if user_initiated:
# God forgive me for what I'm about to do
try:
lock._acquire()
except CannotAcquireLock:
return "Another backup is already being done!"
else:
lock.forever()
# Ensure the backup directory exists.
backup_dir = os.path.join(env["STORAGE_ROOT"], 'backup')
backup_duplicity_dir = os.path.join(backup_dir, 'duplicity')
os.makedirs(backup_duplicity_dir, exist_ok=True)
config = get_backup_config(env)
backup_root = os.path.join(env["STORAGE_ROOT"], 'backup')
backup_cache_dir = os.path.join(backup_root, 'cache')
backup_dir = os.path.join(backup_root, 'encrypted')
# Are backups disabled?
if config["target"] == "off":
return
# On the first run, always do a full backup. Incremental
# will fail. Otherwise do a full backup when the size of
# the increments since the most recent full backup are
# large.
full_backup = full_backup or should_force_full(env)
try:
full_backup = full_backup or should_force_full(config, env)
except Exception as e:
# This was the first call to duplicity, and there might
# be an error already.
print(e)
sys.exit(1)
# Stop services.
shell('check_call', ["/usr/sbin/service", "dovecot", "stop"])
shell('check_call', ["/usr/sbin/service", "postfix", "stop"])
def service_command(service, command, quit=None):
# Execute silently, but if there is an error then display the output & exit.
code, ret = shell('check_output',
["/usr/sbin/service", service, command],
capture_stderr=True,
trap=True)
if code != 0:
print(ret)
if quit:
sys.exit(code)
# Update the backup mirror directory which mirrors the current
# STORAGE_ROOT (but excluding the backups themselves!).
service_command(php_fpm, "stop", quit=True)
service_command("postfix", "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.
# Run as the STORAGE_USER user, not as root. Pass our settings in
# environment variables so the script has access to STORAGE_ROOT.
pre_script = os.path.join(backup_root, 'before-backup')
if os.path.exists(pre_script):
shell('check_call',
['su', env['STORAGE_USER'], '-c', pre_script, config["target"]],
env=env)
# Run a backup of STORAGE_ROOT (but excluding the backups themselves!).
# --allow-source-mismatch is needed in case the box's hostname is changed
# after the first backup. See #396.
try:
shell('check_call', [
"/usr/bin/duplicity",
"/usr/local/bin/duplicity",
"full" if full_backup else "incr",
"--no-encryption",
"--archive-dir", "/tmp/duplicity-archive-dir",
"--name", "mailinabox",
"--exclude", backup_dir,
"--volsize", "100",
"--verbosity", "warning",
"--verbosity", "warning", "--no-print-statistics",
"--archive-dir", backup_cache_dir,
"--exclude", backup_root,
"--volsize", "250",
"--gpg-options", "--cipher-algo=AES256",
env["STORAGE_ROOT"],
"file://" + backup_duplicity_dir
])
get_duplicity_target_url(config),
"--allow-source-mismatch"
] + get_duplicity_additional_args(env),
get_duplicity_env_vars(env))
finally:
# Start services again.
shell('check_call', ["/usr/sbin/service", "dovecot", "start"])
shell('check_call', ["/usr/sbin/service", "postfix", "start"])
service_command("postgrey", "start", quit=False)
service_command("dovecot", "start", quit=False)
service_command("postfix", "start", quit=False)
service_command(php_fpm, "start", quit=False)
# Remove old backups. This deletes all backup data no longer needed
# from more than 31 days ago. Must do this before destroying the
# cache directory or else this command will re-create it.
# from more than 3 days ago.
shell('check_call', [
"/usr/bin/duplicity",
"/usr/local/bin/duplicity",
"remove-older-than",
"%dD" % keep_backups_for_days,
"--archive-dir", "/tmp/duplicity-archive-dir",
"--name", "mailinabox",
"%dD" % config["min_age_in_days"],
"--verbosity", "error",
"--archive-dir", backup_cache_dir,
"--force",
"--verbosity", "warning",
"file://" + backup_duplicity_dir
])
get_duplicity_target_url(config)
] + get_duplicity_additional_args(env),
get_duplicity_env_vars(env))
# Remove duplicity's cache directory because it's redundant with our backup directory.
shutil.rmtree("/tmp/duplicity-archive-dir")
# From duplicity's manual:
# "This should only be necessary after a duplicity session fails or is
# aborted prematurely."
# That may be unlikely here but we may as well ensure we tidy up if
# that does happen - it might just have been a poorly timed reboot.
shell('check_call', [
"/usr/local/bin/duplicity",
"cleanup",
"--verbosity", "error",
"--archive-dir", backup_cache_dir,
"--force",
get_duplicity_target_url(config)
] + get_duplicity_additional_args(env),
get_duplicity_env_vars(env))
# Encrypt all of the new files.
backup_encrypted_dir = os.path.join(backup_dir, 'encrypted')
os.makedirs(backup_encrypted_dir, exist_ok=True)
for fn in os.listdir(backup_duplicity_dir):
fn2 = os.path.join(backup_encrypted_dir, fn) + ".enc"
if os.path.exists(fn2): continue
# Encrypt the backup using the backup private key.
shell('check_call', [
"/usr/bin/openssl",
"enc",
"-aes-256-cbc",
"-a",
"-salt",
"-in", os.path.join(backup_duplicity_dir, fn),
"-out", fn2,
"-pass", "file:%s" % os.path.join(backup_dir, "secret_key.txt"),
])
# The backup can be decrypted with:
# openssl enc -d -aes-256-cbc -a -in latest.tgz.enc -out /dev/stdout -pass file:secret_key.txt | tar -z
# Remove encrypted backups that are no longer needed.
for fn in os.listdir(backup_encrypted_dir):
fn2 = os.path.join(backup_duplicity_dir, fn.replace(".enc", ""))
if os.path.exists(fn2): continue
os.unlink(os.path.join(backup_encrypted_dir, fn))
# Change ownership of backups to the user-data user, so that the after-bcakup
# script can access them.
if get_target_type(config) == 'file':
shell('check_call',
["/bin/chown", "-R", env["STORAGE_USER"], backup_dir])
# Execute a post-backup script that does the copying to a remote server.
# Run as the STORAGE_USER user, not as root. Pass our settings in
# environment variables so the script has access to STORAGE_ROOT.
post_script = os.path.join(backup_dir, 'after-backup')
post_script = os.path.join(backup_root, 'after-backup')
if os.path.exists(post_script):
shell('check_call',
['su', env['STORAGE_USER'], '-c', post_script],
['su', env['STORAGE_USER'], '-c', post_script, config["target"]],
env=env)
# Our nightly cron job executes system status checks immediately after this
# backup. Since it checks that dovecot and postfix are running, block for a
# bit (maximum of 10 seconds each) to give each a chance to finish restarting
# before the status checks might catch them down. See #381.
if user_initiated:
# God forgive me for what I'm about to do
lock._release()
# We don't need to wait for the services to be up in this case
else:
wait_for_service(25, True, env, 10)
wait_for_service(993, True, env, 10)
def run_duplicity_verification():
env = load_environment()
backup_root = os.path.join(env["STORAGE_ROOT"], 'backup')
config = get_backup_config(env)
backup_cache_dir = os.path.join(backup_root, 'cache')
shell('check_call', [
"/usr/local/bin/duplicity",
"--verbosity",
"info",
"verify",
"--compare-data",
"--archive-dir", backup_cache_dir,
"--exclude", backup_root,
get_duplicity_target_url(config),
env["STORAGE_ROOT"],
] + get_duplicity_additional_args(env), get_duplicity_env_vars(env))
def run_duplicity_restore(args):
env = load_environment()
config = get_backup_config(env)
backup_cache_dir = os.path.join(env["STORAGE_ROOT"], 'backup', 'cache')
shell('check_call', [
"/usr/local/bin/duplicity",
"restore",
"--archive-dir", backup_cache_dir,
get_duplicity_target_url(config),
] + get_duplicity_additional_args(env) + args,
get_duplicity_env_vars(env))
def list_target_files(config):
import urllib.parse
try:
target = urllib.parse.urlparse(config["target"])
except ValueError:
return "invalid target"
if target.scheme == "file":
return [(fn, os.path.getsize(os.path.join(target.path, fn)))
for fn in os.listdir(target.path)]
elif target.scheme == "rsync":
rsync_fn_size_re = re.compile(r'.* ([^ ]*) [^ ]* [^ ]* (.*)')
rsync_target = '{host}:{path}'
target_path = target.path
if not target_path.endswith('/'):
target_path = target_path + '/'
if target_path.startswith('/'):
target_path = target_path[1:]
rsync_command = [
'rsync', '-e',
f"/usr/bin/ssh -oStrictHostKeyChecking=no -oBatchMode=yes -p {int(config.get('target_rsync_port', 22))} -i /root/.ssh/id_rsa_miab",
'--list-only', '-r',
rsync_target.format(host=target.netloc, path=target_path)
]
code, listing = shell('check_output',
rsync_command,
trap=True,
capture_stderr=True)
if code == 0:
ret = []
for l in listing.split('\n'):
match = rsync_fn_size_re.match(l)
if match:
ret.append((match.groups()[1],
int(match.groups()[0].replace(',', ''))))
return ret
else:
if 'Permission denied (publickey).' in listing:
reason = "Invalid user or check you correctly copied the SSH key."
elif 'No such file or directory' in listing:
reason = "Provided path {} is invalid.".format(target_path)
elif 'Network is unreachable' in listing:
reason = "The IP address {} is unreachable.".format(
target.hostname)
elif 'Could not resolve hostname' in listing:
reason = "The hostname {} cannot be resolved.".format(
target.hostname)
else:
reason = "Unknown error. " \
"Please check running 'management/backup.py --verify' " \
"from mailinabox sources to debug the issue."
raise ValueError(
"Connection to rsync host failed: {}".format(reason))
elif target.scheme == "s3":
import boto3.s3
from botocore.exceptions import ClientError
# separate bucket from path in target
bucket = target.path[1:].split('/')[0]
path = '/'.join(target.path[1:].split('/')[1:]) + '/'
# If no prefix is specified, set the path to '', otherwise boto won't list the files
if path == '/':
path = ''
if bucket == "":
raise ValueError("Enter an S3 bucket name.")
# connect to the region & bucket
try:
s3 = boto3.client('s3', \
endpoint_url=f'https://{target.hostname}', \
aws_access_key_id=config['target_user'], \
aws_secret_access_key=config['target_pass'])
bucket_objects = s3.list_objects_v2(Bucket=bucket, Prefix=path).get("Contents", [])
backup_list = [(key['Key'][len(path):], key['Size']) for key in bucket_objects]
except ClientError as e:
raise ValueError(e)
return backup_list
elif target.scheme == 'b2':
InMemoryAccountInfo = None
B2Api = None
NonExistentBucket = None
from b2sdk.v1 import InMemoryAccountInfo, B2Api
from b2sdk.v1.exception import NonExistentBucket
info = InMemoryAccountInfo()
b2_api = B2Api(info)
# Extract information from target
b2_application_keyid = target.netloc[:target.netloc.index(':')]
b2_application_key = target.netloc[target.netloc.index(':') +
1:target.netloc.index('@')]
b2_bucket = target.netloc[target.netloc.index('@') + 1:]
try:
b2_api.authorize_account("production", b2_application_keyid,
b2_application_key)
bucket = b2_api.get_bucket_by_name(b2_bucket)
except NonExistentBucket as e:
raise ValueError(
"B2 Bucket does not exist. Please double check your information!"
)
return [(key.file_name, key.size) for key, _ in bucket.ls()]
else:
raise ValueError(config["target"])
def backup_set_custom(env, target, target_user, target_pass, target_rsync_port, min_age):
config = get_backup_config(env, for_save=True)
# min_age must be an int
if isinstance(min_age, str):
min_age = int(min_age)
if isinstance(target_rsync_port, str):
try:
target_rsync_port = int(target_rsync_port)
except:
target_rsync_port = 22
config["target"] = target
config["target_user"] = target_user
config["target_pass"] = target_pass
config["target_rsync_port"] = target_rsync_port
config["min_age_in_days"] = min_age
# Validate.
try:
if config["target"] not in ("off", "local"):
# these aren't supported by the following function, which expects a full url in the target key,
# which is what is there except when loading the config prior to saving
list_target_files(config)
except ValueError as e:
return str(e)
write_backup_config(env, config)
return "OK"
def get_backup_config(env, for_save=False, for_ui=False):
backup_root = os.path.join(env["STORAGE_ROOT"], 'backup')
# Defaults.
config = {"min_age_in_days": 3, "target": "local", "target_rsync_port": 22}
# Merge in anything written to custom.yaml.
try:
custom_config = rtyaml.load(
open(os.path.join(backup_root, 'custom.yaml')))
if not isinstance(custom_config, dict):
raise ValueError() # caught below
config.update(custom_config)
except:
pass
# When updating config.yaml, don't do any further processing on what we find.
if for_save:
return config
# When passing this back to the admin to show the current settings, do not include
# authentication details. The user will have to re-enter it.
if for_ui:
for field in ("target_user", "target_pass"):
if field in config:
del config[field]
# helper fields for the admin
config["file_target_directory"] = os.path.join(backup_root, 'encrypted')
config["enc_pw_file"] = os.path.join(backup_root, 'secret_key.txt')
if config["target"] == "local":
# Expand to the full URL.
config["target"] = "file://" + config["file_target_directory"]
ssh_pub_key = os.path.join('/root', '.ssh', 'id_rsa_miab.pub')
if os.path.exists(ssh_pub_key):
config["ssh_pub_key"] = open(ssh_pub_key, 'r').read()
return config
def write_backup_config(env, newconfig):
backup_root = os.path.join(env["STORAGE_ROOT"], 'backup')
with open(os.path.join(backup_root, 'custom.yaml'), "w") as f:
f.write(rtyaml.dump(newconfig))
if __name__ == "__main__":
import sys
full_backup = "--full" in sys.argv
perform_backup(full_backup)
if sys.argv[-1] == "--verify":
# Run duplicity's verification command to check a) the backup files
# are readable, and b) report if they are up to date.
run_duplicity_verification()
elif sys.argv[-1] == "--list":
# List the saved backup files.
for fn, size in list_target_files(get_backup_config(
load_environment())):
print("{}\t{}".format(fn, size))
elif sys.argv[-1] == "--status":
# Show backup status.
ret = backup_status(load_environment())
print(rtyaml.dump(ret["backups"]))
print("Storage for unmatched files:", ret["unmatched_file_size"])
elif len(sys.argv) >= 2 and sys.argv[1] == "--restore":
# Run duplicity restore. Rest of command line passed as arguments
# to duplicity. The restore path should be specified.
run_duplicity_restore(sys.argv[2:])
else:
# Perform a backup. Add --full to force a full backup rather than
# possibly performing an incremental backup.
full_backup = "--full" in sys.argv
perform_backup(full_backup)

View file

@ -1,155 +0,0 @@
#!/usr/bin/python3
# Helps you purchase a SSL certificate from Gandi.net using
# their API.
#
# Before you begin:
# 1) Create an account on Gandi.net.
# 2) Pre-pay $16 into your account at https://www.gandi.net/prepaid/operations. Wait until the payment goes through.
# 3) Activate your API key first on the test platform (wait a while, refresh the page) and then activate the production API at https://www.gandi.net/admin/api_key.
import sys, re, os.path, urllib.request
import xmlrpc.client
import rtyaml
from utils import load_environment, shell
from web_update import get_web_domains, get_domain_ssl_files, get_web_root
from status_checks import check_certificate
def buy_ssl_certificate(api_key, domain, command, env):
if domain != env['PRIMARY_HOSTNAME'] \
and domain not in get_web_domains(env):
raise ValueError("Domain is not %s or a domain we're serving a website for." % env['PRIMARY_HOSTNAME'])
# Initialize.
gandi = xmlrpc.client.ServerProxy('https://rpc.gandi.net/xmlrpc/')
try:
existing_certs = gandi.cert.list(api_key)
except Exception as e:
if "Invalid API key" in str(e):
print("Invalid API key. Check that you copied the API Key correctly from https://www.gandi.net/admin/api_key.")
sys.exit(1)
else:
raise
# Where is the SSL cert stored?
ssl_key, ssl_certificate, ssl_csr_path = get_domain_ssl_files(domain, env)
# Have we already created a cert for this domain?
for cert in existing_certs:
if cert['cn'] == domain:
break
else:
# No existing cert found. Purchase one.
if command != 'purchase':
print("No certificate or order found yet. If you haven't yet purchased a certificate, run ths script again with the 'purchase' command. Otherwise wait a moment and try again.")
sys.exit(1)
else:
# Start an order for a single standard SSL certificate.
# Use DNS validation. Web-based validation won't work because they
# require a file on HTTP but not HTTPS w/o redirects and we don't
# serve anything plainly over HTTP. Email might be another way but
# DNS is easier to automate.
op = gandi.cert.create(api_key, {
"csr": open(ssl_csr_path).read(),
"dcv_method": "dns",
"duration": 1, # year?
"package": "cert_std_1_0_0",
})
print("An SSL certificate has been ordered.")
print()
print(op)
print()
print("In a moment please run this script again with the 'setup' command.")
if cert['status'] == 'pending':
# Get the information we need to update our DNS with a code so that
# Gandi can verify that we own the domain.
dcv = gandi.cert.get_dcv_params(api_key, {
"csr": open(ssl_csr_path).read(),
"cert_id": cert['id'],
"dcv_method": "dns",
"duration": 1, # year?
"package": "cert_std_1_0_0",
})
if dcv["dcv_method"] != "dns":
raise Exception("Certificate ordered with an unknown validation method.")
# Update our DNS data.
dns_config = env['STORAGE_ROOT'] + '/dns/custom.yaml'
if os.path.exists(dns_config):
dns_records = rtyaml.load(open(dns_config))
else:
dns_records = { }
qname = dcv['md5'] + '.' + domain
value = dcv['sha1'] + '.comodoca.com.'
dns_records[qname] = { "CNAME": value }
with open(dns_config, 'w') as f:
f.write(rtyaml.dump(dns_records))
shell('check_call', ['tools/dns_update'])
# Okay, done with this step.
print("DNS has been updated. Gandi will check within 60 minutes.")
print()
print("See https://www.gandi.net/admin/ssl/%d/details for the status of this order." % cert['id'])
elif cert['status'] == 'valid':
# The certificate is ready.
# Check before we overwrite something we shouldn't.
if os.path.exists(ssl_certificate):
cert_status = check_certificate(None, ssl_certificate, None)
if cert_status != "SELF-SIGNED":
print("Please back up and delete the file %s so I can save your new certificate." % ssl_certificate)
sys.exit(1)
# Form the certificate.
# The certificate comes as a long base64-encoded string. Break in
# into lines in the usual way.
pem = "-----BEGIN CERTIFICATE-----\n"
pem += "\n".join(chunk for chunk in re.split(r"(.{64})", cert['cert']) if chunk != "")
pem += "\n-----END CERTIFICATE-----\n\n"
# Append intermediary certificates.
pem += urllib.request.urlopen("https://www.gandi.net/static/CAs/GandiStandardSSLCA.pem").read().decode("ascii")
# Write out.
with open(ssl_certificate, "w") as f:
f.write(pem)
print("The certificate has been installed in %s. Restarting services..." % ssl_certificate)
# Restart dovecot and if this is for PRIMARY_HOSTNAME.
if domain == env['PRIMARY_HOSTNAME']:
shell('check_call', ["/usr/sbin/service", "dovecot", "restart"])
shell('check_call', ["/usr/sbin/service", "postfix", "restart"])
# Restart nginx in all cases.
shell('check_call', ["/usr/sbin/service", "nginx", "restart"])
else:
print("The certificate has an unknown status. Please check https://www.gandi.net/admin/ssl/%d/details for the status of this order." % cert['id'])
if __name__ == "__main__":
if len(sys.argv) < 4:
print("Usage: python management/buy_certificate.py gandi_api_key domain_name {purchase, setup}")
sys.exit(1)
api_key = sys.argv[1]
domain_name = sys.argv[2]
cmd = sys.argv[3]
buy_ssl_certificate(api_key, domain_name, cmd, load_environment())

204
management/cli.py Executable file
View file

@ -0,0 +1,204 @@
#!/usr/bin/python3
#
# This is a command-line script for calling management APIs
# on the Mail-in-a-Box control panel backend. The script
# reads /var/lib/mailinabox/api.key for the backend's
# root API key. This file is readable only by root, so this
# tool can only be used as root.
import sys
import getpass
import urllib.request
import urllib.error
import json
import re
import csv
def mgmt(cmd, data=None, is_json=False):
# The base URL for the management daemon. (Listens on IPv4 only.)
mgmt_uri = 'http://127.0.0.1:10222'
setup_key_auth(mgmt_uri)
req = urllib.request.Request(
mgmt_uri + cmd,
urllib.parse.urlencode(data).encode("utf8") if data else None)
try:
response = urllib.request.urlopen(req)
except urllib.error.HTTPError as e:
if e.code == 401:
try:
print(e.read().decode("utf8"))
except:
pass
print(
"The management daemon refused access. The API key file may be out of sync. Try 'service mailinabox restart'.",
file=sys.stderr)
elif hasattr(e, 'read'):
print(e.read().decode('utf8'), file=sys.stderr)
else:
print(e, file=sys.stderr)
sys.exit(1)
resp = response.read().decode('utf8')
if is_json:
resp = json.loads(resp)
return resp
def read_password():
while True:
first = getpass.getpass('password: ')
if len(first) < 8:
print("Passwords must be at least eight characters.")
continue
second = getpass.getpass(' (again): ')
if first != second:
print("Passwords not the same. Try again.")
continue
break
return first
def setup_key_auth(mgmt_uri):
key = open('/var/lib/mailinabox/api.key').read().strip()
auth_handler = urllib.request.HTTPBasicAuthHandler()
auth_handler.add_password(realm='Mail-in-a-Box Management Server',
uri=mgmt_uri,
user=key,
passwd='')
opener = urllib.request.build_opener(auth_handler)
urllib.request.install_opener(opener)
if len(sys.argv) < 2:
print("""Usage:
{cli} system default-quota [new default] (set default quota for system)
{cli} user (lists users)
{cli} user add user@domain.com [password]
{cli} user password user@domain.com [password]
{cli} user remove user@domain.com
{cli} user make-admin user@domain.com
{cli} user quota user@domain [new-quota]
{cli} user remove-admin user@domain.com
{cli} user admins (lists admins)
{cli} user mfa show user@domain.com (shows MFA devices for user, if any)
{cli} user mfa disable user@domain.com [id] (disables MFA for user)
{cli} alias (lists aliases)
{cli} alias add incoming.name@domain.com sent.to@other.domain.com
{cli} alias add incoming.name@domain.com 'sent.to@other.domain.com, multiple.people@other.domain.com'
{cli} alias remove incoming.name@domain.com
Removing a mail user does not delete their mail folders on disk. It only prevents IMAP/SMTP login.
""".format(cli="management/cli.py"))
elif sys.argv[1] == "user" and len(sys.argv) == 2:
# Dump a list of users, one per line. Mark admins with an asterisk.
users = mgmt("/mail/users?format=json", is_json=True)
for domain in users:
for user in domain["users"]:
if user['status'] == 'inactive':
continue
print(user['email'], end='')
if "admin" in user['privileges']:
print("*", end='')
if user['quota'] == '0':
print(" unlimited", end='')
else:
print(" " + user['quota'], end='')
print()
elif sys.argv[1] == "user" and sys.argv[2] in ("add", "password"):
if len(sys.argv) < 5:
if len(sys.argv) < 4:
email = input("email: ")
else:
email = sys.argv[3]
pw = read_password()
else:
email, pw = sys.argv[3:5]
if sys.argv[2] == "add":
print(mgmt("/mail/users/add", {"email": email, "password": pw}))
elif sys.argv[2] == "password":
print(mgmt("/mail/users/password", {"email": email, "password": pw}))
elif sys.argv[1] == "user" and sys.argv[2] == "remove" and len(sys.argv) == 4:
print(mgmt("/mail/users/remove", {"email": sys.argv[3]}))
elif sys.argv[1] == "user" and sys.argv[2] in ("make-admin",
"remove-admin") and len(
sys.argv) == 4:
if sys.argv[2] == "make-admin":
action = "add"
else:
action = "remove"
print(
mgmt("/mail/users/privileges/" + action, {
"email": sys.argv[3],
"privilege": "admin"
}))
elif sys.argv[1] == "user" and sys.argv[2] == "admins":
# Dump a list of admin users.
users = mgmt("/mail/users?format=json", is_json=True)
for domain in users:
for user in domain["users"]:
if "admin" in user['privileges']:
print(user['email'])
elif sys.argv[1] == "user" and sys.argv[2] == "quota" and len(sys.argv) == 4:
# Set a user's quota
print(mgmt("/mail/users/quota?text=1&email=%s" % sys.argv[3]))
elif sys.argv[1] == "user" and sys.argv[2] == "quota" and len(sys.argv) == 5:
# Set a user's quota
users = mgmt("/mail/users/quota", {
"email": sys.argv[3],
"quota": sys.argv[4]
})
elif sys.argv[1] == "user" and len(
sys.argv) == 5 and sys.argv[2:4] == ["mfa", "show"]:
# Show MFA status for a user.
status = mgmt("/mfa/status", {"user": sys.argv[4]}, is_json=True)
W = csv.writer(sys.stdout)
W.writerow(["id", "type", "label"])
for mfa in status["enabled_mfa"]:
W.writerow([mfa["id"], mfa["type"], mfa["label"]])
elif sys.argv[1] == "user" and len(
sys.argv) in (5, 6) and sys.argv[2:4] == ["mfa", "disable"]:
# Disable MFA (all or a particular device) for a user.
print(
mgmt(
"/mfa/disable", {
"user": sys.argv[4],
"mfa-id": sys.argv[5] if len(sys.argv) == 6 else None
}))
elif sys.argv[1] == "alias" and len(sys.argv) == 2:
print(mgmt("/mail/aliases"))
elif sys.argv[1] == "alias" and sys.argv[2] == "add" and len(sys.argv) == 5:
print(
mgmt("/mail/aliases/add", {
"address": sys.argv[3],
"forwards_to": sys.argv[4]
}))
elif sys.argv[1] == "alias" and sys.argv[2] == "remove" and len(sys.argv) == 4:
print(mgmt("/mail/aliases/remove", {"address": sys.argv[3]}))
elif sys.argv[1] == "system" and sys.argv[2] == "default-quota" and len(
sys.argv) == 3:
print(mgmt("/system/default-quota?text=1"))
elif sys.argv[1] == "system" and sys.argv[2] == "default-quota" and len(
sys.argv) == 4:
print(mgmt("/system/default-quota", {"default_quota": sys.argv[3]}))
else:
print("Invalid command-line arguments.")
sys.exit(1)

View file

@ -1,27 +1,28 @@
# This list is derived from https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2.
# The columns are ISO_3166-1_alpha-2 code, display name, Wikipedia page name.
# The top 20 countries by number of Internet users are grouped first, see
# The top 21 countries by number of Internet users are grouped first, see
# https://en.wikipedia.org/wiki/List_of_countries_by_number_of_Internet_users.
BR Brazil
CA Canada
CN China
EG Egypt
FR France
DE Germany
IN India
ID Indonesia
IT Italy
JP Japan
MX Mexico
NG Nigeria
PH Philippines
RU Russian Federation Russia
ES Spain
KR South Korea
TR Turkey
GB United Kingdom
US United States
JP Japan
BR Brazil
RU Russian Federation Russia
DE Germany
NG Nigeria
GB United Kingdom
FR France
MX Mexico
EG Egypt
KR South Korea
VN Vietnam
ID Indonesia
PH Philippines
TR Turkey
IT Italy
PK Pakistan
ES Spain
CA Canada
AD Andorra
AE United Arab Emirates
AF Afghanistan
@ -183,7 +184,6 @@ PA Panama
PE Peru
PF French Polynesia
PG Papua New Guinea
PK Pakistan
PL Poland
PM Saint Pierre and Miquelon
PN Pitcairn Pitcairn Islands
Can't render this file because it has a wrong number of fields in line 5.

File diff suppressed because it is too large Load diff

28
management/daily_tasks.sh Executable file
View file

@ -0,0 +1,28 @@
#!/bin/bash
# This script is run daily (at 3am each night).
# Set character encoding flags to ensure that any non-ASCII
# characters don't cause problems. See setup/start.sh and
# the management daemon startup script.
export LANGUAGE=en_US.UTF-8
export LC_ALL=en_US.UTF-8
export LANG=en_US.UTF-8
export LC_TYPE=en_US.UTF-8
# On Mondays, i.e. once a week, send the administrator a report of total emails
# sent and received so the admin might notice server abuse.
if [ `date "+%u"` -eq 1 ]; then
management/mail_log.py -t week | management/email_administrator.py "Mail-in-a-Box Usage Report"
fi
# Take a backup.
management/backup.py 2>&1 | management/email_administrator.py "Backup Status"
# Provision any new certificates for new domains or domains with expiring certificates.
management/ssl_certificates.py -q 2>&1 | management/email_administrator.py "TLS Certificate Provisioning Result"
# Renew the daemon's PGP key if about to expire
management/pgp.py 2>&1 | management/email_administrator.py "PGP Key Renewal Result"
# Run status checks and email the administrator if anything changed.
management/status_checks.py --show-changes 2>&1 | management/email_administrator.py "Status Checks Change Notice"

File diff suppressed because it is too large Load diff

165
management/editconf.py Executable file
View file

@ -0,0 +1,165 @@
#!/usr/bin/python3
#
# This is a helper tool for editing configuration files during the setup
# process. The tool is given new values for settings as command-line
# arguments. It comments-out existing setting values in the configuration
# file and adds new values either after their former location or at the
# end.
#
# The configuration file has settings that look like:
#
# NAME=VALUE
#
# If the -s option is given, then space becomes the delimiter, i.e.:
#
# NAME VALUE
#
# If the -c option is given, then the supplied character becomes the comment character
#
# If the -w option is given, then setting lines continue onto following
# lines while the lines start with whitespace, e.g.:
#
# NAME VAL
# UE
# create the new config file in memory
import sys
import re
def edit_conf(filename,
settings,
delimiter_re,
delimiter,
comment_char,
folded_lines=False,
testing=False,
erase_setting=False):
found = set()
buf = ""
input_lines = list(open(filename, "r+"))
while len(input_lines) > 0:
line = input_lines.pop(0)
# If this configuration file uses folded lines, append any folded lines
# into our input buffer.
if folded_lines and line[0] not in (comment_char, " ", ""):
while len(input_lines) > 0 and input_lines[0][0] in " \t":
line += input_lines.pop(0)
# See if this line is for any settings passed on the command line.
for i in range(len(settings)):
# Check whether this line contain this setting from the command-line arguments.
name, val = settings[i].split("=", 1)
m = re.match(
"(\s*)" + "(" + re.escape(comment_char) + "\s*)?" +
re.escape(name) + delimiter_re + "(.*?)\s*$", line, re.S)
if not m:
continue
indent, is_comment, existing_val = m.groups()
# If this is already the setting, keep it in the file, except:
# * 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
# in the file so check for that first.
if i in found:
break
buf += line
found.add(i)
break
# comment-out the existing line (also comment any folded lines)
if is_comment is None:
buf += comment_char + line.rstrip().replace(
"\n", "\n" + comment_char) + "\n"
else:
# the line is already commented, pass it through
buf += line
# if this option oddly appears more than once, don't add the setting again
# Or if we're clearing it, don't add it
if (i in found) or (not val and erase_setting):
break
# add the new setting
buf += indent + name + delimiter + val + "\n"
# note that we've applied this option
found.add(i)
break
else:
# If did not match any setting names, pass this line through.
buf += line
# Put any settings we didn't see at the end of the file, except those being erased.
for i in range(len(settings)):
if i not in found:
name, val = settings[i].split("=", 1)
if not (not val and erase_setting):
buf += name + delimiter + val + "\n"
if not testing:
# Write out the new file.
with open(filename, "w") as f:
f.write(buf)
else:
# Just print the new file to stdout.
print(buf)
# Run standalone
if __name__ == "__main__":
# sanity check
if len(sys.argv) < 3:
print(
"usage: python3 editconf.py /etc/file.conf [-s] [-w] [-c <CHARACTER>] [-t] NAME=VAL [NAME=VAL ...]"
)
sys.exit(1)
# parse command line arguments
filename = sys.argv[1]
settings = sys.argv[2:]
delimiter = "="
delimiter_re = r"\s*=\s*"
comment_char = "#"
folded_lines = False
testing = False
erase_setting = False
while settings[0][0] == "-" and settings[0] != "--":
opt = settings.pop(0)
if opt == "-s":
# Space is the delimiter
delimiter = " "
delimiter_re = r"\s+"
elif opt == "-e":
# Erase settings that have empty values.
erase_setting = True
elif opt == "-w":
# Line folding is possible in this file.
folded_lines = True
elif opt == "-c":
# Specifies a different comment character.
comment_char = settings.pop(0)
elif opt == "-t":
testing = True
else:
print("Invalid option.")
sys.exit(1)
# sanity check command line
for setting in settings:
try:
name, value = setting.split("=", 1)
except:
import subprocess
print("Invalid command line: ", subprocess.list2cmdline(sys.argv))
sys.exit(1)
edit_conf(filename, settings, delimiter_re, delimiter, comment_char,
folded_lines, testing, erase_setting)

View file

@ -0,0 +1,64 @@
#!/usr/local/lib/mailinabox/env/bin/python
# Reads in STDIN. If the stream is not empty, mail it to the system administrator.
import sys
import html
import smtplib
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from pgp import create_signature
# In Python 3.6:
#from email.message import Message
from utils import load_environment
# Load system environment info.
env = load_environment()
# Process command line args.
subject = sys.argv[1]
# Read in STDIN.
content = sys.stdin.read().strip()
# If there's nothing coming in, just exit.
if content == "":
sys.exit(0)
# create MIME message
msg = MIMEMultipart('alternative')
# In Python 3.6:
#msg = Message()
noreply = "noreply-daemon@" + env['PRIMARY_HOSTNAME']
admin_addr = "administrator@" + env['PRIMARY_HOSTNAME']
msg['From'] = "\"%s\" <%s>" % ("System Management Daemon",
"noreply-daemon@" + env['PRIMARY_HOSTNAME'])
msg['To'] = "administrator@" + env['PRIMARY_HOSTNAME']
msg['Subject'] = "[%s] %s" % (env['PRIMARY_HOSTNAME'], subject)
content_html = "<html><body><pre>{}</pre></body></html>".format(
html.escape(content))
msg.attach(MIMEText(create_signature(content.encode()).decode(), 'plain'))
msg.attach(MIMEText(content_html, 'html'))
# In Python 3.6:
# msg.set_content(content)
#msg.add_alternative(content_html, "html")
# send
smtpclient = smtplib.SMTP('127.0.0.1', 25)
smtpclient.ehlo()
smtpclient.sendmail(
noreply, # MAIL FROM
admin_addr, # RCPT TO
msg.as_string())
smtpclient.quit()

926
management/mail_log.py Executable file
View file

@ -0,0 +1,926 @@
#!/usr/local/lib/mailinabox/env/bin/python
import argparse
import datetime
import gzip
import os.path
import re
import shutil
import tempfile
import textwrap
from collections import defaultdict, OrderedDict
import dateutil.parser
import time
from dateutil.relativedelta import relativedelta
import utils
LOG_FILES = (
'/var/log/mail.log.6.gz',
'/var/log/mail.log.5.gz',
'/var/log/mail.log.4.gz',
'/var/log/mail.log.3.gz',
'/var/log/mail.log.2.gz',
'/var/log/mail.log.1',
'/var/log/mail.log',
)
TIME_DELTAS = OrderedDict([
('all', datetime.timedelta(weeks=52)),
('month', datetime.timedelta(weeks=4)),
('2weeks', datetime.timedelta(days=14)),
('week', datetime.timedelta(days=7)),
('2days', datetime.timedelta(days=2)), ('day', datetime.timedelta(days=1)),
('12hours', datetime.timedelta(hours=12)),
('6hours', datetime.timedelta(hours=6)),
('hour', datetime.timedelta(hours=1)),
('30min', datetime.timedelta(minutes=30)),
('10min', datetime.timedelta(minutes=10)),
('5min', datetime.timedelta(minutes=5)),
('min', datetime.timedelta(minutes=1)),
('today', datetime.datetime.now() -
datetime.datetime.now().replace(hour=0, minute=0, second=0))
])
END_DATE = NOW = datetime.datetime.now()
START_DATE = None
VERBOSE = False
# List of strings to filter users with
FILTERS = None
# What to show (with defaults)
SCAN_OUT = True # Outgoing email
SCAN_IN = True # Incoming email
SCAN_DOVECOT_LOGIN = True # Dovecot Logins
SCAN_GREY = False # Greylisted email
SCAN_BLOCKED = False # Rejected email
def scan_files(collector):
""" Scan files until they run out or the earliest date is reached """
stop_scan = False
for fn in LOG_FILES:
tmp_file = None
if not os.path.exists(fn):
continue
elif fn[-3:] == '.gz':
tmp_file = tempfile.NamedTemporaryFile()
shutil.copyfileobj(gzip.open(fn), tmp_file)
if VERBOSE:
print("Processing file", fn, "...")
fn = tmp_file.name if tmp_file else fn
for line in readline(fn):
if scan_mail_log_line(line.strip(), collector) is False:
if stop_scan:
return
stop_scan = True
else:
stop_scan = False
def scan_mail_log(env):
""" Scan the system's mail log files and collect interesting data
This function scans the 2 most recent mail log files in /var/log/.
Args:
env (dict): Dictionary containing MiaB settings
"""
collector = {
"scan_count": 0, # Number of lines scanned
"parse_count":
0, # Number of lines parsed (i.e. that had their contents examined)
"scan_time": time.time(), # The time in seconds the scan took
"sent_mail": OrderedDict(), # Data about email sent by users
"received_mail": OrderedDict(), # Data about email received by users
"logins": OrderedDict(), # Data about login activity
"postgrey": {}, # Data about greylisting of email addresses
"rejected": OrderedDict(), # Emails that were blocked
"known_addresses": None, # Addresses handled by the Miab installation
"other-services": set(),
}
try:
import mailconfig
collector["known_addresses"] = (
set(mailconfig.get_mail_users(env))
| set(alias[0] for alias in mailconfig.get_mail_aliases(env)))
except ImportError:
pass
print("Scanning logs from {:%Y-%m-%d %H:%M:%S} to {:%Y-%m-%d %H:%M:%S}".
format(START_DATE, END_DATE))
# Scan the lines in the log files until the date goes out of range
scan_files(collector)
if not collector["scan_count"]:
print("No log lines scanned...")
return
collector["scan_time"] = time.time() - collector["scan_time"]
print(
"{scan_count} Log lines scanned, {parse_count} lines parsed in {scan_time:.2f} "
"seconds\n".format(**collector))
# Print Sent Mail report
if collector["sent_mail"]:
msg = "Sent email"
print_header(msg)
data = OrderedDict(
sorted(collector["sent_mail"].items(), key=email_sort))
print_user_table(
data.keys(),
data=[
("sent", [u["sent_count"] for u in data.values()]),
("hosts", [len(u["hosts"]) for u in data.values()]),
],
sub_data=[
("sending hosts", [u["hosts"] for u in data.values()]),
],
activity=[
("sent", [u["activity-by-hour"] for u in data.values()]),
],
earliest=[u["earliest"] for u in data.values()],
latest=[u["latest"] for u in data.values()],
)
accum = defaultdict(int)
data = collector["sent_mail"].values()
for h in range(24):
accum[h] = sum(d["activity-by-hour"][h] for d in data)
print_time_table(["sent"], [accum])
# Print Received Mail report
if collector["received_mail"]:
msg = "Received email"
print_header(msg)
data = OrderedDict(
sorted(collector["received_mail"].items(), key=email_sort))
print_user_table(
data.keys(),
data=[
("received", [u["received_count"] for u in data.values()]),
],
activity=[
("sent", [u["activity-by-hour"] for u in data.values()]),
],
earliest=[u["earliest"] for u in data.values()],
latest=[u["latest"] for u in data.values()],
)
accum = defaultdict(int)
for h in range(24):
accum[h] = sum(d["activity-by-hour"][h] for d in data.values())
print_time_table(["received"], [accum])
# Print login report
if collector["logins"]:
msg = "User logins per hour"
print_header(msg)
data = OrderedDict(sorted(collector["logins"].items(), key=email_sort))
# Get a list of all of the protocols seen in the logs in reverse count order.
all_protocols = defaultdict(int)
for u in data.values():
for protocol_name, count in u["totals_by_protocol"].items():
all_protocols[protocol_name] += count
all_protocols = [
k for k, v in sorted(all_protocols.items(), key=lambda kv: -kv[1])
]
print_user_table(
data.keys(),
data=[
(
protocol_name,
[
round(
u["totals_by_protocol"][protocol_name] /
(u["latest"] - u["earliest"]).total_seconds() *
60 * 60, 1) if
(u["latest"] - u["earliest"]).total_seconds() > 0 else
0 # prevent division by zero
for u in data.values()
]) for protocol_name in all_protocols
],
sub_data=[("Protocol and Source", [[
"{} {}: {} times".format(protocol_name, host, count)
for (protocol_name, host), count in sorted(
u["totals_by_protocol_and_host"].items(),
key=lambda kv: -kv[1])
] for u in data.values()])],
activity=[
(protocol_name,
[u["activity-by-hour"][protocol_name] for u in data.values()])
for protocol_name in all_protocols
],
earliest=[u["earliest"] for u in data.values()],
latest=[u["latest"] for u in data.values()],
numstr=lambda n: str(round(n, 1)),
)
accum = {
protocol_name: defaultdict(int)
for protocol_name in all_protocols
}
for h in range(24):
for protocol_name in all_protocols:
accum[protocol_name][h] = sum(
d["activity-by-hour"][protocol_name][h]
for d in data.values())
print_time_table(
all_protocols,
[accum[protocol_name] for protocol_name in all_protocols])
if collector["postgrey"]:
msg = "Greylisted Email {:%Y-%m-%d %H:%M:%S} and {:%Y-%m-%d %H:%M:%S}"
print_header(msg.format(START_DATE, END_DATE))
print(textwrap.fill(
"The following mail was greylisted, meaning the emails were temporarily rejected. "
"Legitimate senders must try again after three minutes.",
width=80,
initial_indent=" ",
subsequent_indent=" "),
end='\n\n')
data = OrderedDict(
sorted(collector["postgrey"].items(), key=email_sort))
users = []
received = []
senders = []
sender_clients = []
delivered_dates = []
for recipient in data:
sorted_recipients = sorted(data[recipient].items(),
key=lambda kv: kv[1][0] or kv[1][1])
for (client_address,
sender), (first_date, delivered_date) in sorted_recipients:
if first_date:
users.append(recipient)
received.append(first_date)
senders.append(sender)
delivered_dates.append(delivered_date)
sender_clients.append(client_address)
print_user_table(
users,
data=[("received", received), ("sender", senders),
("delivered",
[str(d) or "no retry yet" for d in delivered_dates]),
("sending host", sender_clients)],
delimit=True,
)
if collector["rejected"]:
msg = "Blocked Email {:%Y-%m-%d %H:%M:%S} and {:%Y-%m-%d %H:%M:%S}"
print_header(msg.format(START_DATE, END_DATE))
data = OrderedDict(
sorted(collector["rejected"].items(), key=email_sort))
rejects = []
if VERBOSE:
for user_data in data.values():
user_rejects = []
for date, sender, message in user_data["blocked"]:
if len(sender) > 64:
sender = sender[:32] + "" + sender[-32:]
user_rejects.append("%s - %s " % (date, sender))
user_rejects.append(" %s" % message)
rejects.append(user_rejects)
print_user_table(
data.keys(),
data=[
("blocked", [len(u["blocked"]) for u in data.values()]),
],
sub_data=[
("blocked emails", rejects),
],
earliest=[u["earliest"] for u in data.values()],
latest=[u["latest"] for u in data.values()],
)
if collector["other-services"] and VERBOSE and False:
print_header("Other services")
print("The following unkown services were found in the log file.")
print(" ", *sorted(list(collector["other-services"])), sep='\n')
def scan_mail_log_line(line, collector):
""" Scan a log line and extract interesting data """
m = re.match(r"(\w+[\s]+\d+ \d+:\d+:\d+) ([\w]+ )?([\w\-/]+)[^:]*: (.*)",
line)
if not m:
return True
date, system, service, log = m.groups()
collector["scan_count"] += 1
# print()
# print("date:", date)
# print("host:", system)
# print("service:", service)
# print("log:", log)
# Replaced the dateutil parser for a less clever way of parser that is roughly 4 times faster.
# date = dateutil.parser.parse(date)
# strptime fails on Feb 29 with ValueError: day is out of range for month if correct year is not provided.
# See https://bugs.python.org/issue26460
date = datetime.datetime.strptime(
str(NOW.year) + ' ' + date, '%Y %b %d %H:%M:%S')
# if log date in future, step back a year
if date > NOW:
date = date.replace(year=NOW.year - 1)
#print("date:", date)
# Check if the found date is within the time span we are scanning
if date > END_DATE:
# Don't process, and halt
return False
elif date < START_DATE:
# Don't process, but continue
return True
if service == "postfix/submission/smtpd":
if SCAN_OUT:
scan_postfix_submission_line(date, log, collector)
elif service == "postfix/lmtp":
if SCAN_IN:
scan_postfix_lmtp_line(date, log, collector)
elif service.endswith("-login"):
if SCAN_DOVECOT_LOGIN:
scan_dovecot_login_line(date, log, collector, service[:4])
elif service == "postgrey":
if SCAN_GREY:
scan_postgrey_line(date, log, collector)
elif service == "postfix/smtpd":
if SCAN_BLOCKED:
scan_postfix_smtpd_line(date, log, collector)
elif service in ("postfix/qmgr", "postfix/pickup", "postfix/cleanup",
"postfix/scache", "spampd", "postfix/anvil",
"postfix/master", "opendkim", "postfix/lmtp",
"postfix/tlsmgr", "anvil"):
# nothing to look at
return True
else:
collector["other-services"].add(service)
return True
collector["parse_count"] += 1
return True
def scan_postgrey_line(date, log, collector):
""" Scan a postgrey log line and extract interesting data """
m = re.match(
"action=(greylist|pass), reason=(.*?), (?:delay=\d+, )?client_name=(.*), "
"client_address=(.*), sender=(.*), recipient=(.*)", log)
if m:
action, reason, client_name, client_address, sender, user = m.groups()
if user_match(user):
# Might be useful to group services that use a lot of mail different servers on sub
# domains like <sub>1.domein.com
# if '.' in client_name:
# addr = client_name.split('.')
# if len(addr) > 2:
# client_name = '.'.join(addr[1:])
key = (client_address if client_name == 'unknown' else client_name,
sender)
rep = collector["postgrey"].setdefault(user, {})
if action == "greylist" and reason == "new":
rep[key] = (date, rep[key][1] if key in rep else None)
elif action == "pass":
rep[key] = (rep[key][0] if key in rep else None, date)
def scan_postfix_smtpd_line(date, log, collector):
""" Scan a postfix smtpd log line and extract interesting data """
# Check if the incoming mail was rejected
m = re.match(
"NOQUEUE: reject: RCPT from .*?: (.*?); from=<(.*?)> to=<(.*?)>", log)
if m:
message, sender, user = m.groups()
# skip this, if reported in the greylisting report
if "Recipient address rejected: Greylisted" in message:
return
# only log mail to known recipients
if user_match(user):
if collector["known_addresses"] is None or user in collector[
"known_addresses"]:
data = collector["rejected"].get(user, {
"blocked": [],
"earliest": None,
"latest": None,
})
# simplify this one
m = re.search(
r"Client host \[(.*?)\] blocked using zen.spamhaus.org; (.*)",
message)
if m:
message = "ip blocked: " + m.group(2)
else:
# simplify this one too
m = re.search(
r"Sender address \[.*@(.*)\] blocked using dbl.spamhaus.org; (.*)",
message)
if m:
message = "domain blocked: " + m.group(2)
if data["earliest"] is None:
data["earliest"] = date
data["latest"] = date
data["blocked"].append((date, sender, message))
collector["rejected"][user] = data
def scan_dovecot_login_line(date, log, collector, protocol_name):
""" Scan a dovecot login log line and extract interesting data """
m = re.match("Info: Login: user=<(.*?)>, method=PLAIN, rip=(.*?),", log)
if m:
# TODO: CHECK DIT
user, host = m.groups()
if user_match(user):
add_login(user, date, protocol_name, host, collector)
def add_login(user, date, protocol_name, host, collector):
# Get the user data, or create it if the user is new
data = collector["logins"].get(
user, {
"earliest": None,
"latest": None,
"totals_by_protocol": defaultdict(int),
"totals_by_protocol_and_host": defaultdict(int),
"activity-by-hour": defaultdict(lambda: defaultdict(int)),
})
if data["earliest"] is None:
data["earliest"] = date
data["latest"] = date
data["totals_by_protocol"][protocol_name] += 1
data["totals_by_protocol_and_host"][(protocol_name, host)] += 1
if host not in ("127.0.0.1", "::1") or True:
data["activity-by-hour"][protocol_name][date.hour] += 1
collector["logins"][user] = data
def scan_postfix_lmtp_line(date, log, collector):
""" Scan a postfix lmtp log line and extract interesting data
It is assumed that every log of postfix/lmtp indicates an email that was successfully
received by Postfix.
"""
m = re.match("([A-Z0-9]+): to=<(\S+)>, .* Saved", log)
if m:
_, user = m.groups()
if user_match(user):
# Get the user data, or create it if the user is new
data = collector["received_mail"].get(
user, {
"received_count": 0,
"earliest": None,
"latest": None,
"activity-by-hour": defaultdict(int),
})
data["received_count"] += 1
data["activity-by-hour"][date.hour] += 1
if data["earliest"] is None:
data["earliest"] = date
data["latest"] = date
collector["received_mail"][user] = data
def scan_postfix_submission_line(date, log, collector):
""" Scan a postfix submission log line and extract interesting data
Lines containing a sasl_method with the values PLAIN or LOGIN are assumed to indicate a sent
email.
"""
# Match both the 'plain' and 'login' sasl methods, since both authentication methods are
# allowed by Dovecot. Exclude trailing comma after the username when additional fields
# follow after.
m = re.match(
"([A-Z0-9]+): client=(\S+), sasl_method=(PLAIN|LOGIN), sasl_username=(\S+)(?<!,)",
log)
if m:
_, client, method, user = m.groups()
if user_match(user):
# Get the user data, or create it if the user is new
data = collector["sent_mail"].get(
user, {
"sent_count": 0,
"hosts": set(),
"earliest": None,
"latest": None,
"activity-by-hour": defaultdict(int),
})
data["sent_count"] += 1
data["hosts"].add(client)
data["activity-by-hour"][date.hour] += 1
if data["earliest"] is None:
data["earliest"] = date
data["latest"] = date
collector["sent_mail"][user] = data
# Also log this as a login.
add_login(user, date, "smtp", client, collector)
# Utility functions
def readline(filename):
""" A generator that returns the lines of a file
"""
with open(filename, errors='replace') as file:
while True:
line = file.readline()
if not line:
break
yield line
def user_match(user):
""" Check if the given user matches any of the filters """
return FILTERS is None or any(u in user for u in FILTERS)
def email_sort(email):
""" Split the given email address into a reverse order tuple, for sorting i.e (domain, name) """
return tuple(reversed(email[0].split('@')))
def valid_date(string):
""" Validate the given date string fetched from the --enddate argument """
try:
date = dateutil.parser.parse(string)
except ValueError:
raise argparse.ArgumentTypeError("Unrecognized date and/or time '%s'" %
string)
return date
# Print functions
def print_time_table(labels, data, do_print=True):
labels.insert(0, "hour")
data.insert(0, [str(h) for h in range(24)])
temp = "{:<%d} " % max(len(l) for l in labels)
lines = []
for label in labels:
lines.append(temp.format(label))
for h in range(24):
max_len = max(len(str(d[h])) for d in data)
base = "{:>%d} " % max(2, max_len)
for i, d in enumerate(data):
lines[i] += base.format(d[h])
lines.insert(0, "┬ totals by time of day:")
lines.append("" + (len(lines[-1]) - 2) * "")
if do_print:
print("\n".join(lines))
else:
return lines
def print_user_table(users,
data=None,
sub_data=None,
activity=None,
latest=None,
earliest=None,
delimit=False,
numstr=str):
str_temp = "{:<32} "
lines = []
data = data or []
col_widths = len(data) * [0]
col_left = len(data) * [False]
vert_pos = 0
do_accum = all(isinstance(n, (int, float)) for _, d in data for n in d)
data_accum = len(data) * ([0] if do_accum else [" "])
last_user = None
for row, user in enumerate(users):
if delimit:
if last_user and last_user != user:
lines.append(len(lines[-1]) * "")
last_user = user
line = "{:<32} ".format(user[:31] + "" if len(user) > 32 else user)
for col, (l, d) in enumerate(data):
if isinstance(d[row], str):
col_str = str_temp.format(d[row][:31] +
"" if len(d[row]) > 32 else d[row])
col_left[col] = True
elif isinstance(d[row], datetime.datetime):
col_str = "{:<20}".format(str(d[row]))
col_left[col] = True
else:
temp = "{:>%s}" % max(5, len(l) + 1, len(str(d[row])) + 1)
col_str = temp.format(str(d[row]))
col_widths[col] = max(col_widths[col], len(col_str))
line += col_str
if do_accum:
data_accum[col] += d[row]
try:
if None not in [latest, earliest]:
vert_pos = len(line)
e = earliest[row]
l = latest[row]
timespan = relativedelta(l, e)
if timespan.months:
temp = "{:0.1f} months"
line += temp.format(timespan.months + timespan.days / 30.0)
elif timespan.days:
temp = "{:0.1f} days"
line += temp.format(timespan.days + timespan.hours / 24.0)
elif (e.hour, e.minute) == (l.hour, l.minute):
temp = "{:%H:%M}"
line += temp.format(e)
else:
temp = "{:%H:%M} - {:%H:%M}"
line += temp.format(e, l)
except KeyError:
pass
lines.append(line.rstrip())
try:
if VERBOSE:
if sub_data is not None:
for l, d in sub_data:
if d[row]:
lines.append("")
lines.append("%s" % l)
lines.append("├─%s" % (len(l) * ""))
lines.append("")
max_len = 0
for v in list(d[row]):
lines.append("%s" % v)
max_len = max(max_len, len(v))
lines.append("" + (max_len + 1) * "")
if activity is not None:
lines.extend(
print_time_table([label for label, _ in activity],
[data[row] for _, data in activity],
do_print=False))
except KeyError:
pass
header = str_temp.format("")
for col, (l, _) in enumerate(data):
if col_left[col]:
header += l.ljust(max(5, len(l) + 1, col_widths[col]))
else:
header += l.rjust(max(5, len(l) + 1, col_widths[col]))
if None not in (latest, earliest):
header += " │ timespan "
lines.insert(0, header.rstrip())
table_width = max(len(l) for l in lines)
t_line = table_width * ""
b_line = table_width * ""
if vert_pos:
t_line = t_line[:vert_pos + 1] + "" + t_line[vert_pos + 2:]
b_line = b_line[:vert_pos +
1] + ("" if VERBOSE else "") + b_line[vert_pos + 2:]
lines.insert(1, t_line)
lines.append(b_line)
# Print totals
data_accum = [numstr(a) for a in data_accum]
footer = str_temp.format("Totals:" if do_accum else " ")
for row, (l, _) in enumerate(data):
temp = "{:>%d}" % max(5, len(l) + 1)
footer += temp.format(data_accum[row])
try:
if None not in [latest, earliest]:
max_l = max(latest)
min_e = min(earliest)
timespan = relativedelta(max_l, min_e)
if timespan.days:
temp = "{:0.2f} days"
footer += temp.format(timespan.days + timespan.hours / 24.0)
elif (min_e.hour, min_e.minute) == (max_l.hour, max_l.minute):
temp = "{:%H:%M}"
footer += temp.format(min_e)
else:
temp = "{:%H:%M} - {:%H:%M}"
footer += temp.format(min_e, max_l)
except KeyError:
pass
lines.append(footer)
print("\n".join(lines))
def print_header(msg):
print('\n' + msg)
print("" * len(msg), '\n')
if __name__ == "__main__":
try:
env_vars = utils.load_environment()
except FileNotFoundError:
env_vars = {}
parser = argparse.ArgumentParser(
description=
"Scan the mail log files for interesting data. By default, this script "
"shows today's incoming and outgoing mail statistics. This script was ("
"re)written for the Mail-in-a-box email server."
"https://github.com/mail-in-a-box/mailinabox",
add_help=False)
# Switches to determine what to parse and what to ignore
parser.add_argument("-r",
"--received",
help="Scan for received emails.",
action="store_true")
parser.add_argument("-s",
"--sent",
help="Scan for sent emails.",
action="store_true")
parser.add_argument("-l",
"--logins",
help="Scan for user logins to IMAP/POP3.",
action="store_true")
parser.add_argument("-g",
"--grey",
help="Scan for greylisted emails.",
action="store_true")
parser.add_argument("-b",
"--blocked",
help="Scan for blocked emails.",
action="store_true")
parser.add_argument(
"-t",
"--timespan",
choices=TIME_DELTAS.keys(),
default='today',
metavar='<time span>',
help="Time span to scan, going back from the end date. Possible values: "
"{}. Defaults to 'today'.".format(", ".join(list(TIME_DELTAS.keys()))))
# keep the --startdate arg for backward compatibility
parser.add_argument(
"-d",
"--enddate",
"--startdate",
action="store",
dest="enddate",
type=valid_date,
metavar='<end date>',
help="Date and time to end scanning the log file. If no date is "
"provided, scanning will end at the current date and time. "
"Alias --startdate is for compatibility.")
parser.add_argument(
"-u",
"--users",
action="store",
dest="users",
metavar='<email1,email2,email...>',
help="Comma separated list of (partial) email addresses to filter the "
"output with.")
parser.add_argument('-h',
'--help',
action='help',
help="Print this message and exit.")
parser.add_argument("-v",
"--verbose",
help="Output extra data where available.",
action="store_true")
args = parser.parse_args()
if args.enddate is not None:
END_DATE = args.enddate
if args.timespan == 'today':
args.timespan = 'day'
print("Setting end date to {}".format(END_DATE))
START_DATE = END_DATE - TIME_DELTAS[args.timespan]
VERBOSE = args.verbose
if args.received or args.sent or args.logins or args.grey or args.blocked:
SCAN_IN = args.received
if not SCAN_IN:
print("Ignoring received emails")
SCAN_OUT = args.sent
if not SCAN_OUT:
print("Ignoring sent emails")
SCAN_DOVECOT_LOGIN = args.logins
if not SCAN_DOVECOT_LOGIN:
print("Ignoring logins")
SCAN_GREY = args.grey
if SCAN_GREY:
print("Showing greylisted emails")
SCAN_BLOCKED = args.blocked
if SCAN_BLOCKED:
print("Showing blocked emails")
if args.users is not None:
FILTERS = args.users.strip().split(',')
scan_mail_log(env_vars)

File diff suppressed because it is too large Load diff

158
management/mfa.py Normal file
View file

@ -0,0 +1,158 @@
import base64
import hmac
import io
import os
import pyotp
import qrcode
from mailconfig import open_database
def get_user_id(email, c):
c.execute('SELECT id FROM users WHERE email=?', (email, ))
r = c.fetchone()
if not r:
raise ValueError("User does not exist.")
return r[0]
def get_mfa_state(email, env):
c = open_database(env)
c.execute(
'SELECT id, type, secret, mru_token, label FROM mfa WHERE user_id=?',
(get_user_id(email, c), ))
return [{
"id": r[0],
"type": r[1],
"secret": r[2],
"mru_token": r[3],
"label": r[4]
} for r in c.fetchall()]
def get_public_mfa_state(email, env):
mfa_state = get_mfa_state(email, env)
return [{
"id": s["id"],
"type": s["type"],
"label": s["label"]
} for s in mfa_state]
def get_hash_mfa_state(email, env):
mfa_state = get_mfa_state(email, env)
return [{
"id": s["id"],
"type": s["type"],
"secret": s["secret"]
} for s in mfa_state]
def enable_mfa(email, type, secret, token, label, env):
if type == "totp":
validate_totp_secret(secret)
# Sanity check with the provide current token.
totp = pyotp.TOTP(secret)
if not totp.verify(token, valid_window=1):
raise ValueError("Invalid token.")
else:
raise ValueError("Invalid MFA type.")
conn, c = open_database(env, with_connection=True)
c.execute(
'INSERT INTO mfa (user_id, type, secret, label) VALUES (?, ?, ?, ?)',
(get_user_id(email, c), type, secret, label))
conn.commit()
def set_mru_token(email, mfa_id, token, env):
conn, c = open_database(env, with_connection=True)
c.execute('UPDATE mfa SET mru_token=? WHERE user_id=? AND id=?',
(token, get_user_id(email, c), mfa_id))
conn.commit()
def disable_mfa(email, mfa_id, env):
conn, c = open_database(env, with_connection=True)
if mfa_id is None:
# Disable all MFA for a user.
c.execute('DELETE FROM mfa WHERE user_id=?', (get_user_id(email, c), ))
else:
# Disable a particular MFA mode for a user.
c.execute('DELETE FROM mfa WHERE user_id=? AND id=?',
(get_user_id(email, c), mfa_id))
conn.commit()
return c.rowcount > 0
def validate_totp_secret(secret):
if type(secret) != str or secret.strip() == "":
raise ValueError("No secret provided.")
if len(secret) != 32:
raise ValueError("Secret should be a 32 characters base32 string")
def provision_totp(email, env):
# Make a new secret.
secret = base64.b32encode(os.urandom(20)).decode('utf-8')
validate_totp_secret(secret) # sanity check
# Make a URI that we encode within a QR code.
uri = pyotp.TOTP(secret).provisioning_uri(
name=email,
issuer_name=env["PRIMARY_HOSTNAME"] + " Mail-in-a-Box Control Panel")
# Generate a QR code as a base64-encode PNG image.
qr = qrcode.make(uri)
byte_arr = io.BytesIO()
qr.save(byte_arr, format='PNG')
png_b64 = base64.b64encode(byte_arr.getvalue()).decode('utf-8')
return {"type": "totp", "secret": secret, "qr_code_base64": png_b64}
def validate_auth_mfa(email, request, env):
# Validates that a login request satisfies any MFA modes
# that have been enabled for the user's account. Returns
# a tuple (status, [hints]). status is True for a successful
# MFA login, False for a missing token. If status is False,
# hints is an array of codes that indicate what the user
# can try. Possible codes are:
# "missing-totp-token"
# "invalid-totp-token"
mfa_state = get_mfa_state(email, env)
# If no MFA modes are added, return True.
if len(mfa_state) == 0:
return (True, [])
# Try the enabled MFA modes.
hints = set()
for mfa_mode in mfa_state:
if mfa_mode["type"] == "totp":
# Check that a token is present in the X-Auth-Token header.
# If not, give a hint that one can be supplied.
token = request.headers.get('x-auth-token')
if not token:
hints.add("missing-totp-token")
continue
# Check for a replay attack.
if hmac.compare_digest(token, mfa_mode['mru_token'] or ""):
# If the token fails, skip this MFA mode.
hints.add("invalid-totp-token")
continue
# Check the token.
totp = pyotp.TOTP(mfa_mode["secret"])
if not totp.verify(token, valid_window=1):
hints.add("invalid-totp-token")
continue
# On success, record the token to prevent a replay attack.
set_mru_token(email, mfa_mode['id'], token, env)
return (True, [])
# On a failed login, indicate failure and any hints for what the user can do instead.
return (False, list(hints))

2
management/munin_start.sh Executable file
View file

@ -0,0 +1,2 @@
#!/bin/bash
mkdir -p /var/run/munin && chown munin /var/run/munin

178
management/pgp.py Executable file
View file

@ -0,0 +1,178 @@
#!/usr/local/lib/mailinabox/env/bin/python
# Tools to manipulate PGP keys
import gpg
import utils
import datetime
import shutil
import tempfile
env = utils.load_environment()
# Import daemon's keyring - usually in /home/user-data/.gnupg/
gpghome = env['GNUPGHOME']
daemon_key_fpr = env['PGPKEY']
default_context = gpg.Context(armor=True, home_dir=gpghome)
# Auxiliary function to process the key in order to be read more conveniently
def key_representation(key):
if key is None:
return None
key_rep = {
"master_fpr": key.fpr,
"revoked": key.revoked != 0,
"ids": [],
"ids_emails": {},
"subkeys": []
}
now = datetime.datetime.utcnow()
key_rep["ids"] = [id.uid for id in key.uids]
# No duplicate email addresses in this list
key_rep["ids_emails"] = list({id.email for id in key.uids})
key_rep["subkeys"] = [{
"master":
skey.fpr == key.fpr,
"sign":
skey.can_sign == 1,
"cert":
skey.can_certify == 1,
"encr":
skey.can_encrypt == 1,
"auth":
skey.can_authenticate == 1,
"fpr":
skey.fpr,
"expires":
skey.expires if skey.expires != 0 else None,
"expires_date":
datetime.datetime.utcfromtimestamp(skey.expires).date().isoformat()
if skey.expires != 0 else None,
"expires_days": (datetime.datetime.utcfromtimestamp(skey.expires) -
now).days if skey.expires != 0 else None,
"expired":
skey.expired == 1,
"algorithm":
gpg.core.pubkey_algo_name(skey.pubkey_algo),
"bits":
skey.length
} for skey in key.subkeys]
return key_rep
# Tests an import as for whether we have any sort of private key material in our import
def contains_private_keys(imports):
with tempfile.TemporaryDirectory() as tmpdir:
with gpg.Context(home_dir=tmpdir, armor=True) as tmp:
result = tmp.key_import(imports)
try:
return result.secret_read != 0
except AttributeError:
raise ValueError("Import is not a valid PGP key block!")
# Decorator: Copies the homedir of a context onto a temporary directory and returns a context operating over that tmpdir
def fork_context(f, context=default_context):
from os.path import isdir, isfile
def dirs_files_only(current_dir, files):
ignore = []
for f in files:
path = f"{current_dir}{f}"
if not isdir(path) and not isfile(path):
ignore.append(f)
return ignore
def wrapped(*args, **kwargs):
with tempfile.TemporaryDirectory() as tmpdir:
shutil.copytree(context.home_dir,
f"{tmpdir}/gnupg",
ignore=dirs_files_only)
kwargs["context"] = gpg.Context(armor=context.armor,
home_dir=f"{tmpdir}/gnupg")
kwargs["buffer"] = gpg.Data()
return f(*args, **kwargs)
return wrapped
def get_key(fingerprint, context=default_context):
try:
return context.get_key(fingerprint, secret=False)
except KeyError:
return None
except gpg.errors.GPGMEError:
return None
def get_daemon_key(context=default_context):
if daemon_key_fpr is None or daemon_key_fpr == "":
return None
return context.get_key(daemon_key_fpr, secret=True)
def get_imported_keys(context=default_context):
# All the keys in the keyring, except for the daemon's key
return list(
filter(lambda k: k.fpr != daemon_key_fpr,
context.keylist(secret=False)))
def import_key(key, context=default_context):
data = str.encode(key)
if contains_private_keys(data):
raise ValueError("Import cannot contain private keys!")
return context.key_import(data)
def export_key(fingerprint, context=default_context):
if get_key(fingerprint) is None:
return None
# Key does exist, export it!
return context.key_export(pattern=fingerprint)
def delete_key(fingerprint, context=default_context):
key = get_key(fingerprint)
if fingerprint == daemon_key_fpr:
raise ValueError("You cannot delete the daemon's key!")
elif key is None:
return None
context.op_delete_ext(
key, gpg.constants.DELETE_ALLOW_SECRET | gpg.constants.DELETE_FORCE)
return True
# Key usage
# Uses the daemon key to sign the provided message. If 'detached' is True, only the signature will be returned
def create_signature(data, detached=False, context=default_context):
signed_data, _ = context.sign(data,
mode=gpg.constants.sig.mode.DETACH if
detached else gpg.constants.sig.mode.CLEAR)
return signed_data
if __name__ == "__main__":
import sys
import utils
# Check if we should renew the key
daemon_key = get_daemon_key()
exp = daemon_key.subkeys[0].expires
now = datetime.datetime.utcnow()
days_left = (datetime.datetime.utcfromtimestamp(exp) - now).days
if days_left > 14:
sys.exit(0)
else:
utils.shell("check_output", ["management/pgp_renew.sh"])

12
management/pgp_renew.sh Executable file
View file

@ -0,0 +1,12 @@
#!/bin/bash
# Renews the daemon's PGP key, if needed.
source /etc/mailinabox.conf # load global vars
export GNUPGHOME # Dump into the environment so that gpg uses it as homedir
gpg --batch --command-fd=0 --edit-key "${PGPKEY-}" << EOF;
key 0
expire
180d
save
EOF

785
management/ssl_certificates.py Executable file
View file

@ -0,0 +1,785 @@
#!/usr/local/lib/mailinabox/env/bin/python
# Utilities for installing and selecting SSL certificates.
import os
import os.path
import re
import shutil
import subprocess
import tempfile
from utils import shell, safe_domain_name, sort_domains
import idna
# SELECTING SSL CERTIFICATES FOR USE IN WEB
def get_ssl_certificates(env):
# Scan all of the installed SSL certificates and map every domain
# that the certificates are good for to the best certificate for
# the domain.
from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey
from cryptography.x509 import Certificate
# The certificates are all stored here:
ssl_root = os.path.join(env["STORAGE_ROOT"], 'ssl')
# List all of the files in the SSL directory and one level deep.
def get_file_list():
if not os.path.exists(ssl_root):
return
for fn in os.listdir(ssl_root):
if fn == 'ssl_certificate.pem':
# This is always a symbolic link
# to the certificate to use for
# PRIMARY_HOSTNAME. Don't let it
# be eligible for use because we
# could end up creating a symlink
# to itself --- we want to find
# the cert that it should be a
# symlink to.
continue
fn = os.path.join(ssl_root, fn)
if os.path.isfile(fn):
yield fn
elif os.path.isdir(fn):
for fn1 in os.listdir(fn):
fn1 = os.path.join(fn, fn1)
if os.path.isfile(fn1):
yield fn1
# Remember stuff.
private_keys = {}
certificates = []
# Scan each of the files to find private keys and certificates.
# We must load all of the private keys first before processing
# certificates so that we can check that we have a private key
# available before using a certificate.
for fn in get_file_list():
try:
pem = load_pem(load_cert_chain(fn)[0])
except ValueError:
# Not a valid PEM format for a PEM type we care about.
continue
# Is it a private key?
if isinstance(pem, RSAPrivateKey):
private_keys[pem.public_key().public_numbers()] = { "filename": fn, "key": pem }
# Is it a certificate?
if isinstance(pem, Certificate):
certificates.append({ "filename": fn, "cert": pem })
# Process the certificates.
domains = {}
for cert in certificates:
# What domains is this certificate good for?
cert_domains, primary_domain = get_certificate_domains(cert["cert"])
cert["primary_domain"] = primary_domain
# Is there a private key file for this certificate?
private_key = private_keys.get(cert["cert"].public_key().public_numbers())
if not private_key:
continue
cert["private_key"] = private_key
# Add this cert to the list of certs usable for the domains.
for domain in cert_domains:
# The primary hostname can only use a certificate mapped
# to the system private key.
if domain == env['PRIMARY_HOSTNAME']:
if cert["private_key"]["filename"] != os.path.join(env['STORAGE_ROOT'], 'ssl', 'ssl_private_key.pem'):
continue
domains.setdefault(domain, []).append(cert)
# Sort the certificates to prefer good ones.
import datetime
now = datetime.datetime.utcnow()
ret = {}
for domain, cert_list in domains.items():
#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 : (
# must be valid NOW
cert["cert"].not_valid_before <= now <= cert["cert"].not_valid_after,
# prefer one that is not self-signed
cert["cert"].issuer != cert["cert"].subject,
###########################################################
# The above lines ensure that valid certificates are chosen
# over invalid certificates. The lines below choose between
# multiple valid certificates available for this domain.
###########################################################
# prefer one with the expiration furthest into the future so
# that we can easily rotate to new certs as we get them
cert["cert"].not_valid_after,
###########################################################
# We always choose the certificate that is good for the
# longest period of time. This is important for how we
# provision certificates for Let's Encrypt. To ensure that
# we don't re-provision every night, we have to ensure that
# if we choose to provison a certificate that it will
# *actually* be used so the provisioning logic knows it
# doesn't still need to provision a certificate for the
# domain.
###########################################################
# in case a certificate is installed in multiple paths,
# prefer the... lexicographically last one?
cert["filename"],
), reverse=True)
cert = cert_list.pop(0)
ret[domain] = {
"private-key": cert["private_key"]["filename"],
"certificate": cert["filename"],
"primary-domain": cert["primary_domain"],
"certificate_object": cert["cert"],
}
return ret
def get_domain_ssl_files(domain,
ssl_certificates,
env,
allow_missing_cert=False,
use_main_cert=True):
if use_main_cert or not allow_missing_cert:
# Get the system certificate info.
ssl_private_key = os.path.join(
os.path.join(env["STORAGE_ROOT"], 'ssl', 'ssl_private_key.pem'))
ssl_certificate = os.path.join(
os.path.join(env["STORAGE_ROOT"], 'ssl', 'ssl_certificate.pem'))
system_certificate = {
"private-key": ssl_private_key,
"certificate": ssl_certificate,
"primary-domain": env['PRIMARY_HOSTNAME'],
"certificate_object":
load_pem(load_cert_chain(ssl_certificate)[0]),
}
if use_main_cert:
if domain == env['PRIMARY_HOSTNAME']:
# The primary domain must use the server certificate because
# it is hard-coded in some service configuration files.
return system_certificate
wildcard_domain = re.sub("^[^\.]+", "*", domain)
if domain in ssl_certificates:
return ssl_certificates[domain]
elif wildcard_domain in ssl_certificates:
return ssl_certificates[wildcard_domain]
elif not allow_missing_cert:
# No valid certificate is available for this domain! Return default files.
return system_certificate
else:
# No valid certificate is available for this domain.
return None
# PROVISIONING CERTIFICATES FROM LETSENCRYPT
def get_certificates_to_provision(env,
limit_domains=None,
show_valid_certs=True):
# Get a set of domain names that we can provision certificates for
# using certbot. We start with domains that the box is serving web
# for and subtract:
# * domains not in limit_domains if limit_domains is not empty
# * domains with custom "A" records, i.e. they are hosted elsewhere
# * domains with actual "A" records that point elsewhere (misconfiguration)
# * domains that already have certificates that will be valid for a while
from web_update import get_web_domains
from status_checks import query_dns, normalize_ip
existing_certs = get_ssl_certificates(env)
plausible_web_domains = get_web_domains(env, exclude_dns_elsewhere=False)
actual_web_domains = get_web_domains(env)
domains_to_provision = set()
domains_cant_provision = {}
for domain in plausible_web_domains:
# Skip domains that the user doesn't want to provision now.
if limit_domains and domain not in limit_domains:
continue
# Check that there isn't an explicit A/AAAA record.
if domain not in actual_web_domains:
domains_cant_provision[
domain] = "The domain has a custom DNS A/AAAA record that points the domain elsewhere, so there is no point to installing a TLS certificate here and we could not automatically provision one anyway because provisioning requires access to the website (which isn't here)."
# Check that the DNS resolves to here.
else:
# Does the domain resolve to this machine in public DNS? If not,
# we can't do domain control validation. For IPv6 is configured,
# make sure both IPv4 and IPv6 are correct because we don't know
# how Let's Encrypt will connect.
bad_dns = []
for rtype, value in [("A", env["PUBLIC_IP"]),
("AAAA", env.get("PUBLIC_IPV6"))]:
if not value:
continue # IPv6 is not configured
response = query_dns(domain, rtype)
if response != normalize_ip(value):
bad_dns.append("%s (%s)" % (response, rtype))
if bad_dns:
domains_cant_provision[domain] = "The domain name does not resolve to this machine: " \
+ (", ".join(bad_dns)) \
+ "."
else:
# DNS is all good.
# Check for a good existing cert.
existing_cert = get_domain_ssl_files(domain,
existing_certs,
env,
use_main_cert=False,
allow_missing_cert=True)
if existing_cert:
existing_cert_check = check_certificate(
domain,
existing_cert['certificate'],
existing_cert['private-key'],
warn_if_expiring_soon=14)
if existing_cert_check[0] == "OK":
if show_valid_certs:
domains_cant_provision[
domain] = "The domain has a valid certificate already. ({} Certificate: {}, private key {})".format(
existing_cert_check[1],
existing_cert['certificate'],
existing_cert['private-key'])
continue
domains_to_provision.add(domain)
return (domains_to_provision, domains_cant_provision)
def provision_certificates(env, limit_domains):
# What domains should we provision certificates for? And what
# errors prevent provisioning for other domains.
domains, domains_cant_provision = get_certificates_to_provision(
env, limit_domains=limit_domains)
# Build a list of what happened on each domain or domain-set.
ret = []
for domain, error in domains_cant_provision.items():
ret.append({
"domains": [domain],
"log": [error],
"result": "skipped",
})
# Break into groups by DNS zone: Group every domain with its parent domain, if
# its parent domain is in the list of domains to request a certificate for.
# Start with the zones so that if the zone doesn't need a certificate itself,
# its children will still be grouped together. Sort the provision domains to
# put parents ahead of children.
# Since Let's Encrypt requests are limited to 100 domains at a time,
# we'll create a list of lists of domains where the inner lists have
# at most 100 items. By sorting we also get the DNS zone domain as the first
# entry in each list (unless we overflow beyond 100) which ends up as the
# primary domain listed in each certificate.
from dns_update import get_dns_zones
certs = {}
for zone, zonefile in get_dns_zones(env):
certs[zone] = [[]]
for domain in sort_domains(domains, env):
# Does the domain end with any domain we've seen so far.
for parent in certs.keys():
if domain.endswith("." + parent):
# Add this to the parent's list of domains.
# Start a new group if the list already has
# 100 items.
if len(certs[parent][-1]) == 100:
certs[parent].append([])
certs[parent][-1].append(domain)
break
else:
# This domain is not a child of any domain we've seen yet, so
# start a new group. This shouldn't happen since every zone
# was already added.
certs[domain] = [[domain]]
# Flatten to a list of lists of domains (from a mapping). Remove empty
# lists (zones with no domains that need certs).
certs = sum(certs.values(), [])
certs = [_ for _ in certs if len(_) > 0]
# Prepare to provision.
# Where should we put our Let's Encrypt account info and state cache.
account_path = os.path.join(env['STORAGE_ROOT'], 'ssl/lets_encrypt')
if not os.path.exists(account_path):
os.mkdir(account_path)
# Provision certificates.
for domain_list in certs:
ret.append({
"domains": domain_list,
"log": [],
})
try:
# Create a CSR file for our master private key so that certbot
# uses our private key.
key_file = os.path.join(env['STORAGE_ROOT'], 'ssl',
'ssl_private_key.pem')
with tempfile.NamedTemporaryFile() as csr_file:
# We could use openssl, but certbot requires
# that the CN domain and SAN domains match
# the domain list passed to certbot, and adding
# SAN domains openssl req is ridiculously complicated.
# subprocess.check_output([
# "openssl", "req", "-new",
# "-key", key_file,
# "-out", csr_file.name,
# "-subj", "/CN=" + domain_list[0],
# "-sha256" ])
from cryptography import x509
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.serialization import Encoding
from cryptography.hazmat.primitives import hashes
from cryptography.x509.oid import NameOID
builder = x509.CertificateSigningRequestBuilder()
builder = builder.subject_name(
x509.Name([
x509.NameAttribute(NameOID.COMMON_NAME, domain_list[0])
]))
builder = builder.add_extension(x509.BasicConstraints(
ca=False, path_length=None),
critical=True)
builder = builder.add_extension(x509.SubjectAlternativeName(
[x509.DNSName(d) for d in domain_list]),
critical=False)
request = builder.sign(load_pem(load_cert_chain(key_file)[0]),
hashes.SHA256(), default_backend())
with open(csr_file.name, "wb") as f:
f.write(request.public_bytes(Encoding.PEM))
# Provision, writing to a temporary file.
webroot = os.path.join(account_path, 'webroot')
os.makedirs(webroot, exist_ok=True)
with tempfile.TemporaryDirectory() as d:
cert_file = os.path.join(d, 'cert_and_chain.pem')
print("Provisioning TLS certificates for " +
", ".join(domain_list) + ".")
certbotret = subprocess.check_output(
[
"certbot",
"certonly",
# "-v", # just enough to see ACME errors
"--non-interactive", # will fail if user hasn't registered during Mail-in-a-Box setup
"--agree-tos", # Automatically agrees to Let's Encrypt TOS
"--register-unsafely-without-email", # The daemon takes care of renewals
# first will be main domain
"-d",
",".join(domain_list),
# use our private key; unfortunately this doesn't work with auto-renew so we need to save cert manually
"--csr",
csr_file.name,
# we only use the full chain
"--cert-path",
os.path.join(d, 'cert'),
# we only use the full chain
"--chain-path",
os.path.join(d, 'chain'),
"--fullchain-path",
cert_file,
"--webroot",
"--webroot-path",
webroot,
"--config-dir",
account_path,
# "--staging",
],
stderr=subprocess.STDOUT).decode("utf8")
install_cert_copy_file(cert_file, env)
ret[-1]["log"].append(certbotret)
ret[-1]["result"] = "installed"
except subprocess.CalledProcessError as e:
ret[-1]["log"].append(e.output.decode("utf8"))
ret[-1]["result"] = "error"
except Exception as e:
ret[-1]["log"].append(str(e))
ret[-1]["result"] = "error"
# Run post-install steps.
ret.extend(post_install_func(env))
# Return what happened with each certificate request.
return ret
def provision_certificates_cmdline():
import sys
from exclusiveprocess import Lock
from utils import load_environment
Lock(die=True).forever()
env = load_environment()
quiet = False
domains = []
for arg in sys.argv[1:]:
if arg == "-q":
quiet = True
else:
domains.append(arg)
# Go.
status = provision_certificates(env, limit_domains=domains)
# Show what happened.
for request in status:
if isinstance(request, str):
print(request)
else:
if quiet and request['result'] == 'skipped':
continue
print(request['result'] + ":", ", ".join(request['domains']) + ":")
for line in request["log"]:
print(line)
print()
# INSTALLING A NEW CERTIFICATE FROM THE CONTROL PANEL
def create_csr(domain, ssl_key, country_code, env):
return shell("check_output", [
"openssl", "req", "-new", "-key", ssl_key, "-sha256", "-subj",
"/C=%s/CN=%s" % (country_code, domain)
])
def install_cert(domain, ssl_cert, ssl_chain, env, raw=False):
# Write the combined cert+chain to a temporary path and validate that it is OK.
# The certificate always goes above the chain.
import tempfile
fd, fn = tempfile.mkstemp('.pem')
os.write(fd, (ssl_cert + '\n' + ssl_chain).encode("ascii"))
os.close(fd)
# Do validation on the certificate before installing it.
ssl_private_key = os.path.join(
os.path.join(env["STORAGE_ROOT"], 'ssl', 'ssl_private_key.pem'))
cert_status, cert_status_details = check_certificate(
domain, fn, ssl_private_key)
if cert_status != "OK":
if cert_status == "SELF-SIGNED":
cert_status = "This is a self-signed certificate. I can't install that."
os.unlink(fn)
if cert_status_details is not None:
cert_status += " " + cert_status_details
return cert_status
# Copy certifiate into ssl directory.
install_cert_copy_file(fn, env)
# Run post-install steps.
ret = post_install_func(env)
if raw:
return ret
return "\n".join(ret)
def install_cert_copy_file(fn, env):
# Where to put it?
# Make a unique path for the certificate.
from cryptography.hazmat.primitives import hashes
from binascii import hexlify
cert = load_pem(load_cert_chain(fn)[0])
all_domains, cn = get_certificate_domains(cert)
path = "%s-%s-%s.pem" % (
# common name, which should be filename safe because it is IDNA-encoded, but in case of a malformed cert make sure it's ok to use as a filename
safe_domain_name(cn),
cert.not_valid_after.date().isoformat().replace("-",
""), # expiration date
hexlify(cert.fingerprint(
hashes.SHA256())).decode("ascii")[0:8], # fingerprint prefix
)
ssl_certificate = os.path.join(
os.path.join(env["STORAGE_ROOT"], 'ssl', path))
# Install the certificate.
os.makedirs(os.path.dirname(ssl_certificate), exist_ok=True)
shutil.move(fn, ssl_certificate)
def post_install_func(env):
ret = []
# Get the certificate to use for PRIMARY_HOSTNAME.
ssl_certificates = get_ssl_certificates(env)
cert = get_domain_ssl_files(env['PRIMARY_HOSTNAME'],
ssl_certificates,
env,
use_main_cert=False)
if not cert:
# Ruh-row, we don't have any certificate usable
# for the primary hostname.
ret.append("there is no valid certificate for " +
env['PRIMARY_HOSTNAME'])
# Symlink the best cert for PRIMARY_HOSTNAME to the system
# certificate path, which is hard-coded for various purposes, and then
# restart postfix and dovecot.
system_ssl_certificate = os.path.join(
os.path.join(env["STORAGE_ROOT"], 'ssl', 'ssl_certificate.pem'))
if cert and os.readlink(system_ssl_certificate) != cert['certificate']:
# Update symlink.
ret.append("updating primary certificate")
ssl_certificate = cert['certificate']
os.unlink(system_ssl_certificate)
os.symlink(ssl_certificate, system_ssl_certificate)
# Restart postfix and dovecot so they pick up the new file.
shell('check_call', ["/usr/sbin/service", "postfix", "restart"])
shell('check_call', ["/usr/sbin/service", "dovecot", "restart"])
ret.append("mail services restarted")
# The DANE TLSA record will remain valid so long as the private key
# hasn't changed. We don't ever change the private key automatically.
# If the user does it, they must manually update DNS.
# Update the web configuration so nginx picks up the new certificate file.
from web_update import do_web_update
ret.append(do_web_update(env))
return ret
# VALIDATION OF CERTIFICATES
def check_certificate(domain,
ssl_certificate,
ssl_private_key,
warn_if_expiring_soon=10,
rounded_time=False,
just_check_domain=False):
# Check that the ssl_certificate & ssl_private_key files are good
# for the provided domain.
from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey
from cryptography.x509 import Certificate
# The ssl_certificate file may contain a chain of certificates. We'll
# need to split that up before we can pass anything to openssl or
# parse them in Python. Parse it with the cryptography library.
try:
ssl_cert_chain = load_cert_chain(ssl_certificate)
cert = load_pem(ssl_cert_chain[0])
if not isinstance(cert, Certificate):
raise ValueError("This is not a certificate file.")
except ValueError as e:
return ("There is a problem with the certificate file: %s" % str(e),
None)
# First check that the domain name is one of the names allowed by
# the certificate.
if domain is not None:
certificate_names, cert_primary_name = get_certificate_domains(cert)
# Check that the domain appears among the acceptable names, or a wildcard
# form of the domain name (which is a stricter check than the specs but
# should work in normal cases).
wildcard_domain = re.sub("^[^\.]+", "*", domain)
if domain not in certificate_names and wildcard_domain not in certificate_names:
return (
"The certificate is for the wrong domain name. It is for %s." %
", ".join(sorted(certificate_names)), None)
# Second, check that the certificate matches the private key.
if ssl_private_key is not None:
try:
priv_key = load_pem(open(ssl_private_key, 'rb').read())
except ValueError as e:
return ("The private key file %s is not a private key file: %s" %
(ssl_private_key, str(e)), None)
if not isinstance(priv_key, RSAPrivateKey):
return ("The private key file %s is not a private key file." %
ssl_private_key, None)
if priv_key.public_key().public_numbers() != cert.public_key(
).public_numbers():
return (
"The certificate does not correspond to the private key at %s."
% ssl_private_key, None)
# We could also use the openssl command line tool to get the modulus
# listed in each file. The output of each command below looks like "Modulus=XXXXX".
# $ openssl rsa -inform PEM -noout -modulus -in ssl_private_key
# $ openssl x509 -in ssl_certificate -noout -modulus
# Third, check if the certificate is self-signed. Return a special flag string.
if cert.issuer == cert.subject:
return ("SELF-SIGNED", None)
# When selecting which certificate to use for non-primary domains, we check if the primary
# certificate or a www-parent-domain certificate is good for the domain. There's no need
# to run extra checks beyond this point.
if just_check_domain:
return ("OK", None)
# Check that the certificate hasn't expired. The datetimes returned by the
# certificate are 'naive' and in UTC. We need to get the current time in UTC.
import datetime
now = datetime.datetime.utcnow()
if not (cert.not_valid_before <= now <= cert.not_valid_after):
return (
"The certificate has expired or is not yet valid. It is valid from %s to %s."
% (cert.not_valid_before, cert.not_valid_after), None)
# Next validate that the certificate is valid. This checks whether the certificate
# is self-signed, that the chain of trust makes sense, that it is signed by a CA
# that Ubuntu has installed on this machine's list of CAs, and I think that it hasn't
# expired.
# The certificate chain has to be passed separately and is given via STDIN.
# This command returns a non-zero exit status in most cases, so trap errors.
retcode, verifyoutput = shell(
'check_output',
[
"openssl",
"verify",
"-verbose",
"-purpose",
"sslserver",
"-policy_check",
] +
([] if len(ssl_cert_chain) == 1 else ["-untrusted", "/proc/self/fd/0"])
+ [ssl_certificate],
input=b"\n\n".join(ssl_cert_chain[1:]),
trap=True)
if "self signed" in verifyoutput:
# Certificate is self-signed. Probably we detected this above.
return ("SELF-SIGNED", None)
elif retcode != 0:
if "unable to get local issuer certificate" in verifyoutput:
return (
"The certificate is missing an intermediate chain or the intermediate chain is incorrect or incomplete. (%s)"
% verifyoutput, None)
# There is some unknown problem. Return the `openssl verify` raw output.
return ("There is a problem with the certificate.",
verifyoutput.strip())
else:
# `openssl verify` returned a zero exit status so the cert is currently
# good.
# But is it expiring soon?
cert_expiration_date = cert.not_valid_after
ndays = (cert_expiration_date - now).days
if not rounded_time or ndays <= 10:
# Yikes better renew soon!
expiry_info = "The certificate expires in %d days on %s." % (
ndays, cert_expiration_date.date().isoformat())
else:
# We'll renew it with Lets Encrypt.
expiry_info = "The certificate expires on %s." % cert_expiration_date.date(
).isoformat()
if warn_if_expiring_soon and ndays <= warn_if_expiring_soon:
# Warn on day 10 to give 4 days for us to automatically renew the
# certificate, which occurs on day 14.
return ("The certificate is expiring soon: " + expiry_info, None)
# Return the special OK code.
return ("OK", expiry_info)
def load_cert_chain(pemfile):
# A certificate .pem file may contain a chain of certificates.
# Load the file and split them apart.
re_pem = rb"(-+BEGIN (?:.+)-+[\r\n]+(?:[A-Za-z0-9+/=]{1,64}[\r\n]+)+-+END (?:.+)-+[\r\n]+)"
with open(pemfile, "rb") as f:
pem = f.read() + b"\n" # ensure trailing newline
pemblocks = re.findall(re_pem, pem)
if len(pemblocks) == 0:
raise ValueError("File does not contain valid PEM data.")
return pemblocks
def load_pem(pem):
# Parse a "---BEGIN .... END---" PEM string and return a Python object for it
# using classes from the cryptography package.
from cryptography.x509 import load_pem_x509_certificate
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.backends import default_backend
pem_type = re.match(b"-+BEGIN (.*?)-+[\r\n]", pem)
if pem_type is None:
raise ValueError("File is not a valid PEM-formatted file.")
pem_type = pem_type.group(1)
if pem_type in (b"RSA PRIVATE KEY", b"PRIVATE KEY"):
return serialization.load_pem_private_key(pem,
password=None,
backend=default_backend())
if pem_type == b"CERTIFICATE":
return load_pem_x509_certificate(pem, default_backend())
raise ValueError("Unsupported PEM object type: " +
pem_type.decode("ascii", "replace"))
def get_certificate_domains(cert):
from cryptography.x509 import DNSName, ExtensionNotFound, OID_COMMON_NAME, OID_SUBJECT_ALTERNATIVE_NAME
import idna
names = set()
cn = None
# The domain may be found in the Subject Common Name (CN). This comes back as an IDNA (ASCII)
# string, which is the format we store domains in - so good.
try:
cn = cert.subject.get_attributes_for_oid(OID_COMMON_NAME)[0].value
names.add(cn)
except IndexError:
# No common name? Certificate is probably generated incorrectly.
# But we'll let it error-out when it doesn't find the domain.
pass
# ... or be one of the Subject Alternative Names. The cryptography library handily IDNA-decodes
# the names for us. We must encode back to ASCII, but wildcard certificates can't pass through
# IDNA encoding/decoding so we must special-case. See https://github.com/pyca/cryptography/pull/2071.
def idna_decode_dns_name(dns_name):
if dns_name.startswith("*."):
return "*." + idna.encode(dns_name[2:]).decode('ascii')
else:
return idna.encode(dns_name).decode('ascii')
try:
sans = cert.extensions.get_extension_for_oid(
OID_SUBJECT_ALTERNATIVE_NAME).value.get_values_for_type(DNSName)
for san in sans:
names.add(idna_decode_dns_name(san))
except ExtensionNotFound:
pass
return names, cn
if __name__ == "__main__":
# Provision certificates.
provision_certificates_cmdline()

File diff suppressed because it is too large Load diff

View file

@ -1,159 +1,354 @@
<style>
#alias_table .actions > * { padding-right: 3px; }
#alias_table .alias-required .remove { display: none }
#alias_table .alias-auto .actions>* {
display: none
}
#addalias-form .hidden {
display: none;
}
.btn.btn-xs {
padding: .1rem .3rem;
font-size: .75rem;
border-radius: .2rem;
}
</style>
<h2>Aliases</h2>
<h3>Add a mail alias</h3>
<p>Aliases are email forwarders. An alias can forward email to a <a href="javascript: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="#" onclick="return show_panel('users')">mail
user</a> or to any email address.</p>
<form class="form-horizontal" role="form" onsubmit="do_add_alias(); return false;">
<div class="form-group">
<label for="addaliasEmail" class="col-sm-2 control-label">Email Address</label>
<div class="col-sm-10">
<input type="text" class="form-control" id="addaliasEmail" placeholder="Incoming Email Address">
</div>
</div>
<div class="form-group">
<label for="addaliasTargets" class="col-sm-2 control-label">Forward To</label>
<div class="col-sm-10">
<textarea class="form-control" rows="3" id="addaliasTargets" placeholder="Forward to these email addresses (one per line or separated by commas)"></textarea>
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
<button id="add-alias-button" type="submit" class="btn btn-primary">Add</button>
<button id="alias-cancel" class="btn btn-default hidden" onclick="aliases_reset_form(); return false;">Cancel</button>
</div>
</div>
<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>
<form id="addalias-form" class="form-horizontal" role="form" onsubmit="do_add_alias(); return false;">
<div class="mb-3">
<div id="alias_type_buttons" class="btn-group btn-group-xs">
<button type="button" class="btn btn-secondary" data-mode="regular">Regular</button>
<button type="button" class="btn btn-secondary" data-mode="catchall">Catch-All</button>
<button type="button" class="btn btn-secondary" data-mode="domainalias">Domain Alias</button>
</div>
<div id="alias_mode_info" class="text-info small" style="display: none; margin: .5em 0 0 0;">
<span class="catchall hidden">A catch-all alias captures all otherwise unmatched email to a domain.</span>
<span class="domainalias hidden">A domain alias forwards all otherwise unmatched email from one domain to
another domain, preserving the part before the @-sign.</span>
</div>
</div>
<div class="col-lg-9 mb-3">
<div class="input-group">
<label for="addaliasAddress" class="input-group-text">Alias</label>
<input type="email" class="form-control" id="addaliasAddress">
</div>
<div style="margin-top: 3px; padding-left: 3px; font-size: 90%" class="text-muted ">
<span class="catchall domainalias">Enter just the part of an email address starting with the @-sign.</span>
You may use international (non-ASCII) characters for the domain part of the email address only.
</div>
</div>
<div class="col-lg-9 mb-3">
<div class="form-floating">
<textarea class="form-control" style="min-height: 8em;" id="addaliasForwardsTo"></textarea>
<label for="addaliasForwardsTo">Forwards To</label>
</div>
<div style="margin-top: 3px; padding-left: 3px; font-size: 90%">
<span class="domainalias text-muted">Enter just the part of an email address starting with the
@-sign.</span>
<span class="text-danger">Only forward mail to addresses handled by this Mail-in-a-Box, since mail forwarded
by aliases to other domains may be rejected or filtered by the receiver. To forward mail to other
domains, create a mail user and then log into webmail for the user and create a filter rule to forward
mail.</span>
</div>
</div>
<h4>Permitted Senders</h4>
<div class="col-lg-9 mb-3">
<div class="mb-3">
<div class="mb-3">
<div class="radio">
<label>
<input id="addaliasForwardsToNotAdvanced" name="addaliasForwardsToDivToggle" type="radio"
checked onclick="$('#addaliasForwardsToDiv').toggle(false)">
Any mail user listed in the Forwards To box can send mail claiming to be from <span
class="regularalias">the alias address</span><span class="catchall domainalias">any address
on the alias domain</span>.
</label>
</div>
<div class="radio">
<label>
<input id="addaliasForwardsToAdvanced" name="addaliasForwardsToDivToggle" type="radio"
id="addaliasForwardsToDivShower" onclick="$('#addaliasForwardsToDiv').toggle(true)">
I&rsquo;ll enter the mail users that can send mail claiming to be from <span
class="regularalias">the alias address</span><span class="catchall domainalias">any address
on the alias domain</span>.
</label>
</div>
</div>
<div id="addaliasForwardsToDiv" style="display: none;">
<div class="form-floating">
<textarea class="form-control" style="min-height: 8em;" id="addaliasSenders"
placeholder="one user per line or separated by commas"></textarea>
<label for="addaliasSenders">Permitted Senders</label>
</div>
<small>One user per line or separated by commas</small>
</div>
</div>
</div>
<button id="add-alias-button" type="submit" class="btn btn-primary">Add Alias</button>
</form>
<h3>Existing mail aliases</h3>
<table id="alias_table" class="table" style="width: auto">
<thead>
<tr>
<th></th>
<th>Email Address<br></th>
<th>Forwards To</th>
</tr>
</thead>
<tbody>
</tbody>
<table id="alias_table" class="table">
<caption></caption>
<thead>
<tr>
<th></th>
<th>Alias<br></th>
<th>Forwards To</th>
<th>Permitted Senders</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
<p style="margin-top: 1.5em"><small>Hostmaster@, postmaster@, and admin@ email addresses are required on some domains.</small></p>
<p style="margin-top: 1.5em"><small>hostmaster@, postmaster@, admin@ and abuse@ email addresses are required on some
domains.</small></p>
<div style="display: none">
<table>
<tr id="alias-template">
<td class='actions'>
<a href="#" onclick="aliases_edit(this); return false;" class='edit' title="Edit Alias">
<span class="glyphicon glyphicon-pencil"></span>
</a>
<a href="#" onclick="aliases_remove(this); return false;" class='remove' title="Remove Alias">
<span class="glyphicon glyphicon-trash"></span>
</a>
</td>
<td class='email'> </td>
<td class='target'> </td>
</tr>
</table>
<table>
<tr id="alias-template">
<td class='actions row justify-content-evenly' style="min-width: 4em; border: none;">
<button class="col-5 btn btn-xs btn-default" onclick="aliases_edit(this); scroll_top(); return false;"
class='edit' title="Edit Alias">
<span class="text-center text-primary fas fa-pen"></span>
</button>
<button class="col-5 btn btn-xs btn-default" onclick="aliases_remove(this); return false;"
class='remove' title="Remove Alias">
<span class="text-center text-danger fas fa-trash"></span>
</button>
</td>
<td class='address'> </td>
<td class='forwardsTo'> </td>
<td class='senders'> </td>
</tr>
</table>
</div>
<h3>Mail aliases API (advanced)</h3>
<p>Use your box&rsquo;s mail aliases API to add and remove mail aliases from the command-line or custom services you
build.</p>
<p>Usage:</p>
<pre>curl -X <b>VERB</b> [-d "<b>parameters</b>"] --user {email}:{password} https://{{hostname}}/admin/mail/aliases[<b>action</b>]</pre>
<p>Brackets denote an optional argument. Please note that the POST body <code>parameters</code> must be URL-encoded.</p>
<p>The email and password given to the <code>--user</code> option must be an administrative user on this system.</p>
<h4 style="margin-bottom: 0">Verbs</h4>
<table class="table" style="margin-top: .5em">
<caption></caption>
<thead>
<th>Verb</th>
<th>Action</th>
<th></th>
</thead>
<tr>
<td><b>GET</b></td>
<td><i>(none)</i></td>
<td>Returns a list of existing mail aliases. Adding <code>?format=json</code> to the URL will give JSON-encoded
results.</td>
</tr>
<tr>
<td><b>POST</b></td>
<td class="font-monospace">/add</td>
<td>Adds a new mail alias. Required POST-body parameters are <code>address</code> and <code>forwards_to</code>.
</td>
</tr>
<tr>
<td><b>POST</b></td>
<td class="font-monospace">/remove</td>
<td>Removes a mail alias. Required POST-body parameter is <code>address</code>.</td>
</tr>
</table>
<h4>Examples:</h4>
<p>Try these examples. For simplicity the examples omit the <code>--user me@mydomain.com:yourpassword</code> command
line argument which you must fill in with your email address and password.</p>
<pre># Gives a JSON-encoded list of all mail aliases
curl -X GET https://{{hostname}}/admin/mail/aliases?format=json
# Adds a new alias
curl -X POST -d "address=new_alias@mydomail.com" -d "forwards_to=my_email@mydomain.com" https://{{hostname}}/admin/mail/aliases/add
# Removes an alias
curl -X POST -d "address=new_alias@mydomail.com" https://{{hostname}}/admin/mail/aliases/remove
</pre>
<script>
function show_aliases() {
$('#alias_table tbody').html("<tr><td colspan='2' class='text-muted'>Loading...</td></tr>")
api(
"/mail/aliases",
"GET",
{ format: 'json' },
function(r) {
$('#alias_table tbody').html("");
for (var i = 0; i < r.length; i++) {
var n = $("#alias-template").clone();
n.attr('id', '');
function show_aliases() {
$('#alias_table tbody').html("<tr><td colspan='2' class='text-muted'>Loading...</td></tr>")
api(
"/mail/aliases",
"GET",
{ format: 'json' },
function (r) {
$('#alias_table tbody').html("");
for (var i = 0; i < r.length; i++) {
var hdr = $("<tr><th colspan='4' class='bg-light'></th></tr>");
hdr.find('th').text(r[i].domain);
$('#alias_table tbody').append(hdr);
if (r[i].required) n.addClass('alias-required');
n.attr('data-email', r[i].source);
n.find('td.email').text(r[i].source)
for (var j = 0; j < r[i].destination.length; j++)
n.find('td.target').append($("<div></div>").text(r[i].destination[j]))
$('#alias_table tbody').append(n);
}
})
}
for (var k = 0; k < r[i].aliases.length; k++) {
var alias = r[i].aliases[k];
var is_alias_add_update = false;
function do_add_alias() {
var title = (!is_alias_add_update) ? "Add Alias" : "Update Alias";
var email = $("#addaliasEmail").val();
var targets = $("#addaliasTargets").val();
api(
"/mail/aliases/add",
"POST",
{
update_if_exists: is_alias_add_update ? '1' : '0',
source: email,
destination: targets
},
function(r) {
// Responses are multiple lines of pre-formatted text.
show_modal_error(title, $("<pre/>").text(r));
show_aliases()
aliases_reset_form();
},
function(r) {
show_modal_error(title, r);
});
return false;
}
var n = $("#alias-template").clone();
n.attr('id', '');
function aliases_reset_form() {
$("#addaliasEmail").prop('disabled', false);
$("#addaliasEmail").val('')
$("#addaliasTargets").val('')
$('#alias-cancel').addClass('hidden');
$('#add-alias-button').text('Add');
is_alias_add_update = false;
}
if (alias.auto) n.addClass('alias-auto');
n.attr('data-address', alias.address_display); // this is decoded from IDNA, but will get re-coded to IDNA on the backend
n.find('td.address').text(alias.address_display)
for (var j = 0; j < alias.forwards_to.length; j++)
n.find('td.forwardsTo').append($("<div></div>").text(alias.forwards_to[j]))
for (var j = 0; j < (alias.permitted_senders ? alias.permitted_senders.length : 0); j++)
n.find('td.senders').append($("<div></div>").text(alias.permitted_senders[j]))
$('#alias_table tbody').append(n);
}
}
})
function aliases_edit(elem) {
var email = $(elem).parents('tr').attr('data-email');
var targetdivs = $(elem).parents('tr').find('.target div');
var targets = "";
for (var i = 0; i < targetdivs.length; i++)
targets += $(targetdivs[i]).text() + "\n";
$('#alias_type_buttons button').off('click').click(function () {
$('#alias_type_buttons button').removeClass('active');
$(this).addClass('active');
$('#addalias-form .regularalias, #addalias-form .catchall, #addalias-form .domainalias').addClass('hidden');
if ($(this).attr('data-mode') == "regular") {
$('#addaliasAddress').attr('type', 'email');
$('#addaliasAddress').attr('placeholder', 'you@example.com (incoming email address)');
$('#addaliasForwardsTo').attr('placeholder', 'one address per line or separated by commas');
$('#alias_mode_info').slideUp();
$('#addalias-form .regularalias').removeClass('hidden');
} else if ($(this).attr('data-mode') == "catchall") {
$('#addaliasAddress').attr('type', 'text');
$('#addaliasAddress').attr('placeholder', '@example.com (incoming catch-all domain)');
$('#addaliasForwardsTo').attr('placeholder', 'one address per line or separated by commas');
$('#alias_mode_info').slideDown();
$('#addalias-form .catchall').removeClass('hidden');
} else if ($(this).attr('data-mode') == "domainalias") {
$('#addaliasAddress').attr('type', 'text');
$('#addaliasAddress').attr('placeholder', '@example.com (incoming catch-all domain)');
$('#addaliasForwardsTo').attr('placeholder', '@example.net (forward to other domain)');
$('#alias_mode_info').slideDown();
$('#addalias-form .domainalias').removeClass('hidden');
}
})
$('#alias_type_buttons button[data-mode="regular"]').click(); // init
}
is_alias_add_update = true;
$('#alias-cancel').removeClass('hidden');
$("#addaliasEmail").prop('disabled', true);
$("#addaliasEmail").val(email);
$("#addaliasTargets").val(targets);
$('#add-alias-button').text('Update');
$('body').animate({ scrollTop: 0 })
}
var is_alias_add_update = false;
function do_add_alias() {
var title = (!is_alias_add_update) ? "Add Alias" : "Update Alias";
var form_address = $("#addaliasAddress").val();
var form_forwardsto = $("#addaliasForwardsTo").val();
var form_senders = ($('#addaliasForwardsToAdvanced').prop('checked') ? $("#addaliasSenders").val() : '');
if ($('#addaliasForwardsToAdvanced').prop('checked') && !/\S/.exec($("#addaliasSenders").val())) {
show_modal_error(title, "You did not enter any permitted senders.");
return false;
}
api(
"/mail/aliases/add",
"POST",
{
update_if_exists: is_alias_add_update ? '1' : '0',
address: form_address,
forwards_to: form_forwardsto,
permitted_senders: form_senders
},
function (r) {
// Responses are multiple lines of pre-formatted text.
show_modal_error(title, $("<pre/>").text(r));
show_aliases()
aliases_reset_form();
},
function (r) {
show_modal_error(title, r);
});
return false;
}
function aliases_remove(elem) {
var email = $(elem).parents('tr').attr('data-email');
show_modal_confirm(
"Remove Alias",
"Remove " + email + "?",
"Remove",
function() {
api(
"/mail/aliases/remove",
"POST",
{
source: email
},
function(r) {
// Responses are multiple lines of pre-formatted text.
show_modal_error("Remove User", $("<pre/>").text(r));
show_aliases();
});
});
}
function aliases_reset_form() {
$("#addaliasAddress").prop('disabled', false);
$("#addaliasAddress").val('')
$("#addaliasForwardsTo").val('')
$("#addaliasSenders").val('')
$('#alias-cancel').addClass('hidden');
$('#add-alias-button').text('Add Alias');
is_alias_add_update = false;
}
function aliases_edit(elem) {
var address = $(elem).parents('tr').attr('data-address');
var receiverdivs = $(elem).parents('tr').find('.forwardsTo div');
var senderdivs = $(elem).parents('tr').find('.senders div');
var forwardsTo = "";
for (var i = 0; i < receiverdivs.length; i++)
forwardsTo += $(receiverdivs[i]).text() + "\n";
var senders = "";
for (var i = 0; i < senderdivs.length; i++)
senders += $(senderdivs[i]).text() + "\n";
if (address.charAt(0) == '@' && forwardsTo.charAt(0) == '@')
$('#alias_type_buttons button[data-mode="domainalias"]').click();
else if (address.charAt(0) == '@')
$('#alias_type_buttons button[data-mode="catchall"]').click();
else
$('#alias_type_buttons button[data-mode="regular"]').click();
$('#alias-cancel').removeClass('hidden');
$("#addaliasAddress").prop('disabled', true);
$("#addaliasAddress").val(address);
$("#addaliasForwardsTo").val(forwardsTo);
$('#addaliasForwardsToAdvanced').prop('checked', senders != "");
$('#addaliasForwardsToNotAdvanced').prop('checked', senders == "");
$("#addaliasSenders").val(senders);
$('#add-alias-button').text('Update');
$('body').animate({ scrollTop: 0 })
is_alias_add_update = true;
}
function aliases_remove(elem) {
var row_address = $(elem).parents('tr').attr('data-address');
show_modal_confirm(
"Remove Alias",
"Remove " + row_address + "?",
"Remove",
function () {
api(
"/mail/aliases/remove",
"POST",
{
address: row_address
},
function (r) {
// Responses are multiple lines of pre-formatted text.
show_modal_error("Remove Alias", $("<pre/>").text(r));
show_aliases();
});
});
}
function scroll_top() {
$('html, body').animate({
scrollTop: $("#panel_aliases").offset().top
}, 1000);
}
</script>

View file

@ -0,0 +1,374 @@
<style>
#custom-dns-current td.long {
word-break: break-all;
}
</style>
<h2>Custom DNS</h2>
<p class="text-warning">This is an advanced configuration page.</p>
<p>It is possible to set custom DNS records on domains hosted here.</p>
<h3>Set custom DNS records</h3>
<p>You can set additional DNS records, such as if you have a website running on another server, to add DKIM records for
external mail providers, or for various confirmation-of-ownership tests.</p>
<form class="form-horizontal" role="form" onsubmit="do_set_custom_dns(); return false;">
<div class="col-lg-10 col-xl-7 mb-3">
<div class="input-group">
<label class="input-group-text">Name</label>
<input type="text" class="form-control" id="customdnsQname" placeholder="subdomain">
<label class="input-group-text">.</label>
<select id="customdnsZone" class="form-select"></select>
</div>
<small>Leave the left field blank to set a record on the chosen domain name, or enter a subdomain.</small>
</div>
<div class="col-lg-10 col-xl-7 mb-3">
<div class="input-group">
<label for="customdnsType" class="input-group-text">Type</label>
<select id="customdnsType" class="form-select" onchange="show_customdns_rtype_hint()">
<option value="A"
data-hint="Enter an IPv4 address (i.e. a dotted quad, such as 123.456.789.012). The 'local' alias sets the record to this box's public IPv4 address.">
A (IPv4 address)</option>
<option value="AAAA"
data-hint="Enter an IPv6 address. The 'local' alias sets the record to this box's public IPv6 address.">
AAAA (IPv6 address)</option>
<option value="CAA"
data-hint="Enter a CA that can issue certificates for this domain in the form of FLAG TAG VALUE. (0 issuewild &quot;letsencrypt.org&quot;)">
CAA (Certificate Authority Authorization)</option>
<option value="CNAME"
data-hint="Enter another domain name followed by a period at the end (e.g. mypage.github.io.).">
CNAME (DNS forwarding)</option>
<option value="TXT" data-hint="Enter arbitrary text.">TXT (text record)</option>
<option value="MX"
data-hint="Enter record in the form of PRIORITY DOMAIN., including trailing period (e.g. 20 mx.example.com.).">
MX (mail exchanger)</option>
<option value="SRV"
data-hint="Enter record in the form of PRIORITY WEIGHT PORT TARGET., including trailing period (e.g. 10 10 5060 sip.example.com.).">
SRV (service record)</option>
<option value="SSHFP" data-hint="Enter record in the form of ALGORITHM TYPE FINGERPRINT.">SSHFP (SSH
fingerprint record)</option>
<option value="NS" data-hint="Enter a hostname to which this subdomain should be delegated to">NS (DNS
subdomain delegation)</option>
</select>
</div>
</div>
<div class="col-lg-10 col-xl-7 mb-3">
<div class="input-group">
<label for="customdnsValue" class="input-group-text">Value</label>
<input type="text" class="form-control" id="customdnsValue" placeholder="">
<label for="customdnsTtl" class="input-group-text">TTL</label>
<input type="number" class="form-control" id="customdnsTtl" placeholder="default">
</div>
<small id="customdnsTypeHint" class="text-info" style="margin-top: .5em"></small>
</div>
<button type="submit" class="btn btn-primary mb-3">Set Record</button>
</form>
<div style="font-size: 90%;">
<b>Sort by:</b>
<a href="#"
onclick="window.miab_custom_dns_data_sort_order='qname'; show_current_custom_dns_update_after_sort(); return false;">Domain
name</a>
<b>|</b>
<a href="#"
onclick="window.miab_custom_dns_data_sort_order='created'; show_current_custom_dns_update_after_sort(); return false;">Created</a>
</div>
<table id="custom-dns-current" class="table col-12" style="display: none; margin-top: 0;">
<caption></caption>
<thead>
<th>Domain Name</th>
<th>Record Type</th>
<th>Value</th>
<th>TTL</th>
<th></th>
</thead>
<tbody>
<tr>
<td colspan="5">Loading...</td>
</tr>
</tbody>
</table>
<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 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;">
<div class="col-12 form-floating mb-3">
<textarea type="text" class="form-control font-monospace" id="secondarydnsHostname"
placeholder="ns1.example.com"></textarea>
<label for="secondarydnsHostname">Secondary Nameservers</label>
<div>
<p class="small">
Multiple secondary servers can be separated with commas or spaces (i.e., <span
class="font-monospace">ns2.hostingcompany.com ns3.hostingcompany.com</span>).
To enable zone transfers to additional servers without listing them as secondary nameservers, add an IP
address or subnet using <span class="font-monospace">xfr:10.20.30.40</span> or <span
class="font-monospace">xfr:10.0.0.0/8</span>.
</p>
<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.
</p>
</div>
</div>
<button type="submit" class="btn btn-primary">Update</button>
</form>
<h3>Custom DNS API</h3>
<p>Use your box&rsquo;s DNS API to set custom DNS records on domains hosted here. For instance, you can create your own
dynamic DNS service.</p>
<p>Usage:</p>
<pre>curl -X <b>VERB</b> [-d "<b>value</b>"] --user {email}:{password} https://{{hostname}}/admin/dns/custom[/<b>qname</b>[/<b>rtype</b>]]</pre>
<p>(Brackets denote an optional argument.)</p>
<h4>Verbs</h4>
<table class="table">
<caption></caption>
<thead>
<th>Verb</th>
<th>Usage</th>
</thead>
<tr>
<td class="font-monospace"><b>GET</b></td>
<td>Returns matching custom DNS records as a JSON array of objects. Each object has the keys <code>qname</code>,
<code>rtype</code>, and <code>value</code>. The optional <code>qname</code> and <code>rtype</code>
parameters in the request URL filter the records returned in the response. The request body
(<code>-d "..."</code>) must be omitted.</td>
</tr>
<tr>
<td class="font-monospace"><b>PUT</b></td>
<td>Sets a custom DNS record replacing any existing records with the same <code>qname</code> and
<code>rtype</code>. Use PUT (instead of POST) when you only have one value for a <code>qname</code> and
<code>rtype</code>, such as typical <code>A</code> records (without round-robin).</td>
</tr>
<tr>
<td class="font-monospace"><b>POST</b></td>
<td>Adds a new custom DNS record. Use POST when you have multiple <code>TXT</code> records or round-robin
<code>A</code> records. (PUT would delete previously added records.)</td>
</tr>
<tr>
<td class="font-monospace"><b>DELETE</b></td>
<td>Deletes custom DNS records. If the request body (<code>-d "..."</code>) is empty or omitted, deletes all
records matching the <code>qname</code> and <code>rtype</code>. If the request body is present, deletes only
the record matching the <code>qname</code>, <code>rtype</code> and value.</td>
</tr>
</table>
<h4>Parameters</h4>
<table class="table">
<caption></caption>
<thead>
<th>Parameter</th>
<th>Value</th>
</thead>
<tr>
<td class="font-monospace"><b>email</b></td>
<td>The email address of any administrative user here.</td>
</tr>
<tr>
<td class="font-monospace"><b>password</b></td>
<td>That user&rsquo;s password.</td>
</tr>
<tr>
<td class="font-monospace"><b>qname</b></td>
<td>The fully qualified domain name for the record you are trying to set. It must be one of the domain names or
a subdomain of one of the domain names hosted on this box. (Add mail users or aliases to add new domains.)
</td>
</tr>
<tr>
<td class="font-monospace"><b>rtype</b></td>
<td>The resource type. Defaults to <code>A</code> if omitted. Possible values: <code>A</code> (an IPv4 address),
<code>AAAA</code> (an IPv6 address), <code>TXT</code> (a text string), <code>CNAME</code> (an alias, which
is a fully qualified domain name &mdash; don&rsquo;t forget the final period), <code>MX</code>,
<code>SRV</code>, <code>SSHFP</code>, <code>CAA</code> or <code>NS</code>.</td>
</tr>
<tr>
<td class="font-monospace"><b>value</b></td>
<td>For PUT, POST, and DELETE, the record&rsquo;s value. If the <code>rtype</code> is <code>A</code> or
<code>AAAA</code> and <code>value</code> is empty or omitted, the IPv4 or IPv6 address of the remote host is
used (be sure to use the <code>-4</code> or <code>-6</code> options to curl). This is handy for dynamic DNS!
</td>
</tr>
</table>
<p>Strict <a href="http://tools.ietf.org/html/rfc4408">SPF</a> and <a
href="https://datatracker.ietf.org/doc/draft-kucherawy-dmarc-base/?include_text=1">DMARC</a> records will be
added to all custom domains unless you override them.</p>
<h4>Examples:</h4>
<p>Try these examples. For simplicity the examples omit the <code>--user me@mydomain.com:yourpassword</code> command
line argument which you must fill in with your email address and password.</p>
<pre># sets laptop.mydomain.com to point to the IP address of the machine you are executing curl on
curl -X PUT https://{{hostname}}/admin/dns/custom/laptop.mydomain.com
# deletes that record and all A records for that domain name
curl -X DELETE https://{{hostname}}/admin/dns/custom/laptop.mydomain.com
# sets a CNAME alias
curl -X PUT -d "bar.mydomain.com." https://{{hostname}}/admin/dns/custom/foo.mydomain.com/cname
# deletes that CNAME and all CNAME records for that domain name
curl -X DELETE https://{{hostname}}/admin/dns/custom/foo.mydomain.com/cname
# adds a TXT record using POST to preserve any previous TXT records
curl -X POST -d "some text here" https://{{hostname}}/admin/dns/custom/foo.mydomain.com/txt
# deletes that one TXT record while preserving other TXT records
curl -X DELETE -d "some text here" https://{{hostname}}/admin/dns/custom/foo.mydomain.com/txt
</pre>
<script>
function show_custom_dns() {
api(
"/dns/secondary-nameserver",
"GET",
{},
function (data) {
$('#secondarydnsHostname').val(data.hostnames.join(' '));
$('#secondarydns-clear-instructions').toggle(data.hostnames.length > 0);
});
api(
"/dns/zones",
"GET",
{},
function (data) {
$('#customdnsZone').text('');
for (var i = 0; i < data.length; i++) {
$('#customdnsZone').append($('<option/>').text(data[i]));
}
});
show_current_custom_dns();
show_customdns_rtype_hint();
}
function show_current_custom_dns() {
api(
"/dns/custom",
"GET",
{},
function (data) {
if (data.length > 0)
$('#custom-dns-current').fadeIn();
else
$('#custom-dns-current').fadeOut();
window.miab_custom_dns_data = data;
show_current_custom_dns_update_after_sort();
});
}
function show_current_custom_dns_update_after_sort() {
var data = window.miab_custom_dns_data;
var sort_key = window.miab_custom_dns_data_sort_order || "qname";
data.sort(function (a, b) { return a["sort-order"][sort_key] - b["sort-order"][sort_key] });
var tbody = $('#custom-dns-current').find("tbody");
tbody.text('');
var last_zone = null;
for (var i = 0; i < data.length; i++) {
if (sort_key == "qname" && data[i].zone != last_zone) {
var r = $("<tr><th colspan=5 class='bg-light'></th></tr>");
r.find("th").text(data[i].zone);
tbody.append(r);
last_zone = data[i].zone;
}
var tr = $("<tr/>");
tbody.append(tr);
tr.attr('data-qname', data[i].qname);
tr.attr('data-rtype', data[i].rtype);
tr.attr('data-value', data[i].value);
tr.append($('<td class="long"/>').text(data[i].qname));
tr.append($('<td/>').text(data[i].rtype));
tr.append($('<td class="long font-monospace" style="max-width: 40em"/>').text(data[i].value));
if (data[i].ttl) {
tr.append($('<td/>').text(data[i].ttl));
} else {
tr.append($('<td/>').html('<i class="">default</i>'));
}
tr.append($('<td class="col-1">[<a href="#" onclick="return delete_custom_dns_record(this)">delete</a>]</td>'));
}
}
function delete_custom_dns_record(elem) {
var qname = $(elem).parents('tr').attr('data-qname');
var rtype = $(elem).parents('tr').attr('data-rtype');
var value = $(elem).parents('tr').attr('data-value');
do_set_custom_dns(qname, rtype, value, "DELETE");
return false;
}
function do_set_secondary_dns() {
api(
"/dns/secondary-nameserver",
"POST",
{
hostnames: $('#secondarydnsHostname').val()
},
function (data) {
if (data == "") return; // nothing updated
show_modal_error("Secondary DNS", $("<pre/>").text(data));
$('#secondarydns-clear-instructions').slideDown();
},
function (err) {
show_modal_error("Secondary DNS", $("<pre/>").text(err));
});
}
function do_set_custom_dns(qname, rtype, value, method) {
if (!qname) {
if ($('#customdnsQname').val() != '')
qname = $('#customdnsQname').val() + '.' + $('#customdnsZone').val();
else
qname = $('#customdnsZone').val();
rtype = $('#customdnsType').val();
value = {
value: $('#customdnsValue').val(),
ttl: $('#customdnsTtl').val()
};
method = 'POST';
} else {
value = {
value: value
}
}
api(
"/dns/custom/" + qname + "/" + rtype,
method,
value,
function (data) {
if (data == "") return; // nothing updated
show_modal_error("Custom DNS", $("<pre/>").text(data));
show_current_custom_dns();
},
function (err) {
show_modal_error("Custom DNS (Error)", $("<pre/>").text(err));
});
}
function show_customdns_rtype_hint() {
$('#customdnsTypeHint').text($("#customdnsType").find('option:selected').attr('data-hint'));
}
</script>

View file

@ -0,0 +1,131 @@
<style>
#external_dns_settings .domain-header {
margin-top: 0.5em;
margin-bottom: 0.5em;
}
#external_dns_settings .values td {
border: 0;
padding-top: .75em;
padding-bottom: 0;
}
#external_dns_settings .value {
word-break: break-all;
}
#external_dns_settings .explanation td {
padding-top: .5em;
padding-bottom: .75em;
font-style: italic;
color: #777;
}
</style>
<h2>External DNS</h2>
<p class="text-warning">This is an advanced configuration page.</p>
<p>Although your box is configured to serve its own DNS, it is possible to host your DNS elsewhere &mdash; such as in
the DNS control panel provided by your domain name registrar or virtual cloud provider &mdash; by copying the DNS
zone information shown in the table below into your external DNS server&rsquo;s control panel.</p>
<p>If you do so, you are responsible for keeping your DNS entries up to date! If you previously enabled DNSSEC on your
domain name by setting a DS record at your registrar, you will likely have to turn it off before changing
nameservers.</p>
<p class="alert" role="alert">
<span class="fas fa-info-circle"></span>
You may encounter zone file errors when attempting to create a TXT record with a long string.
<a href="https://tools.ietf.org/html/rfc4408#section-3.1.3">RFC 4408</a> states a TXT record is allowed to contain
multiple strings, and this technique can be used to construct records that would exceed the 255-byte maximum length.
You may need to adopt this technique when adding DomainKeys. Use a tool like <code>named-checkzone</code> to
validate your zone file.
</p>
<h3>Download zonefile</h3>
<p>You can download your zonefiles here or use the table of records below.</p>
<form class="form-inline" role="form" onsubmit="do_download_zonefile(); return false;">
<div class="col-md-10 col-xl-6 mb-3">
<div class="input-group">
<label for="downloadZonefile" class="input-group-text">Zone</label>
<select id="downloadZonefile" class="form-select"></select>
</div>
</div>
<button type="submit" class="btn btn-primary">Download</button>
</form>
<h3>Records</h3>
<table id="external_dns_settings" class="table">
<caption></caption>
<thead>
<tr>
<th>QName</th>
<th>Type</th>
<th>Value</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
<script>
function show_external_dns() {
api(
"/dns/zones",
"GET",
{},
function (data) {
var zones = $('#downloadZonefile');
zones.text('');
for (var j = 0; j < data.length; j++) {
zones.append($('<option/>').text(data[j]));
}
});
$('#external_dns_settings tbody').html("<tr><td colspan='2' class='text-muted'>Loading...</td></tr>")
api(
"/dns/dump",
"GET",
{},
function (zones) {
$('#external_dns_settings tbody').html("");
for (var j = 0; j < zones.length; j++) {
var h = $("<tr><td colspan='3' class='bg-light'><h4 class='domain-header'/></td></tr>");
h.find("td h4").text(zones[j][0]);
$('#external_dns_settings tbody').append(h);
var r = zones[j][1];
for (var i = 0; i < r.length; i++) {
var n = $("<tr class='values'><td class='qname'/><td class='rtype'/><td class='value font-monospace'/></tr>");
n.find('.qname').text(r[i].qname);
n.find('.rtype').text(r[i].rtype);
n.find('.value').text(r[i].value);
$('#external_dns_settings tbody').append(n);
var n = $("<tr class='explanation'><td colspan='3'/><small/></tr>");
n.find('td small').text(r[i].explanation);
$('#external_dns_settings tbody').append(n);
}
}
})
}
function do_download_zonefile() {
var zone = $('#downloadZonefile').val();
api(
"/dns/zonefile/" + zone,
"GET",
{},
function (data) {
show_modal_error("Download Zonefile", $("<pre/>").text(data));
},
function (err) {
show_modal_error("Download Zonefile (Error)", $("<pre/>").text(err));
});
}
</script>

View file

@ -1,359 +1,630 @@
<!DOCTYPE html>
<!--[if lt IE 7]> <html class="no-js lt-ie9 lt-ie8 lt-ie7"> <![endif]-->
<!--[if IE 7]> <html class="no-js lt-ie9 lt-ie8"> <![endif]-->
<!--[if IE 8]> <html class="no-js lt-ie9"> <![endif]-->
<!--[if gt IE 8]><!--> <html class="no-js"> <!--<![endif]-->
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="viewport" content="width=device-width">
<html lang="en">
<title>{{hostname}} - Mail-in-a-Box Control Panel</title>
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="robots" content="noindex, nofollow">
<title>{{hostname}} - Mail-in-a-Box Control Panel</title>
<link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css">
<style>
@import url(https://fonts.googleapis.com/css?family=Raleway:400,700);
@import url(https://fonts.googleapis.com/css?family=Ubuntu:300);
<meta name="robots" content="noindex, nofollow">
html {
overflow-y: scroll;
}
<link rel="stylesheet" href="/admin/assets/bootstrap/css/bootstrap.min.css">
<link rel="stylesheet" href="/admin/assets/fontawesome/css/fontawesome.min.css">
<link rel="stylesheet" href="/admin/assets/fontawesome/css/solid.min.css">
body {
padding-top: 50px;
padding-bottom: 20px;
}
<style>
body {
overflow-y: scroll;
padding-bottom: 20px;
}
p {
margin-bottom: 1.25em;
}
p {
margin-bottom: 1.25em;
}
h1, h2, h3, h4 {
font-family: Raleway, sans-serif;
font-weight: bold;
}
h1,
h2,
h3,
h4 {
font-family: sans-serif;
font-weight: bold;
}
h2 {
margin: 1em 0;
}
h2 {
margin: 1em 0;
}
h3 {
font-size: 130%;
border-bottom: 1px solid black;
padding-bottom: 3px;
margin-bottom: 13px;
margin-top: 30px;
}
h3 {
font-size: 130%;
border-bottom: 1px solid black;
padding-bottom: 3px;
margin-bottom: 13px;
margin-top: 30px;
}
h4 {
font-size: 110%;
margin-bottom: 13px;
margin-top: 18px;
}
.panel-heading h3 {
border: none;
padding: 0;
margin: 0;
}
.panel {
display: none;
}
h4 {
font-size: 110%;
margin-bottom: 13px;
margin-top: 18px;
}
table.table {
margin: 1.5em 0;
}
</style>
<link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap-theme.min.css">
<style>
</style>
</head>
<body>
<!--[if lt IE 7]>
<p class="chromeframe">You are using an <strong>outdated</strong> browser. Please <a href="http://browsehappy.com/">upgrade your browser</a> or <a href="http://www.google.com/chromeframe/?redirect=true">activate Google Chrome Frame</a> to improve your experience.</p>
<![endif]-->
<div class="navbar navbar-inverse navbar-fixed-top">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="#">{{hostname}}</a>
</div>
<div class="navbar-collapse collapse">
<ul class="nav navbar-nav">
<li class="dropdown active">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">System <b class="caret"></b></a>
<ul class="dropdown-menu">
<li><a href="#system_status" onclick="return show_panel(this);">Status Checks</a></li>
<li><a href="#system_backup" onclick="return show_panel(this);">Backup Status</a></li>
<li class="divider"></li>
<li class="dropdown-header">Super Advanced Options</li>
<li><a href="#system_dns" onclick="return show_panel(this);">DNS (Custom/External)</a></li>
</ul>
</li>
<li class="dropdown active">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">Mail/Sync <b class="caret"></b></a>
<ul class="dropdown-menu">
<li><a href="#mail-guide" onclick="return show_panel(this);">Mail Instructions</a></li>
<li><a href="#sync_guide" onclick="return show_panel(this);">Contacts/Calendar</a></li>
<li class="divider"></li>
<li><a href="#users" onclick="return show_panel(this);">Users</a></li>
<li><a href="#aliases" onclick="return show_panel(this);">Aliases</a></li>
</ul>
</li>
</ul>
<ul class="nav navbar-nav navbar-right">
<li><a href="#" onclick="do_logout(); return false;" style="color: white">Log out?</a></li>
</ul>
</div><!--/.navbar-collapse -->
</div>
</div>
h4:first-child {
margin-top: 6px;
}
<div class="container-fluid">
<div id="panel_system_status" class="container panel">
{% include "system-status.html" %}
</div>
.admin_panel {
display: none;
}
<div id="panel_system_backup" class="container panel">
{% include "system-backup.html" %}
</div>
table.table {
margin: 1.5em 0;
}
<div id="panel_system_dns" class="container panel">
{% include "system-dns.html" %}
</div>
ol li {
margin-bottom: 1em;
}
<div id="panel_login" class="panel">
{% include "login.html" %}
</div>
.if-logged-in {
display: none;
}
<div id="panel_mail-guide" class="container panel">
{% include "mail-guide.html" %}
</div>
.if-logged-in-admin {
display: none;
}
</style>
</head>
<div id="panel_users" class="container panel">
{% include "users.html" %}
</div>
<body class="">
<div id="panel_aliases" class="container panel">
{% include "aliases.html" %}
</div>
<div class="navbar navbar-expand-lg navbar-light" role="navigation">
<div class="container bg-light pt-2 pb-2">
<div class="if-logged-in">
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarContent"
aria-controls="#navbarContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
</div>
<div class="navbar-brand ms-2">
<span class="fas fa-envelope"></span>
<b>{{hostname}}</b>
</div>
<div class="ms-2">
<span class="me-1 fas fa-sun"></span>
<span class="form-switch">
<input class="form-check-input" type="checkbox" role="switch" id="toggle-theme">
<label class="ms-1 fas fa-moon"></label>
</span>
</div>
<div class="collapse navbar-collapse" id="navbarContent">
<ul class="navbar-nav ms-auto">
<li class="nav-item me-1 me-xl-4 dropdown if-logged-in-admin">
<button class="btn dropdown-toggle" type="button" data-bs-toggle="dropdown"
aria-expanded="false">System</button>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="#system_status" onclick="return show_panel(this);">Status
Checks</a></li>
<li><a class="dropdown-item" href="#tls" onclick="return show_panel(this);">TLS (SSL)
Certificates</a></li>
<li><a class="dropdown-item" href="#system_backup" onclick="return show_panel(this);">Backup
Status</a></li>
<li><a class="dropdown-item" href="#smtp_relays" onclick="return show_panel(this);">SMTP
Relays</a></li>
</ul>
</li>
<li class="nav-item me-1 me-xl-4 dropdown if-logged-in-admin">
<button class="btn dropdown-toggle" type="button" data-bs-toggle="dropdown"
aria-expanded="false">Advanced</button>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="#custom_dns" onclick="return show_panel(this);">Custom
DNS</a></li>
<li><a class="dropdown-item" href="#external_dns"
onclick="return show_panel(this);">External DNS</a></li>
<li><a class="dropdown-item" href="#pgp_keyring" onclick="return show_panel(this);">PGP
Keyring Management</a></li>
<li><a class="dropdown-item" href="#wkd" onclick="return show_panel(this);">WKD
Management</a></li>
<li><a class="dropdown-item" href="#munin" onclick="return show_panel(this);">Munin
Monitoring</a></li>
</ul>
</li>
<li class="nav-item me-1 me-xl-4 dropdown if-logged-in-not-admin">
<button class="btn dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">Your Account</button>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="#manage-password" onclick="return show_panel(this);">Manage Password</a></li>
<li><a class="dropdown-item" href="#mfa" onclick="return show_panel(this);">Two-Factor Authentication</a></li>
</ul>
</li>
<li class="nav-item me-1 me-xl-4 btn if-logged-in-not-admin" type="button" href="#mail-guide"
onclick="return show_panel(this);">
Mail Guide
</li>
<li class="nav-item me-1 me-xl-4 dropdown if-logged-in-admin">
<button class="btn dropdown-toggle" type="button" data-bs-toggle="dropdown"
aria-expanded="false">Mail</button>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="#mail-guide" onclick="return show_panel(this);">Mail
Guide</a></li>
<li><a class="dropdown-item" href="#users" onclick="return show_panel(this);">Users</a></li>
<li><a class="dropdown-item" href="#aliases" onclick="return show_panel(this);">Aliases</a>
</li>
<li class="divider"></li>
<li class="dropdown-header">Your Account</li>
<li><a class="dropdown-item" href="#mfa" onclick="return show_panel(this);">Two-Factor
Authentication</a></li>
</ul>
</li>
<li><button class="nav-item me-1 me-xl-4 btn if-logged-in" type="button" href="#sync_guide"
onclick="return show_panel(this);">Contacts/Calendar</button></li>
<li><button class="nav-item me-1 me-xl-4 btn if-logged-in-admin" type="button" href="#web"
onclick="return show_panel(this);">Web</button></li>
<li><button class="nav-item btn btn-secondary if-logged-in" type="button"
onclick="do_logout(); return false;"><b>Logout</b></button></li>
</ul>
</div>
</div>
</div>
<div id="panel_sync_guide" class="container panel">
{% include "sync-guide.html" %}
</div>
<div class="container">
<div id="panel_smtp_relays" class="admin_panel">
{% include "smtp-relays.html" %}
</div>
<hr>
<div id="panel_welcome" class="admin_panel">
{% include "welcome.html" %}
</div>
<footer>
<p>This is a <a href="https://mailinabox.email">Mail-in-a-Box</a>.</p>
</footer>
</div> <!-- /container -->
<div id="panel_system_status" class="admin_panel">
{% include "system-status.html" %}
</div>
<div id="ajax_loading_indicator" style="display: none; position: fixed; left: 0; top: 0; width: 100%; height: 100%; text-align: center; background-color: rgba(255,255,255,.75)">
<div style="margin: 20% auto">
<div><span class="glyphicon glyphicon-time"></span></div>
<div>Loading...</div>
</div>
</div>
<div id="panel_system_backup" class="admin_panel">
{% include "system-backup.html" %}
</div>
<div id="global_modal" class="modal fade" tabindex="-1" role="dialog" aria-labelledby="errorModalTitle" aria-hidden="true">
<div class="modal-dialog modal-sm">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h4 class="modal-title" id="errorModalTitle"> </h4>
</div>
<div class="modal-body">
<p> </p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">OK</button>
<button type="button" class="btn btn-danger" data-dismiss="modal">Yes</button>
</div>
</div>
</div>
</div>
<div id="panel_external_dns" class="admin_panel">
{% include "external-dns.html" %}
</div>
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.10.1/jquery.min.js"></script>
<script src="//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/js/bootstrap.min.js"></script>
<div id="panel_custom_dns" class="admin_panel">
{% include "custom-dns.html" %}
</div>
<script>
var global_modal_state = null;
var global_modal_funcs = null;
<div id="panel_pgp_keyring" class="admin_panel">
{% include "pgp-keyring.html" %}
</div>
$(function() {
$('#global_modal').on('shown.bs.modal', function (e) {
// set focus to first input in the global modal's body
var input = $('#global_modal .modal-body input');
if (input.length > 0) $(input[0]).focus();
})
$('#global_modal .btn-danger').click(function() {
// Don't take action now. Wait for the modal to be totally hidden
// so that we don't attempt to show another modal while this one
// is closing.
global_modal_state = 0; // OK
})
$('#global_modal .btn-default').click(function() {
global_modal_state = 1; // Cancel
})
$('#global_modal').on('hidden.bs.modal', function (e) {
// do the cancel function
if (global_modal_state == null) global_modal_state = 1; // cancel if the user hit ESC or clicked outside of the modal
if (global_modal_funcs && global_modal_funcs[global_modal_state])
global_modal_funcs[global_modal_state]();
})
})
<div id="panel_wkd" class="admin_panel">
{% include "wkd.html" %}
</div>
function show_modal_error(title, message, callback) {
$('#global_modal h4').text(title);
$('#global_modal .modal-body').html("<p/>");
if (typeof question == String) {
$('#global_modal p').text(message);
$('#global_modal .modal-dialog').addClass("modal-sm");
} else {
$('#global_modal p').html("").append(message);
$('#global_modal .modal-dialog').removeClass("modal-sm");
}
$('#global_modal .btn-default').show().text("OK");
$('#global_modal .btn-danger').hide();
global_modal_funcs = [callback, callback];
global_modal_state = null;
$('#global_modal').modal({});
}
<div id="panel_manage-password" class="admin_panel">
{% include "manage-password.html" %}
</div>
function show_modal_confirm(title, question, verb, yes_callback, cancel_callback) {
$('#global_modal h4').text(title);
if (typeof question == String) {
$('#global_modal .modal-dialog').addClass("modal-sm");
$('#global_modal .modal-body').html("<p/>");
$('#global_modal p').text(question);
} else {
$('#global_modal .modal-dialog').removeClass("modal-sm");
$('#global_modal .modal-body').html("").append(question);
}
$('#global_modal .btn-default').show().text("Cancel");
$('#global_modal .btn-danger').show().text(verb);
global_modal_funcs = [yes_callback, cancel_callback];
global_modal_state = null;
$('#global_modal').modal({});
}
<div id="panel_mfa" class="admin_panel">
{% include "mfa.html" %}
</div>
var is_ajax_loading = false;
function ajax(options) {
setTimeout("if (is_ajax_loading) $('#ajax_loading_indicator').fadeIn()", 100);
function hide_loading_indicator() {
is_ajax_loading = false;
$('#ajax_loading_indicator').hide();
}
var old_success = options.success;
var old_error = options.error;
options.success = function(data) {
hide_loading_indicator();
if (data.status == "error")
show_modal_error("Error", data.message);
else if (old_success)
old_success(data);
};
options.error = function(jqxhr) {
hide_loading_indicator();
if (!old_error)
show_modal_error("Error", "Something went wrong, sorry.")
else
old_error(jqxhr.responseText);
};
is_ajax_loading = true;
$.ajax(options);
}
<div id="panel_login" class="admin_panel">
{% include "login.html" %}
</div>
var api_credentials = ["", ""];
function api(url, method, data, callback, callback_error) {
// from http://www.webtoolkit.info/javascript-base64.html
function base64encode(input) {
_keyStr = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
var output = "";
var chr1, chr2, chr3, enc1, enc2, enc3, enc4;
var i = 0;
while (i < input.length) {
chr1 = input.charCodeAt(i++);
chr2 = input.charCodeAt(i++);
chr3 = input.charCodeAt(i++);
enc1 = chr1 >> 2;
enc2 = ((chr1 & 3) << 4) | (chr2 >> 4);
enc3 = ((chr2 & 15) << 2) | (chr3 >> 6);
enc4 = chr3 & 63;
if (isNaN(chr2)) {
enc3 = enc4 = 64;
} else if (isNaN(chr3)) {
enc4 = 64;
}
output = output +
_keyStr.charAt(enc1) + _keyStr.charAt(enc2) +
_keyStr.charAt(enc3) + _keyStr.charAt(enc4);
}
<div id="panel_mail-guide" class="admin_panel">
{% include "mail-guide.html" %}
</div>
return output;
}
<div id="panel_users" class="admin_panel">
{% include "users.html" %}
</div>
ajax({
url: "/admin" + url,
method: method,
data: data,
beforeSend: function(xhr) {
// We don't store user credentials in a cookie to avoid the hassle of CSRF
// attacks. The Authorization header only gets set in our AJAX calls triggered
// by user actions.
xhr.setRequestHeader(
'Authorization',
'Basic ' + base64encode(api_credentials[0] + ':' + api_credentials[1]));
},
success: callback,
error: callback_error,
statusCode: {
403: function(xhr) {
// Credentials are no longer valid. Try to login again.
var p = current_panel;
show_panel('login');
switch_back_to_panel = p;
}
}
})
}
<div id="panel_aliases" class="admin_panel">
{% include "aliases.html" %}
</div>
var current_panel = null;
var switch_back_to_panel = null;
function show_panel(panelid) {
if (panelid.getAttribute)
// we might be passed an HTMLElement <a>.
panelid = panelid.getAttribute('href').substring(1);
<div id="panel_sync_guide" class="admin_panel">
{% include "sync-guide.html" %}
</div>
$('.panel').hide();
$('#panel_' + panelid).show();
if (typeof localStorage != 'undefined')
localStorage.setItem("miab-cp-lastpanel", panelid);
if (window["show_" + panelid])
window["show_" + panelid]();
<div id="panel_web" class="admin_panel">
{% include "web.html" %}
</div>
current_panel = panelid;
switch_back_to_panel = null;
<div id="panel_tls" class="admin_panel">
{% include "ssl.html" %}
</div>
return false; // when called from onclick, cancel navigation
}
<div id="panel_munin" class="admin_panel">
{% include "munin.html" %}
</div>
$(function() {
// Recall saved user credentials.
if (typeof sessionStorage != 'undefined' && sessionStorage.getItem("miab-cp-credentials"))
api_credentials = sessionStorage.getItem("miab-cp-credentials").split(":");
else if (typeof localStorage != 'undefined' && localStorage.getItem("miab-cp-credentials"))
api_credentials = localStorage.getItem("miab-cp-credentials").split(":");
<hr>
// Recall what the user was last looking at.
if (typeof localStorage != 'undefined' && localStorage.getItem("miab-cp-lastpanel")) {
show_panel(localStorage.getItem("miab-cp-lastpanel"));
} else {
show_panel('login');
}
})
<footer>
<p>This is a <a href="https://power-mailinabox.net">Power Mail-in-a-Box</a>.</p>
</footer>
</div> <!-- /container -->
</script>
</body>
<div id="ajax_loading_indicator"
style="display: none; position: fixed; left: 0; top: 0; width: 100%; height: 100%; z-index: 100000; text-align: center; background-color: rgba(0,0,0,.8)">
<div class="justify-content-center" style="margin: 20%;">
<div class="spinner-border text-light" role="status" style="width: 4rem; height: 4rem;"></div>
<div class="text-light display-5">Loading... please wait!</div>
</div>
</div>
<div id="global_modal" class="modal fade" tabindex="-1" role="dialog" aria-labelledby="errorModalTitle"
aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title" id="errorModalTitle"> </h4>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-hidden="true"></button>
</div>
<div class="modal-body">
<p> </p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-bs-dismiss="modal">OK</button>
<button type="button" class="btn btn-danger" data-bs-dismiss="modal">Yes</button>
</div>
</div>
</div>
</div>
<style>
.darkmode,
.darkmode .form-control,
.darkmode .form-select {
background-color: rgb(20, 20, 20) !important;
}
.darkmode .btn-light,
.darkmode .bg-light,
.darkmode .dropdown-menu,
.darkmode .input-group-text,
.darkmode .modal-content,
.darkmode .form-control:disabled,
.darkmode .form-select:disabled {
background-color: rgb(36, 36, 36) !important;
}
.darkmode .form-control,
.darkmode .form-select,
.darkmode .btn-light,
.darkmode .input-group-text,
.darkmode .table,
.darkmode .tr,
.darkmode .thead,
.darkmode .card,
.darkmode .modal-header,
.darkmode .modal-footer {
border-color: rgb(56, 56, 56) !important;
}
.darkmode h3 {
border-color: rgb(206, 212, 218) !important;
}
.darkmode .table>:not(:first-child) {
border-top: 2px solid rgb(206, 212, 218) !important;
}
.darkmode a,
.darkmode b,
.darkmode p,
.darkmode label,
.darkmode span,
.darkmode small,
.darkmode div,
.darkmode td,
.darkmode hr,
.darkmode .form-control,
.darkmode .form-select {
color: rgb(206, 212, 218);
}
.darkmode .btn,
.darkmode h1,
.darkmode h2,
.darkmode h3,
.darkmode h4,
.darkmode h5,
.darkmode .navbar-brand,
.darkmode .dropdown-menu,
.darkmode th,
.darkmode .navbar-brand>span,
.darkmode .navbar-brand>b,
.darkmode .nav-item>*,
.darkmode .input-group-text,
.darkmode .input-group-text>* {
color: rgb(255, 255, 255) !important;
}
.darkmode .btn-close,
.darkmode .navbar-toggler {
filter: invert(97%) sepia(2%) saturate(1242%) hue-rotate(180deg) brightness(93%) contrast(82%);
}
.darkmode .status-error .status-text {
color: rgb(255, 191, 191);
}
.darkmode .status-warning .status-text {
color: rgb(255, 241, 191);
}
.darkmode .status-ok .status-text {
color: rgb(191, 255, 191);
}
.darkmode .status-na .status-text {
color: rgb(155, 155, 155);
}
</style>
<script src="/admin/assets/jquery.min.js"></script>
<script src="/admin/assets/bootstrap/js/bootstrap.bundle.min.js"></script>
<script>
var global_modal_state = null;
var global_modal_funcs = null;
const toggle = document.getElementById("toggle-theme")
function set_dark_mode(isdark) {
if (isdark) {
$("body").addClass("darkmode")
} else {
$("body").removeClass("darkmode")
}
}
if (typeof localStorage != 'undefined' && localStorage.getItem("miab-theme-preference")) {
let themepref = localStorage.getItem("miab-theme-preference")
if (themepref === "dark") {
toggle.checked = true
set_dark_mode(true)
} else if (themepref === "light") {
set_dark_mode(false)
}
} else if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
// Toggle dark mode right now
toggle.checked = true
set_dark_mode(true)
}
$("#toggle-theme").change(() => {
if (typeof localStorage != 'undefined') {
localStorage.setItem("miab-theme-preference", toggle.checked ? "dark" : "light")
}
set_dark_mode(toggle.checked)
})
$(function () {
$('#global_modal').on('shown.bs.modal', function (e) {
// set focus to first input in the global modal's body
var input = $('#global_modal .modal-body input');
if (input.length > 0) $(input[0]).focus();
})
$('#global_modal .btn-danger').click(function () {
// Don't take action now. Wait for the modal to be totally hidden
// so that we don't attempt to show another modal while this one
// is closing.
global_modal_state = 0; // OK
})
$('#global_modal .btn-default').click(function () {
global_modal_state = 1; // Cancel
})
$('#global_modal').on('hidden.bs.modal', function (e) {
// do the cancel function
if (global_modal_state == null) global_modal_state = 1; // cancel if the user hit ESC or clicked outside of the modal
if (global_modal_funcs && global_modal_funcs[global_modal_state])
global_modal_funcs[global_modal_state]();
})
})
const global_modal = new bootstrap.Modal($("#global_modal"))
function show_modal_error(title, message, callback) {
$('#global_modal h4').text(title);
$('#global_modal .modal-body').html("<p/>");
if (typeof question == 'string') {
$('#global_modal p').text(message);
} else {
$('#global_modal p').html("").append(message);
}
$('#global_modal .btn-default').show().text("OK");
$('#global_modal .btn-danger').hide();
global_modal_funcs = [callback, callback];
global_modal_state = null;
global_modal.show();
return false; // handy when called from onclick
}
function show_modal_confirm(title, question, verb, yes_callback, cancel_callback) {
$('#global_modal h4').text(title);
if (typeof question == 'string') {
$('#global_modal .modal-body').html("<p/>");
$('#global_modal p').text(question);
} else {
$('#global_modal .modal-body').html("").append(question);
}
if (typeof verb == 'string') {
$('#global_modal .btn-default').show().text("Cancel");
$('#global_modal .btn-danger').show().text(verb);
} else {
$('#global_modal .btn-default').show().text(verb[1]);
$('#global_modal .btn-danger').show().text(verb[0]);
}
global_modal_funcs = [yes_callback, cancel_callback];
global_modal_state = null;
global_modal.show();
return false; // handy when called from onclick
}
var ajax_num_executing_requests = 0;
function ajax_with_indicator(options) {
setTimeout("if (ajax_num_executing_requests > 0) $('#ajax_loading_indicator').fadeIn()", 100);
function hide_loading_indicator() {
ajax_num_executing_requests--;
if (ajax_num_executing_requests == 0)
$('#ajax_loading_indicator').stop(true).hide(); // stop() prevents an ongoing fade from causing the thing to be shown again after this call
}
var old_success = options.success;
var old_error = options.error;
options.success = function (data) {
hide_loading_indicator();
if (data.status == "error")
show_modal_error("Error", data.message);
else if (old_success)
old_success(data);
};
options.error = function (jqxhr) {
hide_loading_indicator();
if (!old_error)
show_modal_error("Error", "Something went wrong, sorry.")
else
old_error(jqxhr.responseText, jqxhr);
};
ajax_num_executing_requests++;
$.ajax(options);
return false; // handy when called from onclick
}
var api_credentials = null;
function api(url, method, data, callback, callback_error, headers) {
// from http://www.webtoolkit.info/javascript-base64.html
function base64encode(input) {
_keyStr = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
var output = "";
var chr1, chr2, chr3, enc1, enc2, enc3, enc4;
var i = 0;
while (i < input.length) {
chr1 = input.charCodeAt(i++);
chr2 = input.charCodeAt(i++);
chr3 = input.charCodeAt(i++);
enc1 = chr1 >> 2;
enc2 = ((chr1 & 3) << 4) | (chr2 >> 4);
enc3 = ((chr2 & 15) << 2) | (chr3 >> 6);
enc4 = chr3 & 63;
if (isNaN(chr2)) {
enc3 = enc4 = 64;
} else if (isNaN(chr3)) {
enc4 = 64;
}
output = output +
_keyStr.charAt(enc1) + _keyStr.charAt(enc2) +
_keyStr.charAt(enc3) + _keyStr.charAt(enc4);
}
return output;
}
function default_error(text, xhr) {
if (xhr.status != 403) // else handled below
show_modal_error("Error", "Something went wrong, sorry.")
}
ajax_with_indicator({
url: "/admin" + url,
method: method,
cache: false,
data: data,
headers: headers,
// the custom DNS api sends raw POST/PUT bodies --- prevent URL-encoding
processData: typeof data != "string",
mimeType: typeof data == "string" ? "text/plain; charset=ascii" : null,
beforeSend: function (xhr) {
// We don't store user credentials in a cookie to avoid the hassle of CSRF
// attacks. The Authorization header only gets set in our AJAX calls triggered
// by user actions.
if (api_credentials)
xhr.setRequestHeader(
'Authorization',
'Basic ' + base64encode(api_credentials.username + ':' + api_credentials.session_key));
},
success: callback,
error: callback_error || default_error,
statusCode: {
403: function (xhr) {
// Credentials are no longer valid. Try to login again.
do_logout();
switch_back_to_panel = null;
}
}
})
}
var current_panel = null;
var switch_back_to_panel = null;
function do_logout() {
// Clear the session from the backend.
api("/logout", "POST");
// Forget the token.
api_credentials = null;
if (typeof localStorage != 'undefined')
localStorage.removeItem("miab-cp-credentials");
if (typeof sessionStorage != 'undefined')
sessionStorage.removeItem("miab-cp-credentials");
// Return to the start.
show_panel('login');
// Reset menus.
show_hide_menus();
}
function show_panel(panelid) {
if (panelid.getAttribute)
// we might be passed an HTMLElement <a>.
panelid = panelid.getAttribute('href').substring(1);
$('.admin_panel').hide();
$('#panel_' + panelid).show();
if (typeof localStorage != 'undefined')
localStorage.setItem("miab-cp-lastpanel", panelid);
if (window["show_" + panelid])
window["show_" + panelid]();
current_panel = panelid;
switch_back_to_panel = null;
return false; // when called from onclick, cancel navigation
}
$(function () {
// Recall saved user credentials.
try {
if (typeof sessionStorage != 'undefined' && sessionStorage.getItem("miab-cp-credentials"))
api_credentials = JSON.parse(sessionStorage.getItem("miab-cp-credentials"));
else if (typeof localStorage != 'undefined' && localStorage.getItem("miab-cp-credentials"))
api_credentials = JSON.parse(localStorage.getItem("miab-cp-credentials"));
} catch (_) {
}
// Toggle menu state.
show_hide_menus();
// Recall what the user was last looking at.
if (api_credentials != null && typeof localStorage != 'undefined' && localStorage.getItem("miab-cp-lastpanel")) {
show_panel(localStorage.getItem("miab-cp-lastpanel"));
} else if (api_credentials != null) {
show_panel('welcome');
} else {
show_panel('login');
}
})
</script>
</body>
</html>

View file

@ -1,116 +1,201 @@
<h1 style="margin: 1em; text-align: center">{{hostname}}</h1>
<style>
.title {
margin: 0.5em;
text-align: center;
}
{% if no_admins_exist %}
<div class="container">
<div class="col-md-offset-2 col-md-8">
<p class="text-danger">There are no administrative users on this system! To make an administrative user,
log into this machine using SSH (like when you first set it up) and run:</p>
<pre>cd mailinabox
sudo tools/mail.py user make-admin your@emailaddress.com</pre>
<hr>
.subtitle {
margin: 1em;
text-align: center;
}
.login {
margin: 0 auto;
max-width: 32em;
}
.login #loginOtp {
display: none;
}
#loginForm.is-twofactor #loginOtp {
display: block
}
</style>
<h1 class="title">{{hostname}}</h1>
{% if no_users_exist or no_admins_exist %}
<div class="row">
<div class="col-12">
<hr>
{% if no_users_exist %}
<p class="text-danger">There are no users on this system! To make an administrative user,
log into this machine using SSH (like when you first set it up) and run:</p>
<pre>cd mailinabox
sudo management/cli.py user add me@{{hostname}}
sudo management/cli.py user make-admin me@{{hostname}}</pre>
{% else %}
<p class="text-danger">There are no administrative users on this system! To make an administrative user,
log into this machine using SSH (like when you first set it up) and run:</p>
<pre>cd mailinabox
sudo management/cli.py user make-admin me@{{hostname}}</pre>
{% endif %}
<hr>
</div>
</div>
</div>
{% endif %}
<div class="row">
<div class="col-sm-offset-2 col-sm-8 col-md-offset-3 col-md-6 col-lg-offset-4 col-lg-4">
<center>
<p style="margin: 2em">Log in here for your Mail-in-a-Box control panel.</p>
</center>
<p class="subtitle">Log in here for your Mail-in-a-Box control panel.</p>
<form class="form-horizontal" role="form" onsubmit="do_login(); return false;">
<div class="form-group">
<label for="inputEmail3" class="col-sm-2 control-label">Email</label>
<div class="col-sm-10">
<input name="email" type="email" class="form-control" id="loginEmail" placeholder="Email">
</div>
</div>
<div class="form-group">
<label for="inputPassword3" class="col-sm-2 control-label">Password</label>
<div class="col-sm-10">
<input name="password" type="password" class="form-control" id="loginPassword" placeholder="Password">
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
<div class="checkbox">
<label>
<input name='remember' type="checkbox" id="loginRemember"> Remember me
</label>
</div>
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
<button type="submit" class="btn btn-default">Sign in</button>
</div>
</div>
</form>
</div>
<div class="login">
<form id="loginForm" class="form-horizontal" role="form" onsubmit="do_login(); return false;" method="get">
<div class="form-floating mb-3">
<input type="email" class="form-control col-sm-12" id="loginEmail" placeholder="user@example.com">
<label for="loginEmail">Email</label>
</div>
<div class="form-floating mb-3">
<input type="password" class="form-control col-sm-12" id="loginPassword" placeholder="password">
<label for="loginPassword">Password</label>
</div>
<div class="form-floating mb-3" id="loginOtp">
<input type="text" class="form-control" id="loginOtpInput" placeholder="000000" autocomplete="off">
<label for="loginOtpInput">TOTP Code</label>
<div class="help-block" style="margin-top: 5px; font-size: 90%">Enter the six-digit code generated by your two
factor authentication app.</div>
</div>
<div class="form-group mb-3">
<div class="ms-auto">
<input type="checkbox" class="form-check-input" id="loginRemember">
<label class="form-check-label" for="loginRemember">Remember Me</label>
</div>
</div>
<div class="form-group">
<div class="container-fluid">
<button type="submit" class="btn btn-primary col-12">Sign in</button>
</div>
</div>
</form>
</div>
<script>
function do_login() {
if ($('#loginEmail').val() == "") {
show_modal_error("Login Failed", "Enter your email address.")
return false;
}
if ($('#loginPassword').val() == "") {
show_modal_error("Login Failed", "Enter your email password.")
return false;
}
function do_login() {
if ($('#loginEmail').val() == "") {
show_modal_error("Login Failed", "Enter your email address.", function () {
$('#loginEmail').focus();
});
return false;
}
// Exchange the email address & password for an API key.
api_credentials = [$('#loginEmail').val(), $('#loginPassword').val()]
if ($('#loginPassword').val() == "") {
show_modal_error("Login Failed", "Enter your email password.", function () {
$('#loginPassword').focus();
});
return false;
}
api(
"/me",
"GET",
{ },
function(response){
// This API call always succeeds. It returns a JSON object indicating
// whether the request was authenticated or not.
if (response.status != "authorized") {
// Show why the login failed.
show_modal_error("Login Failed", response.reason)
// Exchange the email address & password for an API key.
api_credentials = { username: $('#loginEmail').val(), session_key: $('#loginPassword').val() }
// Reset any saved credentials.
do_logout();
api(
"/login",
"POST",
{},
function (response) {
// This API call always succeeds. It returns a JSON object indicating
// whether the request was authenticated or not.
if (response.status != 'ok') {
if (response.status === 'missing-totp-token' || (response.status === 'invalid' && response.reason == 'invalid-totp-token')) {
$('#loginForm').addClass('is-twofactor');
if (response.reason === "invalid-totp-token") {
show_modal_error("Login Failed", "Incorrect two factor authentication token.");
} else {
setTimeout(() => {
$('#loginOtpInput').focus();
});
}
} else {
$('#loginForm').removeClass('is-twofactor');
} else {
// Login succeeded.
// Show why the login failed.
show_modal_error("Login Failed", response.reason)
// Save the new credentials.
api_credentials = [response.api_key, ""];
// Reset any saved credentials.
do_logout();
}
} else if (!("api_key" in response)) {
// Login succeeded but user might not be authorized!
show_modal_error("Login Failed", "You are not an administrator on this system.")
// Try to wipe the username/password information.
$('#loginEmail').val('');
$('#loginPassword').val('');
// Reset any saved credentials.
do_logout();
// Remember the credentials.
if (typeof localStorage != 'undefined' && typeof sessionStorage != 'undefined') {
if ($('#loginRemember').val()) {
localStorage.setItem("miab-cp-credentials", api_credentials.join(":"));
sessionStorage.removeItem("miab-cp-credentials");
} else {
localStorage.removeItem("miab-cp-credentials");
sessionStorage.setItem("miab-cp-credentials", api_credentials.join(":"));
}
}
} else {
// Login succeeded.
// Open the next panel the user wants to go to.
show_panel(!switch_back_to_panel ? 'system_status' : switch_back_to_panel)
}
})
}
// Save the new credentials.
api_credentials = {
username: response.email,
session_key: response.api_key,
privileges: response.privileges
};
function do_logout() {
api_credentials = ["", ""];
if (typeof localStorage != 'undefined')
localStorage.removeItem("miab-cp-credentials");
if (typeof sessionStorage != 'undefined')
sessionStorage.removeItem("miab-cp-credentials");
show_panel('login');
}
// Try to wipe the username/password information.
$('#loginEmail').val('');
$('#loginPassword').val('');
$('#loginOtpInput').val('');
$('#loginForm').removeClass('is-twofactor');
// Remember the credentials.
if (typeof localStorage != 'undefined' && typeof sessionStorage != 'undefined') {
if ($('#loginRemember').val()) {
localStorage.setItem("miab-cp-credentials", JSON.stringify(api_credentials));
sessionStorage.removeItem("miab-cp-credentials");
} else {
localStorage.removeItem("miab-cp-credentials");
sessionStorage.setItem("miab-cp-credentials", JSON.stringify(api_credentials));
}
}
// Toggle menus.
show_hide_menus();
// 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,
// which confuses the loading indicator.
setTimeout(function () { show_panel(!switch_back_to_panel || switch_back_to_panel == "login" ? 'welcome' : switch_back_to_panel) }, 300);
}
},
undefined,
{
'x-auth-token': $('#loginOtpInput').val()
});
}
function show_login() {
$('#loginForm').removeClass('is-twofactor');
$('#loginOtpInput').val('');
$('#loginEmail,#loginPassword').each(function () {
var input = $(this);
if (!$.trim(input.val())) {
input.focus();
return false;
}
});
}
function show_hide_menus() {
var is_logged_in = (api_credentials != null);
var privs = api_credentials ? api_credentials.privileges : [];
$('.if-logged-in').toggle(is_logged_in);
$('.if-logged-in-admin, .if-logged-in-not-admin').toggle(false);
if (is_logged_in) {
$('.if-logged-in-not-admin').toggle(true);
privs.forEach(function (priv) {
$('.if-logged-in-' + priv).toggle(true);
$('.if-logged-in-not-' + priv).toggle(false);
});
}
$('.if-not-logged-in').toggle(!is_logged_in);
}
</script>

View file

@ -1,62 +1,133 @@
<style>#panel_mail-guide table.table { width: auto; margin-left: 1.5em; }</style>
<style>
#panel_mail-guide table.table {
width: auto;
margin-left: .5em;
}
</style>
<div class="container">
<h2>Checking and Sending Mail</h2>
<p>You can use either webmail, IMAP/SMTP, or Exchange/ActiveSync to access your mail.</p>
<hr>
<div>
<h2 style="margin-bottom: 0">Checking and Sending Mail</h2>
<div class="row">
<div class="col-md-6">
<h4>How to log in</h4>
<div class="col-sm-7">
<h3>Webmail</h3>
<p>Your username and password are the same no matter how you check your mail:</p>
<p>Webmail lets you check your email from any web browser. Your webmail site is:</p>
<p style="margin-left: 2em"><strong><a
href="https://{{hostname}}/mail">https://{{hostname}}/mail</a></strong></p>
<p>Your username is your whole email address.</p>
<table class="table" style="max-width: 30em">
<tr><th>Username:</th> <td>Your whole email address.</td></tr>
<tr><th>Password:</th> <td>Your mail password.</td></tr>
</table>
</div>
</div>
<div class="row">
<div class="col-sm-6">
<h4>Webmail</h4>
<h3>Mobile/desktop apps</h3>
<p>Your webmail site is <a href="https://{{hostname}}/mail">https://{{hostname}}/mail</a>.</p>
</div>
</div>
<h4>Automatic configuration</h4>
<div class="row">
<div class="col-sm-6">
<h4>IMAP/SMTP settings</h4>
<p>iOS and OS X only: Open <a style="font-weight: bold"
href="https://{{hostname}}/mailinabox.mobileconfig">this configuration link</a> on your iOS device
or on your Mac desktop to easily set up mail (IMAP/SMTP), Contacts, and Calendar. Your username is your
whole email address.</p>
<p>This method is preferred on Android devices and is not available on iOS devices.</p>
<h4>Manual configuration</h4>
<p>Use the following settings when you set up your email on your phone, desktop, or other device:</p>
<table class="table">
<thead>
<tr><th style="padding-bottom: 1em; border-bottom: none">Server:</th> <td colspan="2">{{hostname}}</td></tr>
<tr><th>Protocol</th> <th>Port</th> <th>Options</th></tr>
</thead>
<tr><th>IMAP</th> <td>993</td> <td>SSL</td></tr>
<tr><th>SMTP</th> <td>587</td> <td>STARTTLS</td></tr>
<caption></caption>
<thead>
<tr>
<th>Option</th>
<th>Value</th>
</tr>
</thead>
<tr>
<th>Protocol/Method</th>
<td>IMAP</td>
</tr>
<tr>
<th>Mail server</th>
<td>{{hostname}}</td>
<tr>
<th>IMAP Port</th>
<td>993</td>
</tr>
<tr>
<th>IMAP Security</th>
<td>SSL or TLS</td>
</tr>
<tr>
<th>SMTP Port</th>
<td>465</td>
</tr>
<tr>
<th>SMTP Security</td>
<td>SSL or TLS</td>
</tr>
<tr>
<th>Username:</th>
<td>Your whole email address.</td>
</tr>
<tr>
<th>Password:</th>
<td>Your mail password.</td>
</tr>
</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>.</p>
</div>
<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>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>
<div class="col-sm-6">
<h4>Exchange/ActiveSync settings</h4>
<p>On iOS devices and devices on this <a href="http://z-push.org/compatibility/">compatibility list</a>, set up your mail as an Exchange or ActiveSync server:</p>
<p>On iOS devices, devices on this <a href="https://wiki.z-hub.io/display/ZP/Compatibility">compatibility
list</a>, or using Outlook 2007 or later on Windows 7 and later, you may set up your mail as an
Exchange or ActiveSync server. However, we&rsquo;ve found this to be more buggy than using IMAP as
described above. If you encounter any problems, please use the manual settings above.</p>
<table class="table">
<tr><th>Server</th> <td>{{hostname}}</td></tr>
<tr><th>Options</th> <td>Secure Connection</td></tr>
<tr>
<th>Server</th>
<td>{{hostname}}</td>
</tr>
<tr>
<th>Options</th>
<td>Secure Connection</td>
</tr>
</table>
<p>Your device should also provide a contacts list and calendar that syncs to this box when you use this method.</p>
<p>Your device should also provide a contacts list and calendar that syncs to this box when you use this
method.</p>
</div>
<div class="col-sm-5">
<div class="panel panel-default">
<div class="panel-heading">
<h3>Other information about mail on your box</h3>
</div>
<div class="panel-body">
<h4>Greylisting</h4>
<p>Your box uses a technique called greylisting to cut down on spam. Greylisting works by initially
rejecting mail from people you haven&rsquo;t received mail from before. Legitimate mail servers
will attempt redelivery shortly afterwards, but the vast majority of spam gets tricked by this.
If you are waiting for an email from someone new, such as if you are registering on a new
website and are waiting for an email confirmation, please be aware there will be a minimum of 3
minutes delay, depending how soon the remote server attempts redelivery.</p>
<h4>+tag addresses</h4>
<p>Every incoming email address also receives mail for <code>+tag</code> addresses. If your email
address is <code>you@yourdomain.com</code>, you&rsquo;ll also automatically get mail sent to
<code>you+anythinghere@yourdomain.com</code>. Use this as a fast way to segment incoming mail
for your own filtering rules without having to create aliases in this control panel.</p>
<h4>Use only this box to send as you</h4>
<p>Your box sets strict email sending policies for your domain names to make it harder for spam and
other fraudulent mail to claim to be you. Only this machine is authorized to send email on
behalf of your domain names. If you use any other service to send email as you, it will likely
get spam filtered by recipients.</p>
</div>
</div>
</div>
</div>
</div>

View file

@ -0,0 +1,57 @@
<div>
<h2>Manage Password</h2>
<p>Here you can change your account password. The new password is then valid for both this panel and your email.</p>
<p>If you have client emails configured, you'll then need to update the configuration with the new password. See the <a href="#mail-guide" onclick="return show_panel(this);">Mail Guide</a> for more information about this.</p>
<form class="form-horizontal" role="form" onsubmit="set_password_self(); return false;">
<div class="col-lg-10 col-xl-8 mb-3">
<div class="input-group">
<label for="manage-password-new" class="input-group-text col-3">New Password</label>
<input type="password" placeholder="password" class="form-control" id="manage-password-new">
</div>
</div>
<div class="col-lg-10 col-xl-8 mb-3">
<div class="input-group">
<label for="manage-password-confirm" class="input-group-text col-3">Confirm Password</label>
<input type="password" placeholder="password" class="form-control" id="manage-password-confirm">
</div>
</div>
<div class="mt-3">
<button id="manage-password-submit" type="submit" class="btn btn-primary">Save</button>
</div>
<small>After changing your password, you'll be logged out from the account and will need to log in again.</small>
</form>
</div>
<script>
function set_password_self() {
if ($('#manage-password-new').val() !== $('#manage-password-confirm').val()) {
show_modal_error("Set Password", 'Passwords do not match!');
return;
}
let password = $('#manage-password-new').val()
api(
"/mail/users/password",
"POST",
{
email: api_credentials.username,
password: password
},
function (r) {
// Responses are multiple lines of pre-formatted text.
show_modal_error("Set Password", $("<pre/>").text(r), () => {
do_logout()
$('#manage-password-new').val("")
$('#manage-password-confirm').val("")
});
},
function (r) {
show_modal_error("Set Password", r);
}
);
}
</script>

View file

@ -0,0 +1,240 @@
<style>
.twofactor #totp-setup,
.twofactor #disable-2fa,
.twofactor #output-2fa {
display: none;
}
.twofactor.loaded .loading-indicator {
display: none;
}
.twofactor.disabled #disable-2fa,
.twofactor.enabled #totp-setup {
display: none;
}
.twofactor.disabled #totp-setup,
.twofactor.enabled #disable-2fa {
display: block;
}
.twofactor #totp-setup-qr img {
display: block;
width: 256px;
max-width: 100%;
height: auto;
}
.twofactor #output-2fa.visible {
display: block;
}
</style>
<h2>Two-Factor Authentication</h2>
<p>When two-factor authentication is enabled, you will be prompted to enter a six digit code from an
authenticator app (usually on your phone) when you log into this control panel.</p>
<div class="card mb-4">
<div class="card-header text-white bg-danger">
Enabling two-factor authentication does not protect access to your email
</div>
<div class="card-body bg-light">
Enabling two-factor authentication on this page only limits access to this control panel. Remember that most
websites allow you to
reset your password by checking your email, so anyone with access to your email can typically take over
your other accounts. Additionally, if your email address or any alias that forwards to your email
address is a typical domain control validation address (e.g admin@, administrator@, postmaster@, hostmaster@,
webmaster@, abuse@), extra care should be taken to protect the account. <strong>Always use a strong password,
and ensure every administrator account for this control panel does the same.</strong>
</div>
</div>
<div class="twofactor">
<div class="loading-indicator">Loading...</div>
<form id="totp-setup">
<h3>Setup Instructions</h3>
<div class="row gx-5">
<div class="col-12 col-lg-6">
<p><b>1.</b> Install <a href="https://freeotp.github.io/">FreeOTP</a> or <a
href="https://www.pcworld.com/article/3225913/what-is-two-factor-authentication-and-which-2fa-apps-are-best.html">any
other two-factor authentication app</a> that supports TOTP.</p>
<div class="mb-3">
<p style="margin-bottom: 0"><b>2.</b> Scan the QR code in the app or directly enter the secret into
the app:</p>
<div id="totp-setup-qr">
<img class="mt-3 mb-3 ms-auto me-auto" id="twofactor-qrimg">
<div class="input-group">
<label class="input-group-text" for="totp-setup-secret"><b>Secret</b></label>
<input type="text" class="form-control font-monospace" id="totp-setup-secret" disabled />
</div>
</div>
</div>
</div>
<div class="col-12 col-lg-6">
<div class="mb-3">
<label for="otp-label" style="font-weight: normal"><b>3.</b> Optionally, give your device a label so
that you can remember what device you set it up on:</label>
<input type="text" id="totp-setup-label" class="form-control" placeholder="my phone" />
</div>
<div class="mb-3">
<label for="otp" style="font-weight: normal"><b>4.</b> Use the app to generate your first six-digit
code and enter it here:</label>
<input type="text" id="totp-setup-token" class="form-control" placeholder="6-digit code" />
</div>
</div>
</div>
<div class="form-group">
<div>
<button id="totp-setup-submit" disabled type="submit" class="btn btn-primary">Enable Two-Factor
Authentication</button>
</div>
<small>When you click Enable Two-Factor Authentication, you will be logged out of the control panel and will
have to log in
again, now using your two-factor authentication app.</small>
</div>
</form>
<form id="disable-2fa">
<div>
<p>Two-factor authentication is active for your account<span id="mfa-device-label"> on device <span
class="font-monospace"></span></span>.</p>
<button type="submit" class="btn btn-danger">Disable Two-Factor Authentication</button>
</div>
<small>You will have to log into the admin panel again after disabling two-factor authentication.</small>
</form>
</div>
<script>
var el = {
disableForm: document.getElementById('disable-2fa'),
totpSetupForm: document.getElementById('totp-setup'),
totpSetupToken: document.getElementById('totp-setup-token'),
totpSetupSecret: document.getElementById('totp-setup-secret'),
totpSetupLabel: document.getElementById('totp-setup-label'),
totpQr: document.getElementById('totp-setup-qr'),
totpSetupSubmit: document.querySelector('#totp-setup-submit'),
wrapper: document.querySelector('.twofactor')
}
function update_setup_disabled(evt) {
var val = evt.target.value.trim();
if (
typeof val !== 'string' ||
typeof el.totpSetupSecret.value !== 'string' ||
val.length !== 6 ||
el.totpSetupSecret.value.length !== 32 ||
!(/^\+?\d+$/.test(val))
) {
el.totpSetupSubmit.setAttribute('disabled', '');
} else {
el.totpSetupSubmit.removeAttribute('disabled');
}
}
function render_totp_setup(provisioned_totp) {
$('#twofactor-qrimg').attr("src", "data:image/png;base64," + provisioned_totp.qr_code_base64);
el.totpSetupSecret.val = provisioned_totp.secret;
el.totpSetupToken.addEventListener('input', update_setup_disabled);
el.totpSetupForm.addEventListener('submit', do_enable_totp);
el.totpSetupSecret.setAttribute('value', provisioned_totp.secret);
el.wrapper.classList.add('disabled');
}
function render_disable(mfa) {
el.disableForm.addEventListener('submit', do_disable);
el.wrapper.classList.add('enabled');
if (mfa.label) {
$("#mfa-device-label").show()
$("#mfa-device-label .font-monospace").text(mfa.label);
} else {
$("#mfa-device-label").hide()
}
}
function reset_view() {
el.wrapper.classList.remove('loaded', 'disabled', 'enabled');
el.disableForm.removeEventListener('submit', do_disable);
el.totpSetupForm.reset();
el.totpSetupForm.removeEventListener('submit', do_enable_totp);
el.totpSetupSecret.setAttribute('value', '');
el.totpSetupToken.removeEventListener('input', update_setup_disabled);
el.totpSetupSubmit.setAttribute('disabled', '');
$('#twofactor-qrimg').attr("src", "");
$('#totp-setup-secret').attr("value", "");
}
function show_mfa() {
reset_view();
api(
'/mfa/status',
'POST',
{},
function (res) {
el.wrapper.classList.add('loaded');
var has_mfa = false;
res.enabled_mfa.forEach(function (mfa) {
if (mfa.type == "totp") {
render_disable(mfa);
has_mfa = true;
}
});
if (!has_mfa)
render_totp_setup(res.new_mfa.totp);
}
);
}
function do_disable(evt) {
evt.preventDefault();
api(
'/mfa/disable',
'POST',
{ type: 'totp' },
function () {
do_logout();
}
);
return false;
}
function do_enable_totp(evt) {
evt.preventDefault();
api(
'/mfa/totp/enable',
'POST',
{
token: $(el.totpSetupToken).val(),
secret: $(el.totpSetupSecret).val(),
label: $(el.totpSetupLabel).val()
},
function (res) { do_logout(); },
function (res) { show_modal_error("Two-Factor Authentication Setup", res); }
);
return false;
}
</script>

View file

@ -0,0 +1,20 @@
<h2>Munin Monitoring</h2>
<style>
</style>
<p>Opening munin in a new tab... You may need to allow pop-ups for this site.</p>
<script>
function show_munin() {
// Set the cookie.
api(
"/munin",
"GET",
{},
function (r) {
// Redirect.
window.open("/admin/munin/index.html", "_blank");
});
}
</script>

View file

@ -0,0 +1,298 @@
<style>
#pgp_keyring_config .icon {
min-width: 2em;
}
.inline {
display: inline;
}
</style>
<h2>PGP Keyring Management</h2>
<template id="pgpkey-template">
<tr>
<td class="row p-3">
<div class="col-lg-10 col-12">
<b id="isrevoked" class="fs-4"></b>
<div id="uids" class="mb-3 font-monospace">
<div id="uid-template">
<span class="icon fas"></span><span id="id" class="status-text"></span>
</div>
</div>
<b class="fs-5">Subkeys</b>
<div id="subkeys" class="align-middle col-12">
<div class="row" id="subkey-template">
<div style="width: 2em;" id="ismaster" data-bs-toggle="tooltip" title="Master Key"></div>
<div style="width: 5em;">
<b>
<span class="sym" id="sign">S</span>
<span class="sym" id="cert">C</span>
<span class="sym" id="encr">E</span>
<span class="sym" id="auth">A</span>
</b>
</div>
<div class="col-md-2 col-4" id="algo">
<b class="status-text"></b>
</div>
<div class="col" id="expiration">
<span class="status-text"></span>
</div>
<div class="col-md-6 col-12" id="fpr">
<span class="status-text font-monospace"></span>
</div>
</div>
</div>
</div>
<div id="options" class="col-lg-2 col-12">
</div>
</td>
</tr>
</template>
<div id="pgp_keyring_config">
<h3>Daemon's Private Key</h3>
<table id="privatekey" class="container">
</table>
<h3>Imported Public Keys</h3>
<table id="pubkeys" class="container">
</table>
<h3>Import Key</h3>
<p>
You can upload your <b>public</b> key/keychain here. Keys <b>must</b> be submitted in ASCII-armored format.
<br>
If you're using <code>gpg</code>, you can export your public key by following this example:
<pre>
# Get all the keys in the ring
$ <b>gpg --list-keys</b>
/home/you/.gnupg/pubring.kbx
----------------------------
pub rsa4096 1970-01-01 [SC]
247C3553B4B36107BA0490C3CAFCCF3B4965761A
uid [ full ] Someone That I Used to Know &lt;someone@example.com&gt;
sub rsa2048 2020-01-01 [E] [expires: 2069-12-31]
pub rsa4096 1970-01-01 [SC] [expires: 2069-12-31]
52661092E5CD9EEFD7796B19E85F540C9318B69F
uid [ultimate] Me, Myself and I &lt;me@example.net&gt;
sub rsa2048 2020-05-24 [E] [expires: 2069-12-31]
# Let's export the key "Me, Myself and I"
$ <b>gpg --export --armor 52661092E5CD9EEFD7796B19E85F540C9318B69F</b>
<b>-----BEGIN PGP PUBLIC KEY BLOCK-----
copy and paste this block in the area below
-----END PGP PUBLIC KEY BLOCK-----</b>
</pre>
</p>
<div class="form-floating col-12 col-xl-6 mb-3">
<textarea id="pgp_paste_key" class="form-control" style="font-size:80%; font-family: monospace; height: 20em"
placeholder="-----BEGIN PGP PUBLIC KEY BLOCK-----&#xA;stuff here&#xA;-----END PGP PUBLIC KEY BLOCK-----"></textarea>
<label for="pgp_paste_key">Paste your PGP public key here</label>
</div>
<button class="btn btn-primary" onclick="importkey()">Import Key</button>
</div>
<script>
function pretty_fpr(fpr) {
let pfpr = ""
for (let n = 0; n < 2; ++n) {
for (let i = 0; i < 5; ++i) {
pfpr += `${fpr.substring(n * 20 + i * 4, n * 20 + (i + 1) * 4)} `
}
pfpr += " "
}
return pfpr.substring(0, pfpr.length - 2)
}
function key_html(key, darken_bg, daemon) {
let keyrep = $("#pgpkey-template").html()
console.log(keyrep)
keyrep = $(keyrep)
keyrep.attr("id", key.master_fpr)
// Main key config
if (darken_bg) {
keyrep.addClass("bg-light")
}
let rev = keyrep.find("#isrevoked")
if (key.revoked) {
rev.text("This key was revoked by it's owner.")
rev.addClass(".status-error")
} else {
rev.text("Key is not revoked.")
}
let uids = keyrep.find("#uids")
let uidtemplate = $(uids.html())
uids.find("#uid-template").remove()
if (daemon) {
uidtemplate.find(".icon").addClass("fa-robot")
} else {
uidtemplate.find(".icon").addClass("fa-user")
}
key.ids.forEach(id => {
let thisuid = uidtemplate.clone()
thisuid.attr("id", "")
thisuid.find("#id").text(id)
thisuid.appendTo(uids)
});
// Subkeys
const keyflags = ["sign", "cert", "encr", "auth"]
let subkeys = keyrep.find("#subkeys")
let subkeytemplate = subkeys.html()
keyrep.find("#subkey-template").remove()
key.subkeys.forEach(skey => {
let skeyrep = $(subkeytemplate)
skeyrep.attr("id", `sub${skey.fpr}`)
let statusclass
let expiration = skeyrep.find("#expiration")
let exptxt = expiration.find(".status-text")
if (key.revoked) {
statusclass = "status-error"
exptxt.text("Revoked")
} else if (skey.expired) {
statusclass = "status-error"
exptxt.text(`${skey.expires_date} (expired)`)
} else if (skey.expires && skey.expires_days <= 14) {
statusclass = "status-warning"
exptxt.text(`${skey.expires_date} (${skey.expires_days} days)`)
} else if (skey.expires) {
statusclass = "status-ok"
exptxt.text(`${skey.expires_date} (${skey.expires_days} days)`)
} else {
statusclass = "status-ok"
exptxt.text("Does not expire")
}
expiration.addClass(statusclass)
// Master key?
if (skey.master) {
skeyrep.find("#ismaster").addClass("fas fa-key")
}
// Usage flags
keyflags.forEach(flag => {
skeyrep.find(`#${flag}`).addClass(skey[flag] ? statusclass : "status-na")
});
// Algorithm and fingerprint
let algo = skeyrep.find("#algo")
algo.addClass(statusclass)
algo.find(".status-text").html(`${skey.algorithm}, ${skey.bits} bits`)
let fpr = skeyrep.find("#fpr")
fpr.addClass(statusclass)
fpr.find(".status-text").html(pretty_fpr(skey.fpr))
skeyrep.appendTo(subkeys)
});
// Options
if (daemon) {
keyrep.find("#options").html(`<button class="btn btn-primary col-12" onclick="exportkey('${key.master_fpr}')">Export Public Key</button>`)
} else {
keyrep.find("#options").html(`<button class="btn btn-secondary col-12 mb-3" onclick="exportkey('${key.master_fpr}')">Export Public Key</button><button class="btn btn-danger col-12" onclick="rmkey('${key.master_fpr}')">Remove Key</button>`)
}
return keyrep
}
function show_pgp_keyring() {
$('#privatekey').html("<tr><td class='text-muted'>Loading...</td></tr>")
$('#pubkeys').html("<tr><td class='text-muted'>Loading...</td></tr>")
api(
"/system/pgp/",
"GET",
{},
function (r) {
$('#privatekey').html("")
$('#pubkeys').html("")
key_html(r.daemon, true, true).appendTo("#privatekey")
let pendulum = 1
r.imported.forEach(k => {
key_html(k, pendulum > 0, false).appendTo("#pubkeys")
pendulum *= -1
});
}
)
}
function exportkey(fpr) {
api(
`/system/pgp/${fpr}/export`,
"GET",
{},
function (r) {
show_modal_error("PGP Key", `Key export for <b>${fpr}</b>:<br><br><pre>${r}</pre>`)
},
function (_, xhr) {
if (xhr.status == 404) {
show_modal_error("Error", `The key you asked for (<b>${fpr}</b>) does not exist!`)
} else {
// Fallback to the default error modal
show_modal_error("Error", "Something went wrong, sorry.")
}
}
)
}
function rmkey(fpr) {
show_modal_confirm("Delete key", `Are you sure you wish to remove the key with the fingerprint ${pretty_fpr(fpr)}?`, "Yes, remove it", () => {
api(
`/system/pgp/${fpr}`,
"DELETE",
{},
function (r) {
if (r.length == 0) {
show_modal_error("Delete key", "OK", show_pgp_keyring)
} else {
wkd_info = "OK. <b>The following entries were removed from WKD:</b><ul>"
r.forEach(email => {
wkd_info += `<li>${email}</li>`
});
wkd_info += "</ul>"
show_modal_error("Delete key", wkd_info, show_pgp_keyring)
}
},
function (r) {
show_modal_error("Key deletion error", r)
}
)
}, () => { })
}
function importkey() {
api(
"/system/pgp/import",
"POST",
{
key: $("#pgp_paste_key").val()
},
function (r) {
show_modal_error("Import Results", `<ul>
<li><b>Keys read:</b> ${r.keys_read}</li>
<li><b>Keys added:</b> ${r.keys_added}</li>
<li><b>Keys not changed:</b> ${r.keys_unchanged}</li>
<li><b>User id's added:</b> ${r.uids_added}</li>
<li><b>Signatures added:</b> ${r.sigs_added}</li>
<li><b>Revocations added:</b> ${r.revs_added}</li>
</ul>`, show_pgp_keyring)
},
function (r) {
show_modal_error("Import Error", r)
}
)
}
</script>

View file

@ -0,0 +1,322 @@
<style>
</style>
<h2>SMTP Relays</h2>
<p>SMTP Relays are third-party services that can deliver email on your behalf. They
can be useful when, for example, port 25 is blocked, the cloud provider/ISP doesn't provide Reverse DNS, or the IP
address
has a low reputation, among other situations where deliverablity isn't great.</p>
<p>These services are governed by their own terms and as such limits can be imposed in the usage of those services.</p>
<p>Here, you can configure an authenticated SMTP relay and authorize it's associated servers to send mail for you.</p>
<div id="smtp_relay_config">
<form class="form-horizontal" role="form" onsubmit="set_smtp_relay_config(); return false;">
<h3>SMTP Relay Configuration</h3>
<div id="smtp-relays" class="col-12">
<div class="input-group mb-3">
<label for="use_relay" class="input-group-text"><b>Use Relay?</b></label>
<div class="input-group-text">
<div class="form-switch">
<input type="checkbox" role="switch" id="use_relay" class="form-check-input" value=false
onclick="checkfields();">
</div>
</div>
</div>
<div class="input-group mb-3">
<label class="input-group-text">Where to?</label>
<input type="text" class="form-control" id="relay_host" placeholder="relay.example.com">
<label class="input-group-text">:</label>
<div style="width: 20%">
<input type="number" class="form-control" id="relay_port" placeholder="Port (465)">
</div>
</div>
<div class="input-group mb-3">
<label class="input-group-text">Username</label>
<input type="text" class="form-control" id="relay_auth_user" placeholder="">
</div>
<div>
<div class="input-group">
<label class="input-group-text">Password/Key</label>
<input type="password" class="form-control" id="relay_auth_pass" placeholder="">
</div>
<p class="small">If you've already set up a relay before on this box, you can leave this field blank if
you don't want to change it's password.</p>
</div>
</div>
<h3>Authorized Servers</h3>
<p>The relay service should specify the servers where the email will be sent from, please add them below. These
will probably be published in the form of a SPF record. <b>Failure to do so will potentially have your email
sent to spam or even rejected altogether by recipients.</b>
</p>
<p>You can use the button below to attempt to localize the SPF record associated with the service you're using.
</p>
<button id="smtp_relay_autospf_btn" type="button" class="btn btn-secondary" onclick="autodetect_spf()">Detect
SPF
Records</button>
<div id="smtp_relay_autospf"></div>
<br>
<div class="form-group">
<h4>Add your SPF configuration/authorized servers here</h4>
<input type="text" class="form-control" id="relay_authorized_servers"
placeholder="mail1.example.net mail2.example.net">
<p class="small">You can separate multiple servers with commas or spaces. You can also add IP addresses or
subnets using <code>10.20.30.40</code> or <code>10.0.0.0/8</code>. You can "import" SPF records using
<code>spf:example.com</code>. If your provider gave you an SPF record to add to your DNS, you can also paste it here.
</p>
</div>
<h3>DKIM Configuration</h3>
<p>DKIM allows receivers to verify that the email was sent by the relay you configured (this is, somebody you
trust). <b>If your relay provider does not provide you with this information, it's probably safe to skip this step.</b></p>
<div class="col-lg-6 col-md-8 col-12">
<div class="input-group">
<input type="text" class="form-control" id="relay_dkim_selector" placeholder="selector"></td>
<label class="input-group-text" for="relay_dkim_selector">._domainkey.{{hostname}}</label>
</div>
</div>
<h4>Paste the DKIM key here:</h4>
<p><textarea id="relay_dkim_key" class="form-control" style="width: 100%; height: 8em"
placeholder="k=algo;p=K3y/C0N7ent5/dGhpcyBpcyBub3QgYSByZWFsIGtleSwgc28gYmV3YXJlIDrigb4"></textarea>
</p>
<div>
<button type="submit" class="btn btn-primary">Update</button>
</div>
<h3>After configuration</h3>
<p>By that time you should be good to go. If your relay provider provides their own custom DNS verification
methods (<b>including custom DMARC configurations</b>), feel free to publish them on DNS.</p>
</form>
</div>
<script>
const use_relay = document.getElementById("use_relay")
const relay_host = document.getElementById("relay_host")
const relay_port = document.getElementById("relay_port")
const relay_auth_user = document.getElementById("relay_auth_user")
const relay_auth_pass = document.getElementById("relay_auth_pass")
const relay_authorized_servers = document.getElementById("relay_authorized_servers")
const relay_spf_discover = document.getElementById("smtp_relay_autospf_btn")
const relay_spf_discover_results = document.getElementById("smtp_relay_autospf")
const relay_dkim_sel = document.getElementById("relay_dkim_selector")
const relay_dkim_key = document.getElementById("relay_dkim_key")
function checkfields() {
let relay_enabled = use_relay.checked
relay_host.disabled = !relay_enabled
relay_port.disabled = !relay_enabled
relay_auth_user.disabled = !relay_enabled
relay_auth_pass.disabled = !relay_enabled
relay_authorized_servers.disabled = !relay_enabled
relay_spf_discover.disabled = !relay_enabled
relay_spf_discover_results.innerHTML = ""
relay_dkim_sel.disabled = !relay_enabled
relay_dkim_key.disabled = !relay_enabled
}
function show_smtp_relays() {
api(
"/system/smtp/relay",
"GET",
{},
data => {
use_relay.checked = data.enabled
relay_host.value = data.host
relay_port.value = data.port
relay_auth_user.value = data.user
relay_auth_pass.value = ""
relay_authorized_servers.value = ""
if (data.spf_record) {
relay_authorized_servers.value = data.spf_record
}else if (data.authorized_servers) {
data.authorized_servers.forEach(element => {
relay_authorized_servers.value += `${element} `
});
}
if (data.dkim_selector) {
relay_dkim_sel.value = data.dkim_selector
relay_dkim_key.value = data.dkim_rr
}
checkfields()
}
)
}
function set_smtp_relay_config() {
let relay_configuration = {
enabled: use_relay.checked,
host: relay_host.value,
port: relay_port.value,
user: relay_auth_user.value,
key: relay_auth_pass.value,
dkim_selector: relay_dkim_sel.value,
dkim_rr: relay_dkim_key.value
}
if (relay_authorized_servers.value.substr(0, 7) === "v=spf1 ") {
relay_configuration.spf_record = relay_authorized_servers.value
} else {
relay_configuration.authorized_servers = relay_authorized_servers.value
}
api(
"/system/smtp/relay",
"POST",
relay_configuration,
() => {
show_modal_error("Done!", "The configuration has been updated and Postfix was restarted successfully. Please make sure everything is functioning as intended.", () => {
return false
})
},
(e) => {
show_modal_error("Error!", e, () => { return false })
}
)
}
function add_spf(domain) {
document.getElementById(`smtp_relay_spfinclude_${domain}`).disabled = true
let impl = false
relay_authorized_servers.value.split(/[\s,]+/).forEach(s => {
if (s === `spf:${domain}`) {
impl = true
}
});
if (!impl) {
relay_authorized_servers.value += ` spf:${domain}`
}
}
async function autodetect_spf() {
let btn = $("#smtp_relay_autospf_btn")
let results = $("#smtp_relay_autospf")
let host = relay_host.value
if (host.trim() == "") {
results.html("<b>Error:</b> No hostname specified.")
return
}
let hmatches = host.match(/([^\s.\\\/]+\.)+([^\s.\\\/]+)/)
if (!hmatches || hmatches[0] != host) {
results.html(`<b>Error: <code>${host}</code></b> is not a valid hostname.`)
return
}
btn.html("Working...")
btn.prop("disabled", true)
results.html("")
let base_host = hmatches[hmatches.length - 2] + hmatches[hmatches.length - 1]
function record_html(name, rec) {
if (rec.error) {
return `<b>${name}</b> - ${rec.error} Error (${rec.msg})`
} else {
return `<button id="smtp_relay_spfinclude_${name}" type="button" class="btn btn-secondary" onclick="add_spf('${name}')">Include</button> <b>${name}</b> <code>${rec.msg}</code>`
}
}
let records = []
await Promise.all([
query_spf(base_host).then(rr => { records[0] = record_html(base_host, rr) }),
query_spf(`spf.${base_host}`).then(rr => { records[1] = record_html(`spf.${base_host}`, rr) }),
query_spf(`_spf.${base_host}`).then(rr => { records[2] = record_html(`_spf.${base_host}`, rr) })
])
let txt = "<h4>Here's what I've found:</h4><ul>"
records.forEach((r) => {
txt += `<li>${r}</li>`
})
txt += "</ul><small>Only some common subdomains are tested here, so it's possible I've missed some. You <b>SHOULD NOT</b> include records you do not recognize, else there will be servers that can send mail as you, but that you will not use.</small>"
results.html(txt)
btn.html("Detect SPF Records")
btn.prop("disabled", false)
}
function query_spf(hostname) {
// We use Cloudflare's DNS servers for this (DNS over HTTPS)
return new Promise((resolve, _) => {
$.ajax({
url: "https://cloudflare-dns.com/dns-query",
headers: {
accept: "application/dns-json"
},
method: "GET",
data: {
name: hostname,
type: "TXT",
do: false,
cd: false
},
success: (data) => {
const RRTXT = 16
const status_description = [
// From https://www.iana.org/assignments/dns-parameters/dns-parameters.xhtml#dns-parameters-6
// (last checked: 3rd June 2021)
"Success",
"Format Error",
"Server Failure",
"Non-Existent Domain",
"Not Implemented",
"Query Refused",
"Name exists when it should not",
"RR set exists when it should not",
"RR set that should exist does not",
"Server is not authoritative for zone",
"Not Authorized",
"Name not contained in zone",
"DSO-TYPE Not Implemented"
]
if (data.Status != 0) {
if (data.Status > 11) {
return resolve({ error: "DNS", msg: "Unknown Error" })
} else {
return resolve({ error: "DNS", msg: status_description[data.Status] })
}
}
if (data.Answer) {
data.Answer.forEach(ans => {
if (ans.type == 16 && ans.name == hostname && ans.data.substring(1, 7) == "v=spf1") {
return resolve({ msg: ans.data.substring(1, ans.data.length - 1) })
}
})
}
return resolve({ error: "DNS", msg: "No SPF record found" })
},
error: (_, __, err) => {
resolve({ error: "HTTP", msg: err })
}
})
})
}
</script>

View file

@ -0,0 +1,307 @@
<style>
.code-white>code {
color: #ffffff
}
#ssl_provision_result>div>div {
white-space: pre;
}
#ssl_domains .icon {
min-width: 1.6em;
}
</style>
<h2>TLS (SSL) Certificates</h2>
<p>A TLS (formerly called SSL) certificate is a cryptographic file that proves to anyone connecting to a web address
that the connection is secure between you and the owner of that address.</p>
<p>You need a TLS certificate for this box&rsquo;s hostname ({{hostname}}) and every other domain name and subdomain
that this box is hosting a website for (see the list below).</p>
<div id="ssl_provision">
<h3>Provision certificates</h3>
<div id="ssl_provision_p" style="display: none; margin-top: 1.5em">
<p>A TLS certificate can be automatically provisioned from <a href="https://letsencrypt.org/"
target="_blank">Let&rsquo;s Encrypt</a>, a free TLS certificate provider, for:</p>
<ul class="text-primary"></ul>
<div class="container input-group mt-3" style="overflow-x: auto;">
<button id="ssl_provision_button" class="btn btn-primary"
onclick="return provision_tls_cert();">Provision</button>
<label class="input-group-text" for=""><b>By provisioning the certificates, you&rsquo;re agreeing to the <a
href="https://letsencrypt.org/repository">Let&rsquo;s Encrypt Subscriber Agreement</a>.</b></label>
</div>
</div>
</div>
<div id="ssl_provision_result" class="mt-4"></div>
<h3>Certificate status</h3>
<p style="margin-top: 1.5em">Certificates expire after a period of time. All certificates will be automatically renewed
through <a href="https://letsencrypt.org/" target="_blank">Let&rsquo;s Encrypt</a> 14 days prior to expiration.</p>
<table id="ssl_domains" class="table align-middle col-12" style="margin-bottom: 2em; display: none">
<caption></caption>
<thead>
<tr>
<th>Domain</th>
<th>Certificate Status</th>
<th></th>
</tr>
</thead>
<tbody>
</tbody>
</table>
<h3 id="ssl_install_header">Install certificate</h3>
<p>If you don't want to use our automatic Let's Encrypt integration, you can give any other certificate provider a try.
You can generate the needed CSR below.</p>
<div class="col-lg-10 col-xl-8">
<div class="input-group">
<label class="input-group-text" for="ssldomain">Which domain are you getting a certificate for?</label>
<select id="ssldomain" onchange="show_csr()" class="form-select"></select>
</div>
</div>
<p><small>A multi-domain or wildcard certificate will be automatically applied to any domains it is valid for besides
the one you choose above.</small></p>
<div class="col-lg-10 col-xl-8">
<div class="input-group">
<label class="input-group-text" for="sslcc">What country are you in?</label>
<select id="sslcc" onchange="show_csr()" class="form-select">
<option value="">(Select)</option>
{% for code, name in csr_country_codes %}
<option value="{{code}}">{{name}}</option>
{% endfor %}
</select>
</div>
</div>
<p><small>This is required by some TLS certificate providers. You may just pick any if you know your TLS certificate
provider doesn't require it.</small></p>
<div id="csr_info" style="display: none;">
<p>You will need to provide the certificate provider this Certificate Signing Request (CSR):</p>
<div class="col-lg-10 col-xl-8 mb-3">
<textarea class="form-control font-monospace" id="ssl_csr" style="min-height: 26.5em;" readonly></textarea>
</div>
<small>The CSR is safe to share. It can only be used in combination with a secret key stored on this machine.</small>
<p>The certificate provider will then provide you with a TLS/SSL certificate. They may also provide you with an
intermediate chain. Paste each separately into the boxes below:</p>
<div class="row g-4">
<div class="form-floating col-12 col-xl-6">
<textarea id="ssl_paste_cert" class="form-control" style="font-size:80%; font-family: monospace; height: 20em"
placeholder="-----BEGIN CERTIFICATE-----&#xA;stuff here&#xA;-----END CERTIFICATE-----"></textarea>
<label for="ssl_paste_cert" class="ms-3">TLS/SSL Certificate</label>
</div>
<div class="form-floating col-12 col-xl-6">
<textarea id="ssl_paste_chain" class="form-control" style="font-size:80%; font-family: monospace; height: 20em"
placeholder="-----BEGIN CERTIFICATE-----&#xA;stuff here&#xA;-----END CERTIFICATE-----&#xA;-----BEGIN CERTIFICATE-----&#xA;more stuff here&#xA;-----END CERTIFICATE-----"></textarea>
<label for="ssl_paste_chain" class="ms-3">TLS/SSL intermediate Chain (if provided)</label>
</div>
</div>
<br>
<button class="btn btn-primary" onclick="install_cert()">Install</button>
</div>
<script>
function show_tls(keep_provisioning_shown) {
api(
"/ssl/status",
"GET",
{
},
function (res) {
// provisioning status
$("#ssl_provision_p ul").html("")
if (!keep_provisioning_shown)
$('#ssl_provision').toggle(res.can_provision.length > 0)
$('#ssl_provision_p').toggle(res.can_provision.length > 0);
if (res.can_provision.length > 0) {
let hostlist = $("#ssl_provision_p ul")
res.can_provision.forEach(domain => {
$("#ssl_provision_p ul").append($("<li></li>").text(domain))
});
}
// certificate status
var domains = res.status;
var tb = $('#ssl_domains tbody');
tb.text('');
$('#ssldomain').html('<option value="">(select)</option>');
$('#ssl_domains').show();
for (var i = 0; i < domains.length; i++) {
var row = $("<tr><th class='text-wrap'></th><td class='status'><div class='icon fas'></div><span class='status-text'></span></td> <td class='actions'><a href='#' onclick='return ssl_install(this);' class='btn btn-xs'>Install Certificate</a></td></tr>");
tb.append(row);
row.attr('data-domain', domains[i].domain);
row.find('th').text(domains[i].domain);
if (domains[i].status == "not-applicable") {
row.find('.actions a').remove(); // no actions applicable
}
let statusclass
let faclass
switch (domains[i].status) {
case "success":
statusclass = "status-ok"
faclass = "fa-check"
row.find('.actions a').addClass('btn-default').text('Replace Certificate');
break;
case "warning":
statusclass = "status-warning"
faclass = "fa-exclamation-triangle"
row.find('.actions a').addClass('btn-secondary').text('Install Certificate');
break;
case "danger":
statusclass = "status-error"
faclass = "fa-times"
row.find('.actions a').addClass('btn-secondary').text('Install Certificate');
break;
default:
statusclass = "status-na"
row.find('.actions a').remove()
break;
}
let txt = row.find(".status")
txt.addClass(statusclass);
txt.find(".fas").addClass(faclass)
txt.find("span").text(domains[i].text);
$('#ssldomain').append($('<option>').text(domains[i].domain));
}
});
}
function ssl_install(elem) {
var domain = $(elem).parents('tr').attr('data-domain');
$('#ssldomain').val(domain);
show_csr();
$('html, body').animate({ scrollTop: $('#ssl_install_header').offset().top - $('.navbar-fixed-top').height() - 20 })
return false;
}
function show_csr() {
// Can't show a CSR until both inputs are entered.
if ($('#ssldomain').val() == "") return;
if ($('#sslcc').val() == "") return;
// Scroll to it and fetch.
$('#csr_info').slideDown();
$('#ssl_csr').text('Loading...');
api(
"/ssl/csr/" + $('#ssldomain').val(),
"POST",
{
countrycode: $('#sslcc').val()
},
function (data) {
$('#ssl_csr').text(data);
});
}
function install_cert() {
api(
"/ssl/install",
"POST",
{
domain: $('#ssldomain').val(),
cert: $('#ssl_paste_cert').val(),
chain: $('#ssl_paste_chain').val()
},
function (status) {
if (/^OK($|\n)/.test(status)) {
console.log(status)
show_modal_error("TLS Certificate Installation", "Certificate has been installed. Check that you have no connection problems to the domain.", function () { show_ssl(); $('#csr_info').slideUp(); });
} else {
show_modal_error("TLS Certificate Installation", status);
}
});
}
function provision_tls_cert() {
// Automatically provision any certs.
$('#ssl_provision_p .btn').attr('disabled', '1'); // prevent double-clicks
api(
"/ssl/provision",
"POST",
{},
function (status) {
// Clear last attempt.
$('#ssl_provision_result').html("");
may_reenable_provision_button = true;
// Nothing was done. There might also be problem domains, but we've already displayed those.
if (status.requests.length == 0) {
show_modal_error("TLS Certificate Provisioning", "There were no domain names to provision certificates for.");
// don't return - haven't re-enabled the provision button
}
// Each provisioning API call returns zero or more "requests" which represent
// a request to Let's Encrypt for a single certificate. Normally there is just
// one request (for a single multi-domain certificate).
let global_logs = []
for (var i = 0; i < status.requests.length; i++) {
var r = status.requests[i];
if (r.result == "skipped") {
// not interested --- this domain wasn't in the table
// to begin with
continue;
}
// plain log line
if (typeof r === "string") {
if (r !== "") {
global_logs.append(r)
}
continue;
}
let n = $("<div class=\"card mb-3\"><h5 class=\"card-header text-white code-white\"></h5><div class=\"card-body bg-light small\"></div></div>");
$('#ssl_provision_result').append(n);
if (status.requests.length > 0) {
n.find(".card-header").html(`Logs for ${r.domains.map((h) => { return `<code>${h}</code>` }).join(", ")}`);
}
if (r.result == "error") {
n.find(".card-header").addClass("bg-danger")
} else if (r.result == "installed") {
n.find(".card-header").addClass("bg-success")
setTimeout("show_tls(true)", 1);
}
// display the detailed log info in case of problems
let logs = r.log.join("\n")
if (logs === "" || logs === null || logs === undefined) {
logs = "Something wrong happened, sorry!"
}
n.find("div").text(logs);
}
if (global_logs.length !== 0) {
let card = $("<div class=\"card mb-3\"><div class=\"card-body small\"></div></div>")
$('#ssl_provision_result').append(card);
n.find(".card-body").text(global_logs.join("\n"))
}
if (may_reenable_provision_button) {
$('#ssl_provision_p .btn').removeAttr("disabled");
}
});
}
</script>

View file

@ -1,47 +1,95 @@
<div class="container">
<div>
<h2>Contacts &amp; Calendar Synchronization</h2>
<p>This box can hold your contacts and calendar, just like it holds your email.</p>
<hr>
<div class="row">
<div class="col-sm-6">
<div class="row d-flex">
<div class="col-lg-6">
<h4>In your browser</h4>
<p>You can edit your contacts and calendar from your web browser.</p>
<table class="table">
<thead><tr><th>For...</th> <th>Visit this URL</th></tr></thead>
<tr><td>Contacts</td> <td><a href="https://{{hostname}}/cloud/contacts">https://{{hostname}}/cloud/contacts</a></td></tr>
<tr><td>Calendar</td> <td><a href="https://{{hostname}}/cloud/calendar">https://{{hostname}}/cloud/calendar</a></td></tr>
<caption></caption>
<thead>
<tr>
<th>For...</th>
<th>Visit this URL</th>
</tr>
</thead>
<tr>
<th>Contacts</td>
<td><a href="https://{{hostname}}/cloud/contacts">https://{{hostname}}/cloud/contacts</a></td>
</tr>
<tr>
<th>Calendar</td>
<td><a href="https://{{hostname}}/cloud/calendar">https://{{hostname}}/cloud/calendar</a></td>
</tr>
</table>
<p>Log in settings are the same as with <a href="#mail-guide" onclick="return show_panel(this);">mail</a>: your
complete email address and your mail password.</p>
<p>Log in settings are the same as with <a href="#mail-guide" onclick="return show_panel(this);">mail</a>:
your
complete email address and your mail password.</p>
</div>
<div class="col-sm-6">
<div class="col-lg-6">
<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,
your contacts and calendar may already appear on your device.</p>
<p>If you set up your <a href="#mail-guide" onclick="return show_panel(this);">mail</a> using
Exchange/ActiveSync,
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>
<table class="table">
<thead><tr><th>For...</th> <th>Use...</th></tr></thead>
<tr><td>Contacts</td> <td><a href="https://play.google.com/store/apps/details?id=org.dmfs.carddav.sync">CardDAV-Sync free beta</a> (free)</td></tr>
<tr><td>Calendar</td> <td><a href="https://play.google.com/store/apps/details?id=org.dmfs.caldav.lib">CalDAV-Sync</a> ($2.89)</td></tr>
<caption></caption>
<thead>
<tr>
<th>For...</th>
<th>Use...</th>
</tr>
</thead>
<tr>
<td>Contacts and Calendar</td>
<td><a href="https://play.google.com/store/apps/details?id=at.bitfire.davdroid">DAVx⁵</a> ($5.99;
free <a href="https://f-droid.org/packages/at.bitfire.davdroid/">here</a>)</td>
</tr>
<tr>
<td>Only Contacts</td>
<td><a href="https://play.google.com/store/apps/details?id=org.dmfs.carddav.sync">CardDAV-Sync
free</a> (free)</td>
</tr>
<tr>
<td>Only Calendar</td>
<td><a href="https://play.google.com/store/apps/details?id=org.dmfs.caldav.lib">CalDAV-Sync</a>
($2.99)</td>
</tr>
</table>
<p>Use the following settings:</p>
<table class="table">
<tr><td>Account Type</td> <td>CardDAV or CalDAV</td></tr>
<tr><td>Server Name</td> <td>{{hostname}}</td></tr>
<tr><td>Use SSL</td> <td>Yes</td></tr>
<tr><td>Username</td> <td>Your complete email address.</td></tr>
<tr><td>Password</td> <td>Your mail password.</td></tr>
<tr>
<td>Account Type</td>
<td>CardDAV or CalDAV</td>
</tr>
<tr>
<td>Server Name</td>
<td>{{hostname}}</td>
</tr>
<tr>
<td>Use SSL</td>
<td>Yes</td>
</tr>
<tr>
<td>Username</td>
<td>Your complete email address.</td>
</tr>
<tr>
<td>Password</td>
<td>Your mail password.</td>
</tr>
</table>
</div>
</div>

View file

@ -1,78 +1,416 @@
<style>
#backup-status th { text-align: center; }
#backup-status tr.full-backup td { font-weight: bold; }
#backup-status tr.full-backup td {
font-weight: bold;
}
</style>
<h2>Backup Status</h2>
<h3>Copying Backup Files</h3>
<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. The backup is stored on the machine itself. You are responsible for copying the backup files off of the machine.</p>
<h3>Configuration</h3>
<p>Many cloud providers make this easy by allowing you to take snapshots of the machine's disk.</p>
<form class="form-horizontal" role="form" onsubmit="set_custom_backup(); return false;">
<div class="col-lg-10 col-xl-8">
<div class="input-group">
<label class="input-group-text" for="backup-target-type">Backup to</label>
<select class="form-select" rows="1" id="backup-target-type" onchange="toggle_form()">
<option value="off">Nowhere (Disable Backups)</option>
<option value="local">{{hostname}}</option>
<option value="rsync">rsync</option>
<option value="s3">S3 (Amazon or compatible) </option>
<option value="b2">Backblaze B2</option>
</select>
</div>
</div>
<p>You can also use SFTP (FTP over SSH) to copy files from <tt id="backup-location"></tt>. These files are encrpyted, so they are safe to store anywhere. Copy the encryption password from <tt id="backup-encpassword-file"></tt> also but keep it in a safe location.</p>
<!-- LOCAL BACKUP -->
<div class="form-group backup-target-local">
<div class="col-lg-10 col-xl-8 mb-3">
<p>Backups are stored on this machine&rsquo;s own hard disk. You are responsible for periodically using SFTP (FTP
over SSH) to copy the backup files from <tt class="backup-location"></tt> to a safe location. These files are
encrypted, so they are safe to store anywhere.</p>
<p>Separately copy the encryption password from <tt class="backup-encpassword-file"></tt> to a safe and secure
location. You will need this file to decrypt backup files.</p>
</div>
</div>
<!-- RSYNC BACKUP -->
<div class="form-group backup-target-rsync">
<div class="col-lg-10 col-xl-8 mb-3">
<h3>Current Backups</h3>
<p>Backups synced to a remote machine using rsync over SSH, with local
copies in <tt class="backup-location"></tt>. These files are encrypted, so
they are safe to store anywhere.</p>
<p>Separately copy the encryption
password from <tt class="backup-encpassword-file"></tt> to a safe and
secure location. You will need this file to decrypt backup files.</p>
<p>The backup directory currently contains the backups listed below. The total size on disk of the backups is <span id="backup-total-size"></span>.</p>
</div>
</div>
<div class="col-lg-10 col-xl-8 mb-3 backup-target-rsync">
<div class="input-group">
<label class="input-group-text">Where to?</label>
<input type="text" class="form-control" id="backup-target-rsync-host" placeholder="hostname.local">
<label class="input-group-text">:</label>
<div style="width: 20%">
<input type="number" class="form-control" id="backup-target-rsync-port" placeholder="Port (22)">
</div>
</div>
</div>
<div class="col-lg-10 col-xl-8 mb-3 backup-target-rsync">
<div class="input-group">
<label for="backup-target-rsync-path" class="input-group-text">Path</label>
<input type="text" placeholder="/backups/{{hostname}}" class="form-control" id="backup-target-rsync-path">
</div>
</div>
<div class="col-lg-10 col-xl-8 mb-3 backup-target-rsync">
<div class="input-group">
<label for="backup-target-rsync-path" class="input-group-text">Username</label>
<input type="text" class="form-control" id="backup-target-rsync-user">
</div>
</div>
<div class="col-lg-10 col-xl-8 mb-3 backup-target-rsync">
<label for="ssh-pub-key" class="col-sm-2 control-label">
<h4>Public SSH Key</h4>
</label>
<textarea class="form-control font-monospace" id="ssh-pub-key" style="min-height: 12em;" readonly></textarea>
<div class="small" style="margin-top: 2px">
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
passwordless authentication from your mail-in-a-box server and your backup server.
</div>
</div>
<!-- S3 BACKUP -->
<div class="col-lg-10 col-xl-8 mb-3 backup-target-s3">
<p>Backups are stored in an S3-compatible bucket. You must have an AWS or other S3 service account already.</p>
<p>You MUST manually copy the encryption password from <tt class="backup-encpassword-file"></tt> to a safe and
secure location. You will need this file to decrypt backup files. It is <b>NOT</b> stored in your S3 bucket.</p>
</div>
<div class="col-lg-10 col-xl-8 mb-3 backup-target-s3">
<div class="input-group">
<label for="backup-target-s3-host-select" class="input-group-text">S3 Region</label>
<select class="form-select" id="backup-target-s3-host-select">
{% for name, host in backup_s3_hosts %}
<option value="{{host}}">{{name}}</option>
{% endfor %}
<option value="other">Other (non AWS)</option>
</select>
</div>
</div>
<div class="col-lg-10 col-xl-8 mb-3 backup-target-s3">
<div class="input-group">
<label for="backup-target-s3-host" class="input-group-text">S3 Host / Endpoint</label>
<input type="text" placeholder="Endpoint" class="form-control" id="backup-target-s3-host">
</div>
</div>
<div class="col-lg-10 col-xl-8 mb-3 backup-target-s3">
<div class="input-group">
<label for="backup-target-s3-path" class="input-group-text">S3 Path</label>
<input type="text" placeholder="your-bucket-name/backup-directory" class="form-control"
id="backup-target-s3-path">
</div>
</div>
<div class="col-lg-10 col-xl-8 mb-3 backup-target-s3">
<div class="input-group">
<label for="backup-target-user" class="input-group-text">S3 Access Key</label>
<input type="text" class="form-control" id="backup-target-user">
</div>
</div>
<div class="col-lg-10 col-xl-8 mb-3 backup-target-s3">
<div class="input-group">
<label for="backup-target-pass" class="input-group-text">S3 Secret Access Key</label>
<input type="text" class="form-control" id="backup-target-pass">
</div>
</div>
<!-- Backblaze -->
<div class="col-lg-10 col-xl-8 mb-3 backup-target-b2">
<p>Backups are stored in a <a href="https://www.backblaze.com/" target="_blank" rel="noreferrer">Backblaze</a> B2
bucket. You must have a Backblaze account already.</p>
<p>You MUST manually copy the encryption password from <tt class="backup-encpassword-file"></tt> to a safe and
secure location. You will need this file to decrypt backup files. It is NOT stored in your Backblaze B2 bucket.
</p>
</div>
<div class="col-lg-10 col-xl-8 mb-3 backup-target-b2">
<div class="input-group">
<label for="backup-target-b2-user" class="input-group-text">B2 Application KeyID</label>
<input type="text" class="form-control" id="backup-target-b2-user">
</div>
</div>
<div class="col-lg-10 col-xl-8 mb-3 backup-target-b2">
<div class="input-group">
<label for="backup-target-b2-pass" class="input-group-text">B2 Application Key</label>
<input type="text" class="form-control" id="backup-target-b2-pass">
</div>
</div>
<div class="col-lg-10 col-xl-8 mb-3 backup-target-b2">
<div class="input-group">
<label for="backup-target-b2-bucket" class="input-group-text">B2 Bucket</label>
<input type="text" class="form-control" id="backup-target-b2-bucket">
</div>
</div>
<!-- Common -->
<div class="backup-target-local backup-target-rsync backup-target-s3 backup-target-b2">
<hr>
<div class="col-lg-10 col-xl-8">
<div class="input-group">
<label class="input-group-text" for="backup-min-age">Retention Days</label>
<input type="number" class="form-control" id="backup-min-age">
</div>
<div class="small" style="margin-top: 2px">This is the minimum time backup data is kept for. The box makes an
incremental backup most nights, which requires that previous backups back to the most recent full backup be
preserved, so backup data is often kept much longer than this setting. Full backups are made periodically when
the incremental backup data size exceeds a limit.</div>
</div>
</div>
<div class="mt-3">
<button id="set-s3-backup-button" type="submit" class="btn btn-primary">Save</button>
</div>
</form>
<table id="backup-status" class="table" style="width: auto">
<thead>
<th colspan="2">When</th>
<th>Type</th>
<th>Size</th>
<th>Deleted in...</th>
</thead>
<tbody>
</tbody>
<h3>Available backups</h3>
<p>The backup location currently contains the backups listed below. The total size of the backups is currently <span
id="backup-total-size"></span>.</p>
<table id="backup-status" class="table align-middle col-12">
<caption></caption>
<thead>
<th colspan="2">When</th>
<th>Type</th>
<th>Size</th>
<th>Deleted in...</th>
</thead>
<tbody>
</tbody>
</table>
<p><small>The size column in the table indicates the size of the encrpyted backup, but the total size on disk shown above includes storage for unencrpyted intermediate files.</small></p>
<!-- Hide these buttons until we're sure we can use them :) -->
<div class="row justify-content-evenly">
<button id="create-full-backup-button" class="btn btn-primary col-3" onclick="do_backup(true)"
style="display: none;">Create Full Backup Now</button>
<button id="create-incremental-backup-button" class="btn btn-primary col-3" onclick="do_backup(false)"
style="display: none;">Create Incremental Backup Now</button>
</div>
<script>
function nice_size(bytes) {
var powers = ['bytes', 'KB', 'MB', 'GB', 'TB'];
while (true) {
if (powers.length == 1) break;
if (bytes < 1000) break;
bytes /= 1024;
powers.shift();
}
return (Math.round(bytes*10)/10) + " " + powers[0];
}
function show_system_backup() {
$('#backup-status tbody').html("<tr><td colspan='2' class='text-muted'>Loading...</td></tr>")
api(
"/system/backup/status",
"GET",
{ },
function(r) {
$('#backup-location').text(r.encdirectory);
$('#backup-encpassword-file').text(r.encpwfile);
function toggle_form() {
var target_type = $("#backup-target-type").val();
$(".backup-target-local, .backup-target-rsync, .backup-target-s3, .backup-target-b2").hide();
$(".backup-target-" + target_type).show();
$('#backup-status tbody').html("");
var total_disk_size = 0;
for (var i = 0; i < r.backups.length; i++) {
var b = r.backups[i];
var tr = $('<tr/>');
if (b.full) tr.addClass("full-backup");
tr.append( $('<td/>').text(b.date_str + " " + r.tz) );
tr.append( $('<td/>').text(b.date_delta + " ago") );
tr.append( $('<td/>').text(b.full ? "full" : "increment") );
tr.append( $('<td style="text-align: right"/>').text( nice_size(b.encsize)) );
if (b.deleted_in)
tr.append( $('<td/>').text(b.deleted_in) );
else
tr.append( $('<td class="text-muted">n/a</td>') );
$('#backup-status tbody').append(tr);
init_inputs(target_type);
}
total_disk_size += b.size;
total_disk_size += b.encsize;
}
function nice_size(bytes) {
var powers = ['bytes', 'KB', 'MB', 'GB', 'TB'];
while (true) {
if (powers.length == 1) break;
if (bytes < 1000) break;
bytes /= 1024;
powers.shift();
}
// round to have three significant figures but at most one decimal place
if (bytes >= 100)
bytes = Math.round(bytes)
else
bytes = Math.round(bytes * 10) / 10;
return bytes + " " + powers[0];
}
function show_system_backup() {
show_custom_backup()
$('#backup-status tbody').html("<tr><td colspan='5' class='text-muted'>Loading...</td></tr>")
api(
"/system/backup/status",
"GET",
{},
function (r) {
if (r.error) {
show_modal_error("Backup Error", $("<pre/>").text(r.error));
return;
}
$('#backup-status tbody').html("");
var total_disk_size = 0;
if (typeof r.backups == "undefined") {
var tr = $('<tr><td colspan="5">Backups are turned off.</td></tr>');
$('#backup-status tbody').append(tr);
$('#create-full-backup-button').css("display", "none")
$('#create-incremental-backup-button').css("display", "none")
return;
} else if (r.backups.length == 0) {
var tr = $('<tr><td colspan="5">No backups have been made yet.</td></tr>');
$('#backup-status tbody').append(tr);
}
// Backups ARE enabled.
$('#create-full-backup-button').css("display", "unset")
$('#create-incremental-backup-button').css("display", "unset")
for (var i = 0; i < r.backups.length; i++) {
var b = r.backups[i];
var tr = $('<tr/>');
if (b.full) tr.addClass("full-backup");
tr.append($('<td/>').text(b.date_str));
tr.append($('<td/>').text(b.date_delta + " ago"));
tr.append($('<td/>').text(b.full ? "full" : "increment"));
tr.append($('<td/>').text(nice_size(b.size)));
if (b.deleted_in)
tr.append($('<td/>').text(b.deleted_in));
else
tr.append($('<td class="text-muted">unknown</td>'));
$('#backup-status tbody').append(tr);
total_disk_size += b.size;
}
total_disk_size += r.unmatched_file_size;
$('#backup-total-size').text(nice_size(total_disk_size));
})
}
function show_custom_backup() {
$(".backup-target-local, .backup-target-rsync, .backup-target-s3, .backup-target-b2").hide();
api(
"/system/backup/config",
"GET",
{},
function (r) {
$("#backup-target-user").val(r.target_user);
$("#backup-target-pass").val(r.target_pass);
$("#backup-min-age").val(r.min_age_in_days);
$(".backup-location").text(r.file_target_directory);
$(".backup-encpassword-file").text(r.enc_pw_file);
$("#ssh-pub-key").val(r.ssh_pub_key);
if (r.target == "file://" + r.file_target_directory) {
$("#backup-target-type").val("local");
} else if (r.target == "off") {
$("#backup-target-type").val("off");
} else if (r.target.substring(0, 8) == "rsync://") {
$("#backup-target-type").val("rsync");
let uri = r.target.substring(8)
let i = uri.indexOf("/")
let path = [uri.slice(0, i), uri.slice(i + 1)];
let host_parts = path.shift().split('@');
$("#backup-target-rsync-user").val(host_parts[0]);
$("#backup-target-rsync-host").val(host_parts[1]);
$("#backup-target-rsync-path").val(path[0]);
$("#backup-target-rsync-port").val(r.target_rsync_port)
} else if (r.target.substring(0, 5) == "s3://") {
$("#backup-target-type").val("s3");
var hostpath = r.target.substring(5).split('/');
var host = hostpath.shift();
let s3_options = $("#backup-target-s3-host-select option").map(function() {return this.value}).get()
$("#backup-target-s3-host-select").val("other")
for (let h of s3_options) {
console.log(h)
if (h == host) {
$("#backup-target-s3-host-select").val(host)
break
}
}
$("#backup-target-s3-host").val(host);
$("#backup-target-s3-path").val(hostpath.join('/'));
} else if (r.target.substring(0, 5) == "b2://") {
$("#backup-target-type").val("b2");
var targetPath = r.target.substring(5);
var b2_application_keyid = targetPath.split(':')[0];
var b2_applicationkey = targetPath.split(':')[1].split('@')[0];
var b2_bucket = targetPath.split('@')[1];
$("#backup-target-b2-user").val(b2_application_keyid);
$("#backup-target-b2-pass").val(b2_applicationkey);
$("#backup-target-b2-bucket").val(b2_bucket);
}
toggle_form()
})
}
function set_custom_backup() {
var target_type = $("#backup-target-type").val();
var target_user = $("#backup-target-user").val();
var target_pass = $("#backup-target-pass").val();
let target_port = $("#backup-target-rsync-port").val();
var target;
if (target_type == "local" || target_type == "off")
target = target_type;
else if (target_type == "s3")
target = "s3://" + $("#backup-target-s3-host").val() + "/" + $("#backup-target-s3-path").val();
else if (target_type == "rsync") {
target = "rsync://" + $("#backup-target-rsync-user").val() + "@" + $("#backup-target-rsync-host").val()
+ "/" + $("#backup-target-rsync-path").val();
target_user = '';
} else if (target_type == "b2") {
target = 'b2://' + $('#backup-target-b2-user').val() + ':' + $('#backup-target-b2-pass').val()
+ '@' + $('#backup-target-b2-bucket').val()
target_user = '';
target_pass = '';
}
var min_age = $("#backup-min-age").val();
api(
"/system/backup/config",
"POST",
{
target: target,
target_user: target_user,
target_pass: target_pass,
target_rsync_port: target_port,
min_age: min_age
},
function (r) {
// use .text() --- it's a text response, not html
show_modal_error("Backup configuration", $("<p/>").text(r), function () { if (r == "OK") show_system_backup(); }); // refresh after modal on success
},
function (r) {
// use .text() --- it's a text response, not html
show_modal_error("Backup configuration", $("<p/>").text(r));
});
return false;
}
function init_inputs(target_type) {
function set_host(host, overwrite_other) {
if (host !== 'other') {
$("#backup-target-s3-host").val(host);
} else if (overwrite_other) {
$("#backup-target-s3-host").val('');
}
}
if (target_type == "s3") {
$('#backup-target-s3-host-select').off('change').on('change', function () {
set_host($('#backup-target-s3-host-select').val(), true);
});
set_host($('#backup-target-s3-host-select').val(), false);
}
}
function do_backup(is_full) {
let disclaimer = "The backup process will pause some services (such as PHP, Postfix and Dovecot). Depending on the size of the data this can take a while."
if (!is_full) {
disclaimer += "\nDepending on the amount of incremental backups done after the last full backup, the box may decide to do a full backup instead."
}
show_modal_confirm("Warning!", disclaimer, "Start Backup", () => {
api(
"/system/backup/new",
"POST",
{
full: is_full
},
function (r) {
// use .text() --- it's a text response, not html
show_modal_error("Backup configuration", $("<p/>").text(r), function () { if (r == "OK") show_system_backup(); }); // refresh after modal on success
},
function (r) {
// use .text() --- it's a text response, not html
show_modal_error("Backup configuration", $("<p/>").text(r));
});
return false;
})
}
$('#backup-total-size').text(nice_size(total_disk_size));
})
}
</script>

View file

@ -1,118 +0,0 @@
<style>
#external_dns_settings .heading td {
font-weight: bold;
font-size: 120%;
padding-top: 1.5em;
}
#external_dns_settings .heading.first td {
border-top: none;
padding-top: 0;
}
#external_dns_settings .values td {
border: 0;
padding-top: .75em;
padding-bottom: 0;
max-width: 50vw;
word-wrap: break-word;
}
#external_dns_settings .explanation td {
border: 0;
padding-top: .5em;
padding-bottom: .75em;
font-style: italic;
font-size: 95%;
color: #777;
}
</style>
<h2>DNS (Advanced)</h2>
<p class="text-danger">Custom and external DNS are for advanced configurations.</p>
<h3>Custom DNS</h3>
<p>It is possible to set custom DNS records on domains hosted here. For instance, you can create your own dynamic DNS service. To do so, you will need to call your box&rsquo;s DNS API.</p>
<h4>The HTTP POST request</h4>
<p>Send a POST request like this:</p>
<pre>curl -d "" --user {email}:{password} https://{{hostname}}/admin/dns/set/<b>qname</b>[/<b>rtype</b>[/<b>value</b>]]</pre>
<table class="table">
<thead><th>Parameter</th> <th>Value</th></thead>
<tr><td>email</td> <td>The email address of any administrative user here.</td></tr>
<tr><td>password</td> <td>That user&rsquo;s password.</td></tr>
<tr><td>qname</td> <td>The fully qualified domain name for the record you are trying to set.</td></tr>
<tr><td>rtype</td> <td>The resource type. <code>A</code> if omitted. Possible values: <code>A</code> (an IPv4 address), <code>AAAA</code> (an IPv6 address), <code>TXT</code> (a text string), or <code>CNAME</code> (an alias, which is a fully qualified domain name).</td></tr>
<tr><td>value</td> <td>The new record&rsquo;s value. If omitted, the IPv4 address of the remote host is used. This is handy for dynamic DNS! To delete a record, use &ldquo;__delete__&rdquo;.</td></tr>
</table>
<p style="margin-top: 1em">Note that <code>-d ""</code> is merely to ensure curl sends a POST request. You do not need to put anything inside the quotes. You can also pass the value using typical form encoding in the POST body.</p>
<h4>Examples:</h4>
<pre># sets laptop.mydomain.com to point to the IP address of the machine you are executing curl on
curl -d "" --user me@mydomain.com:###### https://{{hostname}}/admin/dns/set/laptop.mydomain.com
# sets an alias
curl -d "" --user me@mydomain.com:###### https://{{hostname}}/admin/dns/set/foo.mydomain.com/cname/bar.mydomain.com
# clears the alias
curl -d "" --user me@mydomain.com:###### https://{{hostname}}/admin/dns/set/bar.mydomain.com/cname/__delete__
# sets a TXT record using the alternate value syntax
curl -d "value=something%20here" --user me@mydomain.com:###### https://{{hostname}}/admin/dns/set/foo.mydomain.com/txt
</pre>
<h3>External DNS</h3>
<p>Although your box is configured to serve its own DNS, it is possible to host your DNS elsewhere.</p>
<p>If you do so, you are responsible for keeping your DNS entries up to date! If you previously enabled DNSSEC on your domain name by setting a DS record at your registrar, you will likely have to turn it off before changing nameservers.</p>
<p>Enter the following DNS entries at your DNS provider:</p>
<table id="external_dns_settings" class="table">
<thead>
<tr>
<th>QName</th>
<th>Type</th>
<th>Value</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
<script>
function show_system_dns() {
$('#external_dns_settings tbody').html("<tr><td colspan='2' class='text-muted'>Loading...</td></tr>")
api(
"/dns/dump",
"GET",
{ },
function(zones) {
$('#external_dns_settings tbody').html("");
for (var j = 0; j < zones.length; j++) {
var h = $("<tr class='heading'><td colspan='3'></td></tr>");
h.find("td").text(zones[j][0]);
$('#external_dns_settings tbody').append(h);
var r = zones[j][1];
for (var i = 0; i < r.length; i++) {
var n = $("<tr class='values'><td class='qname'/><td class='rtype'/><td class='value'/></tr>");
n.find('.qname').text(r[i].qname);
n.find('.rtype').text(r[i].rtype);
n.find('.value').text(r[i].value);
$('#external_dns_settings tbody').append(n);
var n = $("<tr class='explanation'><td colspan='3'/></tr>");
n.find('td').text(r[i].explanation);
$('#external_dns_settings tbody').append(n);
}
}
})
}
</script>

View file

@ -1,79 +1,218 @@
<h2>System Status Checks</h2>
<style>
#system-checks .heading td {
font-weight: bold;
font-size: 120%;
padding-top: 1.5em;
}
#system-checks .heading.first td {
border-top: none;
padding-top: 0;
}
#system-checks .error td {
color: #733;
}
#system-checks .ok td {
color: #030;
}
#system-checks div.extra {
display: none;
margin-top: 1em;
max-width: 50em;
word-wrap: break-word;
}
#system-checks a.showhide {
display: none;
font-size: 85%;
}
#system-checks .pre {
margin: 1em;
font-family: monospace;
white-space: pre-wrap;
}
#system-checks .message {
display: inline;
}
#system-checks .icon {
min-width: 2em;
}
#system-checks .heading {
font-weight: bold;
font-size: 180%;
padding-top: 1.75em;
}
#system-checks .heading.first {
border-top: none;
padding-top: 0;
}
.status-error .fas {
color: rgb(190, 0, 0);
}
.status-error.sym {
color: rgb(190, 0, 0);
}
.status-error .status-text {
color: rgb(70, 0, 0);
}
.status-warning .fas {
color: rgb(191, 150, 0);
}
.status-warning.sym {
color: rgb(191, 150, 0);
}
.status-warning .status-text {
color: rgb(69, 54, 0);
}
.status-ok .fas {
color: rgb(0, 190, 0);
}
.status-ok.sym {
color: rgb(0, 190, 0);
}
.status-ok .status-text {
color: rgb(0, 70, 0);
}
.status-na .fas {
color: rgb(100, 100, 100);
}
.status-na.sym {
color: rgb(100, 100, 100);
}
.status-na .status-text {
color: rgb(100, 100, 100);
}
div.extra {
display: none;
margin-top: 1em;
word-wrap: break-word;
}
#system-checks .showhide {
display: block;
font-size: 85%;
}
#system-checks .pre {
margin: 1em;
font-family: monospace;
white-space: pre-wrap;
}
</style>
<table id="system-checks" class="table" style="max-width: 60em">
<thead>
</thead>
<tbody>
</tbody>
</table>
<div id="system-reboot-required" style="display: none; margin-bottom: 1em;">
<button type="button" class="btn btn-danger" onclick="confirm_reboot(); return false;">Reboot Box</button>
<div>No reboot is necessary.</div>
</div>
<div id="system-privacy-setting" style="display: none">
<div><a onclick="return enable_privacy(!current_privacy_setting)" href="#"><span>Enable/Disable</span>
New-Version Check</a></div>
<p style="line-height: 125%"><small>(When enabled, status checks phone-home to check for a new release of
Mail-in-a-Box.)</small></p>
</div>
<div id="system-checks">
</div>
<script>
function show_system_status() {
$('#system-checks tbody').html("<tr><td colspan='2' class='text-muted'>Loading...</td></tr>")
api(
"/system/status",
"POST",
{ },
function(r) {
$('#system-checks tbody').html("");
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>");
if (i == 0) n.addClass('first')
n.addClass(r[i].type)
if (r[i].type == "ok") n.find('td.status').text("✓")
if (r[i].type == "error") n.find('td.status').text("✖")
n.find('td.message p').text(r[i].text)
$('#system-checks tbody').append(n);
function show_system_status() {
$('#system-checks').html("")
if (r[i].extra.length > 0) {
n.find('a.showhide').show().text("show more").click(function() {
$(this).hide();
$(this).parent().find('.extra').fadeIn();
return false;
});
}
api(
"/system/privacy",
"GET",
{},
function (r) {
current_privacy_setting = r;
$('#system-privacy-setting').show();
$('#system-privacy-setting a span').text(r ? "Enable" : "Disable");
$('#system-privacy-setting p').toggle(r);
});
for (var j = 0; j < r[i].extra.length; j++) {
api(
"/system/reboot",
"GET",
{},
function (r) {
$('#system-reboot-required').show(); // show when r becomes available
$('#system-reboot-required').find('button').toggle(r);
$('#system-reboot-required').find('div').toggle(!r);
});
var m = $("<div/>").text(r[i].extra[j].text)
if (r[i].extra[j].monospace)
m.addClass("pre");
n.find('> td.message > div').append(m);
}
}
})
}
</script>
api(
"/system/status",
"POST",
{},
function (r) {
for (let i = 0; i < r.length; i++) {
let n = $("<div class='col-12'><div class='icon'></div><p class='message status-text' style='margin: 0'/>");
if (i == 0) n.addClass('first')
if (r[i].type == "heading") {
n.addClass(r[i].type)
} else {
n.addClass("status-" + r[i].type)
n.find(".icon").addClass("fas")
}
if (r[i].type == "ok") n.find(".icon").addClass("fa-check")
if (r[i].type == "error") n.find(".icon").addClass("fa-times")
if (r[i].type == "warning") n.find(".icon").addClass("fa-exclamation-triangle")
n.find('p.status-text').text(r[i].text)
if (r[i].extra.length > 0) {
let open = false
n.append("<p class='showhide btn btn-light mt-3' href='#'>Show More</p><div class='extra ps-4 col-12'></div>")
n.find('.showhide').click(function () {
let extra = $(this).parent().find('.extra')
if (open) {
$(this).html("Show More");
extra.fadeOut();
} else {
$(this).html("Show Less");
extra.fadeIn();
}
open = !open
return false;
});
}
for (var j = 0; j < r[i].extra.length; j++) {
var m = $("<div/>").text(r[i].extra[j].text)
if (r[i].extra[j].monospace)
m.addClass("pre");
n.find('.extra').append(m);
}
$('#system-checks').append(n);
if (i < r.length - 1) {
$('#system-checks').append($("<hr>"));
}
}
})
}
var current_privacy_setting = null;
function enable_privacy(status) {
api(
"/system/privacy",
"POST",
{
value: (status ? "private" : "off")
},
function (res) {
show_system_status();
});
return false; // disable link
}
function confirm_reboot() {
show_modal_confirm(
"Reboot",
$("<p>This will reboot your Mail-in-a-Box <code>{{hostname}}</code>.</p> <p>Until the machine is fully restarted, your users will not be able to send and receive email, and you will not be able to connect to this control panel or with SSH. The reboot cannot be cancelled.</p>"),
"Reboot Now",
function () {
api(
"/system/reboot",
"POST",
{},
function (r) {
var msg = "<p>Please reload this page after a minute or so.</p>";
if (r) msg = "<p>The reboot command said:</p> <pre>" + $("<pre/>").text(r).html() + "</pre>"; // successful reboots don't produce any output; the output must be HTML-escaped
show_modal_error("Reboot", msg);
});
});
}
</script>

View file

@ -1,227 +1,453 @@
<h2>Users</h2>
<style>
#user_table tr.account_inactive td .address { color: #888; text-decoration: line-through; }
#user_table .aliases { margin-top: .33em; font-size: 95%; }
#user_table .aliases div:before { content: "⇖ "; }
#user_table .aliases div { }
#user_table .actions { margin-top: .33em; font-size: 95%; }
#user_table .account_inactive .if_active { display: none; }
#user_table .account_active .if_inactive { display: none; }
#user_table tr.account_inactive td.address {
color: #888;
text-decoration: line-through;
}
#user_table .actions {
margin-top: .33em;
font-size: 95%;
}
#user_table .account_inactive .if_active {
display: none;
}
#user_table .account_active .if_inactive {
display: none;
}
#user_table .account_active.if_inactive {
display: none;
}
</style>
<h3>Add a mail user</h3>
<p>Add an email address to this system. This will create a new login username/password. (Use <a href="javascript:show_panel('aliases')">aliases</a> to create email addresses that forward to existing accounts.)</p>
<p>Add an email address to this system. This will create a new login username/password.</p>
<form class="form-inline" role="form" onsubmit="return do_add_user(); return false;">
<div class="form-group">
<label class="sr-only" for="adduserEmail">Email address</label>
<input type="email" class="form-control" id="adduserEmail" placeholder="Email Address">
</div>
<div class="form-group">
<label class="sr-only" for="adduserPassword">Password</label>
<input type="password" class="form-control" id="adduserPassword" placeholder="Password">
</div>
<div class="form-group">
<select class="form-control" id="adduserPrivs">
<option value="">Normal User</option>
<option value="admin">Administrator</option>
</select>
</div>
<button type="submit" class="btn btn-primary">Add User</button>
<form role="form" onsubmit="return do_add_user(); return false;">
<div class="mb-3">
<div class="row g-3">
<div class="col-12 col-lg-6">
<div class="input-group">
<label class="input-group-text" for="adduserEmail">Email address</label>
<input type="email" class="form-control" style="min-width: 15em;" id="adduserEmail"
placeholder="me@example.com">
</div>
</div>
<div class="col-12 col-lg-6">
<div class="input-group">
<label class="input-group-text" for="adduserPassword">Password</label>
<input type="password" class="form-control" style="min-width: 10em;" id="adduserPassword"
placeholder="Password">
</div>
</div>
</div>
</div>
<div class="col-md-8 col-lg-6 mb-3">
<div class="input-group">
<label class="input-group-text" for="adduserPrivs">User Type</label>
<select class="form-select" id="adduserPrivs">
<option value="">Normal User</option>
<option value="admin">Administrator</option>
</select>
</div>
</div>
<div class="col-md-8 col-lg-6 mb-3">
<div class="input-group">
<label class="input-group-text" for="adduserQuota">Storage Quota</label>
<input type="text" class="form-control" id="adduserQuota" placeholder="0">
</div>
</div>
<ul style="margin-top: 1em; padding-left: 1.5em; font-size: 90%;">
<li>Passwords must be at least eight characters. If you're out of ideas, you can <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>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>Quotas may not contain any spaces, commas or decimal points. Suffixes of G (gigabytes) and M (megabytes) are
allowed. For unlimited storage enter 0 (zero)</li>
</ul>
<button type="submit" class="btn btn-primary">Add User</button>
</form>
<p style="margin-top: .5em"><small>
Passwords must be at least four characters and may not contain spaces.
Administrators get access to this control panel.
</small></p>
<h3>Existing mail users</h3>
<table id="user_table" class="table" style="width: auto">
<tbody>
</tbody>
<table id="user_table" class="table col-12">
<caption></caption>
<thead>
<tr>
<th width="35%">Email Address</th>
<th class="">Messages</th>
<th class="">Size</th>
<th class="">Used</th>
<th class="">Quota</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
<div style="display: none">
<table>
<tr id="user-template">
<td class='email'>
<div class='address'> </div>
<table>
<tr id="user-template">
<td class='address'>
</td>
<td class="box-count"></td>
<td class="box-size"></td>
<td class="percent"></td>
<td class="quota">
</td>
<td class='actions'>
<span class='privs'>
</span>
<div class='actions'>
<span class='privs'>
</span>
<span class="if_active">
<a href="#" onclick="users_set_quota(this); return false;" class='setquota' title="Set Quota">
set quota
</a>
|
</span>
<span class="if_active">
<a href="#" onclick="users_set_password(this); return false;" class='setpw' title="Set Password">
set password
</a>
|
</span>
<span class="if_active">
<a href="#" onclick="users_set_password(this); return false;" class='setpw' title="Set Password">
set password
</a>
|
</span>
<span class='add-privs'>
</span>
<span class='add-privs'>
</span>
<a href="#" onclick="users_remove(this); return false;" class='if_active' title="Archive Account">
archive account
</a>
<div class='if_inactive' style='color: #888; font-size: 90%'>To restore account, create a new account with this email address.</div>
</div>
<div class='aliases' style='display: none'> </div>
</td>
</tr>
</table>
<a href="#" onclick="users_remove(this); return false;" class='if_active' title="Archive Account">
archive account
</a>
</td>
</tr>
<tr id="user-extra-template" class="if_inactive">
<td colspan="3" style="border: 0; padding-top: 0">
<div class='restore_info' style='color: #888; font-size: 90%'>To restore account, create a new account with this
email address. Or to permanently delete the mailbox, delete the directory <tt></tt> on the machine.</div>
</td>
</tr>
</table>
</div>
<h3>Mail user API (advanced)</h3>
<p>Use your box&rsquo;s mail user API to add/change/remove users from the command-line or custom services you build.</p>
<p>Usage:</p>
<pre>curl -X <b>VERB</b> [-d "<b>parameters</b>"] --user {email}:{password} https://{{hostname}}/admin/mail/users[<b>action</b>]</pre>
<p>Brackets denote an optional argument. Please note that the POST body <code>parameters</code> must be URL-encoded.</p>
<p>The email and password given to the <code>--user</code> option must be an administrative user on this system.</p>
<h4 style="margin-bottom: 0">Verbs</h4>
<table class="table" style="margin-top: .5em">
<caption></caption>
<thead>
<th>Verb</th>
<th>Action</th>
<th></th>
</thead>
<tr>
<td><b>GET</b< /td>
<td><i>(none)</i></td>
<td>Returns a list of existing mail users. Adding <code>?format=json</code> to the URL will give JSON-encoded
results.</td>
</tr>
<tr>
<td><b>POST</b< /td>
<td class="font-monospace">/add</td>
<td>Adds a new mail user. Required POST-body parameters are <code>email</code> and <code>password</code>. Optional
parameters: <code>privilege=admin</code> and <code>quota</code></td>
</tr>
<tr>
<td><b>POST</b></td>
<td class="font-monospace">/remove</td>
<td>Removes a mail user. Required POST-by parameter is <code>email</code>.</td>
</tr>
<tr>
<td><b>POST</b></td>
<td class="font-monospace">/privileges/add</td>
<td>Used to make a mail user an admin. Required POST-body parameters are <code>email</code> and
<code>privilege=admin</code>.</td>
</tr>
<tr>
<td><b>POST</b></td>
<td class="font-monospace">/privileges/remove</td>
<td>Used to remove the admin privilege from a mail user. Required POST-body parameter is <code>email</code>.</td>
</tr>
<tr>
<td><b>GET</b></td>
<td class="font-monospace">/quota</td>
<td>Get the quota for a mail user. Required POST-body parameters are <code>email</code> and will return JSON result
</td>
</tr>
<tr>
<td><b>POST</b></td>
<td class="font-monospace">/quota</td>
<td>Set the quota for a mail user. Required POST-body parameters are <code>email</code> and <code>quota</code>.</td>
</tr>
</table>
<h4>Examples:</h4>
<p>Try these examples. For simplicity the examples omit the <code>--user me@mydomain.com:yourpassword</code> command
line argument which you must fill in with your administrative email address and password.</p>
<pre># Gives a JSON-encoded list of all mail users
curl -X GET https://{{hostname}}/admin/mail/users?format=json
# Adds a new email user
curl -X POST -d "email=new_user@mydomail.com" -d "password=s3curE_pa5Sw0rD" https://{{hostname}}/admin/mail/users/add
# Removes a email user
curl -X POST -d "email=new_user@mydomail.com" https://{{hostname}}/admin/mail/users/remove
# Adds admin privilege to an email user
curl -X POST -d "email=new_user@mydomail.com" -d "privilege=admin" https://{{hostname}}/admin/mail/users/privileges/add
# Removes admin privilege from an email user
curl -X POST -d "email=new_user@mydomail.com" https://{{hostname}}/admin/mail/users/privileges/remove
</pre>
<script>
function show_users() {
$('#user_table tbody').html("<tr><td colspan='2' class='text-muted'>Loading...</td></tr>")
api(
"/mail/users",
"GET",
{ format: 'json' },
function(r) {
$('#user_table tbody').html("");
for (var i = 0; i < r.length; i++) {
var n = $("#user-template").clone();
n.attr('id', '');
function show_users() {
api(
"/system/default-quota",
"GET",
{},
function (r) {
$('#adduserQuota').val(r['default-quota']);
}
);
n.addClass("account_" + r[i].status);
n.attr('data-email', r[i].email);
n.find('td.email .address').text(r[i].email)
$('#user_table tbody').append(n);
$('#user_table tbody').html("<tr><td colspan='6' class='text-muted'>Loading...</td></tr>")
api(
"/mail/users",
"GET",
{ format: 'json' },
function (r) {
$('#user_table tbody').html("");
for (var i = 0; i < r.length; i++) {
var hdr = $("<tr><th colspan='6' class='bg-light'></th></tr>");
hdr.find('th').text(r[i].domain);
$('#user_table tbody').append(hdr);
if (r[i].status == 'inactive') continue;
for (var k = 0; k < r[i].users.length; k++) {
var user = r[i].users[k];
var add_privs = ["admin"];
var n = $("#user-template").clone();
var n2 = $("#user-extra-template").clone();
n.attr('id', '');
n2.attr('id', '');
$('#user_table tbody').append(n);
$('#user_table tbody').append(n2);
for (var j = 0; j < r[i].privileges.length; j++) {
var p = $("<span><b><span class='name'></span></b> (<a href='#' onclick='mod_priv(this, \"remove\"); return false;' title='Remove Privilege'>remove privilege</a>) |</span>");
p.find('span.name').text(r[i].privileges[j]);
n.find('.privs').append(p);
if (add_privs.indexOf(r[i].privileges[j]) >= 0)
add_privs.splice(add_privs.indexOf(r[i].privileges[j]), 1);
}
n.addClass("account_" + user.status);
n2.addClass("account_" + user.status);
for (var j = 0; j < add_privs.length; j++) {
var p = $("<span><a href='#' onclick='mod_priv(this, \"add\"); return false;' title='Add Privilege'>make <span class='name'></span></a> | </span>");
p.find('span.name').text(add_privs[j]);
n.find('.add-privs').append(p);
}
n.attr('data-email', user.email);
n.attr('data-quota', user.quota);
n.find('.address').text(user.email);
n.find('.box-count').text((user.box_count).toLocaleString('en'));
if (user.box_count == '?') {
n.find('.box-count').attr('title', 'Message count is unkown')
}
n.find('.box-size').text(user.box_size);
if (user.box_size == '?') {
n.find('.box-size').attr('title', 'Mailbox size is unkown')
}
n.find('.percent').text(user.percent);
n.find('.quota').text((user.quota == '0') ? 'unlimited' : user.quota);
n2.find('.restore_info tt').text(user.mailbox);
if (r[i].aliases && r[i].aliases.length > 0) {
n.find('.aliases').show();
for (var j = 0; j < r[i].aliases.length; j++) {
n.find('td.email .aliases').append($("<div/>").text(
r[i].aliases[j][0]
+ (r[i].aliases[j][1].length > 0 ? " ⇐ " + r[i].aliases[j][1].join(", ") : "")
))
}
}
}
})
}
if (user.status == 'inactive') continue;
function do_add_user() {
var email = $("#adduserEmail").val();
var pw = $("#adduserPassword").val();
var privs = $("#adduserPrivs").val();
api(
"/mail/users/add",
"POST",
{
email: email,
password: pw,
privileges: privs
},
function(r) {
// Responses are multiple lines of pre-formatted text.
show_modal_error("Add User", $("<pre/>").text(r));
show_users()
},
function(r) {
show_modal_error("Add User", r);
});
return false;
}
var add_privs = ["admin"];
function users_set_password(elem) {
var email = $(elem).parents('tr').attr('data-email');
show_modal_confirm(
"Archive User",
$("<p>Set a new password for <b>" + email + "</b>?</p> <p><label for='users_set_password_pw' style='display: block; font-weight: normal'>New Password:</label><input type='password' id='users_set_password_pw'></p><p><small>Passwords must be at least four characters and may not contain spaces.</small></p>"),
"Set Password",
function() {
api(
"/mail/users/password",
"POST",
{
email: email,
password: $('#users_set_password_pw').val()
},
function(r) {
// Responses are multiple lines of pre-formatted text.
show_modal_error("Set Password", $("<pre/>").text(r));
},
function(r) {
show_modal_error("Set Password", r);
});
});
}
for (var j = 0; j < user.privileges.length; j++) {
var p = $("<span><b><span class='name'></span></b> (<a href='#' onclick='mod_priv(this, \"remove\"); return false;' title='Remove Privilege'>remove privilege</a>) |</span>");
p.find('span.name').text(user.privileges[j]);
n.find('.privs').append(p);
if (add_privs.indexOf(user.privileges[j]) >= 0)
add_privs.splice(add_privs.indexOf(user.privileges[j]), 1);
}
function users_remove(elem) {
var email = $(elem).parents('tr').attr('data-email');
show_modal_confirm(
"Archive User",
$("<p>Are you sure you want to archive <b>" + email + "</b>?</p> <p>The user's mailboxes will not be deleted (you can do that later), but the user will no longer be able to log into any services on this machine.</p>"),
"Archive",
function() {
api(
"/mail/users/remove",
"POST",
{
email: email
},
function(r) {
// Responses are multiple lines of pre-formatted text.
show_modal_error("Remove User", $("<pre/>").text(r));
show_users();
},
function(r) {
show_modal_error("Remove User", r);
});
});
}
for (var j = 0; j < add_privs.length; j++) {
var p = $("<span><a href='#' onclick='mod_priv(this, \"add\"); return false;' title='Add Privilege'>make <span class='name'></span></a> | </span>");
p.find('span.name').text(add_privs[j]);
n.find('.add-privs').append(p);
}
}
}
})
}
function mod_priv(elem, add_remove) {
var email = $(elem).parents('tr').attr('data-email');
var priv = $(elem).parents('td').find('.name').text();
function do_add_user() {
var email = $("#adduserEmail").val();
var pw = $("#adduserPassword").val();
var privs = $("#adduserPrivs").val();
var quota = $("#adduserQuota").val();
api(
"/mail/users/add",
"POST",
{
email: email,
password: pw,
privileges: privs,
quota: quota
},
function (r) {
// Responses are multiple lines of pre-formatted text.
show_modal_error("Add User", $("<pre/>").text(r));
show_users()
},
function (r) {
show_modal_error("Add User", r);
});
return false;
}
// can't remove your own admin access
if (priv == "admin" && add_remove == "remove" && api_credentials != null && email == api_credentials[0]) {
show_modal_error("Modify Privileges", "You cannot remove the admin privilege from yourself.");
return;
}
function users_set_password(elem) {
var email = $(elem).parents('tr').attr('data-email');
var add_remove1 = add_remove.charAt(0).toUpperCase() + add_remove.substring(1);
show_modal_confirm(
"Modify Privileges",
"Are you sure you want to " + add_remove + " the " + priv + " privilege for <b>" + email + "</b>?",
add_remove1,
function() {
api(
"/mail/users/privileges/" + add_remove,
"POST",
{
email: email,
privilege: priv
},
function(r) {
show_users();
});
});
}
var yourpw = "";
if (api_credentials != null && email == api_credentials.username)
yourpw = "<p class='text-danger'>If you change your own password, you will be logged out of this control panel and will need to log in again.</p>";
show_modal_confirm(
"Set Password",
$("<p>Set a new password for <b>" + email + "</b>?</p> <p><label for='users_set_password_pw' style='display: block; font-weight: normal'>New Password:</label><input type='password' id='users_set_password_pw'></p><p><small>Passwords must be at least eight characters.</small>" + yourpw + "</p>"),
"Set Password",
function () {
api(
"/mail/users/password",
"POST",
{
email: email,
password: $('#users_set_password_pw').val()
},
function (r) {
// Responses are multiple lines of pre-formatted text.
show_modal_error("Set Password", $("<pre/>").text(r));
},
function (r) {
show_modal_error("Set Password", r);
});
});
}
function users_set_quota(elem) {
var email = $(elem).parents('tr').attr('data-email');
var quota = $(elem).parents('tr').attr('data-quota');
show_modal_confirm(
"Set Quota",
$("<p>Set quota for <b>" + email + "</b>?</p>" +
"<p>" +
"<label for='users_set_quota' style='display: block; font-weight: normal'>Quota:</label>" +
"<input type='text' id='users_set_quota' value='" + quota + "'></p>" +
"<p><small>Quotas may not contain any spaces or commas. Suffixes of G (gigabytes) and M (megabytes) are allowed.</small></p>" +
"<p><small>For unlimited storage enter 0 (zero)</small></p>"),
"Set Quota",
function () {
api(
"/mail/users/quota",
"POST",
{
email: email,
quota: $('#users_set_quota').val()
},
function (r) {
show_users();
},
function (r) {
show_modal_error("Set Quota", r);
});
});
}
function users_remove(elem) {
var email = $(elem).parents('tr').attr('data-email');
// can't remove yourself
if (api_credentials != null && email == api_credentials.username) {
show_modal_error("Archive User", "You cannot archive your own account.");
return;
}
show_modal_confirm(
"Archive User",
$("<p>Are you sure you want to archive <b>" + email + "</b>?</p> <p>The user's mailboxes will not be deleted (you can do that later), but the user will no longer be able to log into any services on this machine.</p>"),
"Archive",
function () {
api(
"/mail/users/remove",
"POST",
{
email: email
},
function (r) {
// Responses are multiple lines of pre-formatted text.
show_modal_error("Remove User", $("<pre/>").text(r));
show_users();
},
function (r) {
show_modal_error("Remove User", r);
});
});
}
function mod_priv(elem, add_remove) {
var email = $(elem).parents('tr').attr('data-email');
var priv = $(elem).parents('td').find('.name').text();
// can't remove your own admin access
if (priv == "admin" && add_remove == "remove" && api_credentials != null && email == api_credentials.username) {
show_modal_error("Modify Privileges", "You cannot remove the admin privilege from yourself.");
return;
}
var add_remove1 = add_remove.charAt(0).toUpperCase() + add_remove.substring(1);
show_modal_confirm(
"Modify Privileges",
$("<p>Are you sure you want to " + add_remove + " the " + priv + " privilege for <b>" + email + "</b>?</p>"),
add_remove1,
function () {
api(
"/mail/users/privileges/" + add_remove,
"POST",
{
email: email,
privilege: priv
},
function (r) {
show_users();
});
});
}
function generate_random_password() {
var pw = "";
var charset = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789"; // confusable characters skipped
for (var i = 0; i < 12; i++)
pw += charset.charAt(Math.floor(Math.random() * charset.length));
show_modal_error("Random Password", "<p>Here, try this:</p> <p><code style='font-size: 110%'>" + pw + "</code></p>");
return false; // cancel click
}
</script>

View file

@ -0,0 +1,103 @@
<style>
</style>
<h2>Static Web Hosting</h2>
<p>This machine is serving a simple, static website at <a href="https://{{hostname}}">https://{{hostname}}</a> and at
all domain names that you set up an email user or alias for.</p>
<h3>Uploading web files</h3>
<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>
<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>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>Log in to this machine with the file transfer program. The server is <strong>{{hostname}}</strong>, the protocol
is SSH or SFTP, and use the <strong>SSH login credentials</strong> that you used when you originally created this
machine at your cloud host provider. This is <strong>not</strong> what you use to log in either for email or this
control panel. Your SSH credentials probably involves a private key file.</li>
<li>Upload your <tt>.html</tt> or other files to the directory <tt>{{storage_root}}/www/default</tt> on this machine.
They will appear directly and immediately on the web.</li>
<li>The websites set up on this machine are listed in the table below with where to put the files for each website.
</li>
<table id="web_domains_existing" class="table col-12" style="margin-bottom: 1em;">
<caption></caption>
<thead>
<tr>
<th>Site</th>
<th>Directory for Files</th>
</tr>
</thead>
<tbody>
</tbody>
</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>
</ol>
<script>
function show_web() {
api(
"/web/domains",
"GET",
{
},
function (domains) {
var tb = $('#web_domains_existing tbody');
tb.text('');
for (var i = 0; i < domains.length; i++) {
if (!domains[i].static_enabled) continue;
var row = $("<tr><th class='domain'><a href=''></a></th><td class='directory'><tt/></td> <td class='change-root hidden'><button class='btn btn-default btn-xs' onclick='show_change_web_root(this)'>Change</button></td></tr>");
tb.append(row);
row.attr('data-domain', domains[i].domain);
row.attr('data-custom-web-root', domains[i].custom_root);
row.find('.domain a').text('https://' + domains[i].domain);
row.find('.domain a').attr('href', 'https://' + domains[i].domain);
row.find('.directory tt').text(domains[i].root);
if (domains[i].root != domains[i].custom_root)
row.find('.change-root').removeClass('hidden');
}
});
}
function do_web_update() {
api(
"/web/update",
"POST",
{
},
function (data) {
if (data == "")
data = "Nothing changed.";
else
data = $("<pre/>").text(data);
show_modal_error("Web Update", data, function () { show_web() });
});
}
function show_change_web_root(elem) {
var domain = $(elem).parents('tr').attr('data-domain');
var root = $(elem).parents('tr').attr('data-custom-web-root');
show_modal_confirm(
'Change Root Directory for ' + domain,
$('<p>You can change the static directory for <tt>' + domain + '</tt> to:</p> <p><tt>' + root + '</tt></p> <p>First create this directory on the server. Then click Update to scan for the directory and update web settings.</p>'),
'Update',
function () { do_web_update(); });
}
</script>

View file

@ -0,0 +1,3 @@
<h1 class="title">{{hostname}}</h1>
<p class="subtitle">Welcome to your Power Mail-in-a-Box control panel.</p>

View file

@ -0,0 +1,306 @@
<style>
#wkd_table #uids {
white-space: pre-line;
}
#wkd_table .domain-header {
margin-top: 0.5em;
margin-bottom: 0.5em;
}
</style>
<template id="wkd-entry-template">
<tr>
<td style="vertical-align: middle; white-space: nowrap;" id="email">email@example.com</td>
<td id="key-dropdown" class="dropdown show" style="width: 100%;">
<button class="btn bg-light" style="width: 100%;" data-bs-toggle="dropdown" id="current-key">
</button>
<div class="dropdown-menu" style="width: 100%;" id="menu">
<button class="btn bg-light" style="width:100%;" id="key-none">
<b>None</b>
<br>
<a>Do not serve a key in WKD.</a>
</button>
<button class="btn bg-light" style="width:100%" id="key-sample">
<div class="row">
<div class="col-6" id="fingerprint">
<b class="font-monospace status-text" id="fpr"></b><span id="subkeys"></span>
</div>
<div class="col-6">
<b>
<span class="sym" id="sign">S</span>
<span class="sym" id="cert">C</span>
<span class="sym" id="encr">E</span>
<span class="sym" id="auth">A</span>
</b>,
<span id="expire"><span class="status-text"></span></span>
</div>
</div>
<div id="key-emails">
</div>
</button>
</div>
</td>
</tr>
</template>
<template id="wkd-nokeys-template">
<tr>
<td style="vertical-align: middle; white-space: nowrap;" id="email">email@example.com</td>
<td style="width: 100%; text-align: center;" class="text-muted">
<b>None</b>
<br>
<a>There are no keys in the keyring that contain this email.</a>
</td>
</tr>
</template>
<h2>WKD Management</h2>
<p>WKD (<b>W</b>eb <b>K</b>ey <b>D</b>irectory) is an
<b><a href="https://tools.ietf.org/id/draft-koch-openpgp-webkey-service-11.html">experimental feature</a></b>
that allows users to authoratively publish their public PGP keys on the web, via HTTPS.
</p>
<p>Unlike other solutions (like public keyservers), WKD has the advantage that the owner
of the domain has some degree of control over what keys are published and as such there
is more certainity that the key actually belongs to it's owner.</p>
<p>As of this version, there is no support for WKS - This may or may not change in the future.</p>
<h3>Hosting somewhere else</h3>
<a>
If you have a separate server to host WKD, you can still use it instead of this box on a per-domain basis.
<br>
This box uses the Advanced Method to serve the keys. For example, to host your <code>@some.example.com</code> keys,
you can add a A, AAAA or CNAME record for <code>openpgpkey.some.example.com</code>. It will override the box's
records.
<br><br>
There's not a way to "disable" WKD at the moment - but if you don't want to publish keys, or if you want to use the
Direct method somewhere else, you can always set the records to an unreachable destination
(for example, an A record to <code>0.0.0.0</code>)
</a>
<h3>Emails hosted by this box</h3>
<a>
This box can serve keys to any email address hosted by this box, be it an user or an alias,
granted that the key has one or more user id's with matching emails.
<br>
You can add keys on the PGP Keyring Management menu.
</a>
<div class="input-group mt-3 mb-3">
<label for="wkd-show-all-entries" class="input-group-text"><b>Show all email addresses</b></label>
<div class="input-group-text">
<div class="form-switch">
<input type="checkbox" role="switch" id="wkd-show-all-entries" class="form-check-input" value=false
onclick="toggle_emails_with_no_pgp_key();">
</div>
</div>
</div>
<table id="wkd_table" class="table" style="width: 100%">
<caption></caption>
<thead>
<tr>
<th>Email Address</th>
<th></th>
</tr>
</thead>
<tbody id="wkd_tbody">
</tbody>
</table>
<button id="wkd_submit" class="btn btn-primary" disabled onclick="update_wkd()">Apply</button>
<script>
let dirty_config = {}
let toggleable_lines = []
let show_toggleable_lines = document.getElementById("wkd-show-all-entries").checked
function pretty_long_id(fpr) {
let plid = ""
for (let i = 6; i < 10; ++i) {
plid += `${fpr.substring(i * 4, (i + 1) * 4)} `
}
return plid.substring(0, plid.length - 1)
}
function generate_wkd_menu(email, wkd_config, keys) {
if (wkd_config.options.length == 0) {
// No possible options
let menurep = $("#wkd-nokeys-template").html()
menurep = $(menurep)
menurep.attr("id", `wkd-${email}`)
menurep.find("#email").html(email)
return menurep
}
// else
let menurep = $("#wkd-entry-template").html()
menurep = $(menurep)
menurep.attr("id", `wkd-${email}`)
menurep.find("#email").html(email)
let template = menurep.find("#key-sample")
wkd_config.options.forEach(key => {
let option = template.prop("outerHTML")
option = $(option)
option.attr("id", `key-${key}`)
// Basic information
let keyinfo = keys[key]
let skey = keyinfo.subkeys[0] // Master subkey
let fpr = option.find("#fingerprint")
fpr.find("#fpr").text(pretty_long_id(key))
fpr.find("#subkeys").text(` (${keyinfo.subkeys.length} subkey${keyinfo.subkeys.length == 1 ? "" : "s"})`)
option.click(() => {
$("#wkd_submit").attr("disabled", false)
dirty_config[email] = key
menurep.find("#current-key").html(option.html())
})
option.appendTo(menurep.find("#menu"))
let classType = ""
let expire = option.find("#expire")
let exp = expire.find(".status-text")
// Purpose and expiration
// For evaluation we're only going to use the master subkey
if (keyinfo.revoked) {
classType = "status-error"
exp.text(`revoked`)
} else if (skey.expired) {
classType = "status-error"
exp.text(`expired on ${skey.expires_date}`)
} else if (skey.expires && skey.expires_days <= 14) {
classType = "status-warning"
exp.text(`expires in ${skey.expires_days} days (${skey.expires_date})`)
} else if (skey.expires) {
classType = "status-ok"
exp.text(`expires in ${skey.expires_days} days (${skey.expires_date})`)
} else {
classType = "status-ok"
exp.text("does not expire")
}
expire.addClass(classType)
fpr.addClass(classType)
let s = option.find("#sign")
let c = option.find("#cert")
let e = option.find("#encr")
let a = option.find("#auth")
s.addClass(skey.sign ? classType : "status-na")
c.addClass(skey.cert ? classType : "status-na")
e.addClass(skey.encr ? classType : "status-na")
a.addClass(skey.auth ? classType : "status-na")
let txt = ""
let n_emails = keyinfo.ids_emails.length
if (n_emails == 1) {
txt = `<b class="font-monospace">${email}</b>`
} else if (email.length >= 70) {
// Absurd situation, but can happen!
// We'll only
txt = `<b class="font-monospace">${email}</b> and ${n_emails - 1} other${n_emails == 2 ? "" : "s"}`
} else {
let buf = 70 - email.length
let others = n_emails - 1
let sorted_emails = keyinfo.ids_emails.filter((v) => {
return v != email
})
sorted_emails.sort((x, y) => {
// Longer emails come first. If there's a tie, use alphabetic order
return x.length == y.length ? x.localeCompare(y) : x.length > y.length ? -1 : 1
})
txt = `<b class="font-monospace">${email}</b>`
// From the remaining emails, pick the longest ones within the buffer constraints
// If none, append the "and x others text"
sorted_emails.forEach(e => {
if (e.length <= buf) {
others--
buf -= e.length
txt += `, <span class="font-monospace">${e}</span>`
}
});
if (others > 0) {
txt += ` and ${others} other${others == 1 ? "" : "s"}`
}
}
option.find("#key-emails").html(txt)
});
template.remove()
let nokeyopt = menurep.find("#key-none")
nokeyopt.click(() => {
$("#wkd_submit").attr("disabled", false)
dirty_config[email] = null
menurep.find("#current-key").html(nokeyopt.html())
})
menurep.find("#current-key").html(menurep.find(`#key-${wkd_config.selected ? wkd_config.selected : "none"}`).html())
return menurep
}
function generate_wkd_dropdowns(wkd_info) {
$("#wkd_tbody").html("")
Object.keys(wkd_info.wkd).forEach(domain => {
$(`<tr><td colspan="2" class="bg-light"><h4 class="domain-header">${domain}</h4></td></tr>`).appendTo("#wkd_tbody")
Object.keys(wkd_info.wkd[domain]).forEach(email => {
let menu = generate_wkd_menu(email, wkd_info.wkd[domain][email], wkd_info.keys)
menu.appendTo("#wkd_tbody")
// No possible keys, we can hide this line to save space
if (wkd_info.wkd[domain][email].options.length === 0) {
toggleable_lines.push(menu)
}
})
});
toggle_emails_with_no_pgp_key()
}
function show_wkd() {
toggleable_lines = []
$("#wkd_tbody").html("<tr><td colspan='2' class='text-muted'>Loading...</td></tr>")
api(
"/system/pgp/wkd",
"GET",
{},
(info) => {
$("#wkd_submit").attr("disabled", true)
dirty_config = {}
Object.keys(info.wkd).forEach(domain => {
Object.keys(info.wkd[domain]).forEach(email => {
dirty_config[email] = info.wkd[domain][email].selected
})
})
generate_wkd_dropdowns(info)
}
)
}
function update_wkd() {
api(
"/system/pgp/wkd",
"POST",
dirty_config,
(r) => {
show_modal_error("WKD Management", $("<p/>").text(r), () => { if (r == "OK") show_wkd() })
},
(r) => {
show_modal_error("WKD Management", $("<p/>").text(r))
}
)
}
function toggle_emails_with_no_pgp_key() {
let show_toggleable_lines = document.getElementById("wkd-show-all-entries").checked
toggleable_lines.forEach(line => {
line.toggle(show_toggleable_lines)
});
}
</script>

View file

@ -1,171 +1,234 @@
import os.path
CONF_DIR = os.path.join(os.path.dirname(__file__), "../conf")
# DO NOT import non-standard modules. This module is imported by
# migrate.py which runs on fresh machines before anything is installed
# besides Python.
# THE ENVIRONMENT FILE AT /etc/mailinabox.conf
def load_environment():
# Load settings from /etc/mailinabox.conf.
return load_env_vars_from_file("/etc/mailinabox.conf")
# Load settings from /etc/mailinabox.conf.
return load_env_vars_from_file("/etc/mailinabox.conf")
def load_env_vars_from_file(fn):
# Load settings from a KEY=VALUE file.
import collections
env = collections.OrderedDict()
for line in open(fn): env.setdefault(*line.strip().split("=", 1))
return env
# Load settings from a KEY=VALUE file.
import collections
env = collections.OrderedDict()
for line in open(fn):
env.setdefault(*line.strip().split("=", 1))
return env
def save_environment(env):
with open("/etc/mailinabox.conf", "w") as f:
for k, v in env.items():
f.write("%s=%s\n" % (k, v))
with open("/etc/mailinabox.conf", "w") as f:
for k, v in env.items():
f.write("%s=%s\n" % (k, v))
# THE SETTINGS FILE AT STORAGE_ROOT/settings.yaml.
def write_settings(config, env):
import rtyaml
fn = os.path.join(env['STORAGE_ROOT'], 'settings.yaml')
with open(fn, "w") as f:
f.write(rtyaml.dump(config))
def load_settings(env):
import rtyaml
fn = os.path.join(env['STORAGE_ROOT'], 'settings.yaml')
try:
config = rtyaml.load(open(fn, "r"))
if not isinstance(config, dict):
raise ValueError() # caught below
return config
except:
return {}
# UTILITIES
def safe_domain_name(name):
# Sanitize a domain name so it is safe to use as a file name on disk.
import urllib.parse
return urllib.parse.quote(name, safe='')
# Sanitize a domain name so it is safe to use as a file name on disk.
import urllib.parse
return urllib.parse.quote(name, safe='')
def unsafe_domain_name(name_encoded):
import urllib.parse
return urllib.parse.unquote(name_encoded)
def sort_domains(domain_names, env):
# Put domain names in a nice sorted order. For web_update, PRIMARY_HOSTNAME
# must appear first so it becomes the nginx default server.
# First group PRIMARY_HOSTNAME and its subdomains, then parent domains of PRIMARY_HOSTNAME, then other domains.
groups = ( [], [], [] )
for d in domain_names:
if d == env['PRIMARY_HOSTNAME'] or d.endswith("." + env['PRIMARY_HOSTNAME']):
groups[0].append(d)
elif env['PRIMARY_HOSTNAME'].endswith("." + d):
groups[1].append(d)
else:
groups[2].append(d)
# Put domain names in a nice sorted order.
# Within each group, sort parent domains before subdomains and after that sort lexicographically.
def sort_group(group):
# Find the top-most domains.
top_domains = sorted(d for d in group if len([s for s in group if d.endswith("." + s)]) == 0)
ret = []
for d in top_domains:
ret.append(d)
ret.extend( sort_group([s for s in group if s.endswith("." + d)]) )
return ret
groups = [sort_group(g) for g in groups]
# The nice order will group domain names by DNS zone, i.e. the top-most
# domain name that we serve that ecompasses a set of subdomains. Map
# each of the domain names to the zone that contains them. Walk the domains
# from shortest to longest since zones are always shorter than their
# subdomains.
zones = {}
for domain in sorted(domain_names, key=lambda d: len(d)):
for z in zones.values():
if domain.endswith("." + z):
# We found a parent domain already in the list.
zones[domain] = z
break
else:
# 'break' did not occur: there is no parent domain, so it is its
# own zone.
zones[domain] = domain
# Sort the zones.
zone_domains = sorted(
zones.values(),
key=lambda d: (
# PRIMARY_HOSTNAME or the zone that contains it is always first.
not (d == env['PRIMARY_HOSTNAME'] or env['PRIMARY_HOSTNAME'].
endswith("." + d)),
# Then just dumb lexicographically.
d,
))
# Now sort the domain names that fall within each zone.
domain_names = sorted(
domain_names,
key=lambda d: (
# First by zone.
zone_domains.index(zones[d]),
# PRIMARY_HOSTNAME is always first within the zone that contains it.
d != env['PRIMARY_HOSTNAME'],
# Followed by any of its subdomains.
not d.endswith("." + env['PRIMARY_HOSTNAME']),
# Then in right-to-left lexicographic order of the .-separated parts of the name.
list(reversed(d.split("."))),
))
return domain_names
return groups[0] + groups[1] + groups[2]
def sort_email_addresses(email_addresses, env):
email_addresses = set(email_addresses)
domains = set(email.split("@", 1)[1] for email in email_addresses if "@" in email)
ret = []
for domain in sort_domains(domains, env):
domain_emails = set(email for email in email_addresses if email.endswith("@" + domain))
ret.extend(sorted(domain_emails))
email_addresses -= domain_emails
ret.extend(sorted(email_addresses)) # whatever is left
return ret
def exclusive_process(name):
# Ensure that a process named `name` does not execute multiple
# times concurrently.
import os, sys, atexit
pidfile = '/var/run/mailinabox-%s.pid' % name
mypid = os.getpid()
# Attempt to get a lock on ourself so that the concurrency check
# itself is not executed in parallel.
with open(__file__, 'r+') as flock:
# Try to get a lock. This blocks until a lock is acquired. The
# lock is held until the flock file is closed at the end of the
# with block.
os.lockf(flock.fileno(), os.F_LOCK, 0)
# While we have a lock, look at the pid file. First attempt
# to write our pid to a pidfile if no file already exists there.
try:
with open(pidfile, 'x') as f:
# Successfully opened a new file. Since the file is new
# there is no concurrent process. Write our pid.
f.write(str(mypid))
atexit.register(clear_my_pid, pidfile)
return
except FileExistsError:
# The pid file already exixts, but it may contain a stale
# pid of a terminated process.
with open(pidfile, 'r+') as f:
# Read the pid in the file.
existing_pid = None
try:
existing_pid = int(f.read().strip())
except ValueError:
pass # No valid integer in the file.
# Check if the pid in it is valid.
if existing_pid:
if is_pid_valid(existing_pid):
print("Another %s is already running (pid %d)." % (name, existing_pid), file=sys.stderr)
sys.exit(1)
# Write our pid.
f.seek(0)
f.write(str(mypid))
f.truncate()
atexit.register(clear_my_pid, pidfile)
def clear_my_pid(pidfile):
import os
os.unlink(pidfile)
email_addresses = set(email_addresses)
domains = set(
email.split("@", 1)[1] for email in email_addresses if "@" in email)
ret = []
for domain in sort_domains(domains, env):
domain_emails = set(email for email in email_addresses
if email.endswith("@" + domain))
ret.extend(sorted(domain_emails))
email_addresses -= domain_emails
ret.extend(sorted(email_addresses)) # whatever is left
return ret
def is_pid_valid(pid):
"""Checks whether a pid is a valid process ID of a currently running process."""
# adapted from http://stackoverflow.com/questions/568271/how-to-check-if-there-exists-a-process-with-a-given-pid
import os, errno
if pid <= 0: raise ValueError('Invalid PID.')
try:
os.kill(pid, 0)
except OSError as err:
if err.errno == errno.ESRCH: # No such process
return False
elif err.errno == errno.EPERM: # Not permitted to send signal
return True
else: # EINVAL
raise
else:
return True
def shell(method,
cmd_args,
env={},
capture_stderr=False,
return_bytes=False,
trap=False,
input=None):
# A safe way to execute processes.
# Some processes like apt-get require being given a sane PATH.
import subprocess
def shell(method, cmd_args, env={}, capture_stderr=False, return_bytes=False, trap=False, input=None):
# A safe way to execute processes.
# Some processes like apt-get require being given a sane PATH.
import subprocess
env.update({"PATH": "/sbin:/bin:/usr/sbin:/usr/bin"})
kwargs = {
'env': env,
'stderr': None if not capture_stderr else subprocess.STDOUT,
}
if method == "check_output" and input is not None:
kwargs['input'] = input
env.update({ "PATH": "/sbin:/bin:/usr/sbin:/usr/bin" })
kwargs = {
'env': env,
'stderr': None if not capture_stderr else subprocess.STDOUT,
}
if method == "check_output" and input is not None:
kwargs['input'] = input
if not trap:
ret = getattr(subprocess, method)(cmd_args, **kwargs)
else:
try:
ret = getattr(subprocess, method)(cmd_args, **kwargs)
code = 0
except subprocess.CalledProcessError as e:
ret = e.output
code = e.returncode
if not return_bytes and isinstance(ret, bytes):
ret = ret.decode("utf8")
if not trap:
return ret
else:
return code, ret
if not trap:
ret = getattr(subprocess, method)(cmd_args, **kwargs)
else:
try:
ret = getattr(subprocess, method)(cmd_args, **kwargs)
code = 0
except subprocess.CalledProcessError as e:
ret = e.output
code = e.returncode
if not return_bytes and isinstance(ret, bytes): ret = ret.decode("utf8")
if not trap:
return ret
else:
return code, ret
def create_syslog_handler():
import logging.handlers
handler = logging.handlers.SysLogHandler(address='/dev/log')
handler.setLevel(logging.WARNING)
return handler
import logging.handlers
handler = logging.handlers.SysLogHandler(address='/dev/log')
handler.setLevel(logging.WARNING)
return handler
def du(path):
# Computes the size of all files in the path, like the `du` command.
# Based on http://stackoverflow.com/a/17936789. Takes into account
# soft and hard links.
total_size = 0
seen = set()
for dirpath, dirnames, filenames in os.walk(path):
for f in filenames:
fp = os.path.join(dirpath, f)
try:
stat = os.lstat(fp)
except OSError:
continue
if stat.st_ino in seen:
continue
seen.add(stat.st_ino)
total_size += stat.st_size
return total_size
def wait_for_service(port, public, env, timeout):
# Block until a service on a given port (bound privately or publicly)
# is taking connections, with a maximum timeout.
import socket
import time
start = time.perf_counter()
while True:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(timeout / 3)
try:
s.connect(("127.0.0.1" if not public else env['PUBLIC_IP'], port))
return True
except OSError:
if time.perf_counter() > start + timeout:
return False
time.sleep(min(timeout / 4, 1))
def get_php_version():
# Gets the version of PHP installed in the system.
return shell("check_output", ["/usr/bin/php", "-v"])[4:7]
def get_os_code():
# Massive mess incoming
dist = shell("check_output", ["/usr/bin/lsb_release", "-is"]).strip()
version = shell("check_output", ["/usr/bin/lsb_release", "-rs"]).strip()
if dist == "Debian":
if version == "10":
return "Debian10"
elif version == "11":
return "Debian11"
elif dist == "Ubuntu":
if version == "20.04":
return "Ubuntu2004"
elif version == "22.04":
return "Ubuntu2204"
return None
if __name__ == "__main__":
from web_update import get_web_domains
env = load_environment()
domains = get_web_domains(env)
for domain in domains:
print(domain)

View file

@ -2,51 +2,205 @@
# domains for which a mail account has been set up.
########################################################################
import os, os.path, re, rtyaml
import os.path
import re
import rtyaml
from mailconfig import get_mail_domains
from dns_update import get_custom_dns_config
from utils import shell, safe_domain_name, sort_domains
from dns_update import get_custom_dns_config, get_dns_zones
from ssl_certificates import get_ssl_certificates, get_domain_ssl_files, check_certificate
from utils import shell, safe_domain_name, sort_domains, get_php_version
def get_web_domains(env):
# What domains should we serve websites for?
def get_web_domains(env,
include_www_redirects=True,
include_auto=True,
exclude_dns_elsewhere=True):
# What domains should we serve HTTP(S) for?
domains = set()
# At the least it's the PRIMARY_HOSTNAME so we can serve webmail
# as well as Z-Push for Exchange ActiveSync.
domains.add(env['PRIMARY_HOSTNAME'])
# Also serve web for all mail domains so that we might at least
# provide Webfinger and ActiveSync auto-discover of email settings
# (though the latter isn't really working). These will require that
# an SSL cert be installed.
# Serve web for all mail domains so that we might at least
# provide auto-discover of email settings, and also a static website
# if the user wants to make one.
domains |= get_mail_domains(env)
# ...Unless the domain has an A/AAAA record that maps it to a different
# IP address than this box. Remove those domains from our list.
dns = get_custom_dns_config(env)
for domain, value in dns.items():
if domain not in domains: continue
if (isinstance(value, str) and (value != "local")) \
or (isinstance(value, dict) and ("A" in value) and (value["A"] != "local")) \
or (isinstance(value, dict) and ("AAAA" in value) and (value["AAAA"] != "local")):
domains.remove(domain)
if include_www_redirects and include_auto:
# Add 'www.' subdomains that we want to provide default redirects
# to the main domain for. We'll add 'www.' to any DNS zones, i.e.
# the topmost of each domain we serve.
domains |= set('www.' + zone for zone, zonefile in get_dns_zones(env))
# Sort the list. Put PRIMARY_HOSTNAME first so it becomes the
# default server (nginx's default_server).
if include_auto:
# Add Autoconfiguration domains for domains that there are user accounts at:
# 'autoconfig.' for Mozilla Thunderbird auto setup.
# 'autodiscover.' for ActiveSync autodiscovery (Z-Push).
domains |= set(
'autoconfig.' + maildomain
for maildomain in get_mail_domains(env, users_only=True))
domains |= set(
'autodiscover.' + maildomain
for maildomain in get_mail_domains(env, users_only=True))
# 'mta-sts.' for MTA-STS support for all domains that have email addresses.
domains |= set('mta-sts.' + maildomain
for maildomain in get_mail_domains(env))
# 'openpgpkey.' for WKD support
domains |= set('openpgpkey.' + maildomain
for maildomain in get_mail_domains(env))
if exclude_dns_elsewhere:
# ...Unless the domain has an A/AAAA record that maps it to a different
# IP address than this box. Remove those domains from our list.
domains -= get_domains_with_a_records(env)
# Ensure the PRIMARY_HOSTNAME is in the list so we can serve webmail
# as well as Z-Push for Exchange ActiveSync. This can't be removed
# by a custom A/AAAA record and is never a 'www.' redirect.
domains.add(env['PRIMARY_HOSTNAME'])
# Sort the list so the nginx conf gets written in a stable order.
domains = sort_domains(domains, env)
return domains
def do_web_update(env):
# Build an nginx configuration file.
nginx_conf = open(os.path.join(os.path.dirname(__file__), "../conf/nginx-top.conf")).read()
# Add configuration for each web domain.
template1 = open(os.path.join(os.path.dirname(__file__), "../conf/nginx.conf")).read()
template2 = open(os.path.join(os.path.dirname(__file__), "../conf/nginx-primaryonly.conf")).read()
for domain in get_web_domains(env):
nginx_conf += make_domain_config(domain, template1, template2, env)
def get_domains_with_a_records(env):
domains = set()
dns = get_custom_dns_config(env)
for domain, rtype, value, ttl in dns:
if rtype == "CNAME" or (rtype in ("A", "AAAA")
and value not in ("local", env['PUBLIC_IP'])):
domains.add(domain)
return domains
def get_web_domains_with_root_overrides(env):
# Load custom settings so we can tell what domains have a redirect or proxy set up on '/',
# which means static hosting is not happening.
root_overrides = {}
nginx_conf_custom_fn = os.path.join(env["STORAGE_ROOT"], "www/custom.yaml")
if os.path.exists(nginx_conf_custom_fn):
custom_settings = rtyaml.load(open(nginx_conf_custom_fn))
for domain, settings in custom_settings.items():
for type, value in [('redirect', settings.get('redirects',
{}).get('/')),
('proxy', settings.get('proxies',
{}).get('/'))]:
if value:
root_overrides[domain] = (type, value)
return root_overrides
DOMAIN_EXTERNAL = -1
DOMAIN_PRIMARY = 1
DOMAIN_WWW = 2
DOMAIN_REDIRECT = 4
DOMAIN_WKD = 8
def get_web_domain_flags(env):
flags = dict()
zones = get_dns_zones(env)
email_domains = get_mail_domains(env)
user_domains = get_mail_domains(env, users_only=True)
external = get_domains_with_a_records(env)
redirects = get_web_domains_with_root_overrides(env)
for d in email_domains:
flags[d] = flags.get(d, 0)
flags[f"mta-sts.{d}"] = flags.get(d, 0)
flags[f"openpgpkey.{d}"] = flags.get(d, 0) | DOMAIN_WKD
for d in user_domains:
flags[f"autoconfig.{d}"] = flags.get(d, 0)
flags[f"autodiscover.{d}"] = flags.get(d, 0)
for d, _ in zones:
flags[f"www.{d}"] = flags.get(d, 0) | DOMAIN_WWW
for d in redirects:
flags[d] = flags.get(d, 0) | DOMAIN_REDIRECT
flags[env["PRIMARY_HOSTNAME"]] |= DOMAIN_PRIMARY
# Last check for websites hosted elsewhere
for d in flags.keys():
if d in external:
# -1 = All bits set to 1, assuming twos-complement
flags[d] = DOMAIN_EXTERNAL
return flags
def do_web_update(env):
# Pre-load what SSL certificates we will use for each domain.
ssl_certificates = get_ssl_certificates(env)
# Build an nginx configuration file.
nginx_conf = open(
os.path.join(os.path.dirname(__file__),
"../conf/nginx-top.conf")).read()
nginx_conf = re.sub("{{phpver}}", get_php_version(), nginx_conf)
# Add upstream additions
nginx_upstream_include = os.path.join(env["STORAGE_ROOT"], "www",
".upstream.conf")
if not os.path.exists(nginx_upstream_include):
with open(nginx_upstream_include, "a+") as f:
f.writelines([
f"# Add your nginx-wide configurations here.\n",
"# The following names are already defined:\n\n",
"# # php-default: The php socket used for apps managed by the box. (Roundcube, Z-Push, Nextcloud, etc.) - DO NOT USE!\n",
"# # php-fpm: A php socket not managed by the box. Feel free to use it for your PHP applications\n"
])
nginx_conf += "\ninclude %s;\n" % (nginx_upstream_include)
# Load the templates.
template0 = open(
os.path.join(os.path.dirname(__file__), "../conf/nginx.conf")).read()
template1 = open(
os.path.join(os.path.dirname(__file__),
"../conf/nginx-alldomains.conf")).read()
template2 = open(
os.path.join(os.path.dirname(__file__),
"../conf/nginx-primaryonly.conf")).read()
template3 = "\trewrite ^(.*) https://$REDIRECT_DOMAIN$1 permanent;\n"
template4 = open(
os.path.join(os.path.dirname(__file__),
"../conf/nginx-openpgpkey.conf")).read()
# Add the PRIMARY_HOST configuration first so it becomes nginx's default server.
nginx_conf += make_domain_config(env['PRIMARY_HOSTNAME'],
[template0, template1, template2],
ssl_certificates, env)
# Add configuration all other web domains.
pairs = list(get_web_domain_flags(env).items())
# Sort the domains in some way to keep ordering consistency. Keep domains and subdomains together.
pairs.sort(reverse=False, key=lambda x: x[0][::-1])
for domain, flags in pairs:
if flags & DOMAIN_PRIMARY == DOMAIN_PRIMARY or flags == DOMAIN_EXTERNAL:
# PRIMARY_HOSTNAME is handled above.
continue
if flags & DOMAIN_WWW == 0:
# This is a regular domain.
if flags & DOMAIN_WKD == DOMAIN_WKD:
nginx_conf += make_domain_config(
domain, [template0, template1, template4],
ssl_certificates, env)
elif flags & DOMAIN_REDIRECT == 0:
nginx_conf += make_domain_config(domain,
[template0, template1],
ssl_certificates, env)
else:
nginx_conf += make_domain_config(domain, [template0],
ssl_certificates, env)
else:
# Add default 'www.' redirect.
nginx_conf += make_domain_config(domain, [template0, template3],
ssl_certificates, env)
# Did the file change? If not, don't bother writing & restarting nginx.
nginx_conf_fn = "/etc/nginx/conf.d/local.conf"
@ -67,129 +221,179 @@ def do_web_update(env):
return "web updated\n"
def make_domain_config(domain, template, template_for_primaryhost, env):
# How will we configure this domain.
def make_domain_config(domain, templates, ssl_certificates, env):
# GET SOME VARIABLES
# Where will its root directory be for static files?
root = get_web_root(domain, env)
# What private key and SSL certificate will we use for this domain?
ssl_key, ssl_certificate, csr_path = get_domain_ssl_files(domain, env)
tls_cert = get_domain_ssl_files(domain, ssl_certificates, env)
# For hostnames created after the initial setup, ensure we have an SSL certificate
# available. Make a self-signed one now if one doesn't exist.
ensure_ssl_certificate_exists(domain, ssl_key, ssl_certificate, csr_path, env)
# ADDITIONAL DIRECTIVES.
# Put pieces together.
nginx_conf_parts = re.split("\s*# ADDITIONAL DIRECTIVES HERE\s*", template)
nginx_conf = nginx_conf_parts[0] + "\n"
if domain == env['PRIMARY_HOSTNAME']:
nginx_conf += template_for_primaryhost + "\n"
nginx_conf_extra = ""
# Replace substitution strings in the template & return.
nginx_conf = nginx_conf.replace("$STORAGE_ROOT", env['STORAGE_ROOT'])
nginx_conf = nginx_conf.replace("$HOSTNAME", domain)
nginx_conf = nginx_conf.replace("$ROOT", root)
nginx_conf = nginx_conf.replace("$SSL_KEY", ssl_key)
nginx_conf = nginx_conf.replace("$SSL_CERTIFICATE", ssl_certificate)
# Because the certificate may change, we should recognize this so we
# can trigger an nginx update.
def hashfile(filepath):
import hashlib
sha1 = hashlib.sha1()
f = open(filepath, 'rb')
try:
sha1.update(f.read())
finally:
f.close()
return sha1.hexdigest()
nginx_conf_extra += "\t# ssl files sha1: %s / %s\n" % (hashfile(
tls_cert["private-key"]), hashfile(tls_cert["certificate"]))
# Add in any user customizations in YAML format.
hsts = "yes"
nginx_conf_custom_fn = os.path.join(env["STORAGE_ROOT"], "www/custom.yaml")
if os.path.exists(nginx_conf_custom_fn):
yaml = rtyaml.load(open(nginx_conf_custom_fn))
if domain in yaml:
yaml = yaml[domain]
# any proxy or redirect here?
for path, url in yaml.get("proxies", {}).items():
nginx_conf += "\tlocation %s {\n\t\tproxy_pass %s;\n\t}\n" % (path, url)
# Parse some flags in the fragment of the URL.
pass_http_host_header = False
proxy_redirect_off = False
frame_options_header_sameorigin = False
m = re.search("#(.*)$", url)
if m:
for flag in m.group(1).split(","):
if flag == "pass-http-host":
pass_http_host_header = True
elif flag == "no-proxy-redirect":
proxy_redirect_off = True
elif flag == "frame-options-sameorigin":
frame_options_header_sameorigin = True
url = re.sub("#(.*)$", "", url)
nginx_conf_extra += "\tlocation %s {" % path
nginx_conf_extra += "\n\t\tproxy_pass %s;" % url
if proxy_redirect_off:
nginx_conf_extra += "\n\t\tproxy_redirect off;"
if pass_http_host_header:
nginx_conf_extra += "\n\t\tproxy_set_header Host $http_host;"
if frame_options_header_sameorigin:
nginx_conf_extra += "\n\t\tproxy_set_header X-Frame-Options SAMEORIGIN;"
nginx_conf_extra += "\n\t\tproxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;"
nginx_conf_extra += "\n\t\tproxy_set_header X-Forwarded-Host $http_host;"
nginx_conf_extra += "\n\t\tproxy_set_header X-Forwarded-Proto $scheme;"
nginx_conf_extra += "\n\t\tproxy_set_header X-Real-IP $remote_addr;"
nginx_conf_extra += "\n\t}\n"
for path, alias in yaml.get("aliases", {}).items():
nginx_conf_extra += "\tlocation %s {" % path
nginx_conf_extra += "\n\t\talias %s;" % alias
nginx_conf_extra += "\n\t}\n"
for path, url in yaml.get("redirects", {}).items():
nginx_conf_extra += "\trewrite %s %s permanent;\n" % (path,
url)
# override the HSTS directive type
hsts = yaml.get("hsts", hsts)
# Add the HSTS header.
if hsts == "yes":
nginx_conf_extra += "\tadd_header Strict-Transport-Security \"max-age=15768000\" always;\n"
elif hsts == "preload":
nginx_conf_extra += "\tadd_header Strict-Transport-Security \"max-age=15768000; includeSubDomains; preload\" always;\n"
# Add in any user customizations in the includes/ folder.
nginx_conf_custom_include = os.path.join(env["STORAGE_ROOT"], "www", safe_domain_name(domain) + ".conf")
if os.path.exists(nginx_conf_custom_include):
nginx_conf += "\tinclude %s;\n" % (nginx_conf_custom_include)
nginx_conf_custom_include = os.path.join(
env["STORAGE_ROOT"], "www",
safe_domain_name(domain) + ".conf")
if not os.path.exists(nginx_conf_custom_include):
with open(nginx_conf_custom_include, "a+") as f:
f.writelines([
f"# Custom configurations for {domain} go here\n",
"# To use php: use the \"php-fpm\" alias\n\n",
"index index.html index.htm;\n"
])
# Ending.
nginx_conf += nginx_conf_parts[1]
nginx_conf_extra += "\tinclude %s;\n" % (nginx_conf_custom_include)
# PUT IT ALL TOGETHER
# Combine the pieces. Iteratively place each template into the "# ADDITIONAL DIRECTIVES HERE" placeholder
# of the previous template.
nginx_conf = "# ADDITIONAL DIRECTIVES HERE\n"
for t in templates + [nginx_conf_extra]:
nginx_conf = re.sub("[ \t]*# ADDITIONAL DIRECTIVES HERE *\n", t,
nginx_conf)
# Replace substitution strings in the template & return.
nginx_conf = nginx_conf.replace("$STORAGE_ROOT", env['STORAGE_ROOT'])
nginx_conf = nginx_conf.replace("$HOSTNAME", domain)
nginx_conf = nginx_conf.replace("$ROOT", root)
nginx_conf = nginx_conf.replace("$SSL_KEY", tls_cert["private-key"])
nginx_conf = nginx_conf.replace("$SSL_CERTIFICATE",
tls_cert["certificate"])
nginx_conf = nginx_conf.replace(
"$REDIRECT_DOMAIN",
re.sub(r"^www\.", "",
domain)) # for default www redirects to parent domain
return nginx_conf
def get_web_root(domain, env):
def get_web_root(domain, env, test_exists=True):
# Try STORAGE_ROOT/web/domain_name if it exists, but fall back to STORAGE_ROOT/web/default.
for test_domain in (domain, 'default'):
root = os.path.join(env["STORAGE_ROOT"], "www", safe_domain_name(test_domain))
if os.path.exists(root): break
root = os.path.join(env["STORAGE_ROOT"], "www",
safe_domain_name(test_domain))
if os.path.exists(root) or not test_exists:
break
return root
def get_domain_ssl_files(domain, env):
# What SSL private key will we use? Allow the user to override this, but
# in many cases using the same private key for all domains would be fine.
# Don't allow the user to override the key for PRIMARY_HOSTNAME because
# that's what's in the main file.
ssl_key = os.path.join(env["STORAGE_ROOT"], 'ssl/ssl_private_key.pem')
ssl_key_is_alt = False
alt_key = os.path.join(env["STORAGE_ROOT"], 'ssl/%s/private_key.pem' % safe_domain_name(domain))
if domain != env['PRIMARY_HOSTNAME'] and os.path.exists(alt_key):
ssl_key = alt_key
ssl_key_is_alt = True
# What SSL certificate will we use?
ssl_certificate_primary = os.path.join(env["STORAGE_ROOT"], 'ssl/ssl_certificate.pem')
if domain == env['PRIMARY_HOSTNAME']:
# For PRIMARY_HOSTNAME, use the one we generated at set-up time.
ssl_certificate = ssl_certificate_primary
else:
# For other domains, we'll probably use a certificate in a different path.
ssl_certificate = os.path.join(env["STORAGE_ROOT"], 'ssl/%s/ssl_certificate.pem' % safe_domain_name(domain))
def is_default_web_root(domain, env):
root = os.path.join(env["STORAGE_ROOT"], "www", safe_domain_name(domain))
return not os.path.exists(root)
# But we can be smart and reuse the main SSL certificate if is has
# a Subject Alternative Name matching this domain. Don't do this if
# the user has uploaded a different private key for this domain.
if not ssl_key_is_alt:
from status_checks import check_certificate
if check_certificate(domain, ssl_certificate_primary, None) == "OK":
ssl_certificate = ssl_certificate_primary
# Where would the CSR go? As with the SSL cert itself, the CSR must be
# different for each domain name.
if domain == env['PRIMARY_HOSTNAME']:
csr_path = os.path.join(env["STORAGE_ROOT"], 'ssl/ssl_cert_sign_req.csr')
else:
csr_path = os.path.join(env["STORAGE_ROOT"], 'ssl/%s/certificate_signing_request.csr' % safe_domain_name(domain))
def get_web_domains_info(env):
www_redirects = set(get_web_domains(env)) - \
set(get_web_domains(env, include_www_redirects=False))
has_root_proxy_or_redirect = set(get_web_domains_with_root_overrides(env))
ssl_certificates = get_ssl_certificates(env)
return ssl_key, ssl_certificate, csr_path
def ensure_ssl_certificate_exists(domain, ssl_key, ssl_certificate, csr_path, env):
# For domains besides PRIMARY_HOSTNAME, generate a self-signed certificate if
# a certificate doesn't already exist. See setup/mail.sh for documentation.
if domain == env['PRIMARY_HOSTNAME']:
return
# Sanity check. Shouldn't happen. A non-primary domain might use this
# certificate (see above), but then the certificate should exist anyway.
if ssl_certificate == os.path.join(env["STORAGE_ROOT"], 'ssl/ssl_certificate.pem'):
return
if os.path.exists(ssl_certificate):
return
os.makedirs(os.path.dirname(ssl_certificate), exist_ok=True)
# Generate a new self-signed certificate using the same private key that we already have.
# Start with a CSR.
shell("check_call", [
"openssl", "req", "-new",
"-key", ssl_key,
"-out", csr_path,
"-sha256",
"-subj", "/C=%s/ST=/L=/O=/CN=%s" % (env["CSR_COUNTRY"], domain)])
# And then make the certificate.
shell("check_call", [
"openssl", "x509", "-req",
"-days", "365",
"-in", csr_path,
"-signkey", ssl_key,
"-out", ssl_certificate])
# for the SSL config panel, get cert status
def check_cert(domain):
try:
tls_cert = get_domain_ssl_files(domain,
ssl_certificates,
env,
allow_missing_cert=True)
except OSError: # PRIMARY_HOSTNAME cert is missing
tls_cert = None
if tls_cert is None:
return ("danger", "No certificate installed.")
cert_status, cert_status_details = check_certificate(
domain, tls_cert["certificate"], tls_cert["private-key"])
if cert_status == "OK":
return ("success", "Signed & valid. " + cert_status_details)
elif cert_status == "SELF-SIGNED":
return ("warning",
"Self-signed. Get a signed certificate to stop warnings.")
else:
return ("danger", "Certificate has a problem: " + cert_status)
return [{
"domain":
domain,
"root":
get_web_root(domain, env),
"custom_root":
get_web_root(domain, env, test_exists=False),
"ssl_certificate":
check_cert(domain),
"static_enabled":
domain not in (www_redirects | has_root_proxy_or_redirect),
} for domain in get_web_domains(env)]

254
management/wkd.py Normal file
View file

@ -0,0 +1,254 @@
#!/usr/local/lib/mailinabox/env/bin/python
# WDK (Web Key Directory) Manager: Facilitates discovery of keys by third-parties
# Current relevant documents: https://tools.ietf.org/id/draft-koch-openpgp-webkey-service-11.html
import pgp
import utils
import rtyaml
import mailconfig
import copy
import shutil
import os
import re
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.backends import default_backend
env = utils.load_environment()
wkdpath = f"{env['GNUPGHOME']}/.wkdlist.yml"
class WKDError(Exception):
"""
Errors specifically related to WKD.
"""
def __init__(self, msg):
self.message = msg
def __str__(self):
return self.message
def sha1(message):
h = hashes.Hash(hashes.SHA1(), default_backend())
h.update(message)
return h.finalize()
def zbase32(digest):
# Crudely check if all quintets are complete
if len(digest) % 5 != 0:
raise ValueError("Digest cannot have incomplete chunks of 40 bits!")
base = "ybndrfg8ejkmcpqxot1uwisza345h769"
encoded = ""
for i in range(0, len(digest), 5):
chunk = int.from_bytes(digest[i:i + 5], byteorder="big")
for j in range(35, -5, -5):
encoded += base[(chunk >> j) & 31]
return encoded
# Strips and exports a key so that only the UID's with the provided email remain.
# This is to comply with the following requirement, set forth in section 5 of the draft:
#
# The mail provider MUST make sure to publish a key in a way
# that only the mail address belonging to the requested user
# is part of the User ID packets included in the returned key.
# Other User ID packets and their associated binding signatures
# MUST be removed before publication.
#
# The arguments "buffer" and "context" are automatically added
# by the pgp.fork_context() decorator.
@pgp.fork_context
def strip_and_export(fpr, target_email, buffer=None, context=None):
context.armor = False # We need to disable armor output for this key
k = pgp.get_key(fpr, context)
if k is None:
return None
# Horrible hack: Because it's a reference (aka pointer), we can pass these around the functions
statusref = {"seq_read": False, "sequence": [], "seq_number": -1}
def parse_key_dump(dump):
UID_REGEX = r".*:.* <(.*)>:.*:([0-9]),.*"
at_least_one_not_deleted = False
lines = dump.decode().split("\n")
for line in lines:
if line[0:3] != "uid":
continue
# It's a uid, find the email and the "tag"
m = re.search(UID_REGEX, line)
if m.group(1) != target_email:
statusref["sequence"].append(f"uid {m.group(2)}")
else:
at_least_one_not_deleted = True
if not at_least_one_not_deleted:
raise WKDError("All UID's in this key would have been deleted!")
statusref["sequence"] += ["deluid", "save"]
statusref["seq_read"] = True
def interaction(request, prompt):
if request in ["GOT_IT", "KEY_CONSIDERED", "KEYEXPIRED", ""]:
return 0
elif request == "GET_BOOL":
# No way to confirm interactively, so we just say yes
return "y" # Yeah, I'd also rather just return True but that doesn't work
elif request == "GET_LINE" and prompt == "keyedit.prompt":
if not statusref["seq_read"]:
buffer.seek(0, os.SEEK_SET)
parse_key_dump(buffer.read())
statusref["seq_number"] += 1
seqnum = statusref["seq_number"]
return statusref["sequence"][seqnum]
else:
raise Exception("No idea of what to do!")
buffer.seek(0, os.SEEK_SET)
buffer.write(b'')
context.interact(k, interaction, sink=buffer)
return pgp.export_key(fpr, context)
def email_compatible_with_key(email, fingerprint):
# 1. Does the user exist?
if not email in mailconfig.get_all_mail_addresses(env):
raise ValueError(f"User or alias {email} not found!")
if fingerprint is not None:
key = pgp.get_key(fingerprint)
# 2. Does the key exist?
if key is None:
raise ValueError(f"The key \"{fingerprint}\" does not exist!")
# 3. Does the key have a user id with the email of the user?
if email not in [u.email for u in key.uids]:
raise WKDError(
f"The key \"{fingerprint}\" has no such UID with the email \"{email}\"!"
)
return key
return None
# Gets a table with all the keys that can be served for each user and/or alias
def get_user_fpr_maps():
uk_maps = {}
for email in mailconfig.get_all_mail_addresses(env):
uk_maps[email] = set()
for key in pgp.get_imported_keys() + [pgp.get_daemon_key()]:
for userid in key.uids:
try:
uk_maps[userid.email].add(key.fpr)
except:
# We don't host this email address, so ignore
pass
return uk_maps
# Gets the current WKD configuration
def get_wkd_config():
# Test
try:
with open(wkdpath, "x"):
pass
except:
pass
with open(wkdpath, "r") as wkdfile:
try:
config = rtyaml.load(wkdfile)
if (type(config) != dict):
return {}
return config
except:
return {}
# Sets the WKD configuration. Takes a dictionary {email: fingerprint}.
# email: An user or alias on this box. e.g. "administrator@example.com"
# fingerprint: The fingerprint of the key we want to bind it to. e.g "0123456789ABCDEF0123456789ABCDEF01234567"
def update_wkd_config(config_sample):
config = dict(config_sample)
for email, fingerprint in config_sample.items():
try:
if fingerprint is None or fingerprint == "":
config.pop(email)
else:
email_compatible_with_key(email, fingerprint)
except Exception as err:
raise err
# All conditions met, do the necessary modifications
with open(wkdpath, "w") as wkdfile:
wkdfile.write(rtyaml.dump(config))
# Looks for incompatible email/key pairs on the WKD configuration file
# and returns the uid indexes for compatible email/key pairs
def parse_wkd_list():
removed = []
uidlist = []
with open(wkdpath, "a+") as wkdfile:
wkdfile.seek(0)
config = {}
try:
config = rtyaml.load(wkdfile)
if (type(config) != dict):
config = {}
except:
config = {}
writeable = copy.deepcopy(config)
for u, k in config.items():
try:
key = email_compatible_with_key(u, k)
# Key is compatible
# Swap with the full-length fingerprint (if somehow this was changed by hand)
writeable[u] = key.fpr
uidlist.append((u, key.fpr))
except:
writeable.pop(u)
removed.append((u, k))
# Shove the updated configuration back in the file
wkdfile.truncate(0)
wkdfile.write(rtyaml.dump(writeable))
return (removed, uidlist)
WKD_LOCATION = "/var/lib/mailinabox/wkd/"
def build_wkd():
# Clean everything
try:
shutil.rmtree(WKD_LOCATION)
except FileNotFoundError:
pass
os.mkdir(WKD_LOCATION, mode=0o755)
# We serve WKD for all our emails and aliases (even if there are no keys)
for domain in mailconfig.get_mail_domains(env, users_only=False):
os.mkdir(f"{WKD_LOCATION}/{domain}/", mode=0o755)
for email, fpr in parse_wkd_list()[1]:
local, domain = email.split("@", 1)
localhash = zbase32(sha1(local.lower().encode()))
with open(f"{WKD_LOCATION}/{domain}/{localhash}", "wb") as k:
k.write(strip_and_export(fpr, email))

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)

119
security.md Normal file
View file

@ -0,0 +1,119 @@
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.
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.
Reporting Security Vulnerabilities
----------------------------------
Security vulnerabilities should be reported to the [project's maintainer](https://joshdata.me) via email.
Threat Model
------------
Nothing is perfectly secure, and an adversary with sufficient resources can always penetrate a system.
The primary goal of Mail-in-a-Box is to make deploying a good mail server easy, so we balance ― as everyone does ― privacy and security concerns with the practicality of actually deploying the system. That means we make certain assumptions about adversaries. We assume that adversaries . . .
* Do not have physical access to the box (i.e., we do not aim to protect the box from physical access).
* Have not been given Unix accounts on the box (i.e., we assume all users with shell access are trusted).
On the other hand, we do assume that adversaries are performing passive surveillance and, possibly, active man-in-the-middle attacks. And so:
* User credentials are always sent through SSH/TLS, never in the clear, with modern TLS settings.
* Outbound mail is sent with the highest level of TLS possible.
* The box advertises its support for [DANE TLSA](https://en.wikipedia.org/wiki/DNS-based_Authentication_of_Named_Entities), when DNSSEC is enabled at the domain name registrar, so that inbound mail is more likely to be transmitted securely.
Additional details follow.
User Credentials
----------------
The box's administrator and its (non-administrative) mail users must sometimes communicate their credentials to the box.
### Services behind TLS
These services are protected by [TLS](https://en.wikipedia.org/wiki/Transport_Layer_Security):
* SMTP Submission (ports 465/587). Mail users submit outbound mail through SMTP with TLS (port 465) or STARTTLS (port 587).
* IMAP/POP (ports 993, 995). Mail users check for incoming mail through IMAP or POP over TLS.
* HTTPS (port 443). Webmail, the Exchange/ActiveSync protocol, the administrative control panel, and any static hosted websites are accessed over HTTPS.
The services all follow these rules:
* TLS certificates are generated with 2048-bit RSA keys and SHA-256 fingerprints. The box provides a self-signed certificate by default. The [setup guide](https://mailinabox.email/guide.html) explains how to verify the certificate fingerprint on first login. Users are encouraged to replace the certificate with a proper CA-signed one. ([source](setup/ssl.sh))
* Only TLSv1.2+ are offered (the older SSL protocols are not offered).
* We track the [Mozilla Intermediate Ciphers Recommendation](https://wiki.mozilla.org/Security/Server_Side_TLS), balancing security with supporting a wide range of mail clients. Diffie-Hellman ciphers use a 2048-bit key for forward secrecy. For more details, see the [output of SSLyze for these ports](tests/tls_results.txt).
Additionally:
* SMTP Submission on port 587 will not accept user credentials without STARTTLS (true also of SMTP on port 25 in case of client misconfiguration), and the submission port won't accept mail without encryption. The minimum cipher key length is 128 bits. (The box is of course configured not to be an open relay. User credentials are required to send outbound mail.) ([source](setup/mail-postfix.sh))
* HTTPS (port 443): The HTTPS Strict Transport Security header is set. A redirect from HTTP to HTTPS is offered. The [Qualys SSL Labs test](https://www.ssllabs.com/ssltest) should report an A+ grade. ([source 1](conf/nginx-ssl.conf), [source 2](conf/nginx.conf))
### Password Storage
The passwords for mail users are stored on disk using the [SHA512-CRYPT](http://man7.org/linux/man-pages/man3/crypt.3.html) hashing scheme. ([source](management/mailconfig.py)) Password changes (as well as changes to control panel two-factor authentication settings) expire any control panel login sessions.
### Console access
Console access (e.g. via SSH) is configured by the system image used to create the box, typically from by a cloud virtual machine provider (e.g. Digital Ocean). Mail-in-a-Box does not set any console access settings, although it will warn the administrator in the System Status Checks if password-based login is turned on.
The [setup guide video](https://mailinabox.email/) explains how to verify the host key fingerprint on first login.
If DNSSEC is enabled at the box's domain name's registrar, the SSHFP record that the box automatically puts into DNS can also be used to verify the host key fingerprint by setting `VerifyHostKeyDNS yes` in your `ssh/.config` file or by logging in with `ssh -o VerifyHostKeyDNS=yes`. ([source](management/dns_update.py))
### Brute-force attack mitigation
`fail2ban` provides some protection from brute-force login attacks (repeated logins that guess account passwords) by blocking offending IP addresses at the network level.
The following services are protected: SSH, IMAP (dovecot), SMTP submission (postfix), webmail (roundcube), Nextcloud/CalDAV/CardDAV (over HTTP), and the Mail-in-a-Box control panel (over HTTP).
Some other services running on the box may be missing fail2ban filters.
Outbound Mail
-------------
The basic protocols of email delivery did not plan for the presence of adversaries on the network. For a number of reasons it is not possible in most cases to guarantee that a connection to a recipient server is secure.
### DNSSEC
The first step in resolving the destination server for an email address is performing a DNS look-up for the MX record of the domain name. The box uses a locally-running [DNSSEC](https://en.wikipedia.org/wiki/DNSSEC)-aware nameserver to perform the lookup. If the domain name has DNSSEC enabled, DNSSEC guards against DNS records being tampered with.
### Encryption
The box (along with the vast majority of mail servers) uses [opportunistic encryption](https://en.wikipedia.org/wiki/Opportunistic_encryption), meaning the mail is encrypted in transit and protected from passive eavesdropping, but it is not protected from an active man-in-the-middle attack. Modern encryption settings (TLSv1 and later, no RC4) will be used to the extent the recipient server supports them. ([source](setup/mail-postfix.sh))
### DANE
If the recipient's domain name supports DNSSEC and has published a [DANE TLSA](https://en.wikipedia.org/wiki/DNS-based_Authentication_of_Named_Entities) record, then on-the-wire encryption is forced between the box and the recipient MTA and this encryption is not subject to a man-in-the-middle attack. The TLSA record contains a certificate fingerprint which the receiving MTA (server) must present to the box. ([source](setup/mail-postfix.sh))
### Domain Policy Records
Domain policy records allow recipient MTAs to detect when the _domain_ part of of the sender address in incoming mail has been spoofed. All outbound mail is signed with [DKIM](https://en.wikipedia.org/wiki/DomainKeys_Identified_Mail) and "quarantine" [DMARC](https://en.wikipedia.org/wiki/DMARC) records are automatically set in DNS. Receiving MTAs that implement DMARC will automatically quarantine mail that is "From:" a domain hosted by the box but which was not sent by the box. (Strong [SPF](https://en.wikipedia.org/wiki/Sender_Policy_Framework) records are also automatically set in DNS.) ([source](management/dns_update.py))
### User Policy
While domain policy records prevent other servers from sending mail with a "From:" header that matches a domain hosted on the box (see above), those policy records do not guarantee that the user portion of the sender email address matches the actual sender. In enterprise environments where the box may host the mail of untrusted users, it is important to guard against users impersonating other users.
The box restricts the envelope sender address (also called the return path or MAIL FROM address --- this is different from the "From:" header) that users may put into outbound mail. The envelope sender address must be either their own email address (their SMTP login username) or any alias that they are listed as a permitted sender of. (There is currently no restriction on the contents of the "From:" header.)
Incoming Mail
-------------
### Encryption Settings
As with outbound email, there is no way to require on-the-wire encryption of incoming mail from all senders. When the box receives an incoming email (SMTP on port 25), it offers encryption (STARTTLS) but cannot require that senders use it because some senders may not support STARTTLS at all and other senders may support STARTTLS but not with the latest protocols/ciphers. To give senders the best chance at making use of encryption, the box offers protocols back to TLSv1 and ciphers with key lengths as low as 112 bits. Modern clients (senders) will make use of the 256-bit ciphers and Diffie-Hellman ciphers with a 2048-bit key for perfect forward secrecy, however. ([source](setup/mail-postfix.sh))
### MTA-STS
The box publishes a SMTP MTA Strict Transport Security ([SMTP MTA-STS](https://en.wikipedia.org/wiki/Simple_Mail_Transfer_Protocol#SMTP_MTA_Strict_Transport_Security)) policy (via DNS and HTTPS) in "enforce" mode. Senders that support MTA-STS will use a secure SMTP connection. (MTA-STS tells senders to connect and expect a signed TLS certificate for the "MX" domain without permitting a fallback to an unencrypted connection.)
### DANE
When DNSSEC is enabled at the box's domain name's registrar, [DANE TLSA](https://en.wikipedia.org/wiki/DNS-based_Authentication_of_Named_Entities) records are automatically published in DNS. Senders supporting DANE will enforce encryption on-the-wire between them and the box --- see the section on DANE for outgoing mail above. ([source](management/dns_update.py))
### Filters
Incoming mail is run through several filters. Email is bounced if the sender's IP address is listed in the [Spamhaus Zen blacklist](http://www.spamhaus.org/zen/) or if the sender's domain is listed in the [Spamhaus Domain Block List](http://www.spamhaus.org/dbl/). Greylisting (with [postgrey](http://postgrey.schweikert.ch/)) is also used to cut down on spam. ([source](setup/mail-postfix.sh))

116
setup/bootstrap.sh Executable file → Normal file
View file

@ -2,41 +2,109 @@
#########################################################
# This script is intended to be run like this:
#
# curl https://.../bootstrap.sh | sudo bash
# curl -L https://power-mailinabox.net/setup.sh | sudo bash
#
#########################################################
if [ -z "$TAG" ]; then
TAG=v0.02
fi
# Are we running as root?
if [[ $EUID -ne 0 ]]; then
echo "This script must be run as root. Did you leave out sudo?"
exit
exit 1
fi
# Go to root's home directory.
cd
if [ ! -f /usr/bin/lsb_release ]; then
# Try installing it (apt install lsb-release)
if [ ! -f /usr/bin/apt-get ]; then
echo "This operating system does not have apt-get! This means it is unsupported!"
echo "This script must be run on a system running one of the following OS-es:"
echo "* Debian 10 (buster)"
echo "* Debian 11 (bullseye)"
echo "* Ubuntu 20.04 LTS (Focal Fossa)"
echo "* Ubuntu 22.04 LTS (Jammy Jellyfish)"
exit 1
fi
echo "Installing lsb-release to understand which operating system we're running..."
apt-get -q -q update
DEBIAN_FRONTEND=noninteractive apt-get -q -q install -y lsb-release < /dev/null
fi
if [ -z "$TAG" ]; then
# Make sure we're running on the correct operating system
OS=$(lsb_release -d | sed 's/.*:\s*//')
if [ "$OS" == "Debian GNU/Linux 11 (bullseye)" ] ||
[ "$(echo $OS | grep -o 'Ubuntu 20.04')" == "Ubuntu 20.04" ] ||
[ "$(echo $OS | grep -o 'Ubuntu 22.04')" == "Ubuntu 22.04" ]
then
TAG=v60.5
elif [ "$OS" == "Debian GNU/Linux 10 (buster)" ]; then
echo "We are going to install the last version of Power Mail-in-a-Box supporting Debian 10 (buster)."
echo "IF THIS IS A NEW INSTALLATION, STOP NOW, AND USE A SUPPORTED DISTRIBUTION INSTEAD (ONE OF THESE):"
echo "* Debian 11 (bullseye)"
echo "* Ubuntu 20.04 LTS (Focal Fossa)"
echo "* Ubuntu 22.04 LTS (Jammy Jellyfish)"
echo
echo "IF YOU'RE UPGRADING THE BOX TO THE LATEST VERSION, PLEASE VISIT THIS PAGE FOR NOTES ON HOW TO"
echo "UPGRADE YOUR SISTEM TO DEBIAN 11 (bullseye)"
echo "https://power-mailinabox.net/buster-eol"
while true; do
read -p "Do you want to proceed? ([Y]es/[N]o) " yn
case $yn in
Yes | Y | yes | y )
break
;;
No | N | no | n )
echo "Installation cancelled."
exit 1
;;
* )
;;
esac
done
TAG=v56.5
else
echo "This script must be run on a system running one of the following OS-es:"
echo "* Debian 11 (bullseye)"
echo "* Ubuntu 20.04 LTS (Focal Fossa)"
echo "* Ubuntu 22.04 LTS (Jammy Jellyfish)"
exit 1
fi
fi
# Clone the Mail-in-a-Box repository if it doesn't exist.
if [ ! -d mailinabox ]; then
echo Installing git . . .
apt-get -q -q install -y git
echo Downloading Mail-in-a-Box . . .
git clone -b $TAG --depth 1 https://github.com/mail-in-a-box/mailinabox 2> /dev/null
cd mailinabox
# If it does exist, update it.
else
echo Updating Mail-in-a-Box to $TAG . . .
cd mailinabox
git fetch
if ! git checkout -q $TAG; then
echo "Update failed. Did you modify something in `pwd`?"
exit
if [ ! -d $HOME/mailinabox ]; then
if [ ! -f /usr/bin/git ]; then
echo Installing git . . .
apt-get -q -q update
DEBIAN_FRONTEND=noninteractive apt-get -q -q install -y git < /dev/null
echo
fi
echo Downloading Mail-in-a-Box $TAG. . .
git clone \
-b $TAG --depth 1 \
https://git.nibbletools.com/beenull/power-mailinabox \
$HOME/mailinabox \
< /dev/null 2> /dev/null
echo
fi
# Change directory to it.
cd $HOME/mailinabox
# Update it.
if [ "$TAG" != "$(git describe --tags)" ]; then
echo Updating Mail-in-a-Box to $TAG . . .
git fetch --depth 1 --force --prune origin tag $TAG
if ! git checkout -q $TAG; then
echo "Update failed. Did you modify something in $(pwd)?"
exit 1
fi
echo
fi
# Start setup script.

107
setup/dkim.sh Normal file → Executable file
View file

@ -1,13 +1,17 @@
# OpenDKIM: Sign outgoing mail with DKIM
########################################
# After this, you'll still need to run dns_update.sh to get the DKIM
# signature in the DNS zones.
#!/bin/bash
# OpenDKIM
# --------
#
# OpenDKIM provides a service that puts a DKIM signature on outbound mail.
#
# The DNS configuration for DKIM is done in the management daemon.
source setup/functions.sh # load our functions
source /etc/mailinabox.conf # load global vars
# Install DKIM
apt_install opendkim opendkim-tools
# Install DKIM...
echo Installing OpenDKIM/OpenDMARC...
apt_install opendkim opendkim-tools opendmarc
# Make sure configuration directories exist.
mkdir -p /etc/opendkim;
@ -17,43 +21,104 @@ mkdir -p $STORAGE_ROOT/mail/dkim
# Not quite sure why.
echo "127.0.0.1" > /etc/opendkim/TrustedHosts
# We need to at least create these files, since we reference them later.
# Otherwise, opendkim startup will fail
touch /etc/opendkim/KeyTable
touch /etc/opendkim/SigningTable
if grep -q "ExternalIgnoreList" /etc/opendkim.conf; then
true; # already done
true # already done #NODOC
else
# Add various configuration options to the end.
# Add various configuration options to the end of `opendkim.conf`.
cat >> /etc/opendkim.conf << EOF;
Canonicalization relaxed/simple
MinimumKeyBits 1024
ExternalIgnoreList refile:/etc/opendkim/TrustedHosts
InternalHosts refile:/etc/opendkim/TrustedHosts
KeyTable refile:/etc/opendkim/KeyTable
SigningTable refile:/etc/opendkim/SigningTable
Socket inet:8891@localhost
Socket inet:8891@127.0.0.1
RequireSafeKeys false
EOF
fi
# Create a new DKIM key if we don't have one already. This creates
# mail.private and mail.txt in $STORAGE_ROOT/mail/dkim. The former
# is the actual private key and the latter is the suggested DNS TXT
# entry which we'll want to include in our DNS setup.
# Create a new DKIM key. This creates mail.private and mail.txt
# in $STORAGE_ROOT/mail/dkim. The former is the private key and
# the latter is the suggested DNS TXT entry which we'll include
# in our DNS setup. Note that the files are named after the
# 'selector' of the key, which we can change later on to support
# key rotation.
#
# A 1024-bit key is seen as a minimum standard by several providers
# such as Google. But they and others use a 2048 bit key, so we'll
# do the same. Keys beyond 2048 bits may exceed DNS record limits.
if [ ! -f "$STORAGE_ROOT/mail/dkim/mail.private" ]; then
# Should we specify -h rsa-sha256?
opendkim-genkey -r -s mail -D $STORAGE_ROOT/mail/dkim
opendkim-genkey -b 2048 -r -s mail -D $STORAGE_ROOT/mail/dkim
fi
# Ensure files are owned by the opendkim user and are private otherwise.
chown -R opendkim:opendkim $STORAGE_ROOT/mail/dkim
chmod go-rwx $STORAGE_ROOT/mail/dkim
# Add OpenDKIM as a milter to postfix, which is how it intercepts outgoing
# mail to perform the signing (by adding a mail header).
# Be careful. If we add other milters later, it needs to be concatenated on the smtpd_milters line.
tools/editconf.py /etc/postfix/main.cf \
smtpd_milters=inet:127.0.0.1:8891 \
management/editconf.py /etc/opendmarc.conf -s \
"Syslog=true" \
"Socket=inet:8893@[127.0.0.1]" \
"FailureReports=true"
# 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
# itself, or because you don't trust the arriving header. This added header is
# used by spamassassin to evaluate the mail for spamminess.
management/editconf.py /etc/opendmarc.conf -s \
"SPFIgnoreResults=true"
# SPFSelfValidate causes the filter to perform a fallback SPF check itself
# when it can find no SPF results in the message header. If SPFIgnoreResults
# is also set, it never looks for SPF results in headers and always performs
# the SPF check itself when this is set. This added header is used by
# spamassassin to evaluate the mail for spamminess.
management/editconf.py /etc/opendmarc.conf -s \
"SPFSelfValidate=true"
# Enables generation of failure reports for sending domains that publish a
# "none" policy.
management/editconf.py /etc/opendmarc.conf -s \
"FailureReportsOnNone=true"
# AlwaysAddARHeader Adds an "Authentication-Results:" header field even to
# unsigned messages from domains with no "signs all" policy. The reported DKIM
# result will be "none" in such cases. Normally unsigned mail from non-strict
# domains does not cause the results header field to be added. This added header
# is used by spamassassin to evaluate the mail for spamminess.
management/editconf.py /etc/opendkim.conf -s \
"AlwaysAddARHeader=true"
# Add OpenDKIM and OpenDMARC as milters to postfix, which is how OpenDKIM
# intercepts outgoing mail to perform the signing (by adding a mail header)
# and how they both intercept incoming mail to add Authentication-Results
# headers. The order possibly/probably matters: OpenDMARC relies on the
# OpenDKIM Authentication-Results header already being present.
#
# Be careful. If we add other milters later, this needs to be concatenated
# on the smtpd_milters line.
#
# The OpenDMARC milter is skipped in the SMTP submission listener by
# configuring smtpd_milters there to only list the OpenDKIM milter
# (see mail-postfix.sh).
management/editconf.py /etc/postfix/main.cf \
"smtpd_milters=inet:127.0.0.1:8891 inet:127.0.0.1:8893"\
non_smtpd_milters=\$smtpd_milters \
milter_default_action=accept
# We need to explicitly enable the opendmarc service, or it will not start
hide_output systemctl enable opendmarc
# Restart services.
restart_service opendkim
restart_service opendmarc
restart_service postfix

View file

@ -1,5 +1,5 @@
#!/bin/bash
# DNS: Configure a DNS server to host our own DNS
# DNS
# -----------------------------------------------
# This script installs packages, but the DNS zone files are only
@ -8,72 +8,142 @@
# mail users & aliases created by the user later.
source setup/functions.sh # load our functions
source /etc/mailinabox.conf # load global vars
# Install `nsd`, our DNS server software, and `ldnsutils` which helps
# us sign zones for DNSSEC.
# 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 /etc/nsd
mkdir -p /etc/nsd/zones
touch /etc/nsd/zones.conf
touch /etc/nsd/nsd.conf
# ...but first, we have to create the user because the
# current Ubuntu forgets to do so in the .deb
# see issue #25 and https://bugs.launchpad.net/ubuntu/+source/nsd/+bug/1311886
if id nsd > /dev/null 2>&1; then
true; #echo "nsd user exists... good"; #NODOC
else
useradd nsd;
fi
cat > /etc/nsd/nsd.conf << EOF;
# Do not edit. Overwritten by Mail-in-a-Box setup.
server:
hide-version: yes
logfile: "/var/log/nsd.log"
# Okay now install the packages.
# identify the server (CH TXT ID.SERVER entry).
identity: ""
# The directory for zonefile: files.
zonesdir: "/etc/nsd/zones"
# Allows NSD to bind to IP addresses that are not (yet) added to the
# network interface. This allows nsd to start even if the network stack
# isn't fully ready, which apparently happens in some cases.
# See https://www.nlnetlabs.nl/projects/nsd/nsd.conf.5.html.
ip-transparent: yes
EOF
# Since we have bind9 listening on localhost for locally-generated
# DNS queries that require a recursive nameserver, and the system
# might have other network interfaces for e.g. tunnelling, we have
# to be specific about the network interfaces that nsd binds to.
for ip in $PRIVATE_IP $PRIVATE_IPV6; do
echo " ip-address: $ip" >> /etc/nsd/nsd.conf;
done
# Create a directory for additional configuration directives, including
# the zones.conf file written out by our management daemon.
echo "include: /etc/nsd/nsd.conf.d/*.conf" >> /etc/nsd/nsd.conf;
# Remove the old location of zones.conf that we generate. It will
# now be stored in /etc/nsd/nsd.conf.d.
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
# Prepare nsd's configuration.
sudo mkdir -p /var/run/nsd
# Create DNSSEC signing keys.
mkdir -p "$STORAGE_ROOT/dns/dnssec";
if [ ! -f "$STORAGE_ROOT/dns/dnssec/keys.conf" ]; then
echo "Generating DNSSEC signing keys. This may take a few minutes..."
# Create the Key-Signing Key (KSK) (-k) which is the so-called
# Secure Entry Point. Use a NSEC3-compatible algorithm (best
# practice), and a nice and long keylength. The domain name we
# provide ("_domain_") doesn't matter -- we'll use the same
# keys for all our domains.
KSK=$(umask 077; cd $STORAGE_ROOT/dns/dnssec; ldns-keygen -a RSASHA1-NSEC3-SHA1 -b 2048 -k _domain_);
# TLDs, registrars, and validating nameservers don't all support the same algorithms,
# so we'll generate keys using a few different algorithms so that dns_update.py can
# choose which algorithm to use when generating the zonefiles. See #1953 for recent
# discussion. File for previously used algorithms (i.e. RSASHA1-NSEC3-SHA1) may still
# be in the output directory, and we'll continue to support signing zones with them
# so that trust isn't broken with deployed DS records, but we won't generate those
# keys on new systems.
FIRST=1 #NODOC
for algo in RSASHA256 ECDSAP256SHA256; do
if [ ! -f "$STORAGE_ROOT/dns/dnssec/$algo.conf" ]; then
if [ $FIRST == 1 ]; then
echo "Generating DNSSEC signing keys..."
FIRST=0 #NODOC
fi
# Create the Key-Signing Key (KSK) (with `-k`) which is the so-called
# Secure Entry Point. The domain name we provide ("_domain_") doesn't
# matter -- we'll use the same keys for all our domains.
#
# `ldns-keygen` outputs the new key's filename to stdout, which
# we're capturing into the `KSK` variable.
#
# ldns-keygen uses /dev/random for generating random numbers by default.
# This is slow and unecessary if we ensure /dev/urandom is seeded properly,
# so we use /dev/urandom. See system.sh for an explanation. See #596, #115.
# (This previously used -b 2048 but it's unclear if this setting makes sense
# for non-RSA keys, so it's removed. The RSA-based keys are not recommended
# anymore anyway.)
KSK=$(umask 077; cd $STORAGE_ROOT/dns/dnssec; ldns-keygen -r /dev/urandom -a $algo -k _domain_);
# Now create a Zone-Signing Key (ZSK) which is expected to be
# rotated more often than a KSK, although we have no plans to
# rotate it (and doing so would be difficult to do without
# disturbing DNS availability.) Omit '-k' and use a shorter key.
ZSK=$(umask 077; cd $STORAGE_ROOT/dns/dnssec; ldns-keygen -a RSASHA1-NSEC3-SHA1 -b 1024 _domain_);
# disturbing DNS availability.) Omit `-k`.
# (This previously used -b 1024 but it's unclear if this setting makes sense
# for non-RSA keys, so it's removed.)
ZSK=$(umask 077; cd $STORAGE_ROOT/dns/dnssec; ldns-keygen -r /dev/urandom -a $algo _domain_);
# These generate two sets of files like:
#
# * `K_domain_.+007+08882.ds`: DS record to provide to domain name registrar
# * `K_domain_.+007+08882.key`: public key (goes into DS record & upstream DNS provider like your registrar)
# * `K_domain_.+007+08882.ds`: DS record normally provided to domain name registrar (but it's actually invalid with `_domain_` so we don't use this file)
# * `K_domain_.+007+08882.key`: public key
# * `K_domain_.+007+08882.private`: private key (secret!)
# The filenames are unpredictable and encode the key generation
# options. So we'll store the names of the files we just generated.
# We might have multiple keys down the road. This will identify
# what keys are the current keys.
cat > $STORAGE_ROOT/dns/dnssec/keys.conf << EOF;
cat > $STORAGE_ROOT/dns/dnssec/$algo.conf << EOF;
KSK=$KSK
ZSK=$ZSK
EOF
fi
# Force the dns_update script to be run every day to re-sign zones for DNSSEC.
# And loop to do the next algorithm...
done
# Force the dns_update script to be run every day to re-sign zones for DNSSEC
# before they expire. When we sign zones (in `dns_update.py`) we specify a
# 30-day validation window, so we had better re-sign before then.
cat > /etc/cron.daily/mailinabox-dnssec << EOF;
#!/bin/bash
# Mail-in-a-Box
# Re-sign any DNS zones with DNSSEC because the signatures expire periodically.
`pwd`/tools/dns_update
$(pwd)/tools/dns_update
EOF
chmod +x /etc/cron.daily/mailinabox-dnssec

View file

@ -1,16 +1,16 @@
# If there aren't any mail users yet, create one.
if [ -z "`tools/mail.py user`" ]; then
# The outut of "tools/mail.py user" is a list of mail users. If there
if [ -z "$(management/cli.py user)" ]; then
# The outut of "management/cli.py user" is a list of mail users. If there
# aren't any yet, it'll be empty.
# If we didn't ask for an email address at the start, do so now.
if [ -z "$EMAIL_ADDR" ]; then
if [ -z "${EMAIL_ADDR:-}" ]; then
# In an interactive shell, ask the user for an email address.
if [ -z "$NONINTERACTIVE" ]; then
if [ -z "${NONINTERACTIVE:-}" ]; then
input_box "Mail Account" \
"Let's create your first mail account.
\n\nWhat email address do you want?" \
me@`get_default_hostname` \
me@$(get_default_hostname) \
EMAIL_ADDR
if [ -z "$EMAIL_ADDR" ]; then
@ -35,7 +35,7 @@ if [ -z "`tools/mail.py user`" ]; then
else
# Use me@PRIMARY_HOSTNAME
EMAIL_ADDR=me@$PRIMARY_HOSTNAME
EMAIL_PW=1234
EMAIL_PW=12345678
echo
echo "Creating a new administrative mail account for $EMAIL_ADDR with password $EMAIL_PW."
echo
@ -47,11 +47,14 @@ if [ -z "`tools/mail.py user`" ]; then
fi
# Create the user's mail account. This will ask for a password if none was given above.
tools/mail.py user add $EMAIL_ADDR $EMAIL_PW
management/cli.py user add $EMAIL_ADDR ${EMAIL_PW:-}
# Make it an admin.
hide_output tools/mail.py user make-admin $EMAIL_ADDR
hide_output management/cli.py user make-admin $EMAIL_ADDR
# Create an alias to which we'll direct all automatically-created administrative aliases.
tools/mail.py alias add administrator@$PRIMARY_HOSTNAME $EMAIL_ADDR
fi
management/cli.py alias add administrator@$PRIMARY_HOSTNAME $EMAIL_ADDR > /dev/null
fi
# Kick everything that may have been missed
management/mailconfig.py update

View file

@ -1,63 +1,59 @@
# Turn on "strict mode." See http://redsymbol.net/articles/unofficial-bash-strict-mode/.
# -e: exit if any command unexpectedly fails.
# -u: exit if we have a variable typo.
# -o pipefail: don't ignore errors in the non-last command in a pipeline
set -euo pipefail
function hide_output {
# This function hides the output of a command unless the command fails
# and returns a non-zero exit code.
# Get a temporary file.
OUTPUT=$(tempfile)
OUTPUT=$(mktemp)
# Execute command, redirecting stderr/stdout to the temporary file.
$@ &> $OUTPUT
# Execute command, redirecting stderr/stdout to the temporary file. Since we
# check the return code ourselves, disable 'set -e' temporarily.
set +e
"$@" &> "$OUTPUT"
E=$?
set -e
# If the command failed, show the output that was captured in the temporary file.
if [ $? != 0 ]; then
if [ $E != 0 ]; then
# Something failed.
echo
echo FAILED: $@
echo FAILED: "$@"
echo -----------------------------------------
cat $OUTPUT
cat "$OUTPUT"
echo -----------------------------------------
exit $E
fi
# Remove temporary file.
rm -f $OUTPUT
}
function apt_install {
# Report any packages already installed.
PACKAGES=$@
TO_INSTALL=""
ALREADY_INSTALLED=""
for pkg in $PACKAGES; do
if dpkg -s $pkg 2>/dev/null | grep "^Status: install ok installed" > /dev/null; then
if [[ ! -z "$ALREADY_INSTALLED" ]]; then ALREADY_INSTALLED="$ALREADY_INSTALLED, "; fi
ALREADY_INSTALLED="$ALREADY_INSTALLED$pkg (`dpkg -s $pkg | grep ^Version: | sed -e 's/.*: //'`)"
else
TO_INSTALL="$TO_INSTALL""$pkg "
fi
done
# List the packages already installed.
if [[ ! -z "$ALREADY_INSTALLED" ]]; then
echo already installed: $ALREADY_INSTALLED
fi
# List the packages about to be installed.
if [[ ! -z "$TO_INSTALL" ]]; then
echo installing $TO_INSTALL...
fi
# 'DEBIAN_FRONTEND=noninteractive' is to prevent dbconfig-common from asking you questions.
function apt_get_quiet {
# Run apt-get in a totally non-interactive mode.
#
# Somehow all of these options are needed to get it to not ask the user
# questions about a) whether to proceed (-y), b) package options (noninteractive),
# and c) what to do about files changed locally (we don't cause that to happen but
# some VM providers muck with their images; -o).
#
# Although we could pass -qq to apt-get to make output quieter, many packages write to stdout
# and stderr things that aren't really important. Use our hide_output function to capture
# all of that and only show it if there is a problem (i.e. if apt_get returns a failure exit status).
#
# Also note that we still include the whole original package list in the apt-get command in
# case it wants to upgrade anything, I guess? Maybe we can remove it. Doesn't normally make
# a difference.
DEBIAN_FRONTEND=noninteractive \
hide_output \
apt-get -y install $PACKAGES
DEBIAN_FRONTEND=noninteractive hide_output apt-get -y -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confnew" "$@"
}
function apt_install {
# Install a bunch of packages. We used to report which packages were already
# installed and which needed installing, before just running an 'apt-get
# install' for all of the packages. Calling `dpkg` on each package is slow,
# and doesn't affect what we actually do, except in the messages, so let's
# not do that anymore.
apt_get_quiet install "$@"
}
function get_default_hostname {
@ -78,7 +74,7 @@ function get_publicip_from_web_service {
#
# Pass '4' or '6' as an argument to this function to specify
# what type of address to get (IPv4, IPv6).
curl -$1 --fail --silent --max-time 15 icanhazip.com 2>/dev/null
curl -$1 --fail --silent --max-time 15 icanhazip.com 2>/dev/null || /bin/true
}
function get_default_privateip {
@ -121,7 +117,7 @@ function get_default_privateip {
if [ "$1" == "6" ]; then target=2001:4860:4860::8888; fi
# Get the route information.
route=$(ip -$1 -o route get $target | grep -v unreachable)
route=$(ip -$1 -o route get $target 2>/dev/null | grep -v unreachable)
# Parse the address out of the route information.
address=$(echo $route | sed "s/.* src \([^ ]*\).*/\1/")
@ -134,13 +130,19 @@ function get_default_privateip {
fi
echo $address
}
function ufw_allow {
if [ -z "$DISABLE_FIREWALL" ]; then
if [ -z "${DISABLE_FIREWALL:-}" ]; then
# ufw has completely unhelpful output
ufw allow $1 > /dev/null;
ufw allow "$1" > /dev/null;
fi
}
function ufw_limit {
if [ -z "${DISABLE_FIREWALL:-}" ]; then
# ufw has completely unhelpful output
ufw limit "$1" > /dev/null;
fi
}
@ -157,10 +159,13 @@ function input_box {
# input_box "title" "prompt" "defaultvalue" VARIABLE
# The user's input will be stored in the variable VARIABLE.
# The exit code from dialog will be stored in VARIABLE_EXITCODE.
# Temporarily turn off 'set -e' because we need the dialog return code.
declare -n result=$4
declare -n result_code=$4_EXITCODE
set +e
result=$(dialog --stdout --title "$1" --inputbox "$2" 0 0 "$3")
result_code=$?
set -e
}
function input_menu {
@ -170,6 +175,88 @@ function input_menu {
declare -n result=$4
declare -n result_code=$4_EXITCODE
local IFS=^$'\n'
set +e
result=$(dialog --stdout --title "$1" --menu "$2" 0 0 0 $3)
result_code=$?
set -e
}
function wget_verify {
# Downloads a file from the web and checks that it matches
# a provided hash. If the comparison fails, exit immediately.
URL=$1
HASH=$2
DEST=$3
CHECKSUM="$HASH $DEST"
rm -f $DEST
hide_output wget -O $DEST $URL
if ! echo "$CHECKSUM" | sha1sum --check --strict > /dev/null; then
echo "------------------------------------------------------------"
echo "Download of $URL did not match expected checksum."
echo "Found:"
sha1sum $DEST
echo
echo "Expected:"
echo "$CHECKSUM"
rm -f $DEST
exit 1
fi
}
function git_clone {
# Clones a git repository, checks out a particular commit or tag,
# and moves the repository (or a subdirectory in it) to some path.
# We use separate clone and checkout because -b only supports tags
# and branches, but we sometimes want to reference a commit hash
# directly when the repo doesn't provide a tag.
REPO=$1
TREEISH=$2
SUBDIR=$3
TARGETPATH=$4
TMPPATH=/tmp/git-clone-$$
rm -rf $TMPPATH $TARGETPATH
git clone -q $REPO $TMPPATH || exit 1
(cd $TMPPATH; git checkout -q $TREEISH;) || exit 1
mv $TMPPATH/$SUBDIR $TARGETPATH
rm -rf $TMPPATH
}
function php_version {
php --version | head -n 1 | cut -d " " -f 2 | cut -d "." -f 1,2
}
function python_version {
python3 --version | cut -d " " -f 2 | cut -d "." -f 1,2
}
export OS_UNSUPPORTED=0
export OS_DEBIAN_10=1
export OS_UBUNTU_2004=2
export OS_DEBIAN_11=3
export OS_UBUNTU_2204=4
function get_os_code {
# A lot of if-statements here - dirty code looking tasting today
ID=$(lsb_release -is)
VER=$(lsb_release -rs)
if [[ $ID == "Debian" ]]; then
if [[ $VER == "10" ]]; then
echo $OS_DEBIAN_10
return 0
elif [[ $VER == "11" ]]; then
echo $OS_DEBIAN_11
return 0
fi
elif [[ $ID == "Ubuntu" ]]; then
if [[ $VER == "20.04" ]]; then
echo $OS_UBUNTU_2004
return 0
elif [[ $VER == "22.04" ]]; then
echo $OS_UBUNTU_2204
return 0
fi
fi
echo $OS_UNSUPPORTED
}

View file

@ -1,9 +1,9 @@
#!/bin/bash
#
# Dovecot (IMAP and LDA)
# Dovecot (IMAP/POP and LDA)
# ----------------------
#
# Dovecot is *both* the IMAP server (the protocol that email applications
# Dovecot is *both* the IMAP/POP server (the protocol that email applications
# use to query a mailbox) as well as the local delivery agent (LDA),
# meaning it is responsible for writing emails to mailbox storage on disk.
# You could imagine why these things would be bundled together.
@ -18,39 +18,83 @@
source setup/functions.sh # load our functions
source /etc/mailinabox.conf # load global vars
# ### Install packages and basic setup
# Install packages for dovecot. These are all core dovecot plugins,
# but dovecot-lucene is packaged by *us* in the Mail-in-a-Box PPA,
# not by Ubuntu.
echo "Installing Dovecot (IMAP server)..."
apt_install \
dovecot-core dovecot-imapd dovecot-lmtpd dovecot-sqlite sqlite3 \
dovecot-core dovecot-imapd dovecot-pop3d dovecot-lmtpd dovecot-sqlite sqlite3 \
dovecot-sieve dovecot-managesieved
# The dovecot-imapd and dovecot-lmtpd packages automatically enable IMAP and LMTP protocols.
# The `dovecot-imapd`, `dovecot-pop3d`, and `dovecot-lmtpd` packages automatically
# enable IMAP, POP and LMTP protocols.
# Set the location where we'll store user mailboxes.
tools/editconf.py /etc/dovecot/conf.d/10-mail.conf \
# Set basic daemon options.
# The `default_process_limit` is 100, which constrains the total number
# of active IMAP connections (at, say, 5 open connections per user that
# would be 20 users). Set it to 250 times the number of cores this
# machine has, so on a two-core machine that's 500 processes/100 users).
# The `default_vsz_limit` is the maximum amount of virtual memory that
# can be allocated. It should be set *reasonably high* to avoid allocation
# issues with larger mailboxes. We're setting it to 1/3 of the total
# available memory (physical mem + swap) to be sure.
# See here for discussion:
# - https://www.dovecot.org/list/dovecot/2012-August/137569.html
# - https://www.dovecot.org/list/dovecot/2011-December/132455.html
management/editconf.py /etc/dovecot/conf.d/10-master.conf \
default_process_limit=$(echo "$(nproc) * 250" | bc) \
default_vsz_limit=$(echo "$(free -tm | tail -1 | awk '{print $2}') / 3" | bc)M \
log_path=/var/log/mail.log
# The inotify `max_user_instances` default is 128, which constrains
# the total number of watched (IMAP IDLE push) folders by open connections.
# See http://www.dovecot.org/pipermail/dovecot/2013-March/088834.html.
# A reboot is required for this to take effect (which we don't do as
# as a part of setup). Test with `cat /proc/sys/fs/inotify/max_user_instances`.
management/editconf.py /etc/sysctl.conf \
fs.inotify.max_user_instances=1024
# Set the location where we'll store user mailboxes. '%d' is the domain name and '%n' is the
# username part of the user's email address. We'll ensure that no bad domains or email addresses
# are created within the management daemon.
management/editconf.py /etc/dovecot/conf.d/10-mail.conf \
mail_location=maildir:$STORAGE_ROOT/mail/mailboxes/%d/%n \
mail_privileged_group=mail \
first_valid_uid=0
# ### IMAP
# Create, subscribe, and mark as special folders: INBOX, Drafts, Sent, Trash, Spam and Archive.
cp conf/dovecot/conf.d/15-mailboxes.conf /etc/dovecot/conf.d/
cp conf/dovecot/conf.d/20-imap.conf /etc/dovecot/conf.d/
cp conf/dovecot/conf.d/90-quota.conf /etc/dovecot/conf.d/
# ### IMAP/POP
# Require that passwords are sent over SSL only, and allow the usual IMAP authentication mechanisms.
# The LOGIN mechanism is supposedly for Microsoft products like Outlook to do SMTP login (I guess
# since we're using Dovecot to handle SMTP authentication?).
tools/editconf.py /etc/dovecot/conf.d/10-auth.conf \
management/editconf.py /etc/dovecot/conf.d/10-auth.conf \
disable_plaintext_auth=yes \
"auth_mechanisms=plain login"
# Enable SSL, specify the location of the SSL certificate and private key files,
# and allow only good ciphers per http://baldric.net/2013/12/07/tls-ciphers-in-postfix-and-dovecot/.
tools/editconf.py /etc/dovecot/conf.d/10-ssl.conf \
# Enable SSL, specify the location of the SSL certificate and private key files.
# Use Mozilla's "Intermediate" recommendations at https://ssl-config.mozilla.org/#server=dovecot&server-version=2.2.33&config=intermediate&openssl-version=1.1.1,
# except that the current version of Dovecot does not have a TLSv1.3 setting, so we only use TLSv1.2.
management/editconf.py /etc/dovecot/conf.d/10-ssl.conf \
ssl=required \
"ssl_cert=<$STORAGE_ROOT/ssl/ssl_certificate.pem" \
"ssl_key=<$STORAGE_ROOT/ssl/ssl_private_key.pem" \
"ssl_cipher_list=TLSv1+HIGH !SSLv2 !RC4 !aNULL !eNULL !3DES @STRENGTH"
"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_prefer_server_ciphers=no" \
"ssl_dh_parameters_length=2048" \
"ssl_dh=<$STORAGE_ROOT/ssl/dh2048.pem"
# Disable in-the-clear IMAP and POP because we're paranoid (we haven't even
# enabled POP).
# 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
# are made available (IMAPS on port 993; POP3S on port 995).
sed -i "s/#port = 143/port = 0/" /etc/dovecot/conf.d/10-master.conf
sed -i "s/#port = 110/port = 0/" /etc/dovecot/conf.d/10-master.conf
@ -59,14 +103,22 @@ sed -i "s/#port = 110/port = 0/" /etc/dovecot/conf.d/10-master.conf
# this are minimal. But for good measure, let's go to 4 minutes to halve the
# bandwidth and number of times the device's networking might be woken up.
# The risk is that if the connection is silent for too long it might be reset
# by a peer. See #129 and http://razor.occams.info/blog/2014/08/09/how-bad-is-imap-idle/.
tools/editconf.py /etc/dovecot/conf.d/20-imap.conf \
# by a peer. See [#129](https://github.com/mail-in-a-box/mailinabox/issues/129)
# and [How bad is IMAP IDLE](http://razor.occams.info/blog/2014/08/09/how-bad-is-imap-idle/).
management/editconf.py /etc/dovecot/conf.d/20-imap.conf \
imap_idle_notify_interval="4 mins"
# Set POP3 UIDL.
# UIDLs are used by POP3 clients to keep track of what messages they've downloaded.
# For new POP3 servers, the easiest way to set up UIDLs is to use IMAP's UIDVALIDITY
# and UID values, the default in Dovecot.
management/editconf.py /etc/dovecot/conf.d/20-pop3.conf \
pop3_uidl_format="%08Xu%08Xv"
# ### LDA (LMTP)
# Enable Dovecot's LDA service with the LMTP protocol. It will listen
# in port 10026, and Spamassassin will be configured to pass mail there.
# on port 10026, and Spamassassin will be configured to pass mail there.
#
# The disabled unix socket listener is normally how Postfix and Dovecot
# would communicate (see the Postfix setup script for the corresponding
@ -86,43 +138,63 @@ service lmtp {
}
}
# Enable imap-login on localhost to allow the user_external plugin
# for Nextcloud to do imap authentication. (See #1577)
service imap-login {
inet_listener imap {
address = 127.0.0.1
port = 143
}
}
protocol imap {
mail_max_userip_connections = 20
mail_max_userip_connections = 40
}
EOF
# Setting a postmaster_address seems to be required or LMTP won't start.
tools/editconf.py /etc/dovecot/conf.d/15-lda.conf \
# Setting a `postmaster_address` is required or LMTP won't start. An alias
# will be created automatically by our management daemon.
management/editconf.py /etc/dovecot/conf.d/15-lda.conf \
postmaster_address=postmaster@$PRIMARY_HOSTNAME
# ### Sieve
# Enable the Dovecot sieve plugin which let's users run scripts that process
# mail as it comes in. We'll also set a global script that moves mail marked
# as spam by Spamassassin into the user's Spam folder.
# mail as it comes in.
sed -i "s/#mail_plugins = .*/mail_plugins = \$mail_plugins sieve/" /etc/dovecot/conf.d/20-lmtp.conf
# Configure sieve. We'll create a global script that moves mail marked
# as spam by Spamassassin into the user's Spam folder.
#
# * `sieve_before`: The path to our global sieve which handles moving spam to the Spam folder.
#
# * `sieve_before2`: The path to our global sieve directory for sieve which can contain .sieve files
# to run globally for every user before their own sieve files run.
#
# * `sieve_after`: The path to our global sieve directory which can contain .sieve files
# to run globally for every user after their own sieve files run.
#
# * `sieve`: The path to the user's main active script. ManageSieve will create a symbolic
# link here to the actual sieve script. It should not be in the mailbox directory
# (because then it might appear as a folder) and it should not be in the sieve_dir
# (because then I suppose it might appear to the user as one of their scripts).
# * `sieve_dir`: Directory for :personal include scripts for the include extension. This
# is also where the ManageSieve service stores the user's scripts.
cat > /etc/dovecot/conf.d/99-local-sieve.conf << EOF;
plugin {
# The path to our global sieve which handles moving spam to the Spam folder.
sieve_before = /etc/dovecot/sieve-spam.sieve
# The path to the user's main active script. ManageSieve will create a symbolic
# link here to the actual sieve script. It should not be in the mailbox directory
# (because then it might appear as a folder) and it should not be in the sieve_dir
# (because then I suppose it might appear to the user as one of their scripts).
sieve_before2 = $STORAGE_ROOT/mail/sieve/global_before
sieve_after = $STORAGE_ROOT/mail/sieve/global_after
sieve = $STORAGE_ROOT/mail/sieve/%d/%n.sieve
# Directory for :personal include scripts for the include extension. This
# is also where the ManageSieve service stores the user's scripts.
sieve_dir = $STORAGE_ROOT/mail/sieve/%d/%n
sieve_redirect_envelope_from = recipient
sieve_vacation_send_from_recipient = yes
}
EOF
# Copy the global sieve script into where we've told Dovecot to look for it. Then
# compile it. Global scripts must be compiled now because Dovecot won't have
# permission later.
cp `pwd`/conf/sieve-spam.txt /etc/dovecot/sieve-spam.sieve
cp conf/sieve-spam.txt /etc/dovecot/sieve-spam.sieve
sievec /etc/dovecot/sieve-spam.sieve
# PERMISSIONS
@ -137,10 +209,16 @@ chown -R mail.mail $STORAGE_ROOT/mail/mailboxes
# Same for the sieve scripts.
mkdir -p $STORAGE_ROOT/mail/sieve
mkdir -p $STORAGE_ROOT/mail/sieve/global_before
mkdir -p $STORAGE_ROOT/mail/sieve/global_after
chown -R mail.mail $STORAGE_ROOT/mail/sieve
# Allow the IMAP port in the firewall.
# Allow the IMAP/POP ports in the firewall.
ufw_allow imaps
ufw_allow pop3s
# Allow the Sieve port in the firewall.
ufw_allow sieve
# Restart services.
restart_service dovecot

View file

@ -13,11 +13,11 @@
# destinations according to aliases, and passses email on to
# another service for local mail delivery.
#
# The first hop in local mail delivery is to Spamassassin via
# LMTP. Spamassassin then passes mail over to Dovecot for
# The first hop in local mail delivery is to spampd via
# LMTP. spampd then passes mail over to Dovecot for
# storage in the user's mailbox.
#
# Postfix also listens on port 587 (SMTP+STARTLS) for
# Postfix also listens on ports 465/587 (SMTPS, SMTP+STARTLS) for
# connections from users who can authenticate and then sends
# their email out to the outside world. Postfix queries Dovecot
# to authenticate users.
@ -32,127 +32,283 @@ source /etc/mailinabox.conf # load global vars
# ### Install packages.
apt_install postfix postgrey postfix-pcre ca-certificates
# Install postfix's packages.
#
# * `postfix`: The SMTP server.
# * `postfix-pcre`: Enables header filtering.
# * `postgrey`: A mail policy service that soft-rejects mail the first time
# it is received. Spammers don't usually try agian. Legitimate mail
# always will.
# * `ca-certificates`: A trust store used to squelch postfix warnings about
# untrusted opportunistically-encrypted connections.
echo "Installing Postfix (SMTP server)..."
apt_install postfix postfix-sqlite postfix-pcre postgrey ca-certificates libsasl2-modules
# ### Basic Settings
# Have postfix listen on all network interfaces, set our name (the Debian default seems to be localhost),
# and set the name of the local machine to localhost for xxx@localhost mail (but I don't think this will have any effect because
# there is no true local mail delivery). Also set the banner (must have the hostname first, then anything).
tools/editconf.py /etc/postfix/main.cf \
# Set some basic settings...
#
# * Have postfix listen on all network interfaces.
# * Make outgoing connections on a particular interface (if multihomed) so that SPF passes on the receiving side.
# * Set our name (the Debian default seems to be "localhost" but make it our hostname).
# * Set the name of the local machine to localhost, which means xxx@localhost is delivered locally, although we don't use it.
# * Set the SMTP banner (which must have the hostname first, then anything).
management/editconf.py /etc/postfix/main.cf \
inet_interfaces=all \
smtp_bind_address=$PRIVATE_IP \
smtp_bind_address6=$PRIVATE_IPV6 \
myhostname=$PRIMARY_HOSTNAME\
smtpd_banner="\$myhostname ESMTP Hi, I'm a Mail-in-a-Box (Ubuntu/Postfix; see https://mailinabox.email/)" \
smtpd_banner="\$myhostname ESMTP Power Mail-in-a-Box (Postfix)" \
mydestination=localhost
# Tweak some queue settings:
# * Inform users when their e-mail delivery is delayed more than 3 hours (default is not to warn).
# * Stop trying to send an undeliverable e-mail after 2 days (instead of 5), and for bounce messages just try for 1 day.
management/editconf.py /etc/postfix/main.cf \
delay_warning_time=3h \
maximal_queue_lifetime=2d \
bounce_queue_lifetime=1d
# ### Outgoing Mail
# Enable the 'submission' port 587 smtpd server and tweak its settings.
# Enable the 'submission' ports 465 and 587 and tweak their settings.
#
# * Require the best ciphers for incoming connections per http://baldric.net/2013/12/07/tls-ciphers-in-postfix-and-dovecot/.
# but without affecting opportunistic TLS on incoming mail, which will allow any cipher (it's better than none).
# * Enable authentication. It's disabled globally so that it is disabled on port 25,
# so we need to explicitly enable it here.
# * Do not add the OpenDMAC Authentication-Results header. That should only be added
# on incoming mail. Omit the OpenDMARC milter by re-setting smtpd_milters to the
# OpenDKIM milter only. See dkim.sh.
# * Even though we dont allow auth over non-TLS connections (smtpd_tls_auth_only below, and without auth the client cant
# send outbound mail), don't allow non-TLS mail submission on this port anyway to prevent accidental misconfiguration.
# Setting smtpd_tls_security_level=encrypt also triggers the use of the 'mandatory' settings below (but this is ignored with smtpd_tls_wrappermode=yes.)
# * Give it a different name in syslog to distinguish it from the port 25 smtpd server.
# * Add a new cleanup service specific to the submission service ('authclean')
# that filters out privacy-sensitive headers on mail being sent out by
# authenticated users.
tools/editconf.py /etc/postfix/master.cf -s -w \
"submission=inet n - - - - smtpd
# authenticated users. By default Postfix also applies this to attached
# emails but we turn this off by setting nested_header_checks empty.
management/editconf.py /etc/postfix/master.cf -s -w \
"smtps=inet n - - - - smtpd
-o smtpd_tls_wrappermode=yes
-o smtpd_sasl_auth_enable=yes
-o syslog_name=postfix/submission
-o smtpd_tls_ciphers=high -o smtpd_tls_protocols=!SSLv2,!SSLv3
-o smtpd_milters=inet:127.0.0.1:8891
-o cleanup_service_name=authclean" \
"submission=inet n - - - - smtpd
-o smtpd_sasl_auth_enable=yes
-o syslog_name=postfix/submission
-o smtpd_milters=inet:127.0.0.1:8891
-o smtpd_tls_security_level=encrypt
-o cleanup_service_name=authclean" \
"authclean=unix n - - - 0 cleanup
-o header_checks=pcre:/etc/postfix/outgoing_mail_header_filters"
-o header_checks=pcre:/etc/postfix/outgoing_mail_header_filters
-o nested_header_checks="
# Install the `outgoing_mail_header_filters` file required by the new 'authclean' service.
cp conf/postfix_outgoing_mail_header_filters /etc/postfix/outgoing_mail_header_filters
# Enable TLS on these and all other connections (i.e. ports 25 *and* 587) and
# require TLS before a user is allowed to authenticate. This also makes
# opportunistic TLS available on *incoming* mail.
tools/editconf.py /etc/postfix/main.cf \
# Modify the `outgoing_mail_header_filters` file to use the local machine name and ip
# on the first received header line. This may help reduce the spam score of email by
# removing the 127.0.0.1 reference.
sed -i "s/PRIMARY_HOSTNAME/$PRIMARY_HOSTNAME/" /etc/postfix/outgoing_mail_header_filters
sed -i "s/PUBLIC_IP/$PUBLIC_IP/" /etc/postfix/outgoing_mail_header_filters
# Enable TLS on incoming connections. It is not required on port 25, allowing for opportunistic
# encryption. On ports 465 and 587 it is mandatory (see above). Shared and non-shared settings are
# given here. Shared settings include:
# * Require TLS before a user is allowed to authenticate.
# * Set the path to the server TLS certificate and 2048-bit DH parameters for old DH ciphers.
# For port 25 only:
# * Disable extremely old versions of TLS and extremely unsafe ciphers, but some mail servers out in
# the world are very far behind and if we disable too much, they may not be able to use TLS and
# won't fall back to cleartext. So we don't disable too much. smtpd_tls_exclude_ciphers applies to
# both port 25 and port 587, but because we override the cipher list for both, it probably isn't used.
# Use Mozilla's "Old" recommendations at https://ssl-config.mozilla.org/#server=postfix&version=3.4.8&config=old&openssl=1.1.1&guideline=5.6
management/editconf.py /etc/postfix/main.cf \
smtpd_tls_security_level=may\
smtpd_tls_auth_only=yes \
smtpd_tls_cert_file=$STORAGE_ROOT/ssl/ssl_certificate.pem \
smtpd_tls_key_file=$STORAGE_ROOT/ssl/ssl_private_key.pem \
smtpd_tls_dh1024_param_file=$STORAGE_ROOT/ssl/dh2048.pem \
smtpd_tls_protocols="!SSLv2,!SSLv3" \
smtpd_tls_ciphers=medium \
tls_medium_cipherlist=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:DHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES256-SHA256:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:DES-CBC3-SHA \
smtpd_tls_exclude_ciphers=aNULL,DES,3DES,MD5,DES+MD5,RC4 \
tls_preempt_cipherlist=yes \
smtpd_tls_received_header=yes
# For ports 465/587 (via the 'mandatory' settings):
# * Use Mozilla's "Intermediate" TLS recommendations from https://ssl-config.mozilla.org/#server=postfix&server-version=3.3.0&config=intermediate&openssl-version=1.1.1
# using and overriding the "high" cipher list so we don't conflict with the more permissive settings for port 25.
management/editconf.py /etc/postfix/main.cf \
smtpd_tls_mandatory_protocols="!SSLv2,!SSLv3,!TLSv1,!TLSv1.1" \
smtpd_tls_mandatory_ciphers=high \
tls_high_cipherlist=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 \
smtpd_tls_mandatory_exclude_ciphers=aNULL,DES,3DES,MD5,DES+MD5,RC4
# Prevent non-authenticated users from sending mail that requires being
# relayed elsewhere. We don't want to be an "open relay". On outbound
# mail, require one of:
#
# * permit_sasl_authenticated: Authenticated users (i.e. on port 587).
# * permit_mynetworks: Mail that originates locally.
# * reject_unauth_destination: No one else. (Permits mail whose destination is local and rejects other mail.)
tools/editconf.py /etc/postfix/main.cf \
# * `permit_sasl_authenticated`: Authenticated users (i.e. on port 465/587).
# * `permit_mynetworks`: Mail that originates locally.
# * `reject_unauth_destination`: No one else. (Permits mail whose destination is local and rejects other mail.)
management/editconf.py /etc/postfix/main.cf \
smtpd_relay_restrictions=permit_sasl_authenticated,permit_mynetworks,reject_unauth_destination
# ### DANE
#
# When connecting to remote SMTP servers, prefer TLS and use DANE if available.
#
# Prefering ("opportunistic") TLS means Postfix will accept whatever SSL certificate the remote
# end provides, if the remote end offers STARTTLS during the connection. DANE takes this a
# step further:
#
# Prefering ("opportunistic") TLS means Postfix will use TLS if the remote end
# offers it, otherwise it will transmit the message in the clear. Postfix will
# accept whatever SSL certificate the remote end provides. Opportunistic TLS
# protects against passive easvesdropping (but not man-in-the-middle attacks).
# Since we'd rather have poor encryption than none at all, we use Mozilla's
# "Old" recommendations at https://ssl-config.mozilla.org/#server=postfix&server-version=3.3.0&config=old&openssl-version=1.1.1
# for opportunistic encryption but "Intermediate" recommendations when DANE
# is used (see next and above). The cipher lists are set above.
# DANE takes this a step further:
# Postfix queries DNS for the TLSA record on the destination MX host. If no TLSA records are found,
# then opportunistic TLS is used. Otherwise the server certificate must match the TLSA records
# or else the mail bounces. TLSA also requires DNSSEC on the MX host. Postfix doesn't do DNSSEC
# itself but assumes the system's nameserver does and reports DNSSEC status. Thus this also
# relies on our local bind9 server being present and smtp_dns_support_level being set to dnssec
# to use it.
# relies on our local DNS server (see system.sh) and `smtp_dns_support_level=dnssec`.
#
# The smtp_tls_CAfile is superflous, but it turns warnings in the logs about untrusted certs
# into notices about trusted certs. Since in these cases Postfix is doing opportunistic TLS,
# it does not care about whether the remote certificate is trusted. But, looking at the logs,
# it's nice to be able to see that the connection was in fact encrypted for the right party.
# The CA file is provided by the package ca-certificates.
tools/editconf.py /etc/postfix/main.cf \
# The `smtp_tls_CAfile` is superflous, but it eliminates warnings in the logs about untrusted certs,
# which we don't care about seeing because Postfix is doing opportunistic TLS anyway. Better to encrypt,
# even if we don't know if it's to the right party, than to not encrypt at all. Instead we'll
# now see notices about trusted certs. The CA file is provided by the package `ca-certificates`.
management/editconf.py /etc/postfix/main.cf \
smtp_tls_protocols=\!SSLv2,\!SSLv3 \
smtp_tls_ciphers=medium \
smtp_tls_exclude_ciphers=aNULL,RC4 \
smtp_tls_security_level=dane \
smtp_dns_support_level=dnssec \
smtp_tls_mandatory_protocols="!SSLv2,!SSLv3,!TLSv1,!TLSv1.1" \
smtp_tls_mandatory_ciphers=high \
smtp_tls_CAfile=/etc/ssl/certs/ca-certificates.crt \
smtp_tls_loglevel=2
# ### Incoming Mail
# Pass any incoming mail over to a local delivery agent. Spamassassin
# will act as the LDA agent at first. It is listening on port 10025
# with LMTP. Spamassassin will pass the mail over to Dovecot after.
# Pass mail to spampd, which acts as the local delivery agent (LDA),
# which then passes the mail over to the Dovecot LMTP server after.
# spampd runs on port 10025 by default.
#
# In a basic setup we would pass mail directly to Dovecot by setting
# virtual_transport to `lmtp:unix:private/dovecot-lmtp`.
#
tools/editconf.py /etc/postfix/main.cf virtual_transport=lmtp:[127.0.0.1]:10025
management/editconf.py /etc/postfix/main.cf "virtual_transport=lmtp:[127.0.0.1]:10025"
# 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.
management/editconf.py /etc/postfix/main.cf -e lmtp_destination_recipient_limit=
# Who can send mail to us? Some basic filters.
#
# * reject_non_fqdn_sender: Reject not-nice-looking return paths.
# * reject_unknown_sender_domain: Reject return paths with invalid domains.
# * reject_rhsbl_sender: Reject return paths that use blacklisted domains.
# * permit_sasl_authenticated: Authenticated users (i.e. on port 587) can skip further checks.
# * permit_mynetworks: Mail that originates locally can skip further checks.
# * reject_rbl_client: Reject connections from IP addresses blacklisted in zen.spamhaus.org
# * reject_unlisted_recipient: Although Postfix will reject mail to unknown recipients, it's nicer to reject such mail ahead of greylisting rather than after.
# * check_policy_service: Apply greylisting using postgrey.
# * `reject_non_fqdn_sender`: Reject not-nice-looking return paths.
# * `reject_unknown_sender_domain`: Reject return paths with invalid domains.
# * `reject_authenticated_sender_login_mismatch`: Reject if mail FROM address does not match the client SASL login
# * `reject_rhsbl_sender`: Reject return paths that use blacklisted domains.
# * `permit_sasl_authenticated`: Authenticated users (i.e. on port 587) can skip further checks.
# * `permit_mynetworks`: Mail that originates locally can skip further checks.
# * `reject_rbl_client`: Reject connections from IP addresses blacklisted in zen.spamhaus.org
# * `reject_unlisted_recipient`: Although Postfix will reject mail to unknown recipients, it's nicer to reject such mail ahead of greylisting rather than after.
# * `check_policy_service`: Apply greylisting using postgrey.
#
# Notes:
# permit_dnswl_client can pass through mail from whitelisted IP addresses, which would be good to put before greylisting
# so these IPs get mail delivered quickly. But when an IP is not listed in the permit_dnswl_client list (i.e. it is not
# whitelisted) then postfix does a DEFER_IF_REJECT, which results in all "unknown user" sorts of messages turning into
# "450 4.7.1 Client host rejected: Service unavailable". This is a retry code, so the mail doesn't properly bounce.
tools/editconf.py /etc/postfix/main.cf \
smtpd_sender_restrictions="reject_non_fqdn_sender,reject_unknown_sender_domain,reject_rhsbl_sender dbl.spamhaus.org" \
smtpd_recipient_restrictions=permit_sasl_authenticated,permit_mynetworks,"reject_rbl_client zen.spamhaus.org",reject_unlisted_recipient,"check_policy_service inet:127.0.0.1:10023"
# Notes: #NODOC
# permit_dnswl_client can pass through mail from whitelisted IP addresses, which would be good to put before greylisting #NODOC
# so these IPs get mail delivered quickly. But when an IP is not listed in the permit_dnswl_client list (i.e. it is not #NODOC
# whitelisted) then postfix does a DEFER_IF_REJECT, which results in all "unknown user" sorts of messages turning into #NODOC
# "450 4.7.1 Client host rejected: Service unavailable". This is a retry code, so the mail doesn't properly bounce. #NODOC
management/editconf.py /etc/postfix/main.cf \
smtpd_sender_restrictions="reject_non_fqdn_sender,reject_unknown_sender_domain,reject_authenticated_sender_login_mismatch,reject_rhsbl_sender dbl.spamhaus.org" \
smtpd_recipient_restrictions="check_recipient_access sqlite:/etc/postfix/noreply-addresses.cf",permit_sasl_authenticated,permit_mynetworks,"reject_rbl_client zen.spamhaus.org",reject_unlisted_recipient,"check_policy_service inet:127.0.0.1:10023","check_policy_service inet:127.0.0.1:12340"
# Postfix connects to Postgrey on the 127.0.0.1 interface specifically. Ensure that
# Postgrey listens on the same interface (and not IPv6, for instance).
# A lot of legit mail servers try to resend before 300 seconds.
# 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
# e-mails really latter, delay of greylisting has been set to
# 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.
management/editconf.py /etc/default/postgrey \
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
cat > /etc/cron.daily/mailinabox-postgrey-whitelist << EOF;
#!/bin/bash
# Mail-in-a-Box
# check we have a postgrey_whitelist_clients file and that it is not older than 28 days
if [ ! -f /etc/postgrey/whitelist_clients ] || find /etc/postgrey/whitelist_clients -mtime +28 | grep -q '.' ; then
# ok we need to update the file, so lets try to fetch it
if curl https://postgrey.schweikert.ch/pub/postgrey_whitelist_clients --output /tmp/postgrey_whitelist_clients -sS --fail > /dev/null 2>&1 ; then
# if fetching hasn't failed yet then check it is a plain text file
# curl manual states that --fail sometimes still produces output
# this final check will at least check the output is not html
# before moving it into place
if [ "\$(file -b --mime-type /tmp/postgrey_whitelist_clients)" == "text/plain" ]; then
mv /tmp/postgrey_whitelist_clients /etc/postgrey/whitelist_clients
service postgrey restart
else
rm /tmp/postgrey_whitelist_clients
fi
fi
fi
EOF
chmod +x /etc/cron.daily/mailinabox-postgrey-whitelist
/etc/cron.daily/mailinabox-postgrey-whitelist
# Increase the message size limit from 10MB to 128MB.
tools/editconf.py /etc/postfix/main.cf \
# The same limit is specified in nginx.conf for mail submitted via webmail and Z-Push.
management/editconf.py /etc/postfix/main.cf \
message_size_limit=134217728
# Store default configurations for SMTP relays:
management/editconf.py /etc/postfix/main.cf \
smtp_sasl_auth_enable=yes \
smtp_sasl_password_maps="hash:/etc/postfix/sasl_passwd" \
smtp_sasl_security_options=noanonymous \
smtp_sasl_tls_security_options=noanonymous \
smtp_tls_security_level=encrypt \
header_size_limit=4096000
touch /etc/postfix/sasl_passwd
chmod 600 /etc/postfix/sasl_passwd
postmap /etc/postfix/sasl_passwd
# Allow the two SMTP ports in the firewall.
ufw_allow smtp
ufw_allow smtps
ufw_allow submission
# Restart services
restart_service postfix
restart_service postgrey

View file

@ -5,7 +5,7 @@
#
# This script configures user authentication for Dovecot
# and Postfix (which relies on Dovecot) and destination
# validation by quering an Sqlite3 database of mail users.
# validation by quering an Sqlite3 database of mail users.
source setup/functions.sh # load our functions
source /etc/mailinabox.conf # load global vars
@ -20,10 +20,25 @@ db_path=$STORAGE_ROOT/mail/users.sqlite
# Create an empty database if it doesn't yet exist.
if [ ! -f $db_path ]; then
echo Creating new user database: $db_path;
echo "CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT, email TEXT NOT NULL UNIQUE, password TEXT NOT NULL, extra, privileges TEXT NOT NULL DEFAULT '');" | sqlite3 $db_path;
echo "CREATE TABLE aliases (id INTEGER PRIMARY KEY AUTOINCREMENT, source TEXT NOT NULL UNIQUE, destination TEXT NOT NULL);" | sqlite3 $db_path;
echo "CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT, email TEXT NOT NULL UNIQUE, password TEXT NOT NULL, extra, privileges TEXT NOT NULL DEFAULT '', quota TEXT NOT NULL DEFAULT '0');" | sqlite3 $db_path;
echo "CREATE TABLE aliases (id INTEGER PRIMARY KEY AUTOINCREMENT, source TEXT NOT NULL UNIQUE, destination TEXT NOT NULL, permitted_senders TEXT);" | sqlite3 $db_path;
echo "CREATE TABLE noreply (id INTEGER PRIMARY KEY AUTOINCREMENT, email TEXT NOT NULL UNIQUE);" | sqlite3 $db_path
echo "CREATE TABLE mfa (id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, type TEXT NOT NULL, secret TEXT NOT NULL, mru_token TEXT, label TEXT, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE);" | sqlite3 $db_path;
echo "CREATE TABLE auto_aliases (id INTEGER PRIMARY KEY AUTOINCREMENT, source TEXT NOT NULL UNIQUE, destination TEXT NOT NULL, permitted_senders TEXT);" | sqlite3 $db_path;
else
sql=$(sqlite3 $db_path "SELECT sql FROM sqlite_master WHERE name = 'users'");
if echo $sql | grep --invert-match quota; then
echo "ALTER TABLE users ADD COLUMN quota TEXT NOT NULL DEFAULT '0';" | sqlite3 $db_path;
fi
fi
# Recover the database if it was hit by the Roundcube password changer "bug" (#85)
# If the journal_mode is set to wal, postfix cannot read it and we wouldn't
# be able to send or receive mail.
#
# This operation is idempotent so it's safe to run even in healthy databases, too.
echo "PRAGMA journal_mode=delete;" | sqlite3 $db_path > /dev/null
# ### User Authentication
# Have Dovecot query our database, and not system users, for authentication.
@ -38,22 +53,23 @@ passdb {
args = /etc/dovecot/dovecot-sql.conf.ext
}
userdb {
driver = static
args = uid=mail gid=mail home=$STORAGE_ROOT/mail/mailboxes/%d/%n
driver = sql
args = /etc/dovecot/dovecot-sql.conf.ext
}
EOF
# Configure the SQL to query for a user's password.
# Configure the SQL to query for a user's metadata and password.
cat > /etc/dovecot/dovecot-sql.conf.ext << EOF;
driver = sqlite
connect = $db_path
default_pass_scheme = SHA512-CRYPT
password_query = SELECT email as user, password FROM users WHERE email='%u';
user_query = SELECT email AS user, "mail" as uid, "mail" as gid, "$STORAGE_ROOT/mail/mailboxes/%d/%n" as home, '*:bytes=' || quota AS quota_rule FROM users WHERE email='%u';
iterate_query = SELECT email AS user FROM users;
EOF
chmod 0600 /etc/dovecot/dovecot-sql.conf.ext # per Dovecot instructions
# Have Dovecot provide an authorization service that Postfix can access & use.
# Drew Crawford sets the auth-worker process to run as the mail user, but we don't care if it runs as root.
cat > /etc/dovecot/conf.d/99-local-auth.conf << EOF;
service auth {
unix_listener /var/spool/postfix/private/auth {
@ -64,38 +80,96 @@ service auth {
}
EOF
# And have Postfix use that service.
tools/editconf.py /etc/postfix/main.cf \
# And have Postfix use that service. We *disable* it here
# so that authentication is not permitted on port 25 (which
# does not run DKIM on relayed mail, so outbound mail isn't
# correct, see #830), but we enable it specifically for the
# submission port.
management/editconf.py /etc/postfix/main.cf \
smtpd_sasl_type=dovecot \
smtpd_sasl_path=private/auth \
smtpd_sasl_auth_enable=yes
smtpd_sasl_auth_enable=no
# ### Sender Validation
# We use Postfix's reject_authenticated_sender_login_mismatch filter to
# prevent intra-domain spoofing by logged in but untrusted users in outbound
# email. In all outbound mail (the sender has authenticated), the MAIL FROM
# address (aka envelope or return path address) must be "owned" by the user
# who authenticated. An SQL query will find who are the owners of any given
# address.
management/editconf.py /etc/postfix/main.cf \
smtpd_sender_login_maps=sqlite:/etc/postfix/sender-login-maps.cf
# Postfix will query the exact address first, where the priority will be alias
# records first, then user records. If there are no matches for the exact
# address, then Postfix will query just the domain part, which we call
# catch-alls and domain aliases. A NULL permitted_senders column means to
# take the value from the destination column.
cat > /etc/postfix/sender-login-maps.cf << EOF;
dbpath=$db_path
query = SELECT permitted_senders FROM (SELECT permitted_senders, 0 AS priority FROM aliases WHERE source='%s' AND permitted_senders IS NOT NULL UNION SELECT destination AS permitted_senders, 1 AS priority FROM aliases WHERE source='%s' AND permitted_senders IS NULL UNION SELECT email as permitted_senders, 2 AS priority FROM users WHERE email='%s') ORDER BY priority LIMIT 1;
EOF
# ### Destination Validation
# Use a Sqlite3 database to check whether a destination email address exists,
# and to perform any email alias rewrites in Postfix.
tools/editconf.py /etc/postfix/main.cf \
# and to perform any email alias rewrites in Postfix. Additionally, we disable
# SMTPUTF8 because Dovecot's LMTP server that delivers mail to inboxes does
# not support it, and if a message is received with the SMTPUTF8 flag it will
# bounce.
management/editconf.py /etc/postfix/main.cf \
smtputf8_enable=no \
virtual_mailbox_domains=sqlite:/etc/postfix/virtual-mailbox-domains.cf \
virtual_mailbox_maps=sqlite:/etc/postfix/virtual-mailbox-maps.cf \
virtual_alias_maps=sqlite:/etc/postfix/virtual-alias-maps.cf \
local_recipient_maps=\$virtual_mailbox_maps
# SQL statement to check if we handle mail for a domain, either for users or aliases.
# SQL statement to check if we handle incoming mail for a domain, either for users or aliases.
cat > /etc/postfix/virtual-mailbox-domains.cf << EOF;
dbpath=$db_path
query = SELECT 1 FROM users WHERE email LIKE '%%@%s' UNION SELECT 1 FROM aliases WHERE source LIKE '%%@%s'
query = SELECT 1 FROM users WHERE email LIKE '%%@%s' UNION SELECT 1 FROM aliases WHERE source LIKE '%%@%s' UNION SELECT 1 FROM auto_aliases WHERE source LIKE '%%@%s'
EOF
# SQL statement to check if we handle mail for a user.
# SQL statement to check if we handle incoming mail for a user.
cat > /etc/postfix/virtual-mailbox-maps.cf << EOF;
dbpath=$db_path
query = SELECT 1 FROM users WHERE email='%s'
EOF
# SQL statement to rewrite an email address if an alias is present.
#
# Postfix makes multiple queries for each incoming mail. It first
# queries the whole email address, then just the user part in certain
# locally-directed cases (but we don't use this), then just `@`+the
# domain part. The first query that returns something wins. See
# http://www.postfix.org/virtual.5.html.
#
# virtual-alias-maps has precedence over virtual-mailbox-maps, but
# we don't want catch-alls and domain aliases to catch mail for users
# that have been defined on those domains. To fix this, we not only
# query the aliases table but also the users table when resolving
# aliases, i.e. we turn users into aliases from themselves to
# themselves. That means users will match in postfix's first query
# before postfix gets to the third query for catch-alls/domain alises.
#
# If there is both an alias and a user for the same address either
# might be returned by the UNION, so the whole query is wrapped in
# another select that prioritizes the alias definition to preserve
# postfix's preference for aliases for whole email addresses.
#
# Since we might have alias records with an empty destination because
# it might have just permitted_senders, skip any records with an
# empty destination here so that other lower priority rules might match.
cat > /etc/postfix/virtual-alias-maps.cf << EOF;
dbpath=$db_path
query = SELECT destination FROM aliases WHERE source='%s'
query = SELECT destination from (SELECT destination, 0 as priority FROM aliases WHERE source='%s' AND destination<>'' UNION SELECT email as destination, 1 as priority FROM users WHERE email='%s' UNION SELECT destination, 2 as priority FROM auto_aliases WHERE source='%s' AND destination<>'') ORDER BY priority LIMIT 1;
EOF
# SQL statement to check if we're sending to a noreply address.
cat > /etc/postfix/noreply-addresses.cf << EOF;
dbpath=$db_path
query = SELECT 'REJECT This address is not ready to receive email.' FROM noreply WHERE email='%s'
EOF
# Restart Services
@ -104,4 +178,5 @@ EOF
restart_service postfix
restart_service dovecot
# force a recalculation of all user quotas
doveadm quota recalc -A

View file

@ -1,9 +1,69 @@
#!/bin/bash
source setup/functions.sh
source /etc/mailinabox.conf # load global vars
apt_install python3-flask links duplicity libyaml-dev python3-dnspython python3-dateutil
hide_output pip3 install rtyaml
echo "Installing Mail-in-a-Box system management daemon..."
# DEPENDENCIES
# virtualenv is used to isolate the Python 3 packages we
# install via pip from the system-installed packages.
#
# certbot installs EFF's certbot which we use to
# provision free TLS certificates.
#
# gcc and build tools are required to install the latest version
# of duplicity
apt_install python3-pip python3-gpg virtualenv certbot rsync librsync2 python3-fasteners python3-future python3-lockfile \
gcc python3-dev librsync-dev gettext
apt_get_quiet remove --autoremove --purge duplicity || /bin/true
# Duplicity does the actual backups.
# b2sdk is used for backblaze backups.
# boto3 is used for amazon aws backups.
# Both are installed outside the pipenv, so they can be used by duplicity
hide_output pip3 install --upgrade b2sdk boto3 duplicity
# Create a virtualenv for the installation of Python 3 packages
# used by the management daemon.
inst_dir=/usr/local/lib/mailinabox
mkdir -p $inst_dir
venv=$inst_dir/env
if [ ! -d $venv ]; then
hide_output virtualenv -ppython3 $venv
elif [ ! -f $venv/.oscode ]; then
echo "Re-creating Python environment..."
rm -rf $venv
hide_output virtualenv -ppython3 $venv
elif [ "$(cat $venv/.oscode)" != $(get_os_code) ]; then
echo "Existing management environment is from an earlier version of the OS you're running."
echo "Re-creating Python environment..."
rm -rf $venv
hide_output virtualenv -ppython3 $venv
fi
# Upgrade pip because the Ubuntu-packaged version is out of date.
hide_output $venv/bin/pip install --upgrade pip
# Install other Python 3 packages used by the management daemon.
# The first line is the packages that Josh maintains himself!
# NOTE: email_validator is repeated in setup/questions.sh, so please keep the versions synced.
hide_output $venv/bin/pip install --upgrade \
rtyaml "email_validator>=1.0.0" "exclusiveprocess" \
flask dnspython python-dateutil expiringdict gunicorn \
qrcode[pil] pyotp \
"idna>=2.0.0" "cryptography==2.2.2" boto psutil postfix-mta-sts-resolver boto3 b2sdk
# Make the venv use the packaged gpgme bindings (the ones pip provides are severely out-of-date)
if [ ! -d $venv/lib/python$(python_version)/site-packages/gpg/ ]; then
ln -s /usr/lib/python3/dist-packages/gpg/ $venv/lib/python$(python_version)/site-packages/
fi
echo $(get_os_code) > $venv/.oscode
# CONFIGURATION
# Create a backup directory and a random key for encrypting backups.
mkdir -p $STORAGE_ROOT/backup
@ -11,24 +71,90 @@ if [ ! -f $STORAGE_ROOT/backup/secret_key.txt ]; then
$(umask 077; openssl rand -base64 2048 > $STORAGE_ROOT/backup/secret_key.txt)
fi
# Link the management server daemon into a well known location.
rm -f /usr/local/bin/mailinabox-daemon
ln -s `pwd`/management/daemon.py /usr/local/bin/mailinabox-daemon
# Download jQuery and Bootstrap local files
# Make sure we have the directory to save to.
assets_dir=$inst_dir/vendor/assets
rm -rf $assets_dir
mkdir -p $assets_dir
# jQuery CDN URL
jquery_version=3.6.1
jquery_url=https://code.jquery.com # Check this link for new versions
# Get jQuery
wget_verify $jquery_url/jquery-$jquery_version.min.js ea61688671d0c3044f2c5b2f2c4af0a6620ac6c2 $assets_dir/jquery.min.js
# Bootstrap CDN URL
# See https://github.com/twbs/bootstrap/releases to check for new versions
bootstrap_version=5.2.2
bootstrap_url=https://github.com/twbs/bootstrap/releases/download/v$bootstrap_version/bootstrap-$bootstrap_version-dist.zip
# Get Bootstrap
wget_verify $bootstrap_url 740b34c22cef5c2f12a34f084b813ea308fedf74 /tmp/bootstrap.zip
unzip -q /tmp/bootstrap.zip -d $assets_dir
mv $assets_dir/bootstrap-$bootstrap_version-dist $assets_dir/bootstrap
rm -f /tmp/bootstrap.zip
# FontAwesome CDN URL
# See https://github.com/FortAwesome/Font-Awesome/releases to check for new versions
fontawesome_version=6.2.1
fontawesome_url=https://github.com/FortAwesome/Font-Awesome/releases/download/$fontawesome_version/fontawesome-free-$fontawesome_version-web.zip
# Get FontAwesome
wget_verify $fontawesome_url cd0f2bcc9653b56e3e2dd82d6598aa6bbca8d796 /tmp/fontawesome.zip
unzip -q /tmp/fontawesome.zip -d $assets_dir
mv $assets_dir/fontawesome-free-$fontawesome_version-web $assets_dir/fontawesome
rm -f /tmp/fontawesome.zip
# Create an init script to start the management daemon and keep it
# running after a reboot.
rm -f /etc/init.d/mailinabox
ln -s $(pwd)/conf/management-initscript /etc/init.d/mailinabox
hide_output update-rc.d mailinabox defaults
# Perform a daily backup.
cat > /etc/cron.daily/mailinabox-backup << EOF;
# 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;
#!/bin/bash
# Mail-in-a-Box --- Do not edit / will be overwritten on update.
# Perform a backup.
$(pwd)/management/backup.py
EOF
chmod +x /etc/cron.daily/mailinabox-backup
# Set character encoding flags to ensure that any non-ASCII don't cause problems.
export LANGUAGE=en_US.UTF-8
export LC_ALL=en_US.UTF-8
export LANG=en_US.UTF-8
export LC_TYPE=en_US.UTF-8
# Start it.
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
export PYTHONPATH=$(pwd)/management
exec gunicorn -b localhost:10222 -w 1 --timeout 630 wsgi:app
EOF
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
hide_output systemctl link -f /lib/systemd/system/mailinabox.service
hide_output systemctl daemon-reload
hide_output systemctl enable mailinabox.service
# Perform nightly tasks at 3am in system time: take a backup, run
# status checks and email the administrator any changes.
minute=$((RANDOM % 60)) # avoid overloading mailinabox.email
cat > /etc/cron.d/mailinabox-nightly << EOF;
# Mail-in-a-Box --- Do not edit / will be overwritten on update.
# Run nightly tasks: backup, status checks.
$minute 3 * * * root (cd $(pwd) && management/daily_tasks.sh)
EOF
# Start the management server.
restart_service mailinabox
# FOR DEVELOPMENT PURPOSES ONLY:
# If there is a CA certificate in the folder, install it.
# MIAB will only accept a manual certificate installation
# if it is signed by a CA trusted by it.
if [[ -f mailinabox-ca.crt ]]; then
echo "Custom CA certificate detected. Installing..."
rm -f /usr/local/share/ca-certificates/mailinabox-ca.crt
cp mailinabox-ca.crt /usr/local/share/ca-certificates/
hide_output update-ca-certificates --fresh
fi

View file

@ -60,6 +60,144 @@ def migration_5(env):
# The secret key for encrypting backups was world readable. Fix here.
os.chmod(os.path.join(env["STORAGE_ROOT"], 'backup/secret_key.txt'), 0o600)
def migration_6(env):
# We now will generate multiple DNSSEC keys for different algorithms, since TLDs may
# not support them all. .email only supports RSA/SHA-256. Rename the keys.conf file
# to be algorithm-specific.
basepath = os.path.join(env["STORAGE_ROOT"], 'dns/dnssec')
shutil.move(os.path.join(basepath, 'keys.conf'), os.path.join(basepath, 'RSASHA1-NSEC3-SHA1.conf'))
def migration_7(env):
# I previously wanted domain names to be stored in Unicode in the database. Now I want them
# to be in IDNA. Affects aliases only.
import sqlite3
conn = sqlite3.connect(os.path.join(env["STORAGE_ROOT"], "mail/users.sqlite"))
# Get existing alias source addresses.
c = conn.cursor()
c.execute('SELECT source FROM aliases')
aliases = [ row[0] for row in c.fetchall() ]
# Update to IDNA-encoded domains.
for email in aliases:
try:
localpart, domainpart = email.split("@")
domainpart = domainpart.encode("idna").decode("ascii")
newemail = localpart + "@" + domainpart
if newemail != email:
c = conn.cursor()
c.execute("UPDATE aliases SET source=? WHERE source=?", (newemail, email))
if c.rowcount != 1: raise ValueError("Alias not found.")
print("Updated alias", email, "to", newemail)
except Exception as e:
print("Error updating IDNA alias", email, e)
# Save.
conn.commit()
def migration_8(env):
# Delete DKIM keys. We had generated 1024-bit DKIM keys.
# By deleting the key file we'll automatically generate
# a new key, which will be 2048 bits.
os.unlink(os.path.join(env['STORAGE_ROOT'], 'mail/dkim/mail.private'))
def migration_9(env):
# Add a column to the aliases table to store permitted_senders,
# which is a list of user account email addresses that are
# permitted to send mail using this alias instead of their own
# address. This was motivated by the addition of #427 ("Reject
# outgoing mail if FROM does not match Login") - which introduced
# the notion of outbound permitted-senders.
db = os.path.join(env["STORAGE_ROOT"], 'mail/users.sqlite')
shell("check_call", ["sqlite3", db, "ALTER TABLE aliases ADD permitted_senders TEXT"])
def migration_10(env):
# Clean up the SSL certificates directory.
# Move the primary certificate to a new name and then
# symlink it to the system certificate path.
import datetime
system_certificate = os.path.join(env["STORAGE_ROOT"], 'ssl/ssl_certificate.pem')
if not os.path.islink(system_certificate): # not already a symlink
new_path = os.path.join(env["STORAGE_ROOT"], 'ssl', env['PRIMARY_HOSTNAME'] + "-" + datetime.datetime.now().date().isoformat().replace("-", "") + ".pem")
print("Renamed", system_certificate, "to", new_path, "and created a symlink for the original location.")
shutil.move(system_certificate, new_path)
os.symlink(new_path, system_certificate)
# Flatten the directory structure. For any directory
# that contains a single file named ssl_certificate.pem,
# move the file out and name it the same as the directory,
# and remove the directory.
for sslcert in glob.glob(os.path.join( env["STORAGE_ROOT"], 'ssl/*/ssl_certificate.pem' )):
d = os.path.dirname(sslcert)
if len(os.listdir(d)) == 1:
# This certificate is the only file in that directory.
newname = os.path.join(env["STORAGE_ROOT"], 'ssl', os.path.basename(d) + '.pem')
if not os.path.exists(newname):
shutil.move(sslcert, newname)
os.rmdir(d)
def migration_11(env):
# Archive the old Let's Encrypt account directory managed by free_tls_certificates
# because we'll use that path now for the directory managed by certbot.
try:
old_path = os.path.join(env["STORAGE_ROOT"], 'ssl', 'lets_encrypt')
new_path = os.path.join(env["STORAGE_ROOT"], 'ssl', 'lets_encrypt-old')
shutil.move(old_path, new_path)
except:
# meh
pass
def migration_12(env):
# Upgrading to Carddav Roundcube plugin to version 3+, it requires the carddav_*
# tables to be dropped.
# Checking that the roundcube database already exists.
if os.path.exists(os.path.join(env["STORAGE_ROOT"], "mail/roundcube/roundcube.sqlite")):
import sqlite3
conn = sqlite3.connect(os.path.join(env["STORAGE_ROOT"], "mail/roundcube/roundcube.sqlite"))
c = conn.cursor()
# Get a list of all the tables that begin with 'carddav_'
c.execute("SELECT name FROM sqlite_master WHERE type = ? AND name LIKE ?", ('table', 'carddav_%'))
carddav_tables = c.fetchall()
# If there were tables that begin with 'carddav_', drop them
if carddav_tables:
for table in carddav_tables:
try:
table = table[0]
c = conn.cursor()
dropcmd = "DROP TABLE %s" % table
c.execute(dropcmd)
except:
print("Failed to drop table", table, e)
# Save.
conn.commit()
conn.close()
# Delete all sessions, requring users to login again to recreate carddav_*
# databases
conn = sqlite3.connect(os.path.join(env["STORAGE_ROOT"], "mail/roundcube/roundcube.sqlite"))
c = conn.cursor()
c.execute("delete from session;")
conn.commit()
conn.close()
def migration_13(env):
# Add the "mfa" table for configuring MFA for login to the control panel.
db = os.path.join(env["STORAGE_ROOT"], 'mail/users.sqlite')
shell("check_call", ["sqlite3", db, "CREATE TABLE mfa (id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, type TEXT NOT NULL, secret TEXT NOT NULL, mru_token TEXT, label TEXT, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE);"])
def migration_14(env):
# Add the "noreply" table for noreply addresses.
db = os.path.join(env["STORAGE_ROOT"], 'mail/users.sqlite')
shell("check_call", ["sqlite3", db, "CREATE TABLE noreply (id INTEGER PRIMARY KEY AUTOINCREMENT, email TEXT NOT NULL UNIQUE);"])
def migration_15(env):
# Add the "auto_aliases" table.
db = os.path.join(env["STORAGE_ROOT"], 'mail/users.sqlite')
shell("check_call", ["sqlite3", db, "CREATE TABLE auto_aliases (id INTEGER PRIMARY KEY AUTOINCREMENT, source TEXT NOT NULL UNIQUE, destination TEXT NOT NULL, permitted_senders TEXT);"])
###########################################################
def get_current_migration():
ver = 0
while True:
@ -77,13 +215,22 @@ def run_migrations():
env = load_environment()
migration_id_file = os.path.join(env['STORAGE_ROOT'], 'mailinabox.version')
migration_id = None
if os.path.exists(migration_id_file):
with open(migration_id_file) as f:
ourver = int(f.read().strip())
else:
migration_id = f.read().strip();
if migration_id is None:
# Load the legacy location of the migration ID. We'll drop support
# for this eventually.
ourver = int(env.get("MIGRATIONID", "0"))
migration_id = env.get("MIGRATIONID")
if migration_id is None:
print()
print("%s file doesn't exists. Skipping migration..." % (migration_id_file,))
return
ourver = int(migration_id)
while True:
next_ver = (ourver + 1)

84
setup/munin.sh Executable file
View file

@ -0,0 +1,84 @@
#!/bin/bash
# Munin: resource monitoring tool
#################################################
source setup/functions.sh # load our functions
source /etc/mailinabox.conf # load global vars
# install Munin
echo "Installing Munin (system monitoring)..."
apt_install munin munin-node libcgi-fast-perl
# libcgi-fast-perl is needed by /usr/lib/munin/cgi/munin-cgi-graph
# edit config
cat > /etc/munin/munin.conf <<EOF;
dbdir /var/lib/munin
htmldir /var/cache/munin/www
logdir /var/log/munin
rundir /var/run/munin
tmpldir /etc/munin/templates
includedir /etc/munin/munin-conf.d
# path dynazoom uses for requests
cgiurl_graph /admin/munin/cgi-graph
# a simple host tree
[$PRIMARY_HOSTNAME]
address 127.0.0.1
# send alerts to the following address
contacts admin
contact.admin.command mail -s "Munin notification \${var:host}" administrator@$PRIMARY_HOSTNAME
contact.admin.always_send warning critical
EOF
# 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-graph.log
# ensure munin-node knows the name of this machine
# and reduce logging level to warning
management/editconf.py /etc/munin/munin-node.conf -s \
host_name=$PRIMARY_HOSTNAME \
log_level=1
# Update the activated plugins through munin's autoconfiguration.
munin-node-configure --shell --remove-also 2>/dev/null | sh || /bin/true
# Deactivate monitoring of NTP peers. Not sure why anyone would want to monitor a NTP peer. The addresses seem to change
# (which is taken care of my munin-node-configure, but only when we re-run it.)
find /etc/munin/plugins/ -lname /usr/share/munin/plugins/ntp_ -print0 | xargs -0 /bin/rm -f
# Deactivate monitoring of network interfaces that are not up. Otherwise we can get a lot of empty charts.
for f in $(find /etc/munin/plugins/ \( -lname /usr/share/munin/plugins/if_ -o -lname /usr/share/munin/plugins/if_err_ -o -lname /usr/share/munin/plugins/bonding_err_ \)); do
IF=$(echo $f | sed s/.*_//);
if ! grep -qFx up /sys/class/net/$IF/operstate 2>/dev/null; then
rm $f;
fi;
done
# Create a 'state' directory. Not sure why we need to do this manually.
mkdir -p /var/lib/munin-node/plugin-state/
# Create a systemd service for munin.
ln -sf $(pwd)/management/munin_start.sh /usr/local/lib/mailinabox/munin_start.sh
chmod 0744 /usr/local/lib/mailinabox/munin_start.sh
cp --remove-destination conf/munin.service /lib/systemd/system/munin.service # target was previously a symlink so remove first
hide_output systemctl link -f /lib/systemd/system/munin.service
hide_output systemctl daemon-reload
hide_output systemctl unmask munin.service
hide_output systemctl enable munin.service
# Restart services.
restart_service munin
restart_service munin-node
# generate initial statistics so the directory isn't empty
# (We get "Pango-WARNING **: error opening config file '/root/.config/pango/pangorc': Permission denied"
# if we don't explicitly set the HOME directory when sudo'ing.)
# We check to see if munin-cron is already running, if it is, there is no need to run it simultaneously
# generating an error.
if [ ! -f /var/run/munin/munin-update.lock ]; then
sudo -H -u munin munin-cron
fi

View file

@ -1,6 +1,6 @@
# Install the 'host', 'sed', and and 'nc' tools. This script is run before
# the rest of the system setup so we may not yet have things installed.
hide_output apt-get -y install bind9-host sed netcat-openbsd
apt_get_quiet install bind9-host sed netcat-openbsd
# Stop if the PRIMARY_HOSTNAME is listed in the Spamhaus Domain Block List.
# The user might have chosen a name that was previously in use by a spammer

464
setup/nextcloud.sh Executable file
View file

@ -0,0 +1,464 @@
#!/bin/bash
# Nextcloud
##########################
source setup/functions.sh # load our functions
source /etc/mailinabox.conf # load global vars
# ### Installing Nextcloud
echo "Installing Nextcloud (contacts/calendar)..."
# Nextcloud core and app (plugin) versions to install.
# With each version we store a hash to ensure we install what we expect.
# Nextcloud core
# --------------
# * See https://nextcloud.com/changelog for the latest version.
# * Check https://docs.nextcloud.com/server/latest/admin_manual/installation/system_requirements.html
# for whether it supports the version of PHP available on this machine.
# * Since Nextcloud only supports upgrades from consecutive major versions,
# 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
# copying it from the error message when it doesn't match what is below.
nextcloud_ver=24.0.7
nextcloud_hash=7fb1afeb3c212bf5530c3d234b23bf314b47655a
# Nextcloud apps
# --------------
# * Find the most recent tag that is compatible with the Nextcloud version above by
# consulting the <dependencies>...<nextcloud> node at:
# https://github.com/nextcloud-releases/contacts/blob/master/appinfo/info.xml
# https://github.com/nextcloud-releases/calendar/blob/master/appinfo/info.xml
# https://github.com/nextcloud-releases/user_external
# * 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.
contacts_ver=4.2.2
contacts_hash=cbab9a7acdc11a9e2779c20b850bb21faec1c80f
calendar_ver=3.5.2
calendar_hash=dcf2cba6933dc8805ca4b4d04ed7b993ff4652a1
user_external_ver=3.0.0
user_external_hash=0df781b261f55bbde73d8c92da3f99397000972f
# Clear prior packages and install dependencies from apt.
# Just in case someone tries to cross over from the upstream version.
apt-get purge -qq -y owncloud* 2> /dev/null || /bin/true
apt_install php php-fpm \
php-cli php-sqlite3 php-gd php-imap php-curl php-pear curl \
php-dev php-xml php-mbstring php-zip php-apcu php-json \
php-intl php-imagick php-gmp php-bcmath
phpenmod apcu
management/editconf.py /etc/php/$(php_version)/mods-available/apcu.ini -c ';' \
apc.enabled=1 \
apc.enable_cli=1
management/editconf.py /etc/php/$(php_version)/cli/php.ini -c ';' \
apc.enable_cli=1
InstallNextcloud() {
version=$1
hash=$2
version_contacts=$3
hash_contacts=$4
version_calendar=$5
hash_calendar=$6
version_user_external=${7:-}
hash_user_external=${8:-}
echo
echo "Upgrading to Nextcloud version $version"
echo
# Download and verify
wget_verify https://download.nextcloud.com/server/releases/nextcloud-$version.zip $hash /tmp/nextcloud.zip
# Remove the current owncloud/Nextcloud
rm -rf /usr/local/lib/owncloud
# Extract ownCloud/Nextcloud
unzip -q /tmp/nextcloud.zip -d /usr/local/lib
mv /usr/local/lib/nextcloud /usr/local/lib/owncloud
rm -f /tmp/nextcloud.zip
# The two apps we actually want are not in Nextcloud core. Download the releases from
# their github repositories.
mkdir -p /usr/local/lib/owncloud/apps
IFS='.'
read -a checkVer <<< "$version_contacts"
unset IFS
if [ "${checkVer[0]}" -gt 4 ] || [ "${checkVer[0]}" -eq 4 -a "${checkVer[1]}" -gt 0 ] || [ "${checkVer[0]}" -eq 4 -a "${checkVer[2]}" -gt 0 ]; then
# Contacts 4.0.1 and later are downloaded from here
wget_verify https://github.com/nextcloud-releases/contacts/releases/download/v$version_contacts/contacts-v$version_contacts.tar.gz $hash_contacts /tmp/contacts.tgz
else
# 4.0.0 and earlier are downloaded from here
wget_verify https://github.com/Nextcloud/contacts/releases/download/v$version_contacts/contacts.tar.gz $hash_contacts /tmp/contacts.tgz
fi
tar xf /tmp/contacts.tgz -C /usr/local/lib/owncloud/apps/
rm /tmp/contacts.tgz
IFS='.'
read -a checkVer <<< "$version_calendar"
unset IFS
if [ "${checkVer[0]}" -eq 2 -a "${checkVer[1]}" -gt 2 ] || [ "${checkVer[0]}" -gt 2 ]; then
# Calendar 2.3.0 and later are downloaded from here
wget_verify https://github.com/nextcloud-releases/calendar/releases/download/v$version_calendar/calendar-v$version_calendar.tar.gz $hash_calendar /tmp/calendar.tgz
else
wget_verify https://github.com/nextcloud/calendar/releases/download/v$version_calendar/calendar.tar.gz $hash_calendar /tmp/calendar.tgz
fi
tar xf /tmp/calendar.tgz -C /usr/local/lib/owncloud/apps/
rm /tmp/calendar.tgz
# Starting with Nextcloud 15, the app user_external is no longer included in Nextcloud core,
# we will install from their github repository.
if [ -n "$version_user_external" ]; then
IFS='.'
read -a checkVer <<< "$version_user_external"
unset IFS
if [ "${checkVer[0]}" -gt 2 ]; then
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
else
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
fi
tar -xf /tmp/user_external.tgz -C /usr/local/lib/owncloud/apps/
rm /tmp/user_external.tgz
fi
# Fix weird permissions.
chmod 750 /usr/local/lib/owncloud/{apps,config}
# Create a symlink to the config.php in STORAGE_ROOT (for upgrades we're restoring the symlink we previously
# put in, and in new installs we're creating a symlink and will create the actual config later).
ln -sf $STORAGE_ROOT/owncloud/config.php /usr/local/lib/owncloud/config/config.php
# Make sure permissions are correct or the upgrade step won't run.
# $STORAGE_ROOT/owncloud may not yet exist, so use -f to suppress
# that error.
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.
# Then check for success (0=ok and 3=no upgrade needed, both are success).
if [ -e $STORAGE_ROOT/owncloud/owncloud.db ]; then
# ownCloud 8.1.1 broke upgrades. It may fail on the first attempt, but
# that can be OK.
sudo -u www-data php /usr/local/lib/owncloud/occ upgrade
if [ \( $? -ne 0 \) -a \( $? -ne 3 \) ]; then
echo "Trying ownCloud upgrade again to work around ownCloud upgrade bug..."
sudo -u www-data php /usr/local/lib/owncloud/occ upgrade
if [ \( $? -ne 0 \) -a \( $? -ne 3 \) ]; then exit 1; fi
sudo -u www-data php /usr/local/lib/owncloud/occ maintenance:mode --off
echo "...which seemed to work."
fi
# 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
# 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
fi
}
# Current Nextcloud Version, #1623
# Checking /usr/local/lib/owncloud/version.php shows version of the Nextcloud application, not the DB
# $STORAGE_ROOT/owncloud is kept together even during a backup. It is better to rely on config.php than
# version.php since the restore procedure can leave the system in a state where you have a newer Nextcloud
# application version than the database.
# If config.php exists, get version number, otherwise CURRENT_NEXTCLOUD_VER is empty.
#
# Config unlocking, power-mailinabox#86
# If a configuration file already exists, remove the "readonly" tag before starting the upgrade. This is
# necessary (otherwise upgrades will fail).
#
# The lock will be re-applied further down the line when it's safe to do so.
CONFIG_TEMP=$(/bin/mktemp)
if [ -f "$STORAGE_ROOT/owncloud/config.php" ]; then
CURRENT_NEXTCLOUD_VER=$(php -r "include(\"$STORAGE_ROOT/owncloud/config.php\"); echo(\$CONFIG['version']);")
# Unlock configuration directory for upgrades
php <<EOF > $CONFIG_TEMP && mv $CONFIG_TEMP $STORAGE_ROOT/owncloud/config.php;
<?php
include("$STORAGE_ROOT/owncloud/config.php");
\$CONFIG['config_is_read_only'] = false;
echo "<?php\n\\\$CONFIG = ";
var_export(\$CONFIG);
echo ";";
?>
EOF
else
CURRENT_NEXTCLOUD_VER=""
fi
# If the Nextcloud directory is missing (never been installed before, or the nextcloud version to be installed is different
# from the version currently installed, do the install/upgrade
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.
service php$(php_version)-fpm stop &> /dev/null || /bin/true
# Backup the existing ownCloud/Nextcloud.
# Create a backup directory to store the current installation and database to
BACKUP_DIRECTORY=$STORAGE_ROOT/owncloud-backup/$(date +"%Y-%m-%d-%T")
mkdir -p "$BACKUP_DIRECTORY"
if [ -d /usr/local/lib/owncloud/ ]; then
echo "Upgrading Nextcloud --- backing up existing installation, configuration, and database to directory to $BACKUP_DIRECTORY..."
cp -r /usr/local/lib/owncloud "$BACKUP_DIRECTORY/owncloud-install"
fi
if [ -e $STORAGE_ROOT/owncloud/owncloud.db ]; then
cp $STORAGE_ROOT/owncloud/owncloud.db $BACKUP_DIRECTORY
fi
if [ -e $STORAGE_ROOT/owncloud/config.php ]; then
cp $STORAGE_ROOT/owncloud/config.php $BACKUP_DIRECTORY
fi
# If ownCloud or Nextcloud was previously installed....
if [ ! -z ${CURRENT_NEXTCLOUD_VER} ]; then
# Database migrations from ownCloud are no longer possible because ownCloud cannot be run under
# PHP 7.
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."
return 0
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."
return 0
elif [[ ${CURRENT_NEXTCLOUD_VER} =~ ^13 ]]; then
# If we are running Nextcloud 13, upgrade to Nextcloud 14
InstallNextcloud 14.0.6 4e43a57340f04c2da306c8eea98e30040399ae5a 3.3.0 e55d0357c6785d3b1f3b5f21780cb6d41d32443a 2.0.3 9d9717b29337613b72c74e9914c69b74b346c466
CURRENT_NEXTCLOUD_VER="14.0.6"
fi
if [[ ${CURRENT_NEXTCLOUD_VER} =~ ^14 ]]; then
# During the upgrade from Nextcloud 14 to 15, user_external may cause the upgrade to fail.
# We will disable it here before the upgrade and install it again after the upgrade.
hide_output sudo -u www-data php /usr/local/lib/owncloud/console.php app:disable user_external
InstallNextcloud 15.0.8 4129d8d4021c435f2e86876225fb7f15adf764a3 3.3.0 e55d0357c6785d3b1f3b5f21780cb6d41d32443a 2.0.3 9d9717b29337613b72c74e9914c69b74b346c466 0.7.0 555a94811daaf5bdd336c5e48a78aa8567b86437
CURRENT_NEXTCLOUD_VER="15.0.8"
fi
if [[ ${CURRENT_NEXTCLOUD_VER} =~ ^15 ]]; then
InstallNextcloud 16.0.6 0bb3098455ec89f5af77a652aad553ad40a88819 3.3.0 e55d0357c6785d3b1f3b5f21780cb6d41d32443a 2.0.3 9d9717b29337613b72c74e9914c69b74b346c466 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 9d9717b29337613b72c74e9914c69b74b346c466 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 aee680a75e95f26d9285efd3c1e25cf7f3bfd27e 2.0.3 9d9717b29337613b72c74e9914c69b74b346c466 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 aee680a75e95f26d9285efd3c1e25cf7f3bfd27e 2.0.3 9d9717b29337613b72c74e9914c69b74b346c466 1.0.0 3bf2609061d7214e7f0f69dd8883e55c4ec8f50a
CURRENT_NEXTCLOUD_VER="19.0.4"
fi
if [[ ${CURRENT_NEXTCLOUD_VER} =~ ^19 ]]; then
InstallNextcloud 20.0.14 92cac708915f51ee2afc1787fd845476fd090c81 4.0.8 9f368bb2be98c5555b7118648f4cc9fa51e8cb30 3.0.6 ca49bb1ce23f20e10911e39055fd59d7f7a84c30 1.0.0 3bf2609061d7214e7f0f69dd8883e55c4ec8f50a
# Nextcloud 20 needs to have some optional columns added
sudo -u www-data php /usr/local/lib/owncloud/occ db:add-missing-columns
CURRENT_NEXTCLOUD_VER="20.0.14"
fi
if [[ ${CURRENT_NEXTCLOUD_VER} =~ ^20 ]]; then
InstallNextcloud 21.0.9 cf8785107c3c079a1f450743558f4f13c85f37a8 4.1.0 38653b507bd7d953816bbc5e8bea7855867eb1cd 3.2.2 54e9a836adc739be4a2a9301b8d6d2e9d88e02f4 2.1.0 6e5afe7f36f398f864bfdce9cad72200e70322aa
CURRENT_NEXTCLOUD_VER="21.0.9"
fi
if [[ ${CURRENT_NEXTCLOUD_VER} =~ ^21 ]]; then
InstallNextcloud 22.2.6 9d39741f051a8da42ff7df46ceef2653a1dc70d9 4.1.0 38653b507bd7d953816bbc5e8bea7855867eb1cd 3.2.2 54e9a836adc739be4a2a9301b8d6d2e9d88e02f4 3.0.0 0df781b261f55bbde73d8c92da3f99397000972f
CURRENT_NEXTCLOUD_VER="22.2.6"
fi
if [[ ${CURRENT_NEXTCLOUD_VER} =~ ^22 ]]; then
InstallNextcloud 23.0.4 87afec0bf90b3c66289e6fedd851867bc5a58f01 4.1.0 38653b507bd7d953816bbc5e8bea7855867eb1cd 3.2.2 54e9a836adc739be4a2a9301b8d6d2e9d88e02f4 3.0.0 0df781b261f55bbde73d8c92da3f99397000972f
CURRENT_NEXTCLOUD_VER="23.0.4"
fi
fi
InstallNextcloud $nextcloud_ver $nextcloud_hash $contacts_ver $contacts_hash $calendar_ver $calendar_hash $user_external_ver $user_external_hash
fi
# ### Configuring Nextcloud
# Setup Nextcloud if the Nextcloud database does not yet exist. Running setup when
# the database does exist wipes the database and user data.
if [ ! -f $STORAGE_ROOT/owncloud/owncloud.db ]; then
# Create user data directory
mkdir -p $STORAGE_ROOT/owncloud
# Create an initial configuration file.
instanceid=oc$(echo $PRIMARY_HOSTNAME | sha1sum | fold -w 10 | head -n 1)
cat > $STORAGE_ROOT/owncloud/config.php <<EOF;
<?php
\$CONFIG = array (
'datadirectory' => '$STORAGE_ROOT/owncloud',
'instanceid' => '$instanceid',
'forcessl' => true, # if unset/false, Nextcloud sends a HSTS=0 header, which conflicts with nginx config
'overwritewebroot' => '/cloud',
'overwrite.cli.url' => '/cloud',
'user_backends' => array(
array(
'class' => '\OCA\UserExternal\IMAP',
'arguments' => array(
'127.0.0.1', 143, null, null, false, false
),
),
),
'memcache.local' => '\OC\Memcache\APCu',
'mail_smtpmode' => 'sendmail',
'mail_smtpsecure' => '',
'mail_smtpauthtype' => 'LOGIN',
'mail_smtpauth' => false,
'mail_smtphost' => '',
'mail_smtpport' => '',
'mail_smtpname' => '',
'mail_smtppassword' => '',
'mail_from_address' => 'owncloud',
);
?>
EOF
# Create an auto-configuration file to fill in database settings
# when the install script is run. Make an administrator account
# here or else the install can't finish.
adminpassword=$(dd if=/dev/urandom bs=1 count=40 2>/dev/null | sha1sum | fold -w 30 | head -n 1)
cat > /usr/local/lib/owncloud/config/autoconfig.php <<EOF;
<?php
\$AUTOCONFIG = array (
# storage/database
'directory' => '$STORAGE_ROOT/owncloud',
'dbtype' => 'sqlite3',
'dbname' => 'owncloud',
# create an administrator account with a random password so that
# the user does not have to enter anything on first load of Nextcloud
'adminlogin' => 'root',
'adminpass' => '$adminpassword',
);
?>
EOF
# Set permissions
chown -R www-data.www-data $STORAGE_ROOT/owncloud /usr/local/lib/owncloud
# 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
# settings and deletes the autoconfig.php file.
(cd /usr/local/lib/owncloud; sudo -u www-data php /usr/local/lib/owncloud/index.php;)
fi
# Update config.php.
# * trusted_domains is reset to localhost by autoconfig starting with ownCloud 8.1.1,
# so set it here. It also can change if the box's PRIMARY_HOSTNAME changes, so
# this will make sure it has the right value.
# * Some settings weren't included in previous versions of Mail-in-a-Box.
# * We need to set the timezone to the system timezone to allow fail2ban to ban
# users within the proper timeframe
# * We need to set the logdateformat to something that will work correctly with fail2ban
# * mail_domain' needs to be set every time we run the setup. Making sure we are setting
# the correct domain name if the domain is being change from the previous setup.
# Use PHP to read the settings file, modify it, and write out the new settings array.
TIMEZONE=$(cat /etc/timezone)
php <<EOF > $CONFIG_TEMP && mv $CONFIG_TEMP $STORAGE_ROOT/owncloud/config.php;
<?php
include("$STORAGE_ROOT/owncloud/config.php");
\$CONFIG['config_is_read_only'] = true;
\$CONFIG['trusted_domains'] = array('$PRIMARY_HOSTNAME');
\$CONFIG['memcache.local'] = '\OC\Memcache\APCu';
\$CONFIG['overwrite.cli.url'] = '/cloud';
\$CONFIG['mail_from_address'] = 'administrator'; # just the local part, matches our master administrator address
\$CONFIG['logtimezone'] = '$TIMEZONE';
\$CONFIG['logdateformat'] = 'Y-m-d H:i:s';
\$CONFIG['mail_domain'] = '$PRIMARY_HOSTNAME';
\$CONFIG['user_backends'] = array(
array(
'class' => '\OCA\UserExternal\IMAP',
'arguments' => array(
'127.0.0.1', 143, null, null, false, false
),
),
);
echo "<?php\n\\\$CONFIG = ";
var_export(\$CONFIG);
echo ";";
?>
EOF
chown www-data.www-data $STORAGE_ROOT/owncloud/config.php
# Enable/disable apps. Note that this must be done after the Nextcloud setup.
# The firstrunwizard gave Josh all sorts of problems, so disabling that.
# user_external is what allows Nextcloud to use IMAP for login. The contacts
# 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 /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 /usr/local/lib/owncloud/console.php app:enable calendar
# 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?
# Check for success (0=ok, 3=no upgrade needed).
sudo -u www-data php /usr/local/lib/owncloud/occ upgrade
if [ \( $? -ne 0 \) -a \( $? -ne 3 \) ]; then exit 1; fi
# Disable default apps that we don't support
sudo -u www-data \
php /usr/local/lib/owncloud/occ app:disable photos dashboard activity \
| (grep -v "No such app enabled" || /bin/true)
# Set PHP FPM values to support large file uploads
# (semicolon is the comment character in this file, hashes produce deprecation warnings)
management/editconf.py /etc/php/$(php_version)/fpm/php.ini -c ';' \
upload_max_filesize=16G \
post_max_size=16G \
output_buffering=16384 \
memory_limit=512M \
max_execution_time=600 \
short_open_tag=On
# Set Nextcloud recommended opcache settings
management/editconf.py /etc/php/$(php_version)/cli/conf.d/10-opcache.ini -c ';' \
opcache.enable=1 \
opcache.enable_cli=1 \
opcache.interned_strings_buffer=8 \
opcache.max_accelerated_files=10000 \
opcache.memory_consumption=128 \
opcache.save_comments=1 \
opcache.revalidate_freq=1
# Migrate users_external data from <0.6.0 to version 3.0.0 (see https://github.com/nextcloud/user_external).
# This version was probably in use in Mail-in-a-Box v0.41 (February 26, 2019) and earlier.
# We moved to v0.6.3 in 193763f8. Ignore errors - maybe there are duplicated users with the
# 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.
cat > /etc/cron.d/mailinabox-nextcloud << EOF;
#!/bin/bash
# Mail-in-a-Box
*/5 * * * * root sudo -u www-data php -f /usr/local/lib/owncloud/cron.php
EOF
chmod +x /etc/cron.d/mailinabox-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.
# But if we wanted to, we would do this:
# ```
# for user in $(management/cli.py user admins); do
# sqlite3 $STORAGE_ROOT/owncloud/owncloud.db "INSERT OR IGNORE INTO oc_group_user VALUES ('admin', '$user')"
# done
# ```
# Enable PHP modules and restart PHP.
restart_service php$(php_version)-fpm

View file

@ -1,141 +0,0 @@
#!/bin/bash
# Owncloud
##########################
source setup/functions.sh # load our functions
source /etc/mailinabox.conf # load global vars
# ### Installing ownCloud
apt_install \
dbconfig-common \
php5-cli php5-sqlite php5-gd php5-imap php5-curl php-pear php-apc curl libapr1 libtool libcurl4-openssl-dev php-xml-parser \
php5 php5-dev php5-gd php5-fpm memcached php5-memcache unzip
apt-get purge -qq -y owncloud*
# Install ownCloud from source of this version:
owncloud_ver=7.0.2
# Check if ownCloud dir exist, and check if version matches owncloud_ver (if either doesn't - install/upgrade)
if [ ! -d /usr/local/lib/owncloud/ ] \
|| ! grep -q $owncloud_ver /usr/local/lib/owncloud/version.php; then
echo installing ownCloud...
rm -f /tmp/owncloud.zip
wget -qO /tmp/owncloud.zip https://download.owncloud.org/community/owncloud-$owncloud_ver.zip
unzip -u -o -q /tmp/owncloud.zip -d /usr/local/lib #either extracts new or replaces current files
hide_output php /usr/local/lib/owncloud/occ upgrade #if OC is up-to-date it wont matter
rm -f /tmp/owncloud.zip
fi
# ### Configuring ownCloud
# Setup ownCloud if the ownCloud database does not yet exist. Running setup when
# the database does exist wipes the database and user data.
if [ ! -f $STORAGE_ROOT/owncloud/owncloud.db ]; then
# Create a configuration file.
TIMEZONE=$(cat /etc/timezone)
instanceid=oc$(echo $PRIMARY_HOSTNAME | sha1sum | fold -w 10 | head -n 1)
cat > /usr/local/lib/owncloud/config/config.php <<EOF;
<?php
\$CONFIG = array (
'datadirectory' => '$STORAGE_ROOT/owncloud',
'instanceid' => '$instanceid',
'trusted_domains' =>
array (
0 => '$PRIMARY_HOSTNAME',
),
'forcessl' => true, # if unset/false, ownCloud sends a HSTS=0 header, which conflicts with nginx config
'overwritewebroot' => '/cloud',
'user_backends' => array(
array(
'class'=>'OC_User_IMAP',
'arguments'=>array('{localhost:993/imap/ssl/novalidate-cert}')
)
),
"memcached_servers" => array (
array('localhost', 11211),
),
'mail_smtpmode' => 'sendmail',
'mail_smtpsecure' => '',
'mail_smtpauthtype' => 'LOGIN',
'mail_smtpauth' => false,
'mail_smtphost' => '',
'mail_smtpport' => '',
'mail_smtpname' => '',
'mail_smtppassword' => '',
'mail_from_address' => 'owncloud',
'mail_domain' => '$PRIMARY_HOSTNAME',
'logtimezone' => '$TIMEZONE',
);
?>
EOF
# Create an auto-configuration file to fill in database settings
# when the install script is run. Make an administrator account
# here or else the install can't finish.
adminpassword=$(dd if=/dev/random bs=1 count=40 2>/dev/null | sha1sum | fold -w 30 | head -n 1)
cat > /usr/local/lib/owncloud/config/autoconfig.php <<EOF;
<?php
\$AUTOCONFIG = array (
# storage/database
'directory' => '$STORAGE_ROOT/owncloud',
'dbtype' => 'sqlite3',
# create an administrator account with a random password so that
# the user does not have to enter anything on first load of ownCloud
'adminlogin' => 'root',
'adminpass' => '$adminpassword',
);
?>
EOF
# Create user data directory and set permissions
mkdir -p $STORAGE_ROOT/owncloud
chown -R www-data.www-data $STORAGE_ROOT/owncloud /usr/local/lib/owncloud
# Execute ownCloud's setup step, which creates the ownCloud sqlite database.
# It also wipes it if it exists. And it deletes the autoconfig.php file.
(cd /usr/local/lib/owncloud; sudo -u www-data php /usr/local/lib/owncloud/index.php;)
fi
# Enable/disable apps. Note that this must be done after the ownCloud setup.
# The firstrunwizard gave Josh all sorts of problems, so disabling that.
# user_external is what allows ownCloud to use IMAP for login.
hide_output php /usr/local/lib/owncloud/console.php app:disable firstrunwizard
hide_output php /usr/local/lib/owncloud/console.php app:enable user_external
# Set PHP FPM values to support large file uploads
# (semicolon is the comment character in this file, hashes produce deprecation warnings)
tools/editconf.py /etc/php5/fpm/php.ini -c ';' \
upload_max_filesize=16G \
post_max_size=16G \
output_buffering=16384 \
memory_limit=512M \
max_execution_time=600 \
short_open_tag=On
# Set up a cron job for owncloud.
cat > /etc/cron.hourly/mailinabox-owncloud << EOF;
#!/bin/bash
# Mail-in-a-Box
sudo -u www-data php -f /usr/local/lib/owncloud/cron.php
EOF
chmod +x /etc/cron.hourly/mailinabox-owncloud
# There's nothing much of interest that a user could do as an admin for ownCloud,
# and there's a lot they could mess up, so we don't make any users admins of ownCloud.
# But if we wanted to, we would do this:
# ```
# for user in $(tools/mail.py user admins); do
# sqlite3 $STORAGE_ROOT/owncloud/owncloud.db "INSERT OR IGNORE INTO oc_group_user VALUES ('admin', '$user')"
# done
# ```
# Enable PHP modules and restart PHP.
php5enmod imap
restart_service php5-fpm

54
setup/pgp.sh Executable file
View file

@ -0,0 +1,54 @@
#!/bin/bash
# Daemon PGP Keyring
# ------------------
#
# Initializes the PGP keyring at /home/user-data/.gnupg
# For this, we will generate a new PGP keypair (if one isn't already present)
source setup/functions.sh # load our functions
source /etc/mailinabox.conf # load global vars
export GNUPGHOME # Dump into the environment so that gpg uses it as homedir
# Install gnupg
apt_install gnupg
function gpg_keygen {
# Generates a private key.
gpg --generate-key --batch 2> /tmp/gpg_keygen_log << EOF;
%no-protection
Key-Type: RSA
Key-Length: 4096
Key-Usage: sign,encrypt,auth
Name-Real: System Management Daemon
Name-Email: noreply-daemon@${PRIMARY_HOSTNAME}
Expire-Date: 180d
%commit
EOF
}
# Generate a new key if:
# - There isn't a fingerprint on /etc/mailinabox.conf
# - The configured fingerprint doesn't actually exist
if [ -z "${PGPKEY:-}" -o "$(gpg --list-secret-keys 2> /dev/null | grep ${PGPKEY:-\r\n})" = "" ]; then
echo "No keypair found. Generating daemon's PGP keypair..."
gpg_keygen
if [ $? -ne 0 ]; then
echo ""
echo "Key generation failed!" 1>&2
echo "============================" 1>&2
cat /tmp/gpg_keygen_log 1>&2
echo "============================" 1>&2
exit 1
fi
FPR=$(cat /tmp/gpg_keygen_log | tr -d '\n' | sed -r 's/.*([0-9A-F]{40}).*/\1/g')
echo "Generated key with fingerprint $FPR"
chown -R root:root $GNUPGHOME
# Remove the old key fingerprint from the configuration if it exists, and add the new one
echo "$(cat /etc/mailinabox.conf | grep -v "PGPKEY")" > /etc/mailinabox.conf
echo "PGPKEY=$FPR" >> /etc/mailinabox.conf
fi

Some files were not shown because too many files have changed in this diff Show more