Compare commits

...

1129 Commits
v0.12b ... main

Author SHA1 Message Date
Joshua Tauberer a332be6a7b
Fixed bugs found by the ShellCheck linter (#1457) 2024-04-03 09:25:32 -04:00
Teal Dulcet c7faccf1fa Fixed SC2244: Prefer explicit -n to check non-empty string. 2024-04-03 09:22:50 -04:00
Teal Dulcet ec497efa69 Quote echo commands to preserve whitespace. 2024-04-03 09:22:50 -04:00
Teal Dulcet 55a8be4aa9 Removed unnecessary bc commands. 2024-04-03 09:22:50 -04:00
Teal Dulcet 3399b25084 Replaced the pwd command with Bash's $PWD variable. 2024-04-03 09:22:50 -04:00
Teal Dulcet 2afd0451c1 Fixed SC2007: Use $((..)) instead of deprecated $[..]. 2024-04-03 09:22:50 -04:00
Teal Dulcet 27cf11d8ec Fixed SC2005: Useless echo. 2024-04-03 09:22:50 -04:00
Teal Dulcet 44d9f6eebd Fixed SC2236: Use -n instead of ! -z. 2024-04-03 09:22:50 -04:00
Teal Dulcet 4b7d4ba0a6 Fixed SC2166: Prefer [ p ] && [ q ] as [ p -a q ] is not well defined. 2024-04-03 09:22:50 -04:00
Teal Dulcet 67bcaea71e Fixed SC2091: Remove surrounding $() to avoid executing output. 2024-04-03 09:22:50 -04:00
Teal Dulcet bdf4155bed Fixed SC2046: Quote to prevent word splitting. 2024-04-03 09:21:34 -04:00
Teal Dulcet f1888f2043 Fixed SC2148: Add a shebang. 2024-04-03 09:21:34 -04:00
Teal Dulcet 33559bb844 Fixed SC2164: Use 'cd ... || exit' in case cd fails. 2024-04-03 09:21:34 -04:00
Teal Dulcet 30c4681e80 Fixed SC2086: Double quote to prevent globbing and word splitting. 2024-04-03 09:20:20 -04:00
Teal Dulcet 133bae1300 Fixed SC2006: Use $(...) notation instead of legacy backticks `...`. 2024-04-03 05:17:25 -07:00
Joshua Tauberer 830c83daa1 v68 2024-04-01 10:55:52 -04:00
Joshua Tauberer 7382c18e8f CHANGELOG entries 2024-04-01 10:54:21 -04:00
Joshua Tauberer fa72e015ee Update SMTP Smuggling protection to the 'long-term fix'
* Revert "Guard against SMTP smuggling", commit faf23f150c, by restoring the setting to its default.
* Revert "[security] SMTP smuggling: update short term fix (#2346)", commmit e931e103fe, by restoring the setting to its default.
* Set smtpd_forbid_bare_newline=normalize.
2024-03-23 13:15:32 -04:00
KiekerJan 1a239c55bb
More robust reading of sshd configuration (#2330)
Use sshd -T instead of directly reading the configuration files
2024-03-23 11:16:40 -04:00
Gio 9b450469eb
Mail guide: OS X -> macOS (#2306) 2024-03-23 09:04:43 -04:00
jvolkenant 163b1a297e
Silence "wal" output on setup using hide_output (#2368) 2024-03-23 08:49:24 -04:00
Joshua Tauberer 18b8f9ab4b Revert "Allow customizations to Roundcube settings to persist between updates by including a configuration override file, if it exists (#2333)"
This reverts commit 1b8cdeb644.

It didn't execute. I should have tried it first.
2024-03-10 08:25:34 -04:00
KiekerJan 0b1d92388a
Take spamhaus return codes into account in status check and postfix config (#2332) 2024-03-10 08:09:36 -04:00
Crag-Monkey 1b8cdeb644
Allow customizations to Roundcube settings to persist between updates by including a configuration override file, if it exists (#2333) 2024-03-10 08:02:16 -04:00
Bastian Bittorf 1053340124
setup/preflight.sh: fix some minor shellcheck complaints (#2342)
This file passes shellcheck now without errors.
This paritally fixes #1457 - the former errors where:

$ shellcheck setup/preflight.sh

In setup/preflight.sh line 1:
^-- SC2148 (error): Tips depend on target shell and yours is unknown. Add a shebang or a 'shell' directive.

In setup/preflight.sh line 29:
if [ $TOTAL_PHYSICAL_MEM -lt 490000 ]; then
     ^-----------------^ SC2086 (info): Double quote to prevent globbing and word splitting.

Did you mean:
if [ "$TOTAL_PHYSICAL_MEM" -lt 490000 ]; then

In setup/preflight.sh line 31:
	TOTAL_PHYSICAL_MEM=$(expr \( \( $TOTAL_PHYSICAL_MEM \* 1024 \) / 1000 \) / 1000)
                             ^--^ SC2003 (style): expr is antiquated. Consider rewriting this using $((..)), ${} or [[ ]].
                                        ^-----------------^ SC2086 (info): Double quote to prevent globbing and word splitting.

Did you mean:
	TOTAL_PHYSICAL_MEM=$(expr \( \( "$TOTAL_PHYSICAL_MEM" \* 1024 \) / 1000 \) / 1000)

In setup/preflight.sh line 38:
if [ $TOTAL_PHYSICAL_MEM -lt 750000 ]; then
     ^-----------------^ SC2086 (info): Double quote to prevent globbing and word splitting.

Did you mean:
if [ "$TOTAL_PHYSICAL_MEM" -lt 750000 ]; then

For more information:
  https://www.shellcheck.net/wiki/SC2148 -- Tips depend on target shell and y...
  https://www.shellcheck.net/wiki/SC2086 -- Double quote to prevent globbing ...
  https://www.shellcheck.net/wiki/SC2003 -- expr is antiquated. Consider rewr...
2024-03-10 08:01:13 -04:00
Joshua Tauberer 315d2cf691
Fixed errors found by the Ruff Python linter (#2343) 2024-03-10 07:57:19 -04:00
Teal Dulcet dbc2b5eee0 Fixed ISC003 (explicit-string-concatenation): Explicitly concatenated string should be implicitly concatenated 2024-03-10 07:56:49 -04:00
Teal Dulcet 775a4223de Fixed F821 (undefined-name): Undefined name `e` 2024-03-10 07:56:49 -04:00
Teal Dulcet 618c466b84 Fixed SIM114 (if-with-same-arms): Combine `if` branches using logical `or` operator 2024-03-10 07:56:49 -04:00
Teal Dulcet a32354fd91 Fixed PLR5501 (collapsible-else-if): Use `elif` instead of `else` then `if`, to reduce indentation 2024-03-10 07:56:49 -04:00
Teal Dulcet 1d79f9bb2b Fixed PERF401 (manual-list-comprehension): Use a list comprehension to create a transformed list 2024-03-10 07:56:49 -04:00
Teal Dulcet cacf6d2006 Fixed E721 (type-comparison): Use `is` and `is not` for type comparisons, or `isinstance()` for isinstance checks 2024-03-10 07:56:49 -04:00
Teal Dulcet f0377dd59e Fixed SIM105 (suppressible-exception) 2024-03-10 07:56:49 -04:00
Teal Dulcet 6a47133e3f Fixed F811 (redefined-while-unused): Redefinition of unused `sys` from line 10 2024-03-10 07:56:49 -04:00
Teal Dulcet 7f456d8e8b Fixed ISC002 (multi-line-implicit-string-concatenation): Implicitly concatenated string literals over multiple lines 2024-03-10 07:56:49 -04:00
Teal Dulcet e466b9bb53 Fixed RUF005 (collection-literal-concatenation) 2024-03-10 07:56:49 -04:00
Teal Dulcet 0e9193651d Fixed PLW1514 (unspecified-encoding): `open` in text mode without explicit `encoding` argument 2024-03-10 07:56:49 -04:00
Teal Dulcet a02b59d4e4 Fixed F401 (unused-import): `socket.timeout` imported but unused 2024-03-10 07:56:49 -04:00
Teal Dulcet 15bddcbc39 Fixed RUF010 (explicit-f-string-type-conversion): Use explicit conversion flag 2024-03-10 07:56:49 -04:00
Teal Dulcet c719fce40a Fixed UP032 (f-string): Use f-string instead of `format` call 2024-03-10 07:56:49 -04:00
Teal Dulcet 3111cf56de Fixed EM102 (f-string-in-exception): Exception must not use an f-string literal, assign to variable first 2024-03-10 07:56:49 -04:00
Teal Dulcet 6508d47da1 Fixed C405 (unnecessary-literal-set): Unnecessary `list` literal (rewrite as a `set` literal) 2024-03-10 07:56:49 -04:00
Teal Dulcet 9b961b7ba0 Fixed UP024 (os-error-alias): Replace aliased errors with `OSError` 2024-03-10 07:56:49 -04:00
Teal Dulcet b13cef9b1d Fixed PIE790 (unnecessary-placeholder): Unnecessary `pass` statement 2024-03-10 07:56:49 -04:00
Teal Dulcet 8b9d3ec094 Fixed W292 (missing-newline-at-end-of-file): No newline at end of file 2024-03-10 07:56:49 -04:00
Teal Dulcet d1d3d08d70 Fixed B006 (mutable-argument-default): Do not use mutable data structures for argument defaults 2024-03-10 07:56:49 -04:00
Teal Dulcet 922c59ddaf Fixed SIM212 (if-expr-with-twisted-arms): Use `with_lines if with_lines else []` instead of `[] if not with_lines else with_lines` 2024-03-10 07:56:49 -04:00
Teal Dulcet 20a99c0ab8 Fixed UP041 (timeout-error-alias): Replace aliased errors with `TimeoutError` 2024-03-10 07:56:49 -04:00
Teal Dulcet 54af4725f9 Fixed C404 (unnecessary-list-comprehension-dict): Unnecessary `list` comprehension (rewrite as a `dict` comprehension) 2024-03-10 07:56:49 -04:00
Teal Dulcet fd4fcdaf53 Fixed E712 (true-false-comparison): Comparison to `False` should be `cond is False` or `if not cond:` 2024-03-10 07:56:49 -04:00
Teal Dulcet d661d623dc Fixed RUF017 (quadratic-list-summation): Avoid quadratic list summation 2024-03-10 07:56:49 -04:00
Teal Dulcet f621789298 Fixed SIM118 (in-dict-keys): Use `key in dict` instead of `key in dict.keys()` 2024-03-10 07:56:49 -04:00
Teal Dulcet ec32e1d578 Fixed E703 (useless-semicolon): Statement ends with an unnecessary semicolon 2024-03-10 07:56:49 -04:00
Teal Dulcet 57dcd4bb51 Fixed E713 (not-in-test): Test for membership should be `not in` 2024-03-10 07:56:49 -04:00
Teal Dulcet 845393b6e0 Fixed RET503 (implicit-return): Missing explicit `return` at the end of function able to return non-`None` value 2024-03-10 07:56:49 -04:00
Teal Dulcet c585c1ecf6 Fixed W291 (trailing-whitespace): Trailing whitespace 2024-03-10 07:56:49 -04:00
Teal Dulcet e0e6f1081b Fixed C414 (unnecessary-double-cast-or-process): Unnecessary `list` call within `sorted()` 2024-03-10 07:56:49 -04:00
Teal Dulcet 4999ed7b1c Fixed Q003 (avoidable-escaped-quote): Change outer quotes to avoid escaping inner quotes 2024-03-10 07:54:51 -04:00
Teal Dulcet ca8f06d590 Fixed PLR1711 (useless-return): Useless `return` statement at end of function 2024-03-10 07:54:51 -04:00
Teal Dulcet 57d05c1ab2 Fixed B007 (unused-loop-control-variable) 2024-03-10 07:54:51 -04:00
Teal Dulcet c953e5784d Fixed C401 (unnecessary-generator-set): Unnecessary generator (rewrite as a `set` comprehension) 2024-03-10 07:54:51 -04:00
Teal Dulcet 81a4da0181 Fixed SIM110 (reimplemented-builtin) 2024-03-10 07:54:51 -04:00
Teal Dulcet 99d3929f99 Fixed E711 (none-comparison) 2024-03-10 07:54:51 -04:00
Teal Dulcet 541f31b1ba Fixed FURB113 (repeated-append) 2024-03-10 07:54:51 -04:00
Teal Dulcet e8d1c037cb Fixed SIM102 (collapsible-if): Use a single `if` statement instead of nested `if` statements 2024-03-10 07:54:51 -04:00
Teal Dulcet 67b9d0b279 Fixed PLW0108 (unnecessary-lambda): Lambda may be unnecessary; consider inlining inner function 2024-03-10 07:54:51 -04:00
Teal Dulcet 3d72c32b1d Fixed W605 (invalid-escape-sequence) 2024-03-10 07:54:51 -04:00
Teal Dulcet 14a5613dc8 Fixed UP031 (printf-string-formatting): Use format specifiers instead of percent format 2024-03-10 07:54:51 -04:00
Teal Dulcet 64540fbb44 Fixed UP034 (extraneous-parentheses): Avoid extraneous parentheses 2024-03-10 07:54:51 -04:00
Teal Dulcet eefc0514b2 Fixed UP030 (format-literals): Use implicit references for positional format fields 2024-03-10 07:54:51 -04:00
Teal Dulcet fba92de051 Fixed SIM108 (if-else-block-instead-of-if-exp) 2024-03-10 07:54:51 -04:00
Teal Dulcet 51dc7615f7 Fixed RSE102 (unnecessary-paren-on-raise-exception): Unnecessary parentheses on raised exception 2024-03-10 07:54:51 -04:00
Teal Dulcet 13b38cc04d Fixed F841 (unused-variable) 2024-03-10 07:54:51 -04:00
Teal Dulcet 2b426851f9 Fixed UP032 (f-string): Use f-string instead of `format` call 2024-03-10 07:54:51 -04:00
Teal Dulcet b7f70b17ac Fixed RET504 (unnecessary-assign) 2024-03-10 07:54:51 -04:00
Teal Dulcet 6bfd1e5140 Fixed W293 (blank-line-with-whitespace): Blank line contains whitespace 2024-03-10 07:54:51 -04:00
Teal Dulcet 555ecc1ebb Fixed PIE810 (multiple-starts-ends-with): Call `startswith` once with a `tuple` 2024-03-10 07:54:51 -04:00
Teal Dulcet dd61844ced Fixed EM101 (raw-string-in-exception): Exception must not use a string literal, assign to variable first 2024-03-10 07:54:51 -04:00
Teal Dulcet 49124cc9ca Fixed PLR6201 (literal-membership): Use a `set` literal when testing for membership 2024-03-10 07:54:51 -04:00
Teal Dulcet cb922ec286 Fixed UP015 (redundant-open-modes): Unnecessary open mode parameters 2024-03-10 07:54:49 -04:00
Teal Dulcet 0ee64f2fe8 Fixed F401 (unused-import) 2024-03-10 07:54:21 -04:00
KiekerJan 785c337fb3
Make reading of previous status check result more robust (#2347) 2024-03-10 07:27:04 -04:00
KiekerJan 293d56c781
Update javascript libraries used by control panel (#2351) 2024-03-10 07:26:33 -04:00
KiekerJan 040d0cbb7c
Update roundcube to 1.6.6 (#2360) 2024-03-10 07:24:29 -04:00
Michael Heuberger 111468efb9
Bump Nextcloud to v26.0.12 (#2310)
Also
- bumps calendar and contacts apps
- reformats some comments (line-breaking)
- adds extra comments for the next developer
2024-03-10 07:22:51 -04:00
John James Jacoby 4ad679da47
Issue-2354: Silence "wal" output on setup (#2356)
Silence "wal" output from RoundCube Sqlite customization, inside of webmail.sh.

Co-authored-by: solomon-s-b

Fixes #2354.
2024-03-10 07:16:03 -04:00
solomon-s-b 84919fefa4
Fix miab-munin.conf filter not capturing HTTP/2.0 (#2359) 2024-03-10 07:15:25 -04:00
KiekerJan e931e103fe
[security] SMTP smuggling: update short term fix (#2346)
Update short term fix according to postfix advisory at https://www.postfix.org/smtp-smuggling.html.
2024-01-10 09:34:06 -05:00
Joshua Tauberer 7646095b94 v67 2023-12-22 08:56:43 -05:00
Joshua Tauberer faf23f150c Guard against SMTP smuggling
This short-term workaround is recommended at https://www.postfix.org/smtp-smuggling.html:

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

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

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

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

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

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

This reverts commit 26709a3c1d.

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

* simplify a bit

---------

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

* replace exception pass with error message

* simplify dns exception catching

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

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

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

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

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

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

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

Also:

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

Although PHP 8.1 is the stock version in Ubuntu 22.04, it's not supported by Nextcloud yet, and it likely will never be supported by the the version of Nextcloud that succeeds the last version of Nextcloud that supports PHP 7.2, and we have to install the next version so that an upgrade is permitted, so skipping to PHP 8.1 may not be easily possible.
2022-07-28 14:02:46 -04:00
Joshua Tauberer b41a0ad80e Drop some hacks that we needed for Ubuntu 18.04
* certbot's PPA is no longer needed because a recent version is now included in the Ubuntu respository.
* Un-pin b2sdk (reverts 69d8fdef99 and d829d74048).
* Revert boto+s3 workaround for duplicity (partial revert of 99474b348f).
* Revert old "fix boto 2 conflict on Google Compute Engine instances" (cf33be4596) which is probably no longer needed.
2022-07-28 14:02:46 -04:00
Rauno Moisto 78569e9a88 Fix DeprecationWarning in dnspython query vs resolve method
The resolve method disables resolving relative names by default. This change probably makes a7710e90 unnecessary. @JoshData added some additional changes from query to resolve.
2022-07-28 14:02:46 -04:00
Daniel Mabbett 8cb360fe36 Configure nsd listening interfaces before installing nsd so that it does not interfere with bind9 2022-07-28 14:02:46 -04:00
Joshua Tauberer f534a530d4 Update and drop some package and file names for Ubuntu 22.04
* Fix path to bind9 startup options file in Ubuntu 22.04.
* tinymce has not been a Roundcube requirement recently and is no longer a package in Ubuntu 22.04
* Upgrade Vagrant box to Ubuntu 22.04
2022-07-28 14:02:46 -04:00
Joshua Tauberer 2abcafd670 Update Ubuntu version checks from 18.04 to 22.04 2022-07-28 14:02:44 -04:00
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
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
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
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
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
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
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
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 ba80d9e72d
Show backup retention period form when configuring B2 backups (#2024) 2021-08-23 06:25:41 -04: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
NewbieOrange 21ad26e452
Disable auto-complete for 2FA code in the control panel login form (#2013) 2021-07-28 16:39:40 -04:00
Joshua Tauberer 4cb46ea465 v0.54 2021-06-20 15:50:04 -04: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
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
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
jvolkenant af62e7a99b
Fixes unbound variable when upgrading from Nextcloud 13 (#1913) 2021-02-06 16:49:43 -05: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
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
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
Hilko 3422cc61ce
Include en_US.UTF-8 locale in daemon startup (#1883)
Fixes #1881.
2020-12-19 19:11:58 -05: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
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
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
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 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
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
Joshua Tauberer a8ea456b49 Reorganize the MFA backend methods 2020-09-26 09:58:25 -04: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
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 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
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 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 e6102eacfb
AXFR Transfers (for secondary DNS servers): Allow IPv6 addresses (#1787) 2020-07-08 18:26:47 -04: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
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
Joshua Tauberer 7de8fc9bc0 v0.45 2020-05-16 06:45:23 -04: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
Michael Becker 40b21c466d
Fypo fix in users.html (#1748) 2020-04-13 22:10:52 -04: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
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
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
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
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
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
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
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
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
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
Ryan Stubbs bad38840d8 Fix type on alias edit page (#1520) 2019-02-11 20:14:56 -05: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
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
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 7ee91f6ae6
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
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
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
115 changed files with 12865 additions and 4017 deletions

30
.editorconfig Normal file
View File

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

3
.gitignore vendored
View File

@ -4,3 +4,6 @@ management/__pycache__/
tools/__pycache__/
externals/
.env
.vagrant
api/docs/api-docs.html
*.code-workspace

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.

View File

@ -9,78 +9,92 @@ Mail-in-a-Box helps individuals take back control of their email by defining a o
* * *
I am trying to:
Our goals are to:
* 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.
* Have automated, auditable, and [idempotent](https://web.archive.org/web/20190518072631/https://sharknet.us/2014/02/01/automated-configuration-management-challenges-with-idempotency/) configuration.
* **Not** make a totally unhackable, NSA-proof server.
* **Not** make something customizable by power users.
This setup is what has been powering my own personal email since September 2013.
Additionally, this project has a [Code of Conduct](CODE_OF_CONDUCT.md), which supersedes the goals above. Please review it when joining our community.
The Box
-------
Mail-in-a-Box turns a fresh Ubuntu 14.04 LTS 64-bit machine into a working mail server by installing and configuring various components.
In The Box
----------
It is a one-click email appliance (see the [setup guide](https://mailinabox.email/guide.html)). There are no user-configurable setup options. It "just works".
Mail-in-a-Box turns a fresh Ubuntu 22.04 LTS 64-bit machine into a working mail server by installing and configuring various components.
It is a one-click email appliance. There are no user-configurable setup options. It "just works."
The components installed are:
* SMTP ([postfix](http://www.postfix.org/)), IMAP ([dovecot](http://dovecot.org/)), CardDAV/CalDAV ([ownCloud](http://owncloud.org/)), Exchange ActiveSync ([z-push](https://github.com/fmbiete/Z-Push-contrib))
* Webmail ([Roundcube](http://roundcube.net/)), static website hosting ([nginx](http://nginx.org/))
* Spam filtering ([spamassassin](https://spamassassin.apache.org/)), greylisting ([postgrey](http://postgrey.schweikert.ch/))
* DNS ([nsd4](http://www.nlnetlabs.nl/projects/nsd/)) with [SPF](https://en.wikipedia.org/wiki/Sender_Policy_Framework), DKIM ([OpenDKIM](http://www.opendkim.org/)), [DMARC](https://en.wikipedia.org/wiki/DMARC), [DNSSEC](https://en.wikipedia.org/wiki/DNSSEC), [DANE TLSA](https://en.wikipedia.org/wiki/DNS-based_Authentication_of_Named_Entities), and [SSHFP](https://tools.ietf.org/html/rfc4255) records automatically set
* Firewall ([ufw](https://launchpad.net/ufw)), intrusion protection ([fail2ban](http://www.fail2ban.org/wiki/index.php/Main_Page)), system monitoring ([munin](http://munin-monitoring.org/))
* SMTP ([postfix](http://www.postfix.org/)), IMAP ([Dovecot](http://dovecot.org/)), CardDAV/CalDAV ([Nextcloud](https://nextcloud.com/)), and Exchange ActiveSync ([z-push](http://z-push.org/)) servers
* Webmail ([Roundcube](http://roundcube.net/)), mail filter rules (thanks to Roundcube and Dovecot), and email client autoconfig settings (served by [nginx](http://nginx.org/))
* Spam filtering ([spamassassin](https://spamassassin.apache.org/)) and greylisting ([postgrey](http://postgrey.schweikert.ch/))
* DNS ([nsd4](https://www.nlnetlabs.nl/projects/nsd/)) with [SPF](https://en.wikipedia.org/wiki/Sender_Policy_Framework), DKIM ([OpenDKIM](http://www.opendkim.org/)), [DMARC](https://en.wikipedia.org/wiki/DMARC), [DNSSEC](https://en.wikipedia.org/wiki/DNSSEC), [DANE TLSA](https://en.wikipedia.org/wiki/DNS-based_Authentication_of_Named_Entities), [MTA-STS](https://tools.ietf.org/html/rfc8461), and [SSHFP](https://tools.ietf.org/html/rfc4255) policy records automatically set
* TLS certificates are automatically provisioned using [Let's Encrypt](https://letsencrypt.org/) for protecting https and all of the other services on the box
* Backups ([duplicity](http://duplicity.nongnu.org/)), firewall ([ufw](https://launchpad.net/ufw)), intrusion protection ([fail2ban](http://www.fail2ban.org/wiki/index.php/Main_Page)), and basic system monitoring ([munin](http://munin-monitoring.org/))
It also includes:
It also includes system management tools:
* A control panel and API for adding/removing mail users, aliases, custom DNS records, etc. and detailed system monitoring.
* Our own builds of postgrey and dovecot-lucene distributed via the [Mail-in-a-Box PPA](https://launchpad.net/~mail-in-a-box/+archive/ubuntu/ppa) on Launchpad.
* Comprehensive health monitoring that checks each day that services are running, ports are open, TLS certificates are valid, and DNS records are correct
* A control panel for adding/removing mail users, aliases, custom DNS records, configuring backups, etc.
* An API for all of the actions on the control panel
Internationalized domain names are supported and configured easily (but SMTPUTF8 is not supported, unfortunately).
It also supports static website hosting since the box is serving HTTPS anyway. (To serve a website for your domains elsewhere, just add a custom DNS "A" record in you Mail-in-a-Box's control panel to point domains to another server.)
For more information on how Mail-in-a-Box handles your privacy, see the [security details page](security.md).
The Security
Installation
------------
See the [security guide](security.md) for more information about the box's security configuration (TLS, password storage, etc).
See the [setup guide](https://mailinabox.email/guide.html) for detailed, user-friendly instructions.
I sign the release tags on git. To verify that a tag is signed by me, you can perform the following steps:
For experts, start with a completely fresh (really, I mean it) Ubuntu 22.04 LTS 64-bit machine. On the machine...
# Download my PGP key.
$ curl -s https://keybase.io/joshdata/key.asc | gpg --import
gpg: key C10BDD81: public key "Joshua Tauberer <jt@occams.info>" imported
Clone this repository and checkout the tag corresponding to the most recent release:
# Clone this repository.
$ git clone https://github.com/mail-in-a-box/mailinabox
$ cd mailinabox
$ git checkout v68
# Verify the tag.
$ git verify-tag v0.12b
gpg: Signature made ..... using RSA key ID C10BDD81
gpg: Good signature from "Joshua Tauberer <jt@occams.info>"
gpg: WARNING: This key is not certified with a trusted signature!
gpg: There is no indication that the signature belongs to the owner.
Primary key fingerprint: 5F4C 0E73 13CC D744 693B 2AEA B920 41F4 C10B DD81
Begin the installation.
# Check out the tag.
$ git checkout v0.12b
$ sudo setup/start.sh
The installation will install, uninstall, and configure packages to turn the machine into a working, good mail server.
For help, DO NOT contact Josh directly --- I don't do tech support by email or tweet (no exceptions).
Post your question on the [discussion forum](https://discourse.mailinabox.email/) instead, where maintainers and Mail-in-a-Box users may be able to help you.
Note that while we want everything to "just work," we can't control the rest of the Internet. Other mail services might block or spam-filter email sent from your Mail-in-a-Box.
This is a challenge faced by everyone who runs their own mail server, with or without Mail-in-a-Box. See our discussion forum for tips about that.
Contributing and Development
----------------------------
Mail-in-a-Box is an open source project. Your contributions and pull requests are welcome. See [CONTRIBUTING](CONTRIBUTING.md) to get started.
The key ID and fingerprint above should match my [Keybase.io key](https://keybase.io/joshdata) and the fingerprint I publish on [my homepage](https://razor.occams.info/).
The Acknowledgements
--------------------
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>.
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/sovereign/sovereign) by Alex Payne, and conversations with <a href="https://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>.
Mail-in-a-Box is similar to [iRedMail](http://www.iredmail.org/) and [Modoboa](https://github.com/tonioo/modoboa).
The History
-----------
* 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).
* In August 2013 I began Mail-in-a-Box by combining my own mail server configuration with the setup in ["NSA-proof your email in 2 hours"](http://sealedabstract.com/code/nsa-proof-your-e-mail-in-2-hours/) and making the setup steps reproducible with bash scripts.
* Mail-in-a-Box was a semifinalist in the 2014 [Knight News Challenge](https://www.newschallenge.org/challenge/2014/submissions/mail-in-a-box), but it was not selected as a winner.
* Mail-in-a-Box hit the front page of Hacker News in [April](https://news.ycombinator.com/item?id=7634514) 2014, [September](https://news.ycombinator.com/item?id=8276171) 2014, and [May](https://news.ycombinator.com/item?id=9624267) 2015.
* Mail-in-a-Box hit the front page of Hacker News in [April](https://news.ycombinator.com/item?id=7634514) 2014, [September](https://news.ycombinator.com/item?id=8276171) 2014, [May](https://news.ycombinator.com/item?id=9624267) 2015, and [November](https://news.ycombinator.com/item?id=13050500) 2016.
* FastCompany mentioned Mail-in-a-Box a [roundup of privacy projects](http://www.fastcompany.com/3047645/your-own-private-cloud) on June 26, 2015.

18
Vagrantfile vendored
View File

@ -2,27 +2,23 @@
# vi: set ft=ruby :
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.box = "ubuntu/jammy64"
# 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.hostname = "mailinabox.lan"
config.vm.network "private_network", ip: "192.168.56.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.
# 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=auto
export PUBLIC_IPV6=auto
export PRIMARY_HOSTNAME=auto-easy
export CSR_COUNTRY=US
export PRIMARY_HOSTNAME=auto
#export SKIP_NETWORK_CHECKS=1
# Start the setup script.

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>

2732
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

@ -1,22 +0,0 @@
# Fail2Ban filter Dovecot authentication and pop3/imap server
# For Mail-in-a-Box
[INCLUDES]
before = common.conf
[Definition]
_daemon = (auth|dovecot(-auth)?|auth-worker)
failregex = ^%(__prefix_line)s(pop3|imap)-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,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/\d+\.\d+\" 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 =

View File

@ -1,19 +0,0 @@
# Fail2Ban configuration file for Mail-in-a-Box
[DEFAULT]
# This should ban dumb brute-force attacks, not oblivious users.
findtime = 30
maxretry = 20
# JAILS
[ssh-ddos]
enabled = true
[sasl]
enabled = true
[dovecot]
enabled = true
filter = dovecotimap

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

View File

@ -18,8 +18,6 @@
<string>PRIMARY_HOSTNAME</string>
<key>CalDAVPort</key>
<real>443</real>
<key>CalDAVPrincipalURL</key>
<string>/cloud/remote.php/caldav/calendars/</string>
<key>CalDAVUseSSL</key>
<true/>
<key>PayloadDescription</key>
@ -55,7 +53,7 @@
<key>OutgoingMailServerHostName</key>
<string>PRIMARY_HOSTNAME</string>
<key>OutgoingMailServerPortNumber</key>
<integer>587</integer>
<integer>465</integer>
<key>OutgoingMailServerUseSSL</key>
<true/>
<key>OutgoingPasswordSameAsIncomingPassword</key>

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,135 +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
# Ensure Python reads/writes files in UTF-8. If the machine
# triggers some other locale in Python, like ASCII encoding,
# Python may not be able to read/write files. Here and in
# setup/start.sh (where the locale is also installed if not
# already present).
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
# 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

@ -16,12 +16,12 @@
<outgoingServer type="smtp">
<hostname>PRIMARY_HOSTNAME</hostname>
<port>587</port>
<socketType>STARTTLS</socketType>
<port>465</port>
<socketType>SSL</socketType>
<username>%EMAILADDRESS%</username>
<authentication>password-cleartext</authentication>
<addThisServer>true</addThisServer>
<useGlobalPreferredServer>true</useGlobalPreferredServer>
<useGlobalPreferredServer>false</useGlobalPreferredServer>
</outgoingServer>
<documentation url="https://PRIMARY_HOSTNAME/">

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

@ -18,6 +18,12 @@
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;
}
# Roundcube Webmail configuration.
rewrite ^/mail$ /mail/ redirect;
@ -70,7 +76,7 @@
# takes precedence over all non-regex matches and only regex matches that
# come after it (i.e. none of those, since this is the last one.) That means
# we're blocking dotfiles in the static hosted sites but not the FastCGI-
# handled locations for ownCloud (which serves user-uploaded files that might
# 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;

View File

@ -1,23 +1,41 @@
# 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://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.
# 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-fpm;
}
}
location ~ ^(/cloud)((?:/ocs)?/[^/]+\.php)(/.*)?$ {
# note: ~ has precendence over a regular location block
@ -34,13 +52,11 @@
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;
client_max_body_size 1G;
fastcgi_buffers 64 4K;
}
location ^~ /owncloud-xaccel/ {
# This directory is for MOD_X_ACCEL_REDIRECT_ENABLED. ownCloud sends the full file
# 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.
@ -50,11 +66,16 @@
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;
# This addresses those service discovery issues mentioned in:
# https://docs.nextcloud.com/server/23/admin_manual/issues/general_troubleshooting.html#service-discovery
rewrite ^/.well-known/webfinger /cloud/index.php/.well-known/webfinger redirect;
rewrite ^/.well-known/nodeinfo /cloud/index.php/.well-known/nodeinfo redirect;
# ADDITIONAL DIRECTIVES HERE

View File

@ -1,74 +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 STORAGE_ROOT/ssl/dh2048.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 127.0.0.1 valid=86400;
resolver_timeout 10;
# h/t https://gist.github.com/konklone/6532544

View File

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

View File

@ -1,6 +1,8 @@
## $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;
@ -12,16 +14,25 @@ server {
# error pages and in the "Server" HTTP-Header.
server_tokens off;
# 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 / {
# 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;
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name $HOSTNAME;
@ -31,7 +42,6 @@ server {
ssl_certificate $SSL_CERTIFICATE;
ssl_certificate_key $SSL_KEY;
include /etc/nginx/nginx-ssl.conf;
# ADDITIONAL DIRECTIVES HERE
}

View File

@ -1,7 +1,7 @@
# 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

View File

@ -1,6 +1,7 @@
<html>
<head>
<title>this is a mail-in-a-box</title>
<meta name="robots" content="noindex">
</head>
<body>
<h1>this is a mail-in-a-box</h1>

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,10 +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', '');
define('CALDAV_SUPPORTS_SYNC', true);
define('CALDAV_PERSONAL', 'PRINCIPAL');
define('CALDAV_SUPPORTS_SYNC', false);
define('CALDAV_MAX_SYNC_PERIOD', 2147483647);
?>

View File

@ -7,7 +7,7 @@
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 */
@ -17,7 +17,7 @@ define('CARDDAV_CONTACTS_FOLDER_NAME', '%u Addressbook');
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,138 +1,158 @@
import base64, os, os.path, hmac
import base64, hmac, json, secrets
from datetime import timedelta
from flask import make_response
from expiringdict import ExpiringDict
import utils
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_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, encoding='utf-8') 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):
"""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.
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 an API key, the user's email is returned as None."""
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 decode(s):
return base64.b64decode(s.encode('ascii')).decode('ascii')
def parse_basic_auth(header):
def parse_http_authorization_basic(header):
def decode(s):
return base64.b64decode(s.encode('ascii')).decode('ascii')
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:
raise ValueError("No authorization header provided.")
username, password = parse_http_authorization_basic(request.headers.get('Authorization', ''))
if username in {None, ""}:
msg = "Authorization header invalid."
raise ValueError(msg)
username, password = parse_basic_auth(header)
if username.strip() == "" and password.strip() == "":
msg = "No email address, password, session key, or API key provided."
raise ValueError(msg)
if username in (None, ""):
raise ValueError("Authorization header invalid.")
elif username == self.key:
# The user passed the API key which grants administrative privs.
# 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() == "":
msg = "Enter a password."
raise ValueError(msg)
else:
# The user is trying to log in with a username and user-specific
# API key or password. Raises or returns privs.
return (username, self.get_user_credentials(username, password, env))
def get_user_credentials(self, email, pw, env):
# Validate a user's credentials. On success returns a list of
# privileges (e.g. [] or ['admin']). On failure raises a ValueError
# with a login error message.
# Sanity check.
if email == "" or pw == "":
raise ValueError("Enter an email address and password.")
# The password might be a user-specific API key. create_user_key raises
# a ValueError if the user does not exist.
if hmac.compare_digest(self.create_user_key(email, env), pw):
# OK.
pass
else:
# Get the hashed password of the user. Raise a ValueError if the
# email address does not correspond to a user.
pw_hash = get_mail_password(email, env)
# Authenticate.
try:
# 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", "pw",
"-p", pw,
"-t", pw_hash,
])
except:
# Login failed.
raise ValueError("Invalid password.")
# 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)
# Get privileges for authorization. This call should never fail because by this
# point we know the email address is a valid user. But on error the call will
# return a tuple of an error message and an HTTP status code.
privs = get_mail_user_privileges(email, env)
# 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])
# Return a list of privileges.
return privs
# Return the authorization information.
return (username, privs)
def create_user_key(self, email, env):
# Store an HMAC with the client. The hashed message of the HMAC will be the user's
# email address & hashed password and the key will be the master API key. The user of
# course has their own email address and password. We assume they do not have the master
# API key (unless they are trusted anyway). The HMAC proves that they authenticated
# with us in some other way to get the HMAC. Including the password means that when
# a user's password is reset, the HMAC changes and they will correctly need to log
# in to the control panel again. This method raises a ValueError if the user does
# not exist, due to get_mail_password.
msg = b"AUTH:" + email.encode("utf8") + b" " + get_mail_password(email, env).encode("utf8")
return hmac.new(self.key.encode('ascii'), msg, digestmod="sha256").hexdigest()
def 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.
def _generate_key(self):
raw_key = os.urandom(32)
return base64.b64encode(raw_key).decode('ascii')
# Authenticate.
try:
# 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", "pw",
"-p", pw,
"-t", pw_hash,
])
except:
# Login failed.
msg = "Incorrect email address or password."
raise ValueError(msg)
# 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))
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")
# 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,32 +1,38 @@
#!/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 encrypted backup is made using duplicity into the
# directory STORAGE_ROOT/backup/encrypted. The password used for
# encryption is stored in backup/secret_key.txt.
# 3) The stopped services are restarted.
# 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 os, os.path, re, datetime, sys
import dateutil.parser, dateutil.relativedelta, dateutil.tz
import rtyaml
from exclusiveprocess import Lock
from utils import exclusive_process, load_environment, shell, wait_for_service
# Destroy backups when the most recent increment in the chain
# that depends on it is this many days old.
keep_backups_for_days = 3
from utils import load_environment, shell, wait_for_service
def backup_status(env):
# What is the current status of backups?
# Loop through all of the files in STORAGE_ROOT/backup/encrypted 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 dissbled, 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())
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.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
@ -34,78 +40,119 @@ def backup_status(env):
if rd.days == 1: return "%d day, %d hours" % (rd.days, rd.hours)
return "%d hours, %d minutes" % (rd.hours, rd.minutes)
backups = { }
backup_root = os.path.join(env["STORAGE_ROOT"], 'backup')
backup_dir = os.path.join(backup_root, 'encrypted')
os.makedirs(backup_dir, exist_ok=True) # os.listdir fails if directory does not exist
for fn in os.listdir(backup_dir):
# 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
"volumes": int(keys[2]), # number of archive volumes for this backup (not really helpful)
}
code, collection_status = shell('check_output', [
"/usr/bin/duplicity",
"collection-status",
"--archive-dir", backup_cache_dir,
"--gpg-options", "'--cipher-algo=AES256'",
"--log-fd", "1",
*get_duplicity_additional_args(env),
get_duplicity_target_url(config)
],
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", " 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: raise ValueError(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, "the future?"),
"full": m.group("incbase") is None,
"previous": m.group("incbase"),
"size": 0,
}
backups[key]["size"] += os.path.getsize(os.path.join(backup_dir, fn))
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)
# Get the average size of incremental backups and the size of the
# most recent full backup.
# 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"]
# Predict how many more increments until the next full backup,
# and add to that the time we hold onto backups, to predict
# how long the most recent full backup+increments will be held
# onto. Round up since the backup occurs on the night following
# when the threshold is met.
# 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
if incremental_count > 0 and first_full_size is not None:
deleted_in = "approx. %d days" % round(keep_backups_for_days + (.5 * first_full_size - incremental_size) / (incremental_size/incremental_count) + .5)
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)
# When will a backup be deleted?
# ...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
days_ago = now - datetime.timedelta(days=keep_backups_for_days)
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"]), "on next daily backup")
# 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": backup_dir,
"encpwfile": os.path.join(backup_root, 'secret_key.txt'),
"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.
@ -117,68 +164,151 @@ 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):
env = load_environment()
exclusive_process("backup")
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')
# In an older version of this script, duplicity was called
# such that it did not encrypt the backups it created (in
# backup/duplicity), and instead openssl was called separately
# after each backup run, creating AES256 encrypted copies of
# each file created by duplicity in backup/encrypted.
#
# We detect the transition by the presence of backup/duplicity
# and handle it by 'dupliception': we move all the old *un*encrypted
# duplicity files up out of the backup/duplicity directory (as
# backup/ is excluded from duplicity runs) in order that it is
# included in the next run, and we delete backup/encrypted (which
# duplicity will output files directly to, post-transition).
old_backup_dir = os.path.join(backup_root, 'duplicity')
migrated_unencrypted_backup_dir = os.path.join(env["STORAGE_ROOT"], "migrated_unencrypted_backup")
if os.path.isdir(old_backup_dir):
# Move the old unencrypted files to a new location outside of
# the backup root so they get included in the next (new) backup.
# Then we'll delete them. Also so that they do not get in the
# way of duplicity doing a full backup on the first run after
# we take care of this.
shutil.move(old_backup_dir, migrated_unencrypted_backup_dir)
# The backup_dir (backup/encrypted) now has a new purpose.
# Clear it out.
shutil.rmtree(backup_dir)
# 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)
# Stop services.
shell('check_call', ["/usr/sbin/service", "dovecot", "stop"])
shell('check_call', ["/usr/sbin/service", "postfix", "stop"])
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.
with open(os.path.join(backup_root, 'secret_key.txt')) as f:
backup_root = os.path.join(env["STORAGE_ROOT"], 'backup')
with open(os.path.join(backup_root, 'secret_key.txt'), encoding="utf-8") as f:
passphrase = f.readline().strip()
if len(passphrase) < 43: raise Exception("secret_key.txt's first line is too short!")
env_with_passphrase = { "PASSPHRASE" : passphrase }
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. (The S3 region name is also stored in the
# hostname part of the URL, in the username portion, which we also
# have to drop here).
target[1], target[2] = target[2].lstrip('/').split('/', 1)
target = urlunsplit(target)
return target
def get_duplicity_additional_args(env):
config = get_backup_config(env)
if get_target_type(config) == 'rsync':
# Extract a port number for the ssh transport. Duplicity accepts the
# optional port number syntax in the target, but it doesn't appear to act
# on it, so we set the ssh port explicitly via the duplicity options.
from urllib.parse import urlsplit
try:
port = urlsplit(config["target"]).port
except ValueError:
port = 22
if port is None:
port = 22
return [
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.
# The region name, which is required by some non-AWS endpoints,
# is saved inside the username portion of the URL.
from urllib.parse import urlsplit, urlunsplit
target = urlsplit(config["target"])
endpoint_url = urlunsplit(("https", target.hostname, '', '', ''))
args = ["--s3-endpoint-url", endpoint_url]
if target.username: # region name is stuffed here
args += ["--s3-region-name", target.username]
return args
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):
return config["target"].split(":")[0]
def perform_backup(full_backup):
env = load_environment()
# Create an global exclusive lock so that the backup script
# cannot be run more than one.
Lock(die=True).forever()
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.
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.
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)
service_command("php8.0-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
@ -187,35 +317,37 @@ def perform_backup(full_backup):
shell('check_call', [
"/usr/bin/duplicity",
"full" if full_backup else "incr",
"--verbosity", "warning", "--no-print-statistics",
"--archive-dir", backup_cache_dir,
"--exclude", backup_root,
"--volsize", "250",
"--gpg-options", "--cipher-algo=AES256",
"--gpg-options", "'--cipher-algo=AES256'",
"--allow-source-mismatch",
*get_duplicity_additional_args(env),
env["STORAGE_ROOT"],
"file://" + backup_dir,
"--allow-source-mismatch"
get_duplicity_target_url(config),
],
env_with_passphrase)
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"])
# Once the migrated backup is included in a new backup, it can be deleted.
if os.path.isdir(migrated_unencrypted_backup_dir):
shutil.rmtree(migrated_unencrypted_backup_dir)
service_command("postgrey", "start", quit=False)
service_command("dovecot", "start", quit=False)
service_command("postfix", "start", quit=False)
service_command("php8.0-fpm", "start", quit=False)
# Remove old backups. This deletes all backup data no longer needed
# from more than 3 days ago.
shell('check_call', [
"/usr/bin/duplicity",
"remove-older-than",
"%dD" % keep_backups_for_days,
"%dD" % config["min_age_in_days"],
"--verbosity", "error",
"--archive-dir", backup_cache_dir,
"--force",
"file://" + backup_dir
*get_duplicity_additional_args(env),
get_duplicity_target_url(config)
],
env_with_passphrase)
get_duplicity_env_vars(env))
# From duplicity's manual:
# "This should only be necessary after a duplicity session fails or is
@ -225,15 +357,18 @@ def perform_backup(full_backup):
shell('check_call', [
"/usr/bin/duplicity",
"cleanup",
"--verbosity", "error",
"--archive-dir", backup_cache_dir,
"--force",
"file://" + backup_dir
*get_duplicity_additional_args(env),
get_duplicity_target_url(config)
],
env_with_passphrase)
get_duplicity_env_vars(env))
# Change ownership of backups to the user-data user, so that the after-bcakup
# script can access them.
shell('check_call', ["/bin/chown", "-R", env["STORAGE_USER"], backup_dir])
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
@ -241,7 +376,7 @@ def perform_backup(full_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
@ -254,9 +389,9 @@ def perform_backup(full_backup):
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')
backup_dir = os.path.join(backup_root, 'encrypted')
env_with_passphrase = { "PASSPHRASE" : open(os.path.join(backup_root, 'secret_key.txt')).read() }
shell('check_call', [
"/usr/bin/duplicity",
"--verbosity", "info",
@ -264,16 +399,248 @@ def run_duplicity_verification():
"--compare-data",
"--archive-dir", backup_cache_dir,
"--exclude", backup_root,
"file://" + backup_dir,
*get_duplicity_additional_args(env),
get_duplicity_target_url(config),
env["STORAGE_ROOT"],
], env_with_passphrase)
], 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/bin/duplicity",
"restore",
"--archive-dir", backup_cache_dir,
*get_duplicity_additional_args(env),
get_duplicity_target_url(config),
*args],
get_duplicity_env_vars(env))
def print_duplicity_command():
import shlex
env = load_environment()
config = get_backup_config(env)
backup_cache_dir = os.path.join(env["STORAGE_ROOT"], 'backup', 'cache')
for k, v in get_duplicity_env_vars(env).items():
print(f"export {k}={shlex.quote(v)}")
print("duplicity", "{command}", shlex.join([
"--archive-dir", backup_cache_dir,
*get_duplicity_additional_args(env),
get_duplicity_target_url(config)
]))
def list_target_files(config):
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}'
# Strip off any trailing port specifier because it's not valid in rsync's
# DEST syntax. Explicitly set the port number for the ssh transport.
user_host, *_ = target.netloc.rsplit(':', 1)
try:
port = target.port
except ValueError:
port = 22
if port is None:
port = 22
target_path = target.path
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 -i /root/.ssh/id_rsa_miab -oStrictHostKeyChecking=no -oBatchMode=yes -p {port}',
'--list-only',
'-r',
rsync_target.format(
host=user_host,
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 = f"Provided path {target_path} is invalid."
elif 'Network is unreachable' in listing:
reason = f"The IP address {target.hostname} is unreachable."
elif 'Could not resolve hostname' in listing:
reason = f"The hostname {target.hostname} cannot be resolved."
else:
reason = ("Unknown error."
"Please check running 'management/backup.py --verify'"
"from mailinabox sources to debug the issue.")
msg = f"Connection to rsync host failed: {reason}"
raise ValueError(msg)
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 == "":
msg = "Enter an S3 bucket name."
raise ValueError(msg)
# 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)['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':
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 = urllib.parse.unquote(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:
msg = "B2 Bucket does not exist. Please double check your information!"
raise ValueError(msg)
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, 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)
config["target"] = target
config["target_user"] = target_user
config["target_pass"] = target_pass
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",
}
# Merge in anything written to custom.yaml.
try:
with open(os.path.join(backup_root, 'custom.yaml'), encoding="utf-8") as f:
custom_config = rtyaml.load(f)
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):
with open(ssh_pub_key, encoding="utf-8") as f:
config["ssh_pub_key"] = f.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", encoding="utf-8") as f:
f.write(rtyaml.dump(newconfig))
if __name__ == "__main__":
import sys
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(f"{fn}\t{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:])
elif sys.argv[-1] == "--duplicity-command":
print_duplicity_command()
else:
# Perform a backup. Add --full to force a full backup rather than
# possibly performing an incremental backup.

144
management/cli.py Executable file
View File

@ -0,0 +1,144 @@
#!/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, getpass, urllib.request, urllib.error, json, csv
import contextlib
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:
with contextlib.suppress(Exception):
print(e.read().decode("utf8"))
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):
with open('/var/lib/mailinabox/api.key', encoding='utf-8') as f:
key = f.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} 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 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='')
print()
elif sys.argv[1] == "user" and sys.argv[2] in {"add", "password"}:
if len(sys.argv) < 5:
email = input('email: ') if len(sys.argv) < 4 else 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:
action = 'add' if sys.argv[2] == 'make-admin' else '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 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] }))
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.

View File

@ -1,32 +1,45 @@
#!/usr/bin/python3
#!/usr/local/lib/mailinabox/env/bin/python3
#
# The API can be accessed on the command line, e.g. use `curl` like so:
# curl --user $(</var/lib/mailinabox/api.key): http://localhost:10222/mail/users
#
# During development, you can start the Mail-in-a-Box control panel
# by running this script, e.g.:
#
# service mailinabox stop # stop the system process
# DEBUG=1 management/daemon.py
# service mailinabox start # when done debugging, start it up again
import os, os.path, re, json
import os, os.path, re, json, time
import multiprocessing.pool
from functools import wraps
from flask import Flask, request, render_template, abort, Response, send_from_directory
from flask import Flask, request, render_template, Response, send_from_directory, make_response
import auth, utils
from mailconfig import get_mail_users, get_mail_users_ex, get_admins, add_mail_user, set_mail_password, remove_mail_user
from mailconfig import get_mail_user_privileges, add_remove_mail_user_privilege
from mailconfig import get_mail_aliases, get_mail_aliases_ex, get_mail_domains, add_mail_alias, remove_mail_alias
# Create a worker pool for the status checks. The pool should
# live across http requests so we don't baloon the system with
# processes.
import multiprocessing.pool
pool = multiprocessing.pool.Pool(processes=10)
from mfa import get_public_mfa_state, provision_totp, validate_totp_secret, enable_mfa, disable_mfa
import contextlib
env = utils.load_environment()
auth_service = auth.KeyAuthService()
auth_service = auth.AuthService()
# We may deploy via a symbolic link, which confuses flask's template finding.
me = __file__
try:
with contextlib.suppress(OSError):
me = os.readlink(__file__)
except OSError:
pass
# for generating CSRs we need a list of country codes
csr_country_codes = []
with open(os.path.join(os.path.dirname(me), "csr_country_codes.tsv"), encoding="utf-8") as f:
for line in f:
if line.strip() == "" or line.startswith("#"): continue
code, name = line.strip().split("\t")[0:2]
csr_country_codes.append((code, name))
app = Flask(__name__, template_folder=os.path.abspath(os.path.join(os.path.dirname(me), "templates")))
@ -34,26 +47,39 @@ app = Flask(__name__, template_folder=os.path.abspath(os.path.join(os.path.dirna
def authorized_personnel_only(viewfunc):
@wraps(viewfunc)
def newview(*args, **kwargs):
# Authenticate the passed credentials, which is either the API key or a username:password pair.
# Authenticate the passed credentials, which is either the API key or a username:password pair
# and an optional X-Auth-Token token.
error = None
privs = []
try:
email, privs = auth_service.authenticate(request, env)
except ValueError as e:
# Write a line in the log recording the failed login, unless no authorization header
# was given which can happen on an initial request before a 403 response.
if "Authorization" in request.headers:
log_failed_login(request)
# Authentication failed.
privs = []
error = str(e)
# Authorized to access an API view?
if "admin" in privs:
# Call view func.
# Store the email address of the logged in user so it can be accessed
# from the API methods that affect the calling user.
request.user_email = email
request.user_privs = privs
# Call view func.
return viewfunc(*args, **kwargs)
elif not error:
if not error:
error = "You are not an administrator."
# Not authorized. Return a 401 (send auth) and a prompt to authorize by default.
status = 401
headers = {
'WWW-Authenticate': 'Basic realm="{0}"'.format(auth_service.auth_realm),
'WWW-Authenticate': f'Basic realm="{auth_service.auth_realm}"',
'X-Reason': error,
}
@ -63,7 +89,7 @@ def authorized_personnel_only(viewfunc):
status = 403
headers = None
if request.headers.get('Accept') in (None, "", "*/*"):
if request.headers.get('Accept') in {None, "", "*/*"}:
# Return plain text output.
return Response(error+"\n", status=status, mimetype='text/plain', headers=headers)
else:
@ -79,8 +105,8 @@ def authorized_personnel_only(viewfunc):
def unauthorized(error):
return auth_service.make_unauthorized_response()
def json_response(data):
return Response(json.dumps(data, indent=2, sort_keys=True)+'\n', status=200, mimetype='application/json')
def json_response(data, status=200):
return Response(json.dumps(data, indent=2, sort_keys=True)+'\n', status=status, mimetype='application/json')
###################################
@ -90,46 +116,75 @@ def json_response(data):
def index():
# Render the control panel. This route does not require user authentication
# so it must be safe!
no_users_exist = (len(get_mail_users(env)) == 0)
no_admins_exist = (len(get_admins(env)) == 0)
import boto3.s3
backup_s3_hosts = [(r, f"s3.{r}.amazonaws.com") for r in boto3.session.Session().get_available_regions('s3')]
return render_template('index.html',
hostname=env['PRIMARY_HOSTNAME'],
storage_root=env['STORAGE_ROOT'],
no_users_exist=no_users_exist,
no_admins_exist=no_admins_exist,
backup_s3_hosts=backup_s3_hosts,
csr_country_codes=csr_country_codes,
)
@app.route('/me')
def me():
# Create a session key by checking the username/password in the Authorization header.
@app.route('/login', methods=["POST"])
def login():
# Is the caller authorized?
try:
email, privs = auth_service.authenticate(request, env)
email, privs = auth_service.authenticate(request, env, login_only=True)
except ValueError as e:
return json_response({
"status": "invalid",
"reason": str(e),
if "missing-totp-token" in str(e):
return json_response({
"status": "missing-totp-token",
"reason": str(e),
})
else:
# Log the failed login
log_failed_login(request)
return json_response({
"status": "invalid",
"reason": str(e),
})
# Return a new session for the user.
resp = {
"status": "ok",
"email": email,
"privileges": privs,
"api_key": auth_service.create_session_key(email, env, type='login'),
}
# Is authorized as admin? Return an API key for future use.
if "admin" in privs:
resp["api_key"] = auth_service.create_user_key(email, env)
app.logger.info(f"New login session created for {email}")
# Return.
return json_response(resp)
@app.route('/logout', methods=["POST"])
def logout():
try:
email, _ = auth_service.authenticate(request, env, logout=True)
app.logger.info(f"{email} logged out")
except ValueError:
pass
finally:
return json_response({ "status": "ok" })
# MAIL
@app.route('/mail/users')
@authorized_personnel_only
def mail_users():
if request.args.get("format", "") == "json":
return json_response(get_mail_users_ex(env, with_archived=True, with_slow_info=True))
return json_response(get_mail_users_ex(env, with_archived=True))
else:
return "".join(x+"\n" for x in get_mail_users(env))
@ -179,14 +234,15 @@ def mail_aliases():
if request.args.get("format", "") == "json":
return json_response(get_mail_aliases_ex(env))
else:
return "".join(x+"\t"+y+"\n" for x, y in get_mail_aliases(env))
return "".join(address+"\t"+receivers+"\t"+(senders or "")+"\n" for address, receivers, senders, auto in get_mail_aliases(env))
@app.route('/mail/aliases/add', methods=['POST'])
@authorized_personnel_only
def mail_aliases_add():
return add_mail_alias(
request.form.get('source', ''),
request.form.get('destination', ''),
request.form.get('address', ''),
request.form.get('forwards_to', ''),
request.form.get('permitted_senders', ''),
env,
update_if_exists=(request.form.get('update_if_exists', '') == '1')
)
@ -194,7 +250,7 @@ def mail_aliases_add():
@app.route('/mail/aliases/remove', methods=['POST'])
@authorized_personnel_only
def mail_aliases_remove():
return remove_mail_alias(request.form.get('source', ''), env)
return remove_mail_alias(request.form.get('address', ''), env)
@app.route('/mail/domains')
@authorized_personnel_only
@ -222,31 +278,64 @@ def dns_update():
@authorized_personnel_only
def dns_get_secondary_nameserver():
from dns_update import get_custom_dns_config, get_secondary_dns
return json_response({ "hostname": get_secondary_dns(get_custom_dns_config(env)) })
return json_response({ "hostnames": get_secondary_dns(get_custom_dns_config(env), mode=None) })
@app.route('/dns/secondary-nameserver', methods=['POST'])
@authorized_personnel_only
def dns_set_secondary_nameserver():
from dns_update import set_secondary_dns
try:
return set_secondary_dns(request.form.get('hostname'), env)
return set_secondary_dns([ns.strip() for ns in re.split(r"[, ]+", request.form.get('hostnames') or "") if ns.strip() != ""], env)
except ValueError as e:
return (str(e), 400)
@app.route('/dns/custom')
@authorized_personnel_only
def dns_get_records(qname=None, rtype=None):
from dns_update import get_custom_dns_config
return json_response([
{
"qname": r[0],
"rtype": r[1],
"value": r[2],
}
for r in get_custom_dns_config(env)
if r[0] != "_secondary_nameserver"
and (not qname or r[0] == qname)
and (not rtype or r[1] == rtype) ])
# Get the current set of custom DNS records.
from dns_update import get_custom_dns_config, get_dns_zones
records = get_custom_dns_config(env, only_real_records=True)
# Filter per the arguments for the more complex GET routes below.
records = [r for r in records
if (not qname or r[0] == qname)
and (not rtype or r[1] == rtype) ]
# Make a better data structure.
records = [
{
"qname": r[0],
"rtype": r[1],
"value": r[2],
"sort-order": { },
} for r in records ]
# To help with grouping by zone in qname sorting, label each record with which zone it is in.
# There's an inconsistency in how we handle zones in get_dns_zones and in sort_domains, so
# do this first before sorting the domains within the zones.
zones = utils.sort_domains([z[0] for z in get_dns_zones(env)], env)
for r in records:
for z in zones:
if r["qname"] == z or r["qname"].endswith("." + z):
r["zone"] = z
break
# Add sorting information. The 'created' order follows the order in the YAML file on disk,
# which tracs the order entries were added in the control panel since we append to the end.
# The 'qname' sort order sorts by our standard domain name sort (by zone then by qname),
# then by rtype, and last by the original order in the YAML file (since sorting by value
# may not make sense, unless we parse IP addresses, for example).
for i, r in enumerate(records):
r["sort-order"]["created"] = i
domain_sort_order = utils.sort_domains([r["qname"] for r in records], env)
for i, r in enumerate(sorted(records, key = lambda r : (
zones.index(r["zone"]) if r.get("zone") else 0, # record is not within a zone managed by the box
domain_sort_order.index(r["qname"]),
r["rtype"]))):
r["sort-order"]["qname"] = i
# Return.
return json_response(records)
@app.route('/dns/custom/<qname>', methods=['GET', 'POST', 'PUT', 'DELETE'])
@app.route('/dns/custom/<qname>/<rtype>', methods=['GET', 'POST', 'PUT', 'DELETE'])
@ -265,9 +354,9 @@ def dns_set_record(qname, rtype="A"):
# Get the existing records matching the qname and rtype.
return dns_get_records(qname, rtype)
elif request.method in ("POST", "PUT"):
elif request.method in {"POST", "PUT"}:
# There is a default value for A/AAAA records.
if rtype in ("A", "AAAA") and value == "":
if rtype in {"A", "AAAA"} and value == "":
value = request.environ.get("HTTP_X_FORWARDED_FOR") # normally REMOTE_ADDR but we're behind nginx as a reverse proxy
# Cannot add empty records.
@ -283,7 +372,7 @@ def dns_set_record(qname, rtype="A"):
# make this action set (replace all records for this
# qname-rtype pair) rather than add (add a new record).
action = "set"
elif request.method == "DELETE":
if value == '':
# Delete all records for this qname-type pair.
@ -306,24 +395,126 @@ def dns_get_dump():
from dns_update import build_recommended_dns
return json_response(build_recommended_dns(env))
@app.route('/dns/zonefile/<zone>')
@authorized_personnel_only
def dns_get_zonefile(zone):
from dns_update import get_dns_zonefile
return Response(get_dns_zonefile(zone, env), status=200, mimetype='text/plain')
# SSL
@app.route('/ssl/status')
@authorized_personnel_only
def ssl_get_status():
from ssl_certificates import get_certificates_to_provision
from web_update import get_web_domains_info, get_web_domains
# What domains can we provision certificates for? What unexpected problems do we have?
provision, cant_provision = get_certificates_to_provision(env, show_valid_certs=False)
# What's the current status of TLS certificates on all of the domain?
domains_status = get_web_domains_info(env)
domains_status = [
{
"domain": d["domain"],
"status": d["ssl_certificate"][0],
"text": d["ssl_certificate"][1] + (" " + cant_provision[d["domain"]] if d["domain"] in cant_provision else "")
} for d in domains_status ]
# Warn the user about domain names not hosted here because of other settings.
for domain in set(get_web_domains(env, exclude_dns_elsewhere=False)) - set(get_web_domains(env)):
domains_status.append({
"domain": domain,
"status": "not-applicable",
"text": "The domain's website is hosted elsewhere.",
})
return json_response({
"can_provision": utils.sort_domains(provision, env),
"status": domains_status,
})
@app.route('/ssl/csr/<domain>', methods=['POST'])
@authorized_personnel_only
def ssl_get_csr(domain):
from web_update import get_domain_ssl_files, create_csr
ssl_key, ssl_certificate, ssl_via = get_domain_ssl_files(domain, env)
return create_csr(domain, ssl_key, env)
from ssl_certificates import create_csr
ssl_private_key = os.path.join(os.path.join(env["STORAGE_ROOT"], 'ssl', 'ssl_private_key.pem'))
return create_csr(domain, ssl_private_key, request.form.get('countrycode', ''), env)
@app.route('/ssl/install', methods=['POST'])
@authorized_personnel_only
def ssl_install_cert():
from web_update import install_cert
from web_update import get_web_domains
from ssl_certificates import install_cert
domain = request.form.get('domain')
ssl_cert = request.form.get('cert')
ssl_chain = request.form.get('chain')
if domain not in get_web_domains(env):
return "Invalid domain name."
return install_cert(domain, ssl_cert, ssl_chain, env)
@app.route('/ssl/provision', methods=['POST'])
@authorized_personnel_only
def ssl_provision_certs():
from ssl_certificates import provision_certificates
requests = provision_certificates(env, limit_domains=None)
return json_response({ "requests": requests })
# multi-factor auth
@app.route('/mfa/status', methods=['POST'])
@authorized_personnel_only
def mfa_get_status():
# Anyone accessing this route is an admin, and we permit them to
# see the MFA status for any user if they submit a 'user' form
# field. But we don't include provisioning info since a user can
# only provision for themselves.
email = request.form.get('user', request.user_email) # user field if given, otherwise the user making the request
try:
resp = {
"enabled_mfa": get_public_mfa_state(email, env)
}
if email == request.user_email:
resp.update({
"new_mfa": {
"totp": provision_totp(email, env)
}
})
except ValueError as e:
return (str(e), 400)
return json_response(resp)
@app.route('/mfa/totp/enable', methods=['POST'])
@authorized_personnel_only
def totp_post_enable():
secret = request.form.get('secret')
token = request.form.get('token')
label = request.form.get('label')
if not isinstance(token, str):
return ("Bad Input", 400)
try:
validate_totp_secret(secret)
enable_mfa(request.user_email, "totp", secret, token, label, env)
except ValueError as e:
return (str(e), 400)
return "OK"
@app.route('/mfa/disable', methods=['POST'])
@authorized_personnel_only
def totp_post_disable():
# Anyone accessing this route is an admin, and we permit them to
# disable the MFA status for any user if they submit a 'user' form
# field.
email = request.form.get('user', request.user_email) # user field if given, otherwise the user making the request
try:
result = disable_mfa(email, request.form.get('mfa-id') or None, env) # convert empty string to None
except ValueError as e:
return (str(e), 400)
if result: # success
return "OK"
else: # error
return ("Invalid user or MFA id.", 400)
# WEB
@app.route('/web/domains')
@ -376,7 +567,11 @@ def system_status():
def print_line(self, message, monospace=False):
self.items[-1]["extra"].append({ "text": message, "monospace": monospace })
output = WebOutput()
run_checks(False, env, output, pool)
# Create a temporary pool of processes for the status checks
with multiprocessing.pool.Pool(processes=5) as pool:
run_checks(False, env, output, pool)
pool.close()
pool.join()
return json_response(output.items)
@app.route('/system/updates')
@ -384,8 +579,7 @@ def system_status():
def show_updates():
from status_checks import list_apt_updates
return "".join(
"%s (%s)\n"
% (p["package"], p["version"])
"{} ({})\n".format(p["package"], p["version"])
for p in list_apt_updates())
@app.route('/system/update-packages', methods=["POST"])
@ -396,40 +590,183 @@ def do_updates():
"DEBIAN_FRONTEND": "noninteractive"
})
@app.route('/system/reboot', methods=["GET"])
@authorized_personnel_only
def needs_reboot():
from status_checks import is_reboot_needed_due_to_package_installation
if is_reboot_needed_due_to_package_installation():
return json_response(True)
else:
return json_response(False)
@app.route('/system/reboot', methods=["POST"])
@authorized_personnel_only
def do_reboot():
# To keep the attack surface low, we don't allow a remote reboot if one isn't necessary.
from status_checks import is_reboot_needed_due_to_package_installation
if is_reboot_needed_due_to_package_installation():
return utils.shell("check_output", ["/sbin/shutdown", "-r", "now"], capture_stderr=True)
else:
return "No reboot is required, so it is not allowed."
@app.route('/system/backup/status')
@authorized_personnel_only
def backup_status():
from backup import backup_status
return json_response(backup_status(env))
try:
return json_response(backup_status(env))
except Exception as e:
return json_response({ "error": str(e) })
@app.route('/system/backup/config', methods=["GET"])
@authorized_personnel_only
def backup_get_custom():
from backup import get_backup_config
return json_response(get_backup_config(env, for_ui=True))
@app.route('/system/backup/config', methods=["POST"])
@authorized_personnel_only
def backup_set_custom():
from backup import backup_set_custom
return json_response(backup_set_custom(env,
request.form.get('target', ''),
request.form.get('target_user', ''),
request.form.get('target_pass', ''),
request.form.get('min_age', '')
))
@app.route('/system/privacy', methods=["GET"])
@authorized_personnel_only
def privacy_status_get():
config = utils.load_settings(env)
return json_response(config.get("privacy", True))
@app.route('/system/privacy', methods=["POST"])
@authorized_personnel_only
def privacy_status_set():
config = utils.load_settings(env)
config["privacy"] = (request.form.get('value') == "private")
utils.write_settings(config, env)
return "OK"
# MUNIN
@app.route('/munin/')
@app.route('/munin/<path:filename>')
@authorized_personnel_only
def munin(filename=""):
# Checks administrative access (@authorized_personnel_only) and then just proxies
# the request to static files.
def munin_start():
# Munin pages, static images, and dynamically generated images are served
# outside of the AJAX API. We'll start with a 'start' API that sets a cookie
# that subsequent requests will read for authorization. (We don't use cookies
# for the API to avoid CSRF vulnerabilities.)
response = make_response("OK")
response.set_cookie("session", auth_service.create_session_key(request.user_email, env, type='cookie'),
max_age=60*30, secure=True, httponly=True, samesite="Strict") # 30 minute duration
return response
def check_request_cookie_for_admin_access():
session = auth_service.get_session(None, request.cookies.get("session", ""), "cookie", env)
if not session: return False
privs = get_mail_user_privileges(session["email"], env)
if not isinstance(privs, list): return False
if "admin" not in privs: return False
return True
def authorized_personnel_only_via_cookie(f):
@wraps(f)
def g(*args, **kwargs):
if not check_request_cookie_for_admin_access():
return Response("Unauthorized", status=403, mimetype='text/plain', headers={})
return f(*args, **kwargs)
return g
@app.route('/munin/<path:filename>')
@authorized_personnel_only_via_cookie
def munin_static_file(filename=""):
# Proxy the request to static files.
if filename == "": filename = "index.html"
return send_from_directory("/var/cache/munin/www", filename)
@app.route('/munin/cgi-graph/<path:filename>')
@authorized_personnel_only_via_cookie
def munin_cgi(filename):
""" Relay munin cgi dynazoom requests
/usr/lib/munin/cgi/munin-cgi-graph is a perl cgi script in the munin package
that is responsible for generating binary png images _and_ associated HTTP
headers based on parameters in the requesting URL. All output is written
to stdout which munin_cgi splits into response headers and binary response
data.
munin-cgi-graph reads environment variables to determine
what it should do. It expects a path to be in the env-var PATH_INFO, and a
querystring to be in the env-var QUERY_STRING.
munin-cgi-graph has several failure modes. Some write HTTP Status headers and
others return nonzero exit codes.
Situating munin_cgi between the user-agent and munin-cgi-graph enables keeping
the cgi script behind mailinabox's auth mechanisms and avoids additional
support infrastructure like spawn-fcgi.
"""
COMMAND = 'su munin --preserve-environment --shell=/bin/bash -c /usr/lib/munin/cgi/munin-cgi-graph'
# su changes user, we use the munin user here
# --preserve-environment retains the environment, which is where Popen's `env` data is
# --shell=/bin/bash ensures the shell used is bash
# -c "/usr/lib/munin/cgi/munin-cgi-graph" passes the command to run as munin
# "%s" is a placeholder for where the request's querystring will be added
if filename == "":
return ("a path must be specified", 404)
query_str = request.query_string.decode("utf-8", 'ignore')
env = {'PATH_INFO': '/%s/' % filename, 'REQUEST_METHOD': 'GET', 'QUERY_STRING': query_str}
code, binout = utils.shell('check_output',
COMMAND.split(" ", 5),
# Using a maxsplit of 5 keeps the last arguments together
env=env,
return_bytes=True,
trap=True)
if code != 0:
# nonzero returncode indicates error
app.logger.error("munin_cgi: munin-cgi-graph returned nonzero exit code, %s", code)
return ("error processing graph image", 500)
# /usr/lib/munin/cgi/munin-cgi-graph returns both headers and binary png when successful.
# A double-Windows-style-newline always indicates the end of HTTP headers.
headers, image_bytes = binout.split(b'\r\n\r\n', 1)
response = make_response(image_bytes)
for line in headers.splitlines():
name, value = line.decode("utf8").split(':', 1)
response.headers[name] = value
if 'Status' in response.headers and '404' in response.headers['Status']:
app.logger.warning("munin_cgi: munin-cgi-graph returned 404 status code. PATH_INFO=%s", env['PATH_INFO'])
return response
def log_failed_login(request):
# We need to figure out the ip to list in the message, all our calls are routed
# through nginx who will put the original ip in X-Forwarded-For.
# During setup we call the management interface directly to determine the user
# status. So we can't always use X-Forwarded-For because during setup that header
# will not be present.
ip = request.headers.getlist("X-Forwarded-For")[0] if request.headers.getlist("X-Forwarded-For") else request.remote_addr
# We need to add a timestamp to the log message, otherwise /dev/log will eat the "duplicate"
# message.
app.logger.warning( f"Mail-in-a-Box Management Daemon: Failed login attempt from ip {ip} - timestamp {time.time()}")
# APP
if __name__ == '__main__':
if "DEBUG" in os.environ: app.debug = True
if "APIKEY" in os.environ: auth_service.key = os.environ["APIKEY"]
if "DEBUG" in os.environ:
# Turn on Flask debugging.
app.debug = True
if not app.debug:
app.logger.addHandler(utils.create_syslog_handler())
# For testing on the command line, you can use `curl` like so:
# curl --user $(</var/lib/mailinabox/api.key): http://localhost:10222/mail/users
auth_service.write_key()
# For testing in the browser, you can copy the API key that's output to the
# debug console and enter that as the username
app.logger.info('API key: ' + auth_service.key)
#app.logger.info('API key: ' + auth_service.key)
# Start the application server. Listens on 127.0.0.1 (IPv4 only).
app.run(port=10222)

25
management/daily_tasks.sh Executable file
View File

@ -0,0 +1,25 @@
#!/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"
# 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

View File

@ -0,0 +1,60 @@
#!/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
# 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]
# Administrator's email address.
admin_addr = "administrator@" + env['PRIMARY_HOSTNAME']
# 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()
msg['From'] = '"{}" <{}>'.format(env['PRIMARY_HOSTNAME'], admin_addr)
msg['To'] = admin_addr
msg['Subject'] = "[{}] {}".format(env['PRIMARY_HOSTNAME'], subject)
content_html = f'<html><body><pre style="overflow-x: scroll; white-space: pre;">{html.escape(content)}</pre></body></html>'
msg.attach(MIMEText(content, '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(
admin_addr, # MAIL FROM
admin_addr, # RCPT TO
msg.as_string())
smtpclient.quit()

View File

@ -1,121 +1,869 @@
#!/usr/bin/python3
#!/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 re, os.path
import dateutil.parser
import time
from dateutil.relativedelta import relativedelta
import mailconfig
import utils
def scan_mail_log(logger, env):
collector = {
"other-services": set(),
"imap-logins": { },
"postgrey": { },
"rejected-mail": { },
}
collector["real_mail_addresses"] = set(mailconfig.get_mail_users(env)) | set(alias[0] for alias in mailconfig.get_mail_aliases(env))
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',
)
for fn in ('/var/log/mail.log.1', '/var/log/mail.log'):
if not os.path.exists(fn): continue
with open(fn, 'rb') as log:
for line in log:
line = line.decode("utf8", errors='replace')
scan_mail_log_line(line.strip(), collector)
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))
])
if collector["imap-logins"]:
logger.add_heading("Recent IMAP Logins")
logger.print_block("The most recent login from each remote IP adddress is show.")
for k in utils.sort_email_addresses(collector["imap-logins"], env):
for ip, date in sorted(collector["imap-logins"][k].items(), key = lambda kv : kv[1]):
logger.print_line(k + "\t" + str(date) + "\t" + ip)
END_DATE = NOW = datetime.datetime.now()
START_DATE = None
if collector["postgrey"]:
logger.add_heading("Greylisted Mail")
logger.print_block("The following mail was greylisted, meaning the emails were temporarily rejected. Legitimate senders will try again within ten minutes.")
logger.print_line("recipient" + "\t" + "received" + "\t" + "sender" + "\t" + "delivered")
for recipient in utils.sort_email_addresses(collector["postgrey"], env):
for (client_address, sender), (first_date, delivered_date) in sorted(collector["postgrey"][recipient].items(), key = lambda kv : kv[1][0]):
logger.print_line(recipient + "\t" + str(first_date) + "\t" + sender + "\t" + (("delivered " + str(delivered_date)) if delivered_date else "no retry yet"))
VERBOSE = False
if collector["rejected-mail"]:
logger.add_heading("Rejected Mail")
logger.print_block("The following incoming mail was rejected.")
for k in utils.sort_email_addresses(collector["rejected-mail"], env):
for date, sender, message in collector["rejected-mail"][k]:
logger.print_line(k + "\t" + str(date) + "\t" + sender + "\t" + message)
# 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()
with gzip.open(fn, 'rb') as f:
shutil.copyfileobj(f, 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)) |
{alias[0] for alias in mailconfig.get_mail_aliases(env)})
except ImportError:
pass
print(f"Scanning logs from {START_DATE:%Y-%m-%d %H:%M:%S} to {END_DATE:%Y-%m-%d %H:%M:%S}"
)
# 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", [[
f"{protocol_name} {host}: {count} times"
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.extend((f'{date} - {sender} ', ' %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(collector["other-services"]), sep='\n')
if len(collector["other-services"]) > 0:
logger.add_heading("Other")
logger.print_block("Unrecognized services in the log: " + ", ".join(collector["other-services"]))
def scan_mail_log_line(line, collector):
m = re.match(r"(\S+ \d+ \d+:\d+:\d+) (\S+) (\S+?)(\[\d+\])?: (.*)", line)
if not m: return
""" Scan a log line and extract interesting data """
date, system, service, pid, log = m.groups()
date = dateutil.parser.parse(date)
if service == "dovecot":
scan_dovecot_line(date, log, collector)
m = re.match(r"(\w+[\s]+\d+ \d+:\d+:\d+) ([\w]+ )?([\w\-/]+)[^:]*: (.*)", line)
elif service == "postgrey":
scan_postgrey_line(date, log, collector)
if not m:
return True
elif service == "postfix/smtpd":
scan_postfix_smtpd_line(date, log, collector)
date, _system, service, log = m.groups()
collector["scan_count"] += 1
elif service in ("postfix/qmgr", "postfix/pickup", "postfix/cleanup",
"postfix/scache", "spampd", "postfix/anvil", "postfix/master",
"opendkim", "postfix/lmtp", "postfix/tlsmgr"):
# nothing to look at
pass
# print()
# print("date:", date)
# print("host:", system)
# print("service:", service)
# print("log:", log)
else:
collector["other-services"].add(service)
# 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_dovecot_line(date, log, collector):
m = re.match("imap-login: Login: user=<(.*?)>, method=PLAIN, rip=(.*?),", log)
if m:
login, ip = m.group(1), m.group(2)
if ip != "127.0.0.1": # local login from webmail/zpush
collector["imap-logins"].setdefault(login, {})[ip] = date
def scan_postgrey_line(date, log, collector):
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, recipient = m.groups()
key = (client_address, sender)
if action == "greylist" and reason == "new":
collector["postgrey"].setdefault(recipient, {})[key] = (date, None)
elif action == "pass" and reason == "triplet found" and key in collector["postgrey"].get(recipient, {}):
collector["postgrey"][recipient][key] = (collector["postgrey"][recipient][key][0], date)
""" Scan a postgrey log line and extract interesting data """
m = re.match(r"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):
m = re.match("NOQUEUE: reject: RCPT from .*?: (.*?); from=<(.*?)> to=<(.*?)>", log)
if m:
message, sender, recipient = m.groups()
if recipient in collector["real_mail_addresses"]:
# only log mail to real recipients
""" Scan a postfix smtpd log line and extract interesting data """
# skip this, is reported in the greylisting report
if "Recipient address rejected: Greylisted" in message:
return
# Check if the incoming mail was rejected
# simplify this one
m = re.search(r"Client host \[(.*?)\] blocked using zen.spamhaus.org; (.*)", message)
if m:
message = "ip blocked: " + m.group(2)
m = re.match("NOQUEUE: reject: RCPT from .*?: (.*?); from=<(.*?)> to=<(.*?)>", log)
# 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 m:
message, sender, user = m.groups()
collector["rejected-mail"].setdefault(recipient, []).append( (date, sender, message) )
# 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) and (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(r"([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(r"([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', encoding='utf-8') 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 = [temp.format(label) for label in labels]
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))
return None
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 = f"{d[row]!s:<20}"
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.extend(('', '%s' % l, '├─%s' % (len(l) * ''), ''))
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__":
from status_checks import ConsoleOutput
env = utils.load_environment()
scan_mail_log(ConsoleOutput(), env)
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(f"Setting end date to {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)

View File

@ -1,14 +1,23 @@
#!/usr/bin/python3
#!/usr/local/lib/mailinabox/env/bin/python
import subprocess, shutil, os, sqlite3, re
# NOTE:
# This script is run both using the system-wide Python 3
# interpreter (/usr/bin/python3) as well as through the
# virtualenv (/usr/local/lib/mailinabox/env). So only
# import packages at the top level of this script that
# are installed in *both* contexts. We use the system-wide
# Python 3 in setup/questions.sh to validate the email
# address entered by the user.
import os, sqlite3, re
import utils
from email_validator import validate_email as validate_email_, EmailNotValidError
import idna
def validate_email(email, mode=None):
# Checks that an email address is syntactically valid. Returns True/False.
# Until Postfix supports SMTPUTF8, an email address may contain ASCII
# characters only; IDNs must be IDNA-encoded.
# An email address may contain ASCII characters only because Dovecot's
# authentication mechanism gets confused with other character encodings.
#
# When mode=="user", we're checking that this can be a user account name.
# Dovecot has tighter restrictions - letters, numbers, underscore, and
@ -58,7 +67,7 @@ def sanitize_idn_email_address(email):
except (ValueError, idna.IDNAError):
# ValueError: String does not have a single @-sign, so it is not
# a valid email address. IDNAError: Domain part is not IDNA-valid.
# Validation is not this function's job, so return value unchanged.
# Validation is not this function's job, so return value unchanged.
# If there are non-ASCII characters it will be filtered out by
# validate_email.
return email
@ -77,10 +86,7 @@ def prettify_idn_email_address(email):
def is_dcv_address(email):
email = email.lower()
for localpart in ("admin", "administrator", "postmaster", "hostmaster", "webmaster"):
if email.startswith(localpart+"@") or email.startswith(localpart+"+"):
return True
return False
return any(email.startswith((localpart + "@", localpart + "+")) for localpart in ("admin", "administrator", "postmaster", "hostmaster", "webmaster", "abuse"))
def open_database(env, with_connection=False):
conn = sqlite3.connect(env["STORAGE_ROOT"] + "/mail/users.sqlite")
@ -96,7 +102,7 @@ def get_mail_users(env):
users = [ row[0] for row in c.fetchall() ]
return utils.sort_email_addresses(users, env)
def get_mail_users_ex(env, with_archived=False, with_slow_info=False):
def get_mail_users_ex(env, with_archived=False):
# Returns a complex data structure of all user accounts, optionally
# including archived (status="inactive") accounts.
#
@ -130,26 +136,22 @@ def get_mail_users_ex(env, with_archived=False, with_slow_info=False):
}
users.append(user)
if with_slow_info:
user["mailbox_size"] = utils.du(os.path.join(env['STORAGE_ROOT'], 'mail/mailboxes', *reversed(email.split("@"))))
# Add in archived accounts.
if with_archived:
root = os.path.join(env['STORAGE_ROOT'], 'mail/mailboxes')
for domain in os.listdir(root):
for user in os.listdir(os.path.join(root, domain)):
email = user + "@" + domain
mbox = os.path.join(root, domain, user)
if email in active_accounts: continue
user = {
"email": email,
"privileges": "",
"status": "inactive",
"mailbox": mbox,
}
users.append(user)
if with_slow_info:
user["mailbox_size"] = utils.du(mbox)
if os.path.isdir(os.path.join(root, domain)):
for user in os.listdir(os.path.join(root, domain)):
email = user + "@" + domain
mbox = os.path.join(root, domain, user)
if email in active_accounts: continue
user = {
"email": email,
"privileges": [],
"status": "inactive",
"mailbox": mbox,
}
users.append(user)
# Group by domain.
domains = { }
@ -181,14 +183,13 @@ def get_admins(env):
return users
def get_mail_aliases(env):
# Returns a sorted list of tuples of (alias, forward-to string).
# Returns a sorted list of tuples of (address, forward-tos, permitted-senders, auto).
c = open_database(env)
c.execute('SELECT source, destination FROM aliases')
aliases = { row[0]: row[1] for row in c.fetchall() } # make dict
c.execute('SELECT source, destination, permitted_senders, 0 as auto FROM aliases UNION SELECT source, destination, permitted_senders, 1 as auto FROM auto_aliases')
aliases = { row[0]: row for row in c.fetchall() } # make dict
# put in a canonical order: sort by domain, then by email address lexicographically
aliases = [ (source, aliases[source]) for source in utils.sort_email_addresses(aliases.keys(), env) ]
return aliases
return [ aliases[address] for address in utils.sort_email_addresses(aliases.keys(), env) ]
def get_mail_aliases_ex(env):
# Returns a complex data structure of all mail aliases, similar
@ -199,10 +200,11 @@ def get_mail_aliases_ex(env):
# domain: "domain.tld",
# alias: [
# {
# source: "name@domain.tld", # IDNA-encoded
# source_display: "name@domain.tld", # full Unicode
# destination: ["target1@domain.com", "target2@domain.com", ...],
# required: True|False
# address: "name@domain.tld", # IDNA-encoded
# address_display: "name@domain.tld", # full Unicode
# forwards_to: ["user1@domain.com", "receiver-only1@domain.com", ...],
# permitted_senders: ["user1@domain.com", "sender-only1@domain.com", ...] OR null,
# auto: True|False
# },
# ...
# ]
@ -210,48 +212,59 @@ def get_mail_aliases_ex(env):
# ...
# ]
required_aliases = get_required_aliases(env)
domains = {}
for source, destination in get_mail_aliases(env):
for address, forwards_to, permitted_senders, auto in get_mail_aliases(env):
# skip auto domain maps since these are not informative in the control panel's aliases list
if auto and address.startswith("@"): continue
# get alias info
domain = get_domain(source)
required = (source in required_aliases)
domain = get_domain(address)
# add to list
if not domain in domains:
if domain not in domains:
domains[domain] = {
"domain": domain,
"aliases": [],
}
domains[domain]["aliases"].append({
"source": source,
"source_display": prettify_idn_email_address(source),
"destination": [prettify_idn_email_address(d.strip()) for d in destination.split(",")],
"required": required,
"address": address,
"address_display": prettify_idn_email_address(address),
"forwards_to": [prettify_idn_email_address(r.strip()) for r in forwards_to.split(",")],
"permitted_senders": [prettify_idn_email_address(s.strip()) for s in permitted_senders.split(",")] if permitted_senders is not None else None,
"auto": bool(auto),
})
# Sort domains.
domains = [domains[domain] for domain in utils.sort_domains(domains.keys(), env)]
# Sort aliases within each domain first by required-ness then lexicographically by source address.
# Sort aliases within each domain first by required-ness then lexicographically by address.
for domain in domains:
domain["aliases"].sort(key = lambda alias : (alias["required"], alias["source"]))
domain["aliases"].sort(key = lambda alias : (alias["auto"], alias["address"]))
return domains
def get_domain(emailaddr, as_unicode=True):
# Gets the domain part of an email address. Turns IDNA
# back to Unicode for display.
ret = emailaddr.split('@', 1)[1]
if as_unicode: ret = idna.decode(ret.encode('ascii'))
if as_unicode:
try:
ret = idna.decode(ret.encode('ascii'))
except (ValueError, UnicodeError, idna.IDNAError):
# Looks like we have an invalid email address in
# the database. Now is not the time to complain.
pass
return ret
def get_mail_domains(env, filter_aliases=lambda alias : True):
def get_mail_domains(env, filter_aliases=lambda alias : True, users_only=False):
# Returns the domain names (IDNA-encoded) of all of the email addresses
# configured on the system.
return set(
[get_domain(addr, as_unicode=False) for addr in get_mail_users(env)]
+ [get_domain(source, as_unicode=False) for source, target in get_mail_aliases(env) if filter_aliases((source, target)) ]
)
# configured on the system. If users_only is True, only return domains
# with email addresses that correspond to user accounts. Exclude Unicode
# forms of domain names listed in the automatic aliases table.
domains = []
domains.extend([get_domain(login, as_unicode=False) for login in get_mail_users(env)])
if not users_only:
domains.extend([get_domain(address, as_unicode=False) for address, _, _, auto in get_mail_aliases(env) if filter_aliases(address) and not auto ])
return set(domains)
def add_mail_user(email, pw, privs, env):
# validate email
@ -295,25 +308,6 @@ def add_mail_user(email, pw, privs, env):
# write databasebefore next step
conn.commit()
# Create & subscribe the user's INBOX, Trash, Spam, and Drafts folders.
# * Our sieve rule for spam expects that the Spam folder exists.
# * 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, so create it
# to avoid unnecessary polling.
# Check if the mailboxes exist before creating them. When creating a user that had previously
# been deleted, the mailboxes will still exist because they are still on disk.
try:
existing_mboxes = utils.shell('check_output', ["doveadm", "mailbox", "list", "-u", email, "-8"], capture_stderr=True).split("\n")
except subprocess.CalledProcessError as e:
c.execute("DELETE FROM users WHERE email=?", (email,))
conn.commit()
return ("Failed to initialize the user: " + e.output.decode("utf8"), 400)
for folder in ("INBOX", "Trash", "Spam", "Drafts"):
if folder not in existing_mboxes:
utils.shell('check_call', ["doveadm", "mailbox", "create", "-u", email, "-s", folder])
# Update things in case any new domains are added.
return kick(env, "mail user added")
@ -406,67 +400,90 @@ def add_remove_mail_user_privilege(email, priv, action, env):
return "OK"
def add_mail_alias(source, destination, env, update_if_exists=False, do_kick=True):
def add_mail_alias(address, forwards_to, permitted_senders, env, update_if_exists=False, do_kick=True):
# convert Unicode domain to IDNA
source = sanitize_idn_email_address(source)
address = sanitize_idn_email_address(address)
# Our database is case sensitive (oops), which affects mail delivery
# (Postfix always queries in lowercase?), so force lowercase.
source = source.lower()
address = address.lower()
# validate source
source = source.strip()
if source == "":
return ("No incoming email address provided.", 400)
if not validate_email(source, mode='alias'):
return ("Invalid incoming email address (%s)." % source, 400)
# validate address
address = address.strip()
if address == "":
return ("No email address provided.", 400)
if not validate_email(address, mode='alias'):
return ("Invalid email address (%s)." % address, 400)
# validate forwards_to
validated_forwards_to = []
forwards_to = forwards_to.strip()
# extra checks for email addresses used in domain control validation
is_dcv_source = is_dcv_address(source)
# validate destination
dests = []
destination = destination.strip()
is_dcv_source = is_dcv_address(address)
# Postfix allows a single @domain.tld as the destination, which means
# the local part on the address is preserved in the rewrite. We must
# try to convert Unicode to IDNA first before validating that it's a
# legitimate alias address. Don't allow this sort of rewriting for
# DCV source addresses.
d1 = sanitize_idn_email_address(destination)
if validate_email(d1, mode='alias') and not is_dcv_source:
dests.append(d1)
r1 = sanitize_idn_email_address(forwards_to)
if validate_email(r1, mode='alias') and not is_dcv_source:
validated_forwards_to.append(r1)
else:
# Parse comma and \n-separated destination emails & validate. In this
# case, the recipients must be complete email addresses.
for line in destination.split("\n"):
# case, the forwards_to must be complete email addresses.
for line in forwards_to.split("\n"):
for email in line.split(","):
email = email.strip()
if email == "": continue
email = sanitize_idn_email_address(email) # Unicode => IDNA
# Strip any +tag from email alias and check privileges
privileged_email = re.sub(r"(?=\+)[^@]*(?=@)",'',email)
if not validate_email(email):
return ("Invalid destination email address (%s)." % email, 400)
if is_dcv_source and not is_dcv_address(email) and "admin" not in get_mail_user_privileges(email, env, empty_on_error=True):
return ("Invalid receiver email address (%s)." % email, 400)
if is_dcv_source and not is_dcv_address(email) and "admin" not in get_mail_user_privileges(privileged_email, env, empty_on_error=True):
# Make domain control validation hijacking a little harder to mess up by
# requiring aliases for email addresses typically used in DCV to forward
# only to accounts that are administrators on this system.
return ("This alias can only have administrators of this system as destinations because the address is frequently used for domain control validation.", 400)
dests.append(email)
if len(destination) == 0:
return ("No destination email address(es) provided.", 400)
destination = ",".join(dests)
validated_forwards_to.append(email)
# validate permitted_senders
valid_logins = get_mail_users(env)
validated_permitted_senders = []
permitted_senders = permitted_senders.strip()
# Parse comma and \n-separated sender logins & validate. The permitted_senders must be
# valid usernames.
for line in permitted_senders.split("\n"):
for login in line.split(","):
login = login.strip()
if login == "": continue
if login not in valid_logins:
return ("Invalid permitted sender: %s is not a user on this system." % login, 400)
validated_permitted_senders.append(login)
# Make sure the alias has either a forwards_to or a permitted_sender.
if len(validated_forwards_to) + len(validated_permitted_senders) == 0:
return ("The alias must either forward to an address or have a permitted sender.", 400)
# save to db
forwards_to = ",".join(validated_forwards_to)
permitted_senders = None if len(validated_permitted_senders) == 0 else ",".join(validated_permitted_senders)
conn, c = open_database(env, with_connection=True)
try:
c.execute("INSERT INTO aliases (source, destination) VALUES (?, ?)", (source, destination))
c.execute("INSERT INTO aliases (source, destination, permitted_senders) VALUES (?, ?, ?)", (address, forwards_to, permitted_senders))
return_status = "alias added"
except sqlite3.IntegrityError:
if not update_if_exists:
return ("Alias already exists (%s)." % source, 400)
return ("Alias already exists (%s)." % address, 400)
else:
c.execute("UPDATE aliases SET destination = ? WHERE source = ?", (destination, source))
c.execute("UPDATE aliases SET destination = ?, permitted_senders = ? WHERE source = ?", (forwards_to, permitted_senders, address))
return_status = "alias updated"
conn.commit()
@ -474,21 +491,30 @@ def add_mail_alias(source, destination, env, update_if_exists=False, do_kick=Tru
if do_kick:
# Update things in case any new domains are added.
return kick(env, return_status)
return None
def remove_mail_alias(source, env, do_kick=True):
def remove_mail_alias(address, env, do_kick=True):
# convert Unicode domain to IDNA
source = sanitize_idn_email_address(source)
address = sanitize_idn_email_address(address)
# remove
conn, c = open_database(env, with_connection=True)
c.execute("DELETE FROM aliases WHERE source=?", (source,))
c.execute("DELETE FROM aliases WHERE source=?", (address,))
if c.rowcount != 1:
return ("That's not an alias (%s)." % source, 400)
return ("That's not an alias (%s)." % address, 400)
conn.commit()
if do_kick:
# Update things in case any domains are removed.
return kick(env, "alias removed")
return None
def add_auto_aliases(aliases, env):
conn, c = open_database(env, with_connection=True)
c.execute("DELETE FROM auto_aliases")
for source, destination in aliases.items():
c.execute("INSERT INTO auto_aliases (source, destination) VALUES (?, ?)", (source, destination))
conn.commit()
def get_system_administrator(env):
return "administrator@" + env['PRIMARY_HOSTNAME']
@ -507,17 +533,21 @@ def get_required_aliases(env):
# email on that domain are the required aliases or a catch-all/domain-forwarder.
real_mail_domains = get_mail_domains(env,
filter_aliases = lambda alias :
not alias[0].startswith("postmaster@") and not alias[0].startswith("admin@")
and not alias[0].startswith("@")
not alias.startswith("postmaster@")
and not alias.startswith("admin@")
and not alias.startswith("abuse@")
and not alias.startswith("@")
)
# Create postmaster@ and admin@ for all domains we serve mail on.
# postmaster@ is assumed to exist by our Postfix configuration. admin@
# isn't anything, but it might save the user some trouble e.g. when
# Create postmaster@, admin@ and abuse@ for all domains we serve
# mail on. postmaster@ is assumed to exist by our Postfix configuration.
# admin@isn't anything, but it might save the user some trouble e.g. when
# buying an SSL certificate.
# abuse@ is part of RFC2142: https://www.ietf.org/rfc/rfc2142.txt
for domain in real_mail_domains:
aliases.add("postmaster@" + domain)
aliases.add("admin@" + domain)
aliases.add("abuse@" + domain)
return aliases
@ -529,40 +559,36 @@ def kick(env, mail_result=None):
if mail_result is not None:
results.append(mail_result + "\n")
# Ensure every required alias exists.
auto_aliases = { }
existing_users = get_mail_users(env)
existing_aliases = get_mail_aliases(env)
# Mape required aliases to the administrator alias (which should be created manually).
administrator = get_system_administrator(env)
required_aliases = get_required_aliases(env)
def ensure_admin_alias_exists(source):
# If a user account exists with that address, we're good.
if source in existing_users:
return
# Does this alias exists?
for s, t in existing_aliases:
if s == source:
return
# Doesn't exist.
administrator = get_system_administrator(env)
if source == administrator: return # don't make an alias from the administrator to itself --- this alias must be created manually
add_mail_alias(source, administrator, env, do_kick=False)
results.append("added alias %s (=> %s)\n" % (source, administrator))
for alias in required_aliases:
ensure_admin_alias_exists(alias)
if alias == administrator: continue # don't make an alias from the administrator to itself --- this alias must be created manually
auto_aliases[alias] = administrator
# Remove auto-generated postmaster/admin on domains we no
# longer have any other email addresses for.
for source, target in existing_aliases:
user, domain = source.split("@")
if user in ("postmaster", "admin") \
and source not in required_aliases \
and target == get_system_administrator(env):
remove_mail_alias(source, env, do_kick=False)
results.append("removed alias %s (was to %s; domain no longer used for email)\n" % (source, target))
# Add domain maps from Unicode forms of IDNA domains to the ASCII forms stored in the alias table.
for domain in get_mail_domains(env):
try:
domain_unicode = idna.decode(domain.encode("ascii"))
if domain == domain_unicode: continue # not an IDNA/Unicode domain
auto_aliases["@" + domain_unicode] = "@" + domain
except (ValueError, UnicodeError, idna.IDNAError):
continue
add_auto_aliases(auto_aliases, env)
# Remove auto-generated postmaster/admin/abuse alises from the main aliases table.
# They are now stored in the auto_aliases table.
for address, forwards_to, _permitted_senders, auto in get_mail_aliases(env):
user, domain = address.split("@")
if user in {"postmaster", "admin", "abuse"} \
and address not in required_aliases \
and forwards_to == get_system_administrator(env) \
and not auto:
remove_mail_alias(address, env, do_kick=False)
results.append(f"removed alias {address} (was to {forwards_to}; domain no longer used for email)\n")
# Update DNS and nginx in case any domains are added/removed.
@ -577,12 +603,11 @@ def kick(env, mail_result=None):
def validate_password(pw):
# validate password
if pw.strip() == "":
raise ValueError("No password provided.")
if re.search(r"[\s]", pw):
raise ValueError("Passwords cannot contain spaces.")
if len(pw) < 4:
raise ValueError("Passwords must be at least four characters.")
msg = "No password provided."
raise ValueError(msg)
if len(pw) < 8:
msg = "Passwords must be at least eight characters."
raise ValueError(msg)
if __name__ == "__main__":
import sys

145
management/mfa.py Normal file
View File

@ -0,0 +1,145 @@
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):
msg = "Invalid token."
raise ValueError(msg)
else:
msg = "Invalid MFA type."
raise ValueError(msg)
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 not isinstance(secret, str) or secret.strip() == "":
msg = "No secret provided."
raise ValueError(msg)
if len(secret) != 32:
msg = "Secret should be a 32 characters base32 string"
raise ValueError(msg)
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

685
management/ssl_certificates.py Executable file
View File

@ -0,0 +1,685 @@
#!/usr/local/lib/mailinabox/env/bin/python
# Utilities for installing and selecting SSL certificates.
import os, os.path, re, shutil, subprocess, tempfile
from utils import shell, safe_domain_name, sort_domains
import functools
import operator
# 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'] and 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 and 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(r"^[^\.]+", "*", 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(f"{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:
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 = functools.reduce(operator.iadd, 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
"-d", ",".join(domain_list), # first will be main domain
"--csr", csr_file.name, # use our private key; unfortunately this doesn't work with auto-renew so we need to save cert manually
"--cert-path", os.path.join(d, 'cert'), # we only use the full chain
"--chain-path", os.path.join(d, 'chain'), # we only use the full 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", f"/C={country_code}/CN={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 = "{}-{}-{}.pem".format(
safe_domain_name(cn), # 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
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(r"^[^\.]+", "*", 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:
with open(ssl_private_key, 'rb') as f:
priv_key = load_pem(f.read())
except ValueError as e:
return (f"The private key file {ssl_private_key} is not a private key file: {e!s}", 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 (f"The certificate has expired or is not yet valid. It is valid from {cert.not_valid_before} to {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:
msg = "File does not contain valid PEM data."
raise ValueError(msg)
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:
msg = "File is not a valid PEM-formatted file."
raise ValueError(msg)
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,39 +1,68 @@
<style>
#alias_table .actions > * { padding-right: 3px; }
#alias_table .alias-required .remove { display: none }
#alias_table .alias-auto .actions > * { display: none }
</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="#users">mail user</a> or to any email address.</p>
<form class="form-horizontal" role="form" onsubmit="do_add_alias(); return false;">
<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="form-group">
<div class="col-sm-offset-1 col-sm-11">
<div id="alias_type_buttons" class="btn-group btn-group-xs">
<button type="button" class="btn btn-default active" data-mode="regular">Regular</button>
<button type="button" class="btn btn-default" data-mode="regular">Regular</button>
<button type="button" class="btn btn-default" data-mode="catchall">Catch-All</button>
<button type="button" class="btn btn-default" 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. Enter just a part of an email address starting with the @-sign.</span>
<span class="domainalias hidden">A domain alias forwards all otherwise unmatched mail from one domain to another domain, preserving the part before the @-sign.</span>
<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>
<div class="form-group">
<label for="addaliasEmail" class="col-sm-1 control-label">Alias</label>
<label for="addaliasAddress" class="col-sm-1 control-label">Alias</label>
<div class="col-sm-10">
<input type="email" class="form-control" id="addaliasEmail">
<div style="margin-top: 3px; padding-left: 3px; font-size: 90%" class="text-muted">You may use international (non-ASCII) characters for the domain part of the email address only.</div>
<input type="email" class="form-control" id="addaliasAddress">
<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>
<div class="form-group">
<label for="addaliasTargets" class="col-sm-1 control-label">Forward To</label>
<label for="addaliasForwardsTo" class="col-sm-1 control-label">Forwards To</label>
<div class="col-sm-10">
<textarea class="form-control" rows="3" id="addaliasTargets"></textarea>
<textarea class="form-control" rows="3" id="addaliasForwardsTo"></textarea>
<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>
</div>
<div class="form-group">
<label for="addaliasSenders" class="col-sm-1 control-label">Permitted Senders</label>
<div class="col-sm-10">
<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 id="addaliasForwardsToDiv" style="margin-top: .5em; margin-left: 1.4em; display: none;">
<textarea class="form-control" rows="3" id="addaliasSenders" placeholder="one user per line or separated by commas"></textarea>
</div>
</div>
</div>
<div class="form-group">
@ -51,13 +80,14 @@
<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>
@ -70,12 +100,48 @@
<span class="glyphicon glyphicon-trash"></span>
</a>
</td>
<td class='email'> </td>
<td class='target'> </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">
<thead><th>Verb</th> <th>Action</th><th></th></thead>
<tr><td>GET</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>POST</td><td>/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>POST</td><td>/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() {
@ -87,8 +153,8 @@ function show_aliases() {
function(r) {
$('#alias_table tbody').html("");
for (var i = 0; i < r.length; i++) {
var hdr = $("<tr><td colspan='3'><h4/></td></tr>");
hdr.find('h4').text(r[i].domain);
var hdr = $("<tr><th colspan='4' style='background-color: #EEE'></th></tr>");
hdr.find('th').text(r[i].domain);
$('#alias_table tbody').append(hdr);
for (var k = 0; k < r[i].aliases.length; k++) {
@ -97,11 +163,13 @@ function show_aliases() {
var n = $("#alias-template").clone();
n.attr('id', '');
if (alias.required) n.addClass('alias-required');
n.attr('data-email', alias.source_display); // this is decoded from IDNA, but will get re-coded to IDNA on the backend
n.find('td.email').text(alias.source_display)
for (var j = 0; j < alias.destination.length; j++)
n.find('td.target').append($("<div></div>").text(alias.destination[j]))
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);
}
}
@ -111,25 +179,25 @@ function show_aliases() {
$('#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") {
$('#addaliasEmail').attr('type', 'email');
$('#addaliasEmail').attr('placeholder', 'incoming email address (e.g. you@yourdomain.com)');
$('#addaliasTargets').attr('placeholder', 'forward to these email addresses (one per line or separated by commas)');
$('#addaliasAddress').attr('type', 'email');
$('#addaliasAddress').attr('placeholder', 'you@yourdomain.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") {
$('#addaliasEmail').attr('type', 'text');
$('#addaliasEmail').attr('placeholder', 'incoming catch-all address (e.g. @yourdomain.com)');
$('#addaliasTargets').attr('placeholder', 'forward to these email addresses (one per line or separated by commas)');
$('#addaliasAddress').attr('type', 'text');
$('#addaliasAddress').attr('placeholder', '@yourdomain.com (incoming catch-all domain)');
$('#addaliasForwardsTo').attr('placeholder', 'one address per line or separated by commas');
$('#alias_mode_info').slideDown();
$('#alias_mode_info span').addClass('hidden');
$('#alias_mode_info span.catchall').removeClass('hidden');
$('#addalias-form .catchall').removeClass('hidden');
} else if ($(this).attr('data-mode') == "domainalias") {
$('#addaliasEmail').attr('type', 'text');
$('#addaliasEmail').attr('placeholder', 'incoming domain (@yourdomain.com)');
$('#addaliasTargets').attr('placeholder', 'forward to domain (@yourdomain.com)');
$('#addaliasAddress').attr('type', 'text');
$('#addaliasAddress').attr('placeholder', '@yourdomain.com (incoming catch-all domain)');
$('#addaliasForwardsTo').attr('placeholder', '@otherdomain.com (forward to other domain)');
$('#alias_mode_info').slideDown();
$('#alias_mode_info span').addClass('hidden');
$('#alias_mode_info span.domainalias').removeClass('hidden');
$('#addalias-form .domainalias').removeClass('hidden');
}
})
$('#alias_type_buttons button[data-mode="regular"]').click(); // init
@ -139,15 +207,21 @@ function show_aliases() {
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();
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',
source: email,
destination: targets
address: form_address,
forwards_to: form_forwardsto,
permitted_senders: form_senders
},
function(r) {
// Responses are multiple lines of pre-formatted text.
@ -162,52 +236,59 @@ function do_add_alias() {
}
function aliases_reset_form() {
$("#addaliasEmail").prop('disabled', false);
$("#addaliasEmail").val('')
$("#addaliasTargets").val('')
$("#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 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";
is_alias_add_update = true;
$('#alias-cancel').removeClass('hidden');
$("#addaliasEmail").prop('disabled', true);
$("#addaliasEmail").val(email);
$("#addaliasTargets").val(targets);
$('#add-alias-button').text('Update');
if (email.charAt(0) == '@' && targets.charAt(0) == '@')
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 (email.charAt(0) == '@')
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 email = $(elem).parents('tr').attr('data-email');
var row_address = $(elem).parents('tr').attr('data-address');
show_modal_confirm(
"Remove Alias",
"Remove " + email + "?",
"Remove " + row_address + "?",
"Remove",
function() {
api(
"/mail/aliases/remove",
"POST",
{
source: email
address: row_address
},
function(r) {
// Responses are multiple lines of pre-formatted text.
show_modal_error("Remove User", $("<pre/>").text(r));
show_modal_error("Remove Alias", $("<pre/>").text(r));
show_aliases();
});
});

View File

@ -10,7 +10,7 @@
<p>It is possible to set custom DNS records on domains hosted here.</p>
<h3>Set Custom DNS Records</h3>
<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>
@ -31,11 +31,15 @@
<label for="customdnsType" class="col-sm-1 control-label">Type</label>
<div class="col-sm-10">
<select id="customdnsType" class="form-control" style="max-width: 400px" 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).">A (IPv4 address)</option>
<option value="AAAA" data-hint="Enter an IPv6 address.">AAAA (IPv6 address)</option>
<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 PRIORIY DOMAIN., including trailing period (e.g. 20 mx.example.com.).">MX (mail exchanger)</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>
@ -53,7 +57,13 @@
</div>
</form>
<table id="custom-dns-current" class="table" style="width: auto; display: none">
<div style="text-align: right; font-size; 90%; margin-top: 1em;">
sort by:
<a href="#" onclick="window.miab_custom_dns_data_sort_order='qname'; show_current_custom_dns_update_after_sort(); return false;">domain name</a>
|
<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" style="width: auto; display: none; margin-top: 0;">
<thead>
<th>Domain Name</th>
<th>Record Type</th>
@ -65,9 +75,10 @@
</tbody>
</table>
<h3>Using a Secondary Nameserver</h3>
<h3>Using a secondary nameserver</h3>
<p>If your TLD requires you to have two separate nameservers, you can either set up a secondary (aka &ldquo;slave&rdquo;) nameserver or, alternatively, set up <a href="#" onclick="return show_panel('external_dns')">external DNS</a> and ignore the DNS server on this box. If you choose to use a seconday/slave nameserver, you must find a seconday/slave nameserver service provider. Your domain name registrar or virtual cloud provider may provide this service for you. Once you set up the seconday/slave nameserver service, enter the hostname of <em>their</em> secondary nameserver:</p>
<p>If your TLD requires you to have two separate nameservers, you can either set up <a href="#external_dns">external DNS</a> and ignore the DNS server on this box entirely, or use the DNS server on this box but add a secondary (aka &ldquo;slave&rdquo;) nameserver.</p>
<p>If you choose to use a secondary nameserver, you must find a secondary nameserver service provider. Your domain name registrar or virtual cloud provider may provide this service for you. Once you set up the secondary nameserver service, enter the hostname (not the IP address) of <em>their</em> secondary nameserver in the box below.</p>
<form class="form-horizontal" role="form" onsubmit="do_set_secondary_dns(); return false;">
<div class="form-group">
@ -81,9 +92,15 @@
<button type="submit" class="btn btn-primary">Update</button>
</div>
</div>
<div id="secondarydns-clear-instructions" class="form-group" style="display: none">
<div class="form-group">
<div class="col-sm-offset-1 col-sm-11">
<p 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>
<p class="small">
Multiple secondary servers can be separated with commas or spaces (i.e., <code>ns2.hostingcompany.com ns3.hostingcompany.com</code>).
To enable zone transfers to additional servers without listing them as secondary nameservers, prefix a hostname, IP address, or subnet with <code>xfr:</code>, e.g. <code>xfr:10.20.30.40</code> or <code>xfr:10.0.0.0/8</code>.
</p>
<p 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>
</form>
@ -116,7 +133,7 @@
<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. 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>rtype</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>, or <code>SRV</code>.</td></tr>
<tr><td>rtype</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>value</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>
@ -152,8 +169,8 @@ function show_custom_dns() {
"GET",
{ },
function(data) {
$('#secondarydnsHostname').val(data.hostname ? data.hostname : '');
$('#secondarydns-clear-instructions').toggle(data.hostname != null);
$('#secondarydnsHostname').val(data.hostnames.join(' '));
$('#secondarydns-clear-instructions').toggle(data.hostnames.length > 0);
});
api(
@ -181,20 +198,38 @@ function show_current_custom_dns() {
$('#custom-dns-current').fadeIn();
else
$('#custom-dns-current').fadeOut();
window.miab_custom_dns_data = data;
show_current_custom_dns_update_after_sort();
});
}
$('#custom-dns-current').find("tbody").text('');
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=4 style='background-color: #EEE'></th></tr>");
r.find("th").text(data[i].zone);
tbody.append(r);
last_zone = data[i].zone;
}
var tr = $("<tr/>");
$('#custom-dns-current').find("tbody").append(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"/>').text(data[i].value));
tr.append($('<td class="long" style="max-width: 40em"/>').text(data[i].value));
tr.append($('<td>[<a href="#" onclick="return delete_custom_dns_record(this)">delete</a>]</td>'));
}
});
}
function delete_custom_dns_record(elem) {
@ -210,7 +245,7 @@ function do_set_secondary_dns() {
"/dns/secondary-nameserver",
"POST",
{
hostname: $('#secondarydnsHostname').val()
hostnames: $('#secondarydnsHostname').val()
},
function(data) {
if (data == "") return; // nothing updated

View File

@ -34,6 +34,28 @@
<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="glyphicon glyphicon-info-sign"></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="form-group">
<div class="form-group">
<label for="downloadZonefile" class="control-label sr-only">Zone</label>
<select id="downloadZonefile" class="form-control" style="width: auto"> </select>
</div>
<button type="submit" class="btn btn-primary">Download</button>
</div>
</form>
<h3>Records</h3>
<table id="external_dns_settings" class="table">
<thead>
<tr>
@ -48,6 +70,18 @@
<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",
@ -75,4 +109,19 @@ function show_external_dns() {
}
})
}
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

@ -9,18 +9,11 @@
<meta name="robots" content="noindex, nofollow">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.0/css/bootstrap.min.css">
<link rel="stylesheet" href="/admin/assets/bootstrap/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);
html {
overflow-y: scroll;
}
body {
padding-top: 50px;
padding-bottom: 20px;
overflow-y: scroll;
padding-bottom: 20px;
}
p {
@ -28,7 +21,7 @@
}
h1, h2, h3, h4 {
font-family: Raleway, sans-serif;
font-family: sans-serif;
font-weight: bold;
}
@ -43,20 +36,20 @@
margin-bottom: 13px;
margin-top: 30px;
}
.panel-heading h3 {
border: none;
padding: 0;
margin: 0;
}
.panel-heading h3 {
border: none;
padding: 0;
margin: 0;
}
h4 {
font-size: 110%;
margin-bottom: 13px;
margin-top: 18px;
}
h4:first-child {
margin-top: 6px;
}
h4:first-child {
margin-top: 6px;
}
.admin_panel {
display: none;
@ -66,19 +59,44 @@
margin: 1.5em 0;
}
ol li {
margin-bottom: 1em;
}
ol li {
margin-bottom: 1em;
}
.if-logged-in { display: none; }
.if-logged-in-admin { display: none; }
/* The below only gets used if it is supported */
@media (prefers-color-scheme: dark) {
/* Invert invert lightness but not hue */
html {
filter: invert(100%) hue-rotate(180deg);
}
/* Override Boostrap theme here to give more contrast. The black turns to white by the filter. */
.form-control {
color: black !important;
}
/* Revert the invert for the navbar */
button, div.navbar {
filter: invert(100%) hue-rotate(180deg);
}
/* Revert the revert for the dropdowns */
ul.dropdown-menu {
filter: invert(100%) hue-rotate(180deg);
}
}
</style>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.0/css/bootstrap-theme.min.css">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css">
<link rel="stylesheet" href="/admin/assets/bootstrap/css/bootstrap-theme.min.css">
</head>
<body>
<!--[if lt IE 8]><p>Internet Explorer version 8 or any modern web browser is required to use this website, sorry.<![endif]-->
<!--[if gt IE 7]><!-->
<div class="navbar navbar-inverse navbar-fixed-top" role="navigation">
<div class="navbar navbar-inverse navbar-static-top" role="navigation">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target=".navbar-collapse">
@ -91,39 +109,46 @@
</div>
<div class="navbar-collapse collapse">
<ul class="nav navbar-nav">
<li class="dropdown">
<li class="dropdown if-logged-in-admin">
<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="#ssl" onclick="return show_panel(this);">SSL Certificates</a></li>
<li><a href="#system_backup" onclick="return show_panel(this);">Backup Status</a></li>
<li><a href="#system_status">Status Checks</a></li>
<li><a href="#tls">TLS (SSL) Certificates</a></li>
<li><a href="#system_backup">Backup Status</a></li>
<li class="divider"></li>
<li class="dropdown-header">Advanced Pages</li>
<li><a href="#custom_dns" onclick="return show_panel(this);">Custom DNS</a></li>
<li><a href="#external_dns" onclick="return show_panel(this);">External DNS</a></li>
<li><a href="/admin/munin">Munin Monitoring</a></li>
<li><a href="#custom_dns">Custom DNS</a></li>
<li><a href="#external_dns">External DNS</a></li>
<li><a href="#munin">Munin Monitoring</a></li>
</ul>
</li>
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">Mail <b class="caret"></b></a>
<li><a href="#mail-guide" class="if-logged-in-not-admin">Mail</a></li>
<li class="dropdown if-logged-in-admin">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">Mail &amp; Users <b class="caret"></b></a>
<ul class="dropdown-menu">
<li><a href="#mail-guide" onclick="return show_panel(this);">Instructions</a></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>
<li><a href="#mail-guide">Instructions</a></li>
<li><a href="#users">Users</a></li>
<li><a href="#aliases">Aliases</a></li>
<li class="divider"></li>
<li class="dropdown-header">Your Account</li>
<li><a href="#mfa">Two-Factor Authentication</a></li>
</ul>
</li>
<li><a href="#sync_guide" onclick="return show_panel(this);">Contacts/Calendar</a></li>
<li><a href="#web" onclick="return show_panel(this);">Web</a></li>
<li><a href="#version" onclick="return show_panel(this);">Version</a></li>
<li><a href="#sync_guide" class="if-logged-in">Contacts/Calendar</a></li>
<li><a href="#web" class="if-logged-in-admin">Web</a></li>
</ul>
<ul class="nav navbar-nav navbar-right">
<li><a href="#" onclick="do_logout(); return false;" style="color: white">Log out?</a></li>
<li class="if-logged-in"><a href="#" onclick="do_logout(); return false;" style="color: white">Log out</a></li>
</ul>
</div><!--/.navbar-collapse -->
</div>
</div>
<div class="container">
<div id="panel_welcome" class="admin_panel">
{% include "welcome.html" %}
</div>
<div id="panel_system_status" class="admin_panel">
{% include "system-status.html" %}
</div>
@ -140,6 +165,10 @@
{% include "custom-dns.html" %}
</div>
<div id="panel_mfa" class="admin_panel">
{% include "mfa.html" %}
</div>
<div id="panel_login" class="admin_panel">
{% include "login.html" %}
</div>
@ -164,12 +193,12 @@
{% include "web.html" %}
</div>
<div id="panel_ssl" class="admin_panel">
<div id="panel_tls" class="admin_panel">
{% include "ssl.html" %}
</div>
<div id="panel_version" class="admin_panel">
{% include "version.html" %}
<div id="panel_munin" class="admin_panel">
{% include "munin.html" %}
</div>
<hr>
@ -204,8 +233,8 @@
</div>
</div>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.0/js/bootstrap.min.js"></script>
<script src="/admin/assets/jquery.min.js"></script>
<script src="/admin/assets/bootstrap/js/bootstrap.min.js"></script>
<script>
var global_modal_state = null;
@ -215,8 +244,8 @@ $(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();
})
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
@ -231,7 +260,7 @@ $(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]();
})
})
})
function show_modal_error(title, message, callback) {
@ -276,7 +305,7 @@ function show_modal_confirm(title, question, verb, yes_callback, cancel_callback
}
var ajax_num_executing_requests = 0;
function ajax(options) {
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--;
@ -294,7 +323,7 @@ function ajax(options) {
};
options.error = function(jqxhr) {
hide_loading_indicator();
if (!old_error)
if (!old_error)
show_modal_error("Error", "Something went wrong, sorry.")
else
old_error(jqxhr.responseText, jqxhr);
@ -304,8 +333,8 @@ function ajax(options) {
return false; // handy when called from onclick
}
var api_credentials = ["", ""];
function api(url, method, data, callback, callback_error) {
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+/=";
@ -338,12 +367,12 @@ function api(url, method, data, callback, callback_error) {
show_modal_error("Error", "Something went wrong, sorry.")
}
ajax({
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,
@ -352,9 +381,10 @@ function api(url, method, data, callback, callback_error) {
// 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]));
if (api_credentials)
xhr.setRequestHeader(
'Authorization',
'Basic ' + base64encode(api_credentials.username + ':' + api_credentials.session_key));
},
success: callback,
error: callback_error || default_error,
@ -371,34 +401,64 @@ function api(url, method, data, callback, callback_error) {
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)
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
}
window.onhashchange = function() {
var panelid = window.location.hash.substring(1);
show_panel(panelid);
};
$(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(":");
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 (typeof localStorage != 'undefined' && localStorage.getItem("miab-cp-lastpanel")) {
show_panel(localStorage.getItem("miab-cp-lastpanel"));
if (api_credentials != null && window.location.hash) {
var panelid = window.location.hash.substring(1);
show_panel(panelid);
} else if (api_credentials != null) {
show_panel('welcome');
} else {
show_panel('login');
}

View File

@ -1,4 +1,29 @@
<h1 style="margin: 1em; text-align: center">{{hostname}}</h1>
<style>
.title {
margin: 1em;
text-align: center;
}
.subtitle {
margin: 2em;
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">
@ -7,23 +32,23 @@
<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 tools/mail.py user add me@{{hostname}}
sudo tools/mail.py user make-admin me@{{hostname}}</pre>
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 tools/mail.py user make-admin me@{{hostname}}</pre>
sudo management/cli.py user make-admin me@{{hostname}}</pre>
{% endif %}
<hr>
</div>
</div>
</div>
{% endif %}
<p style="margin: 2em; text-align: center;">Log in here for your Mail-in-a-Box control panel.</p>
<p class="subtitle">Log in here for your Mail-in-a-Box control panel.</p>
<div style="margin: 0 auto; max-width: 32em;">
<form class="form-horizontal" role="form" onsubmit="do_login(); return false;">
<div class="login">
<form id="loginForm" class="form-horizontal" role="form" onsubmit="do_login(); return false;" method="get">
<div class="form-group">
<label for="inputEmail3" class="col-sm-3 control-label">Email</label>
<div class="col-sm-9">
@ -36,6 +61,13 @@ sudo tools/mail.py user make-admin me@{{hostname}}</pre>
<input name="password" type="password" class="form-control" id="loginPassword" placeholder="Password">
</div>
</div>
<div class="form-group" id="loginOtp">
<label for="loginOtpInput" class="col-sm-3 control-label">Code</label>
<div class="col-sm-9">
<input type="text" class="form-control" id="loginOtpInput" placeholder="6-digit code" autocomplete="off">
<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>
<div class="form-group">
<div class="col-sm-offset-3 col-sm-9">
<div class="checkbox">
@ -53,35 +85,51 @@ sudo tools/mail.py user make-admin me@{{hostname}}</pre>
</form>
</div>
<script>
function do_login() {
if ($('#loginEmail').val() == "") {
show_modal_error("Login Failed", "Enter your email address.")
show_modal_error("Login Failed", "Enter your email address.", function() {
$('#loginEmail').focus();
});
return false;
}
if ($('#loginPassword').val() == "") {
show_modal_error("Login Failed", "Enter your email password.")
show_modal_error("Login Failed", "Enter your email password.", function() {
$('#loginPassword').focus();
});
return false;
}
// Exchange the email address & password for an API key.
api_credentials = [$('#loginEmail').val(), $('#loginPassword').val()]
api_credentials = { username: $('#loginEmail').val(), session_key: $('#loginPassword').val() }
api(
"/me",
"GET",
{ },
function(response){
"/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") {
// Show why the login failed.
show_modal_error("Login Failed", response.reason)
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');
// Reset any saved credentials.
do_logout();
// Show why the login failed.
show_modal_error("Login Failed", response.reason)
// 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.")
@ -93,37 +141,77 @@ function do_login() {
// Login succeeded.
// Save the new credentials.
api_credentials = [response.email, response.api_key];
api_credentials = { username: response.email,
session_key: response.api_key,
privileges: response.privileges };
// 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", api_credentials.join(":"));
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", api_credentials.join(":"));
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 ? 'system_status' : switch_back_to_panel) }, 300);
setTimeout(function() {
if (window.location.hash) {
var panelid = window.location.hash.substring(1);
show_panel(panelid);
} else {
show_panel(
!switch_back_to_panel || switch_back_to_panel == "login"
? 'welcome'
: switch_back_to_panel)
}
}, 300);
}
})
},
undefined,
{
'x-auth-token': $('#loginOtpInput').val()
});
}
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');
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

@ -16,7 +16,7 @@
<h4>Automatic configuration</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>iOS and macOS 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>
<h4>Manual configuration</h4>
@ -29,18 +29,20 @@
<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</td></tr>
<tr><th>SMTP Port</th> <td>587</td></tr>
<tr><th>SMTP Security</td> <td>STARTTLS <small>(&ldquo;always&rdquo; or &ldquo;required&rdquo;, if prompted)</small></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> separately.</p>
<p>In addition to setting up your email, you&rsquo;ll also need to set up <a href="#sync_guide">contacts and calendar synchronization</a> separately.</p>
<p>As an alternative to IMAP you can also use the POP protocol: choose POP as the protocol, port 995, and SSL or TLS security in your mail client. The SMTP settings and usernames and passwords remain the same. However, we recommend you use IMAP instead.</p>
<h4>Exchange/ActiveSync settings</h4>
<p>On iOS devices, devices on this <a href="http://z-push.org/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>
<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>
@ -57,7 +59,7 @@
</div>
<div class="panel-body">
<h4>Greylisting</h4>
<p>Your box using a technique called greylisting to cut down on spam. Greylisting works by delaying mail from people you haven&rsquo;t received mail from before for up to about 10 minutes. The vast majority of spam gets tricked by this. If you are waiting for an email from someone new, such as if you are registering on a new website and are waiting for an email confirmation, please give it up to 10-15 minutes to arrive.</p>
<p>Your box 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>

View File

@ -0,0 +1,242 @@
<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="panel panel-danger">
<div class="panel-heading">
Enabling two-factor authentication does not protect access to your email
</div>
<div class="panel-body">
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="form-group">
<p>1. 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>
<div class="form-group">
<p style="margin-bottom: 0">2. Scan the QR code in the app or directly enter the secret into the app:</p>
<div id="totp-setup-qr"></div>
</div>
<div class="form-group">
<label for="otp-label" style="font-weight: normal">3. 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="form-group">
<label for="otp" style="font-weight: normal">4. 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>
<input type="hidden" id="totp-setup-secret" />
<div class="form-group">
<p>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.</p>
<button id="totp-setup-submit" disabled type="submit" class="btn">Enable Two-Factor Authentication</button>
</div>
</form>
<form id="disable-2fa">
<div class="form-group">
<p>Two-factor authentication is active for your account<span id="mfa-device-label"></span>.</p>
<p>You will have to log into the admin panel again after disabling two-factor authentication.</p>
</div>
<div class="form-group">
<button type="submit" class="btn btn-danger">Disable Two-Factor Authentication</button>
</div>
</form>
<div id="output-2fa" class="panel panel-danger">
<div class="panel-body"></div>
</div>
</div>
<script>
var el = {
disableForm: document.getElementById('disable-2fa'),
output: document.getElementById('output-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) {
var img = document.createElement('img');
img.src = "data:image/png;base64," + provisioned_totp.qr_code_base64;
var code = document.createElement('div');
code.innerHTML = `Secret: ${provisioned_totp.secret}`;
el.totpQr.appendChild(img);
el.totpQr.appendChild(code);
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").text(" on device '" + mfa.label + "'");
}
function hide_error() {
el.output.querySelector('.panel-body').innerHTML = '';
el.output.classList.remove('visible');
}
function render_error(msg) {
el.output.querySelector('.panel-body').innerHTML = msg;
el.output.classList.add('visible');
}
function reset_view() {
el.wrapper.classList.remove('loaded', 'disabled', 'enabled');
el.disableForm.removeEventListener('submit', do_disable);
hide_error();
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', '');
el.totpQr.innerHTML = '';
}
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();
hide_error();
api(
'/mfa/disable',
'POST',
{ type: 'totp' },
function() {
do_logout();
}
);
return false;
}
function do_enable_totp(evt) {
evt.preventDefault();
hide_error();
api(
'/mfa/totp/enable',
'POST',
{
token: $(el.totpSetupToken).val(),
secret: $(el.totpSetupSecret).val(),
label: $(el.totpSetupLabel).val()
},
function(res) { do_logout(); },
function(res) { render_error(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

@ -1,12 +1,31 @@
<style>
</style>
<h2>SSL Certificates</h2>
<h2>TLS (SSL) Certificates</h2>
<h3>Certificate Status</h3>
<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>
<table id="ssl_domains" class="table" style="margin-bottom: 2em; width: auto;">
<div id="ssl_provision">
<h3>Provision certificates</h3>
<div id="ssl_provision_p" style="display: none; margin-top: 1.5em">
<button onclick='return provision_tls_cert();' class='btn btn-primary' style="float: left; margin: 0 1.5em 1em 0;">Provision</button>
<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:<br>
<span class="text-primary"></span></p>
</div>
<div class="clearfix"> </div>
<div id="ssl_provision_result"></div>
</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" style="margin-bottom: 2em; width: auto; display: none">
<thead>
<tr>
<th>Domain</th>
@ -18,29 +37,39 @@
</tbody>
</table>
<p>Advanced:<br>Install a multi-domain or wildcard certificate for the <code>{{hostname}}</code> domain to have it automatically applied to any domains it is valid for.</p>
<h3 id="ssl_install_header">Install SSL Certificate</h3>
<h3 id="ssl_install_header">Install certificate</h3>
<p>There are many places where you can get a free or cheap SSL certificate. We recommend <a href="https://www.namecheap.com/security/ssl-certificates/domain-validation.aspx">Namecheap&rsquo;s $9 certificate</a> or <a href="https://www.startssl.com/">StartSSL&rsquo;s free express lane</a>.</p>
<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>
<p>Which domain are you getting an SSL certificate for?</p>
<p>Which domain are you getting a certificate for?</p>
<p><select id="ssldomain" onchange="show_csr()" class="form-control" style="width: auto"></select></p>
<p>(A multi-domain or wildcard certificate will be automatically applied to any domains it is valid for besides the one you choose above.)</p>
<p>What country are you in? This is required by some TLS certificate providers. You may leave this blank if you know your TLS certificate provider doesn't require it.</p>
<p><select id="sslcc" onchange="show_csr()" class="form-control" style="width: auto">
<option value="">(Select)</option>
{% for code, name in csr_country_codes %}
<option value="{{code}}">{{name}}</option>
{% endfor %}
</select></p>
<div id="csr_info" style="display: none">
<p>You will need to provide the SSL certificate provider this Certificate Signing Request (CSR):</p>
<p>You will need to provide the certificate provider this Certificate Signing Request (CSR):</p>
<pre id="ssl_csr"></pre>
<p><small>The CSR is safe to share. It can only be used in combination with a secret key stored on this machine.</small></p>
<p>The SSL certificate provider will then provide you with an SSL certificate. They may also provide you with an intermediate chain. Paste each separately into the boxes below:</p>
<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>
<p style="margin-bottom: .5em">SSL certificate:</p>
<p style="margin-bottom: .5em">TLS/SSL certificate:</p>
<p><textarea id="ssl_paste_cert" class="form-control" style="max-width: 40em; height: 8em" placeholder="-----BEGIN CERTIFICATE-----&#xA;stuff here&#xA;-----END CERTIFICATE-----"></textarea></p>
<p style="margin-bottom: .5em">SSL intermediate chain (if provided):</p>
<p style="margin-bottom: .5em">TLS/SSL intermediate chain (if provided):</p>
<p><textarea id="ssl_paste_chain" class="form-control" style="max-width: 40em; height: 8em" placeholder="-----BEGIN CERTIFICATE-----&#xA;stuff here&#xA;-----END CERTIFICATE-----&#xA;-----BEGIN CERTIFICATE-----&#xA;more stuff here&#xA;-----END CERTIFICATE-----"></textarea></p>
<p>After you paste in the information, click the install button.</p>
@ -49,26 +78,41 @@
</div>
<script>
function show_ssl() {
function show_tls(keep_provisioning_shown) {
api(
"/web/domains",
"/ssl/status",
"GET",
{
},
function(domains) {
function(res) {
// provisioning status
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)
$('#ssl_provision_p span').text(res.can_provision.join(", "));
// 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='domain'><a href=''></a></th><td class='status'></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('.domain a').text(domains[i].domain);
row.find('.domain a').attr('href', 'https://' + domains[i].domain);
row.addClass("text-" + domains[i].ssl_certificate[0]);
row.find('.status').text(domains[i].ssl_certificate[1]);
if (domains[i].ssl_certificate[0] == "success") {
if (domains[i].status == "not-applicable") {
domains[i].status = "muted"; // text-muted css class
row.find('.actions a').remove(); // no actions applicable
}
row.addClass("text-" + domains[i].status);
row.find('.status').text(domains[i].text);
if (domains[i].status == "success") {
row.find('.actions a').addClass('btn-default').text('Replace Certificate');
} else {
row.find('.actions a').addClass('btn-primary').text('Install Certificate');
@ -82,18 +126,24 @@ function show_ssl() {
function ssl_install(elem) {
var domain = $(elem).parents('tr').attr('data-domain');
$('#ssldomain').val(domain);
$('#csr_info').slideDown();
$('#ssl_csr').text('Loading...');
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);
@ -112,10 +162,75 @@ function install_cert() {
function(status) {
if (/^OK($|\n)/.test(status)) {
console.log(status)
show_modal_error("SSL Certificate Installation", "Certificate has been installed. Check that you have no connection problems to the domain.", function() { show_ssl(); $('#csr_info').slideUp(); });
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("SSL Certificate Installation", status);
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').text("");
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).
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;
}
// create an HTML block to display the results of this request
var n = $("<div><h4/><p/></div>");
$('#ssl_provision_result').append(n);
// plain log line
if (typeof r === "string") {
n.find("p").text(r);
continue;
}
// show a header only to disambiguate request blocks
if (status.requests.length > 0)
n.find("h4").text(r.domains.join(", "));
if (r.result == "error") {
n.find("p").addClass("text-danger").text(r.message);
} else if (r.result == "installed") {
n.find("p").addClass("text-success").text("The TLS certificate was provisioned and installed.");
setTimeout("show_tls(true)", 1); // update main table of certificate statuses, call with arg keep_provisioning_shown true so that we don't clear what we just outputted
}
// display the detailed log info in case of problems
var trace = $("<div class='small text-muted' style='margin-top: 1.5em'>Log:</div>");
n.append(trace);
for (var j = 0; j < r.log.length; j++)
trace.append($("<div/>").text(r.log[j]));
}
if (may_reenable_provision_button)
$('#ssl_provision_p .btn').removeAttr("disabled");
});
}
</script>

View File

@ -17,22 +17,22 @@
<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
<p>Log in settings are the same as with <a href="#mail-guide">mail</a>: your
complete email address and your mail password.</p>
</div>
<div class="col-sm-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,
<p>If you set up your <a href="#mail-guide">mail</a> using Exchange/ActiveSync,
your contacts and calendar may already appear on your device.</p>
<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 and Calendar</td> <td><a href="https://play.google.com/store/apps/details?id=at.bitfire.davdroid">DAVdroid</a> ($3.69; free <a href="https://f-droid.org/repository/browse/?fdfilter=dav&fdid=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 beta</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.89)</td></tr>
<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>

View File

@ -5,17 +5,169 @@
<h2>Backup Status</h2>
<h3>Copying Backup Files</h3>
<p>The box makes an incremental backup each night. You can store the backup on any Amazon Web Services S3-compatible service, or other options.</p>
<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="form-group">
<label for="backup-target-type" class="col-sm-2 control-label">Backup to:</label>
<div class="col-sm-2">
<select class="form-control" 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>
<!-- LOCAL BACKUP -->
<div class="form-group backup-target-local">
<div class="col-sm-10 col-sm-offset-2">
<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-sm-10 col-sm-offset-2">
<p>You can also use SFTP (FTP over SSH) to copy files from <tt id="backup-location"></tt>. These files are encrypted, 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>
<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>
<h3>Current Backups</h3>
</div>
</div>
<div class="form-group backup-target-rsync">
<label for="backup-target-rsync-host" class="col-sm-2 control-label">Hostname</label>
<div class="col-sm-8">
<input type="text" placeholder="hostname.local" class="form-control" rows="1" id="backup-target-rsync-host">
<div class="small" style="margin-top: 2px">
The hostname at your rsync provider, e.g. <tt>da2327.rsync.net</tt>. Optionally includes a colon
and the provider's non-standard ssh port number, e.g. <tt>u215843.your-storagebox.de:23</tt>.
</div>
</div>
</div>
<div class="form-group backup-target-rsync">
<label for="backup-target-rsync-path" class="col-sm-2 control-label">Path</label>
<div class="col-sm-8">
<input type="text" placeholder="/backups/{{hostname}}" class="form-control" rows="1" id="backup-target-rsync-path">
</div>
</div>
<div class="form-group backup-target-rsync">
<label for="backup-target-rsync-user" class="col-sm-2 control-label">Username</label>
<div class="col-sm-8">
<input type="text" class="form-control" rows="1" id="backup-target-rsync-user">
</div>
</div>
<div class="form-group backup-target-rsync">
<label for="ssh-pub-key" class="col-sm-2 control-label">Public SSH Key</label>
<div class="col-sm-8">
<input type="text" class="form-control" rows="1" id="ssh-pub-key" readonly>
<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>
<div id="copy_pub_key_div" class="col-sm">
<button type="button" class="btn btn-small" onclick="copy_pub_key_to_clipboard()">Copy</button>
</div>
</div>
<!-- S3 BACKUP -->
<div class="form-group backup-target-s3">
<div class="col-sm-10 col-sm-offset-2">
<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>
<div class="form-group backup-target-s3">
<label for="backup-target-s3-host-select" class="col-sm-2 control-label">S3 Region</label>
<div class="col-sm-8">
<select class="form-control" rows="1" 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="form-group backup-target-s3">
<label for="backup-target-s3-host" class="col-sm-2 control-label">S3 Host / Endpoint</label>
<div class="col-sm-8">
<input type="text" placeholder="https://s3.backuphost.com" class="form-control" rows="1" id="backup-target-s3-host">
</div>
</div>
<div class="form-group backup-target-s3">
<label for="backup-target-s3-region-name" class="col-sm-2 control-label">S3 Region Name <span style="font-weight: normal">(if required)</span></label>
<div class="col-sm-8">
<input type="text" placeholder="region.name" class="form-control" rows="1" id="backup-target-s3-region-name">
</div>
</div>
<div class="form-group backup-target-s3">
<label for="backup-target-s3-path" class="col-sm-2 control-label">S3 Bucket &amp; Path</label>
<div class="col-sm-8">
<input type="text" placeholder="bucket-name/backup-directory" class="form-control" rows="1" id="backup-target-s3-path">
</div>
</div>
<div class="form-group backup-target-s3">
<label for="backup-target-user" class="col-sm-2 control-label">S3 Access Key</label>
<div class="col-sm-8">
<input type="text" class="form-control" rows="1" id="backup-target-user">
</div>
</div>
<div class="form-group backup-target-s3">
<label for="backup-target-pass" class="col-sm-2 control-label">S3 Secret Access Key</label>
<div class="col-sm-8">
<input type="text" class="form-control" rows="1" id="backup-target-pass">
</div>
</div>
<!-- Backblaze -->
<div class="form-group backup-target-b2">
<div class="col-sm-10 col-sm-offset-2">
<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>
<div class="form-group backup-target-b2">
<label for="backup-target-b2-user" class="col-sm-2 control-label">B2 Application KeyID</label>
<div class="col-sm-8">
<input type="text" class="form-control" rows="1" id="backup-target-b2-user">
</div>
</div>
<div class="form-group backup-target-b2">
<label for="backup-target-b2-pass" class="col-sm-2 control-label">B2 Application Key</label>
<div class="col-sm-8">
<input type="text" class="form-control" rows="1" id="backup-target-b2-pass">
</div>
</div>
<div class="form-group backup-target-b2">
<label for="backup-target-b2-bucket" class="col-sm-2 control-label">B2 Bucket</label>
<div class="col-sm-8">
<input type="text" class="form-control" rows="1" id="backup-target-b2-bucket">
</div>
</div>
<!-- Common -->
<div class="form-group backup-target-local backup-target-rsync backup-target-s3 backup-target-b2">
<label for="min-age" class="col-sm-2 control-label">Retention Days:</label>
<div class="col-sm-8">
<input type="number" class="form-control" rows="1" id="min-age">
<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="form-group">
<div class="col-sm-offset-2 col-sm-10">
<button id="set-s3-backup-button" type="submit" class="btn btn-primary">Save</button>
</div>
</div>
</form>
<p>The backup directory currently contains the backups listed below. The total size on disk of the backups is currently <span id="backup-total-size"></span>.</p>
<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" style="width: auto">
<thead>
@ -27,8 +179,16 @@
<tbody>
</tbody>
</table>
<script>
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();
init_inputs(target_type);
}
function nice_size(bytes) {
var powers = ['bytes', 'KB', 'MB', 'GB', 'TB'];
while (true) {
@ -46,19 +206,27 @@ function nice_size(bytes) {
}
function show_system_backup() {
show_custom_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.directory);
$('#backup-encpassword-file').text(r.encpwfile);
if (r.error) {
show_modal_error("Backup Error", $("<pre/>").text(r.error));
return;
}
$('#backup-status tbody').html("");
var total_disk_size = 0;
if (r.backups.length == 0) {
if (typeof r.backups == "undefined") {
var tr = $('<tr><td colspan="3">Backups are turned off.</td></tr>');
$('#backup-status tbody').append(tr);
return;
} else if (r.backups.length == 0) {
var tr = $('<tr><td colspan="3">No backups have been made yet.</td></tr>');
$('#backup-status tbody').append(tr);
}
@ -67,7 +235,7 @@ function show_system_backup() {
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_str) );
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.size)) );
@ -80,7 +248,153 @@ function show_system_backup() {
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);
$("#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://") {
const spec = url_split(r.target);
$("#backup-target-type").val(spec.scheme);
$("#backup-target-rsync-user").val(spec.user);
$("#backup-target-rsync-host").val(spec.host);
$("#backup-target-rsync-path").val(spec.path);
} else if (r.target.substring(0, 5) == "s3://") {
const spec = url_split(r.target);
$("#backup-target-type").val("s3");
$("#backup-target-s3-host-select").val(spec.host);
$("#backup-target-s3-host").val(spec.host);
$("#backup-target-s3-region-name").val(spec.user); // stuffing the region name in the username
$("#backup-target-s3-path").val(spec.path);
} 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(decodeURIComponent(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();
var target;
if (target_type == "local" || target_type == "off")
target = target_type;
else if (target_type == "s3")
target = "s3://"
+ ($("#backup-target-s3-region-name").val() ? ($("#backup-target-s3-region-name").val() + "@") : "")
+ $("#backup-target-s3-host").val()
+ "/" + $("#backup-target-s3-path").val();
else if (target_type == "rsync") {
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() + ':' + encodeURIComponent($('#backup-target-b2-pass').val())
+ '@' + $('#backup-target-b2-bucket').val()
target_user = '';
target_pass = '';
}
var min_age = $("#min-age").val();
api(
"/system/backup/config",
"POST",
{
target: target,
target_user: target_user,
target_pass: target_pass,
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) {
if(host !== 'other') {
$("#backup-target-s3-host").val(host);
} else {
$("#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());
});
set_host($('#backup-target-s3-host-select').val());
}
}
// Return a two-element array of the substring preceding and the substring following
// the first occurence of separator in string. Return [undefined, string] if the
// separator does not appear in string.
const split1_rest = (string, separator) => {
const index = string.indexOf(separator);
return (index >= 0) ? [string.substring(0, index), string.substring(index + separator.length)] : [undefined, string];
};
// Note: The manifest JS URL class does not work in some security-conscious
// settings, e.g. Brave browser, so we roll our own that handles only what we need.
//
// Use greedy separator parsing to get parts of a MIAB backup target url.
// Note: path will not include a leading forward slash '/'
const url_split = url => {
const [ scheme, scheme_rest ] = split1_rest(url, '://');
const [ user, user_rest ] = split1_rest(scheme_rest, '@');
const [ host, path ] = split1_rest(user_rest, '/');
return {
scheme,
user,
host,
path,
}
};
// Hide Copy button if not in a modern clipboard-supporting environment.
// Using document API because jQuery is not necessarily available in this script scope.
if (!(navigator && navigator.clipboard && navigator.clipboard.writeText)) {
document.getElementById('copy_pub_key_div').hidden = true;
}
function copy_pub_key_to_clipboard() {
const ssh_pub_key = $("#ssh-pub-key").val();
navigator.clipboard.writeText(ssh_pub_key);
}
</script>

View File

@ -10,13 +10,13 @@
border-top: none;
padding-top: 0;
}
#system-checks .status-error td {
#system-checks .status-error td, .summary-error {
color: #733;
}
#system-checks .status-warning td {
#system-checks .status-warning td, .summary-warning {
color: #770;
}
#system-checks .status-ok td {
#system-checks .status-ok td, .summary-ok {
color: #040;
}
#system-checks div.extra {
@ -36,6 +36,25 @@
}
</style>
<div class="row">
<div class="col-md-push-9 col-md-3">
<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> <!-- /col -->
<div class="col-md-pull-3 col-md-8">
<div id="system-checks-summary">
</div>
<table id="system-checks" class="table" style="max-width: 60em">
<thead>
</thead>
@ -43,15 +62,49 @@
</tbody>
</table>
</div> <!-- /col -->
</div> <!-- /row -->
<script>
function show_system_status() {
const summary = $('#system-checks-summary');
summary.html("");
$('#system-checks tbody').html("<tr><td colspan='2' class='text-muted'>Loading...</td></tr>")
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);
});
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);
});
api(
"/system/status",
"POST",
{ },
function(r) {
$('#system-checks tbody').html("");
const ok_symbol = "✓";
const error_symbol = "✖";
const warning_symbol = "?";
let count_by_status = { ok: 0, error: 0, warning: 0 };
for (var i = 0; i < r.length; i++) {
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')
@ -59,9 +112,12 @@ function show_system_status() {
n.addClass(r[i].type)
else
n.addClass("status-" + r[i].type)
if (r[i].type == "ok") n.find('td.status').text("✓")
if (r[i].type == "error") n.find('td.status').text("✖")
if (r[i].type == "warning") n.find('td.status').text("?")
if (r[i].type == "ok") n.find('td.status').text(ok_symbol);
if (r[i].type == "error") n.find('td.status').text(error_symbol);
if (r[i].type == "warning") n.find('td.status').text(warning_symbol);
count_by_status[r[i].type]++;
n.find('td.message p').text(r[i].text)
$('#system-checks tbody').append(n);
@ -81,6 +137,48 @@ function show_system_status() {
n.find('> td.message > div').append(m);
}
}
// Summary counts
summary.html("Summary: ");
if (count_by_status['error'] + count_by_status['warning'] == 0) {
summary.append($('<span class="summary-ok"/>').text(`All ${count_by_status['ok']} ${ok_symbol} OK`));
} else {
summary.append($('<span class="summary-ok"/>').text(`${count_by_status['ok']} ${ok_symbol} OK, `));
summary.append($('<span class="summary-error"/>').text(`${count_by_status['error']} ${error_symbol} Error, `));
summary.append($('<span class="summary-warning"/>').text(`${count_by_status['warning']} ${warning_symbol} Warning`));
}
})
}
var current_privacy_setting = null;
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,7 +1,6 @@
<h2>Users</h2>
<style>
#user_table h4 { margin: 1em 0 0 0; }
#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; }
@ -31,10 +30,10 @@
<button type="submit" class="btn btn-primary">Add User</button>
</form>
<ul style="margin-top: 1em; padding-left: 1.5em; font-size: 90%;">
<li>Passwords must be at least four characters and may not contain spaces.</li>
<li>Use <a href="javascript:show_panel('aliases')">aliases</a> to create email addresses that forward to existing accounts.</li>
<li>Passwords must be at least eight characters consisting of English letters and numbers only. For best results, <a href="#" onclick="return generate_random_password()">generate a random password</a>.</li>
<li>Use <a href="#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="javascript:show_panel('aliases')">aliases</a> can.</li>
<li>User accounts cannot contain any international (non-ASCII) characters, but <a href="#aliases">aliases</a> can.</li>
</ul>
<h3>Existing mail users</h3>
@ -43,7 +42,6 @@
<tr>
<th width="50%">Email Address</th>
<th>Actions</th>
<th>Mailbox Size</th>
</tr>
</thead>
<tbody>
@ -73,8 +71,6 @@
archive account
</a>
</td>
<td class='mailboxsize'>
</td>
</tr>
<tr id="user-extra-template" class="if_inactive">
<td colspan="3" style="border: 0; padding-top: 0">
@ -84,6 +80,48 @@
</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">
<thead><th>Verb</th> <th>Action</th><th></th></thead>
<tr><td>GET</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>POST</td><td>/add</td> <td>Adds a new mail user. Required POST-body parameters are <code>email</code> and <code>password</code>.</td></tr>
<tr><td>POST</td><td>/remove</td> <td>Removes a mail user. Required POST-body parameter is <code>email</code>.</td></tr>
<tr><td>POST</td><td>/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>POST</td><td>/privileges/remove</td> <td>Used to remove the admin privilege from a mail user. Required POST-body parameter is <code>email</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() {
@ -95,8 +133,8 @@ function show_users() {
function(r) {
$('#user_table tbody').html("");
for (var i = 0; i < r.length; i++) {
var hdr = $("<tr><td colspan='3'><h4/></td></tr>");
hdr.find('h4').text(r[i].domain);
var hdr = $("<tr><th colspan='2' style='background-color: #EEE'></th></tr>");
hdr.find('th').text(r[i].domain);
$('#user_table tbody').append(hdr);
for (var k = 0; k < r[i].users.length; k++) {
@ -114,7 +152,6 @@ function show_users() {
n.attr('data-email', user.email);
n.find('.address').text(user.email)
n.find('.mailboxsize').text(nice_size(user.mailbox_size))
n2.find('.restore_info tt').text(user.mailbox);
if (user.status == 'inactive') continue;
@ -166,12 +203,12 @@ function users_set_password(elem) {
var email = $(elem).parents('tr').attr('data-email');
var yourpw = "";
if (api_credentials != null && email == api_credentials[0])
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(
"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>" + yourpw + "</p>"),
"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 and may not contain spaces.</small>" + yourpw + "</p>"),
"Set Password",
function() {
api(
@ -195,7 +232,7 @@ function users_remove(elem) {
var email = $(elem).parents('tr').attr('data-email');
// can't remove yourself
if (api_credentials != null && email == api_credentials[0]) {
if (api_credentials != null && email == api_credentials.username) {
show_modal_error("Archive User", "You cannot archive your own account.");
return;
}
@ -227,7 +264,7 @@ function mod_priv(elem, add_remove) {
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[0]) {
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;
}
@ -250,4 +287,13 @@ function mod_priv(elem, add_remove) {
});
});
}
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></pr");
return false; // cancel click
}
</script>

View File

@ -1,36 +0,0 @@
<style>
</style>
<h2>Mail-in-a-Box Version</h2>
<p>You are running Mail-in-a-Box version <span id="miab-version" style="font-weight: bold">...</span>.</p>
<p>The latest version of Mail-in-a-Box is <button id="miab-get-latest-upstream" onclick="check_latest_version()">Check</button>.</p>
<p>To find the latest version and for upgrade instructions, see <a href="https://mailinabox.email/">https://mailinabox.email/</a>, <a href="https://github.com/mail-in-a-box/mailinabox/blob/master/CHANGELOG.md">release notes</a>, and <a href="https://mailinabox.email/maintenance.html#updating-mail-in-a-box">upgrade instructions</a>.</p>
<script>
function show_version() {
$('#miab-version').text('loading...');
api(
"/system/version",
"GET",
{
},
function(version) {
$('#miab-version').text(version);
});
}
function check_latest_version() {
$('#miab-get-latest-upstream').text('loading...');
api(
"/system/latest-upstream-version",
"POST",
{
},
function(version) {
$('#miab-get-latest-upstream').text(version);
});
}
</script>

View File

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

View File

@ -0,0 +1,16 @@
<style>
.title {
margin: 1em;
text-align: center;
}
.subtitle {
margin: 2em;
text-align: center;
}
</style>
<h1 class="title">{{hostname}}</h1>
<p class="subtitle">Welcome to your Mail-in-a-Box control panel.</p>

View File

@ -1,6 +1,10 @@
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.
@ -10,13 +14,36 @@ 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))
with open(fn, encoding="utf-8") as f:
for line in f:
env.setdefault(*line.strip().split("=", 1))
return env
def save_environment(env):
with open("/etc/mailinabox.conf", "w") as f:
with open("/etc/mailinabox.conf", "w", encoding="utf-8") as f:
for k, v in env.items():
f.write("%s=%s\n" % (k, v))
f.write(f"{k}={v}\n")
# 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", encoding="utf-8") as f:
f.write(rtyaml.dump(config))
def load_settings(env):
import rtyaml
fn = os.path.join(env['STORAGE_ROOT'], 'settings.yaml')
try:
with open(fn, encoding="utf-8") as f:
config = rtyaml.load(f)
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.
@ -24,119 +51,70 @@ def safe_domain_name(name):
return urllib.parse.quote(name, safe='')
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)
# Put domain names in a nice sorted order.
# 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=len):
for z in zones.values():
if domain.endswith("." + z):
# We found a parent domain already in the list.
zones[domain] = z
break
else:
groups[2].append(d)
# 'break' did not occur: there is no parent domain, so it is its
# own zone.
zones[domain] = domain
# 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]
# 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.
return 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 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)
domains = {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))
domain_emails = {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)
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):
def shell(method, cmd_args, env=None, 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
if env is None:
env = {}
env.update({ "PATH": "/sbin:/bin:/usr/sbin:/usr/bin" })
kwargs = {
'env': env,
@ -172,7 +150,7 @@ def du(path):
# soft and hard links.
total_size = 0
seen = set()
for dirpath, dirnames, filenames in os.walk(path):
for dirpath, _dirnames, filenames in os.walk(path):
for f in filenames:
fp = os.path.join(dirpath, f)
try:
@ -200,3 +178,38 @@ def wait_for_service(port, public, env, timeout):
if time.perf_counter() > start+timeout:
return False
time.sleep(min(timeout/4, 1))
def get_ssh_port():
port_value = get_ssh_config_value("port")
if port_value:
return int(port_value)
return None
def get_ssh_config_value(parameter_name):
# Returns ssh configuration value for the provided parameter
try:
output = shell('check_output', ['sshd', '-T'])
except FileNotFoundError:
# sshd is not installed. That's ok.
return None
except subprocess.CalledProcessError:
# error while calling shell command
return None
for line in output.split("\n"):
if " " not in line: continue # there's a blank line at the end
key, values = line.split(" ", 1)
if key == parameter_name:
return values # space-delimited if there are multiple values
# Did not find the parameter!
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,37 +2,57 @@
# domains for which a mail account has been set up.
########################################################################
import os, os.path, shutil, re, tempfile, rtyaml
import os.path, re, rtyaml
from mailconfig import get_mail_domains
from dns_update import get_custom_dns_config, do_dns_update, get_dns_zones
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
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.
# 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)
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 |= {'www.' + zone for zone, zonefile in get_dns_zones(env)}
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 |= {'autoconfig.' + maildomain for maildomain in get_mail_domains(env, users_only=True)}
domains |= {'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 |= {'mta-sts.' + 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'])
# Also 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. These will require an SSL cert.
# ...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_mail_domains(env) - get_domains_with_a_records(env))
# Sort the list so the nginx conf gets written in a stable order.
domains = sort_domains(domains, env)
return sort_domains(domains, env)
return domains
def get_domains_with_a_records(env):
domains = set()
dns = get_custom_dns_config(env)
for domain, rtype, value in dns:
if rtype == "CNAME" or (rtype in ("A", "AAAA") and value != "local"):
if rtype == "CNAME" or (rtype in {"A", "AAAA"} and value not in {"local", env['PUBLIC_IP']}):
domains.add(domain)
return domains
@ -42,7 +62,8 @@ def get_web_domains_with_root_overrides(env):
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))
with open(nginx_conf_custom_fn, encoding='utf-8') as f:
custom_settings = rtyaml.load(f)
for domain, settings in custom_settings.items():
for type, value in [('redirect', settings.get('redirects', {}).get('/')),
('proxy', settings.get('proxies', {}).get('/'))]:
@ -50,50 +71,53 @@ def get_web_domains_with_root_overrides(env):
root_overrides[domain] = (type, value)
return root_overrides
def get_default_www_redirects(env):
# Returns a list of www subdomains that we want to provide default redirects
# for, i.e. any www's that aren't domains the user has actually configured
# to serve for real. Which would be unusual.
web_domains = set(get_web_domains(env))
www_domains = set('www.' + zone for zone, zonefile in get_dns_zones(env))
return sort_domains(www_domains - web_domains - get_domains_with_a_records(env), env)
def do_web_update(env):
# Pre-load what SSL certificates we will use for each domain.
ssl_certificates = get_ssl_certificates(env)
# Helper for reading config files and templates
def read_conf(conf_fn):
with open(os.path.join(os.path.dirname(__file__), "../conf", conf_fn), encoding='utf-8') as f:
return f.read()
# Build an nginx configuration file.
nginx_conf = open(os.path.join(os.path.dirname(__file__), "../conf/nginx-top.conf")).read()
nginx_conf = read_conf("nginx-top.conf")
# Load the templates.
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()
template0 = read_conf("nginx.conf")
template1 = read_conf("nginx-alldomains.conf")
template2 = read_conf("nginx-primaryonly.conf")
template3 = "\trewrite ^(.*) https://$REDIRECT_DOMAIN$1 permanent;\n"
# Add the PRIMARY_HOST configuration first so it becomes nginx's default server.
nginx_conf += make_domain_config(env['PRIMARY_HOSTNAME'], [template0, template1, template2], env)
nginx_conf += make_domain_config(env['PRIMARY_HOSTNAME'], [template0, template1, template2], ssl_certificates, env)
# Add configuration all other web domains.
has_root_proxy_or_redirect = get_web_domains_with_root_overrides(env)
web_domains_not_redirect = get_web_domains(env, include_www_redirects=False)
for domain in get_web_domains(env):
if domain == env['PRIMARY_HOSTNAME']: continue # handled above
if domain not in has_root_proxy_or_redirect:
nginx_conf += make_domain_config(domain, [template0, template1], env)
if domain == env['PRIMARY_HOSTNAME']:
# PRIMARY_HOSTNAME is handled above.
continue
if domain in web_domains_not_redirect:
# This is a regular domain.
if domain not in has_root_proxy_or_redirect:
nginx_conf += make_domain_config(domain, [template0, template1], ssl_certificates, env)
else:
nginx_conf += make_domain_config(domain, [template0], ssl_certificates, env)
else:
nginx_conf += make_domain_config(domain, [template0], env)
# Add default www redirects.
for domain in get_default_www_redirects(env):
nginx_conf += make_domain_config(domain, [template0, template3], env)
# 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"
if os.path.exists(nginx_conf_fn):
with open(nginx_conf_fn) as f:
with open(nginx_conf_fn, encoding='utf-8') as f:
if f.read() == nginx_conf:
return ""
# Save the file.
with open(nginx_conf_fn, "w") as f:
with open(nginx_conf_fn, "w", encoding='utf-8') as f:
f.write(nginx_conf)
# Kick nginx. Since this might be called from the web admin
@ -104,18 +128,14 @@ def do_web_update(env):
return "web updated\n"
def make_domain_config(domain, templates, env):
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, ssl_via = get_domain_ssl_files(domain, 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, env)
tls_cert = get_domain_ssl_files(domain, ssl_certificates, env)
# ADDITIONAL DIRECTIVES.
@ -126,24 +146,65 @@ def make_domain_config(domain, templates, env):
def hashfile(filepath):
import hashlib
sha1 = hashlib.sha1()
f = open(filepath, 'rb')
try:
with open(filepath, 'rb') as f:
sha1.update(f.read())
finally:
f.close()
return sha1.hexdigest()
nginx_conf_extra += "# ssl files sha1: %s / %s\n" % (hashfile(ssl_key), hashfile(ssl_certificate))
nginx_conf_extra += "\t# ssl files sha1: {} / {}\n".format(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))
with open(nginx_conf_custom_fn, encoding='utf-8') as f:
yaml = rtyaml.load(f)
if domain in yaml:
yaml = yaml[domain]
# any proxy or redirect here?
for path, url in yaml.get("proxies", {}).items():
nginx_conf_extra += "\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)
nginx_conf_extra += f"\trewrite {path} {url} permanent;\n"
# 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")
@ -154,18 +215,17 @@ def make_domain_config(domain, templates, env):
# 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]:
for t in [*templates, nginx_conf_extra]:
nginx_conf = re.sub("[ \t]*# ADDITIONAL DIRECTIVES HERE *\n", t, nginx_conf)
# Replace substitution strings in the template & return.
nginx_conf = nginx_conf.replace("$STORAGE_ROOT", env['STORAGE_ROOT'])
nginx_conf = nginx_conf.replace("$HOSTNAME", domain)
nginx_conf = nginx_conf.replace("$ROOT", root)
nginx_conf = nginx_conf.replace("$SSL_KEY", ssl_key)
nginx_conf = nginx_conf.replace("$SSL_CERTIFICATE", ssl_certificate)
nginx_conf = nginx_conf.replace("$REDIRECT_DOMAIN", re.sub(r"^www\.", "", domain)) # for default www redirects to parent domain
nginx_conf = nginx_conf.replace("$SSL_KEY", tls_cert["private-key"])
nginx_conf = nginx_conf.replace("$SSL_CERTIFICATE", tls_cert["certificate"])
return 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, test_exists=True):
# Try STORAGE_ROOT/web/domain_name if it exists, but fall back to STORAGE_ROOT/web/default.
@ -174,144 +234,21 @@ def get_web_root(domain, env, test_exists=True):
if os.path.exists(root) or not test_exists: break
return root
def get_domain_ssl_files(domain, env, allow_shared_cert=True):
# 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')
ssl_via = None
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))
# 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 and allow_shared_cert:
from status_checks import check_certificate
if check_certificate(domain, ssl_certificate_primary, None, just_check_domain=True)[0] == "OK":
ssl_certificate = ssl_certificate_primary
ssl_via = "Using multi/wildcard certificate of %s." % env['PRIMARY_HOSTNAME']
# For a 'www.' domain, see if we can reuse the cert of the parent.
elif domain.startswith('www.'):
ssl_certificate_parent = os.path.join(env["STORAGE_ROOT"], 'ssl/%s/ssl_certificate.pem' % safe_domain_name(domain[4:]))
if os.path.exists(ssl_certificate_parent) and check_certificate(domain, ssl_certificate_parent, None, just_check_domain=True)[0] == "OK":
ssl_certificate = ssl_certificate_parent
ssl_via = "Using multi/wildcard certificate of %s." % domain[4:]
return ssl_key, ssl_certificate, ssl_via
def ensure_ssl_certificate_exists(domain, ssl_key, ssl_certificate, 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 written to a temporary file.
with tempfile.NamedTemporaryFile(mode="w") as csr_fp:
csr_fp.write(create_csr(domain, ssl_key, env))
csr_fp.flush() # since we won't close until after running 'openssl x509', since close triggers delete.
# And then make the certificate.
shell("check_call", [
"openssl", "x509", "-req",
"-days", "365",
"-in", csr_fp.name,
"-signkey", ssl_key,
"-out", ssl_certificate])
def create_csr(domain, ssl_key, env):
return shell("check_output", [
"openssl", "req", "-new",
"-key", ssl_key,
"-sha256",
"-subj", "/C=%s/ST=/L=/O=/CN=%s" % (env["CSR_COUNTRY"], domain)])
def install_cert(domain, ssl_cert, ssl_chain, env):
if domain not in get_web_domains(env) + get_default_www_redirects(env):
return "Invalid domain name."
# Write the combined cert+chain to a temporary path and validate that it is OK.
# The certificate always goes above the chain.
import tempfile, os
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.
from status_checks import check_certificate
ssl_key, ssl_certificate, ssl_via = get_domain_ssl_files(domain, env, allow_shared_cert=False)
cert_status, cert_status_details = check_certificate(domain, fn, ssl_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 the certificate to its expected location.
os.makedirs(os.path.dirname(ssl_certificate), exist_ok=True)
shutil.move(fn, ssl_certificate)
ret = ["OK"]
# When updating the cert for PRIMARY_HOSTNAME, also update DNS because it is
# used in the DANE TLSA record and restart postfix and dovecot which use
# that certificate.
if domain == env['PRIMARY_HOSTNAME']:
ret.append( do_dns_update(env) )
shell('check_call', ["/usr/sbin/service", "postfix", "restart"])
shell('check_call', ["/usr/sbin/service", "dovecot", "restart"])
ret.append("mail services restarted")
# Kick nginx so it sees the cert.
ret.append( do_web_update(env) )
return "\n".join(ret)
def get_web_domains_info(env):
has_root_proxy_or_redirect = get_web_domains_with_root_overrides(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)
# for the SSL config panel, get cert status
def check_cert(domain):
from status_checks import check_certificate
ssl_key, ssl_certificate, ssl_via = get_domain_ssl_files(domain, env)
if not os.path.exists(ssl_certificate):
return ("danger", "No Certificate Installed")
cert_status, cert_status_details = check_certificate(domain, ssl_certificate, ssl_key)
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":
if not ssl_via:
return ("success", "Signed & valid. " + cert_status_details)
else:
# This is an alternate domain but using the same cert as the primary domain.
return ("success", "Signed & valid. " + ssl_via)
return ("success", "Signed & valid. " + cert_status_details)
elif cert_status == "SELF-SIGNED":
return ("warning", "Self-signed. Get a signed certificate to stop warnings.")
else:
@ -323,15 +260,8 @@ def get_web_domains_info(env):
"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 has_root_proxy_or_redirect,
"static_enabled": domain not in (www_redirects | has_root_proxy_or_redirect),
}
for domain in get_web_domains(env)
] + \
[
{
"domain": domain,
"ssl_certificate": check_cert(domain),
"static_enabled": False,
}
for domain in get_default_www_redirects(env)
]

7
management/wsgi.py Normal file
View File

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

View File

@ -1,62 +0,0 @@
POSTGREY_VERSION=1.35-1+miab1
DOVECOT_VERSION=2.2.9-1ubuntu2.1+miab1
all: clean build_postgrey build_dovecot_lucene
clean:
# Clean.
rm -rf /tmp/build
mkdir -p /tmp/build
build_postgrey: clean
# Download the latest Debian postgrey package. It is ahead of Ubuntu,
# and we might as well jump ahead.
git clone git://git.debian.org/git/collab-maint/postgrey.git /tmp/build/postgrey
# Download the corresponding upstream package.
wget -O /tmp/build/postgrey_1.35.orig.tar.gz http://postgrey.schweikert.ch/pub/postgrey-1.35.tar.gz
# Add our source patch to the debian packaging listing.
cp postgrey_sources.diff /tmp/build/postgrey/debian/patches/mailinabox
# Patch the packaging to give it a new version.
patch -p1 -d /tmp/build/postgrey < postgrey.diff
# Build the source package.
(cd /tmp/build/postgrey; dpkg-buildpackage -S -us -uc -nc)
# Sign the packages.
debsign /tmp/build/postgrey_$(POSTGREY_VERSION)_source.changes
# Upload to PPA.
dput ppa:mail-in-a-box/ppa /tmp/build/postgrey_$(POSTGREY_VERSION)_source.changes
# Clear the intermediate files.
rm -rf /tmp/build/postgrey
# TESTING BINARY PACKAGE
#sudo apt-get build-dep -y postgrey
#(cd /tmp/build/postgrey; dpkg-buildpackage -us -uc -nc)
build_dovecot_lucene: clean
# Get the upstream source.
(cd /tmp/build; apt-get source dovecot)
# Patch it so that we build dovecot-lucene (and nothing else).
patch -p1 -d /tmp/build/dovecot-2.2.9 < dovecot_lucene.diff
# Build the source package.
(cd /tmp/build/dovecot-2.2.9; dpkg-buildpackage -S -us -uc -nc)
# Sign the packages.
debsign /tmp/build/dovecot_$(DOVECOT_VERSION)_source.changes
# Upload it.
dput ppa:mail-in-a-box/ppa /tmp/build/dovecot_$(DOVECOT_VERSION)_source.changes
# TESTING BINARY PACKAGE
# Install build dependencies and build dependencies we've added in our patch,
# and then build the binary package.
#sudo apt-get build-dep -y dovecot
#sudo apt-get install libclucene-dev liblzma-dev libexttextcat-dev libstemmer-dev
#(cd /tmp/build/dovecot-2.2.9; dpkg-buildpackage -us -uc -nc)

View File

@ -1,40 +0,0 @@
ppa instructions
================
Mail-in-a-Box maintains a Launchpad.net PPA ([Mail-in-a-Box PPA](https://launchpad.net/~mail-in-a-box/+archive/ubuntu/ppa)) for additional deb's that we want to have installed on systems.
Packages
--------
* postgrey, a fork of [postgrey](http://postgrey.schweikert.ch/) based on the [latest Debian package](http://git.debian.org/?p=collab-maint/postgrey.git), with a modification to whitelist senders that are whitelisted by [dnswl.org](https://www.dnswl.org/) (i.e. don't greylist mail from known good senders).
* dovecot-lucene, [dovecot's lucene full text search plugin](http://wiki2.dovecot.org/Plugins/FTS/Lucene), which isn't built by Ubuntu's dovecot package maintainer unfortunately.
Building
--------
To rebuild the packages in the PPA, you'll need to be @JoshData.
First:
* You should have an account on Launchpad.net.
* Your account should have your GPG key set (to the fingerprint of a GPG key on your system matching the identity at the top of the debian/changelog files).
* You should have write permission to the PPA.
To build:
# Start a clean VM.
vagrant up
# Put your signing keys (on the host machine) into the VM (so it can sign the debs).
gpg --export-secret-keys | vagrant ssh -- gpg --import
# Build & upload to launchpad.
vagrant ssh -- "cd /vagrant && make"
Mail-in-a-Box adds our PPA during setup, but if you need to do that yourself for testing:
apt-add-repository ppa:mail-in-a-box/ppa
apt-get update
apt-get install postgrey dovecot-lucene

12
ppa/Vagrantfile vendored
View File

@ -1,12 +0,0 @@
# -*- mode: ruby -*-
# vi: set ft=ruby :
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.provision :shell, :inline => <<-SH
sudo apt-get update
sudo apt-get install -y git dpkg-dev devscripts dput
SH
end

View File

@ -1,319 +0,0 @@
--- a/debian/control
+++ b/debian/control
@@ -1,210 +1,23 @@
Source: dovecot
Section: mail
Priority: optional
-Maintainer: Ubuntu Developers <ubuntu-devel-discuss@lists.ubuntu.com>
-XSBC-Original-Maintainer: Dovecot Maintainers <jaldhar-dovecot@debian.org>
-Uploaders: Jaldhar H. Vyas <jaldhar@debian.org>, Fabio Tranchitella <kobold@debian.org>, Joel Johnson <mrjoel@lixil.net>, Marco Nenciarini <mnencia@debian.org>
-Build-Depends: debhelper (>= 7.2.3~), dpkg-dev (>= 1.16.1), pkg-config, libssl-dev, libpam0g-dev, libldap2-dev, libpq-dev, libmysqlclient-dev, libsqlite3-dev, libsasl2-dev, zlib1g-dev, libkrb5-dev, drac-dev (>= 1.12-5), libbz2-dev, libdb-dev, libcurl4-gnutls-dev, libexpat-dev, libwrap0-dev, dh-systemd, po-debconf, lsb-release, hardening-wrapper, dh-autoreconf, autotools-dev
+Maintainer: Joshua Tauberer <jt@occams.info>
+XSBC-Original-Maintainer: Ubuntu Developers <ubuntu-devel-discuss@lists.ubuntu.com>
+Build-Depends: debhelper (>= 7.2.3~), dpkg-dev (>= 1.16.1), pkg-config, libssl-dev, libpam0g-dev, libldap2-dev, libpq-dev, libmysqlclient-dev, libsqlite3-dev, libsasl2-dev, zlib1g-dev, libkrb5-dev, drac-dev (>= 1.12-5), libbz2-dev, libdb-dev, libcurl4-gnutls-dev, libexpat-dev, libwrap0-dev, dh-systemd, po-debconf, lsb-release, libclucene-dev (>= 2.3), liblzma-dev, libexttextcat-dev, libstemmer-dev, hardening-wrapper, dh-autoreconf, autotools-dev
Standards-Version: 3.9.4
Homepage: http://dovecot.org/
-Vcs-Git: git://git.debian.org/git/collab-maint/dovecot.git
-Vcs-Browser: http://git.debian.org/?p=collab-maint/dovecot.git
+Vcs-Git: https://github.com/mail-in-a-box/mailinabox
+Vcs-Browser: https://github.com/mail-in-a-box/mailinabox
-Package: dovecot-core
+Package: dovecot-lucene
Architecture: any
-Depends: ${shlibs:Depends}, ${misc:Depends}, libpam-runtime (>= 0.76-13.1), openssl, adduser, ucf (>= 2.0020), ssl-cert (>= 1.0-11ubuntu1), lsb-base (>= 3.2-12ubuntu3)
-Suggests: ntp, dovecot-gssapi, dovecot-sieve, dovecot-pgsql, dovecot-mysql, dovecot-sqlite, dovecot-ldap, dovecot-imapd, dovecot-pop3d, dovecot-lmtpd, dovecot-managesieved, dovecot-solr, ufw
-Recommends: ntpdate
-Provides: dovecot-common
-Replaces: dovecot-common (<< 1:2.0.14-2~), mailavenger (<< 0.8.1-4)
-Breaks: dovecot-common (<< 1:2.0.14-2~), mailavenger (<< 0.8.1-4)
-Description: secure POP3/IMAP server - core files
+Depends: ${shlibs:Depends}, ${misc:Depends}, dovecot-core (>= 1:2.2.9-1ubuntu2.1)
+Description: secure POP3/IMAP server - Lucene support
Dovecot is a mail server whose major goals are security and extreme
reliability. It tries very hard to handle all error conditions and verify
that all data is valid, making it nearly impossible to crash. It supports
mbox/Maildir and its own dbox/mdbox formats, and should also be pretty
fast, extensible, and portable.
.
- This package contains the Dovecot main server and its command line utility.
-
-Package: dovecot-dev
-Architecture: any
-Depends: ${shlibs:Depends}, ${misc:Depends}, dovecot-core (= ${binary:Version})
-Replaces: dovecot-common (<< 1:2.0.14-2~)
-Breaks: dovecot-common (<< 1:2.0.14-2~)
-Description: secure POP3/IMAP server - header files
- Dovecot is a mail server whose major goals are security and extreme
- reliability. It tries very hard to handle all error conditions and verify
- that all data is valid, making it nearly impossible to crash. It supports
- mbox/Maildir and its own dbox/mdbox formats, and should also be pretty
- fast, extensible, and portable.
- .
- This package contains header files needed to compile plugins for the Dovecot
- mail server.
-
-Package: dovecot-imapd
-Architecture: any
-Depends: ${shlibs:Depends}, ${misc:Depends}, dovecot-core (= ${binary:Version}), ucf (>= 2.0020)
-Provides: imap-server
-Description: secure POP3/IMAP server - IMAP daemon
- Dovecot is a mail server whose major goals are security and extreme
- reliability. It tries very hard to handle all error conditions and verify
- that all data is valid, making it nearly impossible to crash. It supports
- mbox/Maildir and its own dbox/mdbox formats, and should also be pretty
- fast, extensible, and portable.
- .
- This package contains the Dovecot IMAP server.
-
-Package: dovecot-pop3d
-Architecture: any
-Depends: ${shlibs:Depends}, ${misc:Depends}, dovecot-core (= ${binary:Version}), ucf (>= 2.0020)
-Provides: pop3-server
-Description: secure POP3/IMAP server - POP3 daemon
- Dovecot is a mail server whose major goals are security and extreme
- reliability. It tries very hard to handle all error conditions and verify
- that all data is valid, making it nearly impossible to crash. It supports
- mbox/Maildir and its own dbox/mdbox formats, and should also be pretty
- fast, extensible, and portable.
- .
- This package contains the Dovecot POP3 server.
-
-Package: dovecot-lmtpd
-Architecture: any
-Depends: ${shlibs:Depends}, ${misc:Depends}, dovecot-core (= ${binary:Version}), ucf (>= 2.0020)
-Replaces: dovecot-common (<< 1:2.0.14-2~)
-Breaks: dovecot-common (<< 1:2.0.14-2~)
-Description: secure POP3/IMAP server - LMTP server
- Dovecot is a mail server whose major goals are security and extreme
- reliability. It tries very hard to handle all error conditions and verify
- that all data is valid, making it nearly impossible to crash. It supports
- mbox/Maildir and its own dbox/mdbox formats, and should also be pretty
- fast, extensible, and portable.
- .
- This package contains the Dovecot LMTP server.
-
-Package: dovecot-managesieved
-Architecture: any
-Depends: ${shlibs:Depends}, ${misc:Depends}, dovecot-core (= ${binary:Version}), dovecot-sieve (= ${binary:Version}), ucf (>= 2.0020)
-Replaces: dovecot-common (<< 1:2.0.14-2~)
-Breaks: dovecot-common (<< 1:2.0.14-2~)
-Description: secure POP3/IMAP server - ManageSieve server
- Dovecot is a mail server whose major goals are security and extreme
- reliability. It tries very hard to handle all error conditions and verify
- that all data is valid, making it nearly impossible to crash. It supports
- mbox/Maildir and its own dbox/mdbox formats, and should also be pretty
- fast, extensible, and portable.
- .
- This package contains the Dovecot ManageSieve server.
-
-Package: dovecot-pgsql
-Architecture: any
-Depends: ${shlibs:Depends}, ${misc:Depends}, dovecot-core (= ${binary:Version})
-Description: secure POP3/IMAP server - PostgreSQL support
- Dovecot is a mail server whose major goals are security and extreme
- reliability. It tries very hard to handle all error conditions and verify
- that all data is valid, making it nearly impossible to crash. It supports
- mbox/Maildir and its own dbox/mdbox formats, and should also be pretty
- fast, extensible, and portable.
- .
- This package provides PostgreSQL support for Dovecot.
-
-Package: dovecot-mysql
-Architecture: any
-Depends: ${shlibs:Depends}, ${misc:Depends}, dovecot-core (= ${binary:Version})
-Description: secure POP3/IMAP server - MySQL support
- Dovecot is a mail server whose major goals are security and extreme
- reliability. It tries very hard to handle all error conditions and verify
- that all data is valid, making it nearly impossible to crash. It supports
- mbox/Maildir and its own dbox/mdbox formats, and should also be pretty
- fast, extensible, and portable.
- .
- This package provides MySQL support for Dovecot.
-
-Package: dovecot-sqlite
-Architecture: any
-Depends: ${shlibs:Depends}, ${misc:Depends}, dovecot-core (= ${binary:Version})
-Description: secure POP3/IMAP server - SQLite support
- Dovecot is a mail server whose major goals are security and extreme
- reliability. It tries very hard to handle all error conditions and verify
- that all data is valid, making it nearly impossible to crash. It supports
- mbox/Maildir and its own dbox/mdbox formats, and should also be pretty
- fast, extensible, and portable.
- .
- This package provides SQLite support for Dovecot.
-
-Package: dovecot-ldap
-Architecture: any
-Depends: ${shlibs:Depends}, ${misc:Depends}, dovecot-core (= ${binary:Version}), ucf (>= 2.0020)
-Description: secure POP3/IMAP server - LDAP support
- Dovecot is a mail server whose major goals are security and extreme
- reliability. It tries very hard to handle all error conditions and verify
- that all data is valid, making it nearly impossible to crash. It supports
- mbox/Maildir and its own dbox/mdbox formats, and should also be pretty
- fast, extensible, and portable.
- .
- This package provides LDAP support for Dovecot.
-
-Package: dovecot-gssapi
-Architecture: any
-Depends: ${shlibs:Depends}, ${misc:Depends}, dovecot-core (= ${binary:Version})
-Description: secure POP3/IMAP server - GSSAPI support
- Dovecot is a mail server whose major goals are security and extreme
- reliability. It tries very hard to handle all error conditions and verify
- that all data is valid, making it nearly impossible to crash. It supports
- mbox/Maildir and its own dbox/mdbox formats, and should also be pretty
- fast, extensible, and portable.
- .
- This package provides GSSAPI authentication support for Dovecot.
-
-Package: dovecot-sieve
-Architecture: any
-Depends: ${shlibs:Depends}, ${misc:Depends}, dovecot-core (= ${binary:Version}), ucf (>= 2.0020)
-Description: secure POP3/IMAP server - Sieve filters support
- Dovecot is a mail server whose major goals are security and extreme
- reliability. It tries very hard to handle all error conditions and verify
- that all data is valid, making it nearly impossible to crash. It supports
- mbox/Maildir and its own dbox/mdbox formats, and should also be pretty
- fast, extensible, and portable.
- .
- This package provides Sieve filters support for Dovecot.
-
-Package: dovecot-solr
-Architecture: any
-Depends: ${shlibs:Depends}, ${misc:Depends}, dovecot-core (= ${binary:Version})
-Description: secure POP3/IMAP server - Solr support
- Dovecot is a mail server whose major goals are security and extreme
- reliability. It tries very hard to handle all error conditions and verify
- that all data is valid, making it nearly impossible to crash. It supports
- mbox/Maildir and its own dbox/mdbox formats, and should also be pretty
- fast, extensible, and portable.
- .
- This package provides Solr full text search support for Dovecot.
-
-Package: dovecot-dbg
-Section: debug
-Priority: extra
-Architecture: any
-Depends: ${misc:Depends}, dovecot-core (= ${binary:Version})
-Description: secure POP3/IMAP server - debug symbols
- Dovecot is a mail server whose major goals are security and extreme
- reliability. It tries very hard to handle all error conditions and verify
- that all data is valid, making it nearly impossible to crash. It supports
- mbox/Maildir and its own dbox/mdbox formats, and should also be pretty
- fast, extensible, and portable.
- .
- This package contains debug symbols for Dovecot.
-
-Package: mail-stack-delivery
-Architecture: all
-Depends: dovecot-core, dovecot-imapd, dovecot-pop3d, dovecot-managesieved,
- postfix, ${misc:Depends}
-Replaces: dovecot-postfix (<< 1:1.2.12-0ubuntu1~)
-Description: mail server delivery agent stack provided by Ubuntu server team
- Ubuntu's mail stack provides fully operational delivery with
- safe defaults and additional options. Out of the box it supports IMAP,
- POP3 and SMTP services with SASL authentication and Maildir as default
- storage engine.
- .
- This package contains configuration files for dovecot.
- .
- This package modifies postfix's configuration to integrate with dovecot
+ This package provides Lucene full text search support for Dovecot. It has been modified by Mail-in-a-Box
+ to supply a dovecot-lucene package compatible with the official ubuntu trusty dovecot-core.
diff --git a/debian/dovecot-lucene.links b/debian/dovecot-lucene.links
new file mode 100644
index 0000000..6ffcbeb
--- /dev/null
+++ b/debian/dovecot-lucene.links
@@ -0,0 +1 @@
+/usr/share/bug/dovecot-core /usr/share/bug/dovecot-lucene
diff --git a/debian/dovecot-lucene.lintian-overrides b/debian/dovecot-lucene.lintian-overrides
new file mode 100644
index 0000000..60d90fd
--- /dev/null
+++ b/debian/dovecot-lucene.lintian-overrides
@@ -0,0 +1,2 @@
+dovecot-lucene: hardening-no-fortify-functions usr/lib/dovecot/modules/lib21_fts_lucene_plugin.so
+
diff --git a/debian/dovecot-lucene.substvars b/debian/dovecot-lucene.substvars
new file mode 100644
index 0000000..ed54f36
--- /dev/null
+++ b/debian/dovecot-lucene.substvars
@@ -0,0 +1,2 @@
+shlibs:Depends=libc6 (>= 2.4), libclucene-core1 (>= 2.3.3.4), libgcc1 (>= 1:4.1.1), libstdc++6 (>= 4.1.1), libstemmer0d (>= 0+svn527)
+misc:Depends=
diff --git a/debian/dovecot-lucene.triggers b/debian/dovecot-lucene.triggers
new file mode 100644
index 0000000..3d933a5
--- /dev/null
+++ b/debian/dovecot-lucene.triggers
@@ -0,0 +1 @@
+activate register-dovecot-plugin
--- a/debian/rules
+++ b/debian/rules
@@ -40,6 +40,7 @@
--with-solr \
--with-ioloop=best \
--with-libwrap \
+ --with-lucene \
--host=$(DEB_HOST_GNU_TYPE) \
--build=$(DEB_BUILD_GNU_TYPE) \
--prefix=/usr \
@@ -95,6 +96,10 @@
dh_testroot
dh_clean -k
dh_installdirs
+ mkdir -p $(CURDIR)/debian/dovecot-lucene/usr/lib/dovecot/modules
+ mv $(CURDIR)/src/plugins/fts-lucene/.libs/* $(CURDIR)/debian/dovecot-lucene/usr/lib/dovecot/modules/
+
+rest_disabled_by_miab:
$(MAKE) install DESTDIR=$(CURDIR)/debian/dovecot-core
$(MAKE) -C $(PIGEONHOLE_DIR) install DESTDIR=$(CURDIR)/debian/dovecot-core
rm `find $(CURDIR)/debian -name '*.la'`
@@ -209,7 +214,7 @@
dh_installdocs -a
dh_installexamples -a
dh_installpam -a
- mv $(CURDIR)/debian/dovecot-core/etc/pam.d/dovecot-core $(CURDIR)/debian/dovecot-core/etc/pam.d/dovecot
+ # mv $(CURDIR)/debian/dovecot-core/etc/pam.d/dovecot-core $(CURDIR)/debian/dovecot-core/etc/pam.d/dovecot
dh_systemd_enable
dh_installinit -pdovecot-core --name=dovecot
dh_systemd_start
@@ -220,10 +225,10 @@
dh_lintian -a
dh_installchangelogs -a ChangeLog
dh_link -a
- dh_strip -a --dbg-package=dovecot-dbg
+ #dh_strip -a --dbg-package=dovecot-dbg
dh_compress -a
dh_fixperms -a
- chmod 0700 debian/dovecot-core/etc/dovecot/private
+ #chmod 0700 debian/dovecot-core/etc/dovecot/private
dh_makeshlibs -a -n
dh_installdeb -a
dh_shlibdeps -a
--- a/debian/changelog
+++ a/debian/changelog
@@ -1,3 +1,9 @@
+dovecot (1:2.2.9-1ubuntu2.1+miab1) trusty; urgency=low
+
+ * Changed to just build dovecot-lucene for Mail-in-a-box PPA
+
+ -- Joshua Tauberer <jt@occams.info> Sat, 14 May 2015 16:13:00 -0400
+
dovecot (1:2.2.9-1ubuntu2.1) trusty-security; urgency=medium
* SECURITY UPDATE: denial of service via SSL connection exhaustion
--- a/debian/copyright 2014-03-07 07:26:37.000000000 -0500
+++ b/debian/copyright 2015-05-23 18:17:42.668005535 -0400
@@ -1,3 +1,7 @@
+This package is a fork by Mail-in-a-box (https://mailinabox.email). Original
+copyright statement follows:
+----------------------------------------------------------------------------
+
This package was debianized by Jaldhar H. Vyas <jaldhar@debian.org> on
Tue, 3 Dec 2002 01:10:07 -0500.

View File

@ -1,80 +0,0 @@
diff --git a/debian/NEWS b/debian/NEWS
index dd09744..de7b640 100644
--- a/debian/NEWS
+++ b/debian/NEWS
@@ -1,3 +1,9 @@
+postgrey (1.35-1+miab1)
+
+ Added DNSWL.org whitelisting.
+
+ -- Joshua Tauberer <jt@occams.info> Mon May 18 18:58:40 EDT 2015
+
postgrey (1.32-1) unstable; urgency=low
Postgrey is now listening to port 10023 and not 60000. The latter was an
diff --git a/debian/changelog b/debian/changelog
index 1058e15..e5e3557 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,9 @@
+postgrey (1.35-1+miab1) trusty; urgency=low
+
+ * Added DNSWL.org whitelisting.
+
+ -- Joshua Tauberer <jt@occams.info> Mon, 18 May 2015 21:58:40 +0000
+
postgrey (1.35-1) unstable; urgency=low
* New upstream release (Closes: 756486)
diff --git a/debian/control b/debian/control
index ce12ba6..0a82855 100644
--- a/debian/control
+++ b/debian/control
@@ -1,14 +1,11 @@
Source: postgrey
Section: mail
Priority: optional
-Maintainer: Antonio Radici <antonio@debian.org>
-Uploaders: Jon Daley <jondaley-guest@alioth.debian.org>
+Maintainer: Joshua Tauberer <jt@occams.info>
Build-Depends: debhelper (>= 7), quilt
Build-Depends-Indep: po-debconf
Standards-Version: 3.9.6
Homepage: http://postgrey.schweikert.ch/
-Vcs-Browser: http://git.debian.org/?p=collab-maint/postgrey.git
-Vcs-Git: git://git.debian.org/git/collab-maint/postgrey.git
Package: postgrey
Architecture: all
@@ -25,3 +22,6 @@ Description: greylisting implementation for Postfix
.
While Postgrey is designed for use with Postfix, it can also be used
with Exim.
+ .
+ This version has been modified by Mail-in-a-Box to whitelist senders
+ in the DNSWL.org list. See https://mailinabox.email.
diff --git a/debian/copyright b/debian/copyright
index 3cbe377..bf09b89 100644
--- a/debian/copyright
+++ b/debian/copyright
@@ -1,6 +1,10 @@
+This package is a fork by Mail-in-a-Box (https://mailinabox.email). Original
+copyright statement follows:
+----------------------------------------------------------------------------
+
This Debian package was prepared by Adrian von Bidder <cmot@debian.org> in
July 2004, then the package was adopted by Antonio Radici <antonio@dyne.org>
-in Sept 2009
+in Sept 2009.
It was downloaded from http://postgrey.schweikert.ch/
diff --git a/debian/patches/series b/debian/patches/series
index f4c5e31..3cd62b8 100644
--- a/debian/patches/series
+++ b/debian/patches/series
@@ -1,3 +1,3 @@
imported-upstream-diff
disable-transaction-logic
-
+mailinabox

View File

@ -1,100 +0,0 @@
Description: whitelist whatever dnswl.org whitelists
.
postgrey (1.35-1+miab1) unstable; urgency=low
.
* Added DNSWL.org whitelisting.
Author: Joshua Tauberer <jt@occams.info>
--- postgrey-1.35.orig/README
+++ postgrey-1.35/README
@@ -13,7 +13,7 @@ Requirements
- BerkeleyDB (Perl Module)
- Berkeley DB >= 4.1 (Library)
- Digest::SHA (Perl Module, only for --privacy option)
-
+- Net::DNS (Perl Module)
Documentation
-------------
--- postgrey-1.35.orig/postgrey
+++ postgrey-1.35/postgrey
@@ -18,6 +18,7 @@ use Fcntl ':flock'; # import LOCK_* cons
use Sys::Hostname;
use Sys::Syslog; # used only to find out which version we use
use POSIX qw(strftime setlocale LC_ALL);
+use Net::DNS; # for DNSWL.org whitelisting
use vars qw(@ISA);
@ISA = qw(Net::Server::Multiplex);
@@ -26,6 +27,8 @@ my $VERSION = '1.35';
my $DEFAULT_DBDIR = '/var/lib/postgrey';
my $CONFIG_DIR = '/etc/postgrey';
+my $dns_resolver = Net::DNS::Resolver->new;
+
sub cidr_parse($)
{
defined $_[0] or return undef;
@@ -48,6 +51,36 @@ sub cidr_match($$$)
return ($addr & $mask) == $net;
}
+sub reverseDottedQuad {
+ # This is the sub _chkValidPublicIP from Net::DNSBL by PJ Goodwin
+ # at http://www.the42.net/net-dnsbl.
+ my ($quad) = @_;
+ if ($quad =~ /^(\d+)\.(\d+)\.(\d+)\.(\d+)$/) {
+ my ($ip1,$ip2,$ip3,$ip4) = ($1, $2, $3, $4);
+ if (
+ $ip1 == 10 || #10.0.0.0/8 (10/8)
+ ($ip1 == 172 && $ip2 >= 16 && $ip2 <= 31) || #172.16.0.0/12 (172.16/12)
+ ($ip1 == 192 && $ip2 == 168) || #192.168.0.0/16 (192.168/16)
+ $quad eq '127.0.0.1' # localhost
+ ) {
+ # toss the RFC1918 specified privates
+ return undef;
+ } elsif (
+ ($ip1 <= 1 || $ip1 > 254) ||
+ ($ip2 < 0 || $ip2 > 255) ||
+ ($ip3 < 0 || $ip3 > 255) ||
+ ($ip4 < 0 || $ip4 > 255)
+ ) {
+ #invalid oct, toss it;
+ return undef;
+ }
+ my $revquad = $ip4 . "." . $ip3 . "." . $ip2 . "." . $ip1;
+ return $revquad;
+ } else { # invalid quad
+ return undef;
+ }
+}
+
sub read_clients_whitelists($)
{
my ($self) = @_;
@@ -361,6 +394,25 @@ sub smtpd_access_policy($$)
}
}
+ # whitelist clients in dnswl.org
+ my $revip = reverseDottedQuad($attr->{client_address});
+ if ($revip) { # valid IP / plausibly in DNSWL
+ my $answer = $dns_resolver->send($revip . '.list.dnswl.org');
+ if ($answer && scalar($answer->answer) > 0) {
+ my @rrs = $answer->answer;
+ if ($rrs[0]->type eq 'A' && $rrs[0]->address ne '127.0.0.255') {
+ # Address appears in DNSWL. (127.0.0.255 means we were rate-limited.)
+ my $code = $rrs[0]->address;
+ if ($code =~ /^127.0.(\d+)\.([0-3])$/) {
+ my %dnswltrust = (0 => 'legitimate', 1 => 'occasional spam', 2 => 'rare spam', 3 => 'highly unlikely to send spam');
+ $code = $2 . '/' . $dnswltrust{$2};
+ }
+ $self->mylog_action($attr, 'pass', 'client whitelisted by dnswl.org (' . $code . ')');
+ return 'DUNNO';
+ }
+ }
+ }
+
# auto whitelist clients (see below for explanation)
my ($cawl_db, $cawl_key, $cawl_count, $cawl_last);
if($self->{postgrey}{awl_clients}) {

View File

@ -1,9 +1,14 @@
Mail-in-a-Box Security Guide
============================
Mail-in-a-Box turns a fresh Ubuntu 14.04 LTS 64-bit machine into a mail server appliance by installing and configuring various components.
Mail-in-a-Box turns a fresh Ubuntu 22.04 LTS 64-bit machine into a mail server appliance by installing and configuring various components.
This page documents the security features of Mail-in-a-Box. The term “box” is used below to mean a configured Mail-in-a-Box.
This page documents the security posture of Mail-in-a-Box. The term “box” is used below to mean a configured Mail-in-a-Box.
Reporting Security Vulnerabilities
----------------------------------
Security vulnerabilities should be reported to the [project's maintainer](https://joshdata.me) via email.
Threat Model
------------
@ -17,8 +22,11 @@ The primary goal of Mail-in-a-Box is to make deploying a good mail server easy,
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.
* Outbound mail is sent with the highest level of TLS possible (more on that below).
* 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
----------------
@ -29,34 +37,24 @@ The box's administrator and its (non-administrative) mail users must sometimes c
These services are protected by [TLS](https://en.wikipedia.org/wiki/Transport_Layer_Security):
* SMTP Submission (port 587). Mail users submit outbound mail through SMTP with STARTTLS on port 587.
* 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:
* SSL 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, TLSv1.1 and TLSv1.2 are offered (the older SSL protocols are not offered).
* Export-grade ciphers, the anonymous DH/ECDH algorithms (aNULL), and clear-text ciphers (eNULL) are not offered.
* The minimum cipher key length offered is 112 bits. The maximum is 256 bits. Diffie-Hellman ciphers use a 2048-bit key for forward secrecy.
* 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 (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))
* 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))
For more details, see the [output of SSLyze for these ports](tests/tls_results.txt).
The cipher and protocol selection are chosen to support the following clients:
* For HTTPS: Firefox 1, Chrome 1, IE 7, Opera 5, Safari 1, Windows XP IE8, Android 2.3, Java 7.
* For other protocols: TBD.
### 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))
When using the web-based administrative control panel, after logging in an API key is placed in the browser's local storage (rather than, say, the user's actual password). The API key is an HMAC based on the user's email address and current password, and it is keyed by a secret known only to the control panel service. By resetting an administrator's password, any HMACs previously generated for that user will expire.
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
@ -66,6 +64,14 @@ The [setup guide video](https://mailinabox.email/) explains how to verify the ho
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
-------------
@ -77,7 +83,7 @@ The first step in resolving the destination server for an email address is perfo
### 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 will be used to the extent the recipient server supports them. ([source](setup/mail-postfix.sh))
The box (along with the vast majority of mail servers) uses [opportunistic encryption](https://en.wikipedia.org/wiki/Opportunistic_encryption), meaning the mail is encrypted in transit and protected from passive eavesdropping, but it is not protected from an active man-in-the-middle attack. Modern encryption settings (TLSv1 and later, no RC4) will be used to the extent the recipient server supports them. ([source](setup/mail-postfix.sh))
### DANE
@ -89,14 +95,20 @@ Domain policy records allow recipient MTAs to detect when the _domain_ part of o
### User Policy
While domain policy records prevent other servers from sending mail with a "From:" header that matches a domain hosted on the box (see above), those policy records do not guarnatee that the user portion of the sender email address matches the actual sender. In enterprise environments where the box may host the mail of untrusted users, it is important to guard against users impersonating other users. The box restricts the envelope sender address that users may put into outbound mail to either a) their own email address (their SMTP login username) or b) any alias that they are listed as a direct recipient of. Note that the envelope sender address is not the same as the "From:" header.
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
### Encryption Settings
As discussed above, there is no way to require on-the-wire encryption of mail. 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 SSLv3 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 forward secrecy, however. ([source](setup/mail-postfix.sh))
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

View File

@ -2,49 +2,87 @@
#########################################################
# This script is intended to be run like this:
#
# curl https://.../bootstrap.sh | sudo bash
# curl https://mailinabox.email/setup.sh | sudo bash
#
#########################################################
if [ -z "$TAG" ]; then
TAG=v0.12b
# If a version to install isn't explicitly given as an environment
# variable, then install the latest version. But the latest version
# depends on the machine's version of Ubuntu. Existing users need to
# be able to upgrade to the latest version available for that version
# of Ubuntu to satisfy the migration requirements.
#
# Also, the system status checks read this script for TAG = (without the
# space, but if we put it in a comment it would confuse the status checks!)
# to get the latest version, so the first such line must be the one that we
# want to display in status checks.
#
# Allow point-release versions of the major releases, e.g. 22.04.1 is OK.
UBUNTU_VERSION=$( lsb_release -d | sed 's/.*:\s*//' | sed 's/\([0-9]*\.[0-9]*\)\.[0-9]/\1/' )
if [ "$UBUNTU_VERSION" == "Ubuntu 22.04 LTS" ]; then
# This machine is running Ubuntu 22.04, which is supported by
# Mail-in-a-Box versions 60 and later.
TAG=v68
elif [ "$UBUNTU_VERSION" == "Ubuntu 18.04 LTS" ]; then
# This machine is running Ubuntu 18.04, which is supported by
# Mail-in-a-Box versions 0.40 through 5x.
echo "Support is ending for Ubuntu 18.04."
echo "Please immediately begin to migrate your data to"
echo "a new machine running Ubuntu 22.04. See:"
echo "https://mailinabox.email/maintenance.html#upgrade"
TAG=v57a
elif [ "$UBUNTU_VERSION" == "Ubuntu 14.04 LTS" ]; then
# This machine is running Ubuntu 14.04, which is supported by
# Mail-in-a-Box versions 1 through v0.30.
echo "Ubuntu 14.04 is no longer supported."
echo "The last version of Mail-in-a-Box supporting Ubuntu 14.04 will be installed."
TAG=v0.30
else
echo "This script may be used only on a machine running Ubuntu 14.04, 18.04, or 22.04."
exit 1
fi
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
# Clone the Mail-in-a-Box repository if it doesn't exist.
if [ ! -d $HOME/mailinabox ]; then
if [ ! -d "$HOME/mailinabox" ]; then
if [ ! -f /usr/bin/git ]; then
echo Installing git . . .
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. . .
if [ "$SOURCE" == "" ]; then
SOURCE=https://github.com/mail-in-a-box/mailinabox
fi
echo "Downloading Mail-in-a-Box $TAG. . ."
git clone \
-b $TAG --depth 1 \
https://github.com/mail-in-a-box/mailinabox \
$HOME/mailinabox \
-b "$TAG" --depth 1 \
"$SOURCE" \
"$HOME/mailinabox" \
< /dev/null 2> /dev/null
echo
fi
# Change directory to it.
cd $HOME/mailinabox
cd "$HOME/mailinabox" || exit
# Update it.
if [ "$TAG" != `git describe` ]; 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
if [ "$TAG" != "$(git describe --always)" ]; 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

View File

@ -10,27 +10,34 @@ source setup/functions.sh # load our functions
source /etc/mailinabox.conf # load global vars
# Install DKIM...
echo "Installing OpenDKIM/OpenDMARC..."
apt_install opendkim opendkim-tools opendmarc
# Make sure configuration directories exist.
mkdir -p /etc/opendkim;
mkdir -p $STORAGE_ROOT/mail/dkim
mkdir -p "$STORAGE_ROOT/mail/dkim"
# Used in InternalHosts and ExternalIgnoreList configuration directives.
# 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 #NODOC
else
# 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
@ -38,7 +45,7 @@ fi
# 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 tha the files are named after the
# 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.
#
@ -46,16 +53,49 @@ fi
# 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
opendkim-genkey -b 2048 -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
chown -R opendkim:opendkim "$STORAGE_ROOT/mail/dkim"
chmod go-rwx "$STORAGE_ROOT/mail/dkim"
tools/editconf.py /etc/opendmarc.conf -s \
"Syslog=true" \
"Socket=inet:8893@[127.0.0.1]"
"Socket=inet:8893@[127.0.0.1]" \
"FailureReports=false"
# 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.
tools/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.
tools/editconf.py /etc/opendmarc.conf -s \
"SPFSelfValidate=true"
# Disables generation of failure reports for sending domains that publish a
# "none" policy.
tools/editconf.py /etc/opendmarc.conf -s \
"FailureReportsOnNone=false"
# 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.
tools/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)
@ -74,6 +114,9 @@ tools/editconf.py /etc/postfix/main.cf \
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

View File

@ -10,22 +10,19 @@
source setup/functions.sh # load our functions
source /etc/mailinabox.conf # load global vars
# Install the packages.
#
# * nsd: The non-recursive nameserver that publishes our DNS records.
# * ldnsutils: Helper utilities for signing DNSSEC zones.
# * openssh-client: Provides ssh-keyscan which we use to create SSHFP records.
apt_install nsd ldnsutils openssh-client
# 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
cat > /etc/nsd/nsd.conf << EOF;
# No not edit. Overwritten by Mail-in-a-Box setup.
# Do not edit. Overwritten by Mail-in-a-Box setup.
server:
hide-version: yes
logfile: "/var/log/nsd.log"
# identify the server (CH TXT ID.SERVER entry).
identity: ""
@ -49,56 +46,79 @@ for ip in $PRIVATE_IP $PRIVATE_IPV6; do
echo " ip-address: $ip" >> /etc/nsd/nsd.conf;
done
echo "include: /etc/nsd/zones.conf" >> /etc/nsd/nsd.conf;
# 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
# Create DNSSEC signing keys.
mkdir -p "$STORAGE_ROOT/dns/dnssec";
# TLDs don't all support the same algorithms, so we'll generate keys using a few
# different algorithms. RSASHA1-NSEC3-SHA1 was possibly the first widely used
# algorithm that supported NSEC3, which is a security best practice. However TLDs
# will probably be moving away from it to a a SHA256-based algorithm.
#
# Supports `RSASHA1-NSEC3-SHA1` (didn't test with `RSASHA256`):
#
# * .info
# * .me
#
# Requires `RSASHA256`
#
# * .email
# * .guide
#
# Supports `RSASHA256` (and defaulting to this)
#
# * .fund
# 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 RSASHA1-NSEC3-SHA1 RSASHA256; do
for algo in RSASHA256 ECDSAP256SHA256; do
if [ ! -f "$STORAGE_ROOT/dns/dnssec/$algo.conf" ]; then
if [ $FIRST == 1 ]; then
echo "Generating DNSSEC signing keys. This may take a few minutes..."
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.
# 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.
KSK=$(umask 077; cd $STORAGE_ROOT/dns/dnssec; ldns-keygen -a $algo -b 2048 -k _domain_);
#
# 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 length.
ZSK=$(umask 077; cd $STORAGE_ROOT/dns/dnssec; ldns-keygen -a $algo -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 normally provided to domain name registrar (but it's actually invalid with `_domain_`)
# * `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!)
@ -106,7 +126,7 @@ if [ ! -f "$STORAGE_ROOT/dns/dnssec/$algo.conf" ]; then
# 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/$algo.conf << EOF;
cat > "$STORAGE_ROOT/dns/dnssec/$algo.conf" << EOF;
KSK=$KSK
ZSK=$ZSK
EOF
@ -122,7 +142,7 @@ 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,17 @@
#!/bin/bash
# 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
@ -22,7 +23,7 @@ if [ -z "`tools/mail.py user`" ]; then
input_box "Mail Account" \
"That's not a valid email address.
\n\nWhat email address do you want?" \
$EMAIL_ADDR \
"$EMAIL_ADDR" \
EMAIL_ADDR
if [ -z "$EMAIL_ADDR" ]; then
# user hit ESC/cancel
@ -35,7 +36,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 +48,11 @@ 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

View File

@ -1,27 +1,39 @@
#!/bin/bash
# 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
PHP_VER=8.0
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.
E=$?
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
rm -f "$OUTPUT"
}
function apt_get_quiet {
@ -39,42 +51,21 @@ function apt_get_quiet {
}
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
# 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.
apt_get_quiet install $PACKAGES
# 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 {
# Guess the machine's hostname. It should be a fully qualified
# domain name suitable for DNS. None of these calls may provide
# the right value, but it's the best guess we can make.
set -- $(hostname --fqdn 2>/dev/null ||
set -- "$(hostname --fqdn 2>/dev/null ||
hostname --all-fqdns 2>/dev/null ||
hostname 2>/dev/null)
hostname 2>/dev/null)"
printf '%s\n' "$1" # return this value
}
@ -86,7 +77,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 {
@ -129,31 +120,37 @@ 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/")
address=$(echo "$route" | sed "s/.* src \([^ ]*\).*/\1/")
if [[ "$1" == "6" && $address == fe80:* ]]; then
# For IPv6 link-local addresses, parse the interface out
# of the route information and append it with a '%'.
interface=$(echo $route | sed "s/.* dev \([^ ]*\).*/\1/")
interface=$(echo "$route" | sed "s/.* dev \([^ ]*\).*/\1/")
address=$address%$interface
fi
echo $address
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
}
function restart_service {
hide_output service $1 restart
hide_output service "$1" restart
}
## Dialog Functions ##
@ -165,10 +162,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 {
@ -178,8 +178,10 @@ function input_menu {
declare -n result=$4
declare -n result_code=$4_EXITCODE
local IFS=^$'\n'
result=$(dialog --stdout --title "$1" --menu "$2" 0 0 0 $3)
set +e
result=$(dialog --stdout --title "$1" --menu "$2" 0 0 0 "$3")
result_code=$?
set -e
}
function wget_verify {
@ -189,17 +191,17 @@ function wget_verify {
HASH=$2
DEST=$3
CHECKSUM="$HASH $DEST"
rm -f $DEST
wget -q -O $DEST $URL || exit 1
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
sha1sum "$DEST"
echo
echo "Expected:"
echo "$CHECKSUM"
rm -f $DEST
rm -f "$DEST"
exit 1
fi
}
@ -215,9 +217,9 @@ function git_clone {
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 "$TARGETPATH"
git clone -q "$REPO" $TMPPATH || exit 1
(cd $TMPPATH; git checkout -q "$TREEISH";) || exit 1
mv $TMPPATH/"$SUBDIR" "$TARGETPATH"
rm -rf $TMPPATH
}

View File

@ -23,9 +23,10 @@ source /etc/mailinabox.conf # load global vars
# 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-pop3d dovecot-lmtpd dovecot-sqlite sqlite3 \
dovecot-sieve dovecot-managesieved dovecot-lucene
dovecot-sieve dovecot-managesieved
# The `dovecot-imapd`, `dovecot-pop3d`, and `dovecot-lmtpd` packages automatically
# enable IMAP, POP and LMTP protocols.
@ -36,8 +37,17 @@ apt_install \
# 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
tools/editconf.py /etc/dovecot/conf.d/10-master.conf \
default_process_limit=$(echo "`nproc` * 250" | bc)
default_process_limit="$(($(nproc) * 250))" \
default_vsz_limit="$(($(free -tm | tail -1 | awk '{print $2}') / 3))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.
@ -51,10 +61,13 @@ tools/editconf.py /etc/sysctl.conf \
# username part of the user's email address. We'll ensure that no bad domains or email addresses
# are created within the management daemon.
tools/editconf.py /etc/dovecot/conf.d/10-mail.conf \
mail_location=maildir:$STORAGE_ROOT/mail/mailboxes/%d/%n \
mail_location="maildir:$STORAGE_ROOT/mail/mailboxes/%d/%n" \
mail_privileged_group=mail \
first_valid_uid=0
# Create, subscribe, and mark as special folders: INBOX, Drafts, Sent, Trash, Spam and Archive.
cp conf/dovecot-mailboxes.conf /etc/dovecot/conf.d/15-mailboxes.conf
# ### IMAP/POP
# Require that passwords are sent over SSL only, and allow the usual IMAP authentication mechanisms.
@ -65,13 +78,17 @@ tools/editconf.py /etc/dovecot/conf.d/10-auth.conf \
"auth_mechanisms=plain login"
# Enable SSL, specify the location of the SSL certificate and private key files.
# Disable obsolete SSL protocols and allow only good ciphers per http://baldric.net/2013/12/07/tls-ciphers-in-postfix-and-dovecot/.
# 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.
tools/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_protocols=!SSLv3 !SSLv2" \
"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/POP because there is no reason for a user to transmit
# login credentials outside of an encrypted connection. Only the over-TLS versions
@ -84,28 +101,18 @@ 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/.
# 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/).
tools/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.
# 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.
tools/editconf.py /etc/dovecot/conf.d/20-pop3.conf \
pop3_uidl_format="%08Xu%08Xv"
# Full Text Search - Enable full text search of mail using dovecot's lucene plugin,
# which *we* package and distribute (dovecot-lucene package).
tools/editconf.py /etc/dovecot/conf.d/10-mail.conf \
mail_plugins="\$mail_plugins fts fts_lucene"
cat > /etc/dovecot/conf.d/90-plugin-fts.conf << EOF;
plugin {
fts = lucene
fts_lucene = whitespace_chars=@.
}
EOF
# ### LDA (LMTP)
# Enable Dovecot's LDA service with the LMTP protocol. It will listen
@ -129,15 +136,23 @@ 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` is required or LMTP won't start. An alias
# will be created automatically by our management daemon.
tools/editconf.py /etc/dovecot/conf.d/15-lda.conf \
postmaster_address=postmaster@$PRIMARY_HOSTNAME
"postmaster_address=postmaster@$PRIMARY_HOSTNAME"
# ### Sieve
@ -150,6 +165,12 @@ sed -i "s/#mail_plugins = .*/mail_plugins = \$mail_plugins sieve/" /etc/dovecot/
#
# * `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
@ -159,8 +180,11 @@ sed -i "s/#mail_plugins = .*/mail_plugins = \$mail_plugins sieve/" /etc/dovecot/
cat > /etc/dovecot/conf.d/99-local-sieve.conf << EOF;
plugin {
sieve_before = /etc/dovecot/sieve-spam.sieve
sieve_before2 = $STORAGE_ROOT/mail/sieve/global_before
sieve_after = $STORAGE_ROOT/mail/sieve/global_after
sieve = $STORAGE_ROOT/mail/sieve/%d/%n.sieve
sieve_dir = $STORAGE_ROOT/mail/sieve/%d/%n
sieve_redirect_envelope_from = recipient
}
EOF
@ -177,16 +201,21 @@ chown -R mail:dovecot /etc/dovecot
chmod -R o-rwx /etc/dovecot
# Ensure mailbox files have a directory that exists and are owned by the mail user.
mkdir -p $STORAGE_ROOT/mail/mailboxes
chown -R mail.mail $STORAGE_ROOT/mail/mailboxes
mkdir -p "$STORAGE_ROOT/mail/mailboxes"
chown -R mail:mail "$STORAGE_ROOT/mail/mailboxes"
# Same for the sieve scripts.
mkdir -p $STORAGE_ROOT/mail/sieve
chown -R mail.mail $STORAGE_ROOT/mail/sieve
mkdir -p "$STORAGE_ROOT/mail/sieve"
mkdir -p "$STORAGE_ROOT/mail/sieve/global_before"
mkdir -p "$STORAGE_ROOT/mail/sieve/global_after"
chown -R mail:mail "$STORAGE_ROOT/mail/sieve"
# 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.
@ -41,78 +41,127 @@ source /etc/mailinabox.conf # load global vars
# always will.
# * `ca-certificates`: A trust store used to squelch postfix warnings about
# untrusted opportunistically-encrypted connections.
#
# postgrey is going to come in via the Mail-in-a-Box PPA, which publishes
# a modified version of postgrey that lets senders whitelisted by dnswl.org
# pass through without being greylisted. So please note [dnswl's license terms](https://www.dnswl.org/?page_id=9):
# > Every user with more than 100000 queries per day on the public nameserver
# > infrastructure and every commercial vendor of dnswl.org data (eg through
# > anti-spam solutions) must register with dnswl.org and purchase a subscription.
apt_install postfix postfix-pcre postgrey ca-certificates
echo "Installing Postfix (SMTP server)..."
apt_install postfix postfix-sqlite postfix-pcre postgrey ca-certificates
# ### Basic Settings
# 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).
tools/editconf.py /etc/postfix/main.cf \
inet_interfaces=all \
myhostname=$PRIMARY_HOSTNAME\
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/)" \
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.
tools/editconf.py /etc/postfix/main.cf \
delay_warning_time=3h \
maximal_queue_lifetime=2d \
bounce_queue_lifetime=1d
# Guard against SMTP smuggling
# This "long-term" fix is recommended at https://www.postfix.org/smtp-smuggling.html.
# This beecame supported in a backported fix in package version 3.6.4-1ubuntu1.3. It is
# unnecessary in Postfix 3.9+ where this is the default. The "short-term" workarounds
# that we previously had are reverted to postfix defaults (though smtpd_discard_ehlo_keywords
# was never included in a released version of Mail-in-a-Box).
tools/editconf.py /etc/postfix/main.cf -e \
smtpd_data_restrictions= \
smtpd_discard_ehlo_keywords=
tools/editconf.py /etc/postfix/main.cf \
smtpd_forbid_bare_newline=normalize
# ### Outgoing Mail
# Enable the 'submission' port 587 smtpd server and tweak its settings.
# Enable the 'submission' ports 465 and 587 and tweak their settings.
#
# * 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.
# * Require the best ciphers for incoming connections per http://baldric.net/2013/12/07/tls-ciphers-in-postfix-and-dovecot/.
# By putting this setting here we leave opportunistic TLS on incoming mail at default cipher settings (any cipher is better than none).
# 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.
# authenticated users. By default Postfix also applies this to attached
# emails but we turn this off by setting nested_header_checks empty.
tools/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_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 smtpd_tls_ciphers=high -o smtpd_tls_exclude_ciphers=aNULL,DES,3DES,MD5,DES+MD5,RC4 -o smtpd_tls_mandatory_protocols=!SSLv2,!SSLv3
-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.
# Set stronger DH parameters, which via openssl tend to default to 1024 bits
# (see ssl.sh).
# 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&server-version=3.3.0&config=old&openssl-version=1.1.1
tools/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_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 \
smtpd_tls_exclude_ciphers=aNULL \
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,RC4 \
tls_preempt_cipherlist=no \
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.
tools/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_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.)
tools/editconf.py /etc/postfix/main.cf \
@ -127,34 +176,47 @@ tools/editconf.py /etc/postfix/main.cf \
# 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=dnssec`.
# relies on our local DNS server (see system.sh) and `smtp_dns_support_level=dnssec`.
#
# 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`.
tools/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
tools/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.
tools/editconf.py /etc/postfix/main.cf -e lmtp_destination_recipient_limit=
# Who can send mail to us? Some basic filters.
#
@ -168,14 +230,15 @@ tools/editconf.py /etc/postfix/main.cf virtual_transport=lmtp:[127.0.0.1]:10025
# * `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.
#
# Note the spamhaus rbl return codes are taken into account as adviced here: https://docs.spamhaus.com/datasets/docs/source/40-real-world-usage/PublicMirrors/MTAs/020-Postfix.html
# 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
tools/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=permit_sasl_authenticated,permit_mynetworks,"reject_rbl_client zen.spamhaus.org",reject_unlisted_recipient,"check_policy_service inet:127.0.0.1:10023"
smtpd_sender_restrictions="reject_non_fqdn_sender,reject_unknown_sender_domain,reject_authenticated_sender_login_mismatch,reject_rhsbl_sender dbl.spamhaus.org=127.0.1.[2..99]" \
smtpd_recipient_restrictions="permit_sasl_authenticated,permit_mynetworks,reject_rbl_client zen.spamhaus.org=127.0.0.[2..11],reject_unlisted_recipient,check_policy_service inet:127.0.0.1:10023"
# Postfix connects to Postgrey on the 127.0.0.1 interface specifically. Ensure that
# Postgrey listens on the same interface (and not IPv6, for instance).
@ -183,9 +246,57 @@ tools/editconf.py /etc/postfix/main.cf \
# 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).
# 180 seconds (default is 300 seconds). We will move the postgrey database
# under $STORAGE_ROOT. This prevents a "warming up" that would have occured
# previously with a migrated or reinstalled OS. We will specify this new path
# with the --dbdir=... option. Arguments within POSTGREY_OPTS can not have spaces,
# including dbdir. This is due to the way the init script sources the
# /etc/default/postgrey file. --dbdir=... either needs to be a path without spaces
# (luckily $STORAGE_ROOT does not currently work with spaces), or it needs to be a
# symlink without spaces that can point to a folder with spaces). We'll just assume
# $STORAGE_ROOT won't have spaces to simplify things.
tools/editconf.py /etc/default/postgrey \
POSTGREY_OPTS=\"'--inet=127.0.0.1:10023 --delay=180'\"
POSTGREY_OPTS=\""--inet=127.0.0.1:10023 --delay=180 --dbdir=$STORAGE_ROOT/mail/postgrey/db"\"
# If the $STORAGE_ROOT/mail/postgrey is empty, copy the postgrey database over from the old location
if [ ! -d "$STORAGE_ROOT/mail/postgrey/db" ]; then
# Stop the service
service postgrey stop
# Ensure the new paths for postgrey db exists
mkdir -p "$STORAGE_ROOT/mail/postgrey/db"
# Move over database files
mv /var/lib/postgrey/* "$STORAGE_ROOT/mail/postgrey/db/" || true
fi
# Ensure permissions are set
chown -R postgrey:postgrey "$STORAGE_ROOT/mail/postgrey/"
chmod 700 "$STORAGE_ROOT/mail/postgrey/"{,db}
# We are going to setup a newer whitelist for postgrey, the version included in the distribution is old
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.
# The same limit is specified in nginx.conf for mail submitted via webmail and Z-Push.
@ -195,6 +306,7 @@ tools/editconf.py /etc/postfix/main.cf \
# Allow the two SMTP ports in the firewall.
ufw_allow smtp
ufw_allow smtps
ufw_allow submission
# Restart services

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
@ -18,10 +18,12 @@ source /etc/mailinabox.conf # load global vars
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;
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, permitted_senders TEXT);" | 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";
fi
# ### User Authentication
@ -38,17 +40,19 @@ 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 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
@ -63,45 +67,58 @@ service auth {
}
EOF
# And have Postfix use that service.
# 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.
tools/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
# Use a Sqlite3 database to set login maps. This is used with
# reject_authenticated_sender_login_mismatch to see if user is
# allowed to send mail using FROM field specified in the request.
# 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.
tools/editconf.py /etc/postfix/main.cf \
smtpd_sender_login_maps=sqlite:/etc/postfix/sender-login-maps.cf
# SQL statement to set login map which includes the case when user is
# sending email using a valid alias.
# This is the same as virtual-alias-maps.cf, See below
# 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 destination from (SELECT destination, 0 as priority FROM aliases WHERE source='%s' UNION SELECT email as destination, 1 as priority FROM users WHERE email='%s') ORDER BY priority LIMIT 1;
query = SELECT 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.
# 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.
tools/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'
@ -127,9 +144,13 @@ EOF
# 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 (SELECT destination, 0 as priority FROM aliases WHERE source='%s' UNION SELECT email as destination, 1 as priority FROM users WHERE email='%s') ORDER BY priority LIMIT 1;
query = SELECT destination from (SELECT destination, 0 as priority FROM aliases WHERE source='%s' 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
# Restart Services

View File

@ -1,48 +1,123 @@
#!/bin/bash
source setup/functions.sh
source /etc/mailinabox.conf # load global vars
# build-essential libssl-dev libffi-dev python3-dev: Required to pip install cryptography.
apt_install python3-flask links duplicity libyaml-dev python3-dnspython python3-dateutil \
build-essential libssl-dev libffi-dev python3-dev
hide_output pip3 install --upgrade rtyaml email_validator idna cryptography
# email_validator is repeated in setup/questions.sh
echo "Installing Mail-in-a-Box system management daemon..."
# Create a backup directory and a random key for encrypting backups.
mkdir -p $STORAGE_ROOT/backup
if [ ! -f $STORAGE_ROOT/backup/secret_key.txt ]; then
$(umask 077; openssl rand -base64 2048 > $STORAGE_ROOT/backup/secret_key.txt)
# DEPENDENCIES
# duplicity is used to make backups of user data.
#
# 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.
apt_install duplicity python3-pip virtualenv certbot rsync
# 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
# 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
# A bug specific to Ubuntu 22.04 and Python 3.10 requires
# forcing a virtualenv directory layout option (see #2335
# and https://github.com/pypa/virtualenv/pull/2415). In
# our issue, reportedly installing python3-distutils didn't
# fix the problem.)
export DEB_PYTHON_INSTALL_LAYOUT='deb'
hide_output virtualenv -ppython3 $venv
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
# 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==37.0.2" psutil postfix-mta-sts-resolver \
b2sdk boto3
# CONFIGURATION
# Create a backup directory and a random key for encrypting backups.
mkdir -p "$STORAGE_ROOT/backup"
if [ ! -f "$STORAGE_ROOT/backup/secret_key.txt" ]; then
umask 077; openssl rand -base64 2048 > "$STORAGE_ROOT/backup/secret_key.txt"
fi
# 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=2.2.4
jquery_url=https://code.jquery.com
# Get jQuery
wget_verify $jquery_url/jquery-$jquery_version.min.js 69bb69e25ca7d5ef0935317584e6153f3fd9a88c $assets_dir/jquery.min.js
# Bootstrap CDN URL
bootstrap_version=3.4.1
bootstrap_url=https://github.com/twbs/bootstrap/releases/download/v$bootstrap_version/bootstrap-$bootstrap_version-dist.zip
# Get Bootstrap
wget_verify $bootstrap_url 0bb64c67c2552014d48ab4db81c2e8c01781f580 /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
# 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
# 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
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 /etc/cron.daily/mailinabox-backup
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 daily status checks. Compare each day to the previous
# for changes and mail the changes to the administrator.
cat > /etc/cron.daily/mailinabox-statuschecks << EOF;
#!/bin/bash
# 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 status checks.
$(pwd)/management/status_checks.py --show-changes --smtp
# Run nightly tasks: backup, status checks.
$minute 3 * * * root (cd $PWD && management/daily_tasks.sh)
EOF
chmod +x /etc/cron.daily/mailinabox-statuschecks
# Start it.
# Start the management server.
restart_service mailinabox

View File

@ -9,6 +9,7 @@ import sys, os, os.path, glob, re, shutil
sys.path.insert(0, 'management')
from utils import load_environment, save_environment, shell
import contextlib
def migration_1(env):
# Re-arrange where we store SSL certificates. There was a typo also.
@ -31,10 +32,8 @@ def migration_1(env):
move_file(sslfn, domain_name, file_type)
# Move the old domains directory if it is now empty.
try:
with contextlib.suppress(Exception):
os.rmdir(os.path.join( env["STORAGE_ROOT"], 'ssl/domains'))
except:
pass
def migration_2(env):
# Delete the .dovecot_sieve script everywhere. This was formerly a copy of our spam -> Spam
@ -101,6 +100,98 @@ def migration_8(env):
# 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)
# 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 "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:
@ -120,8 +211,8 @@ def run_migrations():
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:
migration_id = f.read().strip();
with open(migration_id_file, encoding='utf-8') as f:
migration_id = f.read().strip()
if migration_id is None:
# Load the legacy location of the migration ID. We'll drop support
@ -130,7 +221,7 @@ def run_migrations():
if migration_id is None:
print()
print("%s file doesn't exists. Skipping migration..." % (migration_id_file,))
print(f"{migration_id_file} file doesn't exists. Skipping migration...")
return
ourver = int(migration_id)
@ -161,7 +252,7 @@ def run_migrations():
# Write out our current version now. Do this sooner rather than later
# in case of any problems.
with open(migration_id_file, "w") as f:
with open(migration_id_file, "w", encoding='utf-8') as f:
f.write(str(ourver) + "\n")
# Delete the legacy location of this field.

View File

@ -6,7 +6,9 @@ source setup/functions.sh # load our functions
source /etc/mailinabox.conf # load global vars
# install Munin
apt_install munin munin-node
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;
@ -18,15 +20,65 @@ 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.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
tools/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
sudo -u munin munin-cron
# (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,3 +1,4 @@
#!/bin/bash
# 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.
apt_get_quiet install bind9-host sed netcat-openbsd
@ -6,7 +7,7 @@ apt_get_quiet install bind9-host sed netcat-openbsd
# The user might have chosen a name that was previously in use by a spammer
# and will not be able to reliably send mail. Do this after any automatic
# choices made above.
if host $PRIMARY_HOSTNAME.dbl.spamhaus.org > /dev/null; then
if host "$PRIMARY_HOSTNAME.dbl.spamhaus.org" > /dev/null; then
echo
echo "The hostname you chose '$PRIMARY_HOSTNAME' is listed in the"
echo "Spamhaus Domain Block List. See http://www.spamhaus.org/dbl/"
@ -22,8 +23,8 @@ fi
# The user might have ended up on an IP address that was previously in use
# by a spammer, or the user may be deploying on a residential network. We
# will not be able to reliably send mail in these cases.
REVERSED_IPV4=$(echo $PUBLIC_IP | sed "s/\([0-9]*\).\([0-9]*\).\([0-9]*\).\([0-9]*\)/\4.\3.\2.\1/")
if host $REVERSED_IPV4.zen.spamhaus.org > /dev/null; then
REVERSED_IPV4=$(echo "$PUBLIC_IP" | sed "s/\([0-9]*\).\([0-9]*\).\([0-9]*\).\([0-9]*\)/\4.\3.\2.\1/")
if host "$REVERSED_IPV4.zen.spamhaus.org" > /dev/null; then
echo
echo "The IP address $PUBLIC_IP is listed in the Spamhaus Block List."
echo "See http://www.spamhaus.org/query/ip/$PUBLIC_IP."

454
setup/nextcloud.sh Executable file
View File

@ -0,0 +1,454 @@
#!/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=26.0.12
nextcloud_hash=b55e9f51171c0a9b9ab3686cf5c8ad1a4292ca15
# Nextcloud apps
# --------------
# * Find the most recent tag that is compatible with the Nextcloud version above by:
# https://github.com/nextcloud-releases/contacts/tags
# https://github.com/nextcloud-releases/calendar/tags
# https://github.com/nextcloud/user_external/tags
#
# * For these three packages, contact, calendar and 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:
# Always ensure the versions are supported, see https://apps.nextcloud.com/apps/contacts
contacts_ver=5.5.3
contacts_hash=799550f38e46764d90fa32ca1a6535dccd8316e5
# Always ensure the versions are supported, see https://apps.nextcloud.com/apps/calendar
calendar_ver=4.6.6
calendar_hash=e34a71669a52d997e319d64a984dcd041389eb22
# Always ensure the versions are supported, see https://apps.nextcloud.com/apps/user_external
user_external_ver=3.2.0
user_external_hash=a494073dcdecbbbc79a9c77f72524ac9994d2eec
# Developer advice (test plan)
# ----------------------------
# When upgrading above versions, how to test?
#
# 1. Enter your server instance (or on the Vagrant image)
# 1. Git clone <your fork>
# 2. Git checkout <your fork>
# 3. Run `sudo ./setup/nextcloud.sh`
# 4. Ensure the installation completes. If any hashes mismatch, correct them.
# 5. Enter nextcloud web, run following tests:
# 5.1 You still can create, edit and delete contacts
# 5.2 You still can create, edit and delete calendar events
# 5.3 You still can create, edit and delete users
# 5.4 Go to Administration > Logs and ensure no new errors are shown
# Clear prior packages and install dependencies from apt.
apt-get purge -qq -y owncloud* # we used to use the package manager
apt_install curl php"${PHP_VER}" php"${PHP_VER}"-fpm \
php"${PHP_VER}"-cli php"${PHP_VER}"-sqlite3 php"${PHP_VER}"-gd php"${PHP_VER}"-imap php"${PHP_VER}"-curl \
php"${PHP_VER}"-dev php"${PHP_VER}"-gd php"${PHP_VER}"-xml php"${PHP_VER}"-mbstring php"${PHP_VER}"-zip php"${PHP_VER}"-apcu \
php"${PHP_VER}"-intl php"${PHP_VER}"-imagick php"${PHP_VER}"-gmp php"${PHP_VER}"-bcmath
# Enable APC before Nextcloud tools are run.
tools/editconf.py /etc/php/"$PHP_VER"/mods-available/apcu.ini -c ';' \
apc.enabled=1 \
apc.enable_cli=1
InstallNextcloud() {
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
wget_verify "https://github.com/nextcloud-releases/contacts/archive/refs/tags/v$version_contacts.tar.gz" "$hash_contacts" /tmp/contacts.tgz
tar xf /tmp/contacts.tgz -C /usr/local/lib/owncloud/apps/
rm /tmp/contacts.tgz
wget_verify "https://github.com/nextcloud-releases/calendar/archive/refs/tags/v$version_calendar.tar.gz" "$hash_calendar" /tmp/calendar.tgz
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
wget_verify "https://github.com/nextcloud-releases/user_external/releases/download/v$version_user_external/user_external-v$version_user_external.tar.gz" "$hash_user_external" /tmp/user_external.tgz
tar -xf /tmp/user_external.tgz -C /usr/local/lib/owncloud/apps/
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"$PHP_VER" /usr/local/lib/owncloud/occ upgrade
E=$?
if [ $E -ne 0 ] && [ $E -ne 3 ]; then
echo "Trying ownCloud upgrade again to work around ownCloud upgrade bug..."
sudo -u www-data php"$PHP_VER" /usr/local/lib/owncloud/occ upgrade
E=$?
if [ $E -ne 0 ] && [ $E -ne 3 ]; then exit 1; fi
sudo -u www-data php"$PHP_VER" /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"$PHP_VER" /usr/local/lib/owncloud/occ db:add-missing-indices
sudo -u www-data php"$PHP_VER" /usr/local/lib/owncloud/occ db:add-missing-primary-keys
# Run conversion to BigInt identifiers, this process may take some time on large tables.
sudo -u www-data php"$PHP_VER" /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.
if [ -f "$STORAGE_ROOT/owncloud/config.php" ]; then
CURRENT_NEXTCLOUD_VER=$(php"$PHP_VER" -r "include(\"$STORAGE_ROOT/owncloud/config.php\"); echo(\$CONFIG['version']);")
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 they are not running (which happens on a previously failed install), dont bail.
service php"$PHP_VER"-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 [ -n "${CURRENT_NEXTCLOUD_VER}" ]; then
# Database migrations from ownCloud are no longer possible because ownCloud cannot be run under
# PHP 7.
if [ -e "$STORAGE_ROOT/owncloud/config.php" ]; then
# Remove the read-onlyness of the config, which is needed for migrations, especially for v24
sed -i -e '/config_is_read_only/d' "$STORAGE_ROOT/owncloud/config.php"
fi
if [[ ${CURRENT_NEXTCLOUD_VER} =~ ^[89] ]]; then
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} =~ ^1[3456789] ]]; then
echo "Upgrades from Mail-in-a-Box prior to v60 with Nextcloud 19 or earlier are not supported. Upgrade to the latest Mail-in-a-Box version supported on your machine first. Setup will continue, but skip the Nextcloud migration."
return 0
fi
# Hint: whenever you bump, remember this:
# - Run a server with the previous version
# - On a new if-else block, copy the versions/hashes from the previous version
# - Run sudo ./setup/start.sh on the new machine. Upon completion, test its basic functionalities.
if [[ ${CURRENT_NEXTCLOUD_VER} =~ ^20 ]]; then
InstallNextcloud 21.0.7 f5c7079c5b56ce1e301c6a27c0d975d608bb01c9 4.0.7 45e7cf4bfe99cd8d03625cf9e5a1bb2e90549136 3.0.4 d0284b68135777ec9ca713c307216165b294d0fe
CURRENT_NEXTCLOUD_VER="21.0.7"
fi
if [[ ${CURRENT_NEXTCLOUD_VER} =~ ^21 ]]; then
InstallNextcloud 22.2.6 9d39741f051a8da42ff7df46ceef2653a1dc70d9 4.1.0 697f6b4a664e928d72414ea2731cb2c9d1dc3077 3.2.2 ce4030ab57f523f33d5396c6a81396d440756f5f 3.0.0 0df781b261f55bbde73d8c92da3f99397000972f
CURRENT_NEXTCLOUD_VER="22.2.6"
fi
if [[ ${CURRENT_NEXTCLOUD_VER} =~ ^22 ]]; then
InstallNextcloud 23.0.12 d138641b8e7aabebe69bb3ec7c79a714d122f729 4.1.0 697f6b4a664e928d72414ea2731cb2c9d1dc3077 3.2.2 ce4030ab57f523f33d5396c6a81396d440756f5f 3.0.0 0df781b261f55bbde73d8c92da3f99397000972f
CURRENT_NEXTCLOUD_VER="23.0.12"
fi
if [[ ${CURRENT_NEXTCLOUD_VER} =~ ^23 ]]; then
InstallNextcloud 24.0.12 7aa5d61632c1ccf4ca3ff00fb6b295d318c05599 4.1.0 697f6b4a664e928d72414ea2731cb2c9d1dc3077 3.2.2 ce4030ab57f523f33d5396c6a81396d440756f5f 3.0.0 0df781b261f55bbde73d8c92da3f99397000972f
CURRENT_NEXTCLOUD_VER="24.0.12"
fi
if [[ ${CURRENT_NEXTCLOUD_VER} =~ ^24 ]]; then
InstallNextcloud 25.0.7 a5a565c916355005c7b408dd41a1e53505e1a080 5.3.0 4b0a6666374e3b55cfd2ae9b72e1d458b87d4c8c 4.4.2 21a42e15806adc9b2618760ef94f1797ef399e2f 3.2.0 a494073dcdecbbbc79a9c77f72524ac9994d2eec
CURRENT_NEXTCLOUD_VER="25.0.7"
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',
# 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 || exit; sudo -u www-data php"$PHP_VER" /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)
CONFIG_TEMP=$(/bin/mktemp)
php"$PHP_VER" <<EOF > "$CONFIG_TEMP" && mv "$CONFIG_TEMP" "$STORAGE_ROOT/owncloud/config.php";
<?php
include("$STORAGE_ROOT/owncloud/config.php");
\$CONFIG['config_is_read_only'] = false;
\$CONFIG['trusted_domains'] = array('$PRIMARY_HOSTNAME');
\$CONFIG['memcache.local'] = '\OC\Memcache\APCu';
\$CONFIG['overwrite.cli.url'] = 'https://${PRIMARY_HOSTNAME}/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"$PHP_VER" /usr/local/lib/owncloud/console.php app:disable firstrunwizard
hide_output sudo -u www-data php"$PHP_VER" /usr/local/lib/owncloud/console.php app:enable user_external
hide_output sudo -u www-data php"$PHP_VER" /usr/local/lib/owncloud/console.php app:enable contacts
hide_output sudo -u www-data php"$PHP_VER" /usr/local/lib/owncloud/console.php app:enable calendar
# When upgrading, run the upgrade script again now that apps are enabled. It seems like
# 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"$PHP_VER" /usr/local/lib/owncloud/occ upgrade
E=$?
if [ $E -ne 0 ] && [ $E -ne 3 ]; then exit 1; fi
# Disable default apps that we don't support
sudo -u www-data \
php"$PHP_VER" /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)
tools/editconf.py /etc/php/"$PHP_VER"/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
tools/editconf.py /etc/php/"$PHP_VER"/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 general cron job for Nextcloud.
# Also add another job for Calendar updates, per advice in the Nextcloud docs
# https://docs.nextcloud.com/server/24/admin_manual/groupware/calendar.html#background-jobs
cat > /etc/cron.d/mailinabox-nextcloud << EOF;
#!/bin/bash
# Mail-in-a-Box
*/5 * * * * root sudo -u www-data php$PHP_VER -f /usr/local/lib/owncloud/cron.php
*/5 * * * * root sudo -u www-data php$PHP_VER -f /usr/local/lib/owncloud/occ dav:send-event-reminders
EOF
chmod +x /etc/cron.d/mailinabox-nextcloud
# We also need to change the sending mode from background-job to occ.
# Or else the reminders will just be sent as soon as possible when the background jobs run.
hide_output sudo -u www-data php"$PHP_VER" -f /usr/local/lib/owncloud/occ config:app:set dav sendEventRemindersMode --value occ
# Now set the config to read-only.
# Do this only at the very bottom when no further occ commands are needed.
sed -i'' "s/'config_is_read_only'\s*=>\s*false/'config_is_read_only' => true/" "$STORAGE_ROOT/owncloud/config.php"
# Rotate the nextcloud.log file
cat > /etc/logrotate.d/nextcloud <<EOF
# Nextcloud logs
$STORAGE_ROOT/owncloud/nextcloud.log {
size 10M
create 640 www-data www-data
rotate 30
copytruncate
missingok
compress
}
EOF
# There's nothing much of interest that a user could do as an admin for Nextcloud,
# 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_VER"-fpm

View File

@ -1,188 +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=8.0.4
owncloud_hash=625b1c561ea51426047a3e79eda51ca05e9f978a
# Migrate <= v0.10 setups that stored the ownCloud config.php in /usr/local rather than
# in STORAGE_ROOT. Move the file to STORAGE_ROOT.
if [ ! -f $STORAGE_ROOT/owncloud/config.php ] \
&& [ -f /usr/local/lib/owncloud/config/config.php ]; then
# Move config.php and symlink back into previous location.
echo "Migrating owncloud/config.php to new location."
mv /usr/local/lib/owncloud/config/config.php $STORAGE_ROOT/owncloud/config.php \
&& \
ln -sf $STORAGE_ROOT/owncloud/config.php /usr/local/lib/owncloud/config/config.php
fi
# Check if ownCloud dir exist, and check if version matches owncloud_ver (if either doesn't - install/upgrade)
if [ ! -d /usr/local/lib/owncloud/ ] \
|| ! grep -q $owncloud_ver /usr/local/lib/owncloud/version.php; then
# Download and verify
echo "installing ownCloud..."
wget_verify https://download.owncloud.org/community/owncloud-$owncloud_ver.zip $owncloud_hash /tmp/owncloud.zip
# Clear out the existing ownCloud.
if [ -d /usr/local/lib/owncloud/ ]; then
echo "upgrading ownCloud to $owncloud_ver (backing up existing ownCloud directory to /tmp/owncloud-backup-$$)..."
mv /usr/local/lib/owncloud /tmp/owncloud-backup-$$
fi
# Extract ownCloud
unzip -u -o -q /tmp/owncloud.zip -d /usr/local/lib #either extracts new or replaces current files
rm -f /tmp/owncloud.zip
# The two apps we actually want are not in ownCloud core. Clone them from
# their github repositories.
mkdir -p /usr/local/lib/owncloud/apps
git_clone https://github.com/owncloud/contacts v$owncloud_ver '' /usr/local/lib/owncloud/apps/contacts
git_clone https://github.com/owncloud/calendar v$owncloud_ver '' /usr/local/lib/owncloud/apps/calendar
# 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
# Run the upgrade script (if ownCloud is already up-to-date it wont matter).
hide_output sudo -u www-data php /usr/local/lib/owncloud/occ upgrade
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 user data directory
mkdir -p $STORAGE_ROOT/owncloud
# Create a configuration file.
TIMEZONE=$(cat /etc/timezone)
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',
'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
# Set permissions
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 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
# 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. 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
# 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

View File

@ -1,35 +1,67 @@
#!/bin/bash
# Are we running as root?
if [[ $EUID -ne 0 ]]; then
echo "This script must be run as root. Please re-run like this:"
echo
echo "sudo $0"
echo
exit
exit 1
fi
# Check that we are running on Ubuntu 14.04 LTS (or 14.04.xx).
if [ "`lsb_release -d | sed 's/.*:\s*//' | sed 's/14\.04\.[0-9]/14.04/' `" != "Ubuntu 14.04 LTS" ]; then
echo "Mail-in-a-Box only supports being installed on Ubuntu 14.04, sorry. You are running:"
# Check that we are running on Ubuntu 20.04 LTS (or 20.04.xx).
if [ "$( lsb_release --id --short )" != "Ubuntu" ] || [ "$( lsb_release --release --short )" != "22.04" ]; then
echo "Mail-in-a-Box only supports being installed on Ubuntu 22.04, sorry. You are running:"
echo
lsb_release -d | sed 's/.*:\s*//'
lsb_release --description --short
echo
echo "We can't write scripts that run on every possible setup, sorry."
exit
exit 1
fi
# Check that we have enough memory.
#
# /proc/meminfo reports free memory in kibibytes. Our baseline will be 768 KB,
# which is 750000 kibibytes.
# /proc/meminfo reports free memory in kibibytes. Our baseline will be 512 MB,
# which is 500000 kibibytes.
#
# We will display a warning if the memory is below 768 MB which is 750000 kibibytes
#
# Skip the check if we appear to be running inside of Vagrant, because that's really just for testing.
TOTAL_PHYSICAL_MEM=$(head -n 1 /proc/meminfo | awk '{print $2}')
if [ $TOTAL_PHYSICAL_MEM -lt 750000 ]; then
if [ "$TOTAL_PHYSICAL_MEM" -lt 490000 ]; then
if [ ! -d /vagrant ]; then
TOTAL_PHYSICAL_MEM=$(expr \( \( $TOTAL_PHYSICAL_MEM \* 1024 \) / 1000 \) / 1000)
TOTAL_PHYSICAL_MEM=$(( TOTAL_PHYSICAL_MEM * 1024 / 1000 / 1000 ))
echo "Your Mail-in-a-Box needs more memory (RAM) to function properly."
echo "Please provision a machine with at least 768 MB, 1 GB recommended."
echo "Please provision a machine with at least 512 MB, 1 GB recommended."
echo "This machine has $TOTAL_PHYSICAL_MEM MB memory."
exit
fi
fi
if [ "$TOTAL_PHYSICAL_MEM" -lt 750000 ]; then
echo "WARNING: Your Mail-in-a-Box has less than 768 MB of memory."
echo " It might run unreliably when under heavy load."
fi
# Check that tempfs is mounted with exec
MOUNTED_TMP_AS_NO_EXEC=$(grep "/tmp.*noexec" /proc/mounts || /bin/true)
if [ -n "$MOUNTED_TMP_AS_NO_EXEC" ]; then
echo "Mail-in-a-Box has to have exec rights on /tmp, please mount /tmp with exec"
exit
fi
# Check that no .wgetrc exists
if [ -e ~/.wgetrc ]; then
echo "Mail-in-a-Box expects no overrides to wget defaults, ~/.wgetrc exists"
exit
fi
# Check that we are running on x86_64 or i686 architecture, which are the only
# ones we support / test.
ARCHITECTURE=$(uname -m)
if [ "$ARCHITECTURE" != "x86_64" ] && [ "$ARCHITECTURE" != "i686" ]; then
echo
echo "WARNING:"
echo "Mail-in-a-Box has only been tested on x86_64 and i686 platform"
echo "architectures. Your architecture, $ARCHITECTURE, may not work."
echo "You are on your own."
echo
fi

View File

@ -1,34 +1,38 @@
if [ -z "$NONINTERACTIVE" ]; then
#!/bin/bash
if [ -z "${NONINTERACTIVE:-}" ]; then
# Install 'dialog' so we can ask the user questions. The original motivation for
# this was being able to ask the user for input even if stdin has been redirected,
# e.g. if we piped a bootstrapping install script to bash to get started. In that
# case, the nifty '[ -t 0 ]' test won't work. But with Vagrant we must suppress so we
# use a shell flag instead. Really supress any output from installing dialog.
# use a shell flag instead. Really suppress any output from installing dialog.
#
# Also install depencies needed to validate the email address.
# Also install dependencies needed to validate the email address.
if [ ! -f /usr/bin/dialog ] || [ ! -f /usr/bin/python3 ] || [ ! -f /usr/bin/pip3 ]; then
echo Installing packages needed for setup...
echo "Installing packages needed for setup..."
apt-get -q -q update
apt_get_quiet install dialog python3 python3-pip || exit 1
fi
# email_validator is repeated in setup/management.sh
hide_output pip3 install email_validator || exit 1
# Installing email_validator is repeated in setup/management.sh, but in setup/management.sh
# we install it inside a virtualenv. In this script, we don't have the virtualenv yet
# so we install the python package globally.
hide_output pip3 install "email_validator>=1.0.0" || exit 1
message_box "Mail-in-a-Box Installation" \
"Hello and thanks for deploying a Mail-in-a-Box!
\n\nI'm going to ask you a few questions.
\n\nTo change your answers later, just run 'sudo mailinabox' from the command line."
\n\nTo change your answers later, just run 'sudo mailinabox' from the command line.
\n\nNOTE: You should only install this on a brand new Ubuntu installation 100% dedicated to Mail-in-a-Box. Mail-in-a-Box will, for example, remove apache2."
fi
# The box needs a name.
if [ -z "$PRIMARY_HOSTNAME" ]; then
if [ -z "$DEFAULT_PRIMARY_HOSTNAME" ]; then
if [ -z "${PRIMARY_HOSTNAME:-}" ]; then
if [ -z "${DEFAULT_PRIMARY_HOSTNAME:-}" ]; then
# We recommend to use box.example.com as this hosts name. The
# domain the user possibly wants to use is example.com then.
# We strip the string "box." from the hostname to get the mail
# domain. If the hostname differs, nothing happens here.
DEFAULT_DOMAIN_GUESS=$(echo $(get_default_hostname) | sed -e 's/^box\.//')
DEFAULT_DOMAIN_GUESS=$(get_default_hostname | sed -e 's/^box\.//')
# This is the first run. Ask the user for his email address so we can
# provide the best default for the box's hostname.
@ -48,11 +52,11 @@ you really want.
# user hit ESC/cancel
exit
fi
while ! management/mailconfig.py validate-email "$EMAIL_ADDR"
while ! python3 management/mailconfig.py validate-email "$EMAIL_ADDR"
do
input_box "Your Email Address" \
"That's not a valid email address.\n\nWhat email address are you setting this box up to manage?" \
$EMAIL_ADDR \
"$EMAIL_ADDR" \
EMAIL_ADDR
if [ -z "$EMAIL_ADDR" ]; then
# user hit ESC/cancel
@ -62,7 +66,7 @@ you really want.
# Take the part after the @-sign as the user's domain name, and add
# 'box.' to the beginning to create a default hostname for this machine.
DEFAULT_PRIMARY_HOSTNAME=box.$(echo $EMAIL_ADDR | sed 's/.*@//')
DEFAULT_PRIMARY_HOSTNAME=box.$(echo "$EMAIL_ADDR" | sed 's/.*@//')
fi
input_box "Hostname" \
@ -71,7 +75,7 @@ you really want.
address, so we're suggesting $DEFAULT_PRIMARY_HOSTNAME.
\n\nYou can change it, but we recommend you don't.
\n\nHostname:" \
$DEFAULT_PRIMARY_HOSTNAME \
"$DEFAULT_PRIMARY_HOSTNAME" \
PRIMARY_HOSTNAME
if [ -z "$PRIMARY_HOSTNAME" ]; then
@ -83,30 +87,30 @@ fi
# If the machine is behind a NAT, inside a VM, etc., it may not know
# its IP address on the public network / the Internet. Ask the Internet
# and possibly confirm with user.
if [ -z "$PUBLIC_IP" ]; then
if [ -z "${PUBLIC_IP:-}" ]; then
# Ask the Internet.
GUESSED_IP=$(get_publicip_from_web_service 4)
# On the first run, if we got an answer from the Internet then don't
# ask the user.
if [[ -z "$DEFAULT_PUBLIC_IP" && ! -z "$GUESSED_IP" ]]; then
if [[ -z "${DEFAULT_PUBLIC_IP:-}" && -n "$GUESSED_IP" ]]; then
PUBLIC_IP=$GUESSED_IP
# Otherwise on the first run at least provide a default.
elif [[ -z "$DEFAULT_PUBLIC_IP" ]]; then
elif [[ -z "${DEFAULT_PUBLIC_IP:-}" ]]; then
DEFAULT_PUBLIC_IP=$(get_default_privateip 4)
# On later runs, if the previous value matches the guessed value then
# don't ask the user either.
elif [ "$DEFAULT_PUBLIC_IP" == "$GUESSED_IP" ]; then
elif [ "${DEFAULT_PUBLIC_IP:-}" == "$GUESSED_IP" ]; then
PUBLIC_IP=$GUESSED_IP
fi
if [ -z "$PUBLIC_IP" ]; then
if [ -z "${PUBLIC_IP:-}" ]; then
input_box "Public IP Address" \
"Enter the public IP address of this machine, as given to you by your ISP.
\n\nPublic IP address:" \
$DEFAULT_PUBLIC_IP \
"${DEFAULT_PUBLIC_IP:-}" \
PUBLIC_IP
if [ -z "$PUBLIC_IP" ]; then
@ -118,30 +122,30 @@ fi
# Same for IPv6. But it's optional. Also, if it looks like the system
# doesn't have an IPv6, don't ask for one.
if [ -z "$PUBLIC_IPV6" ]; then
if [ -z "${PUBLIC_IPV6:-}" ]; then
# Ask the Internet.
GUESSED_IP=$(get_publicip_from_web_service 6)
MATCHED=0
if [[ -z "$DEFAULT_PUBLIC_IPV6" && ! -z "$GUESSED_IP" ]]; then
if [[ -z "${DEFAULT_PUBLIC_IPV6:-}" && -n "$GUESSED_IP" ]]; then
PUBLIC_IPV6=$GUESSED_IP
elif [[ "$DEFAULT_PUBLIC_IPV6" == "$GUESSED_IP" ]]; then
elif [[ "${DEFAULT_PUBLIC_IPV6:-}" == "$GUESSED_IP" ]]; then
# No IPv6 entered and machine seems to have none, or what
# the user entered matches what the Internet tells us.
PUBLIC_IPV6=$GUESSED_IP
MATCHED=1
elif [[ -z "$DEFAULT_PUBLIC_IPV6" ]]; then
elif [[ -z "${DEFAULT_PUBLIC_IPV6:-}" ]]; then
DEFAULT_PUBLIC_IP=$(get_default_privateip 6)
fi
if [[ -z "$PUBLIC_IPV6" && $MATCHED == 0 ]]; then
if [[ -z "${PUBLIC_IPV6:-}" && $MATCHED == 0 ]]; then
input_box "IPv6 Address (Optional)" \
"Enter the public IPv6 address of this machine, as given to you by your ISP.
\n\nLeave blank if the machine does not have an IPv6 address.
\n\nPublic IPv6 address:" \
$DEFAULT_PUBLIC_IPV6 \
"${DEFAULT_PUBLIC_IPV6:-}" \
PUBLIC_IPV6
if [ ! $PUBLIC_IPV6_EXITCODE ]; then
if [ ! -n "$PUBLIC_IPV6_EXITCODE" ]; then
# user hit ESC/cancel
exit
fi
@ -151,10 +155,10 @@ fi
# Get the IP addresses of the local network interface(s) that are connected
# to the Internet. We need these when we want to have services bind only to
# the public network interfaces (not loopback, not tunnel interfaces).
if [ -z "$PRIVATE_IP" ]; then
if [ -z "${PRIVATE_IP:-}" ]; then
PRIVATE_IP=$(get_default_privateip 4)
fi
if [ -z "$PRIVATE_IPV6" ]; then
if [ -z "${PRIVATE_IPV6:-}" ]; then
PRIVATE_IPV6=$(get_default_privateip 6)
fi
if [[ -z "$PRIVATE_IP" && -z "$PRIVATE_IPV6" ]]; then
@ -168,35 +172,6 @@ if [[ -z "$PRIVATE_IP" && -z "$PRIVATE_IPV6" ]]; then
exit
fi
# We need a country code to generate a certificate signing request. However
# if a CSR already exists then we won't be generating a new one and there's
# no reason to ask for the country code now. $STORAGE_ROOT has not yet been
# set so we'll check if $DEFAULT_STORAGE_ROOT and $DEFAULT_CSR_COUNTRY are
# set (the values from the current mailinabox.conf) and if the CSR exists
# in the expected location.
if [ ! -z "$DEFAULT_STORAGE_ROOT" ] && [ ! -z "$DEFAULT_CSR_COUNTRY" ] && [ -f $DEFAULT_STORAGE_ROOT/ssl/ssl_cert_sign_req.csr ]; then
CSR_COUNTRY=$DEFAULT_CSR_COUNTRY
fi
if [ -z "$CSR_COUNTRY" ]; then
# Get a list of country codes. Separate codes from country names with a ^.
# The input_menu function modifies shell word expansion to ignore spaces
# (since country names can have spaces) and use ^ instead.
country_code_list=$(grep -v "^#" setup/csr_country_codes.tsv | sed "s/\(..\)\t\([^\t]*\).*/\1^\2/")
input_menu "Country Code" \
"Choose the country where you live or where your organization is based.
\n\n(This is used to create an SSL certificate.)
\n\nCountry Code:" \
"$country_code_list" \
CSR_COUNTRY
if [ -z "$CSR_COUNTRY" ]; then
# user hit ESC/cancel
exit
fi
fi
# Automatic configuration, e.g. as used in our Vagrant configuration.
if [ "$PUBLIC_IP" = "auto" ]; then
# Use a public API to get our public IP address, or fall back to local network configuration.
@ -207,28 +182,23 @@ if [ "$PUBLIC_IPV6" = "auto" ]; then
PUBLIC_IPV6=$(get_publicip_from_web_service 6 || get_default_privateip 6)
fi
if [ "$PRIMARY_HOSTNAME" = "auto" ]; then
# Use reverse DNS to get this machine's hostname. Install bind9-host early.
hide_output apt-get -y install bind9-host
PRIMARY_HOSTNAME=$(get_default_hostname)
elif [ "$PRIMARY_HOSTNAME" = "auto-easy" ]; then
# Generate a probably-unique subdomain under our justtesting.email domain.
PRIMARY_HOSTNAME=`echo $PUBLIC_IP | sha1sum | cut -c1-5`.justtesting.email
fi
# Set STORAGE_USER and STORAGE_ROOT to default values (user-data and /home/user-data), unless
# we've already got those values from a previous run.
if [ -z "$STORAGE_USER" ]; then
STORAGE_USER=$([[ -z "$DEFAULT_STORAGE_USER" ]] && echo "user-data" || echo "$DEFAULT_STORAGE_USER")
if [ -z "${STORAGE_USER:-}" ]; then
STORAGE_USER=$([[ -z "${DEFAULT_STORAGE_USER:-}" ]] && echo "user-data" || echo "$DEFAULT_STORAGE_USER")
fi
if [ -z "$STORAGE_ROOT" ]; then
STORAGE_ROOT=$([[ -z "$DEFAULT_STORAGE_ROOT" ]] && echo "/home/$STORAGE_USER" || echo "$DEFAULT_STORAGE_ROOT")
if [ -z "${STORAGE_ROOT:-}" ]; then
STORAGE_ROOT=$([[ -z "${DEFAULT_STORAGE_ROOT:-}" ]] && echo "/home/$STORAGE_USER" || echo "$DEFAULT_STORAGE_ROOT")
fi
# Show the configuration, since the user may have not entered it manually.
echo
echo "Primary Hostname: $PRIMARY_HOSTNAME"
echo "Public IP Address: $PUBLIC_IP"
if [ ! -z "$PUBLIC_IPV6" ]; then
if [ -n "$PUBLIC_IPV6" ]; then
echo "Public IPv6 Address: $PUBLIC_IPV6"
fi
if [ "$PRIVATE_IP" != "$PUBLIC_IP" ]; then
@ -238,6 +208,6 @@ if [ "$PRIVATE_IPV6" != "$PUBLIC_IPV6" ]; then
echo "Private IPv6 Address: $PRIVATE_IPV6"
fi
if [ -f /usr/bin/git ] && [ -d .git ]; then
echo "Mail-in-a-Box Version: " $(git describe)
echo "Mail-in-a-Box Version: $(git describe --always)"
fi
echo

View File

@ -16,23 +16,40 @@ source setup/functions.sh # load our functions
# ----------------------------------------
# Install packages.
apt_install spampd razor pyzor dovecot-antispam
# libmail-dkim-perl is needed to make the spamassassin DKIM module work.
# For more information see Debian Bug #689414:
# https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=689414
echo "Installing SpamAssassin..."
apt_install spampd razor pyzor dovecot-antispam libmail-dkim-perl
# Allow spamassassin to download new rules.
tools/editconf.py /etc/default/spamassassin \
CRON=1
# Configure pyzor.
hide_output pyzor discover
# Configure pyzor, which is a client to a live database of hashes of
# spam emails. Set the pyzor configuration directory to something sane.
# The default is ~/.pyzor. We used to use that, so we'll kill that old
# directory. Then write the public pyzor server to its servers file.
# That will prevent an automatic download on first use, and also means
# we can skip 'pyzor discover', both of which are currently broken by
# something happening on Sourceforge (#496).
rm -rf ~/.pyzor
tools/editconf.py /etc/spamassassin/local.cf -s \
pyzor_options="--homedir /etc/spamassassin/pyzor"
mkdir -p /etc/spamassassin/pyzor
echo "public.pyzor.org:24441" > /etc/spamassassin/pyzor/servers
# check with: pyzor --homedir /etc/mail/spamassassin/pyzor ping
# Configure spampd:
# * Pass messages on to docevot on port 10026. This is actually the default setting but we don't
# want to lose track of it. (We've configured Dovecot to listen on this port elsewhere.)
# * Increase the maximum message size of scanned messages from the default of 64KB to 500KB, which
# is Spamassassin (spamc)'s own default. Specified in KBytes.
# * Disable localmode so Pyzor, DKIM and DNS checks can be used.
tools/editconf.py /etc/default/spampd \
DESTPORT=10026 \
ADDOPTS="\"--maxsize=500\""
ADDOPTS="\"--maxsize=2000\"" \
LOCALONLY=0
# Spamassassin normally wraps spam as an attachment inside a fresh
# email with a report about the message. This also protects the user
@ -44,9 +61,61 @@ tools/editconf.py /etc/default/spampd \
# content or execute scripts, and it is probably confusing to most users.
#
# Tell Spamassassin not to modify the original message except for adding
# the X-Spam-Status mail header and related headers.
# the X-Spam-Status & X-Spam-Score mail headers and related headers.
tools/editconf.py /etc/spamassassin/local.cf -s \
report_safe=0
report_safe=0 \
"add_header all Report"=_REPORT_ \
"add_header all Score"=_SCORE_
# Authentication-Results SPF/Dmarc checks
# ---------------------------------------
# OpenDKIM and OpenDMARC are configured to validate and add "Authentication-Results: ..."
# headers by checking the sender's SPF & DMARC policies. Instead of blocking mail that fails
# these checks, we can use these headers to evaluate the mail as spam.
#
# Our custom rules are added to their own file so that an update to the deb package config
# does not remove our changes.
#
# We need to escape period's in $PRIMARY_HOSTNAME since spamassassin config uses regex.
escapedprimaryhostname="${PRIMARY_HOSTNAME//./\\.}"
cat > /etc/spamassassin/miab_spf_dmarc.cf << EOF
# Evaluate DMARC Authentication-Results
header DMARC_PASS Authentication-Results =~ /$escapedprimaryhostname; dmarc=pass/
describe DMARC_PASS DMARC check passed
score DMARC_PASS -0.1
header DMARC_NONE Authentication-Results =~ /$escapedprimaryhostname; dmarc=none/
describe DMARC_NONE DMARC record not found
score DMARC_NONE 0.1
header DMARC_FAIL_NONE Authentication-Results =~ /$escapedprimaryhostname; dmarc=fail \(p=none/
describe DMARC_FAIL_NONE DMARC check failed (p=none)
score DMARC_FAIL_NONE 2.0
header DMARC_FAIL_QUARANTINE Authentication-Results =~ /$escapedprimaryhostname; dmarc=fail \(p=quarantine/
describe DMARC_FAIL_QUARANTINE DMARC check failed (p=quarantine)
score DMARC_FAIL_QUARANTINE 5.0
header DMARC_FAIL_REJECT Authentication-Results =~ /$escapedprimaryhostname; dmarc=fail \(p=reject/
describe DMARC_FAIL_REJECT DMARC check failed (p=reject)
score DMARC_FAIL_REJECT 10.0
# Evaluate SPF Authentication-Results
header SPF_PASS Authentication-Results =~ /$escapedprimaryhostname; spf=pass/
describe SPF_PASS SPF check passed
score SPF_PASS -0.1
header SPF_NONE Authentication-Results =~ /$escapedprimaryhostname; spf=none/
describe SPF_NONE SPF record not found
score SPF_NONE 2.0
header SPF_FAIL Authentication-Results =~ /$escapedprimaryhostname; spf=fail/
describe SPF_FAIL SPF check failed
score SPF_FAIL 5.0
EOF
# Bayesean learning
# -----------------
@ -61,12 +130,16 @@ tools/editconf.py /etc/spamassassin/local.cf -s \
# * Writable by the debian-spamd user, which runs /etc/cron.daily/spamassassin.
#
# We'll have these files owned by spampd and grant access to the other two processes.
#
# Spamassassin will change the access rights back to the defaults, so we must also configure
# the filemode in the config file.
tools/editconf.py /etc/spamassassin/local.cf -s \
bayes_path=$STORAGE_ROOT/mail/spamassassin/bayes
bayes_path="$STORAGE_ROOT/mail/spamassassin/bayes" \
bayes_file_mode=0666
mkdir -p $STORAGE_ROOT/mail/spamassassin
chown -R spampd:spampd $STORAGE_ROOT/mail/spamassassin
mkdir -p "$STORAGE_ROOT/mail/spamassassin"
chown -R spampd:spampd "$STORAGE_ROOT/mail/spamassassin"
# To mark mail as spam or ham, just drag it in or out of the Spam folder. We'll
# use the Dovecot antispam plugin to detect the message move operation and execute
@ -82,6 +155,7 @@ cat > /etc/dovecot/conf.d/99-local-spampd.conf << EOF;
plugin {
antispam_backend = pipe
antispam_spam_pattern_ignorecase = SPAM
antispam_trash_pattern_ignorecase = trash;Deleted *
antispam_allow_append_to_spam = yes
antispam_pipe_program_spam_args = /usr/local/bin/sa-learn-pipe.sh;--spam
antispam_pipe_program_notspam_args = /usr/local/bin/sa-learn-pipe.sh;--ham
@ -110,8 +184,8 @@ chmod a+x /usr/local/bin/sa-learn-pipe.sh
# Create empty bayes training data (if it doesn't exist). Once the files exist,
# ensure they are group-writable so that the Dovecot process has access.
sudo -u spampd /usr/bin/sa-learn --sync 2>/dev/null
chmod -R 660 $STORAGE_ROOT/mail/spamassassin
chmod 770 $STORAGE_ROOT/mail/spamassassin
chmod -R 660 "$STORAGE_ROOT/mail/spamassassin"
chmod 770 "$STORAGE_ROOT/mail/spamassassin"
# Initial training?
# sa-learn --ham storage/mail/mailboxes/*/*/cur/

View File

@ -1,52 +1,98 @@
#!/bin/bash
#
# SSL Certificate
# ---------------
# RSA private key, SSL certificate, Diffie-Hellman bits files
# -------------------------------------------
# Create a self-signed SSL certificate if one has not yet been created.
# Create an RSA private key, a self-signed SSL certificate, and some
# Diffie-Hellman cipher bits, if they have not yet been created.
#
# The certificate is for PRIMARY_HOSTNAME specifically and is used for:
# The RSA private key and certificate are used for:
#
# * DNSSEC DANE TLSA records
# * IMAP
# * SMTP submission (port 587) and opportunistic TLS (when on the receiving end)
# * the DNSSEC DANE TLSA record for SMTP
# * HTTPS (for PRIMARY_HOSTNAME only)
# * SMTP (opportunistic TLS for port 25 and submission on ports 465/587)
# * HTTPS
#
# When other domains besides PRIMARY_HOSTNAME are served over HTTPS,
# we generate a domain-specific self-signed certificate in the management
# daemon (web_update.py) as needed.
# The certificate is created with its CN set to the PRIMARY_HOSTNAME. It is
# also used for other domains served over HTTPS until the user installs a
# better certificate for those domains.
#
# The Diffie-Hellman cipher bits are used for SMTP and HTTPS, when a
# Diffie-Hellman cipher is selected during TLS negotiation. Diffie-Hellman
# provides Perfect Forward Secrecy.
source setup/functions.sh # load our functions
source /etc/mailinabox.conf # load global vars
# Show a status line if we are going to take any action in this file.
if [ ! -f /usr/bin/openssl ] \
|| [ ! -f "$STORAGE_ROOT/ssl/ssl_private_key.pem" ] \
|| [ ! -f "$STORAGE_ROOT/ssl/ssl_certificate.pem" ] \
|| [ ! -f "$STORAGE_ROOT/ssl/dh2048.pem" ]; then
echo "Creating initial SSL certificate and perfect forward secrecy Diffie-Hellman parameters..."
fi
# Install openssl.
apt_install openssl
mkdir -p $STORAGE_ROOT/ssl
# Create a directory to store TLS-related things like "SSL" certificates.
mkdir -p "$STORAGE_ROOT/ssl"
# Generate a new private key.
# Set the umask so the key file is not world-readable.
if [ ! -f $STORAGE_ROOT/ssl/ssl_private_key.pem ]; then
#
# The key is only as good as the entropy available to openssl so that it
# can generate a random key. "OpenSSLs built-in RSA key generator ....
# is seeded on first use with (on Linux) 32 bytes read from /dev/urandom,
# the process ID, user ID, and the current time in seconds. [During key
# generation OpenSSL] mixes into the entropy pool the current time in seconds,
# the process ID, and the possibly uninitialized contents of a ... buffer
# ... dozens to hundreds of times."
#
# A perfect storm of issues can cause the generated key to be not very random:
#
# * improperly seeded /dev/urandom, but see system.sh for how we mitigate this
# * the user ID of this process is always the same (we're root), so that seed is useless
# * zero'd memory (plausible on embedded systems, cloud VMs?)
# * a predictable process ID (likely on an embedded/virtualized system)
# * a system clock reset to a fixed time on boot
#
# Since we properly seed /dev/urandom in system.sh we should be fine, but I leave
# in the rest of the notes in case that ever changes.
if [ ! -f "$STORAGE_ROOT/ssl/ssl_private_key.pem" ]; then
# Set the umask so the key file is never world-readable.
(umask 077; hide_output \
openssl genrsa -out $STORAGE_ROOT/ssl/ssl_private_key.pem 2048)
openssl genrsa -out "$STORAGE_ROOT/ssl/ssl_private_key.pem" 2048)
fi
# Generate a certificate signing request.
if [ ! -f $STORAGE_ROOT/ssl/ssl_cert_sign_req.csr ]; then
# Generate a self-signed SSL certificate because things like nginx, dovecot,
# etc. won't even start without some certificate in place, and we need nginx
# so we can offer the user a control panel to install a better certificate.
if [ ! -f "$STORAGE_ROOT/ssl/ssl_certificate.pem" ]; then
# Generate a certificate signing request.
CSR=/tmp/ssl_cert_sign_req-$$.csr
hide_output \
openssl req -new -key $STORAGE_ROOT/ssl/ssl_private_key.pem -out $STORAGE_ROOT/ssl/ssl_cert_sign_req.csr \
-sha256 -subj "/C=$CSR_COUNTRY/ST=/L=/O=/CN=$PRIMARY_HOSTNAME"
fi
openssl req -new -key "$STORAGE_ROOT/ssl/ssl_private_key.pem" -out $CSR \
-sha256 -subj "/CN=$PRIMARY_HOSTNAME"
# Generate a SSL certificate by self-signing.
if [ ! -f $STORAGE_ROOT/ssl/ssl_certificate.pem ]; then
# Generate the self-signed certificate.
CERT=$STORAGE_ROOT/ssl/$PRIMARY_HOSTNAME-selfsigned-$(date --rfc-3339=date | sed s/-//g).pem
hide_output \
openssl x509 -req -days 365 \
-in $STORAGE_ROOT/ssl/ssl_cert_sign_req.csr -signkey $STORAGE_ROOT/ssl/ssl_private_key.pem -out $STORAGE_ROOT/ssl/ssl_certificate.pem
-in $CSR -signkey "$STORAGE_ROOT/ssl/ssl_private_key.pem" -out "$CERT"
# Delete the certificate signing request because it has no other purpose.
rm -f $CSR
# Symlink the certificate into the system certificate path, so system services
# can find it.
ln -s "$CERT" "$STORAGE_ROOT/ssl/ssl_certificate.pem"
fi
# For nginx and postfix, pre-generate some Diffie-Hellman cipher bits which is
# used when a Diffie-Hellman cipher is selected during TLS negotiation. Diffie-Hellman
# provides Perfect Forward Secrecy. openssl's default is 1024 bits, but we'll
# create 2048.
if [ ! -f $STORAGE_ROOT/ssl/dh2048.pem ]; then
openssl dhparam -out $STORAGE_ROOT/ssl/dh2048.pem 2048
# Generate some Diffie-Hellman cipher bits.
# openssl's default bit length for this is 1024 bits, but we'll create
# 2048 bits of bits per the latest recommendations.
if [ ! -f "$STORAGE_ROOT/ssl/dh2048.pem" ]; then
openssl dhparam -out "$STORAGE_ROOT/ssl/dh2048.pem" 2048
fi

View File

@ -4,16 +4,17 @@
source setup/functions.sh # load our functions
# Check system setup: Are we running as root on Ubuntu 14.04 on a
# machine with enough memory? If not, this shows an error and exits.
# Check system setup: Are we running as root on Ubuntu 18.04 on a
# machine with enough memory? Is /tmp mounted with exec.
# If not, this shows an error and exits.
source setup/preflight.sh
# Ensure Python reads/writes files in UTF-8. If the machine
# triggers some other locale in Python, like ASCII encoding,
# Python may not be able to read/write files. Here and in
# the management daemon startup script.
# Python may not be able to read/write files. This is also
# in the management daemon startup script and the cron script.
if [ -z `locale -a | grep en_US.utf8` ]; then
if ! locale -a | grep en_US.utf8 > /dev/null; then
# Generate locale if not exists
hide_output locale-gen en_US.UTF-8
fi
@ -23,6 +24,9 @@ export LC_ALL=en_US.UTF-8
export LANG=en_US.UTF-8
export LC_TYPE=en_US.UTF-8
# Fix so line drawing characters are shown correctly in Putty on Windows. See #744.
export NCURSES_NO_UTF8_ACS=1
# Recall the last settings used if we're running this a second time.
if [ -f /etc/mailinabox.conf ]; then
# Run any system migrations before proceeding. Since this is a second run,
@ -34,47 +38,60 @@ if [ -f /etc/mailinabox.conf ]; then
cat /etc/mailinabox.conf | sed s/^/DEFAULT_/ > /tmp/mailinabox.prev.conf
source /tmp/mailinabox.prev.conf
rm -f /tmp/mailinabox.prev.conf
else
FIRST_TIME_SETUP=1
fi
# Put a start script in a global location. We tell the user to run 'mailinabox'
# in the first dialog prompt, so we should do this before that starts.
cat > /usr/local/bin/mailinabox << EOF;
#!/bin/bash
cd `pwd`
cd $PWD
source setup/start.sh
EOF
chmod +x /usr/local/bin/mailinabox
# Ask the user for the PRIMARY_HOSTNAME, PUBLIC_IP, PUBLIC_IPV6, and CSR_COUNTRY
# Ask the user for the PRIMARY_HOSTNAME, PUBLIC_IP, and PUBLIC_IPV6,
# if values have not already been set in environment variables. When running
# non-interactively, be sure to set values for all! Also sets STORAGE_USER and
# STORAGE_ROOT.
source setup/questions.sh
# Run some network checks to make sure setup on this machine makes sense.
if [ -z "$SKIP_NETWORK_CHECKS" ]; then
# Skip on existing installs since we don't want this to block the ability to
# upgrade, and these checks are also in the control panel status checks.
if [ -z "${DEFAULT_PRIMARY_HOSTNAME:-}" ]; then
if [ -z "${SKIP_NETWORK_CHECKS:-}" ]; then
source setup/network-checks.sh
fi
fi
# Create the STORAGE_USER and STORAGE_ROOT directory if they don't already exist.
#
# Set the directory and all of its parent directories' permissions to world
# readable since it holds files owned by different processes.
#
# If the STORAGE_ROOT is missing the mailinabox.version file that lists a
# migration (schema) number for the files stored there, assume this is a fresh
# installation to that directory and write the file to contain the current
# migration number for this version of Mail-in-a-Box.
if ! id -u $STORAGE_USER >/dev/null 2>&1; then
useradd -m $STORAGE_USER
if ! id -u "$STORAGE_USER" >/dev/null 2>&1; then
useradd -m "$STORAGE_USER"
fi
if [ ! -d $STORAGE_ROOT ]; then
mkdir -p $STORAGE_ROOT
if [ ! -d "$STORAGE_ROOT" ]; then
mkdir -p "$STORAGE_ROOT"
fi
if [ ! -f $STORAGE_ROOT/mailinabox.version ]; then
echo $(setup/migrate.py --current) > $STORAGE_ROOT/mailinabox.version
chown $STORAGE_USER.$STORAGE_USER $STORAGE_ROOT/mailinabox.version
f=$STORAGE_ROOT
while [[ $f != / ]]; do chmod a+rx "$f"; f=$(dirname "$f"); done;
if [ ! -f "$STORAGE_ROOT/mailinabox.version" ]; then
setup/migrate.py --current > "$STORAGE_ROOT/mailinabox.version"
chown "$STORAGE_USER:$STORAGE_USER" "$STORAGE_ROOT/mailinabox.version"
fi
# Save the global options in /etc/mailinabox.conf so that standalone
# tools know where to look for data.
# tools know where to look for data. The default MTA_STS_MODE setting
# is blank unless set by an environment variable, but see web.sh for
# how that is interpreted.
cat > /etc/mailinabox.conf << EOF;
STORAGE_USER=$STORAGE_USER
STORAGE_ROOT=$STORAGE_ROOT
@ -83,7 +100,7 @@ PUBLIC_IP=$PUBLIC_IP
PUBLIC_IPV6=$PUBLIC_IPV6
PRIVATE_IP=$PRIVATE_IP
PRIVATE_IPV6=$PRIVATE_IPV6
CSR_COUNTRY=$CSR_COUNTRY
MTA_STS_MODE=${DEFAULT_MTA_STS_MODE:-enforce}
EOF
# Start service configuration.
@ -97,46 +114,69 @@ source setup/dkim.sh
source setup/spamassassin.sh
source setup/web.sh
source setup/webmail.sh
source setup/owncloud.sh
source setup/nextcloud.sh
source setup/zpush.sh
source setup/management.sh
source setup/munin.sh
# Ping the management daemon to write the DNS and nginx configuration files.
until nc -z -w 4 localhost 10222
# Wait for the management daemon to start...
until nc -z -w 4 127.0.0.1 10222
do
echo Waiting for the Mail-in-a-Box management daemon to start...
echo "Waiting for the Mail-in-a-Box management daemon to start..."
sleep 2
done
# ...and then have it write the DNS and nginx configuration files and start those
# services.
tools/dns_update
tools/web_update
# Give fail2ban another restart. The log files may not all have been present when
# fail2ban was first configured, but they should exist now.
restart_service fail2ban
# If there aren't any mail users yet, create one.
source setup/firstuser.sh
# Register with Let's Encrypt, including agreeing to the Terms of Service.
# We'd let certbot ask the user interactively, but when this script is
# run in the recommended curl-pipe-to-bash method there is no TTY and
# certbot will fail if it tries to ask.
if [ ! -d "$STORAGE_ROOT/ssl/lets_encrypt/accounts/acme-v02.api.letsencrypt.org/" ]; then
echo
echo "-----------------------------------------------"
echo "Mail-in-a-Box uses Let's Encrypt to provision free SSL/TLS certificates"
echo "to enable HTTPS connections to your box. We're automatically"
echo "agreeing you to their subscriber agreement. See https://letsencrypt.org."
echo
certbot register --register-unsafely-without-email --agree-tos --config-dir "$STORAGE_ROOT/ssl/lets_encrypt"
fi
# Done.
echo
echo "-----------------------------------------------"
echo
echo Your Mail-in-a-Box is running.
echo "Your Mail-in-a-Box is running."
echo
echo Please log in to the control panel for further instructions at:
echo "Please log in to the control panel for further instructions at:"
echo
if management/status_checks.py --check-primary-hostname; then
# Show the nice URL if it appears to be resolving and has a valid certificate.
echo https://$PRIMARY_HOSTNAME/admin
echo "https://$PRIMARY_HOSTNAME/admin"
echo
echo If you have a DNS problem use the box\'s IP address and check the SSL fingerprint:
echo https://$PUBLIC_IP/admin
echo "If you have a DNS problem put the box's IP address in the URL"
echo "(https://$PUBLIC_IP/admin) but then check the TLS fingerprint:"
openssl x509 -in "$STORAGE_ROOT/ssl/ssl_certificate.pem" -noout -fingerprint -sha256\
| sed "s/SHA256 Fingerprint=//i"
else
echo https://$PUBLIC_IP/admin
echo "https://$PUBLIC_IP/admin"
echo
echo You will be alerted that the website has an invalid certificate. Check that
echo the certificate fingerprint matches:
echo "You will be alerted that the website has an invalid certificate. Check that"
echo "the certificate fingerprint matches:"
echo
openssl x509 -in "$STORAGE_ROOT/ssl/ssl_certificate.pem" -noout -fingerprint -sha256\
| sed "s/SHA256 Fingerprint=//i"
echo
echo "Then you can confirm the security exception and continue."
echo
fi
openssl x509 -in $STORAGE_ROOT/ssl/ssl_certificate.pem -noout -fingerprint \
| sed "s/SHA1 Fingerprint=//"
echo
echo Then you can confirm the security exception and continue.
echo

View File

@ -1,22 +1,92 @@
#!/bin/bash
source /etc/mailinabox.conf
source setup/functions.sh # load our functions
# Basic System Configuration
# -------------------------
# ### Add Mail-in-a-Box's PPA.
# ### Set hostname of the box
# We've built several .deb packages on our own that we want to include.
# One is a replacement for Ubuntu's stock postgrey package that makes
# some enhancements. The other is dovecot-lucene, a Lucene-based full
# text search plugin for (and by) dovecot, which is not available in
# Ubuntu currently.
# If the hostname is not correctly resolvable sudo can't be used. This will result in
# errors during the install
#
# Add that to the system's list of repositories using add-apt-repository.
# But add-apt-repository may not be installed. If it's not available,
# then install it. But we have to run apt-get update before we try to
# install anything so the package index is up to date. After adding the
# PPA, we have to run apt-get update *again* to load the PPA's index,
# so this must precede the apt-get update line below.
# First set the hostname in the configuration file, then activate the setting
echo "$PRIMARY_HOSTNAME" > /etc/hostname
hostname "$PRIMARY_HOSTNAME"
# ### Fix permissions
# The default Ubuntu Bionic image on Scaleway throws warnings during setup about incorrect
# permissions (group writeable) set on the following directories.
chmod g-w /etc /etc/default /usr
# ### Add swap space to the system
# If the physical memory of the system is below 2GB it is wise to create a
# swap file. This will make the system more resiliant to memory spikes and
# prevent for instance spam filtering from crashing
# We will create a 1G file, this should be a good balance between disk usage
# and buffers for the system. We will only allocate this file if there is more
# than 5GB of disk space available
# The following checks are performed:
# - Check if swap is currently mountend by looking at /proc/swaps
# - Check if the user intents to activate swap on next boot by checking fstab entries.
# - Check if a swapfile already exists
# - Check if the root file system is not btrfs, might be an incompatible version with
# swapfiles. User should hanle it them selves.
# - Check the memory requirements
# - Check available diskspace
# See https://www.digitalocean.com/community/tutorials/how-to-add-swap-on-ubuntu-14-04
# for reference
SWAP_MOUNTED=$(cat /proc/swaps | tail -n+2)
SWAP_IN_FSTAB=$(grep "swap" /etc/fstab || /bin/true)
ROOT_IS_BTRFS=$(grep "\/ .*btrfs" /proc/mounts || /bin/true)
TOTAL_PHYSICAL_MEM=$(head -n 1 /proc/meminfo | awk '{print $2}' || /bin/true)
AVAILABLE_DISK_SPACE=$(df / --output=avail | tail -n 1)
if
[ -z "$SWAP_MOUNTED" ] &&
[ -z "$SWAP_IN_FSTAB" ] &&
[ ! -e /swapfile ] &&
[ -z "$ROOT_IS_BTRFS" ] &&
[ "$TOTAL_PHYSICAL_MEM" -lt 1900000 ] &&
[ "$AVAILABLE_DISK_SPACE" -gt 5242880 ]
then
echo "Adding a swap file to the system..."
# Allocate and activate the swap file. Allocate in 1KB chuncks
# doing it in one go, could fail on low memory systems
dd if=/dev/zero of=/swapfile bs=1024 count=$((1024*1024)) status=none
if [ -e /swapfile ]; then
chmod 600 /swapfile
hide_output mkswap /swapfile
swapon /swapfile
fi
# Check if swap is mounted then activate on boot
if swapon -s | grep -q "\/swapfile"; then
echo "/swapfile none swap sw 0 0" >> /etc/fstab
else
echo "ERROR: Swap allocation failed"
fi
fi
# ### Set log retention policy.
# Set the systemd journal log retention from infinite to 10 days,
# since over time the logs take up a large amount of space.
# (See https://discourse.mailinabox.email/t/journalctl-reclaim-space-on-small-mailinabox/6728/11.)
tools/editconf.py /etc/systemd/journald.conf MaxRetentionSec=10day
# ### Add PPAs.
# We install some non-standard Ubuntu packages maintained by other
# third-party providers. First ensure add-apt-repository is installed.
if [ ! -f /usr/bin/add-apt-repository ]; then
echo "Installing add-apt-repository..."
@ -24,23 +94,37 @@ if [ ! -f /usr/bin/add-apt-repository ]; then
apt_install software-properties-common
fi
hide_output add-apt-repository -y ppa:mail-in-a-box/ppa
# Ensure the universe repository is enabled since some of our packages
# come from there and minimal Ubuntu installs may have it turned off.
hide_output add-apt-repository -y universe
# Install the duplicity PPA.
hide_output add-apt-repository -y ppa:duplicity-team/duplicity-release-git
# Stock PHP is now 8.1, but we're transitioning through 8.0 because
# of Nextcloud.
hide_output add-apt-repository --y ppa:ondrej/php
# ### Update Packages
# Update system packages to make sure we have the latest upstream versions of things from Ubuntu.
# Update system packages to make sure we have the latest upstream versions
# of things from Ubuntu, as well as the directory of packages provide by the
# PPAs so we can install those packages later.
echo Updating system packages...
echo "Updating system packages..."
hide_output apt-get update
apt_get_quiet upgrade
# Old kernels pile up over time and take up a lot of disk space, and because of Mail-in-a-Box
# changes there may be other packages that are no longer needed. Clear out anything apt knows
# is safe to delete.
apt_get_quiet autoremove
# ### Install System Packages
# Install basic utilities.
#
# * haveged: Provides extra entropy to /dev/random so it doesn't stall
# when generating random numbers for private keys (e.g. during
# ldns-keygen).
# * unattended-upgrades: Apt tool to install security updates automatically.
# * cron: Runs background processes periodically.
# * ntp: keeps the system time correct
@ -48,20 +132,127 @@ apt_get_quiet upgrade
# * netcat-openbsd: `nc` command line networking tool
# * git: we install some things directly from github
# * sudo: allows privileged users to execute commands as root without being root
# * coreutils: includes `nproc` tool to report number of processors
# * coreutils: includes `nproc` tool to report number of processors, mktemp
# * bc: allows us to do math to compute sane defaults
# * openssh-client: provides ssh-keygen
apt_install python3 python3-dev python3-pip \
netcat-openbsd wget curl git sudo coreutils bc \
haveged unattended-upgrades cron ntp fail2ban
echo "Installing system packages..."
apt_install python3 python3-dev python3-pip python3-setuptools \
netcat-openbsd wget curl git sudo coreutils bc file \
pollinate openssh-client unzip \
unattended-upgrades cron ntp fail2ban rsyslog
# ### Suppress Upgrade Prompts
# When Ubuntu 20 comes out, we don't want users to be prompted to upgrade,
# because we don't yet support it.
if [ -f /etc/update-manager/release-upgrades ]; then
tools/editconf.py /etc/update-manager/release-upgrades Prompt=never
rm -f /var/lib/ubuntu-release-upgrader/release-upgrade-available
fi
# ### Set the system timezone
#
# Some systems are missing /etc/timezone, which we cat into the configs for
# Z-Push and ownCloud, so we need to set it to something. Daily cron tasks
# like the system backup are run at a time tied to the system timezone, so
# letting the user choose will help us identify the right time to do those
# things (i.e. late at night in whatever timezone the user actually lives
# in).
#
# However, changing the timezone once it is set seems to confuse fail2ban
# and requires restarting fail2ban (done below in the fail2ban
# section) and syslog (see #328). There might be other issues, and it's
# not likely the user will want to change this, so we only ask on first
# setup.
if [ -z "${NONINTERACTIVE:-}" ]; then
if [ ! -f /etc/timezone ] || [ -n "${FIRST_TIME_SETUP:-}" ]; then
# If the file is missing or this is the user's first time running
# Mail-in-a-Box setup, run the interactive timezone configuration
# tool.
dpkg-reconfigure tzdata
restart_service rsyslog
fi
else
# This is a non-interactive setup so we can't ask the user.
# If /etc/timezone is missing, set it to UTC.
if [ ! -f /etc/timezone ]; then
echo "Setting timezone to UTC."
echo "Etc/UTC" > /etc/timezone
restart_service rsyslog
fi
fi
# ### Seed /dev/urandom
#
# /dev/urandom is used by various components for generating random bytes for
# encryption keys and passwords:
#
# * TLS private key (see `ssl.sh`, which calls `openssl genrsa`)
# * DNSSEC signing keys (see `dns.sh`)
# * our management server's API key (via Python's os.urandom method)
# * Roundcube's SECRET_KEY (`webmail.sh`)
#
# Why /dev/urandom? It's the same as /dev/random, except that it doesn't wait
# for a constant new stream of entropy. In practice, we only need a little
# entropy at the start to get going. After that, we can safely pull a random
# stream from /dev/urandom and not worry about how much entropy has been
# added to the stream. (http://www.2uo.de/myths-about-urandom/) So we need
# to worry about /dev/urandom being seeded properly (which is also an issue
# for /dev/random), but after that /dev/urandom is superior to /dev/random
# because it's faster and doesn't block indefinitely to wait for hardware
# entropy. Note that `openssl genrsa` even uses `/dev/urandom`, and if it's
# good enough for generating an RSA private key, it's good enough for anything
# else we may need.
#
# Now about that seeding issue....
#
# /dev/urandom is seeded from "the uninitialized contents of the pool buffers when
# the kernel starts, the startup clock time in nanosecond resolution,...and
# entropy saved across boots to a local file" as well as the order of
# execution of concurrent accesses to /dev/urandom. (Heninger et al 2012,
# https://factorable.net/weakkeys12.conference.pdf) But when memory is zeroed,
# the system clock is reset on boot, /etc/init.d/urandom has not yet run, or
# the machine is single CPU or has no concurrent accesses to /dev/urandom prior
# to this point, /dev/urandom may not be seeded well. After this, /dev/urandom
# draws from the same entropy sources as /dev/random, but it doesn't block or
# issue any warnings if no entropy is actually available. (http://www.2uo.de/myths-about-urandom/)
# Entropy might not be readily available because this machine has no user input
# devices (common on servers!) and either no hard disk or not enough IO has
# ocurred yet --- although haveged tries to mitigate this. So there's a good chance
# that accessing /dev/urandom will not be drawing from any hardware entropy and under
# a perfect-storm circumstance where the other seeds are meaningless, /dev/urandom
# may not be seeded at all.
#
# The first thing we'll do is block until we can seed /dev/urandom with enough
# hardware entropy to get going, by drawing from /dev/random. haveged makes this
# less likely to stall for very long.
echo "Initializing system random number generator..."
dd if=/dev/random of=/dev/urandom bs=1 count=32 2> /dev/null
# This is supposedly sufficient. But because we're not sure if hardware entropy
# is really any good on virtualized systems, we'll also seed from Ubuntu's
# pollinate servers:
pollinate -q -r
# Between these two, we really ought to be all set.
# We need an ssh key to store backups via rsync, if it doesn't exist create one
if [ ! -f /root/.ssh/id_rsa_miab ]; then
echo 'Creating SSH key for backup…'
ssh-keygen -t rsa -b 2048 -a 100 -f /root/.ssh/id_rsa_miab -N '' -q
fi
# ### Package maintenance
#
# Allow apt to install system updates automatically every day.
cat > /etc/apt/apt.conf.d/02periodic <<EOF;
APT::Periodic::MaxAge "7";
APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Unattended-Upgrade "1";
APT::Periodic::Verbose "1";
APT::Periodic::Verbose "0";
EOF
# ### Firewall
@ -69,22 +260,22 @@ EOF
# Various virtualized environments like Docker and some VPSs don't provide #NODOC
# a kernel that supports iptables. To avoid error-like output in these cases, #NODOC
# we skip this if the user sets DISABLE_FIREWALL=1. #NODOC
if [ -z "$DISABLE_FIREWALL" ]; then
if [ -z "${DISABLE_FIREWALL:-}" ]; then
# Install `ufw` which provides a simple firewall configuration.
apt_install ufw
# Allow incoming connections to SSH.
ufw_allow ssh;
ufw_limit ssh;
# ssh might be running on an alternate port. Use sshd -T to dump sshd's #NODOC
# settings, find the port it is supposedly running on, and open that port #NODOC
# too. #NODOC
SSH_PORT=$(sshd -T 2>/dev/null | grep "^port " | sed "s/port //") #NODOC
if [ ! -z "$SSH_PORT" ]; then
if [ -n "$SSH_PORT" ]; then
if [ "$SSH_PORT" != "22" ]; then
echo Opening alternate SSH port $SSH_PORT. #NODOC
ufw_allow $SSH_PORT #NODOC
echo "Opening alternate SSH port $SSH_PORT." #NODOC
ufw_limit "$SSH_PORT" #NODOC
fi
fi
@ -94,50 +285,94 @@ fi #NODOC
# ### Local DNS Service
# Install a local DNS server, rather than using the DNS server provided by the
# ISP's network configuration.
# Install a local recursive DNS server --- i.e. for DNS queries made by
# local services running on this machine.
#
# We do this to ensure that DNS queries
# that *we* make (i.e. looking up other external domains) perform DNSSEC checks.
# We could use Google's Public DNS, but we don't want to create a dependency on
# Google per our goals of decentralization. `bind9`, as packaged for Ubuntu, has
# DNSSEC enabled by default via "dnssec-validation auto".
# (This is unrelated to the box's public, non-recursive DNS server that
# answers remote queries about domain names hosted on this box. For that
# see dns.sh.)
#
# So we'll be running `bind9` bound to 127.0.0.1 for locally-issued DNS queries
# and `nsd` bound to the public ethernet interface for remote DNS queries asking
# about our domain names. `nsd` is configured later.
# The default systemd-resolved service provides local DNS name resolution. By default it
# is a recursive stub nameserver, which means it simply relays requests to an
# external nameserver, usually provided by your ISP or configured in /etc/systemd/resolved.conf.
#
# This won't work for us for three reasons.
#
# 1) We have higher security goals --- we want DNSSEC to be enforced on all
# DNS queries (some upstream DNS servers do, some don't).
# 2) We will configure postfix to use DANE, which uses DNSSEC to find TLS
# certificates for remote servers. DNSSEC validation *must* be performed
# locally because we can't trust an unencrypted connection to an external
# DNS server.
# 3) DNS-based mail server blacklists (RBLs) typically block large ISP
# DNS servers because they only provide free data to small users. Since
# we use RBLs to block incoming mail from blacklisted IP addresses,
# we have to run our own DNS server. See #1424.
#
# systemd-resolved has a setting to perform local DNSSEC validation on all
# requests (in /etc/systemd/resolved.conf, set DNSSEC=yes), but because it's
# a stub server the main part of a request still goes through an upstream
# DNS server, which won't work for RBLs. So we really need a local recursive
# nameserver.
#
# We'll install `bind9`, which as packaged for Ubuntu, has DNSSEC enabled by default via "dnssec-validation auto".
# We'll have it be bound to 127.0.0.1 so that it does not interfere with
# the public, recursive nameserver `nsd` bound to the public ethernet interfaces.
#
# About the settings:
#
# * RESOLVCONF=yes will have `bind9` take over /etc/resolv.conf to tell
# local services that DNS queries are handled on localhost.
# * Adding -4 to OPTIONS will have `bind9` not listen on IPv6 addresses
# so that we're sure there's no conflict with nsd, our public domain
# name server, on IPV6.
# * The listen-on directive in named.conf.options restricts `bind9` to
# binding to the loopback interface instead of all interfaces.
apt_install bind9 resolvconf
tools/editconf.py /etc/default/bind9 \
RESOLVCONF=yes \
# * The max-recursion-queries directive increases the maximum number of iterative queries.
# If more queries than specified are sent, bind9 returns SERVFAIL. After flushing the cache during system checks,
# we ran into the limit thus we are increasing it from 75 (default value) to 100.
apt_install bind9
tools/editconf.py /etc/default/named \
"OPTIONS=\"-u bind -4\""
if ! grep -q "listen-on " /etc/bind/named.conf.options; then
# Add a listen-on directive if it doesn't exist inside the options block.
sed -i "s/^}/\n\tlisten-on { 127.0.0.1; };\n}/" /etc/bind/named.conf.options
fi
if [ -f /etc/resolvconf/resolv.conf.d/original ]; then
echo "Archiving old resolv.conf (was /etc/resolvconf/resolv.conf.d/original, now /etc/resolvconf/resolv.conf.original)." #NODOC
mv /etc/resolvconf/resolv.conf.d/original /etc/resolvconf/resolv.conf.original #NODOC
if ! grep -q "max-recursion-queries " /etc/bind/named.conf.options; then
# Add a max-recursion-queries directive if it doesn't exist inside the options block.
sed -i "s/^}/\n\tmax-recursion-queries 100;\n}/" /etc/bind/named.conf.options
fi
# First we'll disable systemd-resolved's management of resolv.conf and its stub server.
# Breaking the symlink to /run/systemd/resolve/stub-resolv.conf means
# systemd-resolved will read it for DNS servers to use. Put in 127.0.0.1,
# which is where bind9 will be running. Obviously don't do this before
# installing bind9 or else apt won't be able to resolve a server to
# download bind9 from.
rm -f /etc/resolv.conf
tools/editconf.py /etc/systemd/resolved.conf DNSStubListener=no
echo "nameserver 127.0.0.1" > /etc/resolv.conf
# Restart the DNS services.
restart_service bind9
restart_service resolvconf
systemctl restart systemd-resolved
# ### Fail2Ban Service
# Configure the Fail2Ban installation to prevent dumb bruce-force attacks against dovecot, postfix and ssh
cp conf/fail2ban/jail.local /etc/fail2ban/jail.local
cp conf/fail2ban/dovecotimap.conf /etc/fail2ban/filter.d/dovecotimap.conf
# Configure the Fail2Ban installation to prevent dumb bruce-force attacks against dovecot, postfix, ssh, etc.
rm -f /etc/fail2ban/jail.local # we used to use this file but don't anymore
rm -f /etc/fail2ban/jail.d/defaults-debian.conf # removes default config so we can manage all of fail2ban rules in one config
cat conf/fail2ban/jails.conf \
| sed "s/PUBLIC_IPV6/$PUBLIC_IPV6/g" \
| sed "s/PUBLIC_IP/$PUBLIC_IP/g" \
| sed "s#STORAGE_ROOT#$STORAGE_ROOT#" \
> /etc/fail2ban/jail.d/mailinabox.conf
cp -f conf/fail2ban/filter.d/* /etc/fail2ban/filter.d/
# On first installation, the log files that the jails look at don't all exist.
# e.g., The roundcube error log isn't normally created until someone logs into
# Roundcube for the first time. This causes fail2ban to fail to start. Later
# scripts will ensure the files exist and then fail2ban is given another
# restart at the very end of setup.
restart_service fail2ban
systemctl enable fail2ban

View File

@ -8,7 +8,7 @@ source /etc/mailinabox.conf # load global vars
# Some Ubuntu images start off with Apache. Remove it since we
# will use nginx. Use autoremove to remove any Apache depenencies.
if [ -f /usr/sbin/apache2 ]; then
echo Removing apache...
echo "Removing apache..."
hide_output apt-get -y purge apache2 apache2-*
hide_output apt-get -y --purge autoremove
fi
@ -17,32 +17,83 @@ fi
#
# Turn off nginx's default website.
apt_install nginx php5-fpm
echo "Installing Nginx (web server)..."
apt_install nginx php"${PHP_VER}"-cli php"${PHP_VER}"-fpm idn2
rm -f /etc/nginx/sites-enabled/default
# Copy in a nginx configuration file for common and best-practices
# SSL settings from @konklone. Replace STORAGE_ROOT so it can find
# the DH params.
rm -f /etc/nginx/nginx-ssl.conf # we used to put it here
sed "s#STORAGE_ROOT#$STORAGE_ROOT#" \
conf/nginx-ssl.conf > /etc/nginx/nginx-ssl.conf
conf/nginx-ssl.conf > /etc/nginx/conf.d/ssl.conf
# Fix some nginx defaults.
# The server_names_hash_bucket_size seems to prevent long domain names?
#
# The server_names_hash_bucket_size seems to prevent long domain names!
# The default, according to nginx's docs, depends on "the size of the
# processors cache line." It could be as low as 32. We fixed it at
# 64 in 2014 to accommodate a long domain name (20 characters?). But
# even at 64, a 58-character domain name won't work (#93), so now
# we're going up to 128.
#
# Drop TLSv1.0, TLSv1.1, following the Mozilla "Intermediate" recommendations
# at https://ssl-config.mozilla.org/#server=nginx&server-version=1.17.0&config=intermediate&openssl-version=1.1.1.
tools/editconf.py /etc/nginx/nginx.conf -s \
server_names_hash_bucket_size="64;"
server_names_hash_bucket_size="128;" \
ssl_protocols="TLSv1.2 TLSv1.3;"
# Tell PHP not to expose its version number in the X-Powered-By header.
tools/editconf.py /etc/php5/fpm/php.ini -c ';' \
tools/editconf.py /etc/php/"$PHP_VER"/fpm/php.ini -c ';' \
expose_php=Off
# Set PHPs default charset to UTF-8, since we use it. See #367.
tools/editconf.py /etc/php5/fpm/php.ini -c ';' \
tools/editconf.py /etc/php/"$PHP_VER"/fpm/php.ini -c ';' \
default_charset="UTF-8"
# Bump up PHP's max_children to support more concurrent connections
tools/editconf.py /etc/php5/fpm/pool.d/www.conf -c ';' \
pm.max_children=8
# Configure the path environment for php-fpm
tools/editconf.py /etc/php/"$PHP_VER"/fpm/pool.d/www.conf -c ';' \
env[PATH]=/usr/local/bin:/usr/bin:/bin \
# Configure php-fpm based on the amount of memory the machine has
# This is based on the nextcloud manual for performance tuning: https://docs.nextcloud.com/server/17/admin_manual/installation/server_tuning.html
# Some synchronisation issues can occur when many people access the site at once.
# The pm=ondemand setting is used for memory constrained machines < 2GB, this is copied over from PR: 1216
TOTAL_PHYSICAL_MEM=$(head -n 1 /proc/meminfo | awk '{print $2}' || /bin/true)
if [ "$TOTAL_PHYSICAL_MEM" -lt 1000000 ]
then
tools/editconf.py /etc/php/"$PHP_VER"/fpm/pool.d/www.conf -c ';' \
pm=ondemand \
pm.max_children=8 \
pm.start_servers=2 \
pm.min_spare_servers=1 \
pm.max_spare_servers=3
elif [ "$TOTAL_PHYSICAL_MEM" -lt 2000000 ]
then
tools/editconf.py /etc/php/"$PHP_VER"/fpm/pool.d/www.conf -c ';' \
pm=ondemand \
pm.max_children=16 \
pm.start_servers=4 \
pm.min_spare_servers=1 \
pm.max_spare_servers=6
elif [ "$TOTAL_PHYSICAL_MEM" -lt 3000000 ]
then
tools/editconf.py /etc/php/"$PHP_VER"/fpm/pool.d/www.conf -c ';' \
pm=dynamic \
pm.max_children=60 \
pm.start_servers=6 \
pm.min_spare_servers=3 \
pm.max_spare_servers=9
else
tools/editconf.py /etc/php/"$PHP_VER"/fpm/pool.d/www.conf -c ';' \
pm=dynamic \
pm.max_children=120 \
pm.start_servers=12 \
pm.min_spare_servers=6 \
pm.max_spare_servers=18
fi
# Other nginx settings will be configured by the management service
# since it depends on what domains we're serving, which we don't know
@ -71,34 +122,33 @@ cat conf/mozilla-autoconfig.xml \
> /var/lib/mailinabox/mozilla-autoconfig.xml
chmod a+r /var/lib/mailinabox/mozilla-autoconfig.xml
# Create a generic mta-sts.txt file which is exposed via the
# nginx configuration at /.well-known/mta-sts.txt
# more documentation is available on:
# https://www.uriports.com/blog/mta-sts-explained/
# default mode is "enforce". In /etc/mailinabox.conf change
# "MTA_STS_MODE=testing" which means "Messages will be delivered
# as though there was no failure but a report will be sent if
# TLS-RPT is configured" if you are not sure you want this yet. Or "none".
PUNY_PRIMARY_HOSTNAME=$(echo "$PRIMARY_HOSTNAME" | idn2)
cat conf/mta-sts.txt \
| sed "s/MODE/${MTA_STS_MODE}/" \
| sed "s/PRIMARY_HOSTNAME/$PUNY_PRIMARY_HOSTNAME/" \
> /var/lib/mailinabox/mta-sts.txt
chmod a+r /var/lib/mailinabox/mta-sts.txt
# make a default homepage
if [ -d $STORAGE_ROOT/www/static ]; then mv $STORAGE_ROOT/www/static $STORAGE_ROOT/www/default; fi # migration #NODOC
mkdir -p $STORAGE_ROOT/www/default
if [ ! -f $STORAGE_ROOT/www/default/index.html ]; then
cp conf/www_default.html $STORAGE_ROOT/www/default/index.html
if [ -d "$STORAGE_ROOT/www/static" ]; then mv "$STORAGE_ROOT/www/static" "$STORAGE_ROOT/www/default"; fi # migration #NODOC
mkdir -p "$STORAGE_ROOT/www/default"
if [ ! -f "$STORAGE_ROOT/www/default/index.html" ]; then
cp conf/www_default.html "$STORAGE_ROOT/www/default/index.html"
fi
chown -R $STORAGE_USER $STORAGE_ROOT/www
# We previously installed a custom init script to start the PHP FastCGI daemon. #NODOC
# Remove it now that we're using php5-fpm. #NODOC
if [ -L /etc/init.d/php-fastcgi ]; then
echo "Removing /etc/init.d/php-fastcgi, php5-cgi..." #NODOC
rm -f /etc/init.d/php-fastcgi #NODOC
hide_output update-rc.d php-fastcgi remove #NODOC
apt-get -y purge php5-cgi #NODOC
fi
# Remove obsoleted scripts. #NODOC
# exchange-autodiscover is now handled by Z-Push. #NODOC
for f in webfinger exchange-autodiscover; do #NODOC
rm -f /usr/local/bin/mailinabox-$f.php #NODOC
done #NODOC
chown -R "$STORAGE_USER" "$STORAGE_ROOT/www"
# Start services.
restart_service nginx
restart_service php5-fpm
restart_service php"$PHP_VER"-fpm
# Open ports.
ufw_allow http
ufw_allow https

207
setup/webmail.sh Executable file → Normal file
View File

@ -19,139 +19,206 @@ source /etc/mailinabox.conf # load global vars
# and then we'll manually install roundcube from source.
# These dependencies are from `apt-cache showpkg roundcube-core`.
echo "Installing Roundcube (webmail)..."
apt_install \
dbconfig-common \
php5 php5-sqlite php5-mcrypt php5-intl php5-json php5-common php-auth php-net-smtp php-net-socket php-net-sieve php-mail-mime php-crypt-gpg php5-gd php5-pspell \
tinymce libjs-jquery libjs-jquery-mousewheel libmagic1
# We used to install Roundcube from Ubuntu, without triggering the dependencies #NODOC
# on Apache and MySQL, by downloading the debs and installing them manually. #NODOC
# Now that we're beyond that, get rid of those debs before installing from source. #NODOC
apt-get purge -qq -y roundcube* #NODOC
php"${PHP_VER}"-cli php"${PHP_VER}"-sqlite3 php"${PHP_VER}"-intl php"${PHP_VER}"-common php"${PHP_VER}"-curl php"${PHP_VER}"-imap \
php"${PHP_VER}"-gd php"${PHP_VER}"-pspell php"${PHP_VER}"-mbstring libjs-jquery libjs-jquery-mousewheel libmagic1 \
sqlite3
# Install Roundcube from source if it is not already present or if it is out of date.
# Combine the Roundcube version number with the commit hash of vacation_sieve to track
# whether we have the latest version.
VERSION=1.1.2
HASH=df88deae691da3ecf3e9f0aee674c1f3042ea1eb
VACATION_SIEVE_VERSION=91ea6f52216390073d1f5b70b5f6bea0bfaee7e5
PERSISTENT_LOGIN_VERSION=117fbd8f93b56b2bf72ad055193464803ef3bc36
UPDATE_KEY=$VERSION:$VACATION_SIEVE_VERSION:$PERSISTENT_LOGIN_VERSION
# Combine the Roundcube version number with the commit hash of plugins to track
# whether we have the latest version of everything.
# For the latest versions, see:
# https://github.com/roundcube/roundcubemail/releases
# https://github.com/mfreiholz/persistent_login/commits/master
# https://github.com/stremlau/html5_notifier/commits/master
# https://github.com/mstilkerich/rcmcarddav/releases
# The easiest way to get the package hashes is to run this script and get the hash from
# the error message.
VERSION=1.6.6
HASH=7705d2736890c49e7ae3ac75e3ae00ba56187056
PERSISTENT_LOGIN_VERSION=bde7b6840c7d91de627ea14e81cf4133cbb3c07a # version 5.3
HTML5_NOTIFIER_VERSION=68d9ca194212e15b3c7225eb6085dbcf02fd13d7 # version 0.6.4+
CARDDAV_VERSION=4.4.3
CARDDAV_HASH=74f8ba7aee33e78beb9de07f7f44b81f6071b644
UPDATE_KEY=$VERSION:$PERSISTENT_LOGIN_VERSION:$HTML5_NOTIFIER_VERSION:$CARDDAV_VERSION
# paths that are often reused.
RCM_DIR=/usr/local/lib/roundcubemail
RCM_PLUGIN_DIR=${RCM_DIR}/plugins
RCM_CONFIG=${RCM_DIR}/config/config.inc.php
needs_update=0 #NODOC
if [ ! -f /usr/local/lib/roundcubemail/version ]; then
# not installed yet #NODOC
needs_update=1 #NODOC
elif [[ "$UPDATE_KEY" != `cat /usr/local/lib/roundcubemail/version` ]]; then
elif [[ "$UPDATE_KEY" != $(cat /usr/local/lib/roundcubemail/version) ]]; then
# checks if the version is what we want
needs_update=1 #NODOC
fi
if [ $needs_update == 1 ]; then
# if upgrading from 1.3.x, clear the temp_dir
if [ -f /usr/local/lib/roundcubemail/version ]; then
if [ "$(cat /usr/local/lib/roundcubemail/version | cut -c1-3)" == '1.3' ]; then
find /var/tmp/roundcubemail/ -type f ! -name 'RCMTEMP*' -delete
fi
fi
# install roundcube
echo installing Roundcube webmail $VERSION...
wget_verify \
http://downloads.sourceforge.net/project/roundcubemail/roundcubemail/$VERSION/roundcubemail-$VERSION.tar.gz \
https://github.com/roundcube/roundcubemail/releases/download/$VERSION/roundcubemail-$VERSION-complete.tar.gz \
$HASH \
/tmp/roundcube.tgz
tar -C /usr/local/lib -zxf /tmp/roundcube.tgz
tar -C /usr/local/lib --no-same-owner -zxf /tmp/roundcube.tgz
rm -rf /usr/local/lib/roundcubemail
mv /usr/local/lib/roundcubemail-$VERSION/ /usr/local/lib/roundcubemail
mv /usr/local/lib/roundcubemail-$VERSION/ $RCM_DIR
rm -f /tmp/roundcube.tgz
# install roundcube autoreply/vacation plugin
git_clone https://github.com/arodier/Roundcube-Plugins.git $VACATION_SIEVE_VERSION plugins/vacation_sieve /usr/local/lib/roundcubemail/plugins/vacation_sieve
# install roundcube persistent_login plugin
git_clone https://github.com/mfreiholz/Roundcube-Persistent-Login-Plugin.git $PERSISTENT_LOGIN_VERSION '' /usr/local/lib/roundcubemail/plugins/persistent_login
git_clone https://github.com/mfreiholz/Roundcube-Persistent-Login-Plugin.git $PERSISTENT_LOGIN_VERSION '' ${RCM_PLUGIN_DIR}/persistent_login
# install roundcube html5_notifier plugin
git_clone https://github.com/kitist/html5_notifier.git $HTML5_NOTIFIER_VERSION '' ${RCM_PLUGIN_DIR}/html5_notifier
# download and verify the full release of the carddav plugin
wget_verify \
https://github.com/mstilkerich/rcmcarddav/releases/download/v${CARDDAV_VERSION}/carddav-v${CARDDAV_VERSION}.tar.gz \
$CARDDAV_HASH \
/tmp/carddav.tar.gz
# unzip and cleanup
tar -C ${RCM_PLUGIN_DIR} -zxf /tmp/carddav.tar.gz
rm -f /tmp/carddav.tar.gz
# record the version we've installed
echo $UPDATE_KEY > /usr/local/lib/roundcubemail/version
echo $UPDATE_KEY > ${RCM_DIR}/version
fi
# ### Configuring Roundcube
# Generate a safe 24-character secret key of safe characters.
SECRET_KEY=$(dd if=/dev/random bs=1 count=18 2>/dev/null | base64 | fold -w 24 | head -n 1)
# Generate a secret key of PHP-string-safe characters appropriate
# for the cipher algorithm selected below.
SECRET_KEY=$(dd if=/dev/urandom bs=1 count=32 2>/dev/null | base64 | sed s/=//g)
# Create a configuration file.
#
# For security, temp and log files are not stored in the default locations
# which are inside the roundcube sources directory. We put them instead
# in normal places.
cat > /usr/local/lib/roundcubemail/config/config.inc.php <<EOF;
cat > $RCM_CONFIG <<EOF;
<?php
/*
* Do not edit. Written by Mail-in-a-Box. Regenerated on updates.
*/
\$config = array();
\$config['log_dir'] = '/var/log/roundcubemail/';
\$config['temp_dir'] = '/tmp/roundcubemail/';
\$config['temp_dir'] = '/var/tmp/roundcubemail/';
\$config['db_dsnw'] = 'sqlite:///$STORAGE_ROOT/mail/roundcube/roundcube.sqlite?mode=0640';
\$config['default_host'] = 'ssl://localhost';
\$config['default_port'] = 993;
\$config['imap_host'] = 'ssl://localhost:993';
\$config['imap_conn_options'] = array(
'ssl' => array(
'verify_peer' => false,
'verify_peer_name' => false,
),
);
\$config['imap_timeout'] = 15;
\$config['smtp_server'] = 'tls://localhost';
\$config['smtp_port'] = 587;
\$config['smtp_user'] = '%u';
\$config['smtp_pass'] = '%p';
\$config['smtp_host'] = 'tls://127.0.0.1';
\$config['smtp_conn_options'] = array(
'ssl' => array(
'verify_peer' => false,
'verify_peer_name' => false,
),
);
\$config['support_url'] = 'https://mailinabox.email/';
\$config['product_name'] = 'Mail-in-a-Box/Roundcube Webmail';
\$config['des_key'] = '$SECRET_KEY';
\$config['plugins'] = array('archive', 'zipdownload', 'password', 'managesieve', 'jqueryui', 'vacation_sieve', 'persistent_login');
\$config['skin'] = 'classic';
\$config['product_name'] = '$PRIMARY_HOSTNAME Webmail';
\$config['cipher_method'] = 'AES-256-CBC'; # persistent login cookie and potentially other things
\$config['des_key'] = '$SECRET_KEY'; # 37 characters -> ~256 bits for AES-256, see above
\$config['plugins'] = array('html5_notifier', 'archive', 'zipdownload', 'password', 'managesieve', 'jqueryui', 'persistent_login', 'carddav');
\$config['skin'] = 'elastic';
\$config['login_autocomplete'] = 2;
\$config['login_username_filter'] = 'email';
\$config['password_charset'] = 'UTF-8';
\$config['junk_mbox'] = 'Spam';
/* ensure roudcube session id's aren't leaked to other parts of the server */
\$config['session_path'] = '/mail/';
/* prevent CSRF, requires php 7.3+ */
\$config['session_samesite'] = 'Strict';
?>
EOF
# Configure vaction_sieve.
cat > /usr/local/lib/roundcubemail/plugins/vacation_sieve/config.inc.php <<EOF;
# Configure CardDav
cat > ${RCM_PLUGIN_DIR}/carddav/config.inc.php <<EOF;
<?php
/* Do not edit. Written by Mail-in-a-Box. Regenerated on updates. */
\$rcmail_config['vacation_sieve'] = array(
'date_format' => 'd/m/Y',
'working_hours' => array(8,18),
'msg_format' => 'text',
'logon_transform' => array('#([a-z])[a-z]+(\.|\s)([a-z])#i', '\$1\$3'),
'transfer' => array(
'mode' => 'managesieve',
'ms_activate_script' => true,
'host' => 'localhost',
'port' => '4190',
'usetls' => false,
'path' => 'vacation',
)
\$prefs['_GLOBAL']['hide_preferences'] = true;
\$prefs['_GLOBAL']['suppress_version_warning'] = true;
\$prefs['ownCloud'] = array(
'name' => 'ownCloud',
'username' => '%u', // login username
'password' => '%p', // login password
'url' => 'https://${PRIMARY_HOSTNAME}/cloud/remote.php/dav/addressbooks/users/%u/contacts/',
'active' => true,
'readonly' => false,
'refresh_time' => '02:00:00',
'fixed' => array('username','password'),
'preemptive_auth' => '1',
'hide' => false,
);
?>
EOF
# Create writable directories.
mkdir -p /var/log/roundcubemail /tmp/roundcubemail $STORAGE_ROOT/mail/roundcube
chown -R www-data.www-data /var/log/roundcubemail /tmp/roundcubemail $STORAGE_ROOT/mail/roundcube
mkdir -p /var/log/roundcubemail /var/tmp/roundcubemail "$STORAGE_ROOT/mail/roundcube"
chown -R www-data:www-data /var/log/roundcubemail /var/tmp/roundcubemail "$STORAGE_ROOT/mail/roundcube"
# Ensure the log file monitored by fail2ban exists, or else fail2ban can't start.
sudo -u www-data touch /var/log/roundcubemail/errors.log
# Password changing plugin settings
# The config comes empty by default, so we need the settings
# The config comes empty by default, so we need the settings
# we're not planning to change in config.inc.dist...
cp /usr/local/lib/roundcubemail/plugins/password/config.inc.php.dist \
/usr/local/lib/roundcubemail/plugins/password/config.inc.php
cp ${RCM_PLUGIN_DIR}/password/config.inc.php.dist \
${RCM_PLUGIN_DIR}/password/config.inc.php
tools/editconf.py /usr/local/lib/roundcubemail/plugins/password/config.inc.php \
"\$config['password_minimum_length']=6;" \
tools/editconf.py ${RCM_PLUGIN_DIR}/password/config.inc.php \
"\$config['password_minimum_length']=8;" \
"\$config['password_db_dsn']='sqlite:///$STORAGE_ROOT/mail/users.sqlite';" \
"\$config['password_query']='UPDATE users SET password=%D WHERE email=%u';" \
"\$config['password_dovecotpw']='/usr/bin/doveadm pw';" \
"\$config['password_dovecotpw_method']='SHA512-CRYPT';" \
"\$config['password_dovecotpw_with_method']=true;"
"\$config['password_query']='UPDATE users SET password=%P WHERE email=%u';" \
"\$config['password_algorithm']='sha512-crypt';" \
"\$config['password_algorithm_prefix']='{SHA512-CRYPT}';"
# so PHP can use doveadm, for the password changing plugin
usermod -a -G dovecot www-data
# set permissions so that PHP can use users.sqlite
# could use dovecot instead of www-data, but not sure it matters
chown root.www-data $STORAGE_ROOT/mail
chmod 775 $STORAGE_ROOT/mail
chown root.www-data $STORAGE_ROOT/mail/users.sqlite
chmod 664 $STORAGE_ROOT/mail/users.sqlite
chown root:www-data "$STORAGE_ROOT/mail"
chmod 775 "$STORAGE_ROOT/mail"
chown root:www-data "$STORAGE_ROOT/mail/users.sqlite"
chmod 664 "$STORAGE_ROOT/mail/users.sqlite"
# Fix Carddav permissions:
chown -f -R root:www-data ${RCM_PLUGIN_DIR}/carddav
# root:www-data need all permissions, others only read
chmod -R 774 ${RCM_PLUGIN_DIR}/carddav
# Run Roundcube database migration script (database is created if it does not exist)
php"$PHP_VER" ${RCM_DIR}/bin/updatedb.sh --dir ${RCM_DIR}/SQL --package roundcube
chown www-data:www-data "$STORAGE_ROOT/mail/roundcube/roundcube.sqlite"
chmod 664 "$STORAGE_ROOT/mail/roundcube/roundcube.sqlite"
# Patch the Roundcube code to eliminate an issue that causes postfix to reject our sqlite
# user database (see https://github.com/mail-in-a-box/mailinabox/issues/2185)
sed -i.miabold 's/^[^#]\+.\+PRAGMA journal_mode = WAL.\+$/#&/' \
/usr/local/lib/roundcubemail/program/lib/Roundcube/db/sqlite.php
# Because Roundcube wants to set the PRAGMA we just deleted from the source, we apply it here
# to the roundcube database (see https://github.com/roundcube/roundcubemail/issues/8035)
# Database should exist, created by migration script
hide_output sqlite3 "$STORAGE_ROOT/mail/roundcube/roundcube.sqlite" 'PRAGMA journal_mode=WAL;'
# Enable PHP modules.
php5enmod mcrypt
restart_service php5-fpm
phpenmod -v "$PHP_VER" imap
restart_service php"$PHP_VER"-fpm

View File

@ -15,33 +15,51 @@ source /etc/mailinabox.conf # load global vars
# Prereqs.
echo "Installing Z-Push (Exchange/ActiveSync server)..."
apt_install \
php-soap php5-imap libawl-php php5-xsl
php"${PHP_VER}"-soap php"${PHP_VER}"-imap libawl-php php"$PHP_VER"-xml
php5enmod imap
phpenmod -v "$PHP_VER" imap
# Copy Z-Push into place.
TARGETHASH=d0cd5a47c53afac5c3b287006dc8a48a1c4ffcd5
VERSION=2.7.1
TARGETHASH=f15c566b1ad50de24f3f08f505f0c3d8155c2d0d
needs_update=0 #NODOC
if [ ! -f /usr/local/lib/z-push/version ]; then
needs_update=1 #NODOC
elif [[ $TARGETHASH != `cat /usr/local/lib/z-push/version` ]]; then
# checks if the version
elif [[ $VERSION != $(cat /usr/local/lib/z-push/version) ]]; then
# checks if the version
needs_update=1 #NODOC
fi
if [ $needs_update == 1 ]; then
echo installing z-push \(fmbiete fork\)...
git_clone https://github.com/fmbiete/Z-Push-contrib $TARGETHASH '' /usr/local/lib/z-push
# Download
wget_verify "https://github.com/Z-Hub/Z-Push/archive/refs/tags/$VERSION.zip" $TARGETHASH /tmp/z-push.zip
# Extract into place.
rm -rf /usr/local/lib/z-push /tmp/z-push
unzip -q /tmp/z-push.zip -d /tmp/z-push
mv /tmp/z-push/*/src /usr/local/lib/z-push
rm -rf /tmp/z-push.zip /tmp/z-push
# Create admin and top scripts with PHP_VER
rm -f /usr/sbin/z-push-{admin,top}
ln -s /usr/local/lib/z-push/z-push-admin.php /usr/sbin/z-push-admin
ln -s /usr/local/lib/z-push/z-push-top.php /usr/sbin/z-push-top
echo $TARGETHASH > /usr/local/lib/z-push/version
echo '#!/bin/bash' > /usr/sbin/z-push-admin
echo php"$PHP_VER" /usr/local/lib/z-push/z-push-admin.php '"$@"' >> /usr/sbin/z-push-admin
chmod 755 /usr/sbin/z-push-admin
echo '#!/bin/bash' > /usr/sbin/z-push-top
echo php"$PHP_VER" /usr/local/lib/z-push/z-push-top.php '"$@"' >> /usr/sbin/z-push-top
chmod 755 /usr/sbin/z-push-top
echo $VERSION > /usr/local/lib/z-push/version
fi
# Configure default config.
sed -i "s^define('TIMEZONE', .*^define('TIMEZONE', '$(cat /etc/timezone)');^" /usr/local/lib/z-push/config.php
sed -i "s/define('BACKEND_PROVIDER', .*/define('BACKEND_PROVIDER', 'BackendCombined');/" /usr/local/lib/z-push/config.php
sed -i "s/define('USE_FULLEMAIL_FOR_LOGIN', .*/define('USE_FULLEMAIL_FOR_LOGIN', true);/" /usr/local/lib/z-push/config.php
sed -i "s/define('LOG_MEMORY_PROFILER', .*/define('LOG_MEMORY_PROFILER', false);/" /usr/local/lib/z-push/config.php
sed -i "s/define('BUG68532FIXED', .*/define('BUG68532FIXED', false);/" /usr/local/lib/z-push/config.php
sed -i "s/define('LOGLEVEL', .*/define('LOGLEVEL', LOGLEVEL_ERROR);/" /usr/local/lib/z-push/config.php
# Configure BACKEND
rm -f /usr/local/lib/z-push/backend/combined/config.php
@ -50,6 +68,7 @@ cp conf/zpush/backend_combined.php /usr/local/lib/z-push/backend/combined/config
# Configure IMAP
rm -f /usr/local/lib/z-push/backend/imap/config.php
cp conf/zpush/backend_imap.php /usr/local/lib/z-push/backend/imap/config.php
sed -i "s%STORAGE_ROOT%$STORAGE_ROOT%" /usr/local/lib/z-push/backend/imap/config.php
# Configure CardDav
rm -f /usr/local/lib/z-push/backend/carddav/config.php
@ -63,6 +82,7 @@ cp conf/zpush/backend_caldav.php /usr/local/lib/z-push/backend/caldav/config.php
rm -f /usr/local/lib/z-push/autodiscover/config.php
cp conf/zpush/autodiscover_config.php /usr/local/lib/z-push/autodiscover/config.php
sed -i "s/PRIMARY_HOSTNAME/$PRIMARY_HOSTNAME/" /usr/local/lib/z-push/autodiscover/config.php
sed -i "s^define('TIMEZONE', .*^define('TIMEZONE', '$(cat /etc/timezone)');^" /usr/local/lib/z-push/autodiscover/config.php
# Some directories it will use.
@ -73,6 +93,23 @@ chmod 750 /var/lib/z-push
chown www-data:www-data /var/log/z-push
chown www-data:www-data /var/lib/z-push
# Add log rotation
cat > /etc/logrotate.d/z-push <<EOF;
/var/log/z-push/*.log {
weekly
missingok
rotate 52
compress
delaycompress
notifempty
}
EOF
# Restart service.
restart_service php5-fpm
restart_service php"$PHP_VER"-fpm
# Fix states after upgrade
hide_output php"$PHP_VER" /usr/local/lib/z-push/z-push-admin.php -a fixstates

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