mirror of
https://github.com/mail-in-a-box/mailinabox.git
synced 2025-03-30 23:37:05 +00:00
Compare commits
52 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
3efd4257b5 | ||
|
a81c18666f | ||
|
01996141ad | ||
|
c0103045be | ||
|
41cbf0ba8e | ||
|
5ef85f3d02 | ||
|
e6c354c312 | ||
|
432b470d29 | ||
|
d58dd0c91d | ||
|
f73da3db60 | ||
|
626bced707 | ||
|
7f9a348d64 | ||
|
ac383ced4d | ||
|
450c1924d8 | ||
|
c9d37be530 | ||
|
08e69ca459 | ||
|
bd5ba78a99 | ||
|
654f5614af | ||
|
8bb68d60a5 | ||
|
27c510319f | ||
|
67c502e97b | ||
|
55bb35e3ef | ||
|
4259033121 | ||
|
b4170e4095 | ||
|
d8ab444d59 | ||
|
ce45217ab8 | ||
|
18721e42d1 | ||
|
e0b93718a3 | ||
|
2e0482e181 | ||
|
0d7388899c | ||
|
4f094f7859 | ||
|
564ed59bb4 | ||
|
9f87b36ba1 | ||
|
e36c17fc72 | ||
|
3d59f2d7e0 | ||
|
ee0d750b85 | ||
|
d8563be38b | ||
|
81b0e0a64f | ||
|
7ef859ce96 | ||
|
a8d13b84b4 | ||
|
1699ab8c02 | ||
|
ca123515aa | ||
|
3b8f4a2fe8 | ||
|
f453c44d52 | ||
|
41870d22b0 | ||
|
b9c5cd248f | ||
|
162e509b8b | ||
|
60a2b58e57 | ||
|
2ae8cd5713 | ||
|
bc14e80b12 | ||
|
cd959bc522 | ||
|
2803d88894 |
66
CHANGELOG.md
66
CHANGELOG.md
@ -1,6 +1,42 @@
|
||||
CHANGELOG
|
||||
=========
|
||||
|
||||
Version 71 (January 4, 2025)
|
||||
----------------------------
|
||||
|
||||
(Version 71a was posted on January 6, 2025 and fixes a setup regression.)
|
||||
|
||||
Upgrades
|
||||
|
||||
* Roundcube upgraded to version 1.6.9.
|
||||
* Z-Push upgraded to version 2.7.5.
|
||||
|
||||
Automated Maintenance
|
||||
|
||||
* Daily automated tasks are now run at 1am in the box's timezone and full backups are now restricted to running only on Saturdays and Sundays at that time.
|
||||
* Backups now exclude the owncloud-backup folder so that we're not backing up backups.
|
||||
* Old TLS certificates are now automatically deleted to improve control panel performance.
|
||||
|
||||
Setup
|
||||
|
||||
* Fixed broken setup if SSH was configured to listen on multiple ports.
|
||||
* Ubuntu MOTD advertisements are now disabled.
|
||||
* Fixed missing Roundcube dependency package if NextCloud isn't installed.
|
||||
|
||||
Control Panel
|
||||
|
||||
* Improved status checks for secondary nameservers.
|
||||
* Spamhaus is now queried for the box's IPv6 address also.
|
||||
* DSA and EC private keys are now accepted for TLS certificates.
|
||||
* Timeouts for loading slow control panel pages are reduced.
|
||||
|
||||
And other minor fixes.
|
||||
|
||||
Version 70 (August 15, 2024)
|
||||
----------------------------
|
||||
|
||||
* Roundcube is updated to version 1.6.8 fixing security vulnerabilities.
|
||||
|
||||
Version 69 (July 20, 2024)
|
||||
--------------------------
|
||||
|
||||
@ -16,6 +52,8 @@ Other updates:
|
||||
* Added an undocumented option to proxy websockets.
|
||||
* Internal improvements to the code to make it more reliable and readable.
|
||||
|
||||
Version 69a (July 21, 2024) and 69b (July 23, 2024) correct setup failures.
|
||||
|
||||
Version 68 (April 1, 2024)
|
||||
--------------------------
|
||||
|
||||
@ -66,7 +104,7 @@ Version 64 (September 2, 2023)
|
||||
* Fixed backups to work with the latest duplicity package which was not backwards compatible.
|
||||
* Fixed setting B2 as a backup target with a slash in the application key.
|
||||
* Turned off OpenDMARC diagnostic reports sent in response to incoming mail.
|
||||
* Fixed some crashes when using an unrelased version of Mail-in-a-Box.
|
||||
* Fixed some crashes when using an unreleased version of Mail-in-a-Box.
|
||||
* Added z-push administration scripts.
|
||||
|
||||
Version 63 (July 27, 2023)
|
||||
@ -202,7 +240,7 @@ Other:
|
||||
|
||||
* Set systemd journald log retention to 10 days (from no limit) to reduce disk usage.
|
||||
* Fixed log processing for submission lines that have a sasl_sender or other extra information.
|
||||
* Fix DNS secondary nameserver refesh failure retry period.
|
||||
* Fix DNS secondary nameserver refresh failure retry period.
|
||||
|
||||
Version 55 (October 18, 2021)
|
||||
-----------------------------
|
||||
@ -227,7 +265,7 @@ Control panel:
|
||||
Other:
|
||||
|
||||
* Fail2ban's IPv6 support is enabled.
|
||||
* The mail log tool now doesn't crash if there are email addresess in log messages with invalid UTF-8 characters.
|
||||
* The mail log tool now doesn't crash if there are email addresses in log messages with invalid UTF-8 characters.
|
||||
* Additional nsd.conf files can be placed in /etc/nsd.conf.d.
|
||||
|
||||
v0.54 (June 20, 2021)
|
||||
@ -260,7 +298,7 @@ Setup:
|
||||
v0.53a (May 8, 2021)
|
||||
--------------------
|
||||
|
||||
The download URL for Z-Push has been revised becaue the old URL stopped working.
|
||||
The download URL for Z-Push has been revised because the old URL stopped working.
|
||||
|
||||
v0.53 (April 12, 2021)
|
||||
----------------------
|
||||
@ -479,7 +517,7 @@ Changes:
|
||||
* Added support for S3-compatible backup services besides Amazon S3.
|
||||
* Fixed the control panel login page to let LastPass save passwords.
|
||||
* Fixed an error in the user privileges API.
|
||||
* Silenced some spurrious messages.
|
||||
* Silenced some spurious messages.
|
||||
|
||||
Software updates:
|
||||
|
||||
@ -543,7 +581,7 @@ Setup:
|
||||
|
||||
Control Panel:
|
||||
|
||||
* The users page now documents that passwords should only have ASCII characters to prevent character encoding mismaches between clients and the server.
|
||||
* The users page now documents that passwords should only have ASCII characters to prevent character encoding mismatches between clients and the server.
|
||||
* The users page no longer shows user mailbox sizes because this was extremely slow for very large mailboxes.
|
||||
* The Mail-in-a-Box version is now shown in the system status checks even when the new-version check is disabled.
|
||||
* The alises page now warns that alises should not be used to forward mail off of the box. Mail filters within Roundcube are better for that.
|
||||
@ -871,7 +909,7 @@ v0.17c (April 1, 2016)
|
||||
|
||||
This update addresses some minor security concerns and some installation issues.
|
||||
|
||||
ownCoud:
|
||||
ownCloud:
|
||||
|
||||
* Block web access to the configuration parameters (config.php). There is no immediate impact (see [#776](https://github.com/mail-in-a-box/mailinabox/pull/776)), although advanced users may want to take note.
|
||||
|
||||
@ -887,7 +925,7 @@ Control panel:
|
||||
Setup:
|
||||
|
||||
* Setup dialogs did not appear correctly when connecting to SSH using Putty on Windows.
|
||||
* We now install Roundcube from our own mirror because Sourceforge's downloads experience frequent intermittant unavailability.
|
||||
* We now install Roundcube from our own mirror because Sourceforge's downloads experience frequent intermittent unavailability.
|
||||
|
||||
v0.17b (March 1, 2016)
|
||||
----------------------
|
||||
@ -930,7 +968,7 @@ This update primarily adds automatic SSL (now "TLS") certificate provisioning fr
|
||||
|
||||
Control Panel:
|
||||
|
||||
* The SSL certificates (now referred to as "TLS ccertificates") page now supports provisioning free certificates from Let's Encrypt.
|
||||
* The SSL certificates (now referred to as "TLS certificates") page now supports provisioning free certificates from Let's Encrypt.
|
||||
* Report free memory usage.
|
||||
* Fix a crash when the git directory is not checked out to a tag.
|
||||
* When IPv6 is enabled, check that all domains (besides the system hostname) resolve over IPv6.
|
||||
@ -1023,7 +1061,7 @@ Control panel:
|
||||
System:
|
||||
|
||||
* Tweaks to fail2ban settings.
|
||||
* Fixed a spurrious warning while installing munin.
|
||||
* Fixed a spurious warning while installing munin.
|
||||
|
||||
v0.13b (August 30, 2015)
|
||||
------------------------
|
||||
@ -1037,7 +1075,7 @@ Note: v0.13 (no 'a', August 19, 2015) was pulled immediately due to an ownCloud
|
||||
|
||||
Mail:
|
||||
|
||||
* Outbound mail headers (the Recieved: header) are tweaked to possibly improve deliverability.
|
||||
* Outbound mail headers (the Received: 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.
|
||||
|
||||
@ -1069,7 +1107,7 @@ v0.12c was posted to work around the current Sourceforge.net outage: pyzor's rem
|
||||
v0.12b (July 4, 2015)
|
||||
---------------------
|
||||
|
||||
This version corrects a minor regression in v0.12 related to creating aliases targetting multiple addresses.
|
||||
This version corrects a minor regression in v0.12 related to creating aliases targeting multiple addresses.
|
||||
|
||||
v0.12 (July 3, 2015)
|
||||
--------------------
|
||||
@ -1122,7 +1160,7 @@ Control panel:
|
||||
|
||||
System:
|
||||
* The munin system monitoring tool is now installed and accessible at /admin/munin.
|
||||
* ownCloud updated to version 8.0.4. The ownCloud installation step now is reslient to download problems. The ownCloud configuration file is now stored in STORAGE_ROOT to fix loss of data when moving STORAGE_ROOT to a new machine.
|
||||
* ownCloud updated to version 8.0.4. The ownCloud installation step now is resilient to download problems. The ownCloud configuration file is now stored in STORAGE_ROOT to fix loss of data when moving STORAGE_ROOT to a new machine.
|
||||
* The setup scripts now run `apt-get update` prior to installing anything to ensure the apt database is in sync with the packages actually available.
|
||||
|
||||
|
||||
@ -1160,7 +1198,7 @@ DNS:
|
||||
* Internationalized Domain Names (IDNs) should now work in email. If you had custom DNS or custom web settings for internationalized domains, check that they are still working.
|
||||
* It is now possible to set multiple TXT and other types of records on the same domain in the control panel.
|
||||
* The custom DNS API was completely rewritten to support setting multiple records of the same type on a domain. Any existing client code using the DNS API will have to be rewritten. (Existing code will just get 404s back.)
|
||||
* On some systems the `nsd` service failed to start if network inferfaces were not ready.
|
||||
* On some systems the `nsd` service failed to start if network interfaces were not ready.
|
||||
|
||||
System / Control Panel:
|
||||
|
||||
|
@ -2,13 +2,13 @@
|
||||
|
||||
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.
|
||||
We are committed to providing a safe, welcoming, and harassment-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 of Conduct applies to all places where Mail-in-a-Box community activity is occurring, 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.
|
||||
|
||||
|
@ -56,11 +56,11 @@ See the [setup guide](https://mailinabox.email/guide.html) for detailed, user-fr
|
||||
|
||||
For experts, start with a completely fresh (really, I mean it) Ubuntu 22.04 LTS 64-bit machine. On the machine...
|
||||
|
||||
Clone this repository and checkout the tag corresponding to the most recent release:
|
||||
Clone this repository and checkout the tag corresponding to the most recent release (which you can find in the tags or releases lists on GitHub):
|
||||
|
||||
$ git clone https://github.com/mail-in-a-box/mailinabox
|
||||
$ cd mailinabox
|
||||
$ git checkout v69
|
||||
$ git checkout TAGNAME
|
||||
|
||||
Begin the installation.
|
||||
|
||||
|
@ -52,7 +52,7 @@ namespace inbox {
|
||||
|
||||
# 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.
|
||||
# they're using. We'll keep that, plus add Junk as an alternative for Spam.
|
||||
# These are not auto-created.
|
||||
mailbox "Sent Messages" {
|
||||
special_use = \Sent
|
||||
|
@ -74,7 +74,7 @@ action = iptables-allports[name=recidive]
|
||||
# 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
|
||||
# So the notification is omitted. This will prevent message appearing in the mail.log that mail
|
||||
# can't be delivered to fail2ban@$HOSTNAME.
|
||||
|
||||
[postfix-sasl]
|
||||
|
@ -37,7 +37,7 @@
|
||||
return 403;
|
||||
}
|
||||
location ~ /mail/.*\.php {
|
||||
# note: ~ has precendence over a regular location block
|
||||
# note: ~ has precedence over a regular location block
|
||||
include fastcgi_params;
|
||||
fastcgi_split_path_info ^/mail(/.*)()$;
|
||||
fastcgi_index index.php;
|
||||
|
@ -38,7 +38,7 @@
|
||||
}
|
||||
}
|
||||
location ~ ^(/cloud)((?:/ocs)?/[^/]+\.php)(/.*)?$ {
|
||||
# note: ~ has precendence over a regular location block
|
||||
# note: ~ has precedence over a regular location block
|
||||
# Accept URLs like:
|
||||
# /cloud/index.php/apps/files/
|
||||
# /cloud/index.php/apps/files/ajax/scan.php (it's really index.php; see 6fdef379adfdeac86cc2220209bdf4eb9562268d)
|
||||
|
@ -9,13 +9,14 @@
|
||||
|
||||
import os, os.path, re, datetime, sys
|
||||
import dateutil.parser, dateutil.relativedelta, dateutil.tz
|
||||
from datetime import date
|
||||
import rtyaml
|
||||
from exclusiveprocess import Lock
|
||||
|
||||
from utils import load_environment, shell, wait_for_service
|
||||
|
||||
def backup_status(env):
|
||||
# If backups are dissbled, return no status.
|
||||
# If backups are disabled, return no status.
|
||||
config = get_backup_config(env)
|
||||
if config["target"] == "off":
|
||||
return { }
|
||||
@ -157,6 +158,8 @@ def should_force_full(config, env):
|
||||
# since the last full backup is greater than half the size
|
||||
# of that full backup.
|
||||
inc_size = 0
|
||||
# Check if day of week is a weekend day
|
||||
weekend = date.today().weekday()>=5
|
||||
for bak in backup_status(env)["backups"]:
|
||||
if not bak["full"]:
|
||||
# Scan through the incremental backups cumulating
|
||||
@ -165,8 +168,10 @@ def should_force_full(config, env):
|
||||
else:
|
||||
# ...until we reach the most recent full backup.
|
||||
# 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.
|
||||
# on whether it is a weekend day, the size of the
|
||||
# increments relative to the full backup, as well as
|
||||
# the age of the full backup.
|
||||
if weekend:
|
||||
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()):
|
||||
@ -320,6 +325,7 @@ def perform_backup(full_backup):
|
||||
"--verbosity", "warning", "--no-print-statistics",
|
||||
"--archive-dir", backup_cache_dir,
|
||||
"--exclude", backup_root,
|
||||
"--exclude", os.path.join(env["STORAGE_ROOT"], "owncloud-backup"),
|
||||
"--volsize", "250",
|
||||
"--gpg-options", "'--cipher-algo=AES256'",
|
||||
"--allow-source-mismatch",
|
||||
@ -399,6 +405,7 @@ def run_duplicity_verification():
|
||||
"--compare-data",
|
||||
"--archive-dir", backup_cache_dir,
|
||||
"--exclude", backup_root,
|
||||
"--exclude", os.path.join(env["STORAGE_ROOT"], "owncloud-backup"),
|
||||
*get_duplicity_additional_args(env),
|
||||
get_duplicity_target_url(config),
|
||||
env["STORAGE_ROOT"],
|
||||
@ -512,6 +519,9 @@ def list_target_files(config):
|
||||
|
||||
# connect to the region & bucket
|
||||
try:
|
||||
if config['target_user'] == "" and config['target_pass'] == "":
|
||||
s3 = boto3.client('s3', endpoint_url=f'https://{target.hostname}')
|
||||
else:
|
||||
s3 = boto3.client('s3', \
|
||||
endpoint_url=f'https://{target.hostname}', \
|
||||
aws_access_key_id=config['target_user'], \
|
||||
|
@ -65,6 +65,7 @@ if len(sys.argv) < 2:
|
||||
{cli} user password user@domain.com [password]
|
||||
{cli} user remove user@domain.com
|
||||
{cli} user make-admin user@domain.com
|
||||
{cli} user quota user@domain [new-quota] (get or set user quota)
|
||||
{cli} user remove-admin user@domain.com
|
||||
{cli} user admins (lists admins)
|
||||
{cli} user mfa show user@domain.com (shows MFA devices for user, if any)
|
||||
@ -117,6 +118,14 @@ elif sys.argv[1] == "user" and sys.argv[2] == "admins":
|
||||
if "admin" in user['privileges']:
|
||||
print(user['email'])
|
||||
|
||||
elif sys.argv[1] == "user" and sys.argv[2] == "quota" and len(sys.argv) == 4:
|
||||
# Get a user's quota
|
||||
print(mgmt("/mail/users/quota?text=1&email=%s" % sys.argv[3]))
|
||||
|
||||
elif sys.argv[1] == "user" and sys.argv[2] == "quota" and len(sys.argv) == 5:
|
||||
# Set a user's quota
|
||||
users = mgmt("/mail/users/quota", { "email": sys.argv[3], "quota": sys.argv[4] })
|
||||
|
||||
elif sys.argv[1] == "user" and len(sys.argv) == 5 and sys.argv[2:4] == ["mfa", "show"]:
|
||||
# Show MFA status for a user.
|
||||
status = mgmt("/mfa/status", { "user": sys.argv[4] }, is_json=True)
|
||||
@ -141,4 +150,3 @@ elif sys.argv[1] == "alias" and sys.argv[2] == "remove" and len(sys.argv) == 4:
|
||||
else:
|
||||
print("Invalid command-line arguments.")
|
||||
sys.exit(1)
|
||||
|
||||
|
@ -21,6 +21,7 @@ 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
|
||||
from mailconfig import get_mail_quota, set_mail_quota
|
||||
from mfa import get_public_mfa_state, provision_totp, validate_totp_secret, enable_mfa, disable_mfa
|
||||
import contextlib
|
||||
|
||||
@ -191,8 +192,31 @@ def mail_users():
|
||||
@app.route('/mail/users/add', methods=['POST'])
|
||||
@authorized_personnel_only
|
||||
def mail_users_add():
|
||||
quota = request.form.get('quota', '0')
|
||||
try:
|
||||
return add_mail_user(request.form.get('email', ''), request.form.get('password', ''), request.form.get('privileges', ''), env)
|
||||
return add_mail_user(request.form.get('email', ''), request.form.get('password', ''), request.form.get('privileges', ''), quota, env)
|
||||
except ValueError as e:
|
||||
return (str(e), 400)
|
||||
|
||||
@app.route('/mail/users/quota', methods=['GET'])
|
||||
@authorized_personnel_only
|
||||
def get_mail_users_quota():
|
||||
email = request.values.get('email', '')
|
||||
quota = get_mail_quota(email, env)
|
||||
|
||||
if request.values.get('text'):
|
||||
return quota
|
||||
|
||||
return json_response({
|
||||
"email": email,
|
||||
"quota": quota
|
||||
})
|
||||
|
||||
@app.route('/mail/users/quota', methods=['POST'])
|
||||
@authorized_personnel_only
|
||||
def mail_users_quota():
|
||||
try:
|
||||
return set_mail_quota(request.form.get('email', ''), request.form.get('quota'), env)
|
||||
except ValueError as e:
|
||||
return (str(e), 400)
|
||||
|
||||
|
@ -15,7 +15,7 @@ import contextlib
|
||||
|
||||
# From https://stackoverflow.com/questions/3026957/how-to-validate-a-domain-name-using-regex-php/16491074#16491074
|
||||
# This regular expression matches domain names according to RFCs, it also accepts fqdn with an leading dot,
|
||||
# underscores, as well as asteriks which are allowed in domain names but not hostnames (i.e. allowed in
|
||||
# underscores, as well as asterisks which are allowed in domain names but not hostnames (i.e. allowed in
|
||||
# DNS but not in URLs), which are common in certain record types like for DKIM.
|
||||
DOMAIN_RE = r"^(?!\-)(?:[*][.])?(?:[a-zA-Z\d\-_]{0,62}[a-zA-Z\d_]\.){1,126}(?!\d+)[a-zA-Z\d_]{1,63}(\.?)$"
|
||||
|
||||
@ -443,7 +443,7 @@ def build_sshfp_records():
|
||||
|
||||
# Get our local fingerprints by running ssh-keyscan. The output looks
|
||||
# like the known_hosts file: hostname, keytype, fingerprint. The order
|
||||
# of the output is arbitrary, so sort it to prevent spurrious updates
|
||||
# of the output is arbitrary, so sort it to prevent spurious updates
|
||||
# to the zone file (that trigger bumping the serial number). However,
|
||||
# if SSH has been configured to listen on a nonstandard port, we must
|
||||
# specify that port to sshkeyscan.
|
||||
|
@ -319,7 +319,7 @@ def scan_mail_log(env):
|
||||
|
||||
if collector["other-services"] and VERBOSE and False:
|
||||
print_header("Other services")
|
||||
print("The following unkown services were found in the log file.")
|
||||
print("The following unknown services were found in the log file.")
|
||||
print(" ", *sorted(collector["other-services"]), sep='\n│ ')
|
||||
|
||||
|
||||
|
@ -10,6 +10,8 @@
|
||||
# address entered by the user.
|
||||
|
||||
import os, sqlite3, re
|
||||
import subprocess
|
||||
|
||||
import utils
|
||||
from email_validator import validate_email as validate_email_, EmailNotValidError
|
||||
import idna
|
||||
@ -102,6 +104,18 @@ def get_mail_users(env):
|
||||
users = [ row[0] for row in c.fetchall() ]
|
||||
return utils.sort_email_addresses(users, env)
|
||||
|
||||
def sizeof_fmt(num):
|
||||
for unit in ['','K','M','G','T']:
|
||||
if abs(num) < 1024.0:
|
||||
if abs(num) > 99:
|
||||
return "%3.0f%s" % (num, unit)
|
||||
else:
|
||||
return "%2.1f%s" % (num, unit)
|
||||
|
||||
num /= 1024.0
|
||||
|
||||
return str(num)
|
||||
|
||||
def get_mail_users_ex(env, with_archived=False):
|
||||
# Returns a complex data structure of all user accounts, optionally
|
||||
# including archived (status="inactive") accounts.
|
||||
@ -125,13 +139,42 @@ def get_mail_users_ex(env, with_archived=False):
|
||||
users = []
|
||||
active_accounts = set()
|
||||
c = open_database(env)
|
||||
c.execute('SELECT email, privileges FROM users')
|
||||
for email, privileges in c.fetchall():
|
||||
c.execute('SELECT email, privileges, quota FROM users')
|
||||
for email, privileges, quota in c.fetchall():
|
||||
active_accounts.add(email)
|
||||
|
||||
(user, domain) = email.split('@')
|
||||
box_size = 0
|
||||
box_quota = 0
|
||||
percent = ''
|
||||
try:
|
||||
dirsize_file = os.path.join(env['STORAGE_ROOT'], 'mail/mailboxes/%s/%s/maildirsize' % (domain, user))
|
||||
with open(dirsize_file, 'r') as f:
|
||||
box_quota = int(f.readline().split('S')[0])
|
||||
for line in f.readlines():
|
||||
(size, count) = line.split(' ')
|
||||
box_size += int(size)
|
||||
|
||||
try:
|
||||
percent = (box_size / box_quota) * 100
|
||||
except:
|
||||
percent = 'Error'
|
||||
|
||||
except:
|
||||
box_size = '?'
|
||||
box_quota = '?'
|
||||
percent = '?'
|
||||
|
||||
if quota == '0':
|
||||
percent = ''
|
||||
|
||||
user = {
|
||||
"email": email,
|
||||
"privileges": parse_privs(privileges),
|
||||
"quota": quota,
|
||||
"box_quota": box_quota,
|
||||
"box_size": sizeof_fmt(box_size) if box_size != '?' else box_size,
|
||||
"percent": '%3.0f%%' % percent if type(percent) != str else percent,
|
||||
"status": "active",
|
||||
}
|
||||
users.append(user)
|
||||
@ -150,6 +193,9 @@ def get_mail_users_ex(env, with_archived=False):
|
||||
"privileges": [],
|
||||
"status": "inactive",
|
||||
"mailbox": mbox,
|
||||
"box_size": '?',
|
||||
"box_quota": '?',
|
||||
"percent": '?',
|
||||
}
|
||||
users.append(user)
|
||||
|
||||
@ -266,7 +312,7 @@ def get_mail_domains(env, filter_aliases=lambda alias : True, users_only=False):
|
||||
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):
|
||||
def add_mail_user(email, pw, privs, quota, env):
|
||||
# validate email
|
||||
if email.strip() == "":
|
||||
return ("No email address provided.", 400)
|
||||
@ -292,6 +338,14 @@ def add_mail_user(email, pw, privs, env):
|
||||
validation = validate_privilege(p)
|
||||
if validation: return validation
|
||||
|
||||
if quota is None:
|
||||
quota = '0'
|
||||
|
||||
try:
|
||||
quota = validate_quota(quota)
|
||||
except ValueError as e:
|
||||
return (str(e), 400)
|
||||
|
||||
# get the database
|
||||
conn, c = open_database(env, with_connection=True)
|
||||
|
||||
@ -300,14 +354,16 @@ def add_mail_user(email, pw, privs, env):
|
||||
|
||||
# add the user to the database
|
||||
try:
|
||||
c.execute("INSERT INTO users (email, password, privileges) VALUES (?, ?, ?)",
|
||||
(email, pw, "\n".join(privs)))
|
||||
c.execute("INSERT INTO users (email, password, privileges, quota) VALUES (?, ?, ?, ?)",
|
||||
(email, pw, "\n".join(privs), quota))
|
||||
except sqlite3.IntegrityError:
|
||||
return ("User already exists.", 400)
|
||||
|
||||
# write databasebefore next step
|
||||
conn.commit()
|
||||
|
||||
dovecot_quota_recalc(email)
|
||||
|
||||
# Update things in case any new domains are added.
|
||||
return kick(env, "mail user added")
|
||||
|
||||
@ -332,6 +388,55 @@ def hash_password(pw):
|
||||
# http://wiki2.dovecot.org/Authentication/PasswordSchemes
|
||||
return utils.shell('check_output', ["/usr/bin/doveadm", "pw", "-s", "SHA512-CRYPT", "-p", pw]).strip()
|
||||
|
||||
|
||||
def get_mail_quota(email, env):
|
||||
conn, c = open_database(env, with_connection=True)
|
||||
c.execute("SELECT quota FROM users WHERE email=?", (email,))
|
||||
rows = c.fetchall()
|
||||
if len(rows) != 1:
|
||||
return ("That's not a user (%s)." % email, 400)
|
||||
|
||||
return rows[0][0]
|
||||
|
||||
|
||||
def set_mail_quota(email, quota, env):
|
||||
# validate that password is acceptable
|
||||
quota = validate_quota(quota)
|
||||
|
||||
# update the database
|
||||
conn, c = open_database(env, with_connection=True)
|
||||
c.execute("UPDATE users SET quota=? WHERE email=?", (quota, email))
|
||||
if c.rowcount != 1:
|
||||
return ("That's not a user (%s)." % email, 400)
|
||||
conn.commit()
|
||||
|
||||
dovecot_quota_recalc(email)
|
||||
|
||||
return "OK"
|
||||
|
||||
def dovecot_quota_recalc(email):
|
||||
# dovecot processes running for the user will not recognize the new quota setting
|
||||
# a reload is necessary to reread the quota setting, but it will also shut down
|
||||
# running dovecot processes. Email clients generally log back in when they lose
|
||||
# a connection.
|
||||
# subprocess.call(['doveadm', 'reload'])
|
||||
|
||||
# force dovecot to recalculate the quota info for the user.
|
||||
subprocess.call(["doveadm", "quota", "recalc", "-u", email])
|
||||
|
||||
def validate_quota(quota):
|
||||
# validate quota
|
||||
quota = quota.strip().upper()
|
||||
|
||||
if quota == "":
|
||||
raise ValueError("No quota provided.")
|
||||
if re.search(r"[\s,.]", quota):
|
||||
raise ValueError("Quotas cannot contain spaces, commas, or decimal points.")
|
||||
if not re.match(r'^[\d]+[GM]?$', quota):
|
||||
raise ValueError("Invalid quota.")
|
||||
|
||||
return quota
|
||||
|
||||
def get_mail_password(email, env):
|
||||
# Gets the hashed password for a user. Passwords are stored in Dovecot's
|
||||
# password format, with a prefixed scheme.
|
||||
@ -561,7 +666,7 @@ def kick(env, mail_result=None):
|
||||
|
||||
auto_aliases = { }
|
||||
|
||||
# Mape required aliases to the administrator alias (which should be created manually).
|
||||
# Map required aliases to the administrator alias (which should be created manually).
|
||||
administrator = get_system_administrator(env)
|
||||
required_aliases = get_required_aliases(env)
|
||||
for alias in required_aliases:
|
||||
|
@ -14,7 +14,7 @@ def get_ssl_certificates(env):
|
||||
# that the certificates are good for to the best certificate for
|
||||
# the domain.
|
||||
|
||||
from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey
|
||||
from cryptography.hazmat.primitives.asymmetric import dsa, rsa, ec
|
||||
from cryptography.x509 import Certificate
|
||||
|
||||
# The certificates are all stored here:
|
||||
@ -59,13 +59,15 @@ def get_ssl_certificates(env):
|
||||
# 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 })
|
||||
# It is a private key
|
||||
elif (isinstance(pem, rsa.RSAPrivateKey)
|
||||
or isinstance(pem, dsa.DSAPrivateKey)
|
||||
or isinstance(pem, ec.EllipticCurvePrivateKey)):
|
||||
private_keys[pem.public_key().public_numbers()] = { "filename": fn, "key": pem }
|
||||
|
||||
|
||||
# Process the certificates.
|
||||
domains = { }
|
||||
@ -434,7 +436,7 @@ def install_cert(domain, ssl_cert, ssl_chain, env, raw=False):
|
||||
cert_status += " " + cert_status_details
|
||||
return cert_status
|
||||
|
||||
# Copy certifiate into ssl directory.
|
||||
# Copy certificate into ssl directory.
|
||||
install_cert_copy_file(fn, env)
|
||||
|
||||
# Run post-install steps.
|
||||
@ -505,7 +507,7 @@ def check_certificate(domain, ssl_certificate, ssl_private_key, warn_if_expiring
|
||||
# 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.hazmat.primitives.asymmetric import rsa, dsa, ec
|
||||
from cryptography.x509 import Certificate
|
||||
|
||||
# The ssl_certificate file may contain a chain of certificates. We'll
|
||||
@ -539,7 +541,9 @@ def check_certificate(domain, ssl_certificate, ssl_private_key, warn_if_expiring
|
||||
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):
|
||||
if (not isinstance(priv_key, rsa.RSAPrivateKey)
|
||||
and not isinstance(priv_key, dsa.DSAPrivateKey)
|
||||
and not isinstance(priv_key, ec.EllipticCurvePrivateKey)):
|
||||
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():
|
||||
@ -639,7 +643,7 @@ def load_pem(pem):
|
||||
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"}:
|
||||
if pem_type.endswith(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())
|
||||
|
@ -282,26 +282,45 @@ def run_network_checks(env, output):
|
||||
# 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.
|
||||
|
||||
# See https://www.spamhaus.org/news/article/807/using-our-public-mirrors-check-your-return-codes-now. for
|
||||
# information on spamhaus return codes
|
||||
rev_ip4 = ".".join(reversed(env['PUBLIC_IP'].split('.')))
|
||||
zen = query_dns(rev_ip4+'.zen.spamhaus.org', 'A', nxdomain=None)
|
||||
evaluate_spamhaus_lookup(env['PUBLIC_IP'], 'IPv4', rev_ip4, output, zen)
|
||||
|
||||
if not env['PUBLIC_IPV6']:
|
||||
return
|
||||
|
||||
from ipaddress import IPv6Address
|
||||
|
||||
rev_ip6 = ".".join(reversed(IPv6Address(env['PUBLIC_IPV6']).exploded.split(':')))
|
||||
zen = query_dns(rev_ip6+'.zen.spamhaus.org', 'A', nxdomain=None)
|
||||
evaluate_spamhaus_lookup(env['PUBLIC_IPV6'], 'IPv6', rev_ip6, output, zen)
|
||||
|
||||
|
||||
def evaluate_spamhaus_lookup(lookupaddress, lookuptype, lookupdomain, output, zen):
|
||||
# See https://www.spamhaus.org/news/article/807/using-our-public-mirrors-check-your-return-codes-now. for
|
||||
# information on spamhaus return codes
|
||||
if zen is None:
|
||||
output.print_ok("IP address is not blacklisted by zen.spamhaus.org.")
|
||||
output.print_ok(f"{lookuptype} address is not blacklisted by zen.spamhaus.org.")
|
||||
elif zen == "[timeout]":
|
||||
output.print_warning("Connection to zen.spamhaus.org timed out. Could not determine whether this box's IP address is blacklisted. Please try again later.")
|
||||
output.print_warning(f"""Connection to zen.spamhaus.org timed out. Could not determine whether this box's
|
||||
{lookuptype} address is blacklisted. Please try again later.""")
|
||||
elif zen == "[Not Set]":
|
||||
output.print_warning("Could not connect to zen.spamhaus.org. Could not determine whether this box's IP address is blacklisted. Please try again later.")
|
||||
output.print_warning(f"""Could not connect to zen.spamhaus.org. Could not determine whether this box's
|
||||
{lookuptype} address is blacklisted. Please try again later.""")
|
||||
elif zen == "127.255.255.252":
|
||||
output.print_warning("Incorrect spamhaus query: %s. Could not determine whether this box's IP address is blacklisted." % (rev_ip4+'.zen.spamhaus.org'))
|
||||
output.print_warning(f"""Incorrect spamhaus query: {lookupdomain + '.zen.spamhaus.org'}. Could not determine whether
|
||||
this box's {lookuptype} address is blacklisted.""")
|
||||
elif zen == "127.255.255.254":
|
||||
output.print_warning("Mail-in-a-Box is configured to use a public DNS server. This is not supported by spamhaus. Could not determine whether this box's IP address is blacklisted.")
|
||||
output.print_warning(f"""Mail-in-a-Box is configured to use a public DNS server. This is not supported by
|
||||
spamhaus. Could not determine whether this box's {lookuptype} address is blacklisted.""")
|
||||
elif zen == "127.255.255.255":
|
||||
output.print_warning("Too many queries have been performed on the spamhaus server. Could not determine whether this box's IP address is blacklisted.")
|
||||
output.print_warning(f"""Too many queries have been performed on the spamhaus server. Could not determine
|
||||
whether this box's {lookuptype} address is blacklisted.""")
|
||||
else:
|
||||
output.print_error("""The IP address of this machine {} is listed in the Spamhaus Block List (code {}),
|
||||
which may prevent recipients from receiving your email. See http://www.spamhaus.org/query/ip/{}.""".format(env['PUBLIC_IP'], zen, env['PUBLIC_IP']))
|
||||
output.print_error(f"""The {lookuptype} address of this machine {lookupaddress} is listed in the Spamhaus Block
|
||||
List (code {zen}), which may prevent recipients from receiving your email. See
|
||||
http://www.spamhaus.org/query/ip/{lookupaddress}.""")
|
||||
|
||||
|
||||
def run_domain_checks(rounded_time, env, output, pool, domains_to_check=None):
|
||||
# Get the list of domains we handle mail for.
|
||||
@ -521,6 +540,8 @@ def check_dns_zone(domain, env, output, dns_zonefiles):
|
||||
# Check that each custom secondary nameserver resolves the IP address.
|
||||
|
||||
if custom_secondary_ns and not probably_external_dns:
|
||||
SOARecord = query_dns(domain, "SOA", at=env['PUBLIC_IP'])# Explicitly ask the local dns server.
|
||||
|
||||
for ns in custom_secondary_ns:
|
||||
# We must first resolve the nameserver to an IP address so we can query it.
|
||||
ns_ips = query_dns(ns, "A")
|
||||
@ -530,15 +551,36 @@ def check_dns_zone(domain, env, output, dns_zonefiles):
|
||||
# Choose the first IP if nameserver returns multiple
|
||||
ns_ip = ns_ips.split('; ')[0]
|
||||
|
||||
checkSOA = True
|
||||
|
||||
# Now query it to see what it says about this domain.
|
||||
ip = query_dns(domain, "A", at=ns_ip, nxdomain=None)
|
||||
if ip == correct_ip:
|
||||
output.print_ok("Secondary nameserver %s resolved the domain correctly." % ns)
|
||||
output.print_ok(f"Secondary nameserver {ns} resolved the domain correctly.")
|
||||
elif ip is None:
|
||||
output.print_error("Secondary nameserver %s is not configured to resolve this domain." % ns)
|
||||
output.print_error(f"Secondary nameserver {ns} is not configured to resolve this domain.")
|
||||
# No need to check SOA record if not configured as nameserver
|
||||
checkSOA = False
|
||||
elif ip == '[timeout]':
|
||||
output.print_error(f"Secondary nameserver {ns} did not resolve this domain, result: {ip}")
|
||||
checkSOA = False
|
||||
else:
|
||||
output.print_error(f"Secondary nameserver {ns} is not configured correctly. (It resolved this domain as {ip}. It should be {correct_ip}.)")
|
||||
|
||||
if checkSOA:
|
||||
# Check that secondary DNS server is synchronized with our primary DNS server. Simplified by checking the SOA record which has a version number
|
||||
SOASecondary = query_dns(domain, "SOA", at=ns_ip)
|
||||
|
||||
if SOARecord == SOASecondary:
|
||||
output.print_ok(f"Secondary nameserver {ns} has consistent SOA record.")
|
||||
elif SOARecord == '[Not Set]':
|
||||
output.print_error(f"Secondary nameserver {ns} has no SOA record configured.")
|
||||
elif SOARecord == '[timeout]':
|
||||
output.print_error(f"Secondary nameserver {ns} timed out on checking SOA record.")
|
||||
else:
|
||||
output.print_error(f"""Secondary nameserver {ns} has inconsistent SOA record (primary: {SOARecord} versus secondary: {SOASecondary}).
|
||||
Check that synchronization between secondary and primary DNS servers is properly set-up.""")
|
||||
|
||||
def check_dns_zone_suggestions(domain, env, output, dns_zonefiles, domains_with_a_records):
|
||||
# Warn if a custom DNS record is preventing this or the automatic www redirect from
|
||||
# being served.
|
||||
|
@ -73,7 +73,7 @@
|
||||
filter: invert(100%) hue-rotate(180deg);
|
||||
}
|
||||
|
||||
/* Override Boostrap theme here to give more contrast. The black turns to white by the filter. */
|
||||
/* Override Bootstrap theme here to give more contrast. The black turns to white by the filter. */
|
||||
.form-control {
|
||||
color: black !important;
|
||||
}
|
||||
@ -392,7 +392,9 @@ function api(url, method, data, callback, callback_error, headers) {
|
||||
403: function(xhr) {
|
||||
// Credentials are no longer valid. Try to login again.
|
||||
var p = current_panel;
|
||||
clear_credentials();
|
||||
show_panel('login');
|
||||
show_hide_menus();
|
||||
switch_back_to_panel = p;
|
||||
}
|
||||
}
|
||||
@ -402,16 +404,21 @@ function api(url, method, data, callback, callback_error, headers) {
|
||||
var current_panel = null;
|
||||
var switch_back_to_panel = null;
|
||||
|
||||
function do_logout() {
|
||||
// Clear the session from the backend.
|
||||
api("/logout", "POST");
|
||||
|
||||
function clear_credentials() {
|
||||
// Forget the token.
|
||||
api_credentials = null;
|
||||
if (typeof localStorage != 'undefined')
|
||||
localStorage.removeItem("miab-cp-credentials");
|
||||
if (typeof sessionStorage != 'undefined')
|
||||
sessionStorage.removeItem("miab-cp-credentials");
|
||||
}
|
||||
|
||||
function do_logout() {
|
||||
// Clear the session from the backend.
|
||||
api("/logout", "POST");
|
||||
|
||||
// Remove locally stored credentials
|
||||
clear_credentials();
|
||||
|
||||
// Return to the start.
|
||||
show_panel('login');
|
||||
|
@ -361,7 +361,7 @@ function init_inputs(target_type) {
|
||||
}
|
||||
|
||||
// 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
|
||||
// the first occurrence 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);
|
||||
|
@ -6,6 +6,7 @@
|
||||
#user_table .account_inactive .if_active { display: none; }
|
||||
#user_table .account_active .if_inactive { display: none; }
|
||||
#user_table .account_active.if_inactive { display: none; }
|
||||
.row-center { text-align: center; }
|
||||
</style>
|
||||
|
||||
<h3>Add a mail user</h3>
|
||||
@ -27,6 +28,10 @@
|
||||
<option value="admin">Administrator</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="sr-only" for="adduserQuota">Quota</label>
|
||||
<input type="text" class="form-control" id="adduserQuota" placeholder="Quota" style="width:5em;" value="0">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Add User</button>
|
||||
</form>
|
||||
<ul style="margin-top: 1em; padding-left: 1.5em; font-size: 90%;">
|
||||
@ -34,13 +39,17 @@
|
||||
<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="#aliases">aliases</a> can.</li>
|
||||
<li>Quotas may not contain any spaces, commas or decimal points. Suffixes of G (gigabytes) and M (megabytes) are allowed. For unlimited storage enter 0 (zero)</li>
|
||||
</ul>
|
||||
|
||||
<h3>Existing mail users</h3>
|
||||
<table id="user_table" class="table" style="width: auto">
|
||||
<thead>
|
||||
<tr>
|
||||
<th width="50%">Email Address</th>
|
||||
<th width="35%">Email Address</th>
|
||||
<th class="row-center">Size</th>
|
||||
<th class="row-center">Used</th>
|
||||
<th class="row-center">Quota</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@ -53,10 +62,21 @@
|
||||
<tr id="user-template">
|
||||
<td class='address'>
|
||||
</td>
|
||||
<td class="box-size row-center"></td>
|
||||
<td class="percent row-center"></td>
|
||||
<td class="quota row-center">
|
||||
</td>
|
||||
<td class='actions'>
|
||||
<span class='privs'>
|
||||
</span>
|
||||
|
||||
<span class="if_active">
|
||||
<a href="#" onclick="users_set_quota(this); return false;" class='setquota' title="Set Quota">
|
||||
set quota
|
||||
</a>
|
||||
|
|
||||
</span>
|
||||
|
||||
<span class="if_active">
|
||||
<a href="#" onclick="users_set_password(this); return false;" class='setpw' title="Set Password">
|
||||
set password
|
||||
@ -97,10 +117,28 @@
|
||||
<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>/add</td>
|
||||
<td>Adds a new mail user. Required POST-body parameters are <code>email</code> and <code>password</code>. Optional parameters: <code>privilege=admin</code> and <code>quota</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>POST</td>
|
||||
<td>/remove</td>
|
||||
<td>Removes a mail user. Required POST-by 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>
|
||||
<tr>
|
||||
<td>GET</td>
|
||||
<td>/quota</td>
|
||||
<td>Get the quota for a mail user. Required POST-body parameters are <code>email</code> and will return JSON result</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>POST</td>
|
||||
<td>/quota</td>
|
||||
<td>Set the quota for a mail user. Required POST-body parameters are <code>email</code> and <code>quota</code>.</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<h4>Examples:</h4>
|
||||
@ -133,7 +171,7 @@ function show_users() {
|
||||
function(r) {
|
||||
$('#user_table tbody').html("");
|
||||
for (var i = 0; i < r.length; i++) {
|
||||
var hdr = $("<tr><th colspan='2' style='background-color: #EEE'></th></tr>");
|
||||
var hdr = $("<tr><th colspan='6' style='background-color: #EEE'></th></tr>");
|
||||
hdr.find('th').text(r[i].domain);
|
||||
$('#user_table tbody').append(hdr);
|
||||
|
||||
@ -151,7 +189,14 @@ function show_users() {
|
||||
n2.addClass("account_" + user.status);
|
||||
|
||||
n.attr('data-email', user.email);
|
||||
n.find('.address').text(user.email)
|
||||
n.attr('data-quota', user.quota);
|
||||
n.find('.address').text(user.email);
|
||||
n.find('.box-size').text(user.box_size);
|
||||
if (user.box_size == '?') {
|
||||
n.find('.box-size').attr('title', 'Mailbox size is unkown')
|
||||
}
|
||||
n.find('.percent').text(user.percent);
|
||||
n.find('.quota').text((user.quota == '0') ? 'unlimited' : user.quota);
|
||||
n2.find('.restore_info tt').text(user.mailbox);
|
||||
|
||||
if (user.status == 'inactive') continue;
|
||||
@ -180,13 +225,15 @@ function do_add_user() {
|
||||
var email = $("#adduserEmail").val();
|
||||
var pw = $("#adduserPassword").val();
|
||||
var privs = $("#adduserPrivs").val();
|
||||
var quota = $("#adduserQuota").val();
|
||||
api(
|
||||
"/mail/users/add",
|
||||
"POST",
|
||||
{
|
||||
email: email,
|
||||
password: pw,
|
||||
privileges: privs
|
||||
privileges: privs,
|
||||
quota: quota
|
||||
},
|
||||
function(r) {
|
||||
// Responses are multiple lines of pre-formatted text.
|
||||
@ -228,6 +275,36 @@ function users_set_password(elem) {
|
||||
});
|
||||
}
|
||||
|
||||
function users_set_quota(elem) {
|
||||
var email = $(elem).parents('tr').attr('data-email');
|
||||
var quota = $(elem).parents('tr').attr('data-quota');
|
||||
|
||||
show_modal_confirm(
|
||||
"Set Quota",
|
||||
$("<p>Set quota for <b>" + email + "</b>?</p>" +
|
||||
"<p>" +
|
||||
"<label for='users_set_quota' style='display: block; font-weight: normal'>Quota:</label>" +
|
||||
"<input type='text' id='users_set_quota' value='" + quota + "'></p>" +
|
||||
"<p><small>Quotas may not contain any spaces or commas. Suffixes of G (gigabytes) and M (megabytes) are allowed.</small></p>" +
|
||||
"<p><small>For unlimited storage enter 0 (zero)</small></p>"),
|
||||
"Set Quota",
|
||||
function() {
|
||||
api(
|
||||
"/mail/users/quota",
|
||||
"POST",
|
||||
{
|
||||
email: email,
|
||||
quota: $('#users_set_quota').val()
|
||||
},
|
||||
function(r) {
|
||||
show_users();
|
||||
},
|
||||
function(r) {
|
||||
show_modal_error("Set Quota", r);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function users_remove(elem) {
|
||||
var email = $(elem).parents('tr').attr('data-email');
|
||||
|
||||
@ -293,7 +370,7 @@ function generate_random_password() {
|
||||
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");
|
||||
show_modal_error("Random Password", "<p>Here, try this:</p> <p><code style='font-size: 110%'>" + pw + "</code></p>");
|
||||
return false; // cancel click
|
||||
}
|
||||
</script>
|
||||
|
@ -189,6 +189,7 @@ def get_ssh_port():
|
||||
|
||||
def get_ssh_config_value(parameter_name):
|
||||
# Returns ssh configuration value for the provided parameter
|
||||
import subprocess
|
||||
try:
|
||||
output = shell('check_output', ['sshd', '-T'])
|
||||
except FileNotFoundError:
|
||||
|
@ -23,7 +23,7 @@ if [ -z "$TAG" ]; then
|
||||
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=v69
|
||||
TAG=v71a
|
||||
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.
|
||||
@ -89,4 +89,3 @@ fi
|
||||
|
||||
# Start setup script.
|
||||
setup/start.sh
|
||||
|
||||
|
@ -66,7 +66,7 @@ tools/editconf.py /etc/opendmarc.conf -s \
|
||||
"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
|
||||
# of the message. This is useful if you want the filter to perform 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.
|
||||
|
||||
|
@ -101,7 +101,7 @@ if [ ! -f "$STORAGE_ROOT/dns/dnssec/$algo.conf" ]; then
|
||||
# we're capturing into the `KSK` variable.
|
||||
#
|
||||
# ldns-keygen uses /dev/random for generating random numbers by default.
|
||||
# This is slow and unecessary if we ensure /dev/urandom is seeded properly,
|
||||
# This is slow and unnecessary 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
|
||||
|
@ -1,7 +1,7 @@
|
||||
#!/bin/bash
|
||||
# If there aren't any mail users yet, create one.
|
||||
if [ -z "$(management/cli.py user)" ]; then
|
||||
# The outut of "management/cli.py user" is a list of mail users. If there
|
||||
# The output 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.
|
||||
@ -48,7 +48,7 @@ if [ -z "$(management/cli.py user)" ]; then
|
||||
fi
|
||||
|
||||
# Create the user's mail account. This will ask for a password if none was given above.
|
||||
management/cli.py user add "$EMAIL_ADDR" "${EMAIL_PW:-}"
|
||||
management/cli.py user add "$EMAIL_ADDR" ${EMAIL_PW:+"$EMAIL_PW"}
|
||||
|
||||
# Make it an admin.
|
||||
hide_output management/cli.py user make-admin "$EMAIL_ADDR"
|
||||
|
@ -67,6 +67,32 @@ tools/editconf.py /etc/dovecot/conf.d/10-mail.conf \
|
||||
|
||||
# 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
|
||||
sed -i "s/#mail_plugins =\(.*\)/mail_plugins =\1 \$mail_plugins quota/" /etc/dovecot/conf.d/10-mail.conf
|
||||
if ! grep -q "mail_plugins.* imap_quota" /etc/dovecot/conf.d/20-imap.conf; then
|
||||
sed -i "s/\(mail_plugins =.*\)/\1\n mail_plugins = \$mail_plugins imap_quota/" /etc/dovecot/conf.d/20-imap.conf
|
||||
fi
|
||||
|
||||
# configure stuff for quota support
|
||||
if ! grep -q "quota_status_success = DUNNO" /etc/dovecot/conf.d/90-quota.conf; then
|
||||
cat > /etc/dovecot/conf.d/90-quota.conf << EOF;
|
||||
plugin {
|
||||
quota = maildir
|
||||
|
||||
quota_grace = 10%%
|
||||
|
||||
quota_status_success = DUNNO
|
||||
quota_status_nouser = DUNNO
|
||||
quota_status_overquota = "522 5.2.2 Mailbox is full"
|
||||
}
|
||||
|
||||
service quota-status {
|
||||
executable = quota-status -p postfix
|
||||
inet_listener {
|
||||
port = 12340
|
||||
}
|
||||
}
|
||||
EOF
|
||||
fi
|
||||
|
||||
# ### IMAP/POP
|
||||
|
||||
|
@ -37,7 +37,7 @@ source /etc/mailinabox.conf # load global vars
|
||||
# * `postfix`: The SMTP server.
|
||||
# * `postfix-pcre`: Enables header filtering.
|
||||
# * `postgrey`: A mail policy service that soft-rejects mail the first time
|
||||
# it is received. Spammers don't usually try agian. Legitimate mail
|
||||
# it is received. Spammers don't usually try again. Legitimate mail
|
||||
# always will.
|
||||
# * `ca-certificates`: A trust store used to squelch postfix warnings about
|
||||
# untrusted opportunistically-encrypted connections.
|
||||
@ -172,7 +172,7 @@ tools/editconf.py /etc/postfix/main.cf \
|
||||
|
||||
# When connecting to remote SMTP servers, prefer TLS and use DANE if available.
|
||||
#
|
||||
# Prefering ("opportunistic") TLS means Postfix will use TLS if the remote end
|
||||
# Preferring ("opportunistic") TLS means Postfix will use TLS if the remote end
|
||||
# offers it, otherwise it will transmit the message in the clear. Postfix will
|
||||
# accept whatever SSL certificate the remote end provides. Opportunistic TLS
|
||||
# protects against passive easvesdropping (but not man-in-the-middle attacks).
|
||||
@ -188,7 +188,7 @@ tools/editconf.py /etc/postfix/main.cf \
|
||||
# itself but assumes the system's nameserver does and reports DNSSEC status. Thus this also
|
||||
# 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,
|
||||
# The `smtp_tls_CAfile` is superfluous, 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`.
|
||||
@ -230,7 +230,7 @@ tools/editconf.py /etc/postfix/main.cf -e lmtp_destination_recipient_limit=
|
||||
# * `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
|
||||
# Note the spamhaus rbl return codes are taken into account as advised 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
|
||||
@ -238,7 +238,7 @@ tools/editconf.py /etc/postfix/main.cf -e lmtp_destination_recipient_limit=
|
||||
# "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=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"
|
||||
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,check_policy_service inet:127.0.0.1:12340"
|
||||
|
||||
# Postfix connects to Postgrey on the 127.0.0.1 interface specifically. Ensure that
|
||||
# Postgrey listens on the same interface (and not IPv6, for instance).
|
||||
@ -247,7 +247,7 @@ tools/editconf.py /etc/postfix/main.cf \
|
||||
# other MTA have their own intervals. To fix the problem of receiving
|
||||
# e-mails really latter, delay of greylisting has been set to
|
||||
# 180 seconds (default is 300 seconds). We will move the postgrey database
|
||||
# under $STORAGE_ROOT. This prevents a "warming up" that would have occured
|
||||
# under $STORAGE_ROOT. This prevents a "warming up" that would have occurred
|
||||
# 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
|
||||
|
@ -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 querying an Sqlite3 database of mail users.
|
||||
|
||||
source setup/functions.sh # load our functions
|
||||
source /etc/mailinabox.conf # load global vars
|
||||
@ -20,7 +20,7 @@ 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 users (id INTEGER PRIMARY KEY AUTOINCREMENT, email TEXT NOT NULL UNIQUE, password TEXT NOT NULL, extra, privileges TEXT NOT NULL DEFAULT '', quota TEXT NOT NULL DEFAULT '0');" | sqlite3 "$db_path";
|
||||
echo "CREATE TABLE aliases (id INTEGER PRIMARY KEY AUTOINCREMENT, source TEXT NOT NULL UNIQUE, destination TEXT NOT NULL, permitted_senders TEXT);" | sqlite3 "$db_path";
|
||||
echo "CREATE TABLE 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";
|
||||
@ -51,7 +51,7 @@ 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';
|
||||
user_query = SELECT email AS user, "mail" as uid, "mail" as gid, "$STORAGE_ROOT/mail/mailboxes/%d/%n" as home, '*:bytes=' || quota AS quota_rule FROM users WHERE email='%u';
|
||||
iterate_query = SELECT email AS user FROM users;
|
||||
EOF
|
||||
chmod 0600 /etc/dovecot/dovecot-sql.conf.ext # per Dovecot instructions
|
||||
@ -159,4 +159,5 @@ EOF
|
||||
restart_service postfix
|
||||
restart_service dovecot
|
||||
|
||||
|
||||
# force a recalculation of all user quotas
|
||||
doveadm quota recalc -A
|
||||
|
@ -54,7 +54,7 @@ hide_output $venv/bin/pip install --upgrade \
|
||||
# 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"
|
||||
(umask 077; openssl rand -base64 2048 > "$STORAGE_ROOT/backup/secret_key.txt")
|
||||
fi
|
||||
|
||||
|
||||
@ -116,7 +116,7 @@ minute=$((RANDOM % 60)) # avoid overloading mailinabox.email
|
||||
cat > /etc/cron.d/mailinabox-nightly << EOF;
|
||||
# Mail-in-a-Box --- Do not edit / will be overwritten on update.
|
||||
# Run nightly tasks: backup, status checks.
|
||||
$minute 3 * * * root (cd $PWD && management/daily_tasks.sh)
|
||||
$minute 1 * * * root (cd $PWD && management/daily_tasks.sh)
|
||||
EOF
|
||||
|
||||
# Start the management server.
|
||||
|
@ -172,7 +172,7 @@ def migration_12(env):
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
# Delete all sessions, requring users to login again to recreate carddav_*
|
||||
# Delete all sessions, requiring users to login again to recreate carddav_*
|
||||
# databases
|
||||
conn = sqlite3.connect(os.path.join(env["STORAGE_ROOT"], "mail/roundcube/roundcube.sqlite"))
|
||||
c = conn.cursor()
|
||||
@ -190,6 +190,12 @@ def migration_14(env):
|
||||
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 migration_15(env):
|
||||
# Add a column to the users table to store their quota limit. Default to '0' for unlimited.
|
||||
db = os.path.join(env["STORAGE_ROOT"], 'mail/users.sqlite')
|
||||
shell("check_call", ["sqlite3", db, "ALTER TABLE users ADD COLUMN quota TEXT NOT NULL DEFAULT '0';"])
|
||||
|
||||
|
||||
###########################################################
|
||||
|
||||
def get_current_migration():
|
||||
|
@ -8,11 +8,14 @@ if [[ $EUID -ne 0 ]]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 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
|
||||
# Check that we are running on Ubuntu 22.04 LTS (or 22.04.xx).
|
||||
# Pull in the variables defined in /etc/os-release but in a
|
||||
# namespace to avoid polluting our variables.
|
||||
source <(cat /etc/os-release | sed s/^/OS_RELEASE_/)
|
||||
if [ "${OS_RELEASE_ID:-}" != "ubuntu" ] || [ "${OS_RELEASE_VERSION_ID:-}" != "22.04" ]; then
|
||||
echo "Mail-in-a-Box only supports being installed on Ubuntu 22.04, sorry. You are running:"
|
||||
echo
|
||||
lsb_release --description --short
|
||||
echo "${OS_RELEASE_ID:-"Unknown linux distribution"} ${OS_RELEASE_VERSION_ID:-}"
|
||||
echo
|
||||
echo "We can't write scripts that run on every possible setup, sorry."
|
||||
exit 1
|
||||
|
@ -163,7 +163,7 @@ if [ -z "${PRIVATE_IPV6:-}" ]; then
|
||||
fi
|
||||
if [[ -z "$PRIVATE_IP" && -z "$PRIVATE_IPV6" ]]; then
|
||||
echo
|
||||
echo "I could not determine the IP or IPv6 address of the network inteface"
|
||||
echo "I could not determine the IP or IPv6 address of the network interface"
|
||||
echo "for connecting to the Internet. Setup must stop."
|
||||
echo
|
||||
hostname -I
|
||||
|
@ -53,7 +53,7 @@ tools/editconf.py /etc/default/spampd \
|
||||
|
||||
# Spamassassin normally wraps spam as an attachment inside a fresh
|
||||
# email with a report about the message. This also protects the user
|
||||
# from accidentally openening a message with embedded malware.
|
||||
# from accidentally opening a message with embedded malware.
|
||||
#
|
||||
# It's nice to see what rules caused the message to be marked as spam,
|
||||
# but it's also annoying to get to the original message when it is an
|
||||
|
@ -96,3 +96,12 @@ fi
|
||||
if [ ! -f "$STORAGE_ROOT/ssl/dh2048.pem" ]; then
|
||||
openssl dhparam -out "$STORAGE_ROOT/ssl/dh2048.pem" 2048
|
||||
fi
|
||||
|
||||
# Cleanup expired SSL certificates from $STORAGE_ROOT/ssl daily
|
||||
cat > /etc/cron.daily/mailinabox-ssl-cleanup << EOF;
|
||||
#!/bin/bash
|
||||
# Mail-in-a-Box
|
||||
# Cleanup expired SSL certificates
|
||||
$(pwd)/tools/ssl_cleanup
|
||||
EOF
|
||||
chmod +x /etc/cron.daily/mailinabox-ssl-cleanup
|
||||
|
@ -37,7 +37,7 @@ chmod g-w /etc /etc/default /usr
|
||||
# - 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.
|
||||
# swapfiles. User should handle it them selves.
|
||||
# - Check the memory requirements
|
||||
# - Check available diskspace
|
||||
|
||||
@ -59,7 +59,7 @@ if
|
||||
then
|
||||
echo "Adding a swap file to the system..."
|
||||
|
||||
# Allocate and activate the swap file. Allocate in 1KB chuncks
|
||||
# Allocate and activate the swap file. Allocate in 1KB chunks
|
||||
# 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
|
||||
@ -83,6 +83,15 @@ fi
|
||||
# (See https://discourse.mailinabox.email/t/journalctl-reclaim-space-on-small-mailinabox/6728/11.)
|
||||
tools/editconf.py /etc/systemd/journald.conf MaxRetentionSec=10day
|
||||
|
||||
# ### Improve server privacy
|
||||
|
||||
# Disable MOTD adverts to prevent revealing server information in MOTD request headers
|
||||
# See https://ma.ttias.be/what-exactly-being-sent-ubuntu-motd/
|
||||
if [ -f /etc/default/motd-news ]; then
|
||||
tools/editconf.py /etc/default/motd-news ENABLED=0
|
||||
rm -f /var/cache/motd-news
|
||||
fi
|
||||
|
||||
# ### Add PPAs.
|
||||
|
||||
# We install some non-standard Ubuntu packages maintained by other
|
||||
@ -218,7 +227,7 @@ fi
|
||||
# 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
|
||||
# occurred 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.
|
||||
@ -270,14 +279,14 @@ if [ -z "${DISABLE_FIREWALL:-}" ]; then
|
||||
# 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
|
||||
SSH_PORT=$(sshd -T 2>/dev/null | grep "^port " | sed "s/port //" | tr '\n' ' ') #NODOC
|
||||
if [ -n "$SSH_PORT" ]; then
|
||||
if [ "$SSH_PORT" != "22" ]; then
|
||||
|
||||
echo "Opening alternate SSH port $SSH_PORT." #NODOC
|
||||
ufw_limit "$SSH_PORT" #NODOC
|
||||
|
||||
for port in $SSH_PORT; do
|
||||
if [ "$port" != "22" ]; then
|
||||
echo "Opening alternate SSH port $port." #NODOC
|
||||
ufw_limit "$port" #NODOC
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
ufw --force enable;
|
||||
|
@ -6,7 +6,7 @@ source setup/functions.sh # load our functions
|
||||
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.
|
||||
# will use nginx. Use autoremove to remove any Apache dependencies.
|
||||
if [ -f /usr/sbin/apache2 ]; then
|
||||
echo "Removing apache..."
|
||||
hide_output apt-get -y purge apache2 apache2-*
|
||||
|
@ -23,7 +23,7 @@ echo "Installing Roundcube (webmail)..."
|
||||
apt_install \
|
||||
dbconfig-common \
|
||||
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 \
|
||||
php"${PHP_VER}"-gd php"${PHP_VER}"-pspell php"${PHP_VER}"-mbstring php"${PHP_VER}"-xml libjs-jquery libjs-jquery-mousewheel libmagic1 \
|
||||
sqlite3
|
||||
|
||||
# Install Roundcube from source if it is not already present or if it is out of date.
|
||||
@ -36,8 +36,8 @@ apt_install \
|
||||
# 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
|
||||
VERSION=1.6.10
|
||||
HASH=0cfbb457e230793df8c56c2e6d3655cf3818f168
|
||||
PERSISTENT_LOGIN_VERSION=bde7b6840c7d91de627ea14e81cf4133cbb3c07a # version 5.3
|
||||
HTML5_NOTIFIER_VERSION=68d9ca194212e15b3c7225eb6085dbcf02fd13d7 # version 0.6.4+
|
||||
CARDDAV_VERSION=4.4.3
|
||||
@ -145,6 +145,7 @@ cat > $RCM_CONFIG <<EOF;
|
||||
\$config['session_path'] = '/mail/';
|
||||
/* prevent CSRF, requires php 7.3+ */
|
||||
\$config['session_samesite'] = 'Strict';
|
||||
\$config['quota_zero_as_unlimited'] = true;
|
||||
?>
|
||||
EOF
|
||||
|
||||
|
@ -22,8 +22,8 @@ apt_install \
|
||||
phpenmod -v "$PHP_VER" imap
|
||||
|
||||
# Copy Z-Push into place.
|
||||
VERSION=2.7.3
|
||||
TARGETHASH=9d4bec41935e9a4e07880c5ff915bcddbda4443b
|
||||
VERSION=2.7.5
|
||||
TARGETHASH=f0b0b06e255f3496173ab9d28a4f2d985184720e
|
||||
needs_update=0 #NODOC
|
||||
if [ ! -f /usr/local/lib/z-push/version ]; then
|
||||
needs_update=1 #NODOC
|
||||
@ -57,8 +57,6 @@ fi
|
||||
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
|
||||
@ -112,4 +110,6 @@ 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
|
||||
if [ $needs_update == 1 ]; then
|
||||
hide_output php"$PHP_VER" /usr/local/lib/z-push/z-push-admin.php -a fixstates
|
||||
fi
|
||||
|
@ -66,7 +66,7 @@ def test2(tests, server, description):
|
||||
#print(server, ":", qname, rtype, "?", response)
|
||||
continue
|
||||
|
||||
# show prolem
|
||||
# show problem
|
||||
if first:
|
||||
print("Incorrect DNS Response from", description)
|
||||
print()
|
||||
|
17
tools/ssl_cleanup
Executable file
17
tools/ssl_cleanup
Executable file
@ -0,0 +1,17 @@
|
||||
#!/bin/bash
|
||||
# Cleanup SSL certificates which expired more than 7 days ago from $STORAGE_ROOT/ssl and move them to $STORAGE_ROOT/ssl.expired
|
||||
|
||||
source /etc/mailinabox.conf
|
||||
shopt -s extglob nullglob
|
||||
|
||||
retain_after="$(date --date="7 days ago" +%Y%m%d)"
|
||||
|
||||
mkdir -p $STORAGE_ROOT/ssl.expired
|
||||
for file in $STORAGE_ROOT/ssl/*-+([0-9])-+([0-9a-f]).pem; do
|
||||
pem="$(basename "$file")"
|
||||
not_valid_after="$(cut -d- -f1 <<< "${pem: -21}")"
|
||||
|
||||
if [ "$not_valid_after" -lt "$retain_after" ]; then
|
||||
mv "$file" "$STORAGE_ROOT/ssl.expired/${pem}"
|
||||
fi
|
||||
done
|
Loading…
Reference in New Issue
Block a user