From 98628622c7a435f8ba58a50407eccd9fdb7a3fcd Mon Sep 17 00:00:00 2001 From: Michael Heuberger Date: Sat, 17 Jun 2023 03:49:55 +1200 Subject: [PATCH 001/121] 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) --- conf/nginx-primaryonly.conf | 5 +++ setup/nextcloud.sh | 69 ++++++++++++++++++++++++++++++------- 2 files changed, 62 insertions(+), 12 deletions(-) diff --git a/conf/nginx-primaryonly.conf b/conf/nginx-primaryonly.conf index 31bf0095..9ed37043 100644 --- a/conf/nginx-primaryonly.conf +++ b/conf/nginx-primaryonly.conf @@ -73,4 +73,9 @@ 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 diff --git a/setup/nextcloud.sh b/setup/nextcloud.sh index b79de327..9ee1626e 100755 --- a/setup/nextcloud.sh +++ b/setup/nextcloud.sh @@ -21,8 +21,8 @@ echo "Installing Nextcloud (contacts/calendar)..." # 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=23.0.12 -nextcloud_hash=d138641b8e7aabebe69bb3ec7c79a714d122f729 +nextcloud_ver=25.0.7 +nextcloud_hash=a5a565c916355005c7b408dd41a1e53505e1a080 # Nextcloud apps # -------------- @@ -33,12 +33,16 @@ nextcloud_hash=d138641b8e7aabebe69bb3ec7c79a714d122f729 # https://github.com/nextcloud/user_external/blob/master/appinfo/info.xml # * The hash is the SHA1 hash of the ZIP package, which you can find by just running this script and # copying it from the error message when it doesn't match what is below. -contacts_ver=4.2.5 -contacts_hash=8f898f003eb6b1a85c0c43b52f829d3aa759ed88 -calendar_ver=3.5.5 -calendar_hash=8505abcf7b3ab2f32d7ca1593b545e577cbeedb4 -user_external_ver=3.1.0 -user_external_hash=22cabc88b6fc9c26dad3b46be1a652979c9fcf15 +contacts_ver=5.3.0 +contacts_hash=4b0a6666374e3b55cfd2ae9b72e1d458b87d4c8c + +# Always ensure the versions are supported, see https://apps.nextcloud.com/apps/calendar +calendar_ver=4.4.2 +calendar_hash=21a42e15806adc9b2618760ef94f1797ef399e2f + +# And https://apps.nextcloud.com/apps/user_external +user_external_ver=3.2.0 +user_external_hash=a494073dcdecbbbc79a9c77f72524ac9994d2eec # Clear prior packages and install dependencies from apt. @@ -128,6 +132,7 @@ InstallNextcloud() { # 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 @@ -173,6 +178,12 @@ if [ ! -d /usr/local/lib/owncloud/ ] || [[ ! ${CURRENT_NEXTCLOUD_VER} =~ ^$nextc if [ ! -z ${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 @@ -183,6 +194,7 @@ if [ ! -d /usr/local/lib/owncloud/ ] || [[ ! ${CURRENT_NEXTCLOUD_VER} =~ ^$nextc 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 + 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" @@ -191,6 +203,14 @@ if [ ! -d /usr/local/lib/owncloud/ ] || [[ ! ${CURRENT_NEXTCLOUD_VER} =~ ^$nextc 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 7aa5d61632c1ccf4ca3ff00fb6b295d318c05599 4.1.0 697f6b4a664e928d72414ea2731cb2c9d1dc3077 3.2.2 ce4030ab57f523f33d5396c6a81396d440756f5f 3.0.0 22cabc88b6fc9c26dad3b46be1a652979c9fcf15 + CURRENT_NEXTCLOUD_VER="23.0.12" + fi + if [[ ${CURRENT_NEXTCLOUD_VER} =~ ^23 ]]; then + InstallNextcloud 24.0.12 7aa5d61632c1ccf4ca3ff00fb6b295d318c05599 4.1.0 697f6b4a664e928d72414ea2731cb2c9d1dc3077 3.2.2 ce4030ab57f523f33d5396c6a81396d440756f5f 3.0.0 0df781b261f55bbde73d8c92da3f99397000972f + CURRENT_NEXTCLOUD_VER="24.0.12" + fi fi InstallNextcloud $nextcloud_ver $nextcloud_hash $contacts_ver $contacts_hash $calendar_ver $calendar_hash $user_external_ver $user_external_hash @@ -284,12 +304,12 @@ php$PHP_VER < $CONFIG_TEMP && mv $CONFIG_TEMP $STORAGE_ROOT/owncloud/confi /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 < Date: Sat, 29 Jul 2023 12:11:00 -0400 Subject: [PATCH 002/121] Version 63 --- CHANGELOG.md | 5 +++++ README.md | 2 +- setup/bootstrap.sh | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d5516d56..b844626f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +Version 63 (July 27, 2023) +-------------------------- + +* Nextcloud updated to 25.0.7. + Version 62 (May 20, 2023) ------------------------- diff --git a/README.md b/README.md index 6c771656..4d584009 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,7 @@ Clone this repository and checkout the tag corresponding to the most recent rele $ git clone https://github.com/mail-in-a-box/mailinabox $ cd mailinabox - $ git checkout v62 + $ git checkout v63 Begin the installation. diff --git a/setup/bootstrap.sh b/setup/bootstrap.sh index c4ccd401..d5ede149 100644 --- a/setup/bootstrap.sh +++ b/setup/bootstrap.sh @@ -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=v62 + TAG=v63 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. From c034b0f7899c62e085894ba4ac961519181f0161 Mon Sep 17 00:00:00 2001 From: Darren Sanders Date: Tue, 29 Aug 2023 13:37:25 -0700 Subject: [PATCH 003/121] 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. --- management/backup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/management/backup.py b/management/backup.py index 06285ba5..b7102e5b 100755 --- a/management/backup.py +++ b/management/backup.py @@ -57,7 +57,7 @@ def backup_status(env): "/usr/bin/duplicity", "collection-status", "--archive-dir", backup_cache_dir, - "--gpg-options", "--cipher-algo=AES256", + "--gpg-options", "'--cipher-algo=AES256'", "--log-fd", "1", get_duplicity_target_url(config), ] + get_duplicity_additional_args(env), @@ -321,7 +321,7 @@ def perform_backup(full_backup): "--archive-dir", backup_cache_dir, "--exclude", backup_root, "--volsize", "250", - "--gpg-options", "--cipher-algo=AES256", + "--gpg-options", "'--cipher-algo=AES256'", env["STORAGE_ROOT"], get_duplicity_target_url(config), "--allow-source-mismatch" From 674ce92e925027c4afed71bfaae658f4c1106291 Mon Sep 17 00:00:00 2001 From: matidau <65836048+matidau@users.noreply.github.com> Date: Sat, 2 Sep 2023 20:55:15 +1000 Subject: [PATCH 004/121] 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. --- setup/zpush.sh | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/setup/zpush.sh b/setup/zpush.sh index 3f6037bc..51b197ff 100755 --- a/setup/zpush.sh +++ b/setup/zpush.sh @@ -41,7 +41,15 @@ if [ $needs_update == 1 ]; then 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} + 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 From 81866de229ba6889886a9c0bf23d94d0208f3c7e Mon Sep 17 00:00:00 2001 From: Michael Heuberger Date: Sat, 2 Sep 2023 22:59:39 +1200 Subject: [PATCH 005/121] Amend --always option to all git describe commands (#2275) --- management/status_checks.py | 6 +++--- setup/bootstrap.sh | 2 +- setup/questions.sh | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/management/status_checks.py b/management/status_checks.py index b31a9818..07e7dc1d 100755 --- a/management/status_checks.py +++ b/management/status_checks.py @@ -800,7 +800,7 @@ def query_dns(qname, rtype, nxdomain='[Not Set]', at=None, as_list=False): # running bind server), or if the 'at' argument is specified, use that host # as the nameserver. resolver = dns.resolver.get_default_resolver() - + # Make sure at is not a string that cannot be used as a nameserver if at and at not in {'[Not set]', '[timeout]'}: resolver = dns.resolver.Resolver() @@ -912,11 +912,11 @@ def list_apt_updates(apt_update=True): return pkgs def what_version_is_this(env): - # This function runs `git describe --abbrev=0` on the Mail-in-a-Box installation directory. + # This function runs `git describe --always --abbrev=0` on the Mail-in-a-Box installation directory. # Git may not be installed and Mail-in-a-Box may not have been cloned from github, # so this function may raise all sorts of exceptions. miab_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) - tag = shell("check_output", ["/usr/bin/git", "describe", "--abbrev=0"], env={"GIT_DIR": os.path.join(miab_dir, '.git')}).strip() + tag = shell("check_output", ["/usr/bin/git", "describe", "--always", "--abbrev=0"], env={"GIT_DIR": os.path.join(miab_dir, '.git')}).strip() return tag def get_latest_miab_version(): diff --git a/setup/bootstrap.sh b/setup/bootstrap.sh index d5ede149..ee35bcf2 100644 --- a/setup/bootstrap.sh +++ b/setup/bootstrap.sh @@ -73,7 +73,7 @@ fi cd $HOME/mailinabox # Update it. -if [ "$TAG" != $(git describe) ]; then +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 diff --git a/setup/questions.sh b/setup/questions.sh index bf382f49..53e84bea 100644 --- a/setup/questions.sh +++ b/setup/questions.sh @@ -207,6 +207,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 From 3148c621d2a1e432ecb443c287c292cd1b9fd432 Mon Sep 17 00:00:00 2001 From: Dmytro Kyrychuk Date: Sat, 2 Sep 2023 14:03:24 +0300 Subject: [PATCH 006/121] 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 --- management/backup.py | 2 +- management/templates/system-backup.html | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/management/backup.py b/management/backup.py index 06285ba5..6642419e 100755 --- a/management/backup.py +++ b/management/backup.py @@ -509,7 +509,7 @@ def list_target_files(config): # Extract information from target b2_application_keyid = target.netloc[:target.netloc.index(':')] - b2_application_key = target.netloc[target.netloc.index(':')+1:target.netloc.index('@')] + b2_application_key = urllib.parse.unquote(target.netloc[target.netloc.index(':')+1:target.netloc.index('@')]) b2_bucket = target.netloc[target.netloc.index('@')+1:] try: diff --git a/management/templates/system-backup.html b/management/templates/system-backup.html index f7305518..a49d235c 100644 --- a/management/templates/system-backup.html +++ b/management/templates/system-backup.html @@ -291,7 +291,7 @@ function show_custom_backup() { var b2_applicationkey = targetPath.split(':')[1].split('@')[0]; var b2_bucket = targetPath.split('@')[1]; $("#backup-target-b2-user").val(b2_application_keyid); - $("#backup-target-b2-pass").val(b2_applicationkey); + $("#backup-target-b2-pass").val(decodeURIComponent(b2_applicationkey)); $("#backup-target-b2-bucket").val(b2_bucket); } toggle_form() @@ -316,7 +316,7 @@ function set_custom_backup() { + "/" + $("#backup-target-rsync-path").val(); target_user = ''; } else if (target_type == "b2") { - target = 'b2://' + $('#backup-target-b2-user').val() + ':' + $('#backup-target-b2-pass').val() + target = 'b2://' + $('#backup-target-b2-user').val() + ':' + encodeURIComponent($('#backup-target-b2-pass').val()) + '@' + $('#backup-target-b2-bucket').val() target_user = ''; target_pass = ''; From df44056baec1a5dbcbd7961b6898689d6bae2f57 Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 2 Sep 2023 13:07:12 +0200 Subject: [PATCH 007/121] Fix checksums in nextcloud.sh (#2293) --- setup/nextcloud.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup/nextcloud.sh b/setup/nextcloud.sh index 9ee1626e..a2d9d2e5 100755 --- a/setup/nextcloud.sh +++ b/setup/nextcloud.sh @@ -204,7 +204,7 @@ if [ ! -d /usr/local/lib/owncloud/ ] || [[ ! ${CURRENT_NEXTCLOUD_VER} =~ ^$nextc CURRENT_NEXTCLOUD_VER="22.2.6" fi if [[ ${CURRENT_NEXTCLOUD_VER} =~ ^22 ]]; then - InstallNextcloud 23.0.12 7aa5d61632c1ccf4ca3ff00fb6b295d318c05599 4.1.0 697f6b4a664e928d72414ea2731cb2c9d1dc3077 3.2.2 ce4030ab57f523f33d5396c6a81396d440756f5f 3.0.0 22cabc88b6fc9c26dad3b46be1a652979c9fcf15 + 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 From 62efe985f17817493119ca8584e5b91ed5e99e59 Mon Sep 17 00:00:00 2001 From: Aaron Ten Clay Date: Sat, 2 Sep 2023 04:10:04 -0700 Subject: [PATCH 008/121] 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. --- setup/dkim.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/setup/dkim.sh b/setup/dkim.sh index b2541a12..d2d162a7 100755 --- a/setup/dkim.sh +++ b/setup/dkim.sh @@ -63,7 +63,7 @@ chmod go-rwx $STORAGE_ROOT/mail/dkim tools/editconf.py /etc/opendmarc.conf -s \ "Syslog=true" \ "Socket=inet:8893@[127.0.0.1]" \ - "FailureReports=true" + "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 @@ -82,11 +82,11 @@ tools/editconf.py /etc/opendmarc.conf -s \ tools/editconf.py /etc/opendmarc.conf -s \ "SPFSelfValidate=true" -# Enables generation of failure reports for sending domains that publish a +# Disables generation of failure reports for sending domains that publish a # "none" policy. tools/editconf.py /etc/opendmarc.conf -s \ - "FailureReportsOnNone=true" + "FailureReportsOnNone=false" # AlwaysAddARHeader Adds an "Authentication-Results:" header field even to # unsigned messages from domains with no "signs all" policy. The reported DKIM From 7be687e601e39a2dd7ceb78b0e2c3f630050e5d5 Mon Sep 17 00:00:00 2001 From: Jeff Volkenant Date: Wed, 30 Aug 2023 21:48:51 -0700 Subject: [PATCH 009/121] Move source and target positional arguments to the end, required for Duplicity 2.1.0 (Modified by JT.) --- management/backup.py | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/management/backup.py b/management/backup.py index b7102e5b..9bb472b6 100755 --- a/management/backup.py +++ b/management/backup.py @@ -59,8 +59,9 @@ def backup_status(env): "--archive-dir", backup_cache_dir, "--gpg-options", "'--cipher-algo=AES256'", "--log-fd", "1", - get_duplicity_target_url(config), - ] + get_duplicity_additional_args(env), + ] + get_duplicity_additional_args(env) + [ + get_duplicity_target_url(config) + ], get_duplicity_env_vars(env), trap=True) if code != 0: @@ -227,8 +228,8 @@ def get_duplicity_additional_args(env): 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\"", + 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. @@ -322,10 +323,11 @@ def perform_backup(full_backup): "--exclude", backup_root, "--volsize", "250", "--gpg-options", "'--cipher-algo=AES256'", + "--allow-source-mismatch" + ] + get_duplicity_additional_args(env) + [ env["STORAGE_ROOT"], get_duplicity_target_url(config), - "--allow-source-mismatch" - ] + get_duplicity_additional_args(env), + ], get_duplicity_env_vars(env)) finally: # Start services again. @@ -343,8 +345,9 @@ def perform_backup(full_backup): "--verbosity", "error", "--archive-dir", backup_cache_dir, "--force", + ] + get_duplicity_additional_args(env) + [ get_duplicity_target_url(config) - ] + get_duplicity_additional_args(env), + ], get_duplicity_env_vars(env)) # From duplicity's manual: @@ -358,8 +361,9 @@ def perform_backup(full_backup): "--verbosity", "error", "--archive-dir", backup_cache_dir, "--force", + ] + get_duplicity_additional_args(env) + [ get_duplicity_target_url(config) - ] + get_duplicity_additional_args(env), + ], get_duplicity_env_vars(env)) # Change ownership of backups to the user-data user, so that the after-bcakup @@ -396,9 +400,10 @@ def run_duplicity_verification(): "--compare-data", "--archive-dir", backup_cache_dir, "--exclude", backup_root, + ] + get_duplicity_additional_args(env) + [ get_duplicity_target_url(config), env["STORAGE_ROOT"], - ] + get_duplicity_additional_args(env), get_duplicity_env_vars(env)) + ], get_duplicity_env_vars(env)) def run_duplicity_restore(args): env = load_environment() @@ -408,9 +413,10 @@ def run_duplicity_restore(args): "/usr/bin/duplicity", "restore", "--archive-dir", backup_cache_dir, - get_duplicity_target_url(config), - ] + get_duplicity_additional_args(env) + args, - get_duplicity_env_vars(env)) + ] + get_duplicity_additional_args(env) + [ + get_duplicity_target_url(config) + ] + args, + get_duplicity_env_vars(env)) def list_target_files(config): import urllib.parse From 08defb12be94582513707c3901a6b939d005e0e0 Mon Sep 17 00:00:00 2001 From: Joshua Tauberer Date: Sat, 2 Sep 2023 07:49:41 -0400 Subject: [PATCH 010/121] Add a new backup.py command to print the duplicity command to the console to help debugging --- management/backup.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/management/backup.py b/management/backup.py index 9bb472b6..3ad68cc7 100755 --- a/management/backup.py +++ b/management/backup.py @@ -418,6 +418,19 @@ def run_duplicity_restore(args): ] + 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: @@ -624,6 +637,9 @@ if __name__ == "__main__": # 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. From e419b620347e7d3e4782466d540056a212d28ae1 Mon Sep 17 00:00:00 2001 From: Joshua Tauberer Date: Sat, 2 Sep 2023 19:43:23 -0400 Subject: [PATCH 011/121] Version 64 --- CHANGELOG.md | 10 ++++++++++ README.md | 2 +- setup/bootstrap.sh | 2 +- 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b844626f..d41cb25b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,16 @@ CHANGELOG ========= +Version 64 (September 2, 2023) +------------------------------ + +* Fixed broken installation when upgrading from Mail-in-a-Box version 56 (Nextcloud 22) and earlier because of an upstream packaging issue. +* Fixed backups to work with the latest duplicity package which was not backwards compatible. +* Fixed setting B2 as a backup target with a slash in the application key. +* Turned off OpenDMARC diagnostic reports sent in response to incoming mail. +* Fixed some crashes when using an unrelased version of Mail-in-a-Box. +* Added z-push administration scripts. + Version 63 (July 27, 2023) -------------------------- diff --git a/README.md b/README.md index 4d584009..06815cc7 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,7 @@ Clone this repository and checkout the tag corresponding to the most recent rele $ git clone https://github.com/mail-in-a-box/mailinabox $ cd mailinabox - $ git checkout v63 + $ git checkout v64 Begin the installation. diff --git a/setup/bootstrap.sh b/setup/bootstrap.sh index ee35bcf2..6693b329 100644 --- a/setup/bootstrap.sh +++ b/setup/bootstrap.sh @@ -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=v63 + TAG=v64 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. From 28f929dc13bd8aece3805fa906a88ed9f00ade7f Mon Sep 17 00:00:00 2001 From: clpo13 Date: Tue, 10 Oct 2023 10:22:19 -0700 Subject: [PATCH 012/121] Fix typo in system-backup.html: Amazone -> Amazon (#2311) --- management/templates/system-backup.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/management/templates/system-backup.html b/management/templates/system-backup.html index a49d235c..0d948bf0 100644 --- a/management/templates/system-backup.html +++ b/management/templates/system-backup.html @@ -5,7 +5,7 @@

Backup Status

-

The box makes an incremental backup each night. You can store the backup on any Amazone Web Services S3-compatible service, or other options.

+

The box makes an incremental backup each night. You can store the backup on any Amazon Web Services S3-compatible service, or other options.

Configuration

From 2bbc317873b7399ba8a08d2397654f5a1b56b5b8 Mon Sep 17 00:00:00 2001 From: KiekerJan Date: Thu, 26 Oct 2023 15:03:29 +0200 Subject: [PATCH 013/121] Update Roundcube to 1.6.4 (#2317) --- setup/webmail.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/setup/webmail.sh b/setup/webmail.sh index 0be0e1f8..97fa24cb 100755 --- a/setup/webmail.sh +++ b/setup/webmail.sh @@ -36,9 +36,9 @@ 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.1 -HASH=0e1c771ab83ea03bde1fd0be6ab5d09e60b4f293 -PERSISTENT_LOGIN_VERSION=bde7b6840c7d91de627ea14e81cf4133cbb3c07a # version 5.2 +VERSION=1.6.4 +HASH=bfc693d6590542d63171e6a3997fc29f0a5f12ca +PERSISTENT_LOGIN_VERSION=bde7b6840c7d91de627ea14e81cf4133cbb3c07a # version 5.3 HTML5_NOTIFIER_VERSION=68d9ca194212e15b3c7225eb6085dbcf02fd13d7 # version 0.6.4+ CARDDAV_VERSION=4.4.3 CARDDAV_HASH=74f8ba7aee33e78beb9de07f7f44b81f6071b644 From 46d55f786656b25f24bc9020f24880ca8200d3bb Mon Sep 17 00:00:00 2001 From: matidau <65836048+matidau@users.noreply.github.com> Date: Fri, 27 Oct 2023 00:04:13 +1100 Subject: [PATCH 014/121] Update zpush.sh to version 2.7.1 (#2315) Updating to latest release, bugfixes no new features. --- setup/zpush.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup/zpush.sh b/setup/zpush.sh index 51b197ff..152418c0 100755 --- a/setup/zpush.sh +++ b/setup/zpush.sh @@ -22,8 +22,8 @@ apt_install \ phpenmod -v $PHP_VER imap # Copy Z-Push into place. -VERSION=2.7.0 -TARGETHASH=a520bbdc1d637c5aac379611053457edd54f2bf0 +VERSION=2.7.1 +TARGETHASH=f15c566b1ad50de24f3f08f505f0c3d8155c2d0d needs_update=0 #NODOC if [ ! -f /usr/local/lib/z-push/version ]; then needs_update=1 #NODOC From 03145542076c80ab2af3a3b12b94c0da50a631fa Mon Sep 17 00:00:00 2001 From: Joshua Tauberer Date: Fri, 27 Oct 2023 06:02:22 -0400 Subject: [PATCH 015/121] Version 65 --- CHANGELOG.md | 7 +++++++ README.md | 2 +- setup/bootstrap.sh | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d41cb25b..420b6797 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,13 @@ CHANGELOG ========= +Version 65 (October 27, 2023) +----------------------------- + +* Roundcube updated to 1.6.4 fixing a security vulnerability. +* zpush.sh updated to version 2.7.1. +* Fixed a typo in the control panel. + Version 64 (September 2, 2023) ------------------------------ diff --git a/README.md b/README.md index 06815cc7..df387ab4 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,7 @@ Clone this repository and checkout the tag corresponding to the most recent rele $ git clone https://github.com/mail-in-a-box/mailinabox $ cd mailinabox - $ git checkout v64 + $ git checkout v65 Begin the installation. diff --git a/setup/bootstrap.sh b/setup/bootstrap.sh index 6693b329..51863da9 100644 --- a/setup/bootstrap.sh +++ b/setup/bootstrap.sh @@ -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=v64 + TAG=v65 elif [ "$UBUNTU_VERSION" == "Ubuntu 18.04 LTS" ]; then # This machine is running Ubuntu 18.04, which is supported by # Mail-in-a-Box versions 0.40 through 5x. From 371f5bc1b236de40a1ed5d9118140ee13fddf5dc Mon Sep 17 00:00:00 2001 From: Joshua Tauberer Date: Tue, 28 Nov 2023 07:24:30 -0500 Subject: [PATCH 016/121] Fix virtualenv creation reported in #2335 --- CHANGELOG.md | 5 +++++ setup/management.sh | 6 ++++++ 2 files changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 420b6797..a0f35d06 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +In Development +-------------- + +* Some users reported an error installing Mail-in-a-Box related to the virtualenv command. This is hopefully fixed. + Version 65 (October 27, 2023) ----------------------------- diff --git a/setup/management.sh b/setup/management.sh index 7fe4da25..50c24189 100755 --- a/setup/management.sh +++ b/setup/management.sh @@ -27,6 +27,12 @@ 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 From 6d6ce25e03a42ea91888f9b99fcc31163917c5bb Mon Sep 17 00:00:00 2001 From: bilogic <946010+bilogic@users.noreply.github.com> Date: Mon, 4 Dec 2023 22:22:54 +0800 Subject: [PATCH 017/121] Allow specifying another repo to install from in bootstrap.sh (#2334) --- setup/bootstrap.sh | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/setup/bootstrap.sh b/setup/bootstrap.sh index 51863da9..d3bd6fac 100644 --- a/setup/bootstrap.sh +++ b/setup/bootstrap.sh @@ -59,10 +59,14 @@ if [ ! -d $HOME/mailinabox ]; then echo fi + 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 \ + $SOURCE \ $HOME/mailinabox \ < /dev/null 2> /dev/null From fa8c7ddef59d5ae84c31ca50d1c3ec6b5f85f428 Mon Sep 17 00:00:00 2001 From: KiekerJan Date: Mon, 4 Dec 2023 15:23:36 +0100 Subject: [PATCH 018/121] Upgrade roundcube to 1.6.5 (#2329) --- setup/webmail.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup/webmail.sh b/setup/webmail.sh index 97fa24cb..62d18c86 100755 --- a/setup/webmail.sh +++ b/setup/webmail.sh @@ -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.4 -HASH=bfc693d6590542d63171e6a3997fc29f0a5f12ca +VERSION=1.6.5 +HASH=326fcc206cddc00355e98d1e40fd0bcd9baec69f PERSISTENT_LOGIN_VERSION=bde7b6840c7d91de627ea14e81cf4133cbb3c07a # version 5.3 HTML5_NOTIFIER_VERSION=68d9ca194212e15b3c7225eb6085dbcf02fd13d7 # version 0.6.4+ CARDDAV_VERSION=4.4.3 From 8e4e9add78c6744057e9c4f5b80f4beb9f10f8cc Mon Sep 17 00:00:00 2001 From: Joshua Tauberer Date: Sun, 17 Dec 2023 16:29:30 -0500 Subject: [PATCH 019/121] Version 66 --- CHANGELOG.md | 6 ++++-- README.md | 2 +- setup/bootstrap.sh | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a0f35d06..97642a01 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,12 @@ CHANGELOG ========= -In Development --------------- +Version 66 (December 17, 2023) +------------------------------ * Some users reported an error installing Mail-in-a-Box related to the virtualenv command. This is hopefully fixed. +* Roundcube is updated to 1.6.5 fixing a security vulnerability. +* For Mail-in-a-Box developers, a new setup variable is added to pull the source code from a different repository. Version 65 (October 27, 2023) ----------------------------- diff --git a/README.md b/README.md index df387ab4..ac766ae8 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,7 @@ Clone this repository and checkout the tag corresponding to the most recent rele $ git clone https://github.com/mail-in-a-box/mailinabox $ cd mailinabox - $ git checkout v65 + $ git checkout v66 Begin the installation. diff --git a/setup/bootstrap.sh b/setup/bootstrap.sh index d3bd6fac..9399a235 100644 --- a/setup/bootstrap.sh +++ b/setup/bootstrap.sh @@ -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=v65 + TAG=v66 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. From faf23f150c5fa85c8e9af1e345d796d2c36a4577 Mon Sep 17 00:00:00 2001 From: Joshua Tauberer Date: Fri, 22 Dec 2023 08:53:48 -0500 Subject: [PATCH 020/121] Guard against SMTP smuggling This short-term workaround is recommended at https://www.postfix.org/smtp-smuggling.html: smtpd_data_restrictions=reject_unauth_pipelining --- setup/mail-postfix.sh | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/setup/mail-postfix.sh b/setup/mail-postfix.sh index 5787e8c9..de5dee0e 100755 --- a/setup/mail-postfix.sh +++ b/setup/mail-postfix.sh @@ -69,6 +69,11 @@ tools/editconf.py /etc/postfix/main.cf \ maximal_queue_lifetime=2d \ bounce_queue_lifetime=1d +# Guard against SMTP smuggling +# This short-term workaround is recommended at https://www.postfix.org/smtp-smuggling.html +tools/editconf.py /etc/postfix/main.cf \ + smtpd_data_restrictions=reject_unauth_pipelining + # ### Outgoing Mail # Enable the 'submission' ports 465 and 587 and tweak their settings. From 7646095b949e1c786f0742114ce35225898b47b1 Mon Sep 17 00:00:00 2001 From: Joshua Tauberer Date: Fri, 22 Dec 2023 08:56:27 -0500 Subject: [PATCH 021/121] v67 --- CHANGELOG.md | 5 +++++ README.md | 2 +- setup/bootstrap.sh | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 97642a01..c855c214 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +Version 67 (December 22, 2023) +------------------------------ + +* Guard against a newly published vulnerability called SMTP Smuggling. See https://sec-consult.com/blog/detail/smtp-smuggling-spoofing-e-mails-worldwide/. + Version 66 (December 17, 2023) ------------------------------ diff --git a/README.md b/README.md index ac766ae8..8411c450 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,7 @@ Clone this repository and checkout the tag corresponding to the most recent rele $ git clone https://github.com/mail-in-a-box/mailinabox $ cd mailinabox - $ git checkout v66 + $ git checkout v67 Begin the installation. diff --git a/setup/bootstrap.sh b/setup/bootstrap.sh index 9399a235..b52848ee 100644 --- a/setup/bootstrap.sh +++ b/setup/bootstrap.sh @@ -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=v66 + TAG=v67 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. From e931e103fe1d6db81681e3c9732d21e9860acdcd Mon Sep 17 00:00:00 2001 From: KiekerJan Date: Wed, 10 Jan 2024 15:34:06 +0100 Subject: [PATCH 022/121] [security] SMTP smuggling: update short term fix (#2346) Update short term fix according to postfix advisory at https://www.postfix.org/smtp-smuggling.html. --- setup/mail-postfix.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/setup/mail-postfix.sh b/setup/mail-postfix.sh index de5dee0e..6ceb2edf 100755 --- a/setup/mail-postfix.sh +++ b/setup/mail-postfix.sh @@ -72,7 +72,8 @@ tools/editconf.py /etc/postfix/main.cf \ # Guard against SMTP smuggling # This short-term workaround is recommended at https://www.postfix.org/smtp-smuggling.html tools/editconf.py /etc/postfix/main.cf \ - smtpd_data_restrictions=reject_unauth_pipelining + smtpd_data_restrictions=reject_unauth_pipelining \ + smtpd_discard_ehlo_keywords="chunking, silent-discard" # ### Outgoing Mail From 84919fefa4de2374fea2541ae112a1e6ab72ce9a Mon Sep 17 00:00:00 2001 From: solomon-s-b Date: Sun, 10 Mar 2024 13:15:25 +0200 Subject: [PATCH 023/121] Fix miab-munin.conf filter not capturing HTTP/2.0 (#2359) --- conf/fail2ban/filter.d/miab-munin.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conf/fail2ban/filter.d/miab-munin.conf b/conf/fail2ban/filter.d/miab-munin.conf index b254cc62..da257b3e 100644 --- a/conf/fail2ban/filter.d/miab-munin.conf +++ b/conf/fail2ban/filter.d/miab-munin.conf @@ -3,5 +3,5 @@ before = common.conf [Definition] -failregex= - .*GET /admin/munin/.* HTTP/1.1\" 401.* +failregex= - .*GET /admin/munin/.* HTTP/\d+\.\d+\" 401.* ignoreregex = From 4ad679da477cfaa3ef72d18108617e6352e0fbd4 Mon Sep 17 00:00:00 2001 From: John James Jacoby Date: Sun, 10 Mar 2024 06:16:03 -0500 Subject: [PATCH 024/121] 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. --- setup/webmail.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup/webmail.sh b/setup/webmail.sh index 62d18c86..584a47b3 100755 --- a/setup/webmail.sh +++ b/setup/webmail.sh @@ -217,7 +217,7 @@ sed -i.miabold 's/^[^#]\+.\+PRAGMA journal_mode = WAL.\+$/#&/' \ # Because Roundcube wants to set the PRAGMA we just deleted from the source, we apply it here # to the roundcube database (see https://github.com/roundcube/roundcubemail/issues/8035) # Database should exist, created by migration script -sqlite3 $STORAGE_ROOT/mail/roundcube/roundcube.sqlite 'PRAGMA journal_mode=WAL;' +sqlite3 $STORAGE_ROOT/mail/roundcube/roundcube.sqlite 'PRAGMA journal_mode=WAL;' | 2>&1 # Enable PHP modules. phpenmod -v $PHP_VER imap From 111468efb987f5e66a79cceb5a7c5392822b4822 Mon Sep 17 00:00:00 2001 From: Michael Heuberger Date: Mon, 11 Mar 2024 00:22:51 +1300 Subject: [PATCH 025/121] 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 --- .gitignore | 3 ++- setup/nextcloud.sh | 58 +++++++++++++++++++++++++++++++++------------- 2 files changed, 44 insertions(+), 17 deletions(-) diff --git a/.gitignore b/.gitignore index 14e6c4a7..7135c1b3 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,5 @@ tools/__pycache__/ externals/ .env .vagrant -api/docs/api-docs.html \ No newline at end of file +api/docs/api-docs.html +*.code-workspace diff --git a/setup/nextcloud.sh b/setup/nextcloud.sh index a2d9d2e5..947d2f9a 100755 --- a/setup/nextcloud.sh +++ b/setup/nextcloud.sh @@ -21,31 +21,48 @@ echo "Installing Nextcloud (contacts/calendar)..." # 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=25.0.7 -nextcloud_hash=a5a565c916355005c7b408dd41a1e53505e1a080 +nextcloud_ver=26.0.12 +nextcloud_hash=b55e9f51171c0a9b9ab3686cf5c8ad1a4292ca15 # Nextcloud apps # -------------- -# * Find the most recent tag that is compatible with the Nextcloud version above by -# consulting the ... node at: -# https://github.com/nextcloud-releases/contacts/blob/main/appinfo/info.xml -# https://github.com/nextcloud-releases/calendar/blob/main/appinfo/info.xml -# https://github.com/nextcloud/user_external/blob/master/appinfo/info.xml -# * The hash is the SHA1 hash of the ZIP package, which you can find by just running this script and -# copying it from the error message when it doesn't match what is below. -contacts_ver=5.3.0 -contacts_hash=4b0a6666374e3b55cfd2ae9b72e1d458b87d4c8c +# * 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.4.2 -calendar_hash=21a42e15806adc9b2618760ef94f1797ef399e2f +calendar_ver=4.6.6 +calendar_hash=e34a71669a52d997e319d64a984dcd041389eb22 -# And https://apps.nextcloud.com/apps/user_external +# Always ensure the versions are supported, see https://apps.nextcloud.com/apps/user_external user_external_ver=3.2.0 user_external_hash=a494073dcdecbbbc79a9c77f72524ac9994d2eec -# Clear prior packages and install dependencies from apt. +# Developer advice (test plan) +# ---------------------------- +# When upgrading above versions, how to test? +# +# 1. Enter your server instance (or on the Vagrant image) +# 1. Git clone +# 2. Git checkout +# 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 \ @@ -141,7 +158,7 @@ InstallNextcloud() { # 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 +# $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. @@ -195,6 +212,11 @@ if [ ! -d /usr/local/lib/owncloud/ ] || [[ ! ${CURRENT_NEXTCLOUD_VER} =~ ^$nextc 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" @@ -211,6 +233,10 @@ if [ ! -d /usr/local/lib/owncloud/ ] || [[ ! ${CURRENT_NEXTCLOUD_VER} =~ ^$nextc 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 From 040d0cbb7cab4329f9dae4960a42f5dc4d76ce63 Mon Sep 17 00:00:00 2001 From: KiekerJan Date: Sun, 10 Mar 2024 12:24:29 +0100 Subject: [PATCH 026/121] Update roundcube to 1.6.6 (#2360) --- setup/webmail.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup/webmail.sh b/setup/webmail.sh index 584a47b3..68042789 100755 --- a/setup/webmail.sh +++ b/setup/webmail.sh @@ -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.5 -HASH=326fcc206cddc00355e98d1e40fd0bcd9baec69f +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 From 293d56c781505c0af550e1529cdba56d28d72182 Mon Sep 17 00:00:00 2001 From: KiekerJan Date: Sun, 10 Mar 2024 12:26:33 +0100 Subject: [PATCH 027/121] Update javascript libraries used by control panel (#2351) --- setup/management.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/setup/management.sh b/setup/management.sh index 50c24189..34c0aeaf 100755 --- a/setup/management.sh +++ b/setup/management.sh @@ -66,18 +66,18 @@ rm -rf $assets_dir mkdir -p $assets_dir # jQuery CDN URL -jquery_version=2.1.4 +jquery_version=2.2.4 jquery_url=https://code.jquery.com # Get jQuery -wget_verify $jquery_url/jquery-$jquery_version.min.js 43dc554608df885a59ddeece1598c6ace434d747 $assets_dir/jquery.min.js +wget_verify $jquery_url/jquery-$jquery_version.min.js 69bb69e25ca7d5ef0935317584e6153f3fd9a88c $assets_dir/jquery.min.js # Bootstrap CDN URL -bootstrap_version=3.3.7 +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 e6b1000b94e835ffd37f4c6dcbdad43f4b48a02a /tmp/bootstrap.zip +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 From 785c337fb389665f4a8f3a3a193bafa355f1d5ff Mon Sep 17 00:00:00 2001 From: KiekerJan Date: Sun, 10 Mar 2024 12:27:04 +0100 Subject: [PATCH 028/121] Make reading of previous status check result more robust (#2347) --- management/status_checks.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/management/status_checks.py b/management/status_checks.py index 07e7dc1d..e8faf6fb 100755 --- a/management/status_checks.py +++ b/management/status_checks.py @@ -965,7 +965,10 @@ def run_and_output_changes(env, pool): cache_fn = "/var/cache/mailinabox/status_checks.json" if os.path.exists(cache_fn): with open(cache_fn, 'r') as f: - prev = json.load(f) + try: + prev = json.load(f) + except json.JSONDecodeError: + prev = [] # Group the serial output into categories by the headings. def group_by_heading(lines): From 0ee64f2fe865ad491c69a7afb5af3d5c2822a19e Mon Sep 17 00:00:00 2001 From: Teal Dulcet Date: Fri, 22 Dec 2023 07:08:30 -0800 Subject: [PATCH 029/121] Fixed F401 (unused-import) --- management/auth.py | 2 +- management/backup.py | 2 +- management/cli.py | 2 +- management/daemon.py | 4 ++-- management/dns_update.py | 2 +- management/mailconfig.py | 2 +- management/ssl_certificates.py | 1 - management/status_checks.py | 3 +-- management/wsgi.py | 2 +- tests/fail2ban.py | 3 +-- tests/test_dns.py | 2 +- 11 files changed, 11 insertions(+), 14 deletions(-) diff --git a/management/auth.py b/management/auth.py index c576d01c..d450720d 100644 --- a/management/auth.py +++ b/management/auth.py @@ -1,4 +1,4 @@ -import base64, os, os.path, hmac, json, secrets +import base64, hmac, json, secrets from datetime import timedelta from expiringdict import ExpiringDict diff --git a/management/backup.py b/management/backup.py index c2ef7676..0d2a0c07 100755 --- a/management/backup.py +++ b/management/backup.py @@ -7,7 +7,7 @@ # 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, sys +import os, os.path, re, datetime, sys import dateutil.parser, dateutil.relativedelta, dateutil.tz import rtyaml from exclusiveprocess import Lock diff --git a/management/cli.py b/management/cli.py index b32089c6..cbf19fc0 100755 --- a/management/cli.py +++ b/management/cli.py @@ -6,7 +6,7 @@ # 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, re, csv +import sys, getpass, urllib.request, urllib.error, json, csv def mgmt(cmd, data=None, is_json=False): # The base URL for the management daemon. (Listens on IPv4 only.) diff --git a/management/daemon.py b/management/daemon.py index 47548531..5081aea3 100755 --- a/management/daemon.py +++ b/management/daemon.py @@ -11,11 +11,11 @@ # service mailinabox start # when done debugging, start it up again import os, os.path, re, json, time -import multiprocessing.pool, subprocess +import multiprocessing.pool from functools import wraps -from flask import Flask, request, render_template, abort, Response, send_from_directory, make_response +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 diff --git a/management/dns_update.py b/management/dns_update.py index 9a768ea8..64b55dec 100755 --- a/management/dns_update.py +++ b/management/dns_update.py @@ -4,7 +4,7 @@ # and mail aliases and restarts nsd. ######################################################################## -import sys, os, os.path, urllib.parse, datetime, re, hashlib, base64 +import sys, os, os.path, datetime, re, hashlib, base64 import ipaddress import rtyaml import dns.resolver diff --git a/management/mailconfig.py b/management/mailconfig.py index 2fcb9703..4068b393 100755 --- a/management/mailconfig.py +++ b/management/mailconfig.py @@ -9,7 +9,7 @@ # Python 3 in setup/questions.sh to validate the email # address entered by the user. -import subprocess, shutil, os, sqlite3, re +import os, sqlite3, re import utils from email_validator import validate_email as validate_email_, EmailNotValidError import idna diff --git a/management/ssl_certificates.py b/management/ssl_certificates.py index 19f0266a..f3e83a15 100755 --- a/management/ssl_certificates.py +++ b/management/ssl_certificates.py @@ -4,7 +4,6 @@ import os, os.path, re, shutil, subprocess, tempfile from utils import shell, safe_domain_name, sort_domains -import idna # SELECTING SSL CERTIFICATES FOR USE IN WEB diff --git a/management/status_checks.py b/management/status_checks.py index e8faf6fb..927e6102 100755 --- a/management/status_checks.py +++ b/management/status_checks.py @@ -4,11 +4,10 @@ # TLS certificates have been signed, etc., and if not tells the user # what to do next. -import sys, os, os.path, re, subprocess, datetime, multiprocessing.pool +import sys, os, os.path, re, datetime, multiprocessing.pool import asyncio import dns.reversename, dns.resolver -import dateutil.parser, dateutil.tz import idna import psutil import postfix_mta_sts_resolver.resolver diff --git a/management/wsgi.py b/management/wsgi.py index 86cf3af4..9cf21c4b 100644 --- a/management/wsgi.py +++ b/management/wsgi.py @@ -1,5 +1,5 @@ from daemon import app -import auth, utils +import utils app.logger.addHandler(utils.create_syslog_handler()) diff --git a/tests/fail2ban.py b/tests/fail2ban.py index cb55c51f..9979507f 100644 --- a/tests/fail2ban.py +++ b/tests/fail2ban.py @@ -6,7 +6,7 @@ # try to log in to. ###################################################################### -import sys, os, time, functools +import sys, os, time # parse command line @@ -163,7 +163,6 @@ def run_test(testfunc, args, count, within_seconds, parallel): # run testfunc sequentially and still get to count requests within # the required time. So we split the requests across threads. - import requests.exceptions from multiprocessing import Pool restart_fail2ban_service() diff --git a/tests/test_dns.py b/tests/test_dns.py index 25c64bf1..62cf5e78 100755 --- a/tests/test_dns.py +++ b/tests/test_dns.py @@ -7,7 +7,7 @@ # where ipaddr is the IP address of your Mail-in-a-Box # and hostname is the domain name to check the DNS for. -import sys, re, difflib +import sys, re import dns.reversename, dns.resolver if len(sys.argv) < 3: From cb922ec286c99c01c75fa526af27c78c64869838 Mon Sep 17 00:00:00 2001 From: Teal Dulcet Date: Fri, 22 Dec 2023 07:08:56 -0800 Subject: [PATCH 030/121] Fixed UP015 (redundant-open-modes): Unnecessary open mode parameters --- management/auth.py | 2 +- management/backup.py | 4 ++-- management/cli.py | 2 +- management/dns_update.py | 10 +++++----- management/status_checks.py | 6 +++--- management/utils.py | 4 ++-- management/web_update.py | 6 +++--- tools/editconf.py | 2 +- tools/parse-nginx-log-bootstrap-accesses.py | 2 +- 9 files changed, 19 insertions(+), 19 deletions(-) diff --git a/management/auth.py b/management/auth.py index d450720d..653d0485 100644 --- a/management/auth.py +++ b/management/auth.py @@ -22,7 +22,7 @@ class AuthService: def init_system_api_key(self): """Write an API key to a local file so local processes can use the API""" - with open(self.key_path, 'r') as file: + with open(self.key_path) as file: self.key = file.read() def authenticate(self, request, env, login_only=False, logout=False): diff --git a/management/backup.py b/management/backup.py index 0d2a0c07..6ec4d28a 100755 --- a/management/backup.py +++ b/management/backup.py @@ -578,7 +578,7 @@ def get_backup_config(env, for_save=False, for_ui=False): # Merge in anything written to custom.yaml. try: - with open(os.path.join(backup_root, 'custom.yaml'), 'r') as f: + with open(os.path.join(backup_root, 'custom.yaml')) as f: custom_config = rtyaml.load(f) if not isinstance(custom_config, dict): raise ValueError() # caught below config.update(custom_config) @@ -604,7 +604,7 @@ def get_backup_config(env, for_save=False, for_ui=False): 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, 'r') as f: + with open(ssh_pub_key) as f: config["ssh_pub_key"] = f.read() return config diff --git a/management/cli.py b/management/cli.py index cbf19fc0..5e2e331c 100755 --- a/management/cli.py +++ b/management/cli.py @@ -47,7 +47,7 @@ def read_password(): return first def setup_key_auth(mgmt_uri): - with open('/var/lib/mailinabox/api.key', 'r') as f: + with open('/var/lib/mailinabox/api.key') as f: key = f.read().strip() auth_handler = urllib.request.HTTPBasicAuthHandler() diff --git a/management/dns_update.py b/management/dns_update.py index 64b55dec..0869e77d 100755 --- a/management/dns_update.py +++ b/management/dns_update.py @@ -455,7 +455,7 @@ def build_sshfp_records(): # specify that port to sshkeyscan. port = 22 - with open('/etc/ssh/sshd_config', 'r') as f: + with open('/etc/ssh/sshd_config') as f: for line in f: s = line.rstrip().split() if len(s) == 2 and s[0] == 'Port': @@ -606,7 +606,7 @@ def get_dns_zonefile(zone, env): raise ValueError("%s is not a domain name that corresponds to a zone." % zone) nsd_zonefile = "/etc/nsd/zones/" + fn - with open(nsd_zonefile, "r") as f: + with open(nsd_zonefile) as f: return f.read() ######################################################################## @@ -676,7 +676,7 @@ def hash_dnssec_keys(domain, env): oldkeyfn = os.path.join(env['STORAGE_ROOT'], 'dns/dnssec', keyfn + ".private") keydata.append(keytype) keydata.append(keyfn) - with open(oldkeyfn, "r") as fr: + with open(oldkeyfn) as fr: keydata.append( fr.read() ) keydata = "".join(keydata).encode("utf8") return hashlib.sha1(keydata).hexdigest() @@ -704,7 +704,7 @@ def sign_zone(domain, zonefile, env): # Use os.umask and open().write() to securely create a copy that only # we (root) can read. oldkeyfn = os.path.join(env['STORAGE_ROOT'], 'dns/dnssec', keyfn + ext) - with open(oldkeyfn, "r") as fr: + with open(oldkeyfn) as fr: keydata = fr.read() keydata = keydata.replace("_domain_", domain) prev_umask = os.umask(0o77) # ensure written file is not world-readable @@ -815,7 +815,7 @@ def write_opendkim_tables(domains, env): def get_custom_dns_config(env, only_real_records=False): try: - with open(os.path.join(env['STORAGE_ROOT'], 'dns/custom.yaml'), 'r') as f: + with open(os.path.join(env['STORAGE_ROOT'], 'dns/custom.yaml')) as f: custom_dns = rtyaml.load(f) if not isinstance(custom_dns, dict): raise ValueError() # caught below except: diff --git a/management/status_checks.py b/management/status_checks.py index 927e6102..81038300 100755 --- a/management/status_checks.py +++ b/management/status_checks.py @@ -212,7 +212,7 @@ def check_ssh_password(env, output): # the configuration file. if not os.path.exists("/etc/ssh/sshd_config"): return - with open("/etc/ssh/sshd_config", "r") as f: + with open("/etc/ssh/sshd_config") as f: sshd = f.read() if re.search("\nPasswordAuthentication\s+yes", sshd) \ or not re.search("\nPasswordAuthentication\s+no", sshd): @@ -600,7 +600,7 @@ def check_dnssec(domain, env, output, dns_zonefiles, is_checking_primary=False): # record that we suggest using is for the KSK (and that's how the DS records were generated). # We'll also give the nice name for the key algorithm. dnssec_keys = load_env_vars_from_file(os.path.join(env['STORAGE_ROOT'], 'dns/dnssec/%s.conf' % alg_name_map[ds_alg])) - with open(os.path.join(env['STORAGE_ROOT'], 'dns/dnssec/' + dnssec_keys['KSK'] + '.key'), 'r') as f: + with open(os.path.join(env['STORAGE_ROOT'], 'dns/dnssec/' + dnssec_keys['KSK'] + '.key')) as f: dnsssec_pubkey = f.read().split("\t")[3].split(" ")[3] expected_ds_records[ (ds_keytag, ds_alg, ds_digalg, ds_digest) ] = { @@ -963,7 +963,7 @@ def run_and_output_changes(env, pool): # Load previously saved status checks. cache_fn = "/var/cache/mailinabox/status_checks.json" if os.path.exists(cache_fn): - with open(cache_fn, 'r') as f: + with open(cache_fn) as f: try: prev = json.load(f) except json.JSONDecodeError: diff --git a/management/utils.py b/management/utils.py index b5ca7e59..baf99143 100644 --- a/management/utils.py +++ b/management/utils.py @@ -14,7 +14,7 @@ def load_env_vars_from_file(fn): # Load settings from a KEY=VALUE file. import collections env = collections.OrderedDict() - with open(fn, 'r') as f: + with open(fn) as f: for line in f: env.setdefault(*line.strip().split("=", 1)) return env @@ -36,7 +36,7 @@ def load_settings(env): import rtyaml fn = os.path.join(env['STORAGE_ROOT'], 'settings.yaml') try: - with open(fn, "r") as f: + with open(fn) as f: config = rtyaml.load(f) if not isinstance(config, dict): raise ValueError() # caught below return config diff --git a/management/web_update.py b/management/web_update.py index e23bb2d8..46a6f2a0 100644 --- a/management/web_update.py +++ b/management/web_update.py @@ -63,7 +63,7 @@ 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): - with open(nginx_conf_custom_fn, 'r') as f: + with open(nginx_conf_custom_fn) as f: custom_settings = rtyaml.load(f) for domain, settings in custom_settings.items(): for type, value in [('redirect', settings.get('redirects', {}).get('/')), @@ -78,7 +78,7 @@ def do_web_update(env): # Helper for reading config files and templates def read_conf(conf_fn): - with open(os.path.join(os.path.dirname(__file__), "../conf", conf_fn), "r") as f: + with open(os.path.join(os.path.dirname(__file__), "../conf", conf_fn)) as f: return f.read() # Build an nginx configuration file. @@ -156,7 +156,7 @@ def make_domain_config(domain, templates, ssl_certificates, env): hsts = "yes" nginx_conf_custom_fn = os.path.join(env["STORAGE_ROOT"], "www/custom.yaml") if os.path.exists(nginx_conf_custom_fn): - with open(nginx_conf_custom_fn, 'r') as f: + with open(nginx_conf_custom_fn) as f: yaml = rtyaml.load(f) if domain in yaml: yaml = yaml[domain] diff --git a/tools/editconf.py b/tools/editconf.py index ec112b02..ea4d9020 100755 --- a/tools/editconf.py +++ b/tools/editconf.py @@ -76,7 +76,7 @@ for setting in settings: found = set() buf = "" -with open(filename, "r") as f: +with open(filename) as f: input_lines = list(f) while len(input_lines) > 0: diff --git a/tools/parse-nginx-log-bootstrap-accesses.py b/tools/parse-nginx-log-bootstrap-accesses.py index c61ed79e..6207b2e9 100755 --- a/tools/parse-nginx-log-bootstrap-accesses.py +++ b/tools/parse-nginx-log-bootstrap-accesses.py @@ -38,7 +38,7 @@ for date, ip in accesses: # Since logs are rotated, store the statistics permanently in a JSON file. # Load in the stats from an existing file. if os.path.exists(outfn): - with open(outfn, "r") as f: + with open(outfn) as f: existing_data = json.load(f) for date, count in existing_data: if date not in by_date: From 49124cc9caee228cf5595381203b667e82c8750f Mon Sep 17 00:00:00 2001 From: Teal Dulcet Date: Fri, 22 Dec 2023 07:10:25 -0800 Subject: [PATCH 031/121] Fixed PLR6201 (literal-membership): Use a `set` literal when testing for membership --- management/auth.py | 2 +- management/backup.py | 2 +- management/cli.py | 6 +++--- management/daemon.py | 6 +++--- management/dns_update.py | 10 +++++----- management/mail_log.py | 12 ++++++------ management/mailconfig.py | 2 +- management/ssl_certificates.py | 2 +- management/status_checks.py | 18 +++++++++--------- management/web_update.py | 2 +- tools/editconf.py | 2 +- 11 files changed, 32 insertions(+), 32 deletions(-) diff --git a/management/auth.py b/management/auth.py index 653d0485..2d5b8a36 100644 --- a/management/auth.py +++ b/management/auth.py @@ -48,7 +48,7 @@ class AuthService: return username, password username, password = parse_http_authorization_basic(request.headers.get('Authorization', '')) - if username in (None, ""): + if username in {None, ""}: raise ValueError("Authorization header invalid.") if username.strip() == "" and password.strip() == "": diff --git a/management/backup.py b/management/backup.py index 6ec4d28a..bb6dff9a 100755 --- a/management/backup.py +++ b/management/backup.py @@ -556,7 +556,7 @@ def backup_set_custom(env, target, target_user, target_pass, min_age): # Validate. try: - if config["target"] not in ("off", "local"): + 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) diff --git a/management/cli.py b/management/cli.py index 5e2e331c..115ed9f2 100755 --- a/management/cli.py +++ b/management/cli.py @@ -91,7 +91,7 @@ elif sys.argv[1] == "user" and len(sys.argv) == 2: print("*", end='') print() -elif sys.argv[1] == "user" and sys.argv[2] in ("add", "password"): +elif sys.argv[1] == "user" and sys.argv[2] in {"add", "password"}: if len(sys.argv) < 5: if len(sys.argv) < 4: email = input("email: ") @@ -109,7 +109,7 @@ elif sys.argv[1] == "user" and sys.argv[2] in ("add", "password"): 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: +elif sys.argv[1] == "user" and sys.argv[2] in {"make-admin", "remove-admin"} and len(sys.argv) == 4: if sys.argv[2] == "make-admin": action = "add" else: @@ -132,7 +132,7 @@ elif sys.argv[1] == "user" and len(sys.argv) == 5 and sys.argv[2:4] == ["mfa", " 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"]: +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 })) diff --git a/management/daemon.py b/management/daemon.py index 5081aea3..1a35fba7 100755 --- a/management/daemon.py +++ b/management/daemon.py @@ -90,7 +90,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: @@ -355,9 +355,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. diff --git a/management/dns_update.py b/management/dns_update.py index 0869e77d..23aee2a5 100755 --- a/management/dns_update.py +++ b/management/dns_update.py @@ -364,7 +364,7 @@ def build_zone(domain, domain_properties, additional_records, env, is_zone=True) # non-mail domain and also may include qnames from custom DNS records. # Do this once at the end of generating a zone. if is_zone: - qnames_with_a = set(qname for (qname, rtype, value, explanation) in records if rtype in ("A", "AAAA")) + qnames_with_a = set(qname for (qname, rtype, value, explanation) in records if rtype in {"A", "AAAA"}) qnames_with_mx = set(qname for (qname, rtype, value, explanation) in records if rtype == "MX") for qname in qnames_with_a - qnames_with_mx: # Mark this domain as not sending mail with hard-fail SPF and DMARC records. @@ -921,12 +921,12 @@ def set_custom_dns_record(qname, rtype, value, action, env): if not re.search(DOMAIN_RE, qname): raise ValueError("Invalid name.") - if rtype in ("A", "AAAA"): + if rtype in {"A", "AAAA"}: if value != "local": # "local" is a special flag for us v = ipaddress.ip_address(value) # raises a ValueError if there's a problem if rtype == "A" and not isinstance(v, ipaddress.IPv4Address): raise ValueError("That's an IPv6 address.") if rtype == "AAAA" and not isinstance(v, ipaddress.IPv6Address): raise ValueError("That's an IPv4 address.") - elif rtype in ("CNAME", "NS"): + elif rtype in {"CNAME", "NS"}: if rtype == "NS" and qname == zone: raise ValueError("NS records can only be set for subdomains.") @@ -936,7 +936,7 @@ def set_custom_dns_record(qname, rtype, value, action, env): if not re.search(DOMAIN_RE, value): raise ValueError("Invalid value.") - elif rtype in ("CNAME", "TXT", "SRV", "MX", "SSHFP", "CAA"): + elif rtype in {"CNAME", "TXT", "SRV", "MX", "SSHFP", "CAA"}: # anything goes pass else: @@ -979,7 +979,7 @@ def set_custom_dns_record(qname, rtype, value, action, env): # Preserve this record. newconfig.append((_qname, _rtype, _value)) - if action in ("add", "set") and needs_add and value is not None: + if action in {"add", "set"} and needs_add and value is not None: newconfig.append((qname, rtype, value)) made_change = True diff --git a/management/mail_log.py b/management/mail_log.py index 1a963966..38ae1ec6 100755 --- a/management/mail_log.py +++ b/management/mail_log.py @@ -376,9 +376,9 @@ def scan_mail_log_line(line, 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", + elif service in {"postfix/qmgr", "postfix/pickup", "postfix/cleanup", "postfix/scache", "spampd", "postfix/anvil", "postfix/master", "opendkim", "postfix/lmtp", - "postfix/tlsmgr", "anvil"): + "postfix/tlsmgr", "anvil"}: # nothing to look at return True else: @@ -500,7 +500,7 @@ def add_login(user, date, protocol_name, host, collector): 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: + if host not in {"127.0.0.1", "::1"} or True: data["activity-by-hour"][protocol_name][date.hour] += 1 collector["logins"][user] = data @@ -684,7 +684,7 @@ def print_user_table(users, data=None, sub_data=None, activity=None, latest=None data_accum[col] += d[row] try: - if None not in [latest, earliest]: + if None not in {latest, earliest}: vert_pos = len(line) e = earliest[row] l = latest[row] @@ -740,7 +740,7 @@ def print_user_table(users, data=None, sub_data=None, activity=None, latest=None else: header += l.rjust(max(5, len(l) + 1, col_widths[col])) - if None not in (latest, earliest): + if None not in {latest, earliest}: header += " ā”‚ timespan " lines.insert(0, header.rstrip()) @@ -765,7 +765,7 @@ def print_user_table(users, data=None, sub_data=None, activity=None, latest=None footer += temp.format(data_accum[row]) try: - if None not in [latest, earliest]: + if None not in {latest, earliest}: max_l = max(latest) min_e = min(earliest) timespan = relativedelta(max_l, min_e) diff --git a/management/mailconfig.py b/management/mailconfig.py index 4068b393..3bfc8dc0 100755 --- a/management/mailconfig.py +++ b/management/mailconfig.py @@ -588,7 +588,7 @@ def kick(env, mail_result=None): # 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") \ + if user in {"postmaster", "admin", "abuse"} \ and address not in required_aliases \ and forwards_to == get_system_administrator(env) \ and not auto: diff --git a/management/ssl_certificates.py b/management/ssl_certificates.py index f3e83a15..77a411b6 100755 --- a/management/ssl_certificates.py +++ b/management/ssl_certificates.py @@ -637,7 +637,7 @@ def load_pem(pem): if pem_type is None: raise ValueError("File is not a valid PEM-formatted file.") pem_type = pem_type.group(1) - if pem_type in (b"RSA PRIVATE KEY", b"PRIVATE KEY"): + 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()) diff --git a/management/status_checks.py b/management/status_checks.py index 81038300..7cdfffc6 100755 --- a/management/status_checks.py +++ b/management/status_checks.py @@ -151,7 +151,7 @@ def check_service(i, service, env): output.print_error("%s is not running (port %d)." % (service['name'], service['port'])) # Why is nginx not running? - if not running and service["port"] in (80, 443): + if not running and service["port"] in {80, 443}: output.print_line(shell('check_output', ['nginx', '-t'], capture_stderr=True, trap=True)[1].strip()) else: @@ -340,7 +340,7 @@ def run_domain_checks(rounded_time, env, output, pool, domains_to_check=None): domains_to_check = [ d for d in domains_to_check if not ( - d.split(".", 1)[0] in ("www", "autoconfig", "autodiscover", "mta-sts") + d.split(".", 1)[0] in {"www", "autoconfig", "autodiscover", "mta-sts"} and len(d.split(".", 1)) == 2 and d.split(".", 1)[1] in domains_to_check ) @@ -467,7 +467,7 @@ def check_primary_hostname_dns(domain, env, output, dns_domains, dns_zonefiles): # a DNS zone if it is a subdomain of another domain we have a zone for. existing_rdns_v4 = query_dns(dns.reversename.from_address(env['PUBLIC_IP']), "PTR") existing_rdns_v6 = query_dns(dns.reversename.from_address(env['PUBLIC_IPV6']), "PTR") if env.get("PUBLIC_IPV6") else None - if existing_rdns_v4 == domain and existing_rdns_v6 in (None, domain): + if existing_rdns_v4 == domain and existing_rdns_v6 in {None, domain}: output.print_ok("Reverse DNS is set correctly at ISP. [%s ā†¦ %s]" % (my_ips, env['PRIMARY_HOSTNAME'])) elif existing_rdns_v4 == existing_rdns_v6 or existing_rdns_v6 is None: output.print_error("""Your box's reverse DNS is currently %s, but it should be %s. Your ISP or cloud provider will have instructions @@ -636,7 +636,7 @@ def check_dnssec(domain, env, output, dns_zonefiles, is_checking_primary=False): if set(r[1] for r in matched_ds) == { '13' } and set(r[2] for r in matched_ds) <= { '2', '4' }: # all are alg 13 and digest type 2 or 4 output.print_ok("DNSSEC 'DS' record is set correctly at registrar.") return - elif len([r for r in matched_ds if r[1] == '13' and r[2] in ( '2', '4' )]) > 0: # some but not all are alg 13 + elif len([r for r in matched_ds if r[1] == '13' and r[2] in { '2', '4' }]) > 0: # some but not all are alg 13 output.print_ok("DNSSEC 'DS' record is set correctly at registrar. (Records using algorithm other than ECDSAP256SHA256 and digest types other than SHA-256/384 should be removed.)") return else: # no record uses alg 13 @@ -825,7 +825,7 @@ def query_dns(qname, rtype, nxdomain='[Not Set]', at=None, as_list=False): # be expressed in equivalent string forms. Canonicalize the form before # returning them. The caller should normalize any IP addresses the result # of this method is compared with. - if rtype in ("A", "AAAA"): + if rtype in {"A", "AAAA"}: response = [normalize_ip(str(r)) for r in response] if as_list: @@ -841,7 +841,7 @@ def check_ssl_cert(domain, rounded_time, ssl_certificates, env, output): # Check that TLS certificate is signed. # Skip the check if the A record is not pointed here. - if query_dns(domain, "A", None) not in (env['PUBLIC_IP'], None): return + if query_dns(domain, "A", None) not in {env['PUBLIC_IP'], None}: return # Where is the certificate file stored? tls_cert = get_domain_ssl_files(domain, ssl_certificates, env, allow_missing_cert=True) @@ -1002,14 +1002,14 @@ def run_and_output_changes(env, pool): out.add_heading(category + " -- Previously:") elif op == "delete": out.add_heading(category + " -- Removed") - if op in ("replace", "delete"): + if op in {"replace", "delete"}: BufferedOutput(with_lines=prev_lines[i1:i2]).playback(out) if op == "replace": out.add_heading(category + " -- Currently:") elif op == "insert": out.add_heading(category + " -- Added") - if op in ("replace", "insert"): + if op in {"replace", "insert"}: BufferedOutput(with_lines=cur_lines[j1:j2]).playback(out) for category, prev_lines in prev_status.items(): @@ -1095,7 +1095,7 @@ class BufferedOutput: def __init__(self, with_lines=None): self.buf = [] if not with_lines else with_lines def __getattr__(self, attr): - if attr not in ("add_heading", "print_ok", "print_error", "print_warning", "print_block", "print_line"): + if attr not in {"add_heading", "print_ok", "print_error", "print_warning", "print_block", "print_line"}: raise AttributeError # Return a function that just records the call & arguments to our buffer. def w(*args, **kwargs): diff --git a/management/web_update.py b/management/web_update.py index 46a6f2a0..14ad1379 100644 --- a/management/web_update.py +++ b/management/web_update.py @@ -53,7 +53,7 @@ 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 not in ("local", env['PUBLIC_IP'])): + if rtype == "CNAME" or (rtype in {"A", "AAAA"} and value not in {"local", env['PUBLIC_IP']}): domains.add(domain) return domains diff --git a/tools/editconf.py b/tools/editconf.py index ea4d9020..9e397846 100755 --- a/tools/editconf.py +++ b/tools/editconf.py @@ -84,7 +84,7 @@ while len(input_lines) > 0: # If this configuration file uses folded lines, append any folded lines # into our input buffer. - if folded_lines and line[0] not in (comment_char, " ", ""): + if folded_lines and line[0] not in {comment_char, " ", ""}: while len(input_lines) > 0 and input_lines[0][0] in " \t": line += input_lines.pop(0) From dd61844cedc9edc150466bf063320d6180685c65 Mon Sep 17 00:00:00 2001 From: Teal Dulcet Date: Fri, 22 Dec 2023 07:10:48 -0800 Subject: [PATCH 032/121] Fixed EM101 (raw-string-in-exception): Exception must not use a string literal, assign to variable first --- management/auth.py | 12 ++++++++---- management/backup.py | 6 ++++-- management/dns_update.py | 9 ++++++--- management/mailconfig.py | 6 ++++-- management/mfa.py | 12 ++++++++---- management/ssl_certificates.py | 6 ++++-- tests/fail2ban.py | 12 ++++++++---- 7 files changed, 42 insertions(+), 21 deletions(-) diff --git a/management/auth.py b/management/auth.py index 2d5b8a36..36112572 100644 --- a/management/auth.py +++ b/management/auth.py @@ -49,10 +49,12 @@ class AuthService: username, password = parse_http_authorization_basic(request.headers.get('Authorization', '')) if username in {None, ""}: - raise ValueError("Authorization header invalid.") + msg = "Authorization header invalid." + raise ValueError(msg) if username.strip() == "" and password.strip() == "": - raise ValueError("No email address, password, session key, or API key provided.") + msg = "No email address, password, session key, or API key provided." + raise ValueError(msg) # If user passed the system API key, grant administrative privs. This key # is not associated with a user. @@ -72,7 +74,8 @@ class AuthService: # If no password was given, but a username was given, we're missing some information. elif password.strip() == "": - raise ValueError("Enter a password.") + msg = "Enter a password." + raise ValueError(msg) else: # The user is trying to log in with a username and a password @@ -114,7 +117,8 @@ class AuthService: ]) except: # Login failed. - raise ValueError("Incorrect email address or password.") + 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) diff --git a/management/backup.py b/management/backup.py index bb6dff9a..75046971 100755 --- a/management/backup.py +++ b/management/backup.py @@ -507,7 +507,8 @@ def list_target_files(config): path = '' if bucket == "": - raise ValueError("Enter an S3 bucket name.") + msg = "Enter an S3 bucket name." + raise ValueError(msg) # connect to the region & bucket try: @@ -535,7 +536,8 @@ def list_target_files(config): b2_api.authorize_account("production", b2_application_keyid, b2_application_key) bucket = b2_api.get_bucket_by_name(b2_bucket) except NonExistentBucket as e: - raise ValueError("B2 Bucket does not exist. Please double check your information!") + 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: diff --git a/management/dns_update.py b/management/dns_update.py index 23aee2a5..c9e4d359 100755 --- a/management/dns_update.py +++ b/management/dns_update.py @@ -919,7 +919,8 @@ def set_custom_dns_record(qname, rtype, value, action, env): rtype = rtype.upper() if value is not None and qname != "_secondary_nameserver": if not re.search(DOMAIN_RE, qname): - raise ValueError("Invalid name.") + msg = "Invalid name." + raise ValueError(msg) if rtype in {"A", "AAAA"}: if value != "local": # "local" is a special flag for us @@ -928,14 +929,16 @@ def set_custom_dns_record(qname, rtype, value, action, env): if rtype == "AAAA" and not isinstance(v, ipaddress.IPv6Address): raise ValueError("That's an IPv4 address.") elif rtype in {"CNAME", "NS"}: if rtype == "NS" and qname == zone: - raise ValueError("NS records can only be set for subdomains.") + msg = "NS records can only be set for subdomains." + raise ValueError(msg) # ensure value has a trailing dot if not value.endswith("."): value = value + "." if not re.search(DOMAIN_RE, value): - raise ValueError("Invalid value.") + msg = "Invalid value." + raise ValueError(msg) elif rtype in {"CNAME", "TXT", "SRV", "MX", "SSHFP", "CAA"}: # anything goes pass diff --git a/management/mailconfig.py b/management/mailconfig.py index 3bfc8dc0..cfdc3239 100755 --- a/management/mailconfig.py +++ b/management/mailconfig.py @@ -608,9 +608,11 @@ def kick(env, mail_result=None): def validate_password(pw): # validate password if pw.strip() == "": - raise ValueError("No password provided.") + msg = "No password provided." + raise ValueError(msg) if len(pw) < 8: - raise ValueError("Passwords must be at least eight characters.") + msg = "Passwords must be at least eight characters." + raise ValueError(msg) if __name__ == "__main__": import sys diff --git a/management/mfa.py b/management/mfa.py index 32eb5183..148fd912 100644 --- a/management/mfa.py +++ b/management/mfa.py @@ -41,9 +41,11 @@ def enable_mfa(email, type, secret, token, label, env): # Sanity check with the provide current token. totp = pyotp.TOTP(secret) if not totp.verify(token, valid_window=1): - raise ValueError("Invalid token.") + msg = "Invalid token." + raise ValueError(msg) else: - raise ValueError("Invalid MFA type.") + 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)) @@ -67,9 +69,11 @@ def disable_mfa(email, mfa_id, env): def validate_totp_secret(secret): if type(secret) != str or secret.strip() == "": - raise ValueError("No secret provided.") + msg = "No secret provided." + raise ValueError(msg) if len(secret) != 32: - raise ValueError("Secret should be a 32 characters base32 string") + msg = "Secret should be a 32 characters base32 string" + raise ValueError(msg) def provision_totp(email, env): # Make a new secret. diff --git a/management/ssl_certificates.py b/management/ssl_certificates.py index 77a411b6..8ff43b11 100755 --- a/management/ssl_certificates.py +++ b/management/ssl_certificates.py @@ -624,7 +624,8 @@ def load_cert_chain(pemfile): pem = f.read() + b"\n" # ensure trailing newline pemblocks = re.findall(re_pem, pem) if len(pemblocks) == 0: - raise ValueError("File does not contain valid PEM data.") + msg = "File does not contain valid PEM data." + raise ValueError(msg) return pemblocks def load_pem(pem): @@ -635,7 +636,8 @@ def load_pem(pem): from cryptography.hazmat.backends import default_backend pem_type = re.match(b"-+BEGIN (.*?)-+[\r\n]", pem) if pem_type is None: - raise ValueError("File is not a valid PEM-formatted file.") + 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()) diff --git a/tests/fail2ban.py b/tests/fail2ban.py index 9979507f..254232c4 100644 --- a/tests/fail2ban.py +++ b/tests/fail2ban.py @@ -39,7 +39,8 @@ def smtp_test(): try: server.login("fakeuser", "fakepassword") - raise Exception("authentication didn't fail") + msg = "authentication didn't fail" + raise Exception(msg) except smtplib.SMTPAuthenticationError: # athentication should fail pass @@ -61,7 +62,8 @@ def imap_test(): try: M.login("fakeuser", "fakepassword") - raise Exception("authentication didn't fail") + msg = "authentication didn't fail" + raise Exception(msg) except imaplib.IMAP4.error: # authentication should fail pass @@ -85,7 +87,8 @@ def pop_test(): M = None # don't .quit() return M.list() - raise Exception("authentication didn't fail") + msg = "authentication didn't fail" + raise Exception(msg) finally: if M: M.quit() @@ -103,7 +106,8 @@ def managesieve_test(): try: M.login("fakeuser", "fakepassword") - raise Exception("authentication didn't fail") + msg = "authentication didn't fail" + raise Exception(msg) except imaplib.IMAP4.error: # authentication should fail pass From 555ecc1ebb31915721cd26b9f0b0c19485119c33 Mon Sep 17 00:00:00 2001 From: Teal Dulcet Date: Fri, 22 Dec 2023 07:12:34 -0800 Subject: [PATCH 033/121] Fixed PIE810 (multiple-starts-ends-with): Call `startswith` once with a `tuple` --- management/backup.py | 2 +- management/dns_update.py | 2 +- management/mailconfig.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/management/backup.py b/management/backup.py index 75046971..bd8e9f00 100755 --- a/management/backup.py +++ b/management/backup.py @@ -69,7 +69,7 @@ def backup_status(env): # destination for the backups or the last backup job terminated unexpectedly. raise Exception("Something is wrong with the backup: " + collection_status) for line in collection_status.split('\n'): - if line.startswith(" full") or line.startswith(" inc"): + if line.startswith((" full", " inc")): backup = parse_line(line) backups[backup["date"]] = backup diff --git a/management/dns_update.py b/management/dns_update.py index c9e4d359..8afbd376 100755 --- a/management/dns_update.py +++ b/management/dns_update.py @@ -262,7 +262,7 @@ def build_zone(domain, domain_properties, additional_records, env, is_zone=True) has_rec_base = list(records) a_expl = "Required. May have a different value. Sets the IP address that %s resolves to for web hosting and other services besides mail. The A record must be present but its value does not affect mail delivery." % domain if domain_properties[domain]["auto"]: - if domain.startswith("ns1.") or domain.startswith("ns2."): a_expl = False # omit from 'External DNS' page since this only applies if box is its own DNS server + if domain.startswith(("ns1.", "ns2.")): a_expl = False # omit from 'External DNS' page since this only applies if box is its own DNS server if domain.startswith("www."): a_expl = "Optional. Sets the IP address that %s resolves to so that the box can provide a redirect to the parent domain." % domain if domain.startswith("mta-sts."): a_expl = "Optional. MTA-STS Policy Host serving /.well-known/mta-sts.txt." if domain.startswith("autoconfig."): a_expl = "Provides email configuration autodiscovery support for Thunderbird Autoconfig." diff --git a/management/mailconfig.py b/management/mailconfig.py index cfdc3239..3971f825 100755 --- a/management/mailconfig.py +++ b/management/mailconfig.py @@ -87,7 +87,7 @@ def prettify_idn_email_address(email): def is_dcv_address(email): email = email.lower() for localpart in ("admin", "administrator", "postmaster", "hostmaster", "webmaster", "abuse"): - if email.startswith(localpart+"@") or email.startswith(localpart+"+"): + if email.startswith((localpart + "@", localpart + "+")): return True return False From 6bfd1e5140eab643c0e9461fc552c2c504fec473 Mon Sep 17 00:00:00 2001 From: Teal Dulcet Date: Fri, 22 Dec 2023 07:12:50 -0800 Subject: [PATCH 034/121] Fixed W293 (blank-line-with-whitespace): Blank line contains whitespace --- management/backup.py | 6 +++--- management/dns_update.py | 2 +- management/mail_log.py | 2 +- management/utils.py | 2 +- tools/editconf.py | 12 ++++++------ 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/management/backup.py b/management/backup.py index bd8e9f00..c8b33d2f 100755 --- a/management/backup.py +++ b/management/backup.py @@ -226,7 +226,7 @@ def get_duplicity_additional_args(env): 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\"'", @@ -497,7 +497,7 @@ def list_target_files(config): 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:]) + '/' @@ -526,7 +526,7 @@ def list_target_files(config): 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('@')]) diff --git a/management/dns_update.py b/management/dns_update.py index 8afbd376..74ea8a34 100755 --- a/management/dns_update.py +++ b/management/dns_update.py @@ -1044,7 +1044,7 @@ def set_secondary_dns(hostnames, env): resolver = dns.resolver.get_default_resolver() resolver.timeout = 5 resolver.lifetime = 5 - + for item in hostnames: if not item.startswith("xfr:"): # Resolve hostname. diff --git a/management/mail_log.py b/management/mail_log.py index 38ae1ec6..c54c2cb5 100755 --- a/management/mail_log.py +++ b/management/mail_log.py @@ -344,7 +344,7 @@ def scan_mail_log_line(line, collector): # 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') diff --git a/management/utils.py b/management/utils.py index baf99143..0c475d5c 100644 --- a/management/utils.py +++ b/management/utils.py @@ -95,7 +95,7 @@ def sort_domains(domain_names, env): # Then in right-to-left lexicographic order of the .-separated parts of the name. list(reversed(d.split("."))), )) - + return domain_names def sort_email_addresses(email_addresses, env): diff --git a/tools/editconf.py b/tools/editconf.py index 9e397846..898e49b6 100755 --- a/tools/editconf.py +++ b/tools/editconf.py @@ -110,30 +110,30 @@ while len(input_lines) > 0: buf += line found.add(i) break - + # comment-out the existing line (also comment any folded lines) if is_comment is None: buf += comment_char + line.rstrip().replace("\n", "\n" + comment_char) + "\n" else: # the line is already commented, pass it through buf += line - + # if this option already is set don't add the setting again, # or if we're clearing the setting with -e, don't add it if (i in found) or (not val and erase_setting): break - + # add the new setting buf += indent + name + delimiter + val + "\n" - + # note that we've applied this option found.add(i) - + break else: # If did not match any setting names, pass this line through. buf += line - + # Put any settings we didn't see at the end of the file, # except settings being cleared. for i in range(len(settings)): From b7f70b17acf2b8abfca31de9badda9ea5580172b Mon Sep 17 00:00:00 2001 From: Teal Dulcet Date: Fri, 22 Dec 2023 07:13:36 -0800 Subject: [PATCH 035/121] Fixed RET504 (unnecessary-assign) --- management/backup.py | 3 +-- management/mailconfig.py | 3 +-- management/status_checks.py | 3 +-- management/utils.py | 3 +-- management/web_update.py | 6 ++---- 5 files changed, 6 insertions(+), 12 deletions(-) diff --git a/management/backup.py b/management/backup.py index c8b33d2f..0f7674db 100755 --- a/management/backup.py +++ b/management/backup.py @@ -257,8 +257,7 @@ def get_duplicity_env_vars(env): return env def get_target_type(config): - protocol = config["target"].split(":")[0] - return protocol + return config["target"].split(":")[0] def perform_backup(full_backup): env = load_environment() diff --git a/management/mailconfig.py b/management/mailconfig.py index 3971f825..ed06c242 100755 --- a/management/mailconfig.py +++ b/management/mailconfig.py @@ -192,8 +192,7 @@ def get_mail_aliases(env): 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 = [ aliases[address] for address 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 diff --git a/management/status_checks.py b/management/status_checks.py index 7cdfffc6..3964e2df 100755 --- a/management/status_checks.py +++ b/management/status_checks.py @@ -915,8 +915,7 @@ def what_version_is_this(env): # Git may not be installed and Mail-in-a-Box may not have been cloned from github, # so this function may raise all sorts of exceptions. miab_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) - tag = shell("check_output", ["/usr/bin/git", "describe", "--always", "--abbrev=0"], env={"GIT_DIR": os.path.join(miab_dir, '.git')}).strip() - return tag + return shell("check_output", ["/usr/bin/git", "describe", "--always", "--abbrev=0"], env={"GIT_DIR": os.path.join(miab_dir, '.git')}).strip() def get_latest_miab_version(): # This pings https://mailinabox.email/setup.sh and extracts the tag named in diff --git a/management/utils.py b/management/utils.py index 0c475d5c..4ee100f1 100644 --- a/management/utils.py +++ b/management/utils.py @@ -81,7 +81,7 @@ def sort_domains(domain_names, env): )) # Now sort the domain names that fall within each zone. - domain_names = sorted(domain_names, + return sorted(domain_names, key = lambda d : ( # First by zone. zone_domains.index(zones[d]), @@ -96,7 +96,6 @@ def sort_domains(domain_names, env): list(reversed(d.split("."))), )) - return domain_names def sort_email_addresses(email_addresses, env): email_addresses = set(email_addresses) diff --git a/management/web_update.py b/management/web_update.py index 14ad1379..08b272e2 100644 --- a/management/web_update.py +++ b/management/web_update.py @@ -45,9 +45,8 @@ def get_web_domains(env, include_www_redirects=True, include_auto=True, exclude_ domains.add(env['PRIMARY_HOSTNAME']) # 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() @@ -225,9 +224,8 @@ def make_domain_config(domain, templates, ssl_certificates, env): nginx_conf = nginx_conf.replace("$ROOT", root) nginx_conf = nginx_conf.replace("$SSL_KEY", tls_cert["private-key"]) nginx_conf = nginx_conf.replace("$SSL_CERTIFICATE", tls_cert["certificate"]) - nginx_conf = nginx_conf.replace("$REDIRECT_DOMAIN", re.sub(r"^www\.", "", domain)) # for default www redirects to parent domain + return nginx_conf.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. From 2b426851f9f256bb2d4a12e64c6712e6c1a6178e Mon Sep 17 00:00:00 2001 From: Teal Dulcet Date: Fri, 22 Dec 2023 07:14:00 -0800 Subject: [PATCH 036/121] Fixed UP032 (f-string): Use f-string instead of `format` call --- management/backup.py | 10 +++++----- management/daemon.py | 6 +++--- management/dns_update.py | 6 +++--- management/email_administrator.py | 2 +- management/mail_log.py | 9 ++++----- management/status_checks.py | 11 +++++------ tests/test_mail.py | 10 +++------- 7 files changed, 24 insertions(+), 30 deletions(-) diff --git a/management/backup.py b/management/backup.py index 0f7674db..81bd0f82 100755 --- a/management/backup.py +++ b/management/backup.py @@ -482,16 +482,16 @@ def list_target_files(config): if 'Permission denied (publickey).' in listing: reason = "Invalid user or check you correctly copied the SSH key." elif 'No such file or directory' in listing: - reason = "Provided path {} is invalid.".format(target_path) + reason = f"Provided path {target_path} is invalid." elif 'Network is unreachable' in listing: - reason = "The IP address {} is unreachable.".format(target.hostname) + reason = f"The IP address {target.hostname} is unreachable." elif 'Could not resolve hostname' in listing: - reason = "The hostname {} cannot be resolved.".format(target.hostname) + 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." - raise ValueError("Connection to rsync host failed: {}".format(reason)) + raise ValueError(f"Connection to rsync host failed: {reason}") elif target.scheme == "s3": import boto3.s3 @@ -625,7 +625,7 @@ if __name__ == "__main__": elif sys.argv[-1] == "--list": # List the saved backup files. for fn, size in list_target_files(get_backup_config(load_environment())): - print("{}\t{}".format(fn, size)) + print(f"{fn}\t{size}") elif sys.argv[-1] == "--status": # Show backup status. diff --git a/management/daemon.py b/management/daemon.py index 1a35fba7..e53db4e6 100755 --- a/management/daemon.py +++ b/management/daemon.py @@ -80,7 +80,7 @@ def authorized_personnel_only(viewfunc): # 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, } @@ -164,7 +164,7 @@ def login(): "api_key": auth_service.create_session_key(email, env, type='login'), } - app.logger.info("New login session created for {}".format(email)) + app.logger.info(f"New login session created for {email}") # Return. return json_response(resp) @@ -173,7 +173,7 @@ def login(): def logout(): try: email, _ = auth_service.authenticate(request, env, logout=True) - app.logger.info("{} logged out".format(email)) + app.logger.info(f"{email} logged out") except ValueError as e: pass finally: diff --git a/management/dns_update.py b/management/dns_update.py index 74ea8a34..90523d33 100755 --- a/management/dns_update.py +++ b/management/dns_update.py @@ -534,7 +534,7 @@ $TTL 86400 ; default time to live zone += value + "\n" # Append a stable hash of DNSSEC signing keys in a comment. - zone += "\n; DNSSEC signing keys hash: {}\n".format(hash_dnssec_keys(domain, env)) + zone += f"\n; DNSSEC signing keys hash: {hash_dnssec_keys(domain, env)}\n" # DNSSEC requires re-signing a zone periodically. That requires # bumping the serial number even if no other records have changed. @@ -780,7 +780,7 @@ def write_opendkim_tables(domains, env): # So we must have a separate KeyTable entry for each domain. "SigningTable": "".join( - "*@{domain} {domain}\n".format(domain=domain) + f"*@{domain} {domain}\n" for domain in domains ), @@ -789,7 +789,7 @@ def write_opendkim_tables(domains, env): # signing domain must match the sender's From: domain. "KeyTable": "".join( - "{domain} {domain}:mail:{key_file}\n".format(domain=domain, key_file=opendkim_key_file) + f"{domain} {domain}:mail:{opendkim_key_file}\n" for domain in domains ), } diff --git a/management/email_administrator.py b/management/email_administrator.py index c87eda40..f2d7cac1 100755 --- a/management/email_administrator.py +++ b/management/email_administrator.py @@ -41,7 +41,7 @@ msg['From'] = "\"%s\" <%s>" % (env['PRIMARY_HOSTNAME'], admin_addr) msg['To'] = admin_addr msg['Subject'] = "[%s] %s" % (env['PRIMARY_HOSTNAME'], subject) -content_html = '
{}
'.format(html.escape(content)) +content_html = f'
{html.escape(content)}
' msg.attach(MIMEText(content, 'plain')) msg.attach(MIMEText(content_html, 'html')) diff --git a/management/mail_log.py b/management/mail_log.py index c54c2cb5..e52705b9 100755 --- a/management/mail_log.py +++ b/management/mail_log.py @@ -120,8 +120,7 @@ def scan_mail_log(env): except ImportError: pass - print("Scanning logs from {:%Y-%m-%d %H:%M:%S} to {:%Y-%m-%d %H:%M:%S}".format( - START_DATE, END_DATE) + 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 @@ -227,7 +226,7 @@ def scan_mail_log(env): ], sub_data=[ ("Protocol and Source", [[ - "{} {}: {} times".format(protocol_name, host, count) + 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()]) @@ -672,7 +671,7 @@ def print_user_table(users, data=None, sub_data=None, activity=None, latest=None col_str = str_temp.format(d[row][:31] + "ā€¦" if len(d[row]) > 32 else d[row]) col_left[col] = True elif isinstance(d[row], datetime.datetime): - col_str = "{:<20}".format(str(d[row])) + col_str = f"{str(d[row]):<20}" col_left[col] = True else: temp = "{:>%s}" % max(5, len(l) + 1, len(str(d[row])) + 1) @@ -844,7 +843,7 @@ if __name__ == "__main__": END_DATE = args.enddate if args.timespan == 'today': args.timespan = 'day' - print("Setting end date to {}".format(END_DATE)) + print(f"Setting end date to {END_DATE}") START_DATE = END_DATE - TIME_DELTAS[args.timespan] diff --git a/management/status_checks.py b/management/status_checks.py index 3964e2df..3ab2f890 100755 --- a/management/status_checks.py +++ b/management/status_checks.py @@ -268,8 +268,7 @@ def check_free_disk_space(rounded_values, env, output): except: backup_cache_count = 0 if backup_cache_count > 1: - output.print_warning("The backup cache directory {} has more than one backup target cache. Consider clearing this directory to save disk space." - .format(backup_cache_path)) + output.print_warning(f"The backup cache directory {backup_cache_path} has more than one backup target cache. Consider clearing this directory to save disk space.") def check_free_memory(rounded_values, env, output): # Check free memory. @@ -731,9 +730,9 @@ def check_mail_domain(domain, env, output): if policy[1].get("mx") == [env['PRIMARY_HOSTNAME']] and policy[1].get("mode") == "enforce": # policy[0] is the policyid output.print_ok("MTA-STS policy is present.") else: - output.print_error("MTA-STS policy is present but has unexpected settings. [{}]".format(policy[1])) + output.print_error(f"MTA-STS policy is present but has unexpected settings. [{policy[1]}]") else: - output.print_error("MTA-STS policy is missing: {}".format(valid)) + output.print_error(f"MTA-STS policy is missing: {valid}") else: output.print_error("""This domain's DNS MX record is incorrect. It is currently set to '%s' but should be '%s'. Mail will not @@ -752,9 +751,9 @@ def check_mail_domain(domain, env, output): if dbl is None: output.print_ok("Domain is not blacklisted by dbl.spamhaus.org.") elif dbl == "[timeout]": - output.print_warning("Connection to dbl.spamhaus.org timed out. We could not determine whether the domain {} is blacklisted. Please try again later.".format(domain)) + output.print_warning(f"Connection to dbl.spamhaus.org timed out. We could not determine whether the domain {domain} is blacklisted. Please try again later.") elif dbl == "[Not Set]": - output.print_warning("Could not connect to dbl.spamhaus.org. We could not determine whether the domain {} is blacklisted. Please try again later.".format(domain)) + output.print_warning(f"Could not connect to dbl.spamhaus.org. We could not determine whether the domain {domain} is blacklisted. Please try again later.") else: output.print_error("""This domain is listed in the Spamhaus Domain Block List (code %s), which may prevent recipients from receiving your mail. diff --git a/tests/test_mail.py b/tests/test_mail.py index 312f3332..64ed3679 100755 --- a/tests/test_mail.py +++ b/tests/test_mail.py @@ -30,15 +30,11 @@ print("IMAP login is OK.") # Attempt to send a mail to ourself. mailsubject = "Mail-in-a-Box Automated Test Message " + uuid.uuid4().hex emailto = emailaddress -msg = """From: {emailaddress} +msg = f"""From: {emailaddress} To: {emailto} -Subject: {subject} +Subject: {mailsubject} -This is a test message. It should be automatically deleted by the test script.""".format( - emailaddress=emailaddress, - emailto=emailto, - subject=mailsubject, - ) +This is a test message. It should be automatically deleted by the test script.""" # Connect to the server on the SMTP submission TLS port. server = smtplib.SMTP_SSL(host) From 13b38cc04db83719ec967b49b47d7abdca4b4bf6 Mon Sep 17 00:00:00 2001 From: Teal Dulcet Date: Fri, 22 Dec 2023 07:15:14 -0800 Subject: [PATCH 037/121] Fixed F841 (unused-variable) --- management/backup.py | 2 +- management/daemon.py | 2 +- management/dns_update.py | 10 +++++----- management/mail_log.py | 4 ++-- management/ssl_certificates.py | 4 ++-- management/status_checks.py | 4 ++-- tests/fail2ban.py | 4 ++-- 7 files changed, 15 insertions(+), 15 deletions(-) diff --git a/management/backup.py b/management/backup.py index 81bd0f82..3b6b75cf 100755 --- a/management/backup.py +++ b/management/backup.py @@ -534,7 +534,7 @@ def list_target_files(config): try: b2_api.authorize_account("production", b2_application_keyid, b2_application_key) bucket = b2_api.get_bucket_by_name(b2_bucket) - except NonExistentBucket as e: + 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()] diff --git a/management/daemon.py b/management/daemon.py index e53db4e6..026c2d6b 100755 --- a/management/daemon.py +++ b/management/daemon.py @@ -174,7 +174,7 @@ def logout(): try: email, _ = auth_service.authenticate(request, env, logout=True) app.logger.info(f"{email} logged out") - except ValueError as e: + except ValueError: pass finally: return json_response({ "status": "ok" }) diff --git a/management/dns_update.py b/management/dns_update.py index 90523d33..16a589c2 100755 --- a/management/dns_update.py +++ b/management/dns_update.py @@ -471,7 +471,7 @@ def build_sshfp_records(): for key in keys: if key.strip() == "" or key[0] == "#": continue try: - host, keytype, pubkey = key.split(" ") + _host, keytype, pubkey = key.split(" ") yield "%d %d ( %s )" % ( algorithm_number[keytype], 2, # specifies we are using SHA-256 on next line @@ -1049,19 +1049,19 @@ def set_secondary_dns(hostnames, env): if not item.startswith("xfr:"): # Resolve hostname. try: - response = resolver.resolve(item, "A") + resolver.resolve(item, "A") except (dns.resolver.NoNameservers, dns.resolver.NXDOMAIN, dns.resolver.NoAnswer, dns.resolver.Timeout): try: - response = resolver.resolve(item, "AAAA") + resolver.resolve(item, "AAAA") except (dns.resolver.NoNameservers, dns.resolver.NXDOMAIN, dns.resolver.NoAnswer, dns.resolver.Timeout): raise ValueError("Could not resolve the IP address of %s." % item) else: # Validate IP address. try: if "/" in item[4:]: - v = ipaddress.ip_network(item[4:]) # raises a ValueError if there's a problem + ipaddress.ip_network(item[4:]) # raises a ValueError if there's a problem else: - v = ipaddress.ip_address(item[4:]) # raises a ValueError if there's a problem + ipaddress.ip_address(item[4:]) # raises a ValueError if there's a problem except ValueError: raise ValueError("'%s' is not an IPv4 or IPv6 address or subnet." % item[4:]) diff --git a/management/mail_log.py b/management/mail_log.py index e52705b9..965fa74e 100755 --- a/management/mail_log.py +++ b/management/mail_log.py @@ -332,7 +332,7 @@ def scan_mail_log_line(line, collector): if not m: return True - date, system, service, log = m.groups() + date, _system, service, log = m.groups() collector["scan_count"] += 1 # print() @@ -554,7 +554,7 @@ def scan_postfix_submission_line(date, log, collector): m = re.match("([A-Z0-9]+): client=(\S+), sasl_method=(PLAIN|LOGIN), sasl_username=(\S+)(? Date: Fri, 22 Dec 2023 07:16:15 -0800 Subject: [PATCH 038/121] Fixed RSE102 (unnecessary-paren-on-raise-exception): Unnecessary parentheses on raised exception --- management/backup.py | 2 +- management/dns_update.py | 6 +++--- management/utils.py | 2 +- tests/fail2ban.py | 12 ++++++------ 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/management/backup.py b/management/backup.py index 3b6b75cf..93744d7d 100755 --- a/management/backup.py +++ b/management/backup.py @@ -581,7 +581,7 @@ def get_backup_config(env, for_save=False, for_ui=False): try: with open(os.path.join(backup_root, 'custom.yaml')) as f: custom_config = rtyaml.load(f) - if not isinstance(custom_config, dict): raise ValueError() # caught below + if not isinstance(custom_config, dict): raise ValueError # caught below config.update(custom_config) except: pass diff --git a/management/dns_update.py b/management/dns_update.py index 16a589c2..e911135f 100755 --- a/management/dns_update.py +++ b/management/dns_update.py @@ -817,7 +817,7 @@ def get_custom_dns_config(env, only_real_records=False): try: with open(os.path.join(env['STORAGE_ROOT'], 'dns/custom.yaml')) as f: custom_dns = rtyaml.load(f) - if not isinstance(custom_dns, dict): raise ValueError() # caught below + if not isinstance(custom_dns, dict): raise ValueError # caught below except: return [ ] @@ -835,7 +835,7 @@ def get_custom_dns_config(env, only_real_records=False): # No other type of data is allowed. else: - raise ValueError() + raise ValueError for rtype, value2 in values: if isinstance(value2, str): @@ -845,7 +845,7 @@ def get_custom_dns_config(env, only_real_records=False): yield (qname, rtype, value3) # No other type of data is allowed. else: - raise ValueError() + raise ValueError def filter_custom_records(domain, custom_dns_iter): for qname, rtype, value in custom_dns_iter: diff --git a/management/utils.py b/management/utils.py index 4ee100f1..f126f66b 100644 --- a/management/utils.py +++ b/management/utils.py @@ -38,7 +38,7 @@ def load_settings(env): try: with open(fn) as f: config = rtyaml.load(f) - if not isinstance(config, dict): raise ValueError() # caught below + if not isinstance(config, dict): raise ValueError # caught below return config except: return { } diff --git a/tests/fail2ban.py b/tests/fail2ban.py index bf002160..239eea23 100644 --- a/tests/fail2ban.py +++ b/tests/fail2ban.py @@ -33,7 +33,7 @@ def smtp_test(): server = smtplib.SMTP(hostname, 587) except ConnectionRefusedError: # looks like fail2ban worked - raise IsBlocked() + raise IsBlocked server.starttls() server.ehlo_or_helo_if_needed() @@ -58,7 +58,7 @@ def imap_test(): M = imaplib.IMAP4_SSL(hostname) except ConnectionRefusedError: # looks like fail2ban worked - raise IsBlocked() + raise IsBlocked try: M.login("fakeuser", "fakepassword") @@ -77,7 +77,7 @@ def pop_test(): M = poplib.POP3_SSL(hostname) except ConnectionRefusedError: # looks like fail2ban worked - raise IsBlocked() + raise IsBlocked try: M.user('fakeuser') try: @@ -102,7 +102,7 @@ def managesieve_test(): M = imaplib.IMAP4(hostname, 4190) except ConnectionRefusedError: # looks like fail2ban worked - raise IsBlocked() + raise IsBlocked try: M.login("fakeuser", "fakepassword") @@ -134,10 +134,10 @@ def http_test(url, expected_status, postdata=None, qsargs=None, auth=None): timeout=8, verify=False) # don't bother with HTTPS validation, it may not be configured yet except requests.exceptions.ConnectTimeout: - raise IsBlocked() + raise IsBlocked except requests.exceptions.ConnectionError as e: if "Connection refused" in str(e): - raise IsBlocked() + raise IsBlocked raise # some other unexpected condition # return response status code From fba92de0516d58d35217ebd70e02ef33d133ef90 Mon Sep 17 00:00:00 2001 From: Teal Dulcet Date: Fri, 22 Dec 2023 07:17:23 -0800 Subject: [PATCH 039/121] Fixed SIM108 (if-else-block-instead-of-if-exp) --- management/cli.py | 10 ++-------- management/daemon.py | 5 +---- management/dns_update.py | 10 ++-------- management/mailconfig.py | 5 +---- 4 files changed, 6 insertions(+), 24 deletions(-) diff --git a/management/cli.py b/management/cli.py index 115ed9f2..f3394cc0 100755 --- a/management/cli.py +++ b/management/cli.py @@ -93,10 +93,7 @@ elif sys.argv[1] == "user" and len(sys.argv) == 2: elif sys.argv[1] == "user" and sys.argv[2] in {"add", "password"}: if len(sys.argv) < 5: - if len(sys.argv) < 4: - email = input("email: ") - else: - email = sys.argv[3] + email = input('email: ') if len(sys.argv) < 4 else sys.argv[3] pw = read_password() else: email, pw = sys.argv[3:5] @@ -110,10 +107,7 @@ elif sys.argv[1] == "user" and sys.argv[2] == "remove" and len(sys.argv) == 4: print(mgmt("/mail/users/remove", { "email": sys.argv[3] })) elif sys.argv[1] == "user" and sys.argv[2] in {"make-admin", "remove-admin"} and len(sys.argv) == 4: - if sys.argv[2] == "make-admin": - action = "add" - else: - action = "remove" + 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": diff --git a/management/daemon.py b/management/daemon.py index 026c2d6b..0e5b672b 100755 --- a/management/daemon.py +++ b/management/daemon.py @@ -751,10 +751,7 @@ def log_failed_login(request): # 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. - if request.headers.getlist("X-Forwarded-For"): - ip = request.headers.getlist("X-Forwarded-For")[0] - else: - ip = request.remote_addr + 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. diff --git a/management/dns_update.py b/management/dns_update.py index e911135f..3b623302 100755 --- a/management/dns_update.py +++ b/management/dns_update.py @@ -861,10 +861,7 @@ def filter_custom_records(domain, custom_dns_iter): # our short form (None => domain, or a relative QNAME) if # domain is not None. if domain is not None: - if qname == domain: - qname = None - else: - qname = qname[0:len(qname)-len("." + domain)] + qname = None if qname == domain else qname[0:len(qname) - len("." + domain)] yield (qname, rtype, value) @@ -1094,10 +1091,7 @@ def build_recommended_dns(env): # expand qnames for i in range(len(records)): - if records[i][0] == None: - qname = domain - else: - qname = records[i][0] + "." + domain + qname = domain if records[i][0] == None else records[i][0] + "." + domain records[i] = { "qname": qname, diff --git a/management/mailconfig.py b/management/mailconfig.py index ed06c242..787acd64 100755 --- a/management/mailconfig.py +++ b/management/mailconfig.py @@ -476,10 +476,7 @@ def add_mail_alias(address, forwards_to, permitted_senders, env, update_if_exist forwards_to = ",".join(validated_forwards_to) - if len(validated_permitted_senders) == 0: - permitted_senders = None - else: - permitted_senders = ",".join(validated_permitted_senders) + permitted_senders = None if len(validated_permitted_senders) == 0 else ",".join(validated_permitted_senders) conn, c = open_database(env, with_connection=True) try: From eefc0514b24adc5d05e021bf3466fa4a3c504a6e Mon Sep 17 00:00:00 2001 From: Teal Dulcet Date: Fri, 22 Dec 2023 07:17:45 -0800 Subject: [PATCH 040/121] Fixed UP030 (format-literals): Use implicit references for positional format fields --- management/status_checks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/management/status_checks.py b/management/status_checks.py index 693be554..09cb4f0c 100755 --- a/management/status_checks.py +++ b/management/status_checks.py @@ -679,7 +679,7 @@ def check_dnssec(domain, env, output, dns_zonefiles, is_checking_primary=False): output.print_line("") output.print_line("The DS record is currently set to:") for rr in sorted(ds): - output.print_line("Key Tag: {0}, Algorithm: {1}, Digest Type: {2}, Digest: {3}".format(*rr)) + output.print_line("Key Tag: {}, Algorithm: {}, Digest Type: {}, Digest: {}".format(*rr)) def check_mail_domain(domain, env, output): # Check the MX record. From 64540fbb4459cb568fc844394cf28c2884084fb7 Mon Sep 17 00:00:00 2001 From: Teal Dulcet Date: Fri, 22 Dec 2023 07:18:38 -0800 Subject: [PATCH 041/121] Fixed UP034 (extraneous-parentheses): Avoid extraneous parentheses --- management/daemon.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/management/daemon.py b/management/daemon.py index 0e5b672b..ccde2e07 100755 --- a/management/daemon.py +++ b/management/daemon.py @@ -419,7 +419,7 @@ def ssl_get_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 "")) + "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. From 14a5613dc8cb9fe5a3d30d98b07ff628c42ff0e9 Mon Sep 17 00:00:00 2001 From: Teal Dulcet Date: Fri, 22 Dec 2023 07:19:06 -0800 Subject: [PATCH 042/121] Fixed UP031 (printf-string-formatting): Use format specifiers instead of percent format --- management/daemon.py | 5 +- management/dns_update.py | 6 +- management/email_administrator.py | 4 +- management/mail_log.py | 2 +- management/mailconfig.py | 2 +- management/ssl_certificates.py | 10 ++-- management/status_checks.py | 96 ++++++++++++++----------------- management/utils.py | 2 +- management/web_update.py | 4 +- setup/migrate.py | 2 +- tests/fail2ban.py | 2 +- tests/test_dns.py | 2 +- tests/test_smtp_server.py | 6 +- 13 files changed, 67 insertions(+), 76 deletions(-) diff --git a/management/daemon.py b/management/daemon.py index ccde2e07..1d4cc2cd 100755 --- a/management/daemon.py +++ b/management/daemon.py @@ -580,8 +580,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"]) @@ -755,7 +754,7 @@ def log_failed_login(request): # We need to add a timestamp to the log message, otherwise /dev/log will eat the "duplicate" # message. - app.logger.warning( "Mail-in-a-Box Management Daemon: Failed login attempt from ip %s - timestamp %s" % (ip, time.time())) + app.logger.warning( "Mail-in-a-Box Management Daemon: Failed login attempt from ip {} - timestamp {}".format(ip, time.time())) # APP diff --git a/management/dns_update.py b/management/dns_update.py index 3b623302..13d03989 100755 --- a/management/dns_update.py +++ b/management/dns_update.py @@ -620,9 +620,9 @@ def write_nsd_conf(zonefiles, additional_records, env): for domain, zonefile in zonefiles: nsdconf += """ zone: - name: %s - zonefile: %s -""" % (domain, zonefile) + name: {} + zonefile: {} +""".format(domain, zonefile) # If custom secondary nameservers have been set, allow zone transfers # and, if not a subnet, notifies to them. diff --git a/management/email_administrator.py b/management/email_administrator.py index f2d7cac1..bd4d62d5 100755 --- a/management/email_administrator.py +++ b/management/email_administrator.py @@ -37,9 +37,9 @@ msg = MIMEMultipart('alternative') # In Python 3.6: #msg = Message() -msg['From'] = "\"%s\" <%s>" % (env['PRIMARY_HOSTNAME'], admin_addr) +msg['From'] = "\"{}\" <{}>".format(env['PRIMARY_HOSTNAME'], admin_addr) msg['To'] = admin_addr -msg['Subject'] = "[%s] %s" % (env['PRIMARY_HOSTNAME'], subject) +msg['Subject'] = "[{}] {}".format(env['PRIMARY_HOSTNAME'], subject) content_html = f'
{html.escape(content)}
' diff --git a/management/mail_log.py b/management/mail_log.py index 965fa74e..153470b3 100755 --- a/management/mail_log.py +++ b/management/mail_log.py @@ -302,7 +302,7 @@ def scan_mail_log(env): for date, sender, message in user_data["blocked"]: if len(sender) > 64: sender = sender[:32] + "ā€¦" + sender[-32:] - user_rejects.append("%s - %s " % (date, sender)) + user_rejects.append("{} - {} ".format(date, sender)) user_rejects.append(" %s" % message) rejects.append(user_rejects) diff --git a/management/mailconfig.py b/management/mailconfig.py index 787acd64..8b98de6e 100755 --- a/management/mailconfig.py +++ b/management/mailconfig.py @@ -589,7 +589,7 @@ def kick(env, mail_result=None): and forwards_to == get_system_administrator(env) \ and not auto: remove_mail_alias(address, env, do_kick=False) - results.append("removed alias %s (was to %s; domain no longer used for email)\n" % (address, forwards_to)) + results.append("removed alias {} (was to {}; domain no longer used for email)\n".format(address, forwards_to)) # Update DNS and nginx in case any domains are added/removed. diff --git a/management/ssl_certificates.py b/management/ssl_certificates.py index 9c263599..9ae7a0fa 100755 --- a/management/ssl_certificates.py +++ b/management/ssl_certificates.py @@ -211,7 +211,7 @@ def get_certificates_to_provision(env, limit_domains=None, show_valid_certs=True if not value: continue # IPv6 is not configured response = query_dns(domain, rtype) if response != normalize_ip(value): - bad_dns.append("%s (%s)" % (response, rtype)) + bad_dns.append("{} ({})".format(response, rtype)) if bad_dns: domains_cant_provision[domain] = "The domain name does not resolve to this machine: " \ @@ -413,7 +413,7 @@ def create_csr(domain, ssl_key, country_code, env): "openssl", "req", "-new", "-key", ssl_key, "-sha256", - "-subj", "/C=%s/CN=%s" % (country_code, domain)]) + "-subj", "/C={}/CN={}".format(country_code, domain)]) def install_cert(domain, ssl_cert, ssl_chain, env, raw=False): # Write the combined cert+chain to a temporary path and validate that it is OK. @@ -450,7 +450,7 @@ def install_cert_copy_file(fn, env): from binascii import hexlify cert = load_pem(load_cert_chain(fn)[0]) _all_domains, cn = get_certificate_domains(cert) - path = "%s-%s-%s.pem" % ( + 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 @@ -537,7 +537,7 @@ def check_certificate(domain, ssl_certificate, ssl_private_key, warn_if_expiring with open(ssl_private_key, 'rb') as f: priv_key = load_pem(f.read()) except ValueError as e: - return ("The private key file %s is not a private key file: %s" % (ssl_private_key, str(e)), None) + return ("The private key file {} is not a private key file: {}".format(ssl_private_key, str(e)), None) if not isinstance(priv_key, RSAPrivateKey): return ("The private key file %s is not a private key file." % ssl_private_key, None) @@ -565,7 +565,7 @@ def check_certificate(domain, ssl_certificate, ssl_private_key, warn_if_expiring import datetime now = datetime.datetime.utcnow() if not(cert.not_valid_before <= now <= cert.not_valid_after): - return ("The certificate has expired or is not yet valid. It is valid from %s to %s." % (cert.not_valid_before, cert.not_valid_after), None) + return ("The certificate has expired or is not yet valid. It is valid from {} to {}.".format(cert.not_valid_before, cert.not_valid_after), None) # Next validate that the certificate is valid. This checks whether the certificate # is self-signed, that the chain of trust makes sense, that it is signed by a CA diff --git a/management/status_checks.py b/management/status_checks.py index 09cb4f0c..c4422ea8 100755 --- a/management/status_checks.py +++ b/management/status_checks.py @@ -194,7 +194,7 @@ def check_ufw(env, output): for service in get_services(): if service["public"] and not is_port_allowed(ufw, service["port"]): not_allowed_ports += 1 - output.print_error("Port %s (%s) should be allowed in the firewall, please re-run the setup." % (service["port"], service["name"])) + output.print_error("Port {} ({}) should be allowed in the firewall, please re-run the setup.".format(service["port"], service["name"])) if not_allowed_ports == 0: output.print_ok("Firewall is active.") @@ -236,7 +236,7 @@ def check_software_updates(env, output): else: output.print_error("There are %d software packages that can be updated." % len(pkgs)) for p in pkgs: - output.print_line("%s (%s)" % (p["package"], p["version"])) + output.print_line("{} ({})".format(p["package"], p["version"])) def check_system_aliases(env, output): # Check that the administrator alias exists since that's where all @@ -316,9 +316,8 @@ def run_network_checks(env, output): elif zen == "[Not Set]": output.print_warning("Could not connect to zen.spamhaus.org. We could not determine whether your server's IP address is blacklisted. Please try again later.") else: - output.print_error("""The IP address of this machine %s is listed in the Spamhaus Block List (code %s), - which may prevent recipients from receiving your email. See http://www.spamhaus.org/query/ip/%s.""" - % (env['PUBLIC_IP'], zen, env['PUBLIC_IP'])) + 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'])) def run_domain_checks(rounded_time, env, output, pool, domains_to_check=None): # Get the list of domains we handle mail for. @@ -436,30 +435,27 @@ def check_primary_hostname_dns(domain, env, output, dns_domains, dns_zonefiles): # the nameserver, are reporting the right info --- but if the glue is incorrect this # will probably fail. if ns_ips == env['PUBLIC_IP'] + '/' + env['PUBLIC_IP']: - output.print_ok("Nameserver glue records are correct at registrar. [ns1/ns2.%s ā†¦ %s]" % (env['PRIMARY_HOSTNAME'], env['PUBLIC_IP'])) + output.print_ok("Nameserver glue records are correct at registrar. [ns1/ns2.{} ā†¦ {}]".format(env['PRIMARY_HOSTNAME'], env['PUBLIC_IP'])) elif ip == env['PUBLIC_IP']: # The NS records are not what we expect, but the domain resolves correctly, so # the user may have set up external DNS. List this discrepancy as a warning. - output.print_warning("""Nameserver glue records (ns1.%s and ns2.%s) should be configured at your domain name - registrar as having the IP address of this box (%s). They currently report addresses of %s. If you have set up External DNS, this may be OK.""" - % (env['PRIMARY_HOSTNAME'], env['PRIMARY_HOSTNAME'], env['PUBLIC_IP'], ns_ips)) + output.print_warning("""Nameserver glue records (ns1.{} and ns2.{}) should be configured at your domain name + registrar as having the IP address of this box ({}). They currently report addresses of {}. If you have set up External DNS, this may be OK.""".format(env['PRIMARY_HOSTNAME'], env['PRIMARY_HOSTNAME'], env['PUBLIC_IP'], ns_ips)) else: - output.print_error("""Nameserver glue records are incorrect. The ns1.%s and ns2.%s nameservers must be configured at your domain name - registrar as having the IP address %s. They currently report addresses of %s. It may take several hours for - public DNS to update after a change.""" - % (env['PRIMARY_HOSTNAME'], env['PRIMARY_HOSTNAME'], env['PUBLIC_IP'], ns_ips)) + output.print_error("""Nameserver glue records are incorrect. The ns1.{} and ns2.{} nameservers must be configured at your domain name + registrar as having the IP address {}. They currently report addresses of {}. It may take several hours for + public DNS to update after a change.""".format(env['PRIMARY_HOSTNAME'], env['PRIMARY_HOSTNAME'], env['PUBLIC_IP'], ns_ips)) # Check that PRIMARY_HOSTNAME resolves to PUBLIC_IP[V6] in public DNS. ipv6 = query_dns(domain, "AAAA") if env.get("PUBLIC_IPV6") else None if ip == env['PUBLIC_IP'] and not (ipv6 and env['PUBLIC_IPV6'] and ipv6 != normalize_ip(env['PUBLIC_IPV6'])): - output.print_ok("Domain resolves to box's IP address. [%s ā†¦ %s]" % (env['PRIMARY_HOSTNAME'], my_ips)) + output.print_ok("Domain resolves to box's IP address. [{} ā†¦ {}]".format(env['PRIMARY_HOSTNAME'], my_ips)) else: - output.print_error("""This domain must resolve to your box's IP address (%s) in public DNS but it currently resolves - to %s. It may take several hours for public DNS to update after a change. This problem may result from other - issues listed above.""" - % (my_ips, ip + ((" / " + ipv6) if ipv6 is not None else ""))) + output.print_error("""This domain must resolve to your box's IP address ({}) in public DNS but it currently resolves + to {}. It may take several hours for public DNS to update after a change. This problem may result from other + issues listed above.""".format(my_ips, ip + ((" / " + ipv6) if ipv6 is not None else ""))) # Check reverse DNS matches the PRIMARY_HOSTNAME. Note that it might not be @@ -467,13 +463,13 @@ def check_primary_hostname_dns(domain, env, output, dns_domains, dns_zonefiles): existing_rdns_v4 = query_dns(dns.reversename.from_address(env['PUBLIC_IP']), "PTR") existing_rdns_v6 = query_dns(dns.reversename.from_address(env['PUBLIC_IPV6']), "PTR") if env.get("PUBLIC_IPV6") else None if existing_rdns_v4 == domain and existing_rdns_v6 in {None, domain}: - output.print_ok("Reverse DNS is set correctly at ISP. [%s ā†¦ %s]" % (my_ips, env['PRIMARY_HOSTNAME'])) + output.print_ok("Reverse DNS is set correctly at ISP. [{} ā†¦ {}]".format(my_ips, env['PRIMARY_HOSTNAME'])) elif existing_rdns_v4 == existing_rdns_v6 or existing_rdns_v6 is None: - output.print_error("""Your box's reverse DNS is currently %s, but it should be %s. Your ISP or cloud provider will have instructions - on setting up reverse DNS for your box.""" % (existing_rdns_v4, domain) ) + output.print_error("""Your box's reverse DNS is currently {}, but it should be {}. Your ISP or cloud provider will have instructions + on setting up reverse DNS for your box.""".format(existing_rdns_v4, domain) ) else: - output.print_error("""Your box's reverse DNS is currently %s (IPv4) and %s (IPv6), but it should be %s. Your ISP or cloud provider will have instructions - on setting up reverse DNS for your box.""" % (existing_rdns_v4, existing_rdns_v6, domain) ) + output.print_error("""Your box's reverse DNS is currently {} (IPv4) and {} (IPv6), but it should be {}. Your ISP or cloud provider will have instructions + on setting up reverse DNS for your box.""".format(existing_rdns_v4, existing_rdns_v6, domain) ) # Check the TLSA record. tlsa_qname = "_25._tcp." + domain @@ -487,9 +483,8 @@ def check_primary_hostname_dns(domain, env, output, dns_domains, dns_zonefiles): # since TLSA shouldn't be used without DNSSEC. output.print_warning("""The DANE TLSA record for incoming mail is not set. This is optional.""") else: - output.print_error("""The DANE TLSA record for incoming mail (%s) is not correct. It is '%s' but it should be '%s'. - It may take several hours for public DNS to update after a change.""" - % (tlsa_qname, tlsa25, tlsa25_expected)) + output.print_error("""The DANE TLSA record for incoming mail ({}) is not correct. It is '{}' but it should be '{}'. + It may take several hours for public DNS to update after a change.""".format(tlsa_qname, tlsa25, tlsa25_expected)) # Check that the hostmaster@ email address exists. check_alias_exists("Hostmaster contact address", "hostmaster@" + domain, env, output) @@ -498,7 +493,7 @@ def check_alias_exists(alias_name, alias, env, output): mail_aliases = dict([(address, receivers) for address, receivers, *_ in get_mail_aliases(env)]) if alias in mail_aliases: if mail_aliases[alias]: - output.print_ok("%s exists as a mail alias. [%s ā†¦ %s]" % (alias_name, alias, mail_aliases[alias])) + output.print_ok("{} exists as a mail alias. [{} ā†¦ {}]".format(alias_name, alias, mail_aliases[alias])) else: output.print_error("""You must set the destination of the mail alias for %s to direct email to you or another administrator.""" % alias) else: @@ -533,14 +528,12 @@ def check_dns_zone(domain, env, output, dns_zonefiles): output.print_ok("Nameservers are set correctly at registrar. [%s]" % correct_ns) elif ip == correct_ip: # The domain resolves correctly, so maybe the user is using External DNS. - output.print_warning("""The nameservers set on this domain at your domain name registrar should be %s. They are currently %s. - If you are using External DNS, this may be OK.""" - % (correct_ns, existing_ns) ) + output.print_warning("""The nameservers set on this domain at your domain name registrar should be {}. They are currently {}. + If you are using External DNS, this may be OK.""".format(correct_ns, existing_ns) ) probably_external_dns = True else: - output.print_error("""The nameservers set on this domain are incorrect. They are currently %s. Use your domain name registrar's - control panel to set the nameservers to %s.""" - % (existing_ns, correct_ns) ) + output.print_error("""The nameservers set on this domain are incorrect. They are currently {}. Use your domain name registrar's + control panel to set the nameservers to {}.""".format(existing_ns, correct_ns) ) # Check that each custom secondary nameserver resolves the IP address. @@ -561,7 +554,7 @@ def check_dns_zone(domain, env, output, dns_zonefiles): elif ip is None: output.print_error("Secondary nameserver %s is not configured to resolve this domain." % ns) else: - output.print_error("Secondary nameserver %s is not configured correctly. (It resolved this domain as %s. It should be %s.)" % (ns, ip, correct_ip)) + output.print_error("Secondary nameserver {} is not configured correctly. (It resolved this domain as {}. It should be {}.)".format(ns, ip, correct_ip)) 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 @@ -667,8 +660,8 @@ def check_dnssec(domain, env, output, dns_zonefiles, is_checking_primary=False): output.print_line("----------") output.print_line("Key Tag: " + ds_suggestion['keytag']) output.print_line("Key Flags: KSK / 257") - output.print_line("Algorithm: %s / %s" % (ds_suggestion['alg'], ds_suggestion['alg_name'])) - output.print_line("Digest Type: %s / %s" % (ds_suggestion['digalg'], ds_suggestion['digalg_name'])) + output.print_line("Algorithm: {} / {}".format(ds_suggestion['alg'], ds_suggestion['alg_name'])) + output.print_line("Digest Type: {} / {}".format(ds_suggestion['digalg'], ds_suggestion['digalg_name'])) output.print_line("Digest: " + ds_suggestion['digest']) output.print_line("Public Key: ") output.print_line(ds_suggestion['pubkey'], monospace=True) @@ -701,7 +694,7 @@ def check_mail_domain(domain, env, output): # the primary hostname's A record (the MX fallback) is... itself, # which is what we want the MX to be. if domain == env['PRIMARY_HOSTNAME']: - output.print_ok("Domain's email is directed to this domain. [%s has no MX record, which is ok]" % (domain,)) + output.print_ok("Domain's email is directed to this domain. [{} has no MX record, which is ok]".format(domain)) # And a missing MX record is okay on other domains if the A record # matches the A record of the PRIMARY_HOSTNAME. Actually this will @@ -710,16 +703,16 @@ def check_mail_domain(domain, env, output): domain_a = query_dns(domain, "A", nxdomain=None) primary_a = query_dns(env['PRIMARY_HOSTNAME'], "A", nxdomain=None) if domain_a != None and domain_a == primary_a: - output.print_ok("Domain's email is directed to this domain. [%s has no MX record but its A record is OK]" % (domain,)) + output.print_ok("Domain's email is directed to this domain. [{} has no MX record but its A record is OK]".format(domain)) else: - output.print_error("""This domain's DNS MX record is not set. It should be '%s'. Mail will not + output.print_error("""This domain's DNS MX record is not set. It should be '{}'. Mail will not be delivered to this box. It may take several hours for public DNS to update after a - change. This problem may result from other issues listed here.""" % (recommended_mx,)) + change. This problem may result from other issues listed here.""".format(recommended_mx)) elif mxhost == env['PRIMARY_HOSTNAME']: - good_news = "Domain's email is directed to this domain. [%s ā†¦ %s]" % (domain, mx) + good_news = "Domain's email is directed to this domain. [{} ā†¦ {}]".format(domain, mx) if mx != recommended_mx: - good_news += " This configuration is non-standard. The recommended configuration is '%s'." % (recommended_mx,) + good_news += " This configuration is non-standard. The recommended configuration is '{}'.".format(recommended_mx) output.print_ok(good_news) # Check MTA-STS policy. @@ -735,9 +728,9 @@ def check_mail_domain(domain, env, output): output.print_error(f"MTA-STS policy is missing: {valid}") else: - output.print_error("""This domain's DNS MX record is incorrect. It is currently set to '%s' but should be '%s'. Mail will not + output.print_error("""This domain's DNS MX record is incorrect. It is currently set to '{}' but should be '{}'. Mail will not be delivered to this box. It may take several hours for public DNS to update after a change. This problem may result from - other issues listed here.""" % (mx, recommended_mx)) + other issues listed here.""".format(mx, recommended_mx)) # Check that the postmaster@ email address exists. Not required if the domain has a # catch-all address or domain alias. @@ -755,9 +748,9 @@ def check_mail_domain(domain, env, output): elif dbl == "[Not Set]": output.print_warning(f"Could not connect to dbl.spamhaus.org. We could not determine whether the domain {domain} is blacklisted. Please try again later.") else: - output.print_error("""This domain is listed in the Spamhaus Domain Block List (code %s), + output.print_error("""This domain is listed in the Spamhaus Domain Block List (code {}), which may prevent recipients from receiving your mail. - See http://www.spamhaus.org/dbl/ and http://www.spamhaus.org/query/domain/%s.""" % (dbl, domain)) + See http://www.spamhaus.org/dbl/ and http://www.spamhaus.org/query/domain/{}.""".format(dbl, domain)) def check_web_domain(domain, rounded_time, ssl_certificates, env, output): # See if the domain's A record resolves to our PUBLIC_IP. This is already checked @@ -771,13 +764,13 @@ def check_web_domain(domain, rounded_time, ssl_certificates, env, output): if value == normalize_ip(expected): ok_values.append(value) else: - output.print_error("""This domain should resolve to your box's IP address (%s %s) if you would like the box to serve - webmail or a website on this domain. The domain currently resolves to %s in public DNS. It may take several hours for - public DNS to update after a change. This problem may result from other issues listed here.""" % (rtype, expected, value)) + output.print_error("""This domain should resolve to your box's IP address ({} {}) if you would like the box to serve + webmail or a website on this domain. The domain currently resolves to {} in public DNS. It may take several hours for + public DNS to update after a change. This problem may result from other issues listed here.""".format(rtype, expected, value)) return # If both A and AAAA are correct... - output.print_ok("Domain resolves to this box's IP address. [%s ā†¦ %s]" % (domain, '; '.join(ok_values))) + output.print_ok("Domain resolves to this box's IP address. [{} ā†¦ {}]".format(domain, '; '.join(ok_values))) # We need a TLS certificate for PRIMARY_HOSTNAME because that's where the @@ -945,8 +938,7 @@ def check_miab_version(env, output): elif latest_ver is None: output.print_error("Latest Mail-in-a-Box version could not be determined. You are running version %s." % this_ver) else: - output.print_error("A new version of Mail-in-a-Box is available. You are running version %s. The latest version is %s. For upgrade instructions, see https://mailinabox.email. " - % (this_ver, latest_ver)) + output.print_error("A new version of Mail-in-a-Box is available. You are running version {}. The latest version is {}. For upgrade instructions, see https://mailinabox.email. ".format(this_ver, latest_ver)) def run_and_output_changes(env, pool): import json diff --git a/management/utils.py b/management/utils.py index f126f66b..d1c09924 100644 --- a/management/utils.py +++ b/management/utils.py @@ -22,7 +22,7 @@ def load_env_vars_from_file(fn): def save_environment(env): with open("/etc/mailinabox.conf", "w") as f: for k, v in env.items(): - f.write("%s=%s\n" % (k, v)) + f.write("{}={}\n".format(k, v)) # THE SETTINGS FILE AT STORAGE_ROOT/settings.yaml. diff --git a/management/web_update.py b/management/web_update.py index 08b272e2..498f87c5 100644 --- a/management/web_update.py +++ b/management/web_update.py @@ -149,7 +149,7 @@ def make_domain_config(domain, templates, ssl_certificates, env): with open(filepath, 'rb') as f: sha1.update(f.read()) return sha1.hexdigest() - nginx_conf_extra += "\t# ssl files sha1: %s / %s\n" % (hashfile(tls_cert["private-key"]), hashfile(tls_cert["certificate"])) + nginx_conf_extra += "\t# ssl files sha1: {} / {}\n".format(hashfile(tls_cert["private-key"]), hashfile(tls_cert["certificate"])) # Add in any user customizations in YAML format. hsts = "yes" @@ -195,7 +195,7 @@ def make_domain_config(domain, templates, ssl_certificates, env): 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 += "\trewrite {} {} permanent;\n".format(path, url) # override the HSTS directive type hsts = yaml.get("hsts", hsts) diff --git a/setup/migrate.py b/setup/migrate.py index 9065cf40..78899f20 100755 --- a/setup/migrate.py +++ b/setup/migrate.py @@ -222,7 +222,7 @@ def run_migrations(): if migration_id is None: print() - print("%s file doesn't exists. Skipping migration..." % (migration_id_file,)) + print("{} file doesn't exists. Skipping migration...".format(migration_id_file)) return ourver = int(migration_id) diff --git a/tests/fail2ban.py b/tests/fail2ban.py index 239eea23..23f9c298 100644 --- a/tests/fail2ban.py +++ b/tests/fail2ban.py @@ -153,7 +153,7 @@ def restart_fail2ban_service(final=False): if not final: # Stop recidive jails during testing. command += " && sudo fail2ban-client stop recidive" - os.system("%s \"%s\"" % (ssh_command, command)) + os.system("{} \"{}\"".format(ssh_command, command)) def testfunc_runner(i, testfunc, *args): print(i+1, end=" ", flush=True) diff --git a/tests/test_dns.py b/tests/test_dns.py index 62cf5e78..11f6d2ed 100755 --- a/tests/test_dns.py +++ b/tests/test_dns.py @@ -98,7 +98,7 @@ else: # And if that's OK, also check reverse DNS (the PTR record). if not test_ptr("8.8.8.8", "Google Public DNS (Reverse DNS)"): print () - print ("The reverse DNS for %s is not correct. Consult your ISP for how to set the reverse DNS (also called the PTR record) for %s to %s." % (hostname, hostname, ipaddr)) + print ("The reverse DNS for {} is not correct. Consult your ISP for how to set the reverse DNS (also called the PTR record) for {} to {}.".format(hostname, hostname, ipaddr)) sys.exit(1) else: print ("And the reverse DNS for the domain is correct.") diff --git a/tests/test_smtp_server.py b/tests/test_smtp_server.py index 914c94b2..ca7e15ed 100755 --- a/tests/test_smtp_server.py +++ b/tests/test_smtp_server.py @@ -6,11 +6,11 @@ if len(sys.argv) < 3: sys.exit(1) host, toaddr, fromaddr = sys.argv[1:4] -msg = """From: %s -To: %s +msg = """From: {} +To: {} Subject: SMTP server test -This is a test message.""" % (fromaddr, toaddr) +This is a test message.""".format(fromaddr, toaddr) server = smtplib.SMTP(host, 25) server.set_debuglevel(1) From 3d72c32b1d2347e0bf4da884b5a5572fa6f6e567 Mon Sep 17 00:00:00 2001 From: Teal Dulcet Date: Fri, 22 Dec 2023 07:19:40 -0800 Subject: [PATCH 043/121] Fixed W605 (invalid-escape-sequence) --- management/dns_update.py | 2 +- management/mail_log.py | 6 +++--- management/ssl_certificates.py | 4 ++-- management/status_checks.py | 8 ++++---- tests/tls.py | 8 ++++---- tools/editconf.py | 6 +++--- 6 files changed, 17 insertions(+), 17 deletions(-) diff --git a/management/dns_update.py b/management/dns_update.py index 13d03989..ff3ec45d 100755 --- a/management/dns_update.py +++ b/management/dns_update.py @@ -16,7 +16,7 @@ from ssl_certificates import get_ssl_certificates, check_certificate # 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 # DNS but not in URLs), which are common in certain record types like for DKIM. -DOMAIN_RE = "^(?!\-)(?:[*][.])?(?:[a-zA-Z\d\-_]{0,62}[a-zA-Z\d_]\.){1,126}(?!\d+)[a-zA-Z\d_]{1,63}(\.?)$" +DOMAIN_RE = r"^(?!\-)(?:[*][.])?(?:[a-zA-Z\d\-_]{0,62}[a-zA-Z\d_]\.){1,126}(?!\d+)[a-zA-Z\d_]{1,63}(\.?)$" def get_dns_domains(env): # Add all domain names in use by email users and mail aliases, any diff --git a/management/mail_log.py b/management/mail_log.py index 153470b3..1386bebf 100755 --- a/management/mail_log.py +++ b/management/mail_log.py @@ -391,7 +391,7 @@ def scan_mail_log_line(line, collector): def scan_postgrey_line(date, log, collector): """ Scan a postgrey log line and extract interesting data """ - m = re.match("action=(greylist|pass), reason=(.*?), (?:delay=\d+, )?client_name=(.*), " + m = re.match(r"action=(greylist|pass), reason=(.*?), (?:delay=\d+, )?client_name=(.*), " "client_address=(.*), sender=(.*), recipient=(.*)", log) @@ -513,7 +513,7 @@ def scan_postfix_lmtp_line(date, log, collector): """ - m = re.match("([A-Z0-9]+): to=<(\S+)>, .* Saved", log) + m = re.match(r"([A-Z0-9]+): to=<(\S+)>, .* Saved", log) if m: _, user = m.groups() @@ -551,7 +551,7 @@ def scan_postfix_submission_line(date, log, collector): # Match both the 'plain' and 'login' sasl methods, since both authentication methods are # allowed by Dovecot. Exclude trailing comma after the username when additional fields # follow after. - m = re.match("([A-Z0-9]+): client=(\S+), sasl_method=(PLAIN|LOGIN), sasl_username=(\S+)(? self.width-1-len(first_line)): diff --git a/tests/tls.py b/tests/tls.py index e06ddcc9..ac51262d 100644 --- a/tests/tls.py +++ b/tests/tls.py @@ -94,8 +94,8 @@ def sslyze(opts, port, ok_ciphers): # Trim output to make better for storing in git. if "SCAN RESULTS FOR" not in out: # Failed. Just output the error. - out = re.sub("[\w\W]*CHECKING HOST\(S\) AVAILABILITY\n\s*-+\n", "", out) # chop off header that shows the host we queried - out = re.sub("[\w\W]*SCAN RESULTS FOR.*\n\s*-+\n", "", out) # chop off header that shows the host we queried + out = re.sub("[\\w\\W]*CHECKING HOST\\(S\\) AVAILABILITY\n\\s*-+\n", "", out) # chop off header that shows the host we queried + out = re.sub("[\\w\\W]*SCAN RESULTS FOR.*\n\\s*-+\n", "", out) # chop off header that shows the host we queried out = re.sub("SCAN COMPLETED IN .*", "", out) out = out.rstrip(" \n-") + "\n" @@ -105,8 +105,8 @@ def sslyze(opts, port, ok_ciphers): # Pull out the accepted ciphers list for each SSL/TLS protocol # version outputted. accepted_ciphers = set() - for ciphers in re.findall(" Accepted:([\w\W]*?)\n *\n", out): - accepted_ciphers |= set(re.findall("\n\s*(\S*)", ciphers)) + for ciphers in re.findall(" Accepted:([\\w\\W]*?)\n *\n", out): + accepted_ciphers |= set(re.findall("\n\\s*(\\S*)", ciphers)) # Compare to what Mozilla recommends, for a given modernness-level. print(" Should Not Offer: " + (", ".join(sorted(accepted_ciphers-set(ok_ciphers))) or "(none -- good)")) diff --git a/tools/editconf.py b/tools/editconf.py index 898e49b6..d9d05412 100755 --- a/tools/editconf.py +++ b/tools/editconf.py @@ -93,9 +93,9 @@ while len(input_lines) > 0: # Check if this line contain this setting from the command-line arguments. name, val = settings[i].split("=", 1) m = re.match( - "(\s*)" - + "(" + re.escape(comment_char) + "\s*)?" - + re.escape(name) + delimiter_re + "(.*?)\s*$", + r"(\s*)" + + "(" + re.escape(comment_char) + r"\s*)?" + + re.escape(name) + delimiter_re + r"(.*?)\s*$", line, re.S) if not m: continue indent, is_comment, existing_val = m.groups() From 67b9d0b2798c1d05cf4ded26d504fe6d664e73bb Mon Sep 17 00:00:00 2001 From: Teal Dulcet Date: Fri, 22 Dec 2023 07:19:47 -0800 Subject: [PATCH 044/121] Fixed PLW0108 (unnecessary-lambda): Lambda may be unnecessary; consider inlining inner function --- management/dns_update.py | 2 +- management/utils.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/management/dns_update.py b/management/dns_update.py index ff3ec45d..d28db626 100755 --- a/management/dns_update.py +++ b/management/dns_update.py @@ -38,7 +38,7 @@ def get_dns_zones(env): # Exclude domains that are subdomains of other domains we know. Proceed # by looking at shorter domains first. zone_domains = set() - for domain in sorted(domains, key=lambda d : len(d)): + for domain in sorted(domains, key=len): for d in zone_domains: if domain.endswith("." + d): # We found a parent domain already in the list. diff --git a/management/utils.py b/management/utils.py index d1c09924..0cfce602 100644 --- a/management/utils.py +++ b/management/utils.py @@ -59,7 +59,7 @@ def sort_domains(domain_names, env): # from shortest to longest since zones are always shorter than their # subdomains. zones = { } - for domain in sorted(domain_names, key=lambda d : len(d)): + for 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. From e8d1c037cbfb7213486f2765f55ce566705954eb Mon Sep 17 00:00:00 2001 From: Teal Dulcet Date: Fri, 22 Dec 2023 07:20:00 -0800 Subject: [PATCH 045/121] Fixed SIM102 (collapsible-if): Use a single `if` statement instead of nested `if` statements --- management/mail_log.py | 49 +++++++++++++++++----------------- management/ssl_certificates.py | 14 +++++----- management/status_checks.py | 7 +++-- 3 files changed, 33 insertions(+), 37 deletions(-) diff --git a/management/mail_log.py b/management/mail_log.py index 1386bebf..b8494fbe 100755 --- a/management/mail_log.py +++ b/management/mail_log.py @@ -434,36 +434,35 @@ def scan_postfix_smtpd_line(date, log, collector): return # only log mail to known recipients - if user_match(user): - if collector["known_addresses"] is None or user in collector["known_addresses"]: - data = collector["rejected"].get( - user, - { - "blocked": [], - "earliest": None, - "latest": None, - } - ) - # simplify this one + 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"Client host \[(.*?)\] blocked using zen.spamhaus.org; (.*)", message + r"Sender address \[.*@(.*)\] blocked using dbl.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) + message = "domain blocked: " + m.group(2) - if data["earliest"] is None: - data["earliest"] = date - data["latest"] = date - data["blocked"].append((date, sender, message)) + if data["earliest"] is None: + data["earliest"] = date + data["latest"] = date + data["blocked"].append((date, sender, message)) - collector["rejected"][user] = data + collector["rejected"][user] = data def scan_dovecot_login_line(date, log, collector, protocol_name): diff --git a/management/ssl_certificates.py b/management/ssl_certificates.py index 24797c68..d9c5ac51 100755 --- a/management/ssl_certificates.py +++ b/management/ssl_certificates.py @@ -82,9 +82,8 @@ def get_ssl_certificates(env): for domain in cert_domains: # The primary hostname can only use a certificate mapped # to the system private key. - if domain == env['PRIMARY_HOSTNAME']: - if cert["private_key"]["filename"] != os.path.join(env['STORAGE_ROOT'], 'ssl', 'ssl_private_key.pem'): - continue + 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) @@ -149,11 +148,10 @@ def get_domain_ssl_files(domain, ssl_certificates, env, allow_missing_cert=False "certificate_object": load_pem(load_cert_chain(ssl_certificate)[0]), } - if use_main_cert: - if domain == env['PRIMARY_HOSTNAME']: - # The primary domain must use the server certificate because - # it is hard-coded in some service configuration files. - return system_certificate + 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: diff --git a/management/status_checks.py b/management/status_checks.py index dce2718c..1f96c23b 100755 --- a/management/status_checks.py +++ b/management/status_checks.py @@ -420,10 +420,9 @@ def check_primary_hostname_dns(domain, env, output, dns_domains, dns_zonefiles): # If a DS record is set on the zone containing this domain, check DNSSEC now. has_dnssec = False for zone in dns_domains: - if zone == domain or domain.endswith("." + zone): - if query_dns(zone, "DS", nxdomain=None) is not None: - has_dnssec = True - check_dnssec(zone, env, output, dns_zonefiles, is_checking_primary=True) + if (zone == domain or domain.endswith("." + zone)) and query_dns(zone, "DS", nxdomain=None) is not None: + has_dnssec = True + check_dnssec(zone, env, output, dns_zonefiles, is_checking_primary=True) ip = query_dns(domain, "A") ns_ips = query_dns("ns1." + domain, "A") + '/' + query_dns("ns2." + domain, "A") From 541f31b1bae8087ce21537772c222f74969bf4af Mon Sep 17 00:00:00 2001 From: Teal Dulcet Date: Fri, 22 Dec 2023 07:20:10 -0800 Subject: [PATCH 046/121] Fixed FURB113 (repeated-append) --- management/dns_update.py | 3 +-- management/mail_log.py | 8 ++------ 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/management/dns_update.py b/management/dns_update.py index d28db626..515e2195 100755 --- a/management/dns_update.py +++ b/management/dns_update.py @@ -674,8 +674,7 @@ def hash_dnssec_keys(domain, env): keydata = [] for keytype, keyfn in sorted(find_dnssec_signing_keys(domain, env)): oldkeyfn = os.path.join(env['STORAGE_ROOT'], 'dns/dnssec', keyfn + ".private") - keydata.append(keytype) - keydata.append(keyfn) + keydata.extend((keytype, keyfn)) with open(oldkeyfn) as fr: keydata.append( fr.read() ) keydata = "".join(keydata).encode("utf8") diff --git a/management/mail_log.py b/management/mail_log.py index b8494fbe..bb862395 100755 --- a/management/mail_log.py +++ b/management/mail_log.py @@ -302,8 +302,7 @@ def scan_mail_log(env): for date, sender, message in user_data["blocked"]: if len(sender) > 64: sender = sender[:32] + "ā€¦" + sender[-32:] - user_rejects.append("{} - {} ".format(date, sender)) - user_rejects.append(" %s" % message) + user_rejects.extend(('{} - {} '.format(date, sender), ' %s' % message)) rejects.append(user_rejects) print_user_table( @@ -710,10 +709,7 @@ def print_user_table(users, data=None, sub_data=None, activity=None, latest=None if sub_data is not None: for l, d in sub_data: if d[row]: - lines.append("ā”¬") - lines.append("ā”‚ %s" % l) - lines.append("ā”œā”€%sā”€" % (len(l) * "ā”€")) - lines.append("ā”‚") + lines.extend(('ā”¬', 'ā”‚ %s' % l, 'ā”œā”€%sā”€' % (len(l) * 'ā”€'), 'ā”‚')) max_len = 0 for v in list(d[row]): lines.append("ā”‚ %s" % v) From 99d3929f9918885e49b5411f5049dda27d3f325b Mon Sep 17 00:00:00 2001 From: Teal Dulcet Date: Fri, 22 Dec 2023 07:20:28 -0800 Subject: [PATCH 047/121] Fixed E711 (none-comparison) --- management/dns_update.py | 8 ++++---- management/status_checks.py | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/management/dns_update.py b/management/dns_update.py index 515e2195..dc678a25 100755 --- a/management/dns_update.py +++ b/management/dns_update.py @@ -223,7 +223,7 @@ def build_zone(domain, domain_properties, additional_records, env, is_zone=True) subdomain_qname = subdomain[0:-len("." + domain)] subzone = build_zone(subdomain, domain_properties, additional_records, env, is_zone=False) for child_qname, child_rtype, child_value, child_explanation in subzone: - if child_qname == None: + if child_qname is None: child_qname = subdomain_qname else: child_qname += "." + subdomain_qname @@ -968,7 +968,7 @@ def set_custom_dns_record(qname, rtype, value, action, env): # Drop this record. made_change = True continue - if value == None and (_qname, _rtype) == (qname, rtype): + if value is None and (_qname, _rtype) == (qname, rtype): # Drop all qname-rtype records. made_change = True continue @@ -999,7 +999,7 @@ def get_secondary_dns(custom_dns, mode=None): if qname != '_secondary_nameserver': continue for hostname in value.split(" "): hostname = hostname.strip() - if mode == None: + if mode is None: # Just return the setting. values.append(hostname) continue @@ -1090,7 +1090,7 @@ def build_recommended_dns(env): # expand qnames for i in range(len(records)): - qname = domain if records[i][0] == None else records[i][0] + "." + domain + qname = domain if records[i][0] is None else records[i][0] + "." + domain records[i] = { "qname": qname, diff --git a/management/status_checks.py b/management/status_checks.py index 1f96c23b..cc9fd7d4 100755 --- a/management/status_checks.py +++ b/management/status_checks.py @@ -688,7 +688,7 @@ def check_mail_domain(domain, env, output): # of priority-host pairs. mxhost = mx.split('; ')[0].split(' ')[1] - if mxhost == None: + if mxhost is None: # A missing MX record is okay on the primary hostname because # the primary hostname's A record (the MX fallback) is... itself, # which is what we want the MX to be. @@ -701,7 +701,7 @@ def check_mail_domain(domain, env, output): else: domain_a = query_dns(domain, "A", nxdomain=None) primary_a = query_dns(env['PRIMARY_HOSTNAME'], "A", nxdomain=None) - if domain_a != None and domain_a == primary_a: + if domain_a is not None and domain_a == primary_a: output.print_ok("Domain's email is directed to this domain. [{} has no MX record but its A record is OK]".format(domain)) else: output.print_error("""This domain's DNS MX record is not set. It should be '{}'. Mail will not From 81a4da0181a62d81170e2ba414d08ce68f428f47 Mon Sep 17 00:00:00 2001 From: Teal Dulcet Date: Fri, 22 Dec 2023 07:20:48 -0800 Subject: [PATCH 048/121] Fixed SIM110 (reimplemented-builtin) --- management/dns_update.py | 5 +---- management/mailconfig.py | 5 +---- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/management/dns_update.py b/management/dns_update.py index dc678a25..eee99dc3 100755 --- a/management/dns_update.py +++ b/management/dns_update.py @@ -231,10 +231,7 @@ def build_zone(domain, domain_properties, additional_records, env, is_zone=True) has_rec_base = list(records) # clone current state def has_rec(qname, rtype, prefix=None): - for rec in has_rec_base: - if rec[0] == qname and rec[1] == rtype and (prefix is None or rec[2].startswith(prefix)): - return True - return False + return any(rec[0] == qname and rec[1] == rtype and (prefix is None or rec[2].startswith(prefix)) for rec in has_rec_base) # The user may set other records that don't conflict with our settings. # Don't put any TXT records above this line, or it'll prevent any custom TXT records. diff --git a/management/mailconfig.py b/management/mailconfig.py index 8b98de6e..8705d56a 100755 --- a/management/mailconfig.py +++ b/management/mailconfig.py @@ -86,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", "abuse"): - if email.startswith((localpart + "@", 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") From c953e5784d018dcca21a3d80595ac80e61b27fad Mon Sep 17 00:00:00 2001 From: Teal Dulcet Date: Fri, 22 Dec 2023 07:21:04 -0800 Subject: [PATCH 049/121] Fixed C401 (unnecessary-generator-set): Unnecessary generator (rewrite as a `set` comprehension) --- management/dns_update.py | 4 ++-- management/mail_log.py | 2 +- management/status_checks.py | 2 +- management/utils.py | 4 ++-- management/web_update.py | 8 ++++---- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/management/dns_update.py b/management/dns_update.py index eee99dc3..96cffbae 100755 --- a/management/dns_update.py +++ b/management/dns_update.py @@ -361,8 +361,8 @@ def build_zone(domain, domain_properties, additional_records, env, is_zone=True) # non-mail domain and also may include qnames from custom DNS records. # Do this once at the end of generating a zone. if is_zone: - qnames_with_a = set(qname for (qname, rtype, value, explanation) in records if rtype in {"A", "AAAA"}) - qnames_with_mx = set(qname for (qname, rtype, value, explanation) in records if rtype == "MX") + qnames_with_a = {qname for (qname, rtype, value, explanation) in records if rtype in {"A", "AAAA"}} + qnames_with_mx = {qname for (qname, rtype, value, explanation) in records if rtype == "MX"} for qname in qnames_with_a - qnames_with_mx: # Mark this domain as not sending mail with hard-fail SPF and DMARC records. d = (qname+"." if qname else "") + domain diff --git a/management/mail_log.py b/management/mail_log.py index bb862395..ae718e55 100755 --- a/management/mail_log.py +++ b/management/mail_log.py @@ -116,7 +116,7 @@ def scan_mail_log(env): try: import mailconfig collector["known_addresses"] = (set(mailconfig.get_mail_users(env)) | - set(alias[0] for alias in mailconfig.get_mail_aliases(env))) + {alias[0] for alias in mailconfig.get_mail_aliases(env)}) except ImportError: pass diff --git a/management/status_checks.py b/management/status_checks.py index cc9fd7d4..ac4f3021 100755 --- a/management/status_checks.py +++ b/management/status_checks.py @@ -624,7 +624,7 @@ def check_dnssec(domain, env, output, dns_zonefiles, is_checking_primary=False): # # But it may not be preferred. Only algorithm 13 is preferred. Warn if any of the # matched zones uses a different algorithm. - if set(r[1] for r in matched_ds) == { '13' } and set(r[2] for r in matched_ds) <= { '2', '4' }: # all are alg 13 and digest type 2 or 4 + if {r[1] for r in matched_ds} == { '13' } and {r[2] for r in matched_ds} <= { '2', '4' }: # all are alg 13 and digest type 2 or 4 output.print_ok("DNSSEC 'DS' record is set correctly at registrar.") return elif len([r for r in matched_ds if r[1] == '13' and r[2] in { '2', '4' }]) > 0: # some but not all are alg 13 diff --git a/management/utils.py b/management/utils.py index 0cfce602..982c8c8a 100644 --- a/management/utils.py +++ b/management/utils.py @@ -99,10 +99,10 @@ def sort_domains(domain_names, env): 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 diff --git a/management/web_update.py b/management/web_update.py index 498f87c5..6747a5b8 100644 --- a/management/web_update.py +++ b/management/web_update.py @@ -22,17 +22,17 @@ def get_web_domains(env, include_www_redirects=True, include_auto=True, exclude_ # Add 'www.' subdomains that we want to provide default redirects # to the main domain for. We'll add 'www.' to any DNS zones, i.e. # the topmost of each domain we serve. - domains |= set('www.' + zone for zone, zonefile in get_dns_zones(env)) + 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 |= set('autoconfig.' + maildomain for maildomain in get_mail_domains(env, users_only=True)) - domains |= set('autodiscover.' + maildomain for maildomain in get_mail_domains(env, users_only=True)) + 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 |= set('mta-sts.' + maildomain for maildomain in get_mail_domains(env)) + 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 From 57d05c1ab26ef350689ee6c2551629254de04d0c Mon Sep 17 00:00:00 2001 From: Teal Dulcet Date: Fri, 22 Dec 2023 07:21:10 -0800 Subject: [PATCH 050/121] Fixed B007 (unused-loop-control-variable) --- management/dns_update.py | 10 +++++----- management/mailconfig.py | 2 +- management/ssl_certificates.py | 2 +- management/status_checks.py | 2 +- management/utils.py | 2 +- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/management/dns_update.py b/management/dns_update.py index 96cffbae..753a4115 100755 --- a/management/dns_update.py +++ b/management/dns_update.py @@ -513,7 +513,7 @@ $TTL 86400 ; default time to live zone = zone.format(domain=domain, primary_domain=env["PRIMARY_HOSTNAME"]) # Add records. - for subdomain, querytype, value, explanation in records: + for subdomain, querytype, value, _explanation in records: if subdomain: zone += subdomain zone += "\tIN\t" + querytype + "\t" @@ -898,7 +898,7 @@ def write_custom_dns_config(config, env): def set_custom_dns_record(qname, rtype, value, action, env): # validate qname - for zone, fn in get_dns_zones(env): + for zone, _fn in get_dns_zones(env): # It must match a zone apex or be a subdomain of a zone # that we are otherwise hosting. if qname == zone or qname.endswith("."+zone): @@ -992,7 +992,7 @@ def get_secondary_dns(custom_dns, mode=None): resolver.lifetime = 10 values = [] - for qname, rtype, value in custom_dns: + for qname, _rtype, value in custom_dns: if qname != '_secondary_nameserver': continue for hostname in value.split(" "): hostname = hostname.strip() @@ -1078,7 +1078,7 @@ def get_custom_dns_records(custom_dns, qname, rtype): def build_recommended_dns(env): ret = [] - for (domain, zonefile, records) in build_zones(env): + for (domain, _zonefile, records) in build_zones(env): # remove records that we don't display records = [r for r in records if r[3] is not False] @@ -1106,7 +1106,7 @@ if __name__ == "__main__": if sys.argv[-1] == "--lint": write_custom_dns_config(get_custom_dns_config(env), env) else: - for zone, records in build_recommended_dns(env): + for _zone, records in build_recommended_dns(env): for record in records: print("; " + record['explanation']) print(record['qname'], record['rtype'], record['value'], sep="\t") diff --git a/management/mailconfig.py b/management/mailconfig.py index 8705d56a..b6c45c24 100755 --- a/management/mailconfig.py +++ b/management/mailconfig.py @@ -579,7 +579,7 @@ def kick(env, mail_result=None): # 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): + 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 \ diff --git a/management/ssl_certificates.py b/management/ssl_certificates.py index d9c5ac51..4e84fc07 100755 --- a/management/ssl_certificates.py +++ b/management/ssl_certificates.py @@ -262,7 +262,7 @@ def provision_certificates(env, limit_domains): # primary domain listed in each certificate. from dns_update import get_dns_zones certs = { } - for zone, zonefile in get_dns_zones(env): + 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. diff --git a/management/status_checks.py b/management/status_checks.py index ac4f3021..f73e5121 100755 --- a/management/status_checks.py +++ b/management/status_checks.py @@ -88,7 +88,7 @@ def run_services_checks(env, output, pool): all_running = True fatal = False ret = pool.starmap(check_service, ((i, service, env) for i, service in enumerate(get_services())), chunksize=1) - for i, running, fatal2, output2 in sorted(ret): + for _i, running, fatal2, output2 in sorted(ret): if output2 is None: continue # skip check (e.g. no port was set, e.g. no sshd) all_running = all_running and running fatal = fatal or fatal2 diff --git a/management/utils.py b/management/utils.py index 982c8c8a..a8fb5c2f 100644 --- a/management/utils.py +++ b/management/utils.py @@ -148,7 +148,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: From ca8f06d59093c0bb30221c8e56997145235a2f64 Mon Sep 17 00:00:00 2001 From: Teal Dulcet Date: Fri, 22 Dec 2023 07:21:28 -0800 Subject: [PATCH 051/121] Fixed PLR1711 (useless-return): Useless `return` statement at end of function --- management/dns_update.py | 1 - 1 file changed, 1 deletion(-) diff --git a/management/dns_update.py b/management/dns_update.py index 753a4115..43f801d3 100755 --- a/management/dns_update.py +++ b/management/dns_update.py @@ -1072,7 +1072,6 @@ def get_custom_dns_records(custom_dns, qname, rtype): for qname1, rtype1, value in custom_dns: if qname1 == qname and rtype1 == rtype: yield value - return None ######################################################################## From 4999ed7b1cb631ba64f9b0b31892dacdbf6f2f25 Mon Sep 17 00:00:00 2001 From: Teal Dulcet Date: Fri, 22 Dec 2023 07:21:37 -0800 Subject: [PATCH 052/121] Fixed Q003 (avoidable-escaped-quote): Change outer quotes to avoid escaping inner quotes --- management/email_administrator.py | 2 +- management/web_update.py | 4 ++-- tests/fail2ban.py | 4 ++-- tests/test_dns.py | 8 ++++---- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/management/email_administrator.py b/management/email_administrator.py index bd4d62d5..e5307e32 100755 --- a/management/email_administrator.py +++ b/management/email_administrator.py @@ -37,7 +37,7 @@ msg = MIMEMultipart('alternative') # In Python 3.6: #msg = Message() -msg['From'] = "\"{}\" <{}>".format(env['PRIMARY_HOSTNAME'], admin_addr) +msg['From'] = '"{}" <{}>'.format(env['PRIMARY_HOSTNAME'], admin_addr) msg['To'] = admin_addr msg['Subject'] = "[{}] {}".format(env['PRIMARY_HOSTNAME'], subject) diff --git a/management/web_update.py b/management/web_update.py index 6747a5b8..0a652b22 100644 --- a/management/web_update.py +++ b/management/web_update.py @@ -202,9 +202,9 @@ def make_domain_config(domain, templates, ssl_certificates, env): # Add the HSTS header. if hsts == "yes": - nginx_conf_extra += "\tadd_header Strict-Transport-Security \"max-age=15768000\" always;\n" + 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" + 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") diff --git a/tests/fail2ban.py b/tests/fail2ban.py index 23f9c298..04eabacc 100644 --- a/tests/fail2ban.py +++ b/tests/fail2ban.py @@ -11,7 +11,7 @@ import sys, os, time # parse command line if len(sys.argv) != 4: - print("Usage: tests/fail2ban.py \"ssh user@hostname\" hostname owncloud_user") + print('Usage: tests/fail2ban.py "ssh user@hostname" hostname owncloud_user') sys.exit(1) ssh_command, hostname, owncloud_user = sys.argv[1:4] @@ -153,7 +153,7 @@ def restart_fail2ban_service(final=False): if not final: # Stop recidive jails during testing. command += " && sudo fail2ban-client stop recidive" - os.system("{} \"{}\"".format(ssh_command, command)) + os.system('{} "{}"'.format(ssh_command, command)) def testfunc_runner(i, testfunc, *args): print(i+1, end=" ", flush=True) diff --git a/tests/test_dns.py b/tests/test_dns.py index 11f6d2ed..eac0ce1b 100755 --- a/tests/test_dns.py +++ b/tests/test_dns.py @@ -27,10 +27,10 @@ def test(server, description): ("ns2." + primary_hostname, "A", ipaddr), ("www." + hostname, "A", ipaddr), (hostname, "MX", "10 " + primary_hostname + "."), - (hostname, "TXT", "\"v=spf1 mx -all\""), - ("mail._domainkey." + hostname, "TXT", "\"v=DKIM1; k=rsa; s=email; \" \"p=__KEY__\""), + (hostname, "TXT", '"v=spf1 mx -all"'), + ("mail._domainkey." + hostname, "TXT", '"v=DKIM1; k=rsa; s=email; " "p=__KEY__"'), #("_adsp._domainkey." + hostname, "TXT", "\"dkim=all\""), - ("_dmarc." + hostname, "TXT", "\"v=DMARC1; p=quarantine;\""), + ("_dmarc." + hostname, "TXT", '"v=DMARC1; p=quarantine;"'), ] return test2(tests, server, description) @@ -59,7 +59,7 @@ def test2(tests, server, description): response = ["[no value]"] response = ";".join(str(r) for r in response) response = re.sub(r"(\"p=).*(\")", r"\1__KEY__\2", response) # normalize DKIM key - response = response.replace("\"\" ", "") # normalize TXT records (DNSSEC signing inserts empty text string components) + response = response.replace('"" ', "") # normalize TXT records (DNSSEC signing inserts empty text string components) # is it right? if response == expected_answer: From e0e6f1081b180e189ff7913868a65dcc84bcc33e Mon Sep 17 00:00:00 2001 From: Teal Dulcet Date: Fri, 22 Dec 2023 07:22:00 -0800 Subject: [PATCH 053/121] Fixed C414 (unnecessary-double-cast-or-process): Unnecessary `list` call within `sorted()` --- management/mail_log.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/management/mail_log.py b/management/mail_log.py index ae718e55..a8133218 100755 --- a/management/mail_log.py +++ b/management/mail_log.py @@ -320,7 +320,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(" ", *sorted(list(collector["other-services"])), sep='\nā”‚ ') + print(" ", *sorted(collector["other-services"]), sep='\nā”‚ ') def scan_mail_log_line(line, collector): From c585c1ecf66f739c2ba373ff0a7148423126623a Mon Sep 17 00:00:00 2001 From: Teal Dulcet Date: Fri, 22 Dec 2023 07:22:21 -0800 Subject: [PATCH 054/121] Fixed W291 (trailing-whitespace): Trailing whitespace --- management/mail_log.py | 2 +- tools/editconf.py | 2 +- tools/readable_bash.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/management/mail_log.py b/management/mail_log.py index a8133218..b654dcbc 100755 --- a/management/mail_log.py +++ b/management/mail_log.py @@ -547,7 +547,7 @@ def scan_postfix_submission_line(date, log, collector): """ # 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 + # 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+)(? Date: Fri, 22 Dec 2023 07:22:35 -0800 Subject: [PATCH 055/121] Fixed RET503 (implicit-return): Missing explicit `return` at the end of function able to return non-`None` value --- management/mail_log.py | 1 + management/mailconfig.py | 2 ++ 2 files changed, 3 insertions(+) diff --git a/management/mail_log.py b/management/mail_log.py index b654dcbc..0e1b6c5f 100755 --- a/management/mail_log.py +++ b/management/mail_log.py @@ -636,6 +636,7 @@ def print_time_table(labels, data, do_print=True): if do_print: print("\n".join(lines)) + return None else: return lines diff --git a/management/mailconfig.py b/management/mailconfig.py index b6c45c24..e5697929 100755 --- a/management/mailconfig.py +++ b/management/mailconfig.py @@ -491,6 +491,7 @@ def add_mail_alias(address, forwards_to, permitted_senders, env, update_if_exist if do_kick: # Update things in case any new domains are added. return kick(env, return_status) + return None def remove_mail_alias(address, env, do_kick=True): # convert Unicode domain to IDNA @@ -506,6 +507,7 @@ def remove_mail_alias(address, env, do_kick=True): 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) From 57dcd4bb517d79200874e863f9f14ad6e917e9cf Mon Sep 17 00:00:00 2001 From: Teal Dulcet Date: Fri, 22 Dec 2023 07:22:55 -0800 Subject: [PATCH 056/121] Fixed E713 (not-in-test): Test for membership should be `not in` --- management/mailconfig.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/management/mailconfig.py b/management/mailconfig.py index e5697929..4e83fc72 100755 --- a/management/mailconfig.py +++ b/management/mailconfig.py @@ -221,7 +221,7 @@ def get_mail_aliases_ex(env): domain = get_domain(address) # add to list - if not domain in domains: + if domain not in domains: domains[domain] = { "domain": domain, "aliases": [], From ec32e1d57865456cabf10fcd8b36df728283dddc Mon Sep 17 00:00:00 2001 From: Teal Dulcet Date: Fri, 22 Dec 2023 07:23:08 -0800 Subject: [PATCH 057/121] Fixed E703 (useless-semicolon): Statement ends with an unnecessary semicolon --- management/mailconfig.py | 2 +- setup/migrate.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/management/mailconfig.py b/management/mailconfig.py index 4e83fc72..9eb40849 100755 --- a/management/mailconfig.py +++ b/management/mailconfig.py @@ -511,7 +511,7 @@ def remove_mail_alias(address, env, do_kick=True): def add_auto_aliases(aliases, env): conn, c = open_database(env, with_connection=True) - c.execute("DELETE FROM auto_aliases"); + 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() diff --git a/setup/migrate.py b/setup/migrate.py index 78899f20..bf5685ff 100755 --- a/setup/migrate.py +++ b/setup/migrate.py @@ -213,7 +213,7 @@ def run_migrations(): migration_id = None if os.path.exists(migration_id_file): with open(migration_id_file) as f: - migration_id = f.read().strip(); + migration_id = f.read().strip() if migration_id is None: # Load the legacy location of the migration ID. We'll drop support From f62178929800e2a25ab3a3a61ef7ea3e411b1429 Mon Sep 17 00:00:00 2001 From: Teal Dulcet Date: Fri, 22 Dec 2023 07:23:15 -0800 Subject: [PATCH 058/121] Fixed SIM118 (in-dict-keys): Use `key in dict` instead of `key in dict.keys()` --- management/ssl_certificates.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/management/ssl_certificates.py b/management/ssl_certificates.py index 4e84fc07..3e4bbd2a 100755 --- a/management/ssl_certificates.py +++ b/management/ssl_certificates.py @@ -266,7 +266,7 @@ def provision_certificates(env, limit_domains): certs[zone] = [[]] for domain in sort_domains(domains, env): # Does the domain end with any domain we've seen so far. - for parent in certs.keys(): + 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 From d661d623dc3e4bfbc4d1e1059dc1bfa1cd707041 Mon Sep 17 00:00:00 2001 From: Teal Dulcet Date: Fri, 22 Dec 2023 07:23:24 -0800 Subject: [PATCH 059/121] Fixed RUF017 (quadratic-list-summation): Avoid quadratic list summation --- management/ssl_certificates.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/management/ssl_certificates.py b/management/ssl_certificates.py index 3e4bbd2a..3c1df5aa 100755 --- a/management/ssl_certificates.py +++ b/management/ssl_certificates.py @@ -4,6 +4,8 @@ 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 @@ -283,7 +285,7 @@ def provision_certificates(env, limit_domains): # Flatten to a list of lists of domains (from a mapping). Remove empty # lists (zones with no domains that need certs). - certs = sum(certs.values(), []) + certs = functools.reduce(operator.iadd, certs.values(), []) certs = [_ for _ in certs if len(_) > 0] # Prepare to provision. From fd4fcdaf53510ff9b79512a92f602d2128eca53f Mon Sep 17 00:00:00 2001 From: Teal Dulcet Date: Fri, 22 Dec 2023 07:23:57 -0800 Subject: [PATCH 060/121] Fixed E712 (true-false-comparison): Comparison to `False` should be `cond is False` or `if not cond:` --- management/status_checks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/management/status_checks.py b/management/status_checks.py index f73e5121..39149011 100755 --- a/management/status_checks.py +++ b/management/status_checks.py @@ -162,7 +162,7 @@ def check_service(i, service, env): output.print_error("%s is not running (port %d)." % (service['name'], service['port'])) # Flag if local DNS is not running. - if not running and service["port"] == 53 and service["public"] == False: + if not running and service["port"] == 53 and service["public"] is False: fatal = True return (i, running, fatal, output) From 54af4725f9ad8807374cc2ad2ee806f7ff49817b Mon Sep 17 00:00:00 2001 From: Teal Dulcet Date: Fri, 22 Dec 2023 07:24:11 -0800 Subject: [PATCH 061/121] Fixed C404 (unnecessary-list-comprehension-dict): Unnecessary `list` comprehension (rewrite as a `dict` comprehension) --- management/status_checks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/management/status_checks.py b/management/status_checks.py index 39149011..5424f0db 100755 --- a/management/status_checks.py +++ b/management/status_checks.py @@ -489,7 +489,7 @@ def check_primary_hostname_dns(domain, env, output, dns_domains, dns_zonefiles): check_alias_exists("Hostmaster contact address", "hostmaster@" + domain, env, output) def check_alias_exists(alias_name, alias, env, output): - mail_aliases = dict([(address, receivers) for address, receivers, *_ in get_mail_aliases(env)]) + mail_aliases = {address: receivers for address, receivers, *_ in get_mail_aliases(env)} if alias in mail_aliases: if mail_aliases[alias]: output.print_ok("{} exists as a mail alias. [{} ā†¦ {}]".format(alias_name, alias, mail_aliases[alias])) From 20a99c0ab81f10134bd4ce35b33423b18938ad5f Mon Sep 17 00:00:00 2001 From: Teal Dulcet Date: Fri, 22 Dec 2023 07:24:29 -0800 Subject: [PATCH 062/121] Fixed UP041 (timeout-error-alias): Replace aliased errors with `TimeoutError` --- management/status_checks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/management/status_checks.py b/management/status_checks.py index 5424f0db..3bc5faa5 100755 --- a/management/status_checks.py +++ b/management/status_checks.py @@ -916,7 +916,7 @@ def get_latest_miab_version(): try: return re.search(b'TAG=(.*)', urlopen("https://mailinabox.email/setup.sh?ping=1", timeout=5).read()).group(1).decode("utf8") - except (HTTPError, URLError, timeout): + except (TimeoutError, HTTPError, URLError): return None def check_miab_version(env, output): From 922c59ddafa5b604f4d45b9989248b64493e957c Mon Sep 17 00:00:00 2001 From: Teal Dulcet Date: Fri, 22 Dec 2023 07:24:58 -0800 Subject: [PATCH 063/121] Fixed SIM212 (if-expr-with-twisted-arms): Use `with_lines if with_lines else []` instead of `[] if not with_lines else with_lines` --- management/status_checks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/management/status_checks.py b/management/status_checks.py index 3bc5faa5..044a1187 100755 --- a/management/status_checks.py +++ b/management/status_checks.py @@ -1082,7 +1082,7 @@ class ConsoleOutput(FileOutput): class BufferedOutput: # Record all of the instance method calls so we can play them back later. def __init__(self, with_lines=None): - self.buf = [] if not with_lines else with_lines + self.buf = with_lines if with_lines else [] def __getattr__(self, attr): if attr not in {"add_heading", "print_ok", "print_error", "print_warning", "print_block", "print_line"}: raise AttributeError From d1d3d08d70ac126c7a0c0e619398318e8e96fc51 Mon Sep 17 00:00:00 2001 From: Teal Dulcet Date: Fri, 22 Dec 2023 07:25:13 -0800 Subject: [PATCH 064/121] Fixed B006 (mutable-argument-default): Do not use mutable data structures for argument defaults --- management/utils.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/management/utils.py b/management/utils.py index a8fb5c2f..178a03bc 100644 --- a/management/utils.py +++ b/management/utils.py @@ -108,11 +108,13 @@ def sort_email_addresses(email_addresses, env): ret.extend(sorted(email_addresses)) # whatever is left return ret -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, From 8b9d3ec094e679a54d04a6dc259322ef29c79eb7 Mon Sep 17 00:00:00 2001 From: Teal Dulcet Date: Fri, 22 Dec 2023 07:25:22 -0800 Subject: [PATCH 065/121] Fixed W292 (missing-newline-at-end-of-file): No newline at end of file --- management/wsgi.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/management/wsgi.py b/management/wsgi.py index 9cf21c4b..1d5a37b8 100644 --- a/management/wsgi.py +++ b/management/wsgi.py @@ -4,4 +4,4 @@ import utils app.logger.addHandler(utils.create_syslog_handler()) if __name__ == "__main__": - app.run(port=10222) \ No newline at end of file + app.run(port=10222) From b13cef9b1dce9770235de38a954d32c5a8eb0a95 Mon Sep 17 00:00:00 2001 From: Teal Dulcet Date: Fri, 22 Dec 2023 07:25:50 -0800 Subject: [PATCH 066/121] Fixed PIE790 (unnecessary-placeholder): Unnecessary `pass` statement --- tests/fail2ban.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/fail2ban.py b/tests/fail2ban.py index 04eabacc..b9447260 100644 --- a/tests/fail2ban.py +++ b/tests/fail2ban.py @@ -24,7 +24,6 @@ socket.setdefaulttimeout(10) class IsBlocked(Exception): """Tests raise this exception when it appears that a fail2ban jail is in effect, i.e. on a connection refused error.""" - pass def smtp_test(): import smtplib From 9b961b7ba028e4a30556914466707e55028a2912 Mon Sep 17 00:00:00 2001 From: Teal Dulcet Date: Fri, 22 Dec 2023 07:25:57 -0800 Subject: [PATCH 067/121] Fixed UP024 (os-error-alias): Replace aliased errors with `OSError` --- tests/fail2ban.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/fail2ban.py b/tests/fail2ban.py index b9447260..cf5bec88 100644 --- a/tests/fail2ban.py +++ b/tests/fail2ban.py @@ -142,7 +142,7 @@ def http_test(url, expected_status, postdata=None, qsargs=None, auth=None): # return response status code if r.status_code != expected_status: r.raise_for_status() # anything but 200 - raise IOError("Got unexpected status code %s." % r.status_code) + raise OSError("Got unexpected status code %s." % r.status_code) # define how to run a test From 6508d47da149ae55a068befe49193840bae097e5 Mon Sep 17 00:00:00 2001 From: Teal Dulcet Date: Fri, 22 Dec 2023 07:26:06 -0800 Subject: [PATCH 068/121] Fixed C405 (unnecessary-literal-set): Unnecessary `list` literal (rewrite as a `set` literal) --- tests/tls.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/tls.py b/tests/tls.py index ac51262d..10782a9b 100644 --- a/tests/tls.py +++ b/tests/tls.py @@ -142,7 +142,7 @@ for cipher in csv.DictReader(io.StringIO(urllib.request.urlopen("https://raw.git client_compatibility = json.loads(urllib.request.urlopen("https://raw.githubusercontent.com/mail-in-a-box/user-agent-tls-capabilities/master/clients.json").read().decode("utf8")) cipher_clients = { } for client in client_compatibility: - if len(set(client['protocols']) & set(["TLS 1.0", "TLS 1.1", "TLS 1.2"])) == 0: continue # does not support TLS + if len(set(client['protocols']) & {"TLS 1.0", "TLS 1.1", "TLS 1.2"}) == 0: continue # does not support TLS for cipher in client['ciphers']: cipher_clients.setdefault(cipher_names.get(cipher), set()).add("/".join(x for x in [client['client']['name'], client['client']['version'], client['client']['platform']] if x)) From 3111cf56de6100932f299c879fce6dad92650c4e Mon Sep 17 00:00:00 2001 From: Teal Dulcet Date: Fri, 22 Dec 2023 07:40:31 -0800 Subject: [PATCH 069/121] Fixed EM102 (f-string-in-exception): Exception must not use an f-string literal, assign to variable first --- management/backup.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/management/backup.py b/management/backup.py index 93744d7d..13ff4a44 100755 --- a/management/backup.py +++ b/management/backup.py @@ -491,7 +491,8 @@ def list_target_files(config): reason = "Unknown error." \ "Please check running 'management/backup.py --verify'" \ "from mailinabox sources to debug the issue." - raise ValueError(f"Connection to rsync host failed: {reason}") + msg = f"Connection to rsync host failed: {reason}" + raise ValueError(msg) elif target.scheme == "s3": import boto3.s3 From c719fce40a33256ec25d25b8b8a4fa3c44fff904 Mon Sep 17 00:00:00 2001 From: Teal Dulcet Date: Fri, 22 Dec 2023 07:41:21 -0800 Subject: [PATCH 070/121] Fixed UP032 (f-string): Use f-string instead of `format` call --- management/daemon.py | 2 +- management/dns_update.py | 8 +++--- management/mail_log.py | 2 +- management/mailconfig.py | 2 +- management/ssl_certificates.py | 8 +++--- management/status_checks.py | 52 +++++++++++++++++----------------- management/utils.py | 2 +- management/web_update.py | 2 +- setup/migrate.py | 2 +- tests/fail2ban.py | 2 +- tests/test_dns.py | 2 +- tests/test_smtp_server.py | 6 ++-- 12 files changed, 45 insertions(+), 45 deletions(-) diff --git a/management/daemon.py b/management/daemon.py index 1d4cc2cd..0c7531e6 100755 --- a/management/daemon.py +++ b/management/daemon.py @@ -754,7 +754,7 @@ def log_failed_login(request): # We need to add a timestamp to the log message, otherwise /dev/log will eat the "duplicate" # message. - app.logger.warning( "Mail-in-a-Box Management Daemon: Failed login attempt from ip {} - timestamp {}".format(ip, time.time())) + app.logger.warning( f"Mail-in-a-Box Management Daemon: Failed login attempt from ip {ip} - timestamp {time.time()}") # APP diff --git a/management/dns_update.py b/management/dns_update.py index 43f801d3..a4caf3b0 100755 --- a/management/dns_update.py +++ b/management/dns_update.py @@ -615,11 +615,11 @@ def write_nsd_conf(zonefiles, additional_records, env): # Append the zones. for domain, zonefile in zonefiles: - nsdconf += """ + nsdconf += f""" zone: - name: {} - zonefile: {} -""".format(domain, zonefile) + name: {domain} + zonefile: {zonefile} +""" # If custom secondary nameservers have been set, allow zone transfers # and, if not a subnet, notifies to them. diff --git a/management/mail_log.py b/management/mail_log.py index 0e1b6c5f..0e440422 100755 --- a/management/mail_log.py +++ b/management/mail_log.py @@ -302,7 +302,7 @@ def scan_mail_log(env): for date, sender, message in user_data["blocked"]: if len(sender) > 64: sender = sender[:32] + "ā€¦" + sender[-32:] - user_rejects.extend(('{} - {} '.format(date, sender), ' %s' % message)) + user_rejects.extend((f'{date} - {sender} ', ' %s' % message)) rejects.append(user_rejects) print_user_table( diff --git a/management/mailconfig.py b/management/mailconfig.py index 9eb40849..39ca1b6e 100755 --- a/management/mailconfig.py +++ b/management/mailconfig.py @@ -588,7 +588,7 @@ def kick(env, mail_result=None): and forwards_to == get_system_administrator(env) \ and not auto: remove_mail_alias(address, env, do_kick=False) - results.append("removed alias {} (was to {}; domain no longer used for email)\n".format(address, forwards_to)) + 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. diff --git a/management/ssl_certificates.py b/management/ssl_certificates.py index 3c1df5aa..9d866e6b 100755 --- a/management/ssl_certificates.py +++ b/management/ssl_certificates.py @@ -211,7 +211,7 @@ def get_certificates_to_provision(env, limit_domains=None, show_valid_certs=True if not value: continue # IPv6 is not configured response = query_dns(domain, rtype) if response != normalize_ip(value): - bad_dns.append("{} ({})".format(response, rtype)) + bad_dns.append(f"{response} ({rtype})") if bad_dns: domains_cant_provision[domain] = "The domain name does not resolve to this machine: " \ @@ -413,7 +413,7 @@ def create_csr(domain, ssl_key, country_code, env): "openssl", "req", "-new", "-key", ssl_key, "-sha256", - "-subj", "/C={}/CN={}".format(country_code, domain)]) + "-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. @@ -537,7 +537,7 @@ def check_certificate(domain, ssl_certificate, ssl_private_key, warn_if_expiring with open(ssl_private_key, 'rb') as f: priv_key = load_pem(f.read()) except ValueError as e: - return ("The private key file {} is not a private key file: {}".format(ssl_private_key, str(e)), None) + return (f"The private key file {ssl_private_key} is not a private key file: {str(e)}", None) if not isinstance(priv_key, RSAPrivateKey): return ("The private key file %s is not a private key file." % ssl_private_key, None) @@ -565,7 +565,7 @@ def check_certificate(domain, ssl_certificate, ssl_private_key, warn_if_expiring import datetime now = datetime.datetime.utcnow() if not(cert.not_valid_before <= now <= cert.not_valid_after): - return ("The certificate has expired or is not yet valid. It is valid from {} to {}.".format(cert.not_valid_before, cert.not_valid_after), None) + 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 diff --git a/management/status_checks.py b/management/status_checks.py index 044a1187..a753cb59 100755 --- a/management/status_checks.py +++ b/management/status_checks.py @@ -464,11 +464,11 @@ def check_primary_hostname_dns(domain, env, output, dns_domains, dns_zonefiles): if existing_rdns_v4 == domain and existing_rdns_v6 in {None, domain}: output.print_ok("Reverse DNS is set correctly at ISP. [{} ā†¦ {}]".format(my_ips, env['PRIMARY_HOSTNAME'])) elif existing_rdns_v4 == existing_rdns_v6 or existing_rdns_v6 is None: - output.print_error("""Your box's reverse DNS is currently {}, but it should be {}. Your ISP or cloud provider will have instructions - on setting up reverse DNS for your box.""".format(existing_rdns_v4, domain) ) + output.print_error(f"""Your box's reverse DNS is currently {existing_rdns_v4}, but it should be {domain}. Your ISP or cloud provider will have instructions + on setting up reverse DNS for your box.""" ) else: - output.print_error("""Your box's reverse DNS is currently {} (IPv4) and {} (IPv6), but it should be {}. Your ISP or cloud provider will have instructions - on setting up reverse DNS for your box.""".format(existing_rdns_v4, existing_rdns_v6, domain) ) + output.print_error(f"""Your box's reverse DNS is currently {existing_rdns_v4} (IPv4) and {existing_rdns_v6} (IPv6), but it should be {domain}. Your ISP or cloud provider will have instructions + on setting up reverse DNS for your box.""" ) # Check the TLSA record. tlsa_qname = "_25._tcp." + domain @@ -482,8 +482,8 @@ def check_primary_hostname_dns(domain, env, output, dns_domains, dns_zonefiles): # since TLSA shouldn't be used without DNSSEC. output.print_warning("""The DANE TLSA record for incoming mail is not set. This is optional.""") else: - output.print_error("""The DANE TLSA record for incoming mail ({}) is not correct. It is '{}' but it should be '{}'. - It may take several hours for public DNS to update after a change.""".format(tlsa_qname, tlsa25, tlsa25_expected)) + output.print_error(f"""The DANE TLSA record for incoming mail ({tlsa_qname}) is not correct. It is '{tlsa25}' but it should be '{tlsa25_expected}'. + It may take several hours for public DNS to update after a change.""") # Check that the hostmaster@ email address exists. check_alias_exists("Hostmaster contact address", "hostmaster@" + domain, env, output) @@ -492,7 +492,7 @@ def check_alias_exists(alias_name, alias, env, output): mail_aliases = {address: receivers for address, receivers, *_ in get_mail_aliases(env)} if alias in mail_aliases: if mail_aliases[alias]: - output.print_ok("{} exists as a mail alias. [{} ā†¦ {}]".format(alias_name, alias, mail_aliases[alias])) + output.print_ok(f"{alias_name} exists as a mail alias. [{alias} ā†¦ {mail_aliases[alias]}]") else: output.print_error("""You must set the destination of the mail alias for %s to direct email to you or another administrator.""" % alias) else: @@ -527,12 +527,12 @@ def check_dns_zone(domain, env, output, dns_zonefiles): output.print_ok("Nameservers are set correctly at registrar. [%s]" % correct_ns) elif ip == correct_ip: # The domain resolves correctly, so maybe the user is using External DNS. - output.print_warning("""The nameservers set on this domain at your domain name registrar should be {}. They are currently {}. - If you are using External DNS, this may be OK.""".format(correct_ns, existing_ns) ) + output.print_warning(f"""The nameservers set on this domain at your domain name registrar should be {correct_ns}. They are currently {existing_ns}. + If you are using External DNS, this may be OK.""" ) probably_external_dns = True else: - output.print_error("""The nameservers set on this domain are incorrect. They are currently {}. Use your domain name registrar's - control panel to set the nameservers to {}.""".format(existing_ns, correct_ns) ) + output.print_error(f"""The nameservers set on this domain are incorrect. They are currently {existing_ns}. Use your domain name registrar's + control panel to set the nameservers to {correct_ns}.""" ) # Check that each custom secondary nameserver resolves the IP address. @@ -553,7 +553,7 @@ def check_dns_zone(domain, env, output, dns_zonefiles): elif ip is None: output.print_error("Secondary nameserver %s is not configured to resolve this domain." % ns) else: - output.print_error("Secondary nameserver {} is not configured correctly. (It resolved this domain as {}. It should be {}.)".format(ns, ip, correct_ip)) + output.print_error(f"Secondary nameserver {ns} is not configured correctly. (It resolved this domain as {ip}. It should be {correct_ip}.)") 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 @@ -693,7 +693,7 @@ def check_mail_domain(domain, env, output): # the primary hostname's A record (the MX fallback) is... itself, # which is what we want the MX to be. if domain == env['PRIMARY_HOSTNAME']: - output.print_ok("Domain's email is directed to this domain. [{} has no MX record, which is ok]".format(domain)) + output.print_ok(f"Domain's email is directed to this domain. [{domain} has no MX record, which is ok]") # And a missing MX record is okay on other domains if the A record # matches the A record of the PRIMARY_HOSTNAME. Actually this will @@ -702,16 +702,16 @@ def check_mail_domain(domain, env, output): domain_a = query_dns(domain, "A", nxdomain=None) primary_a = query_dns(env['PRIMARY_HOSTNAME'], "A", nxdomain=None) if domain_a is not None and domain_a == primary_a: - output.print_ok("Domain's email is directed to this domain. [{} has no MX record but its A record is OK]".format(domain)) + output.print_ok(f"Domain's email is directed to this domain. [{domain} has no MX record but its A record is OK]") else: - output.print_error("""This domain's DNS MX record is not set. It should be '{}'. Mail will not + output.print_error(f"""This domain's DNS MX record is not set. It should be '{recommended_mx}'. Mail will not be delivered to this box. It may take several hours for public DNS to update after a - change. This problem may result from other issues listed here.""".format(recommended_mx)) + change. This problem may result from other issues listed here.""") elif mxhost == env['PRIMARY_HOSTNAME']: - good_news = "Domain's email is directed to this domain. [{} ā†¦ {}]".format(domain, mx) + good_news = f"Domain's email is directed to this domain. [{domain} ā†¦ {mx}]" if mx != recommended_mx: - good_news += " This configuration is non-standard. The recommended configuration is '{}'.".format(recommended_mx) + good_news += f" This configuration is non-standard. The recommended configuration is '{recommended_mx}'." output.print_ok(good_news) # Check MTA-STS policy. @@ -727,9 +727,9 @@ def check_mail_domain(domain, env, output): output.print_error(f"MTA-STS policy is missing: {valid}") else: - output.print_error("""This domain's DNS MX record is incorrect. It is currently set to '{}' but should be '{}'. Mail will not + output.print_error(f"""This domain's DNS MX record is incorrect. It is currently set to '{mx}' but should be '{recommended_mx}'. Mail will not be delivered to this box. It may take several hours for public DNS to update after a change. This problem may result from - other issues listed here.""".format(mx, recommended_mx)) + other issues listed here.""") # Check that the postmaster@ email address exists. Not required if the domain has a # catch-all address or domain alias. @@ -747,9 +747,9 @@ def check_mail_domain(domain, env, output): elif dbl == "[Not Set]": output.print_warning(f"Could not connect to dbl.spamhaus.org. We could not determine whether the domain {domain} is blacklisted. Please try again later.") else: - output.print_error("""This domain is listed in the Spamhaus Domain Block List (code {}), + output.print_error(f"""This domain is listed in the Spamhaus Domain Block List (code {dbl}), which may prevent recipients from receiving your mail. - See http://www.spamhaus.org/dbl/ and http://www.spamhaus.org/query/domain/{}.""".format(dbl, domain)) + See http://www.spamhaus.org/dbl/ and http://www.spamhaus.org/query/domain/{domain}.""") def check_web_domain(domain, rounded_time, ssl_certificates, env, output): # See if the domain's A record resolves to our PUBLIC_IP. This is already checked @@ -763,9 +763,9 @@ def check_web_domain(domain, rounded_time, ssl_certificates, env, output): if value == normalize_ip(expected): ok_values.append(value) else: - output.print_error("""This domain should resolve to your box's IP address ({} {}) if you would like the box to serve - webmail or a website on this domain. The domain currently resolves to {} in public DNS. It may take several hours for - public DNS to update after a change. This problem may result from other issues listed here.""".format(rtype, expected, value)) + output.print_error(f"""This domain should resolve to your box's IP address ({rtype} {expected}) if you would like the box to serve + webmail or a website on this domain. The domain currently resolves to {value} in public DNS. It may take several hours for + public DNS to update after a change. This problem may result from other issues listed here.""") return # If both A and AAAA are correct... @@ -937,7 +937,7 @@ def check_miab_version(env, output): elif latest_ver is None: output.print_error("Latest Mail-in-a-Box version could not be determined. You are running version %s." % this_ver) else: - output.print_error("A new version of Mail-in-a-Box is available. You are running version {}. The latest version is {}. For upgrade instructions, see https://mailinabox.email. ".format(this_ver, latest_ver)) + output.print_error(f"A new version of Mail-in-a-Box is available. You are running version {this_ver}. The latest version is {latest_ver}. For upgrade instructions, see https://mailinabox.email. ") def run_and_output_changes(env, pool): import json diff --git a/management/utils.py b/management/utils.py index 178a03bc..54b5bb86 100644 --- a/management/utils.py +++ b/management/utils.py @@ -22,7 +22,7 @@ def load_env_vars_from_file(fn): def save_environment(env): with open("/etc/mailinabox.conf", "w") as f: for k, v in env.items(): - f.write("{}={}\n".format(k, v)) + f.write(f"{k}={v}\n") # THE SETTINGS FILE AT STORAGE_ROOT/settings.yaml. diff --git a/management/web_update.py b/management/web_update.py index 0a652b22..0aeae2ec 100644 --- a/management/web_update.py +++ b/management/web_update.py @@ -195,7 +195,7 @@ def make_domain_config(domain, templates, ssl_certificates, env): 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 {} {} permanent;\n".format(path, url) + nginx_conf_extra += f"\trewrite {path} {url} permanent;\n" # override the HSTS directive type hsts = yaml.get("hsts", hsts) diff --git a/setup/migrate.py b/setup/migrate.py index bf5685ff..4b7591ba 100755 --- a/setup/migrate.py +++ b/setup/migrate.py @@ -222,7 +222,7 @@ def run_migrations(): if migration_id is None: print() - print("{} file doesn't exists. Skipping migration...".format(migration_id_file)) + print(f"{migration_id_file} file doesn't exists. Skipping migration...") return ourver = int(migration_id) diff --git a/tests/fail2ban.py b/tests/fail2ban.py index cf5bec88..2f2ac352 100644 --- a/tests/fail2ban.py +++ b/tests/fail2ban.py @@ -152,7 +152,7 @@ def restart_fail2ban_service(final=False): if not final: # Stop recidive jails during testing. command += " && sudo fail2ban-client stop recidive" - os.system('{} "{}"'.format(ssh_command, command)) + os.system(f'{ssh_command} "{command}"') def testfunc_runner(i, testfunc, *args): print(i+1, end=" ", flush=True) diff --git a/tests/test_dns.py b/tests/test_dns.py index eac0ce1b..53fdb545 100755 --- a/tests/test_dns.py +++ b/tests/test_dns.py @@ -98,7 +98,7 @@ else: # And if that's OK, also check reverse DNS (the PTR record). if not test_ptr("8.8.8.8", "Google Public DNS (Reverse DNS)"): print () - print ("The reverse DNS for {} is not correct. Consult your ISP for how to set the reverse DNS (also called the PTR record) for {} to {}.".format(hostname, hostname, ipaddr)) + print (f"The reverse DNS for {hostname} is not correct. Consult your ISP for how to set the reverse DNS (also called the PTR record) for {hostname} to {ipaddr}.") sys.exit(1) else: print ("And the reverse DNS for the domain is correct.") diff --git a/tests/test_smtp_server.py b/tests/test_smtp_server.py index ca7e15ed..246f4ecc 100755 --- a/tests/test_smtp_server.py +++ b/tests/test_smtp_server.py @@ -6,11 +6,11 @@ if len(sys.argv) < 3: sys.exit(1) host, toaddr, fromaddr = sys.argv[1:4] -msg = """From: {} -To: {} +msg = f"""From: {fromaddr} +To: {toaddr} Subject: SMTP server test -This is a test message.""".format(fromaddr, toaddr) +This is a test message.""" server = smtplib.SMTP(host, 25) server.set_debuglevel(1) From 15bddcbc397da9849f88b8040b0903657da00ac4 Mon Sep 17 00:00:00 2001 From: Teal Dulcet Date: Fri, 22 Dec 2023 07:42:13 -0800 Subject: [PATCH 071/121] Fixed RUF010 (explicit-f-string-type-conversion): Use explicit conversion flag --- management/mail_log.py | 2 +- management/ssl_certificates.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/management/mail_log.py b/management/mail_log.py index 0e440422..52dd62d3 100755 --- a/management/mail_log.py +++ b/management/mail_log.py @@ -670,7 +670,7 @@ def print_user_table(users, data=None, sub_data=None, activity=None, latest=None 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"{str(d[row]):<20}" + col_str = f"{d[row]!s:<20}" col_left[col] = True else: temp = "{:>%s}" % max(5, len(l) + 1, len(str(d[row])) + 1) diff --git a/management/ssl_certificates.py b/management/ssl_certificates.py index 9d866e6b..ab0cea4d 100755 --- a/management/ssl_certificates.py +++ b/management/ssl_certificates.py @@ -537,7 +537,7 @@ def check_certificate(domain, ssl_certificate, ssl_private_key, warn_if_expiring 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: {str(e)}", None) + 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) From a02b59d4e4af8f15405557a4597c28ab061d1159 Mon Sep 17 00:00:00 2001 From: Teal Dulcet Date: Fri, 22 Dec 2023 07:42:38 -0800 Subject: [PATCH 072/121] Fixed F401 (unused-import): `socket.timeout` imported but unused --- management/status_checks.py | 1 - 1 file changed, 1 deletion(-) diff --git a/management/status_checks.py b/management/status_checks.py index a753cb59..9f0a1474 100755 --- a/management/status_checks.py +++ b/management/status_checks.py @@ -912,7 +912,6 @@ def get_latest_miab_version(): # This pings https://mailinabox.email/setup.sh and extracts the tag named in # the script to determine the current product version. from urllib.request import urlopen, HTTPError, URLError - from socket import timeout try: return re.search(b'TAG=(.*)', urlopen("https://mailinabox.email/setup.sh?ping=1", timeout=5).read()).group(1).decode("utf8") From 0e9193651d3f9f23e08f9dd7823b4b9642ac120d Mon Sep 17 00:00:00 2001 From: Teal Dulcet Date: Sat, 23 Dec 2023 05:07:25 -0800 Subject: [PATCH 073/121] Fixed PLW1514 (unspecified-encoding): `open` in text mode without explicit `encoding` argument --- management/auth.py | 2 +- management/backup.py | 8 +++--- management/cli.py | 2 +- management/daemon.py | 2 +- management/dns_update.py | 32 ++++++++++----------- management/mail_log.py | 2 +- management/status_checks.py | 10 +++---- management/utils.py | 8 +++--- management/web_update.py | 10 +++---- setup/migrate.py | 4 +-- tools/editconf.py | 4 +-- tools/parse-nginx-log-bootstrap-accesses.py | 4 +-- 12 files changed, 44 insertions(+), 44 deletions(-) diff --git a/management/auth.py b/management/auth.py index 36112572..873047d5 100644 --- a/management/auth.py +++ b/management/auth.py @@ -22,7 +22,7 @@ class AuthService: def init_system_api_key(self): """Write an API key to a local file so local processes can use the API""" - with open(self.key_path) as file: + with open(self.key_path, encoding='utf-8') as file: self.key = file.read() def authenticate(self, request, env, login_only=False, logout=False): diff --git a/management/backup.py b/management/backup.py index 13ff4a44..183ff607 100755 --- a/management/backup.py +++ b/management/backup.py @@ -185,7 +185,7 @@ def get_passphrase(env): # only needs to be 43 base64-characters to match AES256's key # length of 32 bytes. backup_root = os.path.join(env["STORAGE_ROOT"], 'backup') - with open(os.path.join(backup_root, 'secret_key.txt')) as f: + 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!") @@ -580,7 +580,7 @@ def get_backup_config(env, for_save=False, for_ui=False): # Merge in anything written to custom.yaml. try: - with open(os.path.join(backup_root, 'custom.yaml')) as f: + 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) @@ -606,14 +606,14 @@ def get_backup_config(env, for_save=False, for_ui=False): 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) as f: + 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") as f: + with open(os.path.join(backup_root, 'custom.yaml'), "w", encoding="utf-8") as f: f.write(rtyaml.dump(newconfig)) if __name__ == "__main__": diff --git a/management/cli.py b/management/cli.py index f3394cc0..ea060d3f 100755 --- a/management/cli.py +++ b/management/cli.py @@ -47,7 +47,7 @@ def read_password(): return first def setup_key_auth(mgmt_uri): - with open('/var/lib/mailinabox/api.key') as f: + with open('/var/lib/mailinabox/api.key', encoding='utf-8') as f: key = f.read().strip() auth_handler = urllib.request.HTTPBasicAuthHandler() diff --git a/management/daemon.py b/management/daemon.py index 0c7531e6..074e9fa7 100755 --- a/management/daemon.py +++ b/management/daemon.py @@ -36,7 +36,7 @@ except OSError: # 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")) as f: +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] diff --git a/management/dns_update.py b/management/dns_update.py index a4caf3b0..c06ef352 100755 --- a/management/dns_update.py +++ b/management/dns_update.py @@ -295,7 +295,7 @@ def build_zone(domain, domain_properties, additional_records, env, is_zone=True) # Append the DKIM TXT record to the zone as generated by OpenDKIM. # Skip if the user has set a DKIM record already. opendkim_record_file = os.path.join(env['STORAGE_ROOT'], 'mail/dkim/mail.txt') - with open(opendkim_record_file) as orf: + with open(opendkim_record_file, encoding="utf-8") as orf: m = re.match(r'(\S+)\s+IN\s+TXT\s+\( ((?:"[^"]+"\s+)+)\)', orf.read(), re.S) val = "".join(re.findall(r'"([^"]+)"', m.group(2))) if not has_rec(m.group(1), "TXT", prefix="v=DKIM1; "): @@ -452,7 +452,7 @@ def build_sshfp_records(): # specify that port to sshkeyscan. port = 22 - with open('/etc/ssh/sshd_config') as f: + with open('/etc/ssh/sshd_config', encoding="utf-8") as f: for line in f: s = line.rstrip().split() if len(s) == 2 and s[0] == 'Port': @@ -547,7 +547,7 @@ $TTL 86400 ; default time to live # We've signed the domain. Check if we are close to the expiration # time of the signature. If so, we'll force a bump of the serial # number so we can re-sign it. - with open(zonefile + ".signed") as f: + with open(zonefile + ".signed", encoding="utf-8") as f: signed_zone = f.read() expiration_times = re.findall(r"\sRRSIG\s+SOA\s+\d+\s+\d+\s\d+\s+(\d{14})", signed_zone) if len(expiration_times) == 0: @@ -566,7 +566,7 @@ $TTL 86400 ; default time to live if os.path.exists(zonefile): # If the zone already exists, is different, and has a later serial number, # increment the number. - with open(zonefile) as f: + with open(zonefile, encoding="utf-8") as f: existing_zone = f.read() m = re.search(r"(\d+)\s*;\s*serial number", existing_zone) if m: @@ -590,7 +590,7 @@ $TTL 86400 ; default time to live zone = zone.replace("__SERIAL__", serial) # Write the zone file. - with open(zonefile, "w") as f: + with open(zonefile, "w", encoding="utf-8") as f: f.write(zone) return True # file is updated @@ -603,7 +603,7 @@ def get_dns_zonefile(zone, env): raise ValueError("%s is not a domain name that corresponds to a zone." % zone) nsd_zonefile = "/etc/nsd/zones/" + fn - with open(nsd_zonefile) as f: + with open(nsd_zonefile, encoding="utf-8") as f: return f.read() ######################################################################## @@ -631,13 +631,13 @@ zone: # Check if the file is changing. If it isn't changing, # return False to flag that no change was made. if os.path.exists(nsd_conf_file): - with open(nsd_conf_file) as f: + with open(nsd_conf_file, encoding="utf-8") as f: if f.read() == nsdconf: return False # Write out new contents and return True to signal that # configuration changed. - with open(nsd_conf_file, "w") as f: + with open(nsd_conf_file, "w", encoding="utf-8") as f: f.write(nsdconf) return True @@ -672,7 +672,7 @@ def hash_dnssec_keys(domain, env): for keytype, keyfn in sorted(find_dnssec_signing_keys(domain, env)): oldkeyfn = os.path.join(env['STORAGE_ROOT'], 'dns/dnssec', keyfn + ".private") keydata.extend((keytype, keyfn)) - with open(oldkeyfn) as fr: + with open(oldkeyfn, encoding="utf-8") as fr: keydata.append( fr.read() ) keydata = "".join(keydata).encode("utf8") return hashlib.sha1(keydata).hexdigest() @@ -700,12 +700,12 @@ def sign_zone(domain, zonefile, env): # Use os.umask and open().write() to securely create a copy that only # we (root) can read. oldkeyfn = os.path.join(env['STORAGE_ROOT'], 'dns/dnssec', keyfn + ext) - with open(oldkeyfn) as fr: + with open(oldkeyfn, encoding="utf-8") as fr: keydata = fr.read() keydata = keydata.replace("_domain_", domain) prev_umask = os.umask(0o77) # ensure written file is not world-readable try: - with open(newkeyfn + ext, "w") as fw: + with open(newkeyfn + ext, "w", encoding="utf-8") as fw: fw.write(keydata) finally: os.umask(prev_umask) # other files we write should be world-readable @@ -739,7 +739,7 @@ def sign_zone(domain, zonefile, env): # be used, so we'll pre-generate all for each key. One DS record per line. Only one # needs to actually be deployed at the registrar. We'll select the preferred one # in the status checks. - with open("/etc/nsd/zones/" + zonefile + ".ds", "w") as f: + with open("/etc/nsd/zones/" + zonefile + ".ds", "w", encoding="utf-8") as f: for key in ksk_keys: for digest_type in ('1', '2', '4'): rr_ds = shell('check_output', ["/usr/bin/ldns-key2ds", @@ -794,12 +794,12 @@ def write_opendkim_tables(domains, env): for filename, content in config.items(): # Don't write the file if it doesn't need an update. if os.path.exists("/etc/opendkim/" + filename): - with open("/etc/opendkim/" + filename) as f: + with open("/etc/opendkim/" + filename, encoding="utf-8") as f: if f.read() == content: continue # The contents needs to change. - with open("/etc/opendkim/" + filename, "w") as f: + with open("/etc/opendkim/" + filename, "w", encoding="utf-8") as f: f.write(content) did_update = True @@ -811,7 +811,7 @@ def write_opendkim_tables(domains, env): def get_custom_dns_config(env, only_real_records=False): try: - with open(os.path.join(env['STORAGE_ROOT'], 'dns/custom.yaml')) as f: + with open(os.path.join(env['STORAGE_ROOT'], 'dns/custom.yaml'), encoding="utf-8") as f: custom_dns = rtyaml.load(f) if not isinstance(custom_dns, dict): raise ValueError # caught below except: @@ -893,7 +893,7 @@ def write_custom_dns_config(config, env): # Write. config_yaml = rtyaml.dump(dns) - with open(os.path.join(env['STORAGE_ROOT'], 'dns/custom.yaml'), "w") as f: + with open(os.path.join(env['STORAGE_ROOT'], 'dns/custom.yaml'), "w", encoding="utf-8") as f: f.write(config_yaml) def set_custom_dns_record(qname, rtype, value, action, env): diff --git a/management/mail_log.py b/management/mail_log.py index 52dd62d3..0c91365b 100755 --- a/management/mail_log.py +++ b/management/mail_log.py @@ -585,7 +585,7 @@ def scan_postfix_submission_line(date, log, collector): def readline(filename): """ A generator that returns the lines of a file """ - with open(filename, errors='replace') as file: + with open(filename, errors='replace', encoding='utf-8') as file: while True: line = file.readline() if not line: diff --git a/management/status_checks.py b/management/status_checks.py index 9f0a1474..fa27396d 100755 --- a/management/status_checks.py +++ b/management/status_checks.py @@ -212,7 +212,7 @@ def check_ssh_password(env, output): # the configuration file. if not os.path.exists("/etc/ssh/sshd_config"): return - with open("/etc/ssh/sshd_config") as f: + with open("/etc/ssh/sshd_config", encoding="utf-8") as f: sshd = f.read() if re.search("\nPasswordAuthentication\\s+yes", sshd) \ or not re.search("\nPasswordAuthentication\\s+no", sshd): @@ -582,7 +582,7 @@ def check_dnssec(domain, env, output, dns_zonefiles, is_checking_primary=False): expected_ds_records = { } ds_file = '/etc/nsd/zones/' + dns_zonefiles[domain] + '.ds' if not os.path.exists(ds_file): return # Domain is in our database but DNS has not yet been updated. - with open(ds_file) as f: + with open(ds_file, encoding="utf-8") as f: for rr_ds in f: rr_ds = rr_ds.rstrip() ds_keytag, ds_alg, ds_digalg, ds_digest = rr_ds.split("\t")[4].split(" ") @@ -591,7 +591,7 @@ def check_dnssec(domain, env, output, dns_zonefiles, is_checking_primary=False): # record that we suggest using is for the KSK (and that's how the DS records were generated). # We'll also give the nice name for the key algorithm. dnssec_keys = load_env_vars_from_file(os.path.join(env['STORAGE_ROOT'], 'dns/dnssec/%s.conf' % alg_name_map[ds_alg])) - with open(os.path.join(env['STORAGE_ROOT'], 'dns/dnssec/' + dnssec_keys['KSK'] + '.key')) as f: + with open(os.path.join(env['STORAGE_ROOT'], 'dns/dnssec/' + dnssec_keys['KSK'] + '.key'), encoding="utf-8") as f: dnsssec_pubkey = f.read().split("\t")[3].split(" ")[3] expected_ds_records[ (ds_keytag, ds_alg, ds_digalg, ds_digest) ] = { @@ -951,7 +951,7 @@ def run_and_output_changes(env, pool): # Load previously saved status checks. cache_fn = "/var/cache/mailinabox/status_checks.json" if os.path.exists(cache_fn): - with open(cache_fn) as f: + with open(cache_fn, encoding="utf-8") as f: try: prev = json.load(f) except json.JSONDecodeError: @@ -1007,7 +1007,7 @@ def run_and_output_changes(env, pool): # Store the current status checks output for next time. os.makedirs(os.path.dirname(cache_fn), exist_ok=True) - with open(cache_fn, "w") as f: + with open(cache_fn, "w", encoding="utf-8") as f: json.dump(cur.buf, f, indent=True) def normalize_ip(ip): diff --git a/management/utils.py b/management/utils.py index 54b5bb86..929544d1 100644 --- a/management/utils.py +++ b/management/utils.py @@ -14,13 +14,13 @@ def load_env_vars_from_file(fn): # Load settings from a KEY=VALUE file. import collections env = collections.OrderedDict() - with open(fn) as f: + 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(f"{k}={v}\n") @@ -29,14 +29,14 @@ def save_environment(env): def write_settings(config, env): import rtyaml fn = os.path.join(env['STORAGE_ROOT'], 'settings.yaml') - with open(fn, "w") as f: + 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) as f: + with open(fn, encoding="utf-8") as f: config = rtyaml.load(f) if not isinstance(config, dict): raise ValueError # caught below return config diff --git a/management/web_update.py b/management/web_update.py index 0aeae2ec..722c8883 100644 --- a/management/web_update.py +++ b/management/web_update.py @@ -62,7 +62,7 @@ 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): - with open(nginx_conf_custom_fn) as f: + 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('/')), @@ -77,7 +77,7 @@ def do_web_update(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)) as f: + 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. @@ -112,12 +112,12 @@ def do_web_update(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 @@ -155,7 +155,7 @@ def make_domain_config(domain, templates, ssl_certificates, env): hsts = "yes" nginx_conf_custom_fn = os.path.join(env["STORAGE_ROOT"], "www/custom.yaml") if os.path.exists(nginx_conf_custom_fn): - with open(nginx_conf_custom_fn) as f: + with open(nginx_conf_custom_fn, encoding='utf-8') as f: yaml = rtyaml.load(f) if domain in yaml: yaml = yaml[domain] diff --git a/setup/migrate.py b/setup/migrate.py index 4b7591ba..3364b55d 100755 --- a/setup/migrate.py +++ b/setup/migrate.py @@ -212,7 +212,7 @@ 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: + with open(migration_id_file, encoding='utf-8') as f: migration_id = f.read().strip() if migration_id is None: @@ -253,7 +253,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. diff --git a/tools/editconf.py b/tools/editconf.py index 4724bbe8..8b36c6bb 100755 --- a/tools/editconf.py +++ b/tools/editconf.py @@ -76,7 +76,7 @@ for setting in settings: found = set() buf = "" -with open(filename) as f: +with open(filename, encoding="utf-8") as f: input_lines = list(f) while len(input_lines) > 0: @@ -144,7 +144,7 @@ for i in range(len(settings)): if not testing: # Write out the new file. - with open(filename, "w") as f: + with open(filename, "w", encoding="utf-8") as f: f.write(buf) else: # Just print the new file to stdout. diff --git a/tools/parse-nginx-log-bootstrap-accesses.py b/tools/parse-nginx-log-bootstrap-accesses.py index 6207b2e9..8eb74dec 100755 --- a/tools/parse-nginx-log-bootstrap-accesses.py +++ b/tools/parse-nginx-log-bootstrap-accesses.py @@ -38,7 +38,7 @@ for date, ip in accesses: # Since logs are rotated, store the statistics permanently in a JSON file. # Load in the stats from an existing file. if os.path.exists(outfn): - with open(outfn) as f: + with open(outfn, encoding="utf-8") as f: existing_data = json.load(f) for date, count in existing_data: if date not in by_date: @@ -51,5 +51,5 @@ by_date = sorted(by_date.items()) by_date.pop(-1) # Write out. -with open(outfn, "w") as f: +with open(outfn, "w", encoding="utf-8") as f: json.dump(by_date, f, sort_keys=True, indent=True) From e466b9bb5387a83cdd54a177cc49fcc9a85f1210 Mon Sep 17 00:00:00 2001 From: Teal Dulcet Date: Sat, 23 Dec 2023 05:20:45 -0800 Subject: [PATCH 074/121] Fixed RUF005 (collection-literal-concatenation) --- management/backup.py | 20 ++++++++++---------- management/status_checks.py | 2 +- management/web_update.py | 2 +- tests/fail2ban.py | 2 +- tests/tls.py | 2 +- 5 files changed, 14 insertions(+), 14 deletions(-) diff --git a/management/backup.py b/management/backup.py index 183ff607..55320049 100755 --- a/management/backup.py +++ b/management/backup.py @@ -59,7 +59,7 @@ def backup_status(env): "--archive-dir", backup_cache_dir, "--gpg-options", "'--cipher-algo=AES256'", "--log-fd", "1", - ] + get_duplicity_additional_args(env) + [ + *get_duplicity_additional_args(env), get_duplicity_target_url(config) ], get_duplicity_env_vars(env), @@ -322,8 +322,8 @@ def perform_backup(full_backup): "--exclude", backup_root, "--volsize", "250", "--gpg-options", "'--cipher-algo=AES256'", - "--allow-source-mismatch" - ] + get_duplicity_additional_args(env) + [ + "--allow-source-mismatch", + *get_duplicity_additional_args(env), env["STORAGE_ROOT"], get_duplicity_target_url(config), ], @@ -344,7 +344,7 @@ def perform_backup(full_backup): "--verbosity", "error", "--archive-dir", backup_cache_dir, "--force", - ] + get_duplicity_additional_args(env) + [ + *get_duplicity_additional_args(env), get_duplicity_target_url(config) ], get_duplicity_env_vars(env)) @@ -360,7 +360,7 @@ def perform_backup(full_backup): "--verbosity", "error", "--archive-dir", backup_cache_dir, "--force", - ] + get_duplicity_additional_args(env) + [ + *get_duplicity_additional_args(env), get_duplicity_target_url(config) ], get_duplicity_env_vars(env)) @@ -399,7 +399,7 @@ def run_duplicity_verification(): "--compare-data", "--archive-dir", backup_cache_dir, "--exclude", backup_root, - ] + get_duplicity_additional_args(env) + [ + *get_duplicity_additional_args(env), get_duplicity_target_url(config), env["STORAGE_ROOT"], ], get_duplicity_env_vars(env)) @@ -412,9 +412,9 @@ def run_duplicity_restore(args): "/usr/bin/duplicity", "restore", "--archive-dir", backup_cache_dir, - ] + get_duplicity_additional_args(env) + [ - get_duplicity_target_url(config) - ] + args, + *get_duplicity_additional_args(env), + get_duplicity_target_url(config), + *args], get_duplicity_env_vars(env)) def print_duplicity_command(): @@ -426,7 +426,7 @@ def print_duplicity_command(): print(f"export {k}={shlex.quote(v)}") print("duplicity", "{command}", shlex.join([ "--archive-dir", backup_cache_dir, - ] + get_duplicity_additional_args(env) + [ + *get_duplicity_additional_args(env), get_duplicity_target_url(config) ])) diff --git a/management/status_checks.py b/management/status_checks.py index fa27396d..831a7510 100755 --- a/management/status_checks.py +++ b/management/status_checks.py @@ -518,7 +518,7 @@ def check_dns_zone(domain, env, output, dns_zonefiles): secondary_ns = custom_secondary_ns or ["ns2." + env['PRIMARY_HOSTNAME']] existing_ns = query_dns(domain, "NS") - correct_ns = "; ".join(sorted(["ns1." + env['PRIMARY_HOSTNAME']] + secondary_ns)) + correct_ns = "; ".join(sorted(["ns1." + env["PRIMARY_HOSTNAME"], *secondary_ns])) ip = query_dns(domain, "A") probably_external_dns = False diff --git a/management/web_update.py b/management/web_update.py index 722c8883..a0f0f799 100644 --- a/management/web_update.py +++ b/management/web_update.py @@ -215,7 +215,7 @@ def make_domain_config(domain, templates, ssl_certificates, 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. diff --git a/tests/fail2ban.py b/tests/fail2ban.py index 2f2ac352..dbab874a 100644 --- a/tests/fail2ban.py +++ b/tests/fail2ban.py @@ -181,7 +181,7 @@ def run_test(testfunc, args, count, within_seconds, parallel): # Distribute the requests across the pool. asyncresults = [] for i in range(count): - ar = p.apply_async(testfunc_runner, [i, testfunc] + list(args)) + ar = p.apply_async(testfunc_runner, [i, testfunc, *list(args)]) asyncresults.append(ar) # Wait for all runs to finish. diff --git a/tests/tls.py b/tests/tls.py index 10782a9b..8795f36a 100644 --- a/tests/tls.py +++ b/tests/tls.py @@ -88,7 +88,7 @@ def sslyze(opts, port, ok_ciphers): try: # Execute SSLyze. - out = subprocess.check_output([SSLYZE] + common_opts + opts + [connection_string]) + out = subprocess.check_output([SSLYZE, *common_opts, *opts, connection_string]) out = out.decode("utf8") # Trim output to make better for storing in git. From 7f456d8e8ba14e50c2f60cfb78cbb959c93cf10a Mon Sep 17 00:00:00 2001 From: Teal Dulcet Date: Sat, 23 Dec 2023 05:23:58 -0800 Subject: [PATCH 075/121] Fixed ISC002 (multi-line-implicit-string-concatenation): Implicitly concatenated string literals over multiple lines --- management/backup.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/management/backup.py b/management/backup.py index 55320049..3031662b 100755 --- a/management/backup.py +++ b/management/backup.py @@ -488,9 +488,9 @@ def list_target_files(config): 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." + 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) From 6a47133e3fe5616e83a65ac87006d35d54e0cf70 Mon Sep 17 00:00:00 2001 From: Teal Dulcet Date: Sat, 23 Dec 2023 05:24:59 -0800 Subject: [PATCH 076/121] Fixed F811 (redefined-while-unused): Redefinition of unused `sys` from line 10 --- management/backup.py | 1 - 1 file changed, 1 deletion(-) diff --git a/management/backup.py b/management/backup.py index 3031662b..f352fb41 100755 --- a/management/backup.py +++ b/management/backup.py @@ -617,7 +617,6 @@ def write_backup_config(env, newconfig): 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. From f0377dd59ee10891edac84443e4a4018708d016b Mon Sep 17 00:00:00 2001 From: Teal Dulcet Date: Sat, 23 Dec 2023 05:26:32 -0800 Subject: [PATCH 077/121] Fixed SIM105 (suppressible-exception) --- management/cli.py | 5 ++--- management/daemon.py | 5 ++--- management/dns_update.py | 5 ++--- setup/migrate.py | 5 ++--- 4 files changed, 8 insertions(+), 12 deletions(-) diff --git a/management/cli.py b/management/cli.py index ea060d3f..70dbe894 100755 --- a/management/cli.py +++ b/management/cli.py @@ -7,6 +7,7 @@ # 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.) @@ -19,10 +20,8 @@ def mgmt(cmd, data=None, is_json=False): response = urllib.request.urlopen(req) except urllib.error.HTTPError as e: if e.code == 401: - try: + with contextlib.suppress(Exception): print(e.read().decode("utf8")) - except: - pass print("The management daemon refused access. The API key file may be out of sync. Try 'service mailinabox restart'.", file=sys.stderr) elif hasattr(e, 'read'): print(e.read().decode('utf8'), file=sys.stderr) diff --git a/management/daemon.py b/management/daemon.py index 074e9fa7..bb4cd15d 100755 --- a/management/daemon.py +++ b/management/daemon.py @@ -22,6 +22,7 @@ from mailconfig import get_mail_users, get_mail_users_ex, get_admins, add_mail_u 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 mfa import get_public_mfa_state, provision_totp, validate_totp_secret, enable_mfa, disable_mfa +import contextlib env = utils.load_environment() @@ -29,10 +30,8 @@ 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 = [] diff --git a/management/dns_update.py b/management/dns_update.py index c06ef352..5e24f118 100755 --- a/management/dns_update.py +++ b/management/dns_update.py @@ -11,6 +11,7 @@ import dns.resolver from utils import shell, load_env_vars_from_file, safe_domain_name, sort_domains from ssl_certificates import get_ssl_certificates, check_certificate +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, @@ -456,10 +457,8 @@ def build_sshfp_records(): for line in f: s = line.rstrip().split() if len(s) == 2 and s[0] == 'Port': - try: + with contextlib.suppress(ValueError): port = int(s[1]) - except ValueError: - pass break keys = shell("check_output", ["ssh-keyscan", "-4", "-t", "rsa,dsa,ecdsa,ed25519", "-p", str(port), "localhost"]) diff --git a/setup/migrate.py b/setup/migrate.py index 3364b55d..17fbd5cf 100755 --- a/setup/migrate.py +++ b/setup/migrate.py @@ -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 From cacf6d2006db3bf4c7b8785296da31529fe5be9e Mon Sep 17 00:00:00 2001 From: Teal Dulcet Date: Sat, 23 Dec 2023 05:32:34 -0800 Subject: [PATCH 078/121] Fixed E721 (type-comparison): Use `is` and `is not` for type comparisons, or `isinstance()` for isinstance checks --- management/daemon.py | 2 +- management/mfa.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/management/daemon.py b/management/daemon.py index bb4cd15d..3aa6eed2 100755 --- a/management/daemon.py +++ b/management/daemon.py @@ -490,7 +490,7 @@ def totp_post_enable(): secret = request.form.get('secret') token = request.form.get('token') label = request.form.get('label') - if type(token) != str: + if not isinstance(token, str): return ("Bad Input", 400) try: validate_totp_secret(secret) diff --git a/management/mfa.py b/management/mfa.py index 148fd912..6b56ad86 100644 --- a/management/mfa.py +++ b/management/mfa.py @@ -68,7 +68,7 @@ def disable_mfa(email, mfa_id, env): return c.rowcount > 0 def validate_totp_secret(secret): - if type(secret) != str or secret.strip() == "": + if not isinstance(secret, str) or secret.strip() == "": msg = "No secret provided." raise ValueError(msg) if len(secret) != 32: From 1d79f9bb2b0ba29e979af60c0bcbdfd3225ba6b1 Mon Sep 17 00:00:00 2001 From: Teal Dulcet Date: Sat, 23 Dec 2023 05:40:37 -0800 Subject: [PATCH 079/121] Fixed PERF401 (manual-list-comprehension): Use a list comprehension to create a transformed list --- management/dns_update.py | 10 +++------- management/mail_log.py | 5 +---- 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/management/dns_update.py b/management/dns_update.py index 5e24f118..597df243 100755 --- a/management/dns_update.py +++ b/management/dns_update.py @@ -49,9 +49,7 @@ def get_dns_zones(env): zone_domains.add(domain) # Make a nice and safe filename for each domain. - zonefiles = [] - for domain in zone_domains: - zonefiles.append([domain, safe_domain_name(domain) + ".txt"]) + zonefiles = [[domain, safe_domain_name(domain) + ".txt"] for domain in zone_domains] # Sort the list so that the order is nice and so that nsd.conf has a # stable order so we don't rewrite the file & restart the service @@ -195,8 +193,7 @@ def build_zone(domain, domain_properties, additional_records, env, is_zone=True) # User may provide one or more additional nameservers secondary_ns_list = get_secondary_dns(additional_records, mode="NS") \ or ["ns2." + env["PRIMARY_HOSTNAME"]] - for secondary_ns in secondary_ns_list: - records.append((None, "NS", secondary_ns+'.', False)) + records.extend((None, "NS", secondary_ns+'.', False) for secondary_ns in secondary_ns_list) # In PRIMARY_HOSTNAME... @@ -213,8 +210,7 @@ def build_zone(domain, domain_properties, additional_records, env, is_zone=True) records.append(("_443._tcp", "TLSA", build_tlsa_record(env), "Optional. When DNSSEC is enabled, provides out-of-band HTTPS certificate validation for a few web clients that support it.")) # Add a SSHFP records to help SSH key validation. One per available SSH key on this system. - for value in build_sshfp_records(): - records.append((None, "SSHFP", value, "Optional. Provides an out-of-band method for verifying an SSH key before connecting. Use 'VerifyHostKeyDNS yes' (or 'VerifyHostKeyDNS ask') when connecting with ssh.")) + records.extend((None, "SSHFP", value, "Optional. Provides an out-of-band method for verifying an SSH key before connecting. Use 'VerifyHostKeyDNS yes' (or 'VerifyHostKeyDNS ask') when connecting with ssh.") for value in build_sshfp_records()) # Add DNS records for any subdomains of this domain. We should not have a zone for # both a domain and one of its subdomains. diff --git a/management/mail_log.py b/management/mail_log.py index 0c91365b..e127af3b 100755 --- a/management/mail_log.py +++ b/management/mail_log.py @@ -619,10 +619,7 @@ def print_time_table(labels, data, do_print=True): data.insert(0, [str(h) for h in range(24)]) temp = "ā”‚ {:<%d} " % max(len(l) for l in labels) - lines = [] - - for label in labels: - lines.append(temp.format(label)) + lines = [temp.format(label) for label in labels] for h in range(24): max_len = max(len(str(d[h])) for d in data) From a32354fd91551bd0d10ee14cb1bfad7d70ebae66 Mon Sep 17 00:00:00 2001 From: Teal Dulcet Date: Sat, 23 Dec 2023 05:51:45 -0800 Subject: [PATCH 080/121] Fixed PLR5501 (collapsible-else-if): Use `elif` instead of `else` then `if`, to reduce indentation --- management/status_checks.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/management/status_checks.py b/management/status_checks.py index 831a7510..0f060100 100755 --- a/management/status_checks.py +++ b/management/status_checks.py @@ -154,12 +154,11 @@ def check_service(i, service, env): if not running and service["port"] in {80, 443}: output.print_line(shell('check_output', ['nginx', '-t'], capture_stderr=True, trap=True)[1].strip()) + # Service should be running locally. + elif try_connect("127.0.0.1"): + running = True else: - # Service should be running locally. - if try_connect("127.0.0.1"): - running = True - else: - output.print_error("%s is not running (port %d)." % (service['name'], service['port'])) + output.print_error("%s is not running (port %d)." % (service['name'], service['port'])) # Flag if local DNS is not running. if not running and service["port"] == 53 and service["public"] is False: From 618c466b84d6cf00fa4bf5d3c36c94b7e5f0f950 Mon Sep 17 00:00:00 2001 From: Teal Dulcet Date: Sat, 23 Dec 2023 05:52:53 -0800 Subject: [PATCH 081/121] Fixed SIM114 (if-with-same-arms): Combine `if` branches using logical `or` operator --- management/status_checks.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/management/status_checks.py b/management/status_checks.py index 0f060100..cd3e9f28 100755 --- a/management/status_checks.py +++ b/management/status_checks.py @@ -678,9 +678,7 @@ def check_mail_domain(domain, env, output): recommended_mx = "10 " + env['PRIMARY_HOSTNAME'] mx = query_dns(domain, "MX", nxdomain=None) - if mx is None: - mxhost = None - elif mx == "[timeout]": + if mx is None or mx == "[timeout]": mxhost = None else: # query_dns returns a semicolon-delimited list From 775a4223de343e596b30d6dd3f2872d941c26200 Mon Sep 17 00:00:00 2001 From: Teal Dulcet Date: Sat, 23 Dec 2023 05:56:07 -0800 Subject: [PATCH 082/121] Fixed F821 (undefined-name): Undefined name `e` --- setup/migrate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup/migrate.py b/setup/migrate.py index 17fbd5cf..066e0e03 100755 --- a/setup/migrate.py +++ b/setup/migrate.py @@ -167,7 +167,7 @@ def migration_12(env): dropcmd = "DROP TABLE %s" % table c.execute(dropcmd) except: - print("Failed to drop table", table, e) + print("Failed to drop table", table) # Save. conn.commit() conn.close() From dbc2b5eee03745f9d565e17c8b262d7c9c45c412 Mon Sep 17 00:00:00 2001 From: Teal Dulcet Date: Sat, 23 Dec 2023 06:00:04 -0800 Subject: [PATCH 083/121] Fixed ISC003 (explicit-string-concatenation): Explicitly concatenated string should be implicitly concatenated --- tools/editconf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/editconf.py b/tools/editconf.py index 8b36c6bb..0438695b 100755 --- a/tools/editconf.py +++ b/tools/editconf.py @@ -94,7 +94,7 @@ while len(input_lines) > 0: name, val = settings[i].split("=", 1) m = re.match( r"(\s*)" - + "(" + re.escape(comment_char) + r"\s*)?" + "(" + re.escape(comment_char) + r"\s*)?" + re.escape(name) + delimiter_re + r"(.*?)\s*$", line, re.S) if not m: continue From 10533401240a7eadb676e302e0e37a75df6d97f0 Mon Sep 17 00:00:00 2001 From: Bastian Bittorf Date: Sun, 10 Mar 2024 13:01:13 +0100 Subject: [PATCH 084/121] 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... --- setup/preflight.sh | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/setup/preflight.sh b/setup/preflight.sh index bd6d65b7..2105ebec 100644 --- a/setup/preflight.sh +++ b/setup/preflight.sh @@ -1,3 +1,4 @@ +#!/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:" @@ -26,16 +27,16 @@ fi # # 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 490000 ]; 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 512 MB, 1 GB recommended." echo "This machine has $TOTAL_PHYSICAL_MEM MB memory." exit fi fi -if [ $TOTAL_PHYSICAL_MEM -lt 750000 ]; then +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 From 1b8cdeb644bb1f5471e715bb63d024313d3e96f2 Mon Sep 17 00:00:00 2001 From: Crag-Monkey Date: Sun, 10 Mar 2024 07:02:16 -0500 Subject: [PATCH 085/121] Allow customizations to Roundcube settings to persist between updates by including a configuration override file, if it exists (#2333) --- setup/webmail.sh | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/setup/webmail.sh b/setup/webmail.sh index 68042789..a663ebd0 100755 --- a/setup/webmail.sh +++ b/setup/webmail.sh @@ -145,6 +145,9 @@ cat > $RCM_CONFIG < EOF @@ -166,6 +169,9 @@ cat > ${RCM_PLUGIN_DIR}/carddav/config.inc.php < '1', 'hide' => false, ); +/* Persist Customizations */ +\$file = __DIR__.'/config_override.inc.php'; +\if(file_exists($file)) { include $file; } ?> EOF From 0b1d92388acdde16438c2e352e6c7664c73d7410 Mon Sep 17 00:00:00 2001 From: KiekerJan Date: Sun, 10 Mar 2024 13:09:36 +0100 Subject: [PATCH 086/121] Take spamhaus return codes into account in status check and postfix config (#2332) --- management/status_checks.py | 38 +++++++++++++++++++++++++++---------- setup/mail-postfix.sh | 5 +++-- 2 files changed, 31 insertions(+), 12 deletions(-) diff --git a/management/status_checks.py b/management/status_checks.py index cd3e9f28..77019a4b 100755 --- a/management/status_checks.py +++ b/management/status_checks.py @@ -306,14 +306,23 @@ 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) if zen is None: output.print_ok("IP address is not blacklisted by zen.spamhaus.org.") elif zen == "[timeout]": - output.print_warning("Connection to zen.spamhaus.org timed out. We could not determine whether your server's IP address is blacklisted. Please try again later.") + output.print_warning("Connection to zen.spamhaus.org timed out. Could not determine whether this box's IP address is blacklisted. Please try again later.") elif zen == "[Not Set]": - output.print_warning("Could not connect to zen.spamhaus.org. We could not determine whether your server's IP address is blacklisted. Please try again later.") + 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.") + 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')) + 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.") + 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.") 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'])) @@ -451,7 +460,7 @@ def check_primary_hostname_dns(domain, env, output, dns_domains, dns_zonefiles): if ip == env['PUBLIC_IP'] and not (ipv6 and env['PUBLIC_IPV6'] and ipv6 != normalize_ip(env['PUBLIC_IPV6'])): output.print_ok("Domain resolves to box's IP address. [{} ā†¦ {}]".format(env['PRIMARY_HOSTNAME'], my_ips)) else: - output.print_error("""This domain must resolve to your box's IP address ({}) in public DNS but it currently resolves + output.print_error("""This domain must resolve to this box's IP address ({}) in public DNS but it currently resolves to {}. It may take several hours for public DNS to update after a change. This problem may result from other issues listed above.""".format(my_ips, ip + ((" / " + ipv6) if ipv6 is not None else ""))) @@ -463,11 +472,11 @@ def check_primary_hostname_dns(domain, env, output, dns_domains, dns_zonefiles): if existing_rdns_v4 == domain and existing_rdns_v6 in {None, domain}: output.print_ok("Reverse DNS is set correctly at ISP. [{} ā†¦ {}]".format(my_ips, env['PRIMARY_HOSTNAME'])) elif existing_rdns_v4 == existing_rdns_v6 or existing_rdns_v6 is None: - output.print_error(f"""Your box's reverse DNS is currently {existing_rdns_v4}, but it should be {domain}. Your ISP or cloud provider will have instructions - on setting up reverse DNS for your box.""" ) + output.print_error(f"""This box's reverse DNS is currently {existing_rdns_v4}, but it should be {domain}. Your ISP or cloud provider will have instructions + on setting up reverse DNS for this box.""" ) else: - output.print_error(f"""Your box's reverse DNS is currently {existing_rdns_v4} (IPv4) and {existing_rdns_v6} (IPv6), but it should be {domain}. Your ISP or cloud provider will have instructions - on setting up reverse DNS for your box.""" ) + output.print_error(f"""This box's reverse DNS is currently {existing_rdns_v4} (IPv4) and {existing_rdns_v6} (IPv6), but it should be {domain}. Your ISP or cloud provider will have instructions + on setting up reverse DNS for this box.""" ) # Check the TLSA record. tlsa_qname = "_25._tcp." + domain @@ -736,13 +745,22 @@ def check_mail_domain(domain, env, output): # Stop if the domain is listed in the Spamhaus Domain Block List. # The user might have chosen a domain that was previously in use by a spammer # and will not be able to reliably send mail. + + # See https://www.spamhaus.org/news/article/807/using-our-public-mirrors-check-your-return-codes-now. for + # information on spamhaus return codes dbl = query_dns(domain+'.dbl.spamhaus.org', "A", nxdomain=None) if dbl is None: output.print_ok("Domain is not blacklisted by dbl.spamhaus.org.") elif dbl == "[timeout]": - output.print_warning(f"Connection to dbl.spamhaus.org timed out. We could not determine whether the domain {domain} is blacklisted. Please try again later.") + output.print_warning(f"Connection to dbl.spamhaus.org timed out. Could not determine whether the domain {domain} is blacklisted. Please try again later.") elif dbl == "[Not Set]": - output.print_warning(f"Could not connect to dbl.spamhaus.org. We could not determine whether the domain {domain} is blacklisted. Please try again later.") + output.print_warning(f"Could not connect to dbl.spamhaus.org. Could not determine whether the domain {domain} is blacklisted. Please try again later.") + elif dbl == "127.255.255.252": + output.print_warning("Incorrect spamhaus query: %s. Could not determine whether the domain %s is blacklisted." % (domain+'.dbl.spamhaus.org', domain)) + elif dbl == "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 the domain {} is blacklisted.".format(domain)) + elif dbl == "127.255.255.255": + output.print_warning("Too many queries have been performed on the spamhaus server. Could not determine whether the domain {} is blacklisted.".format(domain)) else: output.print_error(f"""This domain is listed in the Spamhaus Domain Block List (code {dbl}), which may prevent recipients from receiving your mail. @@ -760,7 +778,7 @@ def check_web_domain(domain, rounded_time, ssl_certificates, env, output): if value == normalize_ip(expected): ok_values.append(value) else: - output.print_error(f"""This domain should resolve to your box's IP address ({rtype} {expected}) if you would like the box to serve + output.print_error(f"""This domain should resolve to this box's IP address ({rtype} {expected}) if you would like the box to serve webmail or a website on this domain. The domain currently resolves to {value} in public DNS. It may take several hours for public DNS to update after a change. This problem may result from other issues listed here.""") return diff --git a/setup/mail-postfix.sh b/setup/mail-postfix.sh index 6ceb2edf..eab152fb 100755 --- a/setup/mail-postfix.sh +++ b/setup/mail-postfix.sh @@ -224,14 +224,15 @@ 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 # 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). From 18b8f9ab4bfe38ba6d0555bbb0dfbe4eab0a5b4e Mon Sep 17 00:00:00 2001 From: Joshua Tauberer Date: Sun, 10 Mar 2024 08:25:13 -0400 Subject: [PATCH 087/121] Revert "Allow customizations to Roundcube settings to persist between updates by including a configuration override file, if it exists (#2333)" This reverts commit 1b8cdeb644bb1f5471e715bb63d024313d3e96f2. It didn't execute. I should have tried it first. --- setup/webmail.sh | 6 ------ 1 file changed, 6 deletions(-) diff --git a/setup/webmail.sh b/setup/webmail.sh index a663ebd0..68042789 100755 --- a/setup/webmail.sh +++ b/setup/webmail.sh @@ -145,9 +145,6 @@ cat > $RCM_CONFIG < EOF @@ -169,9 +166,6 @@ cat > ${RCM_PLUGIN_DIR}/carddav/config.inc.php < '1', 'hide' => false, ); -/* Persist Customizations */ -\$file = __DIR__.'/config_override.inc.php'; -\if(file_exists($file)) { include $file; } ?> EOF From 163b1a297ef65f2157765170c1ef277ec93e2913 Mon Sep 17 00:00:00 2001 From: jvolkenant Date: Sat, 23 Mar 2024 05:49:24 -0700 Subject: [PATCH 088/121] Silence "wal" output on setup using hide_output (#2368) --- setup/webmail.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup/webmail.sh b/setup/webmail.sh index 68042789..3cff1416 100755 --- a/setup/webmail.sh +++ b/setup/webmail.sh @@ -217,7 +217,7 @@ sed -i.miabold 's/^[^#]\+.\+PRAGMA journal_mode = WAL.\+$/#&/' \ # Because Roundcube wants to set the PRAGMA we just deleted from the source, we apply it here # to the roundcube database (see https://github.com/roundcube/roundcubemail/issues/8035) # Database should exist, created by migration script -sqlite3 $STORAGE_ROOT/mail/roundcube/roundcube.sqlite 'PRAGMA journal_mode=WAL;' | 2>&1 +hide_output sqlite3 $STORAGE_ROOT/mail/roundcube/roundcube.sqlite 'PRAGMA journal_mode=WAL;' # Enable PHP modules. phpenmod -v $PHP_VER imap From 9b450469eb8ff912ed9e909d869e3e9cb61b9646 Mon Sep 17 00:00:00 2001 From: Gio Date: Sat, 23 Mar 2024 21:04:43 +0800 Subject: [PATCH 089/121] Mail guide: OS X -> macOS (#2306) --- management/templates/mail-guide.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/management/templates/mail-guide.html b/management/templates/mail-guide.html index 0ffadbdf..5e3dc1ec 100644 --- a/management/templates/mail-guide.html +++ b/management/templates/mail-guide.html @@ -16,7 +16,7 @@

Automatic configuration

-

iOS and OS X only: Open this configuration link 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.

+

iOS and macOS only: Open this configuration link 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.

Manual configuration

From 1a239c55bb2955af607e2784eec63431ec5b607d Mon Sep 17 00:00:00 2001 From: KiekerJan Date: Sat, 23 Mar 2024 16:16:40 +0100 Subject: [PATCH 090/121] More robust reading of sshd configuration (#2330) Use sshd -T instead of directly reading the configuration files --- management/dns_update.py | 15 +++++-------- management/status_checks.py | 44 +++++++++---------------------------- management/utils.py | 28 +++++++++++++++++++++++ 3 files changed, 44 insertions(+), 43 deletions(-) diff --git a/management/dns_update.py b/management/dns_update.py index 597df243..599f27b1 100755 --- a/management/dns_update.py +++ b/management/dns_update.py @@ -9,7 +9,7 @@ import ipaddress import rtyaml import dns.resolver -from utils import shell, load_env_vars_from_file, safe_domain_name, sort_domains +from utils import shell, load_env_vars_from_file, safe_domain_name, sort_domains, get_ssh_port from ssl_certificates import get_ssl_certificates, check_certificate import contextlib @@ -448,14 +448,11 @@ def build_sshfp_records(): # if SSH has been configured to listen on a nonstandard port, we must # specify that port to sshkeyscan. - port = 22 - with open('/etc/ssh/sshd_config', encoding="utf-8") as f: - for line in f: - s = line.rstrip().split() - if len(s) == 2 and s[0] == 'Port': - with contextlib.suppress(ValueError): - port = int(s[1]) - break + port = get_ssh_port() + + # If nothing returned, SSH is probably not installed. + if not port: + return keys = shell("check_output", ["ssh-keyscan", "-4", "-t", "rsa,dsa,ecdsa,ed25519", "-p", str(port), "localhost"]) keys = sorted(keys.split("\n")) diff --git a/management/status_checks.py b/management/status_checks.py index 77019a4b..51f8e631 100755 --- a/management/status_checks.py +++ b/management/status_checks.py @@ -17,7 +17,7 @@ from web_update import get_web_domains, get_domains_with_a_records from ssl_certificates import get_ssl_certificates, get_domain_ssl_files, check_certificate from mailconfig import get_mail_domains, get_mail_aliases -from utils import shell, sort_domains, load_env_vars_from_file, load_settings +from utils import shell, sort_domains, load_env_vars_from_file, load_settings, get_ssh_port, get_ssh_config_value def get_services(): return [ @@ -65,24 +65,6 @@ def run_checks(rounded_values, env, output, pool, domains_to_check=None): run_network_checks(env, output) run_domain_checks(rounded_values, env, output, pool, domains_to_check=domains_to_check) -def get_ssh_port(): - # Returns ssh port - try: - output = shell('check_output', ['sshd', '-T']) - except FileNotFoundError: - # sshd is not installed. That's ok. - return None - - returnNext = False - for e in output.split(): - if returnNext: - return int(e) - if e == "port": - returnNext = True - - # Did not find port! - return None - def run_services_checks(env, output, pool): # Check that system services are running. all_running = True @@ -206,21 +188,15 @@ def is_port_allowed(ufw, port): return any(re.match(str(port) +"[/ \t].*", item) for item in ufw) def check_ssh_password(env, output): - # Check that SSH login with password is disabled. The openssh-server - # package may not be installed so check that before trying to access - # the configuration file. - if not os.path.exists("/etc/ssh/sshd_config"): - return - with open("/etc/ssh/sshd_config", encoding="utf-8") as f: - sshd = f.read() - if re.search("\nPasswordAuthentication\\s+yes", sshd) \ - or not re.search("\nPasswordAuthentication\\s+no", sshd): - output.print_error("""The SSH server on this machine permits password-based login. A more secure - way to log in is using a public key. Add your SSH public key to $HOME/.ssh/authorized_keys, check - that you can log in without a password, set the option 'PasswordAuthentication no' in - /etc/ssh/sshd_config, and then restart the openssh via 'sudo service ssh restart'.""") - else: - output.print_ok("SSH disallows password-based login.") + config_value = get_ssh_config_value("passwordauthentication") + if config_value: + if config_value == "no": + output.print_ok("SSH disallows password-based login.") + else: + output.print_error("""The SSH server on this machine permits password-based login. A more secure + way to log in is using a public key. Add your SSH public key to $HOME/.ssh/authorized_keys, check + that you can log in without a password, set the option 'PasswordAuthentication no' in + /etc/ssh/sshd_config, and then restart the openssh via 'sudo service ssh restart'.""") def is_reboot_needed_due_to_package_installation(): return os.path.exists("/var/run/reboot-required") diff --git a/management/utils.py b/management/utils.py index 929544d1..397f124d 100644 --- a/management/utils.py +++ b/management/utils.py @@ -179,6 +179,34 @@ def wait_for_service(port, public, env, 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() From fa72e015ee642bef1b1533378c6d67cc1d732bff Mon Sep 17 00:00:00 2001 From: Joshua Tauberer Date: Sat, 23 Mar 2024 12:59:39 -0400 Subject: [PATCH 091/121] Update SMTP Smuggling protection to the 'long-term fix' * Revert "Guard against SMTP smuggling", commit faf23f150c5fa85c8e9af1e345d796d2c36a4577, by restoring the setting to its default. * Revert "[security] SMTP smuggling: update short term fix (#2346)", commmit e931e103fe1d6db81681e3c9732d21e9860acdcd, by restoring the setting to its default. * Set smtpd_forbid_bare_newline=normalize. --- setup/mail-postfix.sh | 12 +++++++++--- tools/editconf.py | 2 +- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/setup/mail-postfix.sh b/setup/mail-postfix.sh index eab152fb..24969513 100755 --- a/setup/mail-postfix.sh +++ b/setup/mail-postfix.sh @@ -70,10 +70,16 @@ tools/editconf.py /etc/postfix/main.cf \ bounce_queue_lifetime=1d # Guard against SMTP smuggling -# This short-term workaround is recommended at https://www.postfix.org/smtp-smuggling.html +# 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_data_restrictions=reject_unauth_pipelining \ - smtpd_discard_ehlo_keywords="chunking, silent-discard" + smtpd_forbid_bare_newline=normalize # ### Outgoing Mail diff --git a/tools/editconf.py b/tools/editconf.py index 0438695b..db19e5f1 100755 --- a/tools/editconf.py +++ b/tools/editconf.py @@ -30,7 +30,7 @@ import sys, re # sanity check if len(sys.argv) < 3: - print("usage: python3 editconf.py /etc/file.conf [-s] [-w] [-c ] [-t] NAME=VAL [NAME=VAL ...]") + print("usage: python3 editconf.py /etc/file.conf [-e] [-s] [-w] [-c ] [-t] NAME=VAL [NAME=VAL ...]") sys.exit(1) # parse command line arguments From 7382c18e8fafcd34534e71b7937fedb5acfa2b43 Mon Sep 17 00:00:00 2001 From: Joshua Tauberer Date: Sat, 23 Mar 2024 13:18:14 -0400 Subject: [PATCH 092/121] CHANGELOG entries --- CHANGELOG.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c855c214..f6f79508 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,31 @@ CHANGELOG ========= +In Development +-------------- + +Package updates: + +* Roundcube updated to version 1.6.6. +* Nextcloud is updated to version 26.0.12. + +Mail: + +* Updated postfix's configuration to guard against SMTP smuggling to the long-term fix (https://www.postfix.org/smtp-smuggling.html). + +Control Panel: + +* Improved reporting of Spamhaus response codes. +* Improved detection of SSH port. +* Fixed an error if last saved status check results were corrupted. +* Other minor fixes. + +Other: + +* fail2ban is updated to see "HTTP/2.0" requests to munin also. +* Internal improvements to the code to make it more reliable and readable. + + Version 67 (December 22, 2023) ------------------------------ From 830c83daa1e7bc902cc3d5606aef9046d1226147 Mon Sep 17 00:00:00 2001 From: Joshua Tauberer Date: Mon, 1 Apr 2024 10:55:17 -0400 Subject: [PATCH 093/121] v68 --- CHANGELOG.md | 4 ++-- README.md | 2 +- setup/bootstrap.sh | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f6f79508..b048d219 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,8 @@ CHANGELOG ========= -In Development --------------- +Version 68 (April 1, 2024) +-------------------------- Package updates: diff --git a/README.md b/README.md index 8411c450..0b3d320b 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,7 @@ Clone this repository and checkout the tag corresponding to the most recent rele $ git clone https://github.com/mail-in-a-box/mailinabox $ cd mailinabox - $ git checkout v67 + $ git checkout v68 Begin the installation. diff --git a/setup/bootstrap.sh b/setup/bootstrap.sh index b52848ee..de373aff 100644 --- a/setup/bootstrap.sh +++ b/setup/bootstrap.sh @@ -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=v67 + 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. From 133bae13001bc2ba5f5279f9286e5d118bc5c4aa Mon Sep 17 00:00:00 2001 From: Teal Dulcet Date: Thu, 21 Dec 2023 06:15:50 -0800 Subject: [PATCH 094/121] Fixed SC2006: Use $(...) notation instead of legacy backticks `...`. --- management/daily_tasks.sh | 2 +- tools/archive_conf_files.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/management/daily_tasks.sh b/management/daily_tasks.sh index db496399..0f7bcdc8 100755 --- a/management/daily_tasks.sh +++ b/management/daily_tasks.sh @@ -11,7 +11,7 @@ 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 +if [ $(date "+%u") -eq 1 ]; then management/mail_log.py -t week | management/email_administrator.py "Mail-in-a-Box Usage Report" fi diff --git a/tools/archive_conf_files.sh b/tools/archive_conf_files.sh index 28b6b24f..299387ec 100644 --- a/tools/archive_conf_files.sh +++ b/tools/archive_conf_files.sh @@ -1,6 +1,6 @@ # Use this script to make an archive of the contents of all # of the configuration files we edit with editconf.py. -for fn in `grep -hr editconf.py setup | sed "s/tools\/editconf.py //" | sed "s/ .*//" | sort | uniq`; do +for fn in $(grep -hr editconf.py setup | sed "s/tools\/editconf.py //" | sed "s/ .*//" | sort | uniq); do echo ====================================================================== echo $fn echo ====================================================================== From 30c4681e802f32c4d0f95c22895abb329ba9d85e Mon Sep 17 00:00:00 2001 From: Teal Dulcet Date: Thu, 21 Dec 2023 06:58:34 -0800 Subject: [PATCH 095/121] Fixed SC2086: Double quote to prevent globbing and word splitting. --- setup/bootstrap.sh | 18 +++---- setup/dkim.sh | 8 +-- setup/dns.sh | 6 +-- setup/firstuser.sh | 8 +-- setup/functions.sh | 40 +++++++-------- setup/mail-dovecot.sh | 16 +++--- setup/mail-postfix.sh | 22 ++++---- setup/mail-users.sh | 12 ++--- setup/management.sh | 6 +-- setup/munin.sh | 10 ++-- setup/network-checks.sh | 6 +-- setup/nextcloud.sh | 94 +++++++++++++++++------------------ setup/questions.sh | 14 +++--- setup/spamassassin.sh | 10 ++-- setup/ssl.sh | 24 ++++----- setup/start.sh | 26 +++++----- setup/system.sh | 14 +++--- setup/web.sh | 34 ++++++------- setup/webmail.sh | 28 +++++------ setup/zpush.sh | 12 ++--- tools/archive_conf_files.sh | 4 +- tools/dns_update | 2 +- tools/owncloud-restore.sh | 16 +++--- tools/owncloud-unlockadmin.sh | 4 +- tools/web_update | 2 +- 25 files changed, 218 insertions(+), 218 deletions(-) mode change 100755 => 100644 setup/webmail.sh diff --git a/setup/bootstrap.sh b/setup/bootstrap.sh index de373aff..8824afe2 100644 --- a/setup/bootstrap.sh +++ b/setup/bootstrap.sh @@ -51,7 +51,7 @@ if [[ $EUID -ne 0 ]]; then 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 . . . apt-get -q -q update @@ -63,24 +63,24 @@ if [ ! -d $HOME/mailinabox ]; then SOURCE=https://github.com/mail-in-a-box/mailinabox fi - echo Downloading Mail-in-a-Box $TAG. . . + echo "Downloading Mail-in-a-Box $TAG. . ." git clone \ - -b $TAG --depth 1 \ - $SOURCE \ - $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" # Update it. 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 "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 diff --git a/setup/dkim.sh b/setup/dkim.sh index d2d162a7..0001d4c4 100755 --- a/setup/dkim.sh +++ b/setup/dkim.sh @@ -15,7 +15,7 @@ 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. @@ -53,12 +53,12 @@ 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" \ diff --git a/setup/dns.sh b/setup/dns.sh index 9b9b1b0a..2501795a 100755 --- a/setup/dns.sh +++ b/setup/dns.sh @@ -106,7 +106,7 @@ if [ ! -f "$STORAGE_ROOT/dns/dnssec/$algo.conf" ]; then # (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_); + 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 @@ -114,7 +114,7 @@ if [ ! -f "$STORAGE_ROOT/dns/dnssec/$algo.conf" ]; then # 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_); + ZSK=$(umask 077; cd "$STORAGE_ROOT/dns/dnssec"; ldns-keygen -r /dev/urandom -a $algo _domain_); # These generate two sets of files like: # @@ -126,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 diff --git a/setup/firstuser.sh b/setup/firstuser.sh index 7caec35d..540c565f 100644 --- a/setup/firstuser.sh +++ b/setup/firstuser.sh @@ -22,7 +22,7 @@ if [ -z "$(management/cli.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 @@ -47,11 +47,11 @@ 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:-}" # Make it an admin. - hide_output management/cli.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. - management/cli.py alias add administrator@$PRIMARY_HOSTNAME $EMAIL_ADDR > /dev/null + management/cli.py alias add "administrator@$PRIMARY_HOSTNAME" "$EMAIL_ADDR" > /dev/null fi diff --git a/setup/functions.sh b/setup/functions.sh index 151c5f40..8e8b79e2 100644 --- a/setup/functions.sh +++ b/setup/functions.sh @@ -16,7 +16,7 @@ function hide_output { # Execute command, redirecting stderr/stdout to the temporary file. Since we # check the return code ourselves, disable 'set -e' temporarily. set +e - "$@" &> $OUTPUT + "$@" &> "$OUTPUT" E=$? set -e @@ -26,13 +26,13 @@ function hide_output { echo echo FAILED: "$@" echo ----------------------------------------- - cat $OUTPUT + cat "$OUTPUT" echo ----------------------------------------- exit $E fi # Remove temporary file. - rm -f $OUTPUT + rm -f "$OUTPUT" } function apt_get_quiet { @@ -62,9 +62,9 @@ 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 } @@ -76,7 +76,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 || /bin/true + curl -"$1" --fail --silent --max-time 15 icanhazip.com 2>/dev/null || /bin/true } function get_default_privateip { @@ -119,19 +119,19 @@ 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 2>/dev/null | 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 { @@ -149,7 +149,7 @@ function ufw_limit { } function restart_service { - hide_output service $1 restart + hide_output service "$1" restart } ## Dialog Functions ## @@ -178,7 +178,7 @@ function input_menu { declare -n result_code=$4_EXITCODE local IFS=^$'\n' set +e - result=$(dialog --stdout --title "$1" --menu "$2" 0 0 0 $3) + result=$(dialog --stdout --title "$1" --menu "$2" 0 0 0 "$3") result_code=$? set -e } @@ -190,17 +190,17 @@ function wget_verify { HASH=$2 DEST=$3 CHECKSUM="$HASH $DEST" - rm -f $DEST - hide_output wget -O $DEST $URL + 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 } @@ -216,9 +216,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 } diff --git a/setup/mail-dovecot.sh b/setup/mail-dovecot.sh index 8d45a50b..39b22515 100755 --- a/setup/mail-dovecot.sh +++ b/setup/mail-dovecot.sh @@ -61,7 +61,7 @@ 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 @@ -152,7 +152,7 @@ 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 @@ -201,14 +201,14 @@ 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 -mkdir -p $STORAGE_ROOT/mail/sieve/global_before -mkdir -p $STORAGE_ROOT/mail/sieve/global_after -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 diff --git a/setup/mail-postfix.sh b/setup/mail-postfix.sh index 24969513..49dfaafa 100755 --- a/setup/mail-postfix.sh +++ b/setup/mail-postfix.sh @@ -55,9 +55,9 @@ apt_install postfix postfix-sqlite postfix-pcre postgrey ca-certificates # * Set the SMTP banner (which must have the hostname first, then anything). tools/editconf.py /etc/postfix/main.cf \ inet_interfaces=all \ - smtp_bind_address=$PRIVATE_IP \ - smtp_bind_address6=$PRIVATE_IPV6 \ - 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 @@ -138,9 +138,9 @@ sed -i "s/PUBLIC_IP/$PUBLIC_IP/" /etc/postfix/outgoing_mail_header_filters 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 \ 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 \ @@ -260,17 +260,17 @@ tools/editconf.py /etc/default/postgrey \ # 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 +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 + mkdir -p "$STORAGE_ROOT/mail/postgrey/db" # Move over database files - mv /var/lib/postgrey/* $STORAGE_ROOT/mail/postgrey/db/ || true + 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} +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; diff --git a/setup/mail-users.sh b/setup/mail-users.sh index b570f037..0c6be0c8 100755 --- a/setup/mail-users.sh +++ b/setup/mail-users.sh @@ -18,12 +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, 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; +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 diff --git a/setup/management.sh b/setup/management.sh index 34c0aeaf..cf2c4995 100755 --- a/setup/management.sh +++ b/setup/management.sh @@ -52,9 +52,9 @@ hide_output $venv/bin/pip install --upgrade \ # 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) +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 diff --git a/setup/munin.sh b/setup/munin.sh index 90f93521..56cdb391 100755 --- a/setup/munin.sh +++ b/setup/munin.sh @@ -40,7 +40,7 @@ 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 \ + host_name="$PRIMARY_HOSTNAME" \ log_level=1 # Update the activated plugins through munin's autoconfiguration. @@ -52,9 +52,9 @@ find /etc/munin/plugins/ -lname /usr/share/munin/plugins/ntp_ -print0 | xargs -0 # 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; + IF=$(echo "$f" | sed s/.*_//); + if ! grep -qFx up "/sys/class/net/$IF/operstate" 2>/dev/null; then + rm "$f"; fi; done @@ -62,7 +62,7 @@ done 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 +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 diff --git a/setup/network-checks.sh b/setup/network-checks.sh index 428fa4ca..5d5351c7 100644 --- a/setup/network-checks.sh +++ b/setup/network-checks.sh @@ -6,7 +6,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 +22,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." diff --git a/setup/nextcloud.sh b/setup/nextcloud.sh index 947d2f9a..d7b57d57 100755 --- a/setup/nextcloud.sh +++ b/setup/nextcloud.sh @@ -65,13 +65,13 @@ user_external_hash=a494073dcdecbbbc79a9c77f72524ac9994d2eec # 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 +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 ';' \ +tools/editconf.py /etc/php/"$PHP_VER"/mods-available/apcu.ini -c ';' \ apc.enabled=1 \ apc.enable_cli=1 @@ -91,7 +91,7 @@ InstallNextcloud() { echo # Download and verify - wget_verify https://download.nextcloud.com/server/releases/nextcloud-$version.zip $hash /tmp/nextcloud.zip + wget_verify "https://download.nextcloud.com/server/releases/nextcloud-$version.zip" "$hash" /tmp/nextcloud.zip # Remove the current owncloud/Nextcloud rm -rf /usr/local/lib/owncloud @@ -105,18 +105,18 @@ InstallNextcloud() { # 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 + 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 + 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 + 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 @@ -126,33 +126,33 @@ InstallNextcloud() { # 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 + 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 + 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 + 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 + sudo -u www-data php"$PHP_VER" /usr/local/lib/owncloud/occ upgrade if [ \( $? -ne 0 \) -a \( $? -ne 3 \) ]; then echo "Trying ownCloud upgrade again to work around ownCloud upgrade bug..." - sudo -u www-data php$PHP_VER /usr/local/lib/owncloud/occ upgrade + sudo -u www-data php"$PHP_VER" /usr/local/lib/owncloud/occ upgrade if [ \( $? -ne 0 \) -a \( $? -ne 3 \) ]; then exit 1; fi - sudo -u www-data php$PHP_VER /usr/local/lib/owncloud/occ maintenance:mode --off + sudo -u www-data php"$PHP_VER" /usr/local/lib/owncloud/occ maintenance:mode --off echo "...which seemed to work." 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 + 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 + sudo -u www-data php"$PHP_VER" /usr/local/lib/owncloud/occ db:convert-filecache-bigint --no-interaction fi } @@ -164,7 +164,7 @@ InstallNextcloud() { # 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']);") + CURRENT_NEXTCLOUD_VER=$(php"$PHP_VER" -r "include(\"$STORAGE_ROOT/owncloud/config.php\"); echo(\$CONFIG['version']);") else CURRENT_NEXTCLOUD_VER="" fi @@ -174,7 +174,7 @@ fi 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 + 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 @@ -184,21 +184,21 @@ if [ ! -d /usr/local/lib/owncloud/ ] || [[ ! ${CURRENT_NEXTCLOUD_VER} =~ ^$nextc 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 + 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 + if [ -e "$STORAGE_ROOT/owncloud/config.php" ]; then + cp "$STORAGE_ROOT/owncloud/config.php" "$BACKUP_DIRECTORY" fi # If ownCloud or Nextcloud was previously installed.... - if [ ! -z ${CURRENT_NEXTCLOUD_VER} ]; then + if [ ! -z "${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 + 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 + sed -i -e '/config_is_read_only/d' "$STORAGE_ROOT/owncloud/config.php" fi if [[ ${CURRENT_NEXTCLOUD_VER} =~ ^[89] ]]; then @@ -246,13 +246,13 @@ fi # 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 +if [ ! -f "$STORAGE_ROOT/owncloud/owncloud.db" ]; then # Create user data directory - mkdir -p $STORAGE_ROOT/owncloud + 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 < "$STORAGE_ROOT/owncloud/config.php" < '$STORAGE_ROOT/owncloud', @@ -305,12 +305,12 @@ EOF EOF # Set permissions - chown -R www-data:www-data $STORAGE_ROOT/owncloud /usr/local/lib/owncloud + chown -R www-data:www-data "$STORAGE_ROOT/owncloud" /usr/local/lib/owncloud # Execute Nextcloud's setup step, which creates the Nextcloud sqlite database. # 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$PHP_VER /usr/local/lib/owncloud/index.php;) + (cd /usr/local/lib/owncloud; sudo -u www-data php"$PHP_VER" /usr/local/lib/owncloud/index.php;) fi # Update config.php. @@ -326,7 +326,7 @@ fi # 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 < $CONFIG_TEMP && mv $CONFIG_TEMP $STORAGE_ROOT/owncloud/config.php; +php"$PHP_VER" < "$CONFIG_TEMP" && mv "$CONFIG_TEMP" "$STORAGE_ROOT/owncloud/config.php"; EOF -chown www-data:www-data $STORAGE_ROOT/owncloud/config.php +chown www-data:www-data "$STORAGE_ROOT/owncloud/config.php" # Enable/disable apps. Note that this must be done after the Nextcloud setup. # 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 +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 +sudo -u www-data php"$PHP_VER" /usr/local/lib/owncloud/occ upgrade if [ \( $? -ne 0 \) -a \( $? -ne 3 \) ]; then exit 1; fi # Disable default apps that we don't support sudo -u www-data \ - php$PHP_VER /usr/local/lib/owncloud/occ app:disable photos dashboard activity \ + php"$PHP_VER" /usr/local/lib/owncloud/occ app:disable photos dashboard activity \ | (grep -v "No such app enabled" || /bin/true) # 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 ';' \ +tools/editconf.py /etc/php/"$PHP_VER"/fpm/php.ini -c ';' \ upload_max_filesize=16G \ post_max_size=16G \ output_buffering=16384 \ @@ -390,7 +390,7 @@ tools/editconf.py /etc/php/$PHP_VER/fpm/php.ini -c ';' \ short_open_tag=On # Set Nextcloud recommended opcache settings -tools/editconf.py /etc/php/$PHP_VER/cli/conf.d/10-opcache.ini -c ';' \ +tools/editconf.py /etc/php/"$PHP_VER"/cli/conf.d/10-opcache.ini -c ';' \ opcache.enable=1 \ opcache.enable_cli=1 \ opcache.interned_strings_buffer=8 \ @@ -404,7 +404,7 @@ tools/editconf.py /etc/php/$PHP_VER/cli/conf.d/10-opcache.ini -c ';' \ # 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 +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 @@ -419,11 +419,11 @@ 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 +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 +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 </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/ diff --git a/setup/ssl.sh b/setup/ssl.sh index 9bd5d539..19a0c048 100755 --- a/setup/ssl.sh +++ b/setup/ssl.sh @@ -26,9 +26,9 @@ 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 + || [ ! -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 @@ -38,7 +38,7 @@ apt_install openssl # Create a directory to store TLS-related things like "SSL" certificates. -mkdir -p $STORAGE_ROOT/ssl +mkdir -p "$STORAGE_ROOT/ssl" # Generate a new private key. # @@ -60,39 +60,39 @@ mkdir -p $STORAGE_ROOT/ssl # # 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 +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 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 +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 $CSR \ + openssl req -new -key "$STORAGE_ROOT/ssl/ssl_private_key.pem" -out $CSR \ -sha256 -subj "/CN=$PRIMARY_HOSTNAME" # 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 $CSR -signkey $STORAGE_ROOT/ssl/ssl_private_key.pem -out $CERT + -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 + ln -s "$CERT" "$STORAGE_ROOT/ssl/ssl_certificate.pem" fi # 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 +if [ ! -f "$STORAGE_ROOT/ssl/dh2048.pem" ]; then + openssl dhparam -out "$STORAGE_ROOT/ssl/dh2048.pem" 2048 fi diff --git a/setup/start.sh b/setup/start.sh index 3abb4fe3..2a977340 100755 --- a/setup/start.sh +++ b/setup/start.sh @@ -75,17 +75,17 @@ fi # 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 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 +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 @@ -142,14 +142,14 @@ source setup/firstuser.sh # 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 +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 +certbot register --register-unsafely-without-email --agree-tos --config-dir "$STORAGE_ROOT/ssl/lets_encrypt" fi # Done. @@ -162,19 +162,19 @@ 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 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\ + 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 - openssl x509 -in $STORAGE_ROOT/ssl/ssl_certificate.pem -noout -fingerprint -sha256\ + openssl x509 -in "$STORAGE_ROOT/ssl/ssl_certificate.pem" -noout -fingerprint -sha256\ | sed "s/SHA256 Fingerprint=//i" echo echo Then you can confirm the security exception and continue. diff --git a/setup/system.sh b/setup/system.sh index 216c2cd8..b4d1ae27 100755 --- a/setup/system.sh +++ b/setup/system.sh @@ -11,8 +11,8 @@ source setup/functions.sh # load our functions # # First set the hostname in the configuration file, then activate the setting -echo $PRIMARY_HOSTNAME > /etc/hostname -hostname $PRIMARY_HOSTNAME +echo "$PRIMARY_HOSTNAME" > /etc/hostname +hostname "$PRIMARY_HOSTNAME" # ### Fix permissions @@ -53,8 +53,8 @@ if [ -z "$SWAP_IN_FSTAB" ] && [ ! -e /swapfile ] && [ -z "$ROOT_IS_BTRFS" ] && - [ $TOTAL_PHYSICAL_MEM -lt 1900000 ] && - [ $AVAILABLE_DISK_SPACE -gt 5242880 ] + [ "$TOTAL_PHYSICAL_MEM" -lt 1900000 ] && + [ "$AVAILABLE_DISK_SPACE" -gt 5242880 ] then echo "Adding a swap file to the system..." @@ -164,7 +164,7 @@ fi # not likely the user will want to change this, so we only ask on first # setup. if [ -z "${NONINTERACTIVE:-}" ]; then - if [ ! -f /etc/timezone ] || [ ! -z ${FIRST_TIME_SETUP:-} ]; then + if [ ! -f /etc/timezone ] || [ ! -z "${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. @@ -273,8 +273,8 @@ if [ -z "${DISABLE_FIREWALL:-}" ]; then if [ ! -z "$SSH_PORT" ]; then if [ "$SSH_PORT" != "22" ]; then - echo Opening alternate SSH port $SSH_PORT. #NODOC - ufw_limit $SSH_PORT #NODOC + echo Opening alternate SSH port "$SSH_PORT". #NODOC + ufw_limit "$SSH_PORT" #NODOC fi fi diff --git a/setup/web.sh b/setup/web.sh index 392b6d92..6969c6ce 100755 --- a/setup/web.sh +++ b/setup/web.sh @@ -19,7 +19,7 @@ fi echo "Installing Nginx (web server)..." -apt_install nginx php${PHP_VER}-cli php${PHP_VER}-fpm idn2 +apt_install nginx php"${PHP_VER}"-cli php"${PHP_VER}"-fpm idn2 rm -f /etc/nginx/sites-enabled/default @@ -46,15 +46,15 @@ tools/editconf.py /etc/nginx/nginx.conf -s \ ssl_protocols="TLSv1.2 TLSv1.3;" # Tell PHP not to expose its version number in the X-Powered-By header. -tools/editconf.py /etc/php/$PHP_VER/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/php/$PHP_VER/fpm/php.ini -c ';' \ +tools/editconf.py /etc/php/"$PHP_VER"/fpm/php.ini -c ';' \ default_charset="UTF-8" # Configure the path environment for php-fpm -tools/editconf.py /etc/php/$PHP_VER/fpm/pool.d/www.conf -c ';' \ +tools/editconf.py /etc/php/"$PHP_VER"/fpm/pool.d/www.conf -c ';' \ env[PATH]=/usr/local/bin:/usr/bin:/bin \ # Configure php-fpm based on the amount of memory the machine has @@ -62,32 +62,32 @@ tools/editconf.py /etc/php/$PHP_VER/fpm/pool.d/www.conf -c ';' \ # 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 ] +if [ "$TOTAL_PHYSICAL_MEM" -lt 1000000 ] then - tools/editconf.py /etc/php/$PHP_VER/fpm/pool.d/www.conf -c ';' \ + 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 ] +elif [ "$TOTAL_PHYSICAL_MEM" -lt 2000000 ] then - tools/editconf.py /etc/php/$PHP_VER/fpm/pool.d/www.conf -c ';' \ + 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 ] +elif [ "$TOTAL_PHYSICAL_MEM" -lt 3000000 ] then - tools/editconf.py /etc/php/$PHP_VER/fpm/pool.d/www.conf -c ';' \ + 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 ';' \ + tools/editconf.py /etc/php/"$PHP_VER"/fpm/pool.d/www.conf -c ';' \ pm=dynamic \ pm.max_children=120 \ pm.start_servers=12 \ @@ -138,16 +138,16 @@ cat conf/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 +chown -R "$STORAGE_USER" "$STORAGE_ROOT/www" # Start services. restart_service nginx -restart_service php$PHP_VER-fpm +restart_service php"$PHP_VER"-fpm # Open ports. ufw_allow http diff --git a/setup/webmail.sh b/setup/webmail.sh old mode 100755 new mode 100644 index 3cff1416..66a85596 --- a/setup/webmail.sh +++ b/setup/webmail.sh @@ -22,8 +22,8 @@ source /etc/mailinabox.conf # load global vars 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}"-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. @@ -170,8 +170,8 @@ cat > ${RCM_PLUGIN_DIR}/carddav/config.inc.php < /usr/sbin/z-push-admin - echo php$PHP_VER /usr/local/lib/z-push/z-push-admin.php '"$@"' >> /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 + 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 @@ -108,8 +108,8 @@ EOF # Restart service. -restart_service php$PHP_VER-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 +hide_output php"$PHP_VER" /usr/local/lib/z-push/z-push-admin.php -a fixstates diff --git a/tools/archive_conf_files.sh b/tools/archive_conf_files.sh index 299387ec..6efbccd5 100644 --- a/tools/archive_conf_files.sh +++ b/tools/archive_conf_files.sh @@ -2,8 +2,8 @@ # of the configuration files we edit with editconf.py. for fn in $(grep -hr editconf.py setup | sed "s/tools\/editconf.py //" | sed "s/ .*//" | sort | uniq); do echo ====================================================================== - echo $fn + echo "$fn" echo ====================================================================== - cat $fn + cat "$fn" done diff --git a/tools/dns_update b/tools/dns_update index 947c3dd1..2bbcce67 100755 --- a/tools/dns_update +++ b/tools/dns_update @@ -3,4 +3,4 @@ POSTDATA=dummy if [ "$1" == "--force" ]; then POSTDATA=force=1 fi -curl -s -d $POSTDATA --user $( Date: Thu, 21 Dec 2023 07:02:42 -0800 Subject: [PATCH 096/121] Fixed SC2164: Use 'cd ... || exit' in case cd fails. --- setup/bootstrap.sh | 2 +- setup/nextcloud.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/setup/bootstrap.sh b/setup/bootstrap.sh index 8824afe2..ace51879 100644 --- a/setup/bootstrap.sh +++ b/setup/bootstrap.sh @@ -74,7 +74,7 @@ if [ ! -d "$HOME/mailinabox" ]; then fi # Change directory to it. -cd "$HOME/mailinabox" +cd "$HOME/mailinabox" || exit # Update it. if [ "$TAG" != $(git describe --always) ]; then diff --git a/setup/nextcloud.sh b/setup/nextcloud.sh index d7b57d57..bba0ab65 100755 --- a/setup/nextcloud.sh +++ b/setup/nextcloud.sh @@ -310,7 +310,7 @@ EOF # Execute Nextcloud's setup step, which creates the Nextcloud sqlite database. # It also wipes it if it exists. And it updates config.php with database # settings and deletes the autoconfig.php file. - (cd /usr/local/lib/owncloud; sudo -u www-data php"$PHP_VER" /usr/local/lib/owncloud/index.php;) + (cd /usr/local/lib/owncloud || exit; sudo -u www-data php"$PHP_VER" /usr/local/lib/owncloud/index.php;) fi # Update config.php. From f1888f2043b0ff2a555ac885d2d81aa0ec5f86a9 Mon Sep 17 00:00:00 2001 From: Teal Dulcet Date: Thu, 21 Dec 2023 07:30:31 -0800 Subject: [PATCH 097/121] Fixed SC2148: Add a shebang. --- setup/firstuser.sh | 1 + setup/functions.sh | 1 + setup/network-checks.sh | 1 + setup/questions.sh | 1 + setup/system.sh | 1 + tools/archive_conf_files.sh | 1 + 6 files changed, 6 insertions(+) diff --git a/setup/firstuser.sh b/setup/firstuser.sh index 540c565f..8d254b53 100644 --- a/setup/firstuser.sh +++ b/setup/firstuser.sh @@ -1,3 +1,4 @@ +#!/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 diff --git a/setup/functions.sh b/setup/functions.sh index 8e8b79e2..5a153d2a 100644 --- a/setup/functions.sh +++ b/setup/functions.sh @@ -1,3 +1,4 @@ +#!/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. diff --git a/setup/network-checks.sh b/setup/network-checks.sh index 5d5351c7..16b9a175 100644 --- a/setup/network-checks.sh +++ b/setup/network-checks.sh @@ -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 diff --git a/setup/questions.sh b/setup/questions.sh index e7803363..2144d51f 100644 --- a/setup/questions.sh +++ b/setup/questions.sh @@ -1,3 +1,4 @@ +#!/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, diff --git a/setup/system.sh b/setup/system.sh index b4d1ae27..59a3fe8e 100755 --- a/setup/system.sh +++ b/setup/system.sh @@ -1,3 +1,4 @@ +#!/bin/bash source /etc/mailinabox.conf source setup/functions.sh # load our functions diff --git a/tools/archive_conf_files.sh b/tools/archive_conf_files.sh index 6efbccd5..d2ec2c9a 100644 --- a/tools/archive_conf_files.sh +++ b/tools/archive_conf_files.sh @@ -1,3 +1,4 @@ +#!/bin/bash # Use this script to make an archive of the contents of all # of the configuration files we edit with editconf.py. for fn in $(grep -hr editconf.py setup | sed "s/tools\/editconf.py //" | sed "s/ .*//" | sort | uniq); do From bdf4155bedccb36117a41bd7d0952c1559e4da67 Mon Sep 17 00:00:00 2001 From: Teal Dulcet Date: Thu, 21 Dec 2023 08:00:26 -0800 Subject: [PATCH 098/121] Fixed SC2046: Quote to prevent word splitting. --- management/daily_tasks.sh | 2 +- setup/bootstrap.sh | 2 +- setup/firstuser.sh | 2 +- setup/mail-dovecot.sh | 4 ++-- setup/nextcloud.sh | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/management/daily_tasks.sh b/management/daily_tasks.sh index 0f7bcdc8..24a97245 100755 --- a/management/daily_tasks.sh +++ b/management/daily_tasks.sh @@ -11,7 +11,7 @@ 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 +if [ "$(date "+%u")" -eq 1 ]; then management/mail_log.py -t week | management/email_administrator.py "Mail-in-a-Box Usage Report" fi diff --git a/setup/bootstrap.sh b/setup/bootstrap.sh index ace51879..b8b365c4 100644 --- a/setup/bootstrap.sh +++ b/setup/bootstrap.sh @@ -77,7 +77,7 @@ fi cd "$HOME/mailinabox" || exit # Update it. -if [ "$TAG" != $(git describe --always) ]; then +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 diff --git a/setup/firstuser.sh b/setup/firstuser.sh index 8d254b53..76514222 100644 --- a/setup/firstuser.sh +++ b/setup/firstuser.sh @@ -11,7 +11,7 @@ if [ -z "$(management/cli.py user)" ]; 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 diff --git a/setup/mail-dovecot.sh b/setup/mail-dovecot.sh index 39b22515..734e7ed3 100755 --- a/setup/mail-dovecot.sh +++ b/setup/mail-dovecot.sh @@ -45,8 +45,8 @@ apt_install \ # - 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_vsz_limit=$(echo "$(free -tm | tail -1 | awk '{print $2}') / 3" | bc)M \ + default_process_limit="$(echo "$(nproc) * 250" | bc)" \ + default_vsz_limit="$(echo "$(free -tm | tail -1 | awk '{print $2}') / 3" | bc)M" \ log_path=/var/log/mail.log # The inotify `max_user_instances` default is 128, which constrains diff --git a/setup/nextcloud.sh b/setup/nextcloud.sh index bba0ab65..b6183789 100755 --- a/setup/nextcloud.sh +++ b/setup/nextcloud.sh @@ -126,7 +126,7 @@ InstallNextcloud() { # 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" + 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 From 67bcaea71e8d59e34787f5fd708ab4f664967e9d Mon Sep 17 00:00:00 2001 From: Teal Dulcet Date: Thu, 21 Dec 2023 08:03:54 -0800 Subject: [PATCH 099/121] Fixed SC2091: Remove surrounding $() to avoid executing output. --- setup/management.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup/management.sh b/setup/management.sh index cf2c4995..955f88b9 100755 --- a/setup/management.sh +++ b/setup/management.sh @@ -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 From 4b7d4ba0a6a8b96e7ee38077c53636adf514de93 Mon Sep 17 00:00:00 2001 From: Teal Dulcet Date: Thu, 21 Dec 2023 08:05:18 -0800 Subject: [PATCH 100/121] Fixed SC2166: Prefer [ p ] && [ q ] as [ p -a q ] is not well defined. --- setup/nextcloud.sh | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/setup/nextcloud.sh b/setup/nextcloud.sh index b6183789..446085c2 100755 --- a/setup/nextcloud.sh +++ b/setup/nextcloud.sh @@ -139,10 +139,12 @@ InstallNextcloud() { # 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 - if [ \( $? -ne 0 \) -a \( $? -ne 3 \) ]; then + 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 - if [ \( $? -ne 0 \) -a \( $? -ne 3 \) ]; then exit 1; fi + 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 @@ -372,7 +374,8 @@ hide_output sudo -u www-data php"$PHP_VER" /usr/local/lib/owncloud/console.php a # 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 -if [ \( $? -ne 0 \) -a \( $? -ne 3 \) ]; then exit 1; fi +E=$? +if [ $E -ne 0 ] && [ $E -ne 3 ]; then exit 1; fi # Disable default apps that we don't support sudo -u www-data \ From 44d9f6eebd14e150b6f17a4cdb7f25470b092485 Mon Sep 17 00:00:00 2001 From: Teal Dulcet Date: Thu, 21 Dec 2023 08:06:26 -0800 Subject: [PATCH 101/121] Fixed SC2236: Use -n instead of ! -z. --- setup/nextcloud.sh | 2 +- setup/questions.sh | 6 +++--- setup/system.sh | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/setup/nextcloud.sh b/setup/nextcloud.sh index 446085c2..1fb19ade 100755 --- a/setup/nextcloud.sh +++ b/setup/nextcloud.sh @@ -194,7 +194,7 @@ if [ ! -d /usr/local/lib/owncloud/ ] || [[ ! ${CURRENT_NEXTCLOUD_VER} =~ ^$nextc fi # If ownCloud or Nextcloud was previously installed.... - if [ ! -z "${CURRENT_NEXTCLOUD_VER}" ]; then + if [ -n "${CURRENT_NEXTCLOUD_VER}" ]; then # Database migrations from ownCloud are no longer possible because ownCloud cannot be run under # PHP 7. diff --git a/setup/questions.sh b/setup/questions.sh index 2144d51f..dd62d44a 100644 --- a/setup/questions.sh +++ b/setup/questions.sh @@ -93,7 +93,7 @@ if [ -z "${PUBLIC_IP:-}" ]; then # 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. @@ -126,7 +126,7 @@ 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 # No IPv6 entered and machine seems to have none, or what @@ -198,7 +198,7 @@ fi 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 diff --git a/setup/system.sh b/setup/system.sh index 59a3fe8e..b49f0120 100755 --- a/setup/system.sh +++ b/setup/system.sh @@ -165,7 +165,7 @@ fi # not likely the user will want to change this, so we only ask on first # setup. if [ -z "${NONINTERACTIVE:-}" ]; then - if [ ! -f /etc/timezone ] || [ ! -z "${FIRST_TIME_SETUP:-}" ]; 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. @@ -271,7 +271,7 @@ if [ -z "${DISABLE_FIREWALL:-}" ]; then # 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 From 27cf11d8ec6f554749da60ae781c2668612efe35 Mon Sep 17 00:00:00 2001 From: Teal Dulcet Date: Thu, 21 Dec 2023 08:08:32 -0800 Subject: [PATCH 102/121] Fixed SC2005: Useless echo. --- setup/questions.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup/questions.sh b/setup/questions.sh index dd62d44a..c18ad386 100644 --- a/setup/questions.sh +++ b/setup/questions.sh @@ -32,7 +32,7 @@ if [ -z "${PRIMARY_HOSTNAME:-}" ]; then # 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. From 2afd0451c13a03bb84fc947cacf8f1929c1d6006 Mon Sep 17 00:00:00 2001 From: Teal Dulcet Date: Thu, 21 Dec 2023 08:10:35 -0800 Subject: [PATCH 103/121] Fixed SC2007: Use $((..)) instead of deprecated $[..]. --- setup/system.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup/system.sh b/setup/system.sh index b49f0120..cc543e79 100755 --- a/setup/system.sh +++ b/setup/system.sh @@ -61,7 +61,7 @@ then # 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 + dd if=/dev/zero of=/swapfile bs=1024 count=$((1024*1024)) status=none if [ -e /swapfile ]; then chmod 600 /swapfile hide_output mkswap /swapfile From 3399b250845bc1f4303a3a9715bdd7c2e449a2b6 Mon Sep 17 00:00:00 2001 From: Teal Dulcet Date: Thu, 21 Dec 2023 08:18:43 -0800 Subject: [PATCH 104/121] Replaced the pwd command with Bash's $PWD variable. --- setup/bootstrap.sh | 2 +- setup/dns.sh | 2 +- setup/management.sh | 4 ++-- setup/munin.sh | 2 +- setup/start.sh | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/setup/bootstrap.sh b/setup/bootstrap.sh index b8b365c4..81b0eec7 100644 --- a/setup/bootstrap.sh +++ b/setup/bootstrap.sh @@ -81,7 +81,7 @@ 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)?" + echo "Update failed. Did you modify something in $PWD?" exit 1 fi echo diff --git a/setup/dns.sh b/setup/dns.sh index 2501795a..a94e890d 100755 --- a/setup/dns.sh +++ b/setup/dns.sh @@ -142,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 diff --git a/setup/management.sh b/setup/management.sh index 955f88b9..7fd844ea 100755 --- a/setup/management.sh +++ b/setup/management.sh @@ -100,7 +100,7 @@ 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 +export PYTHONPATH=$PWD/management exec gunicorn -b localhost:10222 -w 1 --timeout 630 wsgi:app EOF chmod +x $inst_dir/start @@ -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 3 * * * root (cd $PWD && management/daily_tasks.sh) EOF # Start the management server. diff --git a/setup/munin.sh b/setup/munin.sh index 56cdb391..017862de 100755 --- a/setup/munin.sh +++ b/setup/munin.sh @@ -62,7 +62,7 @@ done 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 +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 diff --git a/setup/start.sh b/setup/start.sh index 2a977340..7d1f542e 100755 --- a/setup/start.sh +++ b/setup/start.sh @@ -46,7 +46,7 @@ fi # 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 From 55a8be4aa97508c3e7c453da06d8dcbe6b0e7993 Mon Sep 17 00:00:00 2001 From: Teal Dulcet Date: Thu, 21 Dec 2023 08:29:24 -0800 Subject: [PATCH 105/121] Removed unnecessary bc commands. --- setup/mail-dovecot.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup/mail-dovecot.sh b/setup/mail-dovecot.sh index 734e7ed3..b146e44a 100755 --- a/setup/mail-dovecot.sh +++ b/setup/mail-dovecot.sh @@ -45,8 +45,8 @@ apt_install \ # - 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_vsz_limit="$(echo "$(free -tm | tail -1 | awk '{print $2}') / 3" | bc)M" \ + 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 From ec497efa69e9cbad466519c21e5cdaa4ea6fd57f Mon Sep 17 00:00:00 2001 From: Teal Dulcet Date: Thu, 21 Dec 2023 08:44:22 -0800 Subject: [PATCH 106/121] Quote echo commands to preserve whitespace. --- setup/bootstrap.sh | 2 +- setup/dkim.sh | 2 +- setup/functions.sh | 2 +- setup/questions.sh | 2 +- setup/start.sh | 12 ++++++------ setup/system.sh | 8 ++++---- setup/web.sh | 4 ++-- tools/owncloud-unlockadmin.sh | 14 +++++++------- 8 files changed, 23 insertions(+), 23 deletions(-) diff --git a/setup/bootstrap.sh b/setup/bootstrap.sh index 81b0eec7..58c9243e 100644 --- a/setup/bootstrap.sh +++ b/setup/bootstrap.sh @@ -53,7 +53,7 @@ fi # Clone the Mail-in-a-Box repository if it doesn't exist. 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 diff --git a/setup/dkim.sh b/setup/dkim.sh index 0001d4c4..e4def11f 100755 --- a/setup/dkim.sh +++ b/setup/dkim.sh @@ -10,7 +10,7 @@ source setup/functions.sh # load our functions source /etc/mailinabox.conf # load global vars # Install DKIM... -echo Installing OpenDKIM/OpenDMARC... +echo "Installing OpenDKIM/OpenDMARC..." apt_install opendkim opendkim-tools opendmarc # Make sure configuration directories exist. diff --git a/setup/functions.sh b/setup/functions.sh index 5a153d2a..6a96cc1f 100644 --- a/setup/functions.sh +++ b/setup/functions.sh @@ -25,7 +25,7 @@ function hide_output { if [ $E != 0 ]; then # Something failed. echo - echo FAILED: "$@" + echo "FAILED: $*" echo ----------------------------------------- cat "$OUTPUT" echo ----------------------------------------- diff --git a/setup/questions.sh b/setup/questions.sh index c18ad386..34346595 100644 --- a/setup/questions.sh +++ b/setup/questions.sh @@ -8,7 +8,7 @@ if [ -z "${NONINTERACTIVE:-}" ]; then # # 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 diff --git a/setup/start.sh b/setup/start.sh index 7d1f542e..dbf1c16c 100755 --- a/setup/start.sh +++ b/setup/start.sh @@ -122,7 +122,7 @@ source setup/munin.sh # 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 @@ -156,9 +156,9 @@ fi 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. @@ -171,12 +171,12 @@ if management/status_checks.py --check-primary-hostname; then else 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 "Then you can confirm the security exception and continue." echo fi diff --git a/setup/system.sh b/setup/system.sh index cc543e79..c0959510 100755 --- a/setup/system.sh +++ b/setup/system.sh @@ -111,7 +111,7 @@ hide_output add-apt-repository --y ppa:ondrej/php # 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 @@ -136,7 +136,7 @@ apt_get_quiet autoremove # * bc: allows us to do math to compute sane defaults # * openssh-client: provides ssh-keygen -echo Installing system packages... +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 \ @@ -227,7 +227,7 @@ fi # 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... +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 @@ -274,7 +274,7 @@ if [ -z "${DISABLE_FIREWALL:-}" ]; then if [ -n "$SSH_PORT" ]; then if [ "$SSH_PORT" != "22" ]; then - echo Opening alternate SSH port "$SSH_PORT". #NODOC + echo "Opening alternate SSH port $SSH_PORT." #NODOC ufw_limit "$SSH_PORT" #NODOC fi diff --git a/setup/web.sh b/setup/web.sh index 6969c6ce..acf879a1 100755 --- a/setup/web.sh +++ b/setup/web.sh @@ -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 @@ -124,7 +124,7 @@ 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: +# 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 diff --git a/tools/owncloud-unlockadmin.sh b/tools/owncloud-unlockadmin.sh index 14a41464..8fcaf593 100755 --- a/tools/owncloud-unlockadmin.sh +++ b/tools/owncloud-unlockadmin.sh @@ -9,15 +9,15 @@ source /etc/mailinabox.conf # load global vars ADMIN=$(./mail.py user admins | head -n 1) -test -z "$1" || ADMIN=$1 +test -z "$1" || ADMIN=$1 echo "I am going to unlock admin features for $ADMIN." -echo You can provide another user to unlock as the first argument of this script. +echo "You can provide another user to unlock as the first argument of this script." echo -echo WARNING: you could break mail-in-a-box when fiddling around with Nextcloud\'s admin interface -echo If in doubt, press CTRL-C to cancel. -echo -echo Press enter to continue. +echo "WARNING: you could break mail-in-a-box when fiddling around with Nextcloud's admin interface" +echo "If in doubt, press CTRL-C to cancel." +echo +echo "Press enter to continue." read -sudo -u www-data "php$PHP_VER" /usr/local/lib/owncloud/occ group:adduser admin "$ADMIN" && echo Done. +sudo -u www-data "php$PHP_VER" /usr/local/lib/owncloud/occ group:adduser admin "$ADMIN" && echo "Done." From c7faccf1fa9fc9deb6051e96acb13bb120b50c49 Mon Sep 17 00:00:00 2001 From: Teal Dulcet Date: Thu, 21 Dec 2023 08:49:20 -0800 Subject: [PATCH 107/121] Fixed SC2244: Prefer explicit -n to check non-empty string. --- setup/questions.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup/questions.sh b/setup/questions.sh index 34346595..5c182dcc 100644 --- a/setup/questions.sh +++ b/setup/questions.sh @@ -145,7 +145,7 @@ if [ -z "${PUBLIC_IPV6:-}" ]; then "${DEFAULT_PUBLIC_IPV6:-}" \ PUBLIC_IPV6 - if [ ! "$PUBLIC_IPV6_EXITCODE" ]; then + if [ ! -n "$PUBLIC_IPV6_EXITCODE" ]; then # user hit ESC/cancel exit fi From 30d78cd35a33bb68324e25c7e72ee3048ec46330 Mon Sep 17 00:00:00 2001 From: matidau <65836048+matidau@users.noreply.github.com> Date: Tue, 18 Jun 2024 22:29:22 +1000 Subject: [PATCH 108/121] Update zpush.sh to version 2.7.3 (#2390) --- setup/zpush.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup/zpush.sh b/setup/zpush.sh index e46d72f1..2b24a605 100755 --- a/setup/zpush.sh +++ b/setup/zpush.sh @@ -22,8 +22,8 @@ apt_install \ phpenmod -v "$PHP_VER" imap # Copy Z-Push into place. -VERSION=2.7.1 -TARGETHASH=f15c566b1ad50de24f3f08f505f0c3d8155c2d0d +VERSION=2.7.3 +TARGETHASH=9d4bec41935e9a4e07880c5ff915bcddbda4443b needs_update=0 #NODOC if [ ! -f /usr/local/lib/z-push/version ]; then needs_update=1 #NODOC From 6321ce6ef0876bef6124d13fd068268b1856517c Mon Sep 17 00:00:00 2001 From: matidau <65836048+matidau@users.noreply.github.com> Date: Tue, 18 Jun 2024 22:29:51 +1000 Subject: [PATCH 109/121] Add php8.0-intl package to z-push setup (#2389) --- setup/zpush.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup/zpush.sh b/setup/zpush.sh index 2b24a605..3b14c047 100755 --- a/setup/zpush.sh +++ b/setup/zpush.sh @@ -17,7 +17,7 @@ source /etc/mailinabox.conf # load global vars echo "Installing Z-Push (Exchange/ActiveSync server)..." apt_install \ - php"${PHP_VER}"-soap php"${PHP_VER}"-imap libawl-php php"$PHP_VER"-xml + php"${PHP_VER}"-soap php"${PHP_VER}"-imap libawl-php php"$PHP_VER"-xml php"${PHP_VER}"-intl phpenmod -v "$PHP_VER" imap From 8b9f0489c8cb412458376089a0b59aa30309325b Mon Sep 17 00:00:00 2001 From: John James Jacoby Date: Tue, 18 Jun 2024 07:32:11 -0500 Subject: [PATCH 110/121] Add custom.yaml support for WebSockets (#2385) Fixes #1956. --- management/web_update.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/management/web_update.py b/management/web_update.py index a0f0f799..c31fe8fc 100644 --- a/management/web_update.py +++ b/management/web_update.py @@ -166,6 +166,7 @@ def make_domain_config(domain, templates, ssl_certificates, env): pass_http_host_header = False proxy_redirect_off = False frame_options_header_sameorigin = False + web_sockets = False m = re.search("#(.*)$", url) if m: for flag in m.group(1).split(","): @@ -175,6 +176,8 @@ def make_domain_config(domain, templates, ssl_certificates, env): proxy_redirect_off = True elif flag == "frame-options-sameorigin": frame_options_header_sameorigin = True + elif flag == "web-sockets": + web_sockets = True url = re.sub("#(.*)$", "", url) nginx_conf_extra += "\tlocation %s {" % path @@ -185,6 +188,10 @@ def make_domain_config(domain, templates, ssl_certificates, env): 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;" + if web_sockets: + nginx_conf_extra += "\n\t\tproxy_http_version 1.1;" + nginx_conf_extra += "\n\t\tproxy_set_header Upgrade $http_upgrade;" + nginx_conf_extra += "\n\t\tproxy_set_header Connection 'Upgrade';" 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;" From 4dd1e75ee7100c3eb5c8937595cb019f78f52566 Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 18 Jun 2024 22:35:54 +1000 Subject: [PATCH 111/121] Allow for `Union[None, List[datetime.datetime]]` values when printing user table in weekly mail logs (#2378) * Fix - Allow for `Union[None, List[datetime.datetime]]` when printing user tables for the weekly mail logs. * Add - ruff suppressions. --- management/mail_log.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/management/mail_log.py b/management/mail_log.py index e127af3b..ac07b0da 100755 --- a/management/mail_log.py +++ b/management/mail_log.py @@ -679,7 +679,7 @@ def print_user_table(users, data=None, sub_data=None, activity=None, latest=None data_accum[col] += d[row] try: - if None not in {latest, earliest}: + if None not in [latest, earliest]: # noqa PLR6201 vert_pos = len(line) e = earliest[row] l = latest[row] @@ -732,7 +732,7 @@ def print_user_table(users, data=None, sub_data=None, activity=None, latest=None else: header += l.rjust(max(5, len(l) + 1, col_widths[col])) - if None not in {latest, earliest}: + if None not in [latest, earliest]: # noqa PLR6201 header += " ā”‚ timespan " lines.insert(0, header.rstrip()) @@ -757,7 +757,7 @@ def print_user_table(users, data=None, sub_data=None, activity=None, latest=None footer += temp.format(data_accum[row]) try: - if None not in {latest, earliest}: + if None not in [latest, earliest]: # noqa PLR6201 max_l = max(latest) min_e = min(earliest) timespan = relativedelta(max_l, min_e) From de0fc796d43f2fea0655036c16b9aa0f594f340e Mon Sep 17 00:00:00 2001 From: jvolkenant Date: Tue, 18 Jun 2024 05:37:01 -0700 Subject: [PATCH 112/121] Fix chown during Nexcloud upgrades (#2377) --- setup/nextcloud.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup/nextcloud.sh b/setup/nextcloud.sh index 1fb19ade..a6137fa1 100755 --- a/setup/nextcloud.sh +++ b/setup/nextcloud.sh @@ -131,7 +131,7 @@ InstallNextcloud() { # 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 + 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). From f118a6c0bfc40c602118bfc923e226f31acd2117 Mon Sep 17 00:00:00 2001 From: Michael Heuberger Date: Tue, 9 Jul 2024 00:21:12 +1200 Subject: [PATCH 113/121] Apply small Nextcloud upgrade to 26.0.13 (#2401) --- setup/nextcloud.sh | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/setup/nextcloud.sh b/setup/nextcloud.sh index a6137fa1..b2fd32fe 100755 --- a/setup/nextcloud.sh +++ b/setup/nextcloud.sh @@ -21,8 +21,8 @@ echo "Installing Nextcloud (contacts/calendar)..." # 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_ver=26.0.13 +nextcloud_hash=d5c10b650e5396d5045131c6d22c02a90572527c # Nextcloud apps # -------------- @@ -40,12 +40,12 @@ 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 +calendar_ver=4.7.6 +calendar_hash=a995bca4effeecb2cab25f3bbeac9bfe05fee766 # Always ensure the versions are supported, see https://apps.nextcloud.com/apps/user_external -user_external_ver=3.2.0 -user_external_hash=a494073dcdecbbbc79a9c77f72524ac9994d2eec +user_external_ver=3.3.0 +user_external_hash=280d24eb2a6cb56b4590af8847f925c28d8d853e # Developer advice (test plan) # ---------------------------- From 2f5e736fa0714cc9870dcc284bc26811e4a0148f Mon Sep 17 00:00:00 2001 From: Joshua Tauberer Date: Wed, 15 May 2024 12:36:51 -0400 Subject: [PATCH 114/121] Clean up Nextcloud email settings for calendar invitations I don't think anything is actually changed here but I think my box was missing some of these settings. --- setup/nextcloud.sh | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/setup/nextcloud.sh b/setup/nextcloud.sh index b2fd32fe..496ca0c0 100755 --- a/setup/nextcloud.sh +++ b/setup/nextcloud.sh @@ -274,15 +274,6 @@ if [ ! -f "$STORAGE_ROOT/owncloud/owncloud.db" ]; then ), ), '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 @@ -338,13 +329,10 @@ include("$STORAGE_ROOT/owncloud/config.php"); \$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', @@ -354,6 +342,16 @@ include("$STORAGE_ROOT/owncloud/config.php"); ), ); +\$CONFIG['mail_domain'] = '$PRIMARY_HOSTNAME'; +\$CONFIG['mail_from_address'] = 'administrator'; # just the local part, matches the required administrator alias on mail_domain/$PRIMARY_HOSTNAME +\$CONFIG['mail_smtpmode'] = 'sendmail'; +\$CONFIG['mail_smtpauth'] = true; # if smtpmode is smtp +\$CONFIG['mail_smtphost'] = '127.0.0.1'; # if smtpmode is smtp +\$CONFIG['mail_smtpport'] = '587'; # if smtpmode is smtp +\$CONFIG['mail_smtpsecure'] = ''; # if smtpmode is smtp, must be empty string +\$CONFIG['mail_smtpname'] = ''; # if smtpmode is smtp, set this to a mail user +\$CONFIG['mail_smtppassword'] = ''; # if smtpmode is smtp, set this to the user's password + echo " Date: Sat, 20 Jul 2024 07:33:07 -0400 Subject: [PATCH 115/121] v69 --- CHANGELOG.md | 16 +++++++++++++++- README.md | 2 +- setup/bootstrap.sh | 2 +- 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b048d219..4841ee14 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,21 @@ CHANGELOG ========= +Version 69 (July 20, 2024) +-------------------------- + +Package updates: + +* Nextcloud is updated to 26.0.13. +* Z-Push is updated to 2.7.3. + +Other updates: + +* Fixed an error generating the weekly statistics. +* Fixed file permissions when setting up Nextcloud. +* Added an undocumented option to proxy websockets. +* Internal improvements to the code to make it more reliable and readable. + Version 68 (April 1, 2024) -------------------------- @@ -25,7 +40,6 @@ Other: * fail2ban is updated to see "HTTP/2.0" requests to munin also. * Internal improvements to the code to make it more reliable and readable. - Version 67 (December 22, 2023) ------------------------------ diff --git a/README.md b/README.md index 0b3d320b..2047d48f 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,7 @@ Clone this repository and checkout the tag corresponding to the most recent rele $ git clone https://github.com/mail-in-a-box/mailinabox $ cd mailinabox - $ git checkout v68 + $ git checkout v69 Begin the installation. diff --git a/setup/bootstrap.sh b/setup/bootstrap.sh index 58c9243e..8ee9882c 100644 --- a/setup/bootstrap.sh +++ b/setup/bootstrap.sh @@ -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=v68 + TAG=v69 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. From 2803d8889454fa3a8295c0cfd382cdbe996284d9 Mon Sep 17 00:00:00 2001 From: KiekerJan Date: Sat, 20 Jul 2024 22:44:24 +0200 Subject: [PATCH 116/121] increase timeout for the nginx proxy that provides access to the Mailinabox management daemon (#2407) --- conf/nginx-primaryonly.conf | 1 + 1 file changed, 1 insertion(+) diff --git a/conf/nginx-primaryonly.conf b/conf/nginx-primaryonly.conf index 9ed37043..8b7dd6bb 100644 --- a/conf/nginx-primaryonly.conf +++ b/conf/nginx-primaryonly.conf @@ -8,6 +8,7 @@ rewrite ^/admin/munin$ /admin/munin/ redirect; location /admin/ { proxy_pass http://127.0.0.1:10222/; + proxy_read_timeout 600s; proxy_set_header X-Forwarded-For $remote_addr; add_header X-Frame-Options "DENY"; add_header X-Content-Type-Options nosniff; From cd959bc52263935930f5175b7c5a13215ffae205 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Viktor=20Sz=C3=A9pe?= Date: Sun, 21 Jul 2024 13:01:25 +0200 Subject: [PATCH 117/121] Fix typos (#2406) --- CHANGELOG.md | 22 +++++++++++----------- CODE_OF_CONDUCT.md | 4 ++-- conf/dovecot-mailboxes.conf | 2 +- conf/fail2ban/jails.conf | 2 +- conf/nginx-alldomains.conf | 2 +- conf/nginx-primaryonly.conf | 2 +- management/backup.py | 2 +- management/dns_update.py | 4 ++-- management/mail_log.py | 2 +- management/mailconfig.py | 2 +- management/ssl_certificates.py | 2 +- management/templates/index.html | 2 +- management/templates/system-backup.html | 2 +- setup/dkim.sh | 2 +- setup/dns.sh | 2 +- setup/firstuser.sh | 2 +- setup/mail-postfix.sh | 10 +++++----- setup/mail-users.sh | 2 +- setup/migrate.py | 2 +- setup/questions.sh | 2 +- setup/spamassassin.sh | 2 +- setup/system.sh | 6 +++--- setup/web.sh | 2 +- tests/test_dns.py | 2 +- 24 files changed, 42 insertions(+), 42 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4841ee14..d43789c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -202,7 +202,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 +227,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 +260,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 +479,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 +543,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 +871,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 +887,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 +930,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 +1023,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 +1037,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 +1069,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) -------------------- diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 2efdfdb9..fa1f29f5 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -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. diff --git a/conf/dovecot-mailboxes.conf b/conf/dovecot-mailboxes.conf index 58e2efed..a5fbbb47 100644 --- a/conf/dovecot-mailboxes.conf +++ b/conf/dovecot-mailboxes.conf @@ -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 diff --git a/conf/fail2ban/jails.conf b/conf/fail2ban/jails.conf index c1514b45..0dc0912b 100644 --- a/conf/fail2ban/jails.conf +++ b/conf/fail2ban/jails.conf @@ -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] diff --git a/conf/nginx-alldomains.conf b/conf/nginx-alldomains.conf index 4c81e3f3..c238bab2 100644 --- a/conf/nginx-alldomains.conf +++ b/conf/nginx-alldomains.conf @@ -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; diff --git a/conf/nginx-primaryonly.conf b/conf/nginx-primaryonly.conf index 8b7dd6bb..b361a7b2 100644 --- a/conf/nginx-primaryonly.conf +++ b/conf/nginx-primaryonly.conf @@ -39,7 +39,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) diff --git a/management/backup.py b/management/backup.py index f352fb41..aae6c00b 100755 --- a/management/backup.py +++ b/management/backup.py @@ -15,7 +15,7 @@ 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 { } diff --git a/management/dns_update.py b/management/dns_update.py index 599f27b1..186e14a5 100755 --- a/management/dns_update.py +++ b/management/dns_update.py @@ -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. diff --git a/management/mail_log.py b/management/mail_log.py index ac07b0da..793fec09 100755 --- a/management/mail_log.py +++ b/management/mail_log.py @@ -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ā”‚ ') diff --git a/management/mailconfig.py b/management/mailconfig.py index 39ca1b6e..e623eace 100755 --- a/management/mailconfig.py +++ b/management/mailconfig.py @@ -561,7 +561,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: diff --git a/management/ssl_certificates.py b/management/ssl_certificates.py index ab0cea4d..c9f1126c 100755 --- a/management/ssl_certificates.py +++ b/management/ssl_certificates.py @@ -434,7 +434,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. diff --git a/management/templates/index.html b/management/templates/index.html index 3c6d449e..a468ffb9 100644 --- a/management/templates/index.html +++ b/management/templates/index.html @@ -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; } diff --git a/management/templates/system-backup.html b/management/templates/system-backup.html index 0d948bf0..3adea5b6 100644 --- a/management/templates/system-backup.html +++ b/management/templates/system-backup.html @@ -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); diff --git a/setup/dkim.sh b/setup/dkim.sh index e4def11f..77d996ab 100755 --- a/setup/dkim.sh +++ b/setup/dkim.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. diff --git a/setup/dns.sh b/setup/dns.sh index a94e890d..f2ad3a49 100755 --- a/setup/dns.sh +++ b/setup/dns.sh @@ -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 diff --git a/setup/firstuser.sh b/setup/firstuser.sh index 76514222..91e53d9f 100644 --- a/setup/firstuser.sh +++ b/setup/firstuser.sh @@ -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. diff --git a/setup/mail-postfix.sh b/setup/mail-postfix.sh index 49dfaafa..7b642a2a 100755 --- a/setup/mail-postfix.sh +++ b/setup/mail-postfix.sh @@ -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 @@ -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 diff --git a/setup/mail-users.sh b/setup/mail-users.sh index 0c6be0c8..25a21c41 100755 --- a/setup/mail-users.sh +++ b/setup/mail-users.sh @@ -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 diff --git a/setup/migrate.py b/setup/migrate.py index 066e0e03..94bea923 100755 --- a/setup/migrate.py +++ b/setup/migrate.py @@ -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() diff --git a/setup/questions.sh b/setup/questions.sh index 5c182dcc..600b5cf3 100644 --- a/setup/questions.sh +++ b/setup/questions.sh @@ -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 diff --git a/setup/spamassassin.sh b/setup/spamassassin.sh index e261cf32..d71a8efd 100755 --- a/setup/spamassassin.sh +++ b/setup/spamassassin.sh @@ -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 diff --git a/setup/system.sh b/setup/system.sh index c0959510..fac50df7 100755 --- a/setup/system.sh +++ b/setup/system.sh @@ -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 @@ -218,7 +218,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. diff --git a/setup/web.sh b/setup/web.sh index acf879a1..3aafcd88 100755 --- a/setup/web.sh +++ b/setup/web.sh @@ -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-* diff --git a/tests/test_dns.py b/tests/test_dns.py index 53fdb545..29b681cc 100755 --- a/tests/test_dns.py +++ b/tests/test_dns.py @@ -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() From bc14e80b12aa612db950cb9b7412216e5569c00d Mon Sep 17 00:00:00 2001 From: Teal Dulcet Date: Sun, 21 Jul 2024 05:00:20 -0700 Subject: [PATCH 118/121] Fix no password prompt. Fixes #2408 (#2409) --- setup/firstuser.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup/firstuser.sh b/setup/firstuser.sh index 91e53d9f..b6b1b3c8 100644 --- a/setup/firstuser.sh +++ b/setup/firstuser.sh @@ -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" From 2ae8cd57130df86722e2c6896f78bd52dbe8fb89 Mon Sep 17 00:00:00 2001 From: Joshua Tauberer Date: Sun, 21 Jul 2024 08:02:38 -0400 Subject: [PATCH 119/121] v69a --- CHANGELOG.md | 2 ++ README.md | 4 ++-- setup/bootstrap.sh | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d43789c9..b04be2f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,8 @@ Other updates: * Added an undocumented option to proxy websockets. * Internal improvements to the code to make it more reliable and readable. +Version 69a released on July 21, 2024 corrects a setup failure. + Version 68 (April 1, 2024) -------------------------- diff --git a/README.md b/README.md index 2047d48f..3f8e2b7e 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/setup/bootstrap.sh b/setup/bootstrap.sh index 8ee9882c..d18cd8b8 100644 --- a/setup/bootstrap.sh +++ b/setup/bootstrap.sh @@ -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=v69a 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. From 60a2b58e5735308c1f6f3292271948f8730b9559 Mon Sep 17 00:00:00 2001 From: Joshua Tauberer Date: Tue, 23 Jul 2024 06:15:18 -0400 Subject: [PATCH 120/121] Revert "Fixed SC2091: Remove surrounding $() to avoid executing output." and fix it another way This reverts commit 67bcaea71e8d59e34787f5fd708ab4f664967e9d. The sub-shell was required to prevent the updated umask from affecting later steps. It broke the permissions of the fetched assets for the control panel: https://discourse.mailinabox.email/t/admin-panel-broken-after-restore-upgrade/12112/24 Instead, the `$()` is replaced with just `()` to create a subshell without executing its output. --- setup/management.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup/management.sh b/setup/management.sh index 7fd844ea..fb359cd3 100755 --- a/setup/management.sh +++ b/setup/management.sh @@ -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 From 162e509b8bf0683a70cf92fb8b7623c0dba5d051 Mon Sep 17 00:00:00 2001 From: Joshua Tauberer Date: Tue, 23 Jul 2024 06:20:34 -0400 Subject: [PATCH 121/121] v69b --- CHANGELOG.md | 2 +- setup/bootstrap.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b04be2f3..f27aff7c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,7 +16,7 @@ Other updates: * Added an undocumented option to proxy websockets. * Internal improvements to the code to make it more reliable and readable. -Version 69a released on July 21, 2024 corrects a setup failure. +Version 69a (July 21, 2024) and 69b (July 23, 2024) correct setup failures. Version 68 (April 1, 2024) -------------------------- diff --git a/setup/bootstrap.sh b/setup/bootstrap.sh index d18cd8b8..18c53559 100644 --- a/setup/bootstrap.sh +++ b/setup/bootstrap.sh @@ -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=v69a + TAG=v69b 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.