mirror of
https://github.com/mail-in-a-box/mailinabox.git
synced 2026-03-12 17:07:23 +01:00
Compare commits
27 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2aca421415 | ||
|
|
99474b348f | ||
|
|
8bebaf6a48 | ||
|
|
9004bb6e8e | ||
|
|
69d8fdef99 | ||
|
|
eeee712cf3 | ||
|
|
8f42d97b54 | ||
|
|
6e40c69cb5 | ||
|
|
c0e54f87d7 | ||
|
|
3a7de051ee | ||
|
|
f11cb04a72 | ||
|
|
cb564a130a | ||
|
|
d1d6318862 | ||
|
|
34b7a02f4f | ||
|
|
a312acc3bc | ||
|
|
aab1ec691c | ||
|
|
520caf6557 | ||
|
|
c92fd02262 | ||
|
|
a85c429a85 | ||
|
|
50a5cb90bc | ||
|
|
aac878dce5 | ||
|
|
58b0323b36 | ||
|
|
646f971d8b | ||
|
|
86067be142 | ||
|
|
c67ff241c4 | ||
|
|
7b4cd443bf | ||
|
|
34017548d5 |
41
CHANGELOG.md
41
CHANGELOG.md
@@ -1,6 +1,47 @@
|
|||||||
CHANGELOG
|
CHANGELOG
|
||||||
=========
|
=========
|
||||||
|
|
||||||
|
Version 57 (June 12, 2022)
|
||||||
|
--------------------------
|
||||||
|
|
||||||
|
Setup:
|
||||||
|
|
||||||
|
* Fixed issue upgrading from Mail-in-a-Box v0.40-v0.50 because of a changed URL that Nextcloud is downloaded from.
|
||||||
|
|
||||||
|
Backups:
|
||||||
|
|
||||||
|
* Fixed S3 backups which broke with duplicity 0.8.23.
|
||||||
|
* Fixed Backblaze backups which broke with latest b2sdk package by rolling back its version.
|
||||||
|
|
||||||
|
Control panel:
|
||||||
|
|
||||||
|
* Fixed spurious changes in system status checks messages by sorting DNSSEC DS records.
|
||||||
|
* Fixed fail2ban lockout over IPv6 from excessive loads of the system status checks.
|
||||||
|
* Fixed an incorrect IPv6 system status check message.
|
||||||
|
|
||||||
|
Version 56 (January 19, 2022)
|
||||||
|
-----------------------------
|
||||||
|
|
||||||
|
Software updates:
|
||||||
|
|
||||||
|
* Roundcube updated to 1.5.2 (from 1.5.0), and the persistent_login and CardDAV (to 4.3.0 from 3.0.3) plugins are updated.
|
||||||
|
* Nextcloud updated to 20.0.14 (from 20.0.8), contacts to 4.0.7 (from 3.5.1), and calendar to 3.0.4 (from 2.2.0).
|
||||||
|
|
||||||
|
Setup:
|
||||||
|
|
||||||
|
* Fixed failed setup if a previous attempt failed while updating Nextcloud.
|
||||||
|
|
||||||
|
Control panel:
|
||||||
|
|
||||||
|
* Fixed a crash if a custom DNS entry is not under a zone managed by the box.
|
||||||
|
* Fix DNSSEC instructions typo.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
Version 55 (October 18, 2021)
|
Version 55 (October 18, 2021)
|
||||||
-----------------------------
|
-----------------------------
|
||||||
|
|
||||||
|
|||||||
@@ -20,9 +20,9 @@ _If you're seeing an error message about your *IP address being listed in the Sp
|
|||||||
|
|
||||||
### Modifying your `hosts` file
|
### Modifying your `hosts` file
|
||||||
|
|
||||||
After a while, Mail-in-a-Box will be available at `192.168.50.4` (unless you changed that in your `Vagrantfile`). To be able to use the web-based bits, we recommend to add a hostname to your `hosts` file:
|
After a while, Mail-in-a-Box will be available at `192.168.56.4` (unless you changed that in your `Vagrantfile`). To be able to use the web-based bits, we recommend to add a hostname to your `hosts` file:
|
||||||
|
|
||||||
$ echo "192.168.50.4 mailinabox.lan" | sudo tee -a /etc/hosts
|
$ echo "192.168.56.4 mailinabox.lan" | sudo tee -a /etc/hosts
|
||||||
|
|
||||||
You should now be able to navigate to https://mailinabox.lan/admin using your browser. There should be an initial admin user with the name `me@mailinabox.lan` and the password `12345678`.
|
You should now be able to navigate to https://mailinabox.lan/admin using your browser. There should be an initial admin user with the name `me@mailinabox.lan` and the password `12345678`.
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
$ git clone https://github.com/mail-in-a-box/mailinabox
|
||||||
$ cd mailinabox
|
$ cd mailinabox
|
||||||
$ git checkout v55
|
$ git checkout v57
|
||||||
|
|
||||||
Begin the installation.
|
Begin the installation.
|
||||||
|
|
||||||
|
|||||||
2
Vagrantfile
vendored
2
Vagrantfile
vendored
@@ -9,7 +9,7 @@ Vagrant.configure("2") do |config|
|
|||||||
# the machine's box will let anyone log into it. So instead we'll put the
|
# the machine's box will let anyone log into it. So instead we'll put the
|
||||||
# machine on a private network.
|
# machine on a private network.
|
||||||
config.vm.hostname = "mailinabox.lan"
|
config.vm.hostname = "mailinabox.lan"
|
||||||
config.vm.network "private_network", ip: "192.168.50.4"
|
config.vm.network "private_network", ip: "192.168.56.4"
|
||||||
|
|
||||||
config.vm.provision :shell, :inline => <<-SH
|
config.vm.provision :shell, :inline => <<-SH
|
||||||
# Set environment variables so that the setup script does
|
# Set environment variables so that the setup script does
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ paths:
|
|||||||
x-codeSamples:
|
x-codeSamples:
|
||||||
- lang: curl
|
- lang: curl
|
||||||
source: |
|
source: |
|
||||||
curl -X GET "https://{host}/admin/login" \
|
curl -X POST "https://{host}/admin/login" \
|
||||||
-u "<email>:<password>"
|
-u "<email>:<password>"
|
||||||
responses:
|
responses:
|
||||||
200:
|
200:
|
||||||
@@ -103,13 +103,15 @@ paths:
|
|||||||
x-codeSamples:
|
x-codeSamples:
|
||||||
- lang: curl
|
- lang: curl
|
||||||
source: |
|
source: |
|
||||||
curl -X GET "https://{host}/admin/logout" \
|
curl -X POST "https://{host}/admin/logout" \
|
||||||
-u "<email>:<session_key>"
|
-u "<email>:<session_key>"
|
||||||
responses:
|
responses:
|
||||||
200:
|
200:
|
||||||
description: Successful operation
|
description: Successful operation
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/LogoutResponse'
|
||||||
/system/status:
|
/system/status:
|
||||||
post:
|
post:
|
||||||
tags:
|
tags:
|
||||||
@@ -2723,3 +2725,8 @@ components:
|
|||||||
nullable: true
|
nullable: true
|
||||||
MfaDisableSuccessResponse:
|
MfaDisableSuccessResponse:
|
||||||
type: string
|
type: string
|
||||||
|
LogoutResponse:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
status:
|
||||||
|
type: string
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
# Whitelist our own IP addresses. 127.0.0.1/8 is the default. But our status checks
|
# Whitelist our own IP addresses. 127.0.0.1/8 is the default. But our status checks
|
||||||
# ping services over the public interface so we should whitelist that address of
|
# ping services over the public interface so we should whitelist that address of
|
||||||
# ours too. The string is substituted during installation.
|
# ours too. The string is substituted during installation.
|
||||||
ignoreip = 127.0.0.1/8 PUBLIC_IP
|
ignoreip = 127.0.0.1/8 PUBLIC_IP ::1 PUBLIC_IPV6
|
||||||
|
|
||||||
[dovecot]
|
[dovecot]
|
||||||
enabled = true
|
enabled = true
|
||||||
|
|||||||
@@ -14,11 +14,6 @@ from exclusiveprocess import Lock
|
|||||||
|
|
||||||
from utils import load_environment, shell, wait_for_service, fix_boto
|
from utils import load_environment, shell, wait_for_service, fix_boto
|
||||||
|
|
||||||
rsync_ssh_options = [
|
|
||||||
"--ssh-options= -i /root/.ssh/id_rsa_miab",
|
|
||||||
"--rsync-options= -e \"/usr/bin/ssh -oStrictHostKeyChecking=no -oBatchMode=yes -p 22 -i /root/.ssh/id_rsa_miab\"",
|
|
||||||
]
|
|
||||||
|
|
||||||
def backup_status(env):
|
def backup_status(env):
|
||||||
# If backups are dissbled, return no status.
|
# If backups are dissbled, return no status.
|
||||||
config = get_backup_config(env)
|
config = get_backup_config(env)
|
||||||
@@ -64,9 +59,9 @@ def backup_status(env):
|
|||||||
"--archive-dir", backup_cache_dir,
|
"--archive-dir", backup_cache_dir,
|
||||||
"--gpg-options", "--cipher-algo=AES256",
|
"--gpg-options", "--cipher-algo=AES256",
|
||||||
"--log-fd", "1",
|
"--log-fd", "1",
|
||||||
config["target"],
|
get_duplicity_target_url(config),
|
||||||
] + rsync_ssh_options,
|
] + get_duplicity_additional_args(env),
|
||||||
get_env(env),
|
get_duplicity_env_vars(env),
|
||||||
trap=True)
|
trap=True)
|
||||||
if code != 0:
|
if code != 0:
|
||||||
# Command failed. This is likely due to an improperly configured remote
|
# Command failed. This is likely due to an improperly configured remote
|
||||||
@@ -195,7 +190,48 @@ def get_passphrase(env):
|
|||||||
|
|
||||||
return passphrase
|
return passphrase
|
||||||
|
|
||||||
def get_env(env):
|
def get_duplicity_target_url(config):
|
||||||
|
target = config["target"]
|
||||||
|
|
||||||
|
if get_target_type(config) == "s3":
|
||||||
|
from urllib.parse import urlsplit, urlunsplit
|
||||||
|
target = list(urlsplit(target))
|
||||||
|
|
||||||
|
# Duplicity now defaults to boto3 as the backend for S3, but we have
|
||||||
|
# legacy boto installed (boto3 doesn't support Ubuntu 18.04) so
|
||||||
|
# we retarget for classic boto.
|
||||||
|
target[0] = "boto+" + target[0]
|
||||||
|
|
||||||
|
# In addition, although we store the S3 hostname in the target URL,
|
||||||
|
# duplicity no longer accepts it in the target URL. The hostname in
|
||||||
|
# the target URL must be the bucket name. The hostname is passed
|
||||||
|
# via get_duplicity_additional_args. Move the first part of the
|
||||||
|
# path (the bucket name) into the hostname URL component, and leave
|
||||||
|
# the rest for the path.
|
||||||
|
target[1], target[2] = target[2].lstrip('/').split('/', 1)
|
||||||
|
|
||||||
|
target = urlunsplit(target)
|
||||||
|
|
||||||
|
return target
|
||||||
|
|
||||||
|
def get_duplicity_additional_args(env):
|
||||||
|
config = get_backup_config(env)
|
||||||
|
|
||||||
|
if get_target_type(config) == 'rsync':
|
||||||
|
return [
|
||||||
|
"--ssh-options= -i /root/.ssh/id_rsa_miab",
|
||||||
|
"--rsync-options= -e \"/usr/bin/ssh -oStrictHostKeyChecking=no -oBatchMode=yes -p 22 -i /root/.ssh/id_rsa_miab\"",
|
||||||
|
]
|
||||||
|
elif get_target_type(config) == 's3':
|
||||||
|
# See note about hostname in get_duplicity_target_url.
|
||||||
|
from urllib.parse import urlsplit, urlunsplit
|
||||||
|
target = urlsplit(config["target"])
|
||||||
|
endpoint_url = urlunsplit(("https", target.netloc, '', '', ''))
|
||||||
|
return ["--s3-endpoint-url", endpoint_url]
|
||||||
|
|
||||||
|
return []
|
||||||
|
|
||||||
|
def get_duplicity_env_vars(env):
|
||||||
config = get_backup_config(env)
|
config = get_backup_config(env)
|
||||||
|
|
||||||
env = { "PASSPHRASE" : get_passphrase(env) }
|
env = { "PASSPHRASE" : get_passphrase(env) }
|
||||||
@@ -273,10 +309,10 @@ def perform_backup(full_backup):
|
|||||||
"--volsize", "250",
|
"--volsize", "250",
|
||||||
"--gpg-options", "--cipher-algo=AES256",
|
"--gpg-options", "--cipher-algo=AES256",
|
||||||
env["STORAGE_ROOT"],
|
env["STORAGE_ROOT"],
|
||||||
config["target"],
|
get_duplicity_target_url(config),
|
||||||
"--allow-source-mismatch"
|
"--allow-source-mismatch"
|
||||||
] + rsync_ssh_options,
|
] + get_duplicity_additional_args(env),
|
||||||
get_env(env))
|
get_duplicity_env_vars(env))
|
||||||
finally:
|
finally:
|
||||||
# Start services again.
|
# Start services again.
|
||||||
service_command("dovecot", "start", quit=False)
|
service_command("dovecot", "start", quit=False)
|
||||||
@@ -292,9 +328,9 @@ def perform_backup(full_backup):
|
|||||||
"--verbosity", "error",
|
"--verbosity", "error",
|
||||||
"--archive-dir", backup_cache_dir,
|
"--archive-dir", backup_cache_dir,
|
||||||
"--force",
|
"--force",
|
||||||
config["target"]
|
get_duplicity_target_url(config)
|
||||||
] + rsync_ssh_options,
|
] + get_duplicity_additional_args(env),
|
||||||
get_env(env))
|
get_duplicity_env_vars(env))
|
||||||
|
|
||||||
# From duplicity's manual:
|
# From duplicity's manual:
|
||||||
# "This should only be necessary after a duplicity session fails or is
|
# "This should only be necessary after a duplicity session fails or is
|
||||||
@@ -307,9 +343,9 @@ def perform_backup(full_backup):
|
|||||||
"--verbosity", "error",
|
"--verbosity", "error",
|
||||||
"--archive-dir", backup_cache_dir,
|
"--archive-dir", backup_cache_dir,
|
||||||
"--force",
|
"--force",
|
||||||
config["target"]
|
get_duplicity_target_url(config)
|
||||||
] + rsync_ssh_options,
|
] + get_duplicity_additional_args(env),
|
||||||
get_env(env))
|
get_duplicity_env_vars(env))
|
||||||
|
|
||||||
# Change ownership of backups to the user-data user, so that the after-bcakup
|
# Change ownership of backups to the user-data user, so that the after-bcakup
|
||||||
# script can access them.
|
# script can access them.
|
||||||
@@ -345,9 +381,9 @@ def run_duplicity_verification():
|
|||||||
"--compare-data",
|
"--compare-data",
|
||||||
"--archive-dir", backup_cache_dir,
|
"--archive-dir", backup_cache_dir,
|
||||||
"--exclude", backup_root,
|
"--exclude", backup_root,
|
||||||
config["target"],
|
get_duplicity_target_url(config),
|
||||||
env["STORAGE_ROOT"],
|
env["STORAGE_ROOT"],
|
||||||
] + rsync_ssh_options, get_env(env))
|
] + get_duplicity_additional_args(env), get_duplicity_env_vars(env))
|
||||||
|
|
||||||
def run_duplicity_restore(args):
|
def run_duplicity_restore(args):
|
||||||
env = load_environment()
|
env = load_environment()
|
||||||
@@ -357,9 +393,9 @@ def run_duplicity_restore(args):
|
|||||||
"/usr/bin/duplicity",
|
"/usr/bin/duplicity",
|
||||||
"restore",
|
"restore",
|
||||||
"--archive-dir", backup_cache_dir,
|
"--archive-dir", backup_cache_dir,
|
||||||
config["target"],
|
get_duplicity_target_url(config),
|
||||||
] + rsync_ssh_options + args,
|
] + get_duplicity_additional_args(env) + args,
|
||||||
get_env(env))
|
get_duplicity_env_vars(env))
|
||||||
|
|
||||||
def list_target_files(config):
|
def list_target_files(config):
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
|
|||||||
@@ -330,7 +330,7 @@ def dns_get_records(qname=None, rtype=None):
|
|||||||
r["sort-order"]["created"] = i
|
r["sort-order"]["created"] = i
|
||||||
domain_sort_order = utils.sort_domains([r["qname"] for r in records], env)
|
domain_sort_order = utils.sort_domains([r["qname"] for r in records], env)
|
||||||
for i, r in enumerate(sorted(records, key = lambda r : (
|
for i, r in enumerate(sorted(records, key = lambda r : (
|
||||||
zones.index(r["zone"]),
|
zones.index(r["zone"]) if r.get("zone") else 0, # record is not within a zone managed by the box
|
||||||
domain_sort_order.index(r["qname"]),
|
domain_sort_order.index(r["qname"]),
|
||||||
r["rtype"]))):
|
r["rtype"]))):
|
||||||
r["sort-order"]["qname"] = i
|
r["sort-order"]["qname"] = i
|
||||||
|
|||||||
@@ -484,7 +484,7 @@ def write_nsd_zone(domain, zonefile, records, env, force):
|
|||||||
# @ the PRIMARY_HOSTNAME. Hopefully that's legit.
|
# @ the PRIMARY_HOSTNAME. Hopefully that's legit.
|
||||||
#
|
#
|
||||||
# For the refresh through TTL fields, a good reference is:
|
# For the refresh through TTL fields, a good reference is:
|
||||||
# http://www.peerwisdom.org/2013/05/15/dns-understanding-the-soa-record/
|
# https://www.ripe.net/publications/docs/ripe-203
|
||||||
#
|
#
|
||||||
# A hash of the available DNSSEC keys are added in a comment so that when
|
# A hash of the available DNSSEC keys are added in a comment so that when
|
||||||
# the keys change we force a re-generation of the zone which triggers
|
# the keys change we force a re-generation of the zone which triggers
|
||||||
@@ -497,7 +497,7 @@ $TTL 86400 ; default time to live
|
|||||||
@ IN SOA ns1.{primary_domain}. hostmaster.{primary_domain}. (
|
@ IN SOA ns1.{primary_domain}. hostmaster.{primary_domain}. (
|
||||||
__SERIAL__ ; serial number
|
__SERIAL__ ; serial number
|
||||||
7200 ; Refresh (secondary nameserver update interval)
|
7200 ; Refresh (secondary nameserver update interval)
|
||||||
86400 ; Retry (when refresh fails, how often to try again)
|
3600 ; Retry (when refresh fails, how often to try again, should be lower than the refresh)
|
||||||
1209600 ; Expire (when refresh fails, how long secondary nameserver will keep records around anyway)
|
1209600 ; Expire (when refresh fails, how long secondary nameserver will keep records around anyway)
|
||||||
86400 ; Negative TTL (how long negative responses are cached)
|
86400 ; Negative TTL (how long negative responses are cached)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -549,8 +549,9 @@ def scan_postfix_submission_line(date, log, collector):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
# Match both the 'plain' and 'login' sasl methods, since both authentication methods are
|
# Match both the 'plain' and 'login' sasl methods, since both authentication methods are
|
||||||
# allowed by Dovecot
|
# allowed by Dovecot. Exclude trailing comma after the username when additional fields
|
||||||
m = re.match("([A-Z0-9]+): client=(\S+), sasl_method=(PLAIN|LOGIN), sasl_username=(\S+)", log)
|
# follow after.
|
||||||
|
m = re.match("([A-Z0-9]+): client=(\S+), sasl_method=(PLAIN|LOGIN), sasl_username=(\S+)(?<!,)", log)
|
||||||
|
|
||||||
if m:
|
if m:
|
||||||
_, client, method, user = m.groups()
|
_, client, method, user = m.groups()
|
||||||
|
|||||||
@@ -135,7 +135,7 @@ def check_service(i, service, env):
|
|||||||
|
|
||||||
# IPv4 ok but IPv6 failed. Try the PRIVATE_IPV6 address to see if the service is bound to the interface.
|
# IPv4 ok but IPv6 failed. Try the PRIVATE_IPV6 address to see if the service is bound to the interface.
|
||||||
elif service["port"] != 53 and try_connect(env["PRIVATE_IPV6"]):
|
elif service["port"] != 53 and try_connect(env["PRIVATE_IPV6"]):
|
||||||
output.print_error("%s is running (and available over IPv4 and the local IPv6 address), but it is not publicly accessible at %s:%d." % (service['name'], env['PUBLIC_IP'], service['port']))
|
output.print_error("%s is running (and available over IPv4 and the local IPv6 address), but it is not publicly accessible at %s:%d." % (service['name'], env['PUBLIC_IPV6'], service['port']))
|
||||||
else:
|
else:
|
||||||
output.print_error("%s is running and available over IPv4 but is not accessible over IPv6 at %s port %d." % (service['name'], env['PUBLIC_IPV6'], service['port']))
|
output.print_error("%s is running and available over IPv4 but is not accessible over IPv6 at %s port %d." % (service['name'], env['PUBLIC_IPV6'], service['port']))
|
||||||
|
|
||||||
@@ -253,6 +253,18 @@ def check_free_disk_space(rounded_values, env, output):
|
|||||||
if rounded_values: disk_msg = "The disk has less than 15% free space."
|
if rounded_values: disk_msg = "The disk has less than 15% free space."
|
||||||
output.print_error(disk_msg)
|
output.print_error(disk_msg)
|
||||||
|
|
||||||
|
# Check that there's only one duplicity cache. If there's more than one,
|
||||||
|
# it's probably no longer in use, and we can recommend clearing the cache
|
||||||
|
# to save space. The cache directory may not exist yet, which is OK.
|
||||||
|
backup_cache_path = os.path.join(env['STORAGE_ROOT'], 'backup/cache')
|
||||||
|
try:
|
||||||
|
backup_cache_count = len(os.listdir(backup_cache_path))
|
||||||
|
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))
|
||||||
|
|
||||||
def check_free_memory(rounded_values, env, output):
|
def check_free_memory(rounded_values, env, output):
|
||||||
# Check free memory.
|
# Check free memory.
|
||||||
percent_free = 100 - psutil.virtual_memory().percent
|
percent_free = 100 - psutil.virtual_memory().percent
|
||||||
@@ -646,7 +658,7 @@ def check_dnssec(domain, env, output, dns_zonefiles, is_checking_primary=False):
|
|||||||
output.print_line("Option " + str(i+1) + ":")
|
output.print_line("Option " + str(i+1) + ":")
|
||||||
output.print_line("----------")
|
output.print_line("----------")
|
||||||
output.print_line("Key Tag: " + ds_suggestion['keytag'])
|
output.print_line("Key Tag: " + ds_suggestion['keytag'])
|
||||||
output.print_line("Key Flags: KSK (256)")
|
output.print_line("Key Flags: KSK / 257")
|
||||||
output.print_line("Algorithm: %s / %s" % (ds_suggestion['alg'], ds_suggestion['alg_name']))
|
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("Digest Type: %s / %s" % (ds_suggestion['digalg'], ds_suggestion['digalg_name']))
|
||||||
output.print_line("Digest: " + ds_suggestion['digest'])
|
output.print_line("Digest: " + ds_suggestion['digest'])
|
||||||
@@ -658,7 +670,7 @@ def check_dnssec(domain, env, output, dns_zonefiles, is_checking_primary=False):
|
|||||||
if len(ds) > 0:
|
if len(ds) > 0:
|
||||||
output.print_line("")
|
output.print_line("")
|
||||||
output.print_line("The DS record is currently set to:")
|
output.print_line("The DS record is currently set to:")
|
||||||
for rr in ds:
|
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: {0}, Algorithm: {1}, Digest Type: {2}, Digest: {3}".format(*rr))
|
||||||
|
|
||||||
def check_mail_domain(domain, env, output):
|
def check_mail_domain(domain, env, output):
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
<h2>Backup Status</h2>
|
<h2>Backup Status</h2>
|
||||||
|
|
||||||
<p>The box makes an incremental backup each night. By default the backup is stored on the machine itself, but you can also store in on S3-compatible services like Amazon Web Services (AWS).</p>
|
<p>The box makes an incremental backup each night. By default the backup is stored on the machine itself, but you can also store it on S3-compatible services like Amazon Web Services (AWS).</p>
|
||||||
|
|
||||||
<h3>Configuration</h3>
|
<h3>Configuration</h3>
|
||||||
|
|
||||||
|
|||||||
13
security.md
13
security.md
@@ -3,7 +3,12 @@ Mail-in-a-Box Security Guide
|
|||||||
|
|
||||||
Mail-in-a-Box turns a fresh Ubuntu 18.04 LTS 64-bit machine into a mail server appliance by installing and configuring various components.
|
Mail-in-a-Box turns a fresh Ubuntu 18.04 LTS 64-bit machine into a mail server appliance by installing and configuring various components.
|
||||||
|
|
||||||
This page documents the security features of Mail-in-a-Box. The term “box” is used below to mean a configured Mail-in-a-Box.
|
This page documents the security posture of Mail-in-a-Box. The term “box” is used below to mean a configured Mail-in-a-Box.
|
||||||
|
|
||||||
|
Reporting Security Vulnerabilities
|
||||||
|
----------------------------------
|
||||||
|
|
||||||
|
Security vulnerabilities should be reported to the [project's maintainer](https://joshdata.me) via email.
|
||||||
|
|
||||||
Threat Model
|
Threat Model
|
||||||
------------
|
------------
|
||||||
@@ -49,9 +54,7 @@ Additionally:
|
|||||||
|
|
||||||
### Password Storage
|
### Password Storage
|
||||||
|
|
||||||
The passwords for mail users are stored on disk using the [SHA512-CRYPT](http://man7.org/linux/man-pages/man3/crypt.3.html) hashing scheme. ([source](management/mailconfig.py))
|
The passwords for mail users are stored on disk using the [SHA512-CRYPT](http://man7.org/linux/man-pages/man3/crypt.3.html) hashing scheme. ([source](management/mailconfig.py)) Password changes (as well as changes to control panel two-factor authentication settings) expire any control panel login sessions.
|
||||||
|
|
||||||
When using the web-based administrative control panel, after logging in an API key is placed in the browser's local storage (rather than, say, the user's actual password). The API key is an HMAC based on the user's email address and current password, and it is keyed by a secret known only to the control panel service. By resetting an administrator's password, any HMACs previously generated for that user will expire.
|
|
||||||
|
|
||||||
### Console access
|
### Console access
|
||||||
|
|
||||||
@@ -65,7 +68,7 @@ If DNSSEC is enabled at the box's domain name's registrar, the SSHFP record that
|
|||||||
|
|
||||||
`fail2ban` provides some protection from brute-force login attacks (repeated logins that guess account passwords) by blocking offending IP addresses at the network level.
|
`fail2ban` provides some protection from brute-force login attacks (repeated logins that guess account passwords) by blocking offending IP addresses at the network level.
|
||||||
|
|
||||||
The following services are protected: SSH, IMAP (dovecot), SMTP submission (postfix), webmail (roundcube), Nextcloud/CalDAV/CardDAV (over HTTP), and the Mail-in-a-Box control panel & munin (over HTTP).
|
The following services are protected: SSH, IMAP (dovecot), SMTP submission (postfix), webmail (roundcube), Nextcloud/CalDAV/CardDAV (over HTTP), and the Mail-in-a-Box control panel (over HTTP).
|
||||||
|
|
||||||
Some other services running on the box may be missing fail2ban filters.
|
Some other services running on the box may be missing fail2ban filters.
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ if [ -z "$TAG" ]; then
|
|||||||
# want to display in status checks.
|
# want to display in status checks.
|
||||||
if [ "$(lsb_release -d | sed 's/.*:\s*//' | sed 's/18\.04\.[0-9]/18.04/' )" == "Ubuntu 18.04 LTS" ]; then
|
if [ "$(lsb_release -d | sed 's/.*:\s*//' | sed 's/18\.04\.[0-9]/18.04/' )" == "Ubuntu 18.04 LTS" ]; then
|
||||||
# This machine is running Ubuntu 18.04.
|
# This machine is running Ubuntu 18.04.
|
||||||
TAG=v55
|
TAG=v57
|
||||||
|
|
||||||
elif [ "$(lsb_release -d | sed 's/.*:\s*//' | sed 's/14\.04\.[0-9]/14.04/' )" == "Ubuntu 14.04 LTS" ]; then
|
elif [ "$(lsb_release -d | sed 's/.*:\s*//' | sed 's/14\.04\.[0-9]/14.04/' )" == "Ubuntu 14.04 LTS" ]; then
|
||||||
# This machine is running Ubuntu 14.04.
|
# This machine is running Ubuntu 14.04.
|
||||||
|
|||||||
@@ -25,12 +25,12 @@ done
|
|||||||
#
|
#
|
||||||
# certbot installs EFF's certbot which we use to
|
# certbot installs EFF's certbot which we use to
|
||||||
# provision free TLS certificates.
|
# provision free TLS certificates.
|
||||||
apt_install duplicity python-pip virtualenv certbot
|
apt_install duplicity python-pip virtualenv certbot rsync
|
||||||
|
|
||||||
# b2sdk is used for backblaze backups.
|
# b2sdk is used for backblaze backups.
|
||||||
# boto is used for amazon aws backups.
|
# boto is used for amazon aws backups.
|
||||||
# Both are installed outside the pipenv, so they can be used by duplicity
|
# Both are installed outside the pipenv, so they can be used by duplicity
|
||||||
hide_output pip3 install --upgrade b2sdk boto
|
hide_output pip3 install --upgrade b2sdk==1.14.1 boto
|
||||||
|
|
||||||
# Create a virtualenv for the installation of Python 3 packages
|
# Create a virtualenv for the installation of Python 3 packages
|
||||||
# used by the management daemon.
|
# used by the management daemon.
|
||||||
|
|||||||
@@ -9,6 +9,39 @@ source /etc/mailinabox.conf # load global vars
|
|||||||
|
|
||||||
echo "Installing Nextcloud (contacts/calendar)..."
|
echo "Installing Nextcloud (contacts/calendar)..."
|
||||||
|
|
||||||
|
# Nextcloud core and app (plugin) versions to install.
|
||||||
|
# With each version we store a hash to ensure we install what we expect.
|
||||||
|
|
||||||
|
# Nextcloud core
|
||||||
|
# --------------
|
||||||
|
# * See https://nextcloud.com/changelog for the latest version.
|
||||||
|
# * Check https://docs.nextcloud.com/server/latest/admin_manual/installation/system_requirements.html
|
||||||
|
# for whether it supports the version of PHP available on this machine.
|
||||||
|
# * Since Nextcloud only supports upgrades from consecutive major versions,
|
||||||
|
# we automatically install intermediate versions as needed.
|
||||||
|
# * The hash is the SHA1 hash of the ZIP package, which you can find by just running this script and
|
||||||
|
# copying it from the error message when it doesn't match what is below.
|
||||||
|
nextcloud_ver=20.0.14
|
||||||
|
nextcloud_hash=92cac708915f51ee2afc1787fd845476fd090c81
|
||||||
|
|
||||||
|
# Nextcloud apps
|
||||||
|
# --------------
|
||||||
|
# * Find the most recent tag that is compatible with the Nextcloud version above by
|
||||||
|
# consulting the <dependencies>...<nextcloud> node at:
|
||||||
|
# https://github.com/nextcloud-releases/contacts/blob/master/appinfo/info.xml
|
||||||
|
# https://github.com/nextcloud-releases/calendar/blob/master/appinfo/info.xml
|
||||||
|
# https://github.com/nextcloud/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.0.7
|
||||||
|
contacts_hash=45e7cf4bfe99cd8d03625cf9e5a1bb2e90549136
|
||||||
|
calendar_ver=3.0.4
|
||||||
|
calendar_hash=d0284b68135777ec9ca713c307216165b294d0fe
|
||||||
|
user_external_ver=1.0.0
|
||||||
|
user_external_hash=3bf2609061d7214e7f0f69dd8883e55c4ec8f50a
|
||||||
|
|
||||||
|
# Clear prior packages and install dependencies from apt.
|
||||||
|
|
||||||
apt-get purge -qq -y owncloud* # we used to use the package manager
|
apt-get purge -qq -y owncloud* # we used to use the package manager
|
||||||
|
|
||||||
apt_install php php-fpm \
|
apt_install php php-fpm \
|
||||||
@@ -46,11 +79,11 @@ InstallNextcloud() {
|
|||||||
# their github repositories.
|
# their github repositories.
|
||||||
mkdir -p /usr/local/lib/owncloud/apps
|
mkdir -p /usr/local/lib/owncloud/apps
|
||||||
|
|
||||||
wget_verify https://github.com/nextcloud/contacts/releases/download/v$version_contacts/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/
|
tar xf /tmp/contacts.tgz -C /usr/local/lib/owncloud/apps/
|
||||||
rm /tmp/contacts.tgz
|
rm /tmp/contacts.tgz
|
||||||
|
|
||||||
wget_verify https://github.com/nextcloud/calendar/releases/download/v$version_calendar/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/
|
tar xf /tmp/calendar.tgz -C /usr/local/lib/owncloud/apps/
|
||||||
rm /tmp/calendar.tgz
|
rm /tmp/calendar.tgz
|
||||||
|
|
||||||
@@ -96,16 +129,6 @@ InstallNextcloud() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
# Nextcloud Version to install. Checks are done down below to step through intermediate versions.
|
|
||||||
nextcloud_ver=20.0.8
|
|
||||||
nextcloud_hash=372b0b4bb07c7984c04917aff86b280e68fbe761
|
|
||||||
contacts_ver=3.5.1
|
|
||||||
contacts_hash=d2ffbccd3ed89fa41da20a1dff149504c3b33b93
|
|
||||||
calendar_ver=2.2.0
|
|
||||||
calendar_hash=673ad72ca28adb8d0f209015ff2dca52ffad99af
|
|
||||||
user_external_ver=1.0.0
|
|
||||||
user_external_hash=3bf2609061d7214e7f0f69dd8883e55c4ec8f50a
|
|
||||||
|
|
||||||
# Current Nextcloud Version, #1623
|
# Current Nextcloud Version, #1623
|
||||||
# Checking /usr/local/lib/owncloud/version.php shows version of the Nextcloud application, not the DB
|
# 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
|
||||||
@@ -160,24 +183,25 @@ if [ ! -d /usr/local/lib/owncloud/ ] || [[ ! ${CURRENT_NEXTCLOUD_VER} =~ ^$nextc
|
|||||||
# During the upgrade from Nextcloud 14 to 15, user_external may cause the upgrade to fail.
|
# During the upgrade from Nextcloud 14 to 15, user_external may cause the upgrade to fail.
|
||||||
# We will disable it here before the upgrade and install it again after the upgrade.
|
# We will disable it here before the upgrade and install it again after the upgrade.
|
||||||
hide_output sudo -u www-data php /usr/local/lib/owncloud/console.php app:disable user_external
|
hide_output sudo -u www-data php /usr/local/lib/owncloud/console.php app:disable user_external
|
||||||
InstallNextcloud 15.0.8 4129d8d4021c435f2e86876225fb7f15adf764a3 3.3.0 e55d0357c6785d3b1f3b5f21780cb6d41d32443a 2.0.3 9d9717b29337613b72c74e9914c69b74b346c466 0.7.0 555a94811daaf5bdd336c5e48a78aa8567b86437
|
InstallNextcloud 15.0.8 4129d8d4021c435f2e86876225fb7f15adf764a3 3.3.0 e55d0357c6785d3b1f3b5f21780cb6d41d32443a 2.0.3 a1f3835c752929e3598eb94f22300516867ac6ab 0.7.0 555a94811daaf5bdd336c5e48a78aa8567b86437
|
||||||
CURRENT_NEXTCLOUD_VER="15.0.8"
|
CURRENT_NEXTCLOUD_VER="15.0.8"
|
||||||
fi
|
fi
|
||||||
if [[ ${CURRENT_NEXTCLOUD_VER} =~ ^15 ]]; then
|
if [[ ${CURRENT_NEXTCLOUD_VER} =~ ^15 ]]; then
|
||||||
InstallNextcloud 16.0.6 0bb3098455ec89f5af77a652aad553ad40a88819 3.3.0 e55d0357c6785d3b1f3b5f21780cb6d41d32443a 2.0.3 9d9717b29337613b72c74e9914c69b74b346c466 0.7.0 555a94811daaf5bdd336c5e48a78aa8567b86437
|
InstallNextcloud 16.0.6 0bb3098455ec89f5af77a652aad553ad40a88819 3.3.0 e55d0357c6785d3b1f3b5f21780cb6d41d32443a 2.0.3 a1f3835c752929e3598eb94f22300516867ac6ab 0.7.0 555a94811daaf5bdd336c5e48a78aa8567b86437
|
||||||
CURRENT_NEXTCLOUD_VER="16.0.6"
|
CURRENT_NEXTCLOUD_VER="16.0.6"
|
||||||
fi
|
fi
|
||||||
if [[ ${CURRENT_NEXTCLOUD_VER} =~ ^16 ]]; then
|
if [[ ${CURRENT_NEXTCLOUD_VER} =~ ^16 ]]; then
|
||||||
InstallNextcloud 17.0.6 50b98d2c2f18510b9530e558ced9ab51eb4f11b0 3.3.0 e55d0357c6785d3b1f3b5f21780cb6d41d32443a 2.0.3 9d9717b29337613b72c74e9914c69b74b346c466 0.7.0 555a94811daaf5bdd336c5e48a78aa8567b86437
|
InstallNextcloud 17.0.6 50b98d2c2f18510b9530e558ced9ab51eb4f11b0 3.3.0 e55d0357c6785d3b1f3b5f21780cb6d41d32443a 2.0.3 a1f3835c752929e3598eb94f22300516867ac6ab 0.7.0 555a94811daaf5bdd336c5e48a78aa8567b86437
|
||||||
CURRENT_NEXTCLOUD_VER="17.0.6"
|
CURRENT_NEXTCLOUD_VER="17.0.6"
|
||||||
fi
|
fi
|
||||||
if [[ ${CURRENT_NEXTCLOUD_VER} =~ ^17 ]]; then
|
if [[ ${CURRENT_NEXTCLOUD_VER} =~ ^17 ]]; then
|
||||||
echo "ALTER TABLE oc_flow_operations ADD COLUMN entity VARCHAR;" | sqlite3 $STORAGE_ROOT/owncloud/owncloud.db
|
# Don't exit the install if this column already exists (see #2076)
|
||||||
InstallNextcloud 18.0.10 39c0021a8b8477c3f1733fddefacfa5ebf921c68 3.4.1 aee680a75e95f26d9285efd3c1e25cf7f3bfd27e 2.0.3 9d9717b29337613b72c74e9914c69b74b346c466 1.0.0 3bf2609061d7214e7f0f69dd8883e55c4ec8f50a
|
(echo "ALTER TABLE oc_flow_operations ADD COLUMN entity VARCHAR;" | sqlite3 $STORAGE_ROOT/owncloud/owncloud.db 2>/dev/null) || true
|
||||||
|
InstallNextcloud 18.0.10 39c0021a8b8477c3f1733fddefacfa5ebf921c68 3.4.1 8f685e7dc99758636d660d595e389c324e51e9d1 2.0.3 a1f3835c752929e3598eb94f22300516867ac6ab 1.0.0 3bf2609061d7214e7f0f69dd8883e55c4ec8f50a
|
||||||
CURRENT_NEXTCLOUD_VER="18.0.10"
|
CURRENT_NEXTCLOUD_VER="18.0.10"
|
||||||
fi
|
fi
|
||||||
if [[ ${CURRENT_NEXTCLOUD_VER} =~ ^18 ]]; then
|
if [[ ${CURRENT_NEXTCLOUD_VER} =~ ^18 ]]; then
|
||||||
InstallNextcloud 19.0.4 01e98791ba12f4860d3d4047b9803f97a1b55c60 3.4.1 aee680a75e95f26d9285efd3c1e25cf7f3bfd27e 2.0.3 9d9717b29337613b72c74e9914c69b74b346c466 1.0.0 3bf2609061d7214e7f0f69dd8883e55c4ec8f50a
|
InstallNextcloud 19.0.4 01e98791ba12f4860d3d4047b9803f97a1b55c60 3.4.1 8f685e7dc99758636d660d595e389c324e51e9d1 2.0.3 a1f3835c752929e3598eb94f22300516867ac6ab 1.0.0 3bf2609061d7214e7f0f69dd8883e55c4ec8f50a
|
||||||
CURRENT_NEXTCLOUD_VER="19.0.4"
|
CURRENT_NEXTCLOUD_VER="19.0.4"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -75,6 +75,13 @@ then
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# ### Set log retention policy.
|
||||||
|
|
||||||
|
# Set the systemd journal log retention from infinite to 10 days,
|
||||||
|
# since over time the logs take up a large amount of space.
|
||||||
|
# (See https://discourse.mailinabox.email/t/journalctl-reclaim-space-on-small-mailinabox/6728/11.)
|
||||||
|
tools/editconf.py /etc/systemd/journald.conf MaxRetentionSec=10day
|
||||||
|
|
||||||
# ### Add PPAs.
|
# ### Add PPAs.
|
||||||
|
|
||||||
# We install some non-standard Ubuntu packages maintained by other
|
# We install some non-standard Ubuntu packages maintained by other
|
||||||
@@ -356,6 +363,7 @@ systemctl restart systemd-resolved
|
|||||||
rm -f /etc/fail2ban/jail.local # we used to use this file but don't anymore
|
rm -f /etc/fail2ban/jail.local # we used to use this file but don't anymore
|
||||||
rm -f /etc/fail2ban/jail.d/defaults-debian.conf # removes default config so we can manage all of fail2ban rules in one config
|
rm -f /etc/fail2ban/jail.d/defaults-debian.conf # removes default config so we can manage all of fail2ban rules in one config
|
||||||
cat conf/fail2ban/jails.conf \
|
cat conf/fail2ban/jails.conf \
|
||||||
|
| sed "s/PUBLIC_IPV6/$PUBLIC_IPV6/g" \
|
||||||
| sed "s/PUBLIC_IP/$PUBLIC_IP/g" \
|
| sed "s/PUBLIC_IP/$PUBLIC_IP/g" \
|
||||||
| sed "s#STORAGE_ROOT#$STORAGE_ROOT#" \
|
| sed "s#STORAGE_ROOT#$STORAGE_ROOT#" \
|
||||||
> /etc/fail2ban/jail.d/mailinabox.conf
|
> /etc/fail2ban/jail.d/mailinabox.conf
|
||||||
|
|||||||
@@ -28,13 +28,19 @@ apt_install \
|
|||||||
# Install Roundcube from source if it is not already present or if it is out of date.
|
# Install Roundcube from source if it is not already present or if it is out of date.
|
||||||
# Combine the Roundcube version number with the commit hash of plugins to track
|
# Combine the Roundcube version number with the commit hash of plugins to track
|
||||||
# whether we have the latest version of everything.
|
# whether we have the latest version of everything.
|
||||||
|
# For the latest versions, see:
|
||||||
VERSION=1.5.0
|
# https://github.com/roundcube/roundcubemail/releases
|
||||||
HASH=2a9d11d9c10c8e8756120606c47eef702f00fe6d
|
# https://github.com/mfreiholz/persistent_login/commits/master
|
||||||
PERSISTENT_LOGIN_VERSION=6b3fc450cae23ccb2f393d0ef67aa319e877e435 # version 5.2.0
|
# https://github.com/stremlau/html5_notifier/commits/master
|
||||||
|
# https://github.com/mstilkerich/rcmcarddav/releases
|
||||||
|
# The easiest way to get the package hashes is to run this script and get the hash from
|
||||||
|
# the error message.
|
||||||
|
VERSION=1.5.2
|
||||||
|
HASH=208ce4ca0be423cc0f7070ff59bd03588b4439bf
|
||||||
|
PERSISTENT_LOGIN_VERSION=59ca1b0d3a02cff5fa621c1ad581d15f9d642fe8
|
||||||
HTML5_NOTIFIER_VERSION=68d9ca194212e15b3c7225eb6085dbcf02fd13d7 # version 0.6.4+
|
HTML5_NOTIFIER_VERSION=68d9ca194212e15b3c7225eb6085dbcf02fd13d7 # version 0.6.4+
|
||||||
CARDDAV_VERSION=3.0.3
|
CARDDAV_VERSION=4.3.0
|
||||||
CARDDAV_HASH=d1e3b0d851ffa2c6bd42bf0c04f70d0e1d0d78f8
|
CARDDAV_HASH=4ad7df8843951062878b1375f77c614f68bc5c61
|
||||||
|
|
||||||
UPDATE_KEY=$VERSION:$PERSISTENT_LOGIN_VERSION:$HTML5_NOTIFIER_VERSION:$CARDDAV_VERSION
|
UPDATE_KEY=$VERSION:$PERSISTENT_LOGIN_VERSION:$HTML5_NOTIFIER_VERSION:$CARDDAV_VERSION
|
||||||
|
|
||||||
@@ -77,13 +83,13 @@ if [ $needs_update == 1 ]; then
|
|||||||
|
|
||||||
# download and verify the full release of the carddav plugin
|
# download and verify the full release of the carddav plugin
|
||||||
wget_verify \
|
wget_verify \
|
||||||
https://github.com/blind-coder/rcmcarddav/releases/download/v${CARDDAV_VERSION}/carddav-${CARDDAV_VERSION}.zip \
|
https://github.com/blind-coder/rcmcarddav/releases/download/v${CARDDAV_VERSION}/carddav-v${CARDDAV_VERSION}.tar.gz \
|
||||||
$CARDDAV_HASH \
|
$CARDDAV_HASH \
|
||||||
/tmp/carddav.zip
|
/tmp/carddav.tar.gz
|
||||||
|
|
||||||
# unzip and cleanup
|
# unzip and cleanup
|
||||||
unzip -q /tmp/carddav.zip -d ${RCM_PLUGIN_DIR}
|
tar -C ${RCM_PLUGIN_DIR} -zxf /tmp/carddav.tar.gz
|
||||||
rm -f /tmp/carddav.zip
|
rm -f /tmp/carddav.tar.gz
|
||||||
|
|
||||||
# record the version we've installed
|
# record the version we've installed
|
||||||
echo $UPDATE_KEY > ${RCM_DIR}/version
|
echo $UPDATE_KEY > ${RCM_DIR}/version
|
||||||
|
|||||||
Reference in New Issue
Block a user