Compare commits

..

No commits in common. "main" and "MailcowBackupV3.0" have entirely different histories.

16 changed files with 459 additions and 592 deletions

48
.gitattributes vendored
View File

@ -10,17 +10,17 @@
# Documents # Documents
*.bibtex text diff=bibtex *.bibtex text diff=bibtex
*.doc diff=astextplain *.doc diff=astextplain
*.DOC diff=astextplain *.DOC diff=astextplain
*.docx diff=astextplain *.docx diff=astextplain
*.DOCX diff=astextplain *.DOCX diff=astextplain
*.dot diff=astextplain *.dot diff=astextplain
*.DOT diff=astextplain *.DOT diff=astextplain
*.pdf diff=astextplain *.pdf diff=astextplain
*.PDF diff=astextplain *.PDF diff=astextplain
*.rtf diff=astextplain *.rtf diff=astextplain
*.RTF diff=astextplain *.RTF diff=astextplain
*.md text diff=markdown *.md text
*.tex text diff=tex *.tex text diff=tex
*.adoc text *.adoc text
*.textile text *.textile text
@ -30,7 +30,6 @@
*.tsv text *.tsv text
*.txt text *.txt text
*.sql text *.sql text
*.ps1 text eol=crlf
# Graphics # Graphics
*.png binary *.png binary
@ -49,28 +48,11 @@
# Scripts # Scripts
*.bash text eol=lf *.bash text eol=lf
*.fish text eol=lf
*.sh text eol=lf *.sh text eol=lf
# These are explicitly windows files and should use crlf # These are explicitly windows files and should use crlf
*.bat text eol=crlf *.bat text eol=crlf
*.cmd text eol=crlf *.cmd text eol=crlf
*.ps1 text eol=crlf
# web frontend stack -- force LF so SRI hashes are always correct
*.html text eol=lf
*.htm text eol=lf
*.css text eol=lf
*.min.css text eol=lf
*.js text eol=lf
*.min.js text eol=lf
*.php text eol=lf
# Visual Studio projects (Rider also)
*.cs diff=csharp
*.sln merge=union
*.csproj merge=union
*.vbproj merge=union
*.fsproj merge=union
*.dbproj merge=union
# Serialisation # Serialisation
*.json text *.json text
@ -83,18 +65,12 @@
*.7z binary *.7z binary
*.gz binary *.gz binary
*.tar binary *.tar binary
*.tgz binary
*.zip binary *.zip binary
# Text files where line endings should be preserved
*.patch -text
# #
# Exclude files from exporting # Exclude files from exporting
# #
.gitattributes export-ignore .gitattributes export-ignore
.gitignore export-ignore .gitignore export-ignore
.gitkeep export-ignore
.idea export-ignore
.vscode export-ignore .vscode export-ignore

35
.gitignore vendored
View File

@ -1,27 +1,10 @@
# # ignore all vscode config files
# JetBrains exclusions .vscode/*
# !.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
!.vscode/numbered-bookmarks.json
# User-specific stuff # ignore all generated logfiles
.idea/**/workspace.xml *.log
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/shelf
# Generated files
.idea/**/contentModel.xml
# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
# modules
.idea_modules/
# Editor-based Rest Client
.idea/httpRequests

View File

@ -1,15 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GitToolBoxProjectSettings">
<option name="commitMessageIssueKeyValidationOverride">
<BoolValueOverride>
<option name="enabled" value="true" />
</BoolValueOverride>
</option>
<option name="commitMessageValidationEnabledOverride">
<BoolValueOverride>
<option name="enabled" value="true" />
</BoolValueOverride>
</option>
</component>
</project>

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="UserContentModel"> <component name="ContentModelUserStore">
<attachedFolders /> <attachedFolders />
<explicitIncludes /> <explicitIncludes />
<explicitExcludes /> <explicitExcludes />

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="RIDER_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$/../.." />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

View File

@ -4,19 +4,9 @@
<profile version="1.0"> <profile version="1.0">
<inspection_tool class="BodyLimit" enabled="true" level="ERROR" enabled_by_default="true" /> <inspection_tool class="BodyLimit" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="SubjectBodySeparation" enabled="true" level="ERROR" enabled_by_default="true" /> <inspection_tool class="SubjectBodySeparation" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="SubjectLimit" enabled="true" level="WARNING" enabled_by_default="true"> <inspection_tool class="SubjectLimit" enabled="true" level="ERROR" enabled_by_default="true" />
<option name="RIGHT_MARGIN" value="50" />
</inspection_tool>
</profile> </profile>
</component> </component>
<component name="GitSharedSettings">
<option name="FORCE_PUSH_PROHIBITED_PATTERNS">
<list>
<option value="master" />
<option value="main" />
</list>
</option>
</component>
<component name="VcsDirectoryMappings"> <component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" /> <mapping directory="$PROJECT_DIR$" vcs="Git" />
</component> </component>

3
.vscode/numbered-bookmarks.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"bookmarks": []
}

View File

@ -1,6 +1,6 @@
MIT License MIT License
Copyright (c) 2020-2022 Asif Bacchus (asif@asifbacchus.dev) Copyright (c) 2020 Asif Bacchus
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View File

@ -1,4 +1,4 @@
# Mailcow Backup Using borgbackup # Mailcow Backup Using borgbackup <!-- omit in toc -->
This script automates backing up your Mailcow installation using borgbackup and a remote ssh-capable storage system. I suggest using rsync.net since they This script automates backing up your Mailcow installation using borgbackup and a remote ssh-capable storage system. I suggest using rsync.net since they
have great speeds and a special pricing structure for borgbackup/attic users ([details here](https://www.rsync.net/products/attic.html)). have great speeds and a special pricing structure for borgbackup/attic users ([details here](https://www.rsync.net/products/attic.html)).
@ -14,19 +14,14 @@ # Mailcow Backup Using borgbackup
- Runs 'borg prune' to make sure you are trimming old backups on your schedule - Runs 'borg prune' to make sure you are trimming old backups on your schedule
- Creates a clear, easy to parse log file so you can keep an eye on your backups and any errors/warnings - Creates a clear, easy to parse log file so you can keep an eye on your backups and any errors/warnings
## contents ## Contents <!-- omit in toc -->
<!-- toc -->
- [quick start](#quick-start) - [quick start](#quick-start)
- [configuration file](#configuration-file) - [configuration file](#configuration-file)
- [running the script](#running-the-script) - [running the script](#running-the-script)
- [scheduling your backup via cron](#scheduling-your-backup-via-cron) - [scheduling your backup via cron](#scheduling-your-backup-via-cron)
- [restoring backups](#restoring-backups)
- [final notes](#final-notes) - [final notes](#final-notes)
<!-- tocstop -->
## quick start ## quick start
Clone this repo or download a release file into a directory of your choosing. For all examples in this document, I will assume you will run the script from */scripts/backup*. Make sure the script file is executable and you protect the *.details* file since it contains things like your repo password: Clone this repo or download a release file into a directory of your choosing. For all examples in this document, I will assume you will run the script from */scripts/backup*. Make sure the script file is executable and you protect the *.details* file since it contains things like your repo password:
@ -116,10 +111,6 @@ ## scheduling your backup via cron
7 1 * * * /scripts/backup/backup.sh -l /var/log/mailcow_backup.log > /dev/null 2>&1 7 1 * * * /scripts/backup/backup.sh -l /var/log/mailcow_backup.log > /dev/null 2>&1
``` ```
## restoring backups
Starting with version 3.0, a *restore.sh* file has been included to semi-automate restoring your backups to a clean mailcow instance. There are a few steps required and they are better explained in the wiki than would be possible in a short write-up like this. Please check out the [restore process overview](https://git.asifbacchus.app/asif/MailcowBackup/wiki/8.0-Restore-overview) for more information.
## final notes ## final notes
I think that's everything. For detailed information, please review the [wiki](https://git.asifbacchus.app/asif/MailcowBackup/wiki/_pages). If I've forgotten to document something there, please let me know. I know the wiki is long but, I hate how much stuff for Linux and open-source programs/scripts in general are so poorly documented especially for newbies and I didn't want to make that same mistake. I think that's everything. For detailed information, please review the [wiki](https://git.asifbacchus.app/asif/MailcowBackup/wiki/_pages). If I've forgotten to document something there, please let me know. I know the wiki is long but, I hate how much stuff for Linux and open-source programs/scripts in general are so poorly documented especially for newbies and I didn't want to make that same mistake.

View File

@ -20,11 +20,6 @@
# default is "$HOME" or "~$USER" in that order. If you're unsure, try "$HOME" # default is "$HOME" or "~$USER" in that order. If you're unsure, try "$HOME"
borgBaseDir="/var/borgbackup" borgBaseDir="/var/borgbackup"
# SSH port on which your borg server listens. To connect on a custom port,
# uncomment the line below and change the port number as required.
# If you leave this line commented, the default (port 22) will be used.
#borgSSHPort=22
# full path to the SSH key used to connect to your remote backup server # full path to the SSH key used to connect to your remote backup server
borgSSHKey="/var/borgbackup/private.key" borgSSHKey="/var/borgbackup/private.key"
@ -41,13 +36,12 @@ borgRepoPassphrase="p@ssW0rd"
# FULL PATH where the associated keyfile for your repo is located -- relevant # FULL PATH where the associated keyfile for your repo is located -- relevant
# only if your repo requires a keyfile (i.e. 'keyfile' vs 'repokey') and if you # only if your repo requires a keyfile (i.e. 'keyfile' vs 'repokey') and if you
# are not using the default keyfile location # are not using the default keyfile location
#borgKeyfileLocation="/var/borgbackup/.config/borg/keys/server_address__repo_name" borgKeyfileLocation="/var/borgbackup/.config/borg/keys/server_address__repo_name"
# additional files to backup # REQUIRED: path to text file containing a list (one per line) of files/
# by default, the script will only backup your mailcow program directory # directories to include in your backup. Since this is a generic backup script,
# (e.g. /opt/mailcow-dockerized) and your mailcow data volumes. If you would # nothing is defined by default. Therefore, ONLY files specified in this file
# like additional files included (perhaps certificates, system configuration, # will be backed up!
# etc.) include those locations listed one item per line in the file below
# see repo wiki for more details # see repo wiki for more details
borgXtraListPath="/scripts/backup/xtraLocations.borg" borgXtraListPath="/scripts/backup/xtraLocations.borg"

View File

@ -8,8 +8,9 @@
### 3. remote repo already set-up and configured ### 3. remote repo already set-up and configured
####### #######
### text formatting presents ### text formatting presents
if command -v tput >/dev/null; then if command -v tput > /dev/null; then
bold=$(tput bold) bold=$(tput bold)
cyan=$(tput setaf 6) cyan=$(tput setaf 6)
err=$(tput bold)$(tput setaf 1) err=$(tput bold)$(tput setaf 1)
@ -31,9 +32,11 @@ else
yellow="" yellow=""
fi fi
### trap ### trap
trap trapExit 1 2 3 6 trap trapExit 1 2 3 6
### functions ### functions
# bad configuration value passed in details file # bad configuration value passed in details file
@ -70,74 +73,73 @@ badParam() {
cleanup() { cleanup() {
# cleanup 503 if copied # cleanup 503 if copied
if [ "$err503Copied" -eq 1 ]; then if [ "$err503Copied" -eq 1 ]; then
# shellcheck disable=SC2129
if ! rm -f "$webroot/$err503File" 2>>"$logFile"; then if ! rm -f "$webroot/$err503File" 2>>"$logFile"; then
printf "%s[%s] -- [WARNING] Could not remove 503 error page." \ printf "%s[%s] -- [WARNING] Could not remove 503 error page." \
"$warn" "$(stamp)" >>"$logFile" "$warn" "$(stamp)" >> "$logFile"
printf " Web interface will not function until this file is " \ printf " Web interface will not function until this file is " \
>>"$logFile" >> "$logFile"
printf "removed --%s\n" "$norm" >>"$logFile" printf "removed --%s\n" "$norm" >> "$logFile"
warnCount=$((warnCount + 1)) warnCount=$((warnCount+1))
else else
printf "%s[%s] -- [INFO] 503 error page removed --%s\n" \ printf "%s[%s] -- [INFO] 503 error page removed --%s\n" \
"$cyan" "$(stamp)" "$norm" >>"$logFile" "$cyan" "$(stamp)" "$norm" >> "$logFile"
fi fi
fi fi
# cleanup SQL dump directory if created # cleanup SQL dump directory if created
if [ "$sqlDumpDirCreated" -eq 1 ]; then if [ "$sqlDumpDirCreated" -eq 1 ]; then
if ! rm -rf "$sqlDumpDir" 2>>"$logFile"; then if ! rm -rf "$sqlDumpDir" 2>>"$logFile"; then
printf "%s[%s] -- [WARNING] Could not remove temporary SQL-dump directory. Sorry for the mess. --%s\n" \ printf "%s[%s] -- [WARNING] Could not remove temporary SQL-dump directory. Sorry for the mess. --%s\n" \
"$warn" "$(stamp)" "$norm" >>"$logFile" "$warn" "$(stamp)" "$norm" >> "$logFile"
else else
printf "%s[%s] -- [INFO] Temporary SQL-dump directory removed successfully --%s\n" \ printf "%s[%s] -- [INFO] Temporary SQL-dump directory removed successfully --%s\n" \
"$cyan" "$(stamp)" "$norm" >>"$logFile" "$cyan" "$(stamp)" "$norm" >> "$logFile"
fi fi
fi fi
# start docker containers (no harm if they are already running) # start docker containers (no harm if they are already running)
doDocker start postfix doDocker start postfix
if [ "$dockerResultState" = "true" ]; then if [ "$dockerResultState" = "true" ]; then
printf "%s[%s] -- [INFO] POSTFIX container is running --%s\n" \ printf "%s[%s] -- [INFO] POSTFIX container is running --%s\n" \
"$cyan" "$(stamp)" "$norm" >>"$logFile" "$cyan" "$(stamp)" "$norm" >> "$logFile"
else else
exitError 102 'Could not start POSTFIX container.' 'final' exitError 102 'Could not start POSTFIX container.'
fi fi
doDocker start dovecot doDocker start dovecot
if [ "$dockerResultState" = "true" ]; then if [ "$dockerResultState" = "true" ]; then
printf "%s[%s] -- [INFO] DOVECOT container is running --%s\n" \ printf "%s[%s] -- [INFO] DOVECOT container is running --%s\n" \
"$cyan" "$(stamp)" "$norm" >>"$logFile" "$cyan" "$(stamp)" "$norm" >> "$logFile"
else else
exitError 102 'Could not start DOVECOT container.' 'final' exitError 102 'Could not start DOVECOT container.'
fi fi
} }
doDocker() { doDocker() {
containerName="$(docker ps -a --format '{{ .Names }}' --filter "name=${COMPOSE_PROJECT_NAME}_${2}-mailcow_1" --filter "name=${COMPOSE_PROJECT_NAME}-${2}-mailcow-1")" containerName="$( docker ps -a --format '{{ .Names }}' --filter name=${COMPOSE_PROJECT_NAME}_${2}-mailcow_1 )"
# determine action to take # determine action to take
if [ "$1" = "stop" ]; then if [ "$1" = "stop" ]; then
printf "%s[%s] -- [INFO] Stopping %s-mailcow container --%s\n" \ printf "%s[%s] -- [INFO] Stopping %s-mailcow container --%s\n" \
"$cyan" "$(stamp)" "$2" "$norm" >>"$logFile" "$cyan" "$(stamp)" "$2" "$norm" >> "$logFile"
${dockerCmd} -f "$mcDockerCompose" stop --timeout "$dockerStopTimeout" "$2-mailcow" 2>>"$logFile" docker-compose -f "$mcDockerCompose" stop --timeout "$dockerStopTimeout" "$2-mailcow" 2>> "$logFile"
# set result vars # set result vars
dockerResultState="$(docker inspect -f '{{ .State.Running }}' "$containerName")" dockerResultState="$( docker inspect -f '{{ .State.Running }}' $containerName )"
dockerResultExit="$(docker inspect -f '{{ .State.ExitCode }}' "$containerName")" dockerResultExit="$( docker inspect -f '{{ .State.ExitCode }}' $containerName )"
elif [ "$1" = "start" ]; then elif [ "$1" = "start" ]; then
printf "%s[%s] -- [INFO] Starting %s-mailcow container --%s\n" \ printf "%s[%s] -- [INFO] Starting %s-mailcow container --%s\n" \
"$cyan" "$(stamp)" "$2" "$norm" >>"$logFile" "$cyan" "$(stamp)" "$2" "$norm" >> "$logFile"
${dockerCmd} -f "$mcDockerCompose" start "$2-mailcow" 2>>"$logFile" docker-compose -f "$mcDockerCompose" start "$2-mailcow" 2>> "$logFile"
# set result vars # set result vars
dockerResultState="$(docker inspect -f '{{ .State.Running }}' "$containerName")" dockerResultState="$( docker inspect -f '{{ .State.Running }}' $containerName )"
fi fi
} }
# call cleanup and then exit with error report # call cleanup and then exit with error report
exitError() { exitError() {
printf "%s[%s] -- [ERROR] %s: %s --%s\n" \ printf "%s[%s] -- [ERROR] %s: %s --%s\n" \
"$err" "$(stamp)" "$1" "$2" "$norm" >>"$logFile" "$err" "$(stamp)" "$1" "$2" "$norm" >> "$logFile"
if [ ! "$3" = "final" ]; then cleanup; fi cleanup
# note script completion with error # note script completion with error
printf "%s[%s] --- %s execution completed with error ---%s\n" \ printf "%s[%s] --- %s execution completed with error ---%s\n" \
"$err" "$(stamp)" "$scriptName" "$norm" >>"$logFile" "$err" "$(stamp)" "$scriptName" "$norm" >> "$logFile"
exit "$1" exit "$1"
} }
@ -145,17 +147,12 @@ exitError() {
scriptHelp() { scriptHelp() {
newline newline
printf "%sUsage: %s [parameters]%s\n\n" "$bold" "$scriptName" "$norm" printf "%sUsage: %s [parameters]%s\n\n" "$bold" "$scriptName" "$norm"
textblock "There are NO mandatory parameters. If a parameter is not supplied, its default value will be used. In textblock "There are NO mandatory parameters. If a parameter is not supplied, its default value will be used. In the case of a switch parameter, it will remain DEactivated if NOT specified."
the case of a switch parameter, it will remain *deactivated* if NOT specified."
newline newline
textblock "Parameters are listed then followed by a description of their effect on the following line. Finally, if a default value exists, it will be listed on the next line in (parentheses)." textblock "Parameters are listed then followed by a description of their effect on the following line. Finally, if a default value exists, it will be listed on the next line in (parentheses)."
newline newline
textblock "${magenta}--- script related parameters ---${norm}" textblock "${magenta}--- script related parameters ---${norm}"
newline newline
switchTextblock "-b | --borg"
textblock "FULL path to borg executable file if not in the default location."
defaultsTextblock "(/usr/bin/borg)"
newline
switchTextblock "-c | --config | --details" switchTextblock "-c | --config | --details"
textblock "Path to the configuration key/value-pair file for this script." textblock "Path to the configuration key/value-pair file for this script."
defaultsTextblock "(scriptPath/scriptName.details)" defaultsTextblock "(scriptPath/scriptName.details)"
@ -167,10 +164,6 @@ scriptHelp() {
textblock "Path to write log file" textblock "Path to write log file"
defaultsTextblock "(scriptPath/scriptName.log)" defaultsTextblock "(scriptPath/scriptName.log)"
newline newline
switchTextblock "[SWITCH] --nc | --no-color | --no-colour"
textblock "Do NOT use ANSI colourization in the log file (in case your preferred log viewer does not support it)."
defaultsTextblock "(use ANSI colourization to make log file look good)"
newline
switchTextblock "--compression" switchTextblock "--compression"
textblock "Compression algorithm(s) that borg should use. Please run 'borg help compression' for details." textblock "Compression algorithm(s) that borg should use. Please run 'borg help compression' for details."
defaultsTextblock "(not specified, use borg default of lz4)" defaultsTextblock "(not specified, use borg default of lz4)"
@ -196,7 +189,7 @@ scriptHelp() {
textblock "${magenta}--- mailcow related ---${norm}" textblock "${magenta}--- mailcow related ---${norm}"
newline newline
switchTextblock "-d | --docker-compose" switchTextblock "-d | --docker-compose"
textblock "Path to the mailcow 'docker-compose.yml' file." textblock "Path to mailcow's 'docker-compose.yml' file."
defaultsTextblock "(/opt/mailcow-dockerized/docker-compose.yml)" defaultsTextblock "(/opt/mailcow-dockerized/docker-compose.yml)"
newline newline
switchTextblock "-m | --mailcow-config" switchTextblock "-m | --mailcow-config"
@ -240,33 +233,30 @@ newline() {
# same as exitError but for signal captures # same as exitError but for signal captures
trapExit() { trapExit() {
printf "%s[%s] -- [ERROR] 99: Caught signal --%s\n" \ printf "%s[%s] -- [ERROR] 99: Caught signal --%s\n" \
"$err" "$(stamp)" "$norm" >>"$logFile" "$err" "$(stamp)" "$norm" >> "$logFile"
cleanup cleanup
# note script completion with error # note script completion with error
printf "%s[%s] --- %s execution was terminated via signal ---%s\n" \ printf "%s[%s] --- %s execution was terminated via signal ---%s\n" \
"$err" "$(stamp)" "$scriptName" "$norm" >>"$logFile" "$err" "$(stamp)" "$scriptName" "$norm" >> "$logFile"
exit 99 exit 99
} }
### end of functions ### end of functions
### default variable values ### default variable values
## script related ## script related
# store logfile in the same directory as this script file using the same file # store logfile in the same directory as this script file using the same file
# name as the script but with the extension '.log' # name as the script but with the extension '.log'
scriptVersion="4.0" scriptPath="$( CDPATH='' cd -- "$( dirname -- "$0" )" && pwd -P )"
scriptPath="$(CDPATH='' \cd -- "$(dirname -- "$0")" && pwd -P)" scriptName="$( basename "$0" )"
scriptName="$(basename "$0")"
logFile="$scriptPath/${scriptName%.*}.log" logFile="$scriptPath/${scriptName%.*}.log"
colourizeLogFile=1
warnCount=0 warnCount=0
configDetails="$scriptPath/${scriptName%.*}.details" configDetails="$scriptPath/${scriptName%.*}.details"
err503Copied=0 err503Copied=0
sqlDumpDirCreated=0 sqlDumpDirCreated=0
exclusions=0 exclusions=0
dockerCmd="docker compose"
borgPath="/usr/bin/borg"
# borg output verbosity -- normal # borg output verbosity -- normal
borgCreateParams='--stats' borgCreateParams='--stats'
borgPruneParams='--list' borgPruneParams='--list'
@ -283,172 +273,148 @@ mcDockerCompose="/opt/mailcow-dockerized/docker-compose.yml"
dockerStartTimeout=180 dockerStartTimeout=180
dockerStopTimeout=120 dockerStopTimeout=120
### process startup parameters ### process startup parameters
while [ $# -gt 0 ]; do while [ $# -gt 0 ]; do
case "$1" in case "$1" in
-h | -\? | --help) -h|-\?|--help)
# display help # display help
scriptHelp scriptHelp
exit 0 exit 0
;; ;;
--version) -l|--log)
# display script version # set log file location
printf "\nMailcowBackup - Backup Mailcow using borgbackup to a remote SSH server : Version %s\n\n" ${scriptVersion} if [ -n "$2" ]; then
exit 0 logFile="${2%/}"
;;
-l | --log)
# set log file location
if [ -n "$2" ]; then
logFile="${2%/}"
shift
else
badParam empty "$@"
fi
;;
--nc | --no-color | --no-colour)
# do NOT colourize log file
colourizeLogFile=0
;;
-b | --borg)
# specify non-default borg path
if [ -n "$2" ]; then
borgPath="${2%/}"
shift
else
badParam empty "$@"
fi
;;
-c | --config | --details)
# location of config details file
if [ -n "$2" ]; then
if [ -f "$2" ]; then
configDetails="${2%/}"
shift shift
else else
badParam dne "$@" badParam empty "$@"
fi fi
else ;;
badParam empty "$@" -c|--config|--details)
fi # location of config details file
;; if [ -n "$2" ]; then
--compression) if [ -f "$2" ]; then
# set borg archive compression configDetails="${2%/}"
if [ -n "$2" ]; then shift
borgCompression="$2" else
shift badParam dne "$@"
else fi
badParam empty "$@" else
fi badParam empty "$@"
;; fi
-v | --verbose) ;;
# set verbose logging from borg --compression)
borgCreateParams='--list --stats' # set borg archive compression
borgPruneParams='--list' if [ -n "$2" ]; then
;; borgCompression="$2"
-5 | --use-503)
# enable copying 503 error page to webroot
use503=1
;;
--503-path)
# FULL path to 503 file
if [ -n "$2" ]; then
if [ -f "$2" ]; then
err503Path="${2%/}"
err503File="${2##*/}"
shift shift
else else
badParam dne "$@" badParam empty "$@"
fi fi
else ;;
badParam empty "$@" -v|--verbose)
fi # set verbose logging from borg
;; borgCreateParams='--list --stats'
-w | --webroot) borgPruneParams='--list'
# path to webroot (copy 503) ;;
if [ -n "$2" ]; then -5|--use-503)
if [ -d "$2" ]; then # enable copying 503 error page to webroot
webroot="${2%/}" use503=1
shift ;;
--503-path)
# FULL path to 503 file
if [ -n "$2" ]; then
if [ -f "$2" ]; then
err503Path="${2%/}"
err503File="${2##*/}"
shift
else
badParam dne "$@"
fi
else else
badParam dne "$@" badParam empty "$@"
fi fi
else ;;
badParam empty "$@" -w|--webroot)
fi # path to webroot (copy 503)
;; if [ -n "$2" ]; then
-d | --docker-compose) if [ -d "$2" ]; then
# FULL path to mailcow docker-compose file webroot="${2%/}"
if [ -n "$2" ]; then shift
if [ -f "$2" ]; then else
mcDockerCompose="$2" badParam dne "$@"
shift fi
else else
badParam dne "$@" badParam empty "$@"
fi fi
else ;;
badParam empty "$@" -d|--docker-compose)
fi # FULL path to mailcow docker-compose file
;; if [ -n "$2" ]; then
-m | --mailcow-config) if [ -f "$2" ]; then
# FULL path to mailcow configuration file mcDockerCompose="$2"
if [ -n "$2" ]; then shift
if [ -f "$2" ]; then else
mcConfig="$2" badParam dne "$@"
shift fi
else else
badParam dne "$@" badParam empty "$@"
fi fi
else ;;
badParam empty "$@" -m|--mailcow-config)
fi # FULL path to mailcow configuration file
;; if [ -n "$2" ]; then
-t1 | --timeout-start) if [ -f "$2" ]; then
if [ -z "$2" ]; then mcConfig="$2"
badParam empty "$@" shift
else else
dockerStartTimeout="$2" badParam dne "$@"
shift fi
fi else
;; badParam empty "$@"
-t2 | --timeout-stop) fi
if [ -z "$2" ]; then ;;
badParam empty "$@" -t1|--timeout-start)
else if [ -z "$2" ]; then
dockerStopTimeout="$2" badParam empty "$@"
shift else
fi dockerStartTimeout="$2"
;; shift
*) fi
printf "\n%sUnknown option: %s\n" "$err" "$1" ;;
printf "%sUse '--help' for valid options.%s\n\n" "$cyan" "$norm" -t2|--timeout-stop)
exit 1 if [ -z "$2" ]; then
;; badParam empty "$@"
else
dockerStopTimeout="$2"
shift
fi
;;
*)
printf "\n%sUnknown option: %s\n" "$err" "$1"
printf "%sUse '--help' for valid options.%s\n\n" "$cyan" "$norm"
exit 1
;;
esac esac
shift shift
done done
### check pre-requisites and default values ### check pre-requisites and default values
# check if running as root, otherwise exit # check if running as root, otherwise exit
if [ "$(id -u)" -ne 0 ]; then if [ "$( id -u )" -ne 0 ]; then
printf "\n%sERROR: script MUST be run as ROOT%s\n\n" "$err" "$norm" printf "\n%sERROR: script MUST be run as ROOT%s\n\n" "$err" "$norm"
exit 2 exit 2
fi fi
# verify compose installed and set proper commands based on version
if ! (docker compose version >/dev/null 2>&1); then
if ! (docker-compose --version >/dev/null 2>&1); then
printf "\n%sERROR: Docker Compose not installed or not functioning%s\n\n" "$err" "$norm"
exit 3
fi
dockerCmd="docker-compose"
fi
# does the details file exist? # does the details file exist?
if [ ! -f "$configDetails" ]; then if [ ! -f "$configDetails" ]; then
badParam dne "(--details default)" "$configDetails" badParam dne "(--details default)" "$configDetails"
fi fi
# is borg installed? # is borg installed?
if ! find "$borgPath" -type f -executable >/dev/null 2>&1; then if ! command -v borg > /dev/null; then
printf "\n%sERROR: BORG cannot be found in the specified or default location on this system!%s\n\n" "$err" "$norm" printf "\n%sERROR: BORG is not installed on this system!%s\n\n" "$err" "$norm"
exit 4 exit 3
fi fi
# if 503 functionality is enabled, do 503 related files exist? # if 503 functionality is enabled, do 503 related files exist?
if [ "$use503" -eq 1 ]; then if [ "$use503" -eq 1 ]; then
@ -470,17 +436,6 @@ fi
if [ -n "$borgCompression" ]; then if [ -n "$borgCompression" ]; then
borgCreateParams="${borgCreateParams} --compression ${borgCompression}" borgCreateParams="${borgCreateParams} --compression ${borgCompression}"
fi fi
# remove colourization if parameter specified
if [ "$colourizeLogFile" -eq 0 ]; then
bold=""
cyan=""
err=""
magenta=""
norm=""
ok=""
warn=""
yellow=""
fi
### read mailcow.conf and set vars as needed ### read mailcow.conf and set vars as needed
# shellcheck source=./mailcow.conf.shellcheck # shellcheck source=./mailcow.conf.shellcheck
@ -488,49 +443,53 @@ fi
export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
export COMPOSE_HTTP_TIMEOUT="$dockerStartTimeout" export COMPOSE_HTTP_TIMEOUT="$dockerStartTimeout"
### start logging ### start logging
printf "%s[%s] --- Start %s execution ---%s\n" \ printf "%s[%s] --- Start %s execution ---%s\n" \
"$magenta" "$(stamp)" "$scriptName" "$norm" >>"$logFile" "$magenta" "$(stamp)" "$scriptName" "$norm" >> "$logFile"
printf "%s[%s] -- [INFO] Log located at %s%s%s --%s\n" \ printf "%s[%s] -- [INFO] Log located at %s%s%s --%s\n" \
"$cyan" "$(stamp)" "$yellow" "$logFile" "$cyan" "$norm" >>"$logFile" "$cyan" "$(stamp)" "$yellow" "$logFile" "$cyan" "$norm" >> "$logFile"
### get location of docker volumes ### get location of docker volumes
dockerVolumeMail=$(docker volume inspect -f '{{ .Mountpoint }}' ${COMPOSE_PROJECT_NAME}_vmail-vol-1) dockerVolumeMail=$(docker volume inspect -f '{{ .Mountpoint }}' ${COMPOSE_PROJECT_NAME}_vmail-vol-1)
printf "%s[%s] -- [INFO] Using MAIL volume: %s --%s\n" \ printf "%s[%s] -- [INFO] Using MAIL volume: %s --%s\n" \
"$cyan" "$(stamp)" "$dockerVolumeMail" "$norm" >>"$logFile" "$cyan" "$(stamp)" "$dockerVolumeMail" "$norm" >> "$logFile"
dockerVolumeRspamd=$(docker volume inspect -f '{{ .Mountpoint }}' ${COMPOSE_PROJECT_NAME}_rspamd-vol-1) dockerVolumeRspamd=$(docker volume inspect -f '{{ .Mountpoint }}' ${COMPOSE_PROJECT_NAME}_rspamd-vol-1)
printf "%s[%s] -- [INFO] Using RSPAMD volume: %s --%s\n" \ printf "%s[%s] -- [INFO] Using RSPAMD volume: %s --%s\n" \
"$cyan" "$(stamp)" "$dockerVolumeRspamd" "$norm" >>"$logFile" "$cyan" "$(stamp)" "$dockerVolumeRspamd" "$norm" >> "$logFile"
dockerVolumePostfix=$(docker volume inspect -f '{{ .Mountpoint }}' ${COMPOSE_PROJECT_NAME}_postfix-vol-1) dockerVolumePostfix=$(docker volume inspect -f '{{ .Mountpoint }}' ${COMPOSE_PROJECT_NAME}_postfix-vol-1)
printf "%s[%s] -- [INFO] Using POSTFIX volume: %s --%s\n" \ printf "%s[%s] -- [INFO] Using POSTFIX volume: %s --%s\n" \
"$cyan" "$(stamp)" "$dockerVolumePostfix" "$norm" >>"$logFile" "$cyan" "$(stamp)" "$dockerVolumePostfix" "$norm" >> "$logFile"
dockerVolumeRedis=$(docker volume inspect -f '{{ .Mountpoint }}' ${COMPOSE_PROJECT_NAME}_redis-vol-1) dockerVolumeRedis=$(docker volume inspect -f '{{ .Mountpoint }}' ${COMPOSE_PROJECT_NAME}_redis-vol-1)
printf "%s[%s] -- [INFO] Using REDIS volume: %s --%s\n" \ printf "%s[%s] -- [INFO] Using REDIS volume: %s --%s\n" \
"$cyan" "$(stamp)" "$dockerVolumeRedis" "$norm" >>"$logFile" "$cyan" "$(stamp)" "$dockerVolumeRedis" "$norm" >> "$logFile"
dockerVolumeCrypt=$(docker volume inspect -f '{{ .Mountpoint }}' ${COMPOSE_PROJECT_NAME}_crypt-vol-1) dockerVolumeCrypt=$(docker volume inspect -f '{{ .Mountpoint }}' ${COMPOSE_PROJECT_NAME}_crypt-vol-1)
printf "%s[%s] -- [INFO] Using MAILCRYPT volume: %s --%s\n" \ printf "%s[%s] -- [INFO] Using MAILCRYPT volume: %s --%s\n" \
"$cyan" "$(stamp)" "$dockerVolumeCrypt" "$norm" >>"$logFile" "$cyan" "$(stamp)" "$dockerVolumeCrypt" "$norm" >> "$logFile"
### read details file to get variables needed run borg ### read details file to get variables needed run borg
# check if config details file was provided as a relative or absolute path # check if config details file was provided as a relative or absolute path
case "${configDetails}" in case "${configDetails}" in
/*) /*)
# absolute path, no need to rewrite variable # absolute path, no need to rewrite variable
# shellcheck source=./backup.details # shellcheck source=./backup.details
. "${configDetails}" . "${configDetails}"
;; ;;
*) *)
# relative path, prepend './' to create absolute path # relative path, prepend './' to create absolute path
# shellcheck source=./backup.details # shellcheck source=./backup.details
. "./${configDetails}" . "./${configDetails}"
;; ;;
esac esac
printf "%s[%s] -- [INFO] %s%s%s imported --%s\n" \ printf "%s[%s] -- [INFO] %s%s%s imported --%s\n" \
"$cyan" "$(stamp)" "$yellow" "$configDetails" "$cyan" "$norm" >>"$logFile" "$cyan" "$(stamp)" "$yellow" "$configDetails" "$cyan" "$norm" >> "$logFile"
### Run borg variable checks ### Run borg variable checks
printf "%s[%s] -- [INFO] Verifying supplied borg details --%s\n" \ printf "%s[%s] -- [INFO] Verifying supplied borg details --%s\n" \
"$cyan" "$(stamp)" "$norm" >>"$logFile" "$cyan" "$(stamp)" "$norm" >> "$logFile"
## verify borg base directory ## verify borg base directory
if [ -z "${borgBaseDir}" ]; then if [ -z "${borgBaseDir}" ]; then
@ -539,7 +498,7 @@ elif [ ! -d "${borgBaseDir}" ]; then
badDetails dne 'borgBaseDir' badDetails dne 'borgBaseDir'
fi fi
printf "%sdetails:borgBaseDir %s-- %s[OK]%s\n" \ printf "%sdetails:borgBaseDir %s-- %s[OK]%s\n" \
"$magenta" "$norm" "$ok" "$norm" >>"$logFile" "$magenta" "$norm" "$ok" "$norm" >> "$logFile"
export BORG_BASE_DIR="${borgBaseDir%/}" export BORG_BASE_DIR="${borgBaseDir%/}"
## check path to SSH keyfile ## check path to SSH keyfile
@ -549,37 +508,28 @@ elif [ ! -f "${borgSSHKey}" ]; then
badDetails dne 'borgSSHKey' badDetails dne 'borgSSHKey'
fi fi
printf "%sdetails:borgSSHKey %s-- %s[OK]%s\n" \ printf "%sdetails:borgSSHKey %s-- %s[OK]%s\n" \
"$magenta" "$norm" "$ok" "$norm" >>"$logFile" "$magenta" "$norm" "$ok" "$norm" >> "$logFile"
## check SSH port export BORG_RSH="ssh -i ${borgSSHKey}"
if [ -z "${borgSSHPort}" ]; then
borgSSHPort=22
printf "%sdetails:borgSSHPort %s-- %s[DEFAULT]%s\n" \
"$magenta" "$norm" "$ok" "$norm" >>"$logFile"
else
printf "%sdetails:borgSSHPort %s-- %s[CUSTOM]%s\n" \
"$magenta" "$norm" "$ok" "$norm" >>"$logFile"
fi
export BORG_RSH="ssh -i ${borgSSHKey} -p ${borgSSHPort}"
## check borg repo connect string ## check borg repo connect string
if [ -z "${borgConnectRepo}" ]; then if [ -z "${borgConnectRepo}" ]; then
badDetails empty 'borgConnectRepo' badDetails empty 'borgConnectRepo'
fi fi
printf "%sdetails:borgConnectRepo %s-- %s[OK]%s\n" \ printf "%sdetails:borgConnectRepo %s-- %s[OK]%s\n" \
"$magenta" "$norm" "$ok" "$norm" >>"$logFile" "$magenta" "$norm" "$ok" "$norm" >> "$logFile"
export BORG_REPO="${borgConnectRepo}" export BORG_REPO="${borgConnectRepo}"
## check borg repo password ## check borg repo password
if [ -n "${borgRepoPassphrase}" ]; then if [ -n "${borgRepoPassphrase}" ]; then
printf "%sdetails:borgRepoPassphrase %s-- %s[OK]%s\n" \ printf "%sdetails:borgRepoPassphrase %s-- %s[OK]%s\n" \
"$magenta" "$norm" "$ok" "$norm" >>"$logFile" "$magenta" "$norm" "$ok" "$norm" >> "$logFile"
export BORG_PASSPHRASE="${borgRepoPassphrase}" export BORG_PASSPHRASE="${borgRepoPassphrase}"
else else
# if passwd is blank intentionally, this is insecure # if passwd is blank intentionally, this is insecure
printf "%s-- [WARNING] Using a borg repo without a password is an " \ printf "%s-- [WARNING] Using a borg repo without a password is an " \
"$warn" >>"$logFile" "$warn" >> "$logFile"
printf "insecure configuration --%s\n" "$norm" >>"$logFile" printf "insecure configuration --%s\n" "$norm">> "$logFile"
warnCount=$((warnCount + 1)) warnCount=$((warnCount+1))
# if this was an accident, we need to provide a bogus passwd so borg fails # if this was an accident, we need to provide a bogus passwd so borg fails
# otherwise it will sit forever just waiting for input # otherwise it will sit forever just waiting for input
export BORG_PASSPHRASE="DummyPasswordSoBorgFails" export BORG_PASSPHRASE="DummyPasswordSoBorgFails"
@ -587,26 +537,26 @@ fi
## check borg repository keyfile location ## check borg repository keyfile location
if [ -z "${borgKeyfileLocation}" ]; then if [ -z "${borgKeyfileLocation}" ]; then
printf "%sdetails:borgKeyfileLocation %s-- %s[DEFAULT]%s\n" "$magenta" "$norm" "$ok" "$norm" >>"$logFile" printf "%sdetails:borgKeyfileLocation %s-- %s[DEFAULT]%s\n" "$magenta" "$norm" "$ok" "$norm" >> "$logFile"
else else
# check if keyfile location exists # check if keyfile location exists
if [ ! -f "${borgKeyfileLocation}" ]; then if [ ! -f "${borgKeyfileLocation}" ]; then
badDetails dne 'borgKeyfileLocation' badDetails dne 'borgKeyfileLocation'
fi fi
printf "%sdetails:borgKeyfileLocation %s-- %s[OK]%s\n" "$magenta" "$norm" "$ok" "$norm" >>"$logFile" printf "%sdetails:borgKeyfileLocation %s-- %s[OK]%s\n" "$magenta" "$norm" "$ok" "$norm" >> "$logFile"
export BORG_KEY_FILE="${borgKeyfileLocation}" export BORG_KEY_FILE="${borgKeyfileLocation}"
fi fi
## export borg remote path, if specified ## export borg remote path, if specified
if [ -n "${borgRemote}" ]; then export BORG_REMOTE_PATH="${borgRemote}"; fi if [ -n "${borgRemote}" ]; then export BORG_REMOTE_PATH="${borgRemote}"; fi
## check if exclusion list file is specified ## check if exlusion list file is specified
if [ -n "${borgExcludeListPath}" ]; then if [ -n "${borgExcludeListPath}" ]; then
# check if the file actually exists # check if the file actually exists
if [ ! -f "${borgExcludeListPath}" ]; then if [ ! -f "${borgExcludeListPath}" ]; then
badDetails dne 'borgExcludeListPath' badDetails dne 'borgExcludeListPath'
fi fi
exclusions=1 exclusions=1
fi fi
## read additional files ## read additional files
@ -625,23 +575,25 @@ else
xtraList="${xtraList} ${xtraItem}" xtraList="${xtraList} ${xtraItem}"
fi fi
done <<EOF done <<EOF
$(sed -e '/^\s*#.*$/d' -e '/^\s*$/d' "${borgXtraListPath}") $( sed -e '/^\s*#.*$/d' -e '/^\s*$/d' "${borgXtraListPath}" )
EOF EOF
printf "%sdetails:borgXtraListPath %s-- %s[OK]%s\n" \ printf "%sdetails:borgXtraListPath %s-- %s[OK]%s\n" \
"$magenta" "$norm" "$ok" "$norm" >>"$logFile" "$magenta" "$norm" "$ok" "$norm" >> "$logFile"
fi fi
### set location of sql dump ### set location of sql dump
# this is done before resetting default TMP dir for borg # this is done before resetting default TMP dir for borg
if ! sqlDumpDir=$(mktemp -d 2>/dev/null); then if ! sqlDumpDir=$( mktemp -d 2>/dev/null ); then
exitError 115 'Unable to create temp directory for SQL dump.' exitError 115 'Unable to create temp directory for SQL dump.'
else else
sqlDumpFile="backup-$(date +%Y%m%d_%H%M%S).sql" sqlDumpFile="backup-$( date +%Y%m%d_%H%M%S ).sql"
sqlDumpDirCreated=1 sqlDumpDirCreated=1
printf "%s[%s] -- [INFO] SQL dump file will be stored at: %s --%s\n" \ printf "%s[%s] -- [INFO] SQL dump file will be stored at: %s --%s\n" \
"$cyan" "$(stamp)" "$sqlDumpDir/$sqlDumpFile" "$norm" >>"$logFile" "$cyan" "$(stamp)" "$sqlDumpDir/$sqlDumpFile" "$norm" >> "$logFile"
fi fi
### create borg temp dir: ### create borg temp dir:
## python requires a writable temporary directory when unpacking borg and ## python requires a writable temporary directory when unpacking borg and
## executing commands. This defaults to /tmp but many systems mount /tmp with ## executing commands. This defaults to /tmp but many systems mount /tmp with
@ -654,25 +606,27 @@ if [ ! -d "${borgBaseDir}/tmp" ]; then
exitError 132 "Unable to create borg ${borgBaseDir}/tmp directory" exitError 132 "Unable to create borg ${borgBaseDir}/tmp directory"
else else
printf "%s[%s] -- [INFO] Created %s%s/tmp " \ printf "%s[%s] -- [INFO] Created %s%s/tmp " \
"$cyan" "$(stamp)" "$yellow" "${borgBaseDir}" >>"$logFile" "$cyan" "$(stamp)" "$yellow" "${borgBaseDir}" >> "$logFile"
printf "%s--%s\n" "$cyan" "$norm" >>"$logFile" printf "%s--%s\n" "$cyan" "$norm">> "$logFile"
fi fi
fi fi
export TMPDIR="${borgBaseDir}/tmp" export TMPDIR="${borgBaseDir}/tmp"
### 503 functionality ### 503 functionality
if [ "$use503" -eq 1 ]; then if [ "$use503" -eq 1 ]; then
printf "%s[%s] -- [INFO] Copying 503 error page to " \ printf "%s[%s] -- [INFO] Copying 503 error page to " \
"$cyan" "$(stamp)" >>"$logFile" "$cyan" "$(stamp)" >> "$logFile"
printf "webroot -- %s\n" "$norm" >>"$logFile" printf "webroot -- %s\n" "$norm">> "$logFile"
if ! \cp --force "${err503Path}" "${webroot}/${err503File}" 2>>"$logFile"; then if ! cp --force "${err503Path}" "${webroot}/${err503File}" 2>> "$logFile"
then
printf "%s[%s] -- [WARNING] Failed to copy 503 error page. " \ printf "%s[%s] -- [WARNING] Failed to copy 503 error page. " \
"$warn" "$(stamp)" >>"$logFile" "$warn" "$(stamp)" >> "$logFile"
printf "Web users will NOT be notified --%s\n" "$norm" >>"$logFile" printf "Web users will NOT be notified --%s\n" "$norm" >> "$logFile"
warnCount=$((warnCount + 1)) warnCount=$((warnCount+1))
else else
printf "%s[%s] -- [SUCCESS] 503 error page copied --%s\n" \ printf "%s[%s] -- [SUCCESS] 503 error page copied --%s\n" \
"$ok" "$(stamp)" "$norm" >>"$logFile" "$ok" "$(stamp)" "$norm" >> "$logFile"
# set cleanup flag # set cleanup flag
err503Copied=1 err503Copied=1
fi fi
@ -685,31 +639,33 @@ fi
doDocker stop postfix doDocker stop postfix
if [ "$dockerResultState" = "false" ] && [ "$dockerResultExit" -eq 0 ]; then if [ "$dockerResultState" = "false" ] && [ "$dockerResultExit" -eq 0 ]; then
printf "%s[%s] -- [INFO] POSTFIX container stopped --%s\n" \ printf "%s[%s] -- [INFO] POSTFIX container stopped --%s\n" \
"$cyan" "$(stamp)" "$norm" >>"$logFile" "$cyan" "$(stamp)" "$norm" >> "$logFile"
else else
exitError 101 'Could not stop POSTFIX container.' exitError 101 'Could not stop POSTFIX container.'
fi fi
doDocker stop dovecot doDocker stop dovecot
if [ "$dockerResultState" = "false" ] && [ "$dockerResultExit" -eq 0 ]; then if [ "$dockerResultState" = "false" ] && [ "$dockerResultExit" -eq 0 ]; then
printf "%s[%s] -- [INFO] DOVECOT container stopped --%s\n" \ printf "%s[%s] -- [INFO] DOVECOT container stopped --%s\n" \
"$cyan" "$(stamp)" "$norm" >>"$logFile" "$cyan" "$(stamp)" "$norm" >> "$logFile"
else else
exitError 101 'Could not stop DOVECOT container.' exitError 101 'Could not stop DOVECOT container.'
fi fi
### dump SQL ### dump SQL
printf "%s[%s] -- [INFO] Dumping mailcow SQL database --%s\n" \ printf "%s[%s] -- [INFO] Dumping mailcow SQL database --%s\n" \
"$cyan" "$(stamp)" "$norm" >>"$logFile" "$cyan" "$(stamp)" "$norm" >> "$logFile"
${dockerCmd} exec -T mysql-mailcow mysqldump --default-character-set=utf8mb4 \ docker-compose exec -T mysql-mailcow mysqldump --default-character-set=utf8mb4 \
-u${DBUSER} -p${DBPASS} ${DBNAME} >"$sqlDumpDir/$sqlDumpFile" 2>>"$logFile" -u${DBUSER} -p${DBPASS} ${DBNAME} > "$sqlDumpDir/$sqlDumpFile" 2>> "$logFile"
dumpResult=$(${dockerCmd} exec -T mysql-mailcow echo "$?") dumpResult=$( docker-compose exec -T mysql-mailcow echo "$?" )
if [ "$dumpResult" -eq 0 ]; then if [ "$dumpResult" -eq 0 ]; then
printf "%s[%s] -- [INFO] SQL database dumped successfully --%s\n" \ printf "%s[%s] -- [INFO] SQL database dumped successfully --%s\n" \
"$cyan" "$(stamp)" "$norm" >>"$logFile" "cyan" "$(stamp)" "$norm" >> "$logFile"
else else
exitError 118 'There was an error dumping the mailcow SQL database.' exitError 118 'There was an error dumping the mailcow SQL database.'
fi fi
### dump redis inside container ### dump redis inside container
# delete old redis dump if it exists # delete old redis dump if it exists
if [ -f "$dockerVolumeRedis/dump.rdb" ]; then if [ -f "$dockerVolumeRedis/dump.rdb" ]; then
@ -717,23 +673,24 @@ if [ -f "$dockerVolumeRedis/dump.rdb" ]; then
fi fi
# dump redis # dump redis
printf "%s[%s] -- [INFO] Dumping mailcow redis database --%s\n" \ printf "%s[%s] -- [INFO] Dumping mailcow redis database --%s\n" \
"$cyan" "$(stamp)" "$norm" >>"$logFile" "$cyan" "$(stamp)" "$norm" >> "$logFile"
${dockerCmd} exec -T redis-mailcow redis-cli save >>"$logFile" 2>&1 docker-compose exec -T redis-mailcow redis-cli save >> "$logFile" 2>&1
rdumpResult=$(${dockerCmd} exec -T redis-mailcow echo "$?") rdumpResult=$( docker-compose exec -T redis-mailcow echo "$?" )
if [ "$rdumpResult" -eq 0 ]; then if [ "$rdumpResult" -eq 0 ]; then
printf "%s[%s] -- [INFO] mailcow redis dumped successfully --%s\n" \ printf "%s[%s] -- [INFO] mailcow redis dumped successfully --%s\n" \
"$cyan" "$(stamp)" "$norm" >>"$logFile" "cyan" "$(stamp)" "$norm" >> "$logFile"
else else
exitError 119 'There was an error dumping the mailcow redis database.' exitError 119 'There was an error dumping the mailcow redis database.'
fi fi
### execute borg depending on whether exclusions are defined ### execute borg depending on whether exclusions are defined
printf "%s[%s] -- [INFO] Pre-backup tasks completed, calling borgbackup --%s\n" "$cyan" "$(stamp)" "$norm" >>"$logFile" printf "%s[%s] -- [INFO] Pre-backup tasks completed, calling borgbackup --%s\n" "$cyan" "$(stamp)" "$norm" >> "$logFile"
## construct the proper borg commandline ## construct the proper borg commandline
# base command # base command
if [ "$exclusions" -eq 0 ]; then if [ "$exclusions" -eq 0 ]; then
borgCMD="${borgPath} create --show-rc ${borgCreateParams} \ borgCMD="borg --show-rc create ${borgCreateParams} \
::$(date +%Y-%m-%d_%H%M%S) \ ::$(date +%Y-%m-%d_%H%M%S) \
${mcConfig%/*} \ ${mcConfig%/*} \
${sqlDumpDir} \ ${sqlDumpDir} \
@ -744,7 +701,7 @@ if [ "$exclusions" -eq 0 ]; then
${dockerVolumeCrypt} \ ${dockerVolumeCrypt} \
${xtraList}" ${xtraList}"
elif [ "$exclusions" -eq 1 ]; then elif [ "$exclusions" -eq 1 ]; then
borgCMD="${borgPath} create --show-rc ${borgCreateParams} \ borgCMD="borg --show-rc create ${borgCreateParams} \
--exclude-from ${borgExcludeListPath} \ --exclude-from ${borgExcludeListPath} \
::$(date +%Y-%m-%d_%H%M%S) \ ::$(date +%Y-%m-%d_%H%M%S) \
${mcConfig%/*} \ ${mcConfig%/*} \
@ -759,95 +716,96 @@ fi
# execute borg # execute borg
printf "%s[%s] -- [INFO] Executing borg backup operation --%s\n" \ printf "%s[%s] -- [INFO] Executing borg backup operation --%s\n" \
"$cyan" "$(stamp)" "$norm" >>"$logFile" "$cyan" "$(stamp)" "$norm" >> "$logFile"
${borgCMD} 2>>"$logFile" ${borgCMD} 2>> "$logFile"
borgResult="$?" borgResult="$?"
## check borg exit status ## check borg exit status
if [ "$borgResult" -eq 0 ]; then if [ "$borgResult" -eq 0 ]; then
printf "%s[%s] -- [SUCCESS] Borg backup completed --%s\n" \ printf "%s[%s] -- [SUCCESS] Borg backup completed --%s\n" \
"$ok" "$(stamp)" "$norm" >>"$logFile" "$ok" "$(stamp)" "$norm" >> "$logFile"
elif [ "$borgResult" -eq 1 ]; then elif [ "$borgResult" -eq 1 ]; then
printf "%s[%s] -- [WARNING] Borg completed with warnings. " \ printf "%s[%s] -- [WARNING] Borg completed with warnings. " \
"$warn" "$(stamp)" >>"$logFile" "$warn" "$(stamp)" >> "$logFile"
printf "Review this logfile for details --%s\n" "$norm" >>"$logFile" printf "Review this logfile for details --%s\n" "$norm">> "$logFile"
warnCount=$((warnCount + 1)) warnCount=$((warnCount+1))
elif [ "$borgResult" -ge 2 ]; then elif [ "$borgResult" -ge 2 ]; then
err_1="Borg exited with a critical error. Please review this log file" err_1="Borg exited with a critical error. Please review this log file"
err_2="for details." err_2="for details."
exitError 138 "$err_1 $err_2" exitError 138 "$err_1 $err_2"
else else
printf "%s[%s] -- [WARNING] Borg exited with unknown return code. " \ printf "%s[%s] -- [WARNING] Borg exited with unknown return code. " \
"$warn" "$(stamp)" >>"$logFile" "$warn" "$(stamp)" >> "$logFile"
printf "Review this logfile for details --%s\n" "$norm" >>"$logFile" printf "Review this logfile for details --%s\n" "$norm">> "$logFile"
warnCount=$((warnCount + 1)) warnCount=$((warnCount+1))
fi fi
### execute borg prune if parameters are provided, otherwise skip with a warning
### execute borg prune if paramters are provided, otherwise skip with a warning
if [ -n "${borgPruneSettings}" ]; then if [ -n "${borgPruneSettings}" ]; then
printf "%s[%s] -- [INFO] Executing borg prune operation --%s\n" \ printf "%s[%s] -- [INFO] Executing borg prune operation --%s\n" \
"$cyan" "$(stamp)" "$norm" >>"$logFile" "$cyan" "$(stamp)" "$norm" >> "$logFile"
# shellcheck disable=SC2086 borg prune --show-rc -v ${borgPruneParams} ${borgPruneSettings} \
"${borgPath}" prune --show-rc -v ${borgPruneParams} ${borgPruneSettings} \ 2>> "$logFile"
2>>"$logFile"
borgPruneResult="$?" borgPruneResult="$?"
else else
printf "%s[%s] -- [WARNING] No prune parameters provided. " \ printf "%s[%s] -- [WARNING] No prune parameters provided. " \
"$warn" "$(stamp)" >>"$logFile" "$warn" "$(stamp)" >> "$logFile"
printf "Your archive will continue growing with each backup --%s\n" \ printf "Your archive will continue growing with each backup --%s\n" \
"$norm" >>"$logFile" "$norm" >> "$logFile"
warnCount=$((warnCount + 1)) warnCount=$((warnCount+1))
fi fi
## report on prune operation if executed ## report on prune operation if executed
if [ -n "${borgPruneResult}" ]; then if [ -n "${borgPruneResult}" ]; then
if [ "${borgPruneResult}" -eq 0 ]; then if [ "${borgPruneResult}" -eq 0 ]; then
printf "%s[%s] -- [SUCCESS] Borg prune completed --%s\n" \ printf "%s[%s] -- [SUCCESS] Borg prune completed --%s\n" \
"$ok" "$(stamp)" "$norm" >>"$logFile" "$ok" "$(stamp)" "$norm" >> "$logFile"
elif [ "$borgPruneResult" -eq 1 ]; then elif [ "$borgPruneResult" -eq 1 ]; then
printf "%s[%s] -- [WARNING] Borg prune completed with warnings. " \ printf "%s[%s] -- [WARNING] Borg prune completed with warnings. " \
"$warn" "$(stamp)" >>"$logFile" "$warn" "$(stamp)" >> "$logFile"
printf "Review this logfile for details --%s\n" "$norm" >>"$logFile" printf "Review this logfile for details --%s\n" "$norm" >> "$logFile"
warnCount=$((warnCount + 1)) warnCount=$((warnCount+1))
elif [ "$borgPruneResult" -ge 2 ]; then elif [ "$borgPruneResult" -ge 2 ]; then
err_1="Borg prune exited with a critical error. Please review this" err_1="Borg prune exited with a critical error. Please review this"
err_2="log file for details." err_2="log file for details."
exitError 139 "$err_1 $err_2" exitError 139 "$err_1 $err_2"
else else
printf "%s[%s] -- [WARNING] Borg prune exited with an unknown " \ printf "%s[%s] -- [WARNING] Borg prune exited with an unknown " \
"$warn" "$(stamp)" >>"$logFile" "$warn" "$(stamp)" >> "$logFile"
printf "return code. Review this logfile for details --%s\n" \ printf "return code. Review this logfile for details --%s\n" \
"$norm" >>"$logFile" "$norm" >> "$logFile"
warnCount=$((warnCount + 1)) warnCount=$((warnCount+1))
fi fi
fi fi
### all processes successfully completed, cleanup and exit gracefully ### all processes successfully completed, cleanup and exit gracefully
# note successful completion of borg commands # note successful completion of borg commands
printf "%s[%s] -- [SUCCESS] Backup operations completed --%s\n" \ printf "%s[%s] -- [SUCCESS] Backup operations completed --%s\n" \
"$ok" "$(stamp)" "$norm" >>"$logFile" "$ok" "$(stamp)" "$norm" >> "$logFile"
# cleanup # cleanup
cleanup cleanup
# note complete success, tally warnings and exit # note complete success, tally warnings and exit
printf "%s[%s] -- [SUCCESS] All processes completed --%s\n" \ printf "%s[%s] -- [SUCCESS] All processes completed --%s\n" \
"$ok" "$(stamp)" "$norm" >>"$logFile" "$ok" "$(stamp)" "$norm" >> "$logFile"
printf "%s[%s] --- %s execution completed ---%s\n" \ printf "%s[%s] --- %s execution completed ---%s\n" \
"$magenta" "$(stamp)" "$scriptName" "$norm" >>"$logFile" "$magenta" "$(stamp)" "$scriptName" "$norm" >> "$logFile"
if [ "$warnCount" -gt 0 ]; then if [ "$warnCount" -gt 0 ]; then
printf "%s%s warnings issued!%s\n" "$warn" "${warnCount}" "$norm" >>"$logFile" printf "%s%s warnings issued!%s\n" "$warn" "${warnCount}" "$norm" >> "$logFile"
else else
printf "%s0 warnings issued.%s\n" "$ok" "$norm" >>"$logFile" printf "%s0 warnings issued.%s\n" "$ok" "$norm" >> "$logFile"
fi fi
exit 0 exit 0
### error codes ### error codes
# 1: parameter error # 1: parameter error
# 2: not run as root # 2: not run as root
# 3: docker compose not installed or not functioning # 3: borg not installed
# 4: borg not installed
# 99: TERM signal trapped # 99: TERM signal trapped
# 100: could not change to mailcow-dockerized directory # 100: could not change to mailcow-dockerized directory
# 101: could not stop container(s) # 101: could not stop container(s)

View File

@ -43,7 +43,7 @@ consoleError() {
} }
exitError() { exitError() {
printf "%s[%s] --- %s execution completed with error ---\n%s" "$err" "$(stamp)" "$scriptName" "$norm" >>"$logfile" printf "%s[%s] --- %s execution completed with error ---\n%s" "$err" "$(stamp)" "$scriptName" "$norm" >> "$logfile"
exit "$1" exit "$1"
} }
@ -51,13 +51,13 @@ doRestore() {
sourceFiles=$(find "${backupLocation}" -iname "${1}" -type d) sourceFiles=$(find "${backupLocation}" -iname "${1}" -type d)
if [ -n "$sourceFiles" ]; then if [ -n "$sourceFiles" ]; then
if [ "$verbose" -eq 1 ]; then if [ "$verbose" -eq 1 ]; then
if ( ! (cd "$sourceFiles/_data" && tar -cf - .) | (cd "${2}" && tar xvf -) >>"$logfile"); then if (! (cd "$sourceFiles/_data" && tar -cf - .) | (cd "${2}" && tar xvf -) >> "$logfile" ); then
return 1 return 1
else else
return 0 return 0
fi fi
else else
if ( ! (cd "$sourceFiles/_data" && tar -cf - .) | (cd "${2}" && tar xvf -) >/dev/null 2>&1); then if (! (cd "$sourceFiles/_data" && tar -cf - .) | (cd "${2}" && tar xvf -) > /dev/null 2>&1 ); then
return 1 return 1
else else
return 0 return 0
@ -146,36 +146,35 @@ textNewline() {
} }
trapExit() { trapExit() {
printf "%s[%s] -- [ERROR] 99: Caught signal --%s\n" "$err" "$(stamp)" "$norm" >>"$logfile" printf "%s[%s] -- [ERROR] 99: Caught signal --%s\n" "$err" "$(stamp)" "$norm" >> "$logfile"
printf "%s[%s] --- %s execution terminated via signal ---\n%s" "$err" "$(stamp)" "$scriptName" "$norm" >>"$logfile" printf "%s[%s] --- %s execution terminated via signal ---\n%s" "$err" "$(stamp)" "$scriptName" "$norm" >> "$logfile"
exit 99 exit 99
} }
writeLog() { writeLog() {
if [ "$1" = "task" ]; then if [ "$1" = "task" ]; then
printf "%s[%s] -- [INFO] %s... " "$info" "$(stamp)" "$2" >>"$logfile" printf "%s[%s] -- [INFO] %s... " "$info" "$(stamp)" "$2" >> "$logfile"
elif [ "$1" = "done" ]; then elif [ "$1" = "done" ]; then
if [ -z "$2" ]; then if [ -z "$2" ]; then
printf "%sdone%s --\n%s" "$ok" "$info" "$norm" >>"$logfile" printf "%sdone%s --\n%s" "$ok" "$info" "$norm" >> "$logfile"
elif [ "$2" = "error" ]; then elif [ "$2" = "error" ]; then
printf "%sERROR%s --\n%s" "$err" "$info" "$norm" >>"$logfile" printf "%sERROR%s --\n%s" "$err" "$info" "$norm" >> "$logfile"
elif [ "$2" = "warn" ]; then elif [ "$2" = "warn" ]; then
printf "%swarning%s --\n%s" "$yellow" "$info" "$norm" >>"$logfile" printf "%swarning%s --\n%s" "$yellow" "$info" "$norm" >> "$logfile"
fi fi
elif [ "$1" = "error" ]; then elif [ "$1" = "error" ]; then
printf "%s[%s] -- [ERROR] %s: %s --\n%s" "$err" "$(stamp)" "$2" "$3" "$norm" >>"$logfile" printf "%s[%s] -- [ERROR] %s: %s --\n%s" "$err" "$(stamp)" "$2" "$3" "$norm" >> "$logfile"
elif [ "$1" = "warn" ]; then elif [ "$1" = "warn" ]; then
printf "%s[%s] -- [WARNING] %s --\n%s" "$yellow" "$(stamp)" "$2" "$norm" >>"$logfile" printf "%s[%s] -- [WARNING] %s --\n%s" "$yellow" "$(stamp)" "$2" "$norm" >> "$logfile"
elif [ "$1" = "info" ]; then elif [ "$1" = "info" ]; then
printf "%s[%s] -- [INFO] %s --\n%s" "$info" "$(stamp)" "$2" "$norm" >>"$logfile" printf "%s[%s] -- [INFO] %s --\n%s" "$info" "$(stamp)" "$2" "$norm" >> "$logfile"
elif [ "$1" = "success" ]; then elif [ "$1" = "success" ]; then
printf "%s[%s] -- [SUCCESS] %s --\n%s" "$ok" "$(stamp)" "$2" "$norm" >>"$logfile" printf "%s[%s] -- [SUCCESS] %s --\n%s" "$ok" "$(stamp)" "$2" "$norm" >> "$logfile"
fi fi
} }
### parameter defaults ### parameter defaults
# script related # script related
scriptVersion="4.0"
scriptPath="$(CDPATH='' cd -- "$(dirname -- "$0")" && pwd -P)" scriptPath="$(CDPATH='' cd -- "$(dirname -- "$0")" && pwd -P)"
scriptName="$(basename "$0")" scriptName="$(basename "$0")"
errorCount=0 errorCount=0
@ -196,7 +195,6 @@ mcDockerCompose='/opt/mailcow-dockerized/docker-compose.yml'
sqlRunning=0 sqlRunning=0
dockerStartTimeout=180 dockerStartTimeout=180
dockerStopTimeout=120 dockerStopTimeout=120
dockerCmd="docker compose"
### check if user is root ### check if user is root
if [ "$(id -u)" -ne 0 ]; then if [ "$(id -u)" -ne 0 ]; then
@ -206,16 +204,11 @@ fi
### process startup parameters ### process startup parameters
while [ $# -gt 0 ]; do while [ $# -gt 0 ]; do
case "$1" in case "$1" in
-h | -\? | --help) -h|-\?|--help)
# display help # display help
scriptHelp scriptHelp
;; ;;
--version) -l|--log)
# display script version
printf "\nMailcowRestore - Restore Mailcow from a borgbackup repo : Version %s\n\n" ${scriptVersion}
exit 0
;;
-l | --log)
# set logfile location # set logfile location
if [ -z "$2" ]; then if [ -z "$2" ]; then
consoleError '1' "Log file path cannot be null. Leave unspecified to save log in the same directory as this script." consoleError '1' "Log file path cannot be null. Leave unspecified to save log in the same directory as this script."
@ -223,10 +216,10 @@ while [ $# -gt 0 ]; do
logfile="$2" logfile="$2"
shift shift
;; ;;
-v | --verbose) -v|--verbose)
verbose=1 verbose=1
;; ;;
-d | --docker-compose) -d|--docker-compose)
# FULL path to docker-compose file # FULL path to docker-compose file
if [ -n "$2" ]; then if [ -n "$2" ]; then
if [ -f "$2" ]; then if [ -f "$2" ]; then
@ -239,8 +232,8 @@ while [ $# -gt 0 ]; do
consoleError '1' "$1: cannot be blank/empty." consoleError '1' "$1: cannot be blank/empty."
fi fi
;; ;;
-m | --mailcow-config) -m|--mailcow-config)
# FULL path to mailcow configuration file file # FULL path to mailcow configuration file file
if [ -n "$2" ]; then if [ -n "$2" ]; then
if [ -f "$2" ]; then if [ -f "$2" ]; then
mcConfig="$2" mcConfig="$2"
@ -252,7 +245,7 @@ while [ $# -gt 0 ]; do
consoleError '1' "$1: cannot be blank/empty." consoleError '1' "$1: cannot be blank/empty."
fi fi
;; ;;
-t1 | --timeout-start) -t1|--timeout-start)
if [ -z "$2" ]; then if [ -z "$2" ]; then
consoleError '1' "$1: cannot be blank/empty." consoleError '1' "$1: cannot be blank/empty."
else else
@ -260,7 +253,7 @@ while [ $# -gt 0 ]; do
shift shift
fi fi
;; ;;
-t2 | --timeout-stop) -t2|--timeout-stop)
if [ -z "$2" ]; then if [ -z "$2" ]; then
consoleError '1' "$1: cannot be blank/empty." consoleError '1' "$1: cannot be blank/empty."
else else
@ -268,9 +261,9 @@ while [ $# -gt 0 ]; do
shift shift
fi fi
;; ;;
-b | --backup-location) -b|--backup-location)
if [ -n "$2" ]; then if [ -n "$2" ]; then
if [ -d "$2" ] && [ -n "$(ls -A "$2")" ]; then if [ -d "$2" ] && [ -n "$( ls -A "$2" )" ]; then
backupLocation="${2%/}" backupLocation="${2%/}"
shift shift
else else
@ -316,14 +309,6 @@ export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
if ! command -v docker >/dev/null; then if ! command -v docker >/dev/null; then
consoleError '3' 'docker does not seem to be installed!' consoleError '3' 'docker does not seem to be installed!'
fi fi
# verify compose installed and set proper commands based on version
if ! (docker compose version >/dev/null 2>&1); then
if ! (docker-compose --version >/dev/null 2>&1); then
printf "\n%sERROR: Docker Compose not installed or not functioning%s\n\n" "$err" "$norm"
exit 3
fi
dockerCmd="docker-compose"
fi
# mailcow.conf? # mailcow.conf?
if [ ! -f "$mcConfig" ]; then if [ ! -f "$mcConfig" ]; then
consoleError '1' "mailcow configuration file ($mcConfig) cannot be found." consoleError '1' "mailcow configuration file ($mcConfig) cannot be found."
@ -368,7 +353,7 @@ else
fi fi
fi fi
# write initial log entries # write initial log entries
if ! printf "%s[%s] --- Start %s execution ---\n%s" "$magenta" "$(stamp)" "$scriptName" "$norm" 2>/dev/null >>"$logfile"; then if ! printf "%s[%s] --- Start %s execution ---\n%s" "$magenta" "$(stamp)" "$scriptName" "$norm" 2>/dev/null >> "$logfile"; then
consoleError '1' "Unable to write to log file ($logfile)" consoleError '1' "Unable to write to log file ($logfile)"
fi fi
writeLog 'info' "Log located at $logfile" writeLog 'info' "Log located at $logfile"
@ -398,14 +383,14 @@ if [ "$restoreSQL" -eq 1 ]; then
sqlBackup=$(find "${backupLocation}/tmp" -iname "*.sql") sqlBackup=$(find "${backupLocation}/tmp" -iname "*.sql")
if [ -n "$sqlBackup" ]; then if [ -n "$sqlBackup" ]; then
# start mysql container if not already running # start mysql container if not already running
if ! docker container inspect -f '{{ .State.Running }}' ${COMPOSE_PROJECT_NAME}_mysql-mailcow_1 >/dev/null 2>&1; then if ! docker container inspect -f '{{ .State.Running }}' ${COMPOSE_PROJECT_NAME}_mysql-mailcow_1 > /dev/null 2>&1; then
${dockerCmd} up -d mysql-mailcow >/dev/null 2>&1 docker-compose up -d mysql-mailcow > /dev/null 2>&1
if docker container inspect -f '{{ .State.Running }}' ${COMPOSE_PROJECT_NAME}_mysql-mailcow_1 >/dev/null 2>&1; then if docker container inspect -f '{{ .State.Running }}' ${COMPOSE_PROJECT_NAME}_mysql-mailcow_1 > /dev/null 2>&1; then
sqlRunning=1 sqlRunning=1
else else
writeLog 'done' 'error' writeLog 'done' 'error'
writeLog 'error' '12' "Cannot start mysql-mailcow container -- cannot restore mailcow database!" writeLog 'error' '12' "Cannot start mysql-mailcow container -- cannot restore mailcow database!"
errorCount=$((errorCount + 1)) errorCount=$((errorCount+1))
fi fi
else else
sqlRunning=1 sqlRunning=1
@ -413,29 +398,29 @@ if [ "$restoreSQL" -eq 1 ]; then
else else
writeLog 'done' 'error' writeLog 'done' 'error'
writeLog 'error' '11' "Cannot locate SQL backup -- cannot restore mailcow database!" writeLog 'error' '11' "Cannot locate SQL backup -- cannot restore mailcow database!"
errorCount=$((errorCount + 1)) errorCount=$((errorCount+1))
fi fi
# restore sql # restore sql
if [ "$sqlRunning" -eq 1 ]; then if [ "$sqlRunning" -eq 1 ]; then
if docker exec -i "$(${dockerCmd} ps -q mysql-mailcow)" mysql -u${DBUSER} -p${DBPASS} ${DBNAME} <"${sqlBackup}" >/dev/null 2>&1; then if docker exec -i "$(docker-compose ps -q mysql-mailcow)" mysql -u${DBUSER} -p${DBPASS} ${DBNAME} < "${sqlBackup}" > /dev/null 2>&1; then
writeLog 'done' writeLog 'done'
else else
writeLog 'done' 'error' writeLog 'done' 'error'
writeLog 'error' '13' "Something went wrong while trying to restore SQL database. Perhaps try again?" writeLog 'error' '13' "Something went wrong while trying to restore SQL database. Perhaps try again?"
errorCount=$((errorCount + 1)) errorCount=$((errorCount+1))
fi fi
fi fi
fi fi
### stop containers (necessary for all restore operations except SQL) ### stop containers (necessary for all restore operations except SQL)
writeLog 'task' "Stopping mailcow" writeLog 'task' "Stopping mailcow"
if ! ${dockerCmd} down --timeout "${dockerStopTimeout}" >/dev/null 2>&1; then if ! docker-compose down --timeout "${dockerStopTimeout}" > /dev/null 2>&1; then
writeLog 'done' 'error' writeLog 'done' 'error'
writeLog 'error' '20' "Unable to bring mailcow containers down -- cannot reliably restore. Aborting." writeLog 'error' '20' "Unable to bring mailcow containers down -- cannot reliably restore. Aborting."
exitError 20 exitError 20
fi fi
if [ "$(docker ps --filter "name=${COMPOSE_PROJECT_NAME}" -q | wc -l)" -gt 0 ]; then if [ "$( docker ps --filter "name=${COMPOSE_PROJECT_NAME}" -q | wc -l )" -gt 0 ]; then
writeLog 'done' 'error' writeLog 'done' 'error'
writeLog 'error' '20' "Unable to bring mailcow containers down -- cannot reliably restore. Aborting." writeLog 'error' '20' "Unable to bring mailcow containers down -- cannot reliably restore. Aborting."
exitError 20 exitError 20
@ -451,61 +436,59 @@ if [ "$restoreMail" -eq 1 ]; then
fi fi
# restore email messages # restore email messages
doRestore "${COMPOSE_PROJECT_NAME}_vmail-vol-1" "$dockerVolumeMail" doRestore "${COMPOSE_PROJECT_NAME}_vmail-vol-1" "$dockerVolumeMail"; ec="$?"
ec="$?"
case "$ec" in case "$ec" in
0) 0)
if [ "$verbose" -eq 1 ]; then if [ "$verbose" -eq 1 ]; then
writeLog 'success' "Email messages restored" writeLog 'success' "Email messages restored"
else else
writeLog 'done' writeLog 'done'
fi fi
;; ;;
1) 1)
if [ "$verbose" -eq 1 ]; then if [ "$verbose" -eq 1 ]; then
writeLog 'error' '52' "There was an error restoring one or more email messages." writeLog 'error' '52' "There was an error restoring one or more email messages."
else else
writeLog 'done' 'error' writeLog 'done' 'error'
writeLog 'error' '52' "There was an error restoring one or more email messages." writeLog 'error' '52' "There was an error restoring one or more email messages."
fi fi
;; ;;
2) 2)
if [ "$verbose" -eq 1 ]; then if [ "$verbose" -eq 1 ]; then
writeLog 'error' '51' "Cannot locate email message backups!" writeLog 'error' '51' "Cannot locate email message backups!"
else else
writeLog 'done' 'error' writeLog 'done' 'error'
writeLog 'error' '51' "Cannot locate email message backups!" writeLog 'error' '51' "Cannot locate email message backups!"
fi fi
;; ;;
esac esac
# restore encryption key # restore encryption key
doRestore "${COMPOSE_PROJECT_NAME}_crypt-vol-1" "$dockerVolumeCrypt" doRestore "${COMPOSE_PROJECT_NAME}_crypt-vol-1" "$dockerVolumeCrypt"; ec="$?"
ec="$?"
case "$ec" in case "$ec" in
0) 0)
if [ "$verbose" -eq 1 ]; then if [ "$verbose" -eq 1 ]; then
writeLog 'success' "Encryption key restored" writeLog 'success' "Encryption key restored"
else else
writeLog 'done' writeLog 'done'
fi fi
;; ;;
1) 1)
if [ "$verbose" -eq 1 ]; then if [ "$verbose" -eq 1 ]; then
writeLog 'error' '52' "There was an error restoring the encryption key! Any restored messages are likely *not* readable!" writeLog 'error' '52' "There was an error restoring the encryption key! Any restored messages are likely *not* readable!"
else else
writeLog 'done' 'error' writeLog 'done' 'error'
writeLog 'error' '52' "There was an error restoring the encryption key! Any restored messages are likely *not* readable!" writeLog 'error' '52' "There was an error restoring the encryption key! Any restored messages are likely *not* readable!"
fi fi
;; ;;
2) 2)
if [ "$verbose" -eq 1 ]; then if [ "$verbose" -eq 1 ]; then
writeLog 'error' '51' "Cannot locate encryption key backup!" writeLog 'error' '51' "Cannot locate encryption key backup!"
else else
writeLog 'done' 'error' writeLog 'done' 'error'
writeLog 'error' '51' "Cannot locate encryption key backup!" writeLog 'error' '51' "Cannot locate encryption key backup!"
fi fi
;; ;;
esac esac
fi fi
@ -517,32 +500,31 @@ if [ "$restorePostfix" -eq 1 ]; then
writeLog 'task' "Restoring postfix files" writeLog 'task' "Restoring postfix files"
fi fi
doRestore "${COMPOSE_PROJECT_NAME}_postfix-vol-1" "$dockerVolumePostfix" doRestore "${COMPOSE_PROJECT_NAME}_postfix-vol-1" "$dockerVolumePostfix"; ec="$?"
ec="$?"
case "$ec" in case "$ec" in
0) 0)
if [ "$verbose" -eq 1 ]; then if [ "$verbose" -eq 1 ]; then
writeLog 'success' "Postfix files restored" writeLog 'success' "Postfix files restored"
else else
writeLog 'done' writeLog 'done'
fi fi
;; ;;
1) 1)
if [ "$verbose" -eq 1 ]; then if [ "$verbose" -eq 1 ]; then
writeLog 'error' '52' "There was an error restoring one or more postfix files." writeLog 'error' '52' "There was an error restoring one or more postfix files."
else else
writeLog 'done' 'error' writeLog 'done' 'error'
writeLog 'error' '52' "There was an error restoring one or more postfix files." writeLog 'error' '52' "There was an error restoring one or more postfix files."
fi fi
;; ;;
2) 2)
if [ "$verbose" -eq 1 ]; then if [ "$verbose" -eq 1 ]; then
writeLog 'error' '51' "Cannot locate postfix backups!" writeLog 'error' '51' "Cannot locate postfix backups!"
else else
writeLog 'done' 'error' writeLog 'done' 'error'
writeLog 'error' '51' "Cannot locate postfix backups!" writeLog 'error' '51' "Cannot locate postfix backups!"
fi fi
;; ;;
esac esac
fi fi
@ -554,32 +536,31 @@ if [ "$restoreRspamd" -eq 1 ]; then
writeLog 'task' "Restoring Rspamd files" writeLog 'task' "Restoring Rspamd files"
fi fi
doRestore "${COMPOSE_PROJECT_NAME}_rspamd-vol-1" "$dockerVolumeRspamd" doRestore "${COMPOSE_PROJECT_NAME}_rspamd-vol-1" "$dockerVolumeRspamd"; ec="$?"
ec="$?"
case "$ec" in case "$ec" in
0) 0)
if [ "$verbose" -eq 1 ]; then if [ "$verbose" -eq 1 ]; then
writeLog 'success' "Rspamd files restored" writeLog 'success' "Rspamd files restored"
else else
writeLog 'done' writeLog 'done'
fi fi
;; ;;
1) 1)
if [ "$verbose" -eq 1 ]; then if [ "$verbose" -eq 1 ]; then
writeLog 'error' '52' "There was an error restoring one or more Rspamd files." writeLog 'error' '52' "There was an error restoring one or more Rspamd files."
else else
writeLog 'done' 'error' writeLog 'done' 'error'
writeLog 'error' '52' "There was an error restoring one or more Rspamd files." writeLog 'error' '52' "There was an error restoring one or more Rspamd files."
fi fi
;; ;;
2) 2)
if [ "$verbose" -eq 1 ]; then if [ "$verbose" -eq 1 ]; then
writeLog 'error' '51' "Cannot locate Rspamd backups!" writeLog 'error' '51' "Cannot locate Rspamd backups!"
else else
writeLog 'done' 'error' writeLog 'done' 'error'
writeLog 'error' '51' "Cannot locate Rspamd backups!" writeLog 'error' '51' "Cannot locate Rspamd backups!"
fi fi
;; ;;
esac esac
fi fi
@ -591,55 +572,54 @@ if [ "$restoreRedis" -eq 1 ]; then
writeLog 'task' "Restoring redis database" writeLog 'task' "Restoring redis database"
fi fi
doRestore "${COMPOSE_PROJECT_NAME}_redis-vol-1" "$dockerVolumeRedis" doRestore "${COMPOSE_PROJECT_NAME}_redis-vol-1" "$dockerVolumeRedis"; ec="$?"
ec="$?"
case "$ec" in case "$ec" in
0) 0)
if [ "$verbose" -eq 1 ]; then if [ "$verbose" -eq 1 ]; then
writeLog 'success' "Redis database restored" writeLog 'success' "Redis database restored"
else else
writeLog 'done' writeLog 'done'
fi fi
;; ;;
1) 1)
if [ "$verbose" -eq 1 ]; then if [ "$verbose" -eq 1 ]; then
writeLog 'error' '52' "There was an error restoring the redis database. This is usually *not* a serious issue." writeLog 'error' '52' "There was an error restoring the redis database. This is usually *not* a serious issue."
else else
writeLog 'done' 'error' writeLog 'done' 'error'
writeLog 'error' '52' "There was an error restoring the redis database. This is usually *not* a serious issue." writeLog 'error' '52' "There was an error restoring the redis database. This is usually *not* a serious issue."
fi fi
;; ;;
2) 2)
if [ "$verbose" -eq 1 ]; then if [ "$verbose" -eq 1 ]; then
writeLog 'error' '51' "Cannot locate redis database backups!" writeLog 'error' '51' "Cannot locate redis database backups!"
else else
writeLog 'done' 'error' writeLog 'done' 'error'
writeLog 'error' '51' "Cannot locate redis database backups!" writeLog 'error' '51' "Cannot locate redis database backups!"
fi fi
;; ;;
esac esac
fi fi
### restart mailcow ### restart mailcow
writeLog 'task' "Starting mailcow" writeLog 'task' "Starting mailcow"
if ! ${dockerCmd} up -d >/dev/null 2>&1; then if ! docker-compose up -d > /dev/null 2>&1; then
writeLog 'done' 'warn' writeLog 'done' 'warn'
writeLog 'warn' '21' "Unable to automatically start mailcow containers. Please attempt a manual start and note any errors." writeLog 'warn' '21' "Unable to automatically start mailcow containers. Please attempt a manual start and note any errors."
warnCount=$((warnCount + 1)) warnCount=$((warnCount+1))
fi fi
writeLog 'done' writeLog 'done'
### exit gracefully ### exit gracefully
if [ "$errorCount" -gt 0 ]; then if [ "$errorCount" -gt 0 ]; then
# note non-terminating errors # note non-terminating errors
printf "%s[%s] --- %s execution completed with %s error(s) ---\n%s" "$err" "$(stamp)" "$scriptName" "$errorCount" "$norm" >>"$logfile" printf "%s[%s] --- %s execution completed with %s error(s) ---\n%s" "$err" "$(stamp)" "$scriptName" "$errorCount" "$norm" >> "$logfile"
exit 98 exit 98
elif [ "$warnCount" -gt 0 ]; then elif [ "$warnCount" -gt 0 ]; then
printf "%s[%s] --- %s execution completed with %s warning(s) ---\n%s" "$yellow" "$(stamp)" "$scriptName" "$warnCount" "$norm" >>"$logfile" printf "%s[%s] --- %s execution completed with %s warning(s) ---\n%s" "$yellow" "$(stamp)" "$scriptName" "$warnCount" "$norm" >> "$logfile"
exit 97 exit 97
else else
writeLog 'success' "All processes completed" writeLog 'success' "All processes completed"
printf "%s[%s] --- %s execution completed ---\n%s" "$magenta" "$(stamp)" "$scriptName" "$norm" >>"$logfile" printf "%s[%s] --- %s execution completed ---\n%s" "$magenta" "$(stamp)" "$scriptName" "$norm" >> "$logfile"
exit 0 exit 0
fi fi

View File

@ -49,12 +49,12 @@
# include the paths to important configuration files/directories and/or # include the paths to important configuration files/directories and/or
# data directories # data directories
# mailcow configuration # mailcow configuration (example)
# already backed-up by the script by default based on mailcow.conf location /opt/mailcow-dockerized/
# NGINX (if this host is a reverse proxy, for example) # NGINX (example)
/etc/nginx/ /etc/nginx/
/usr/share/nginx/html/ /usr/share/nginx/html/
# LetsEncrypt # LetsEncrypt (example)
/etc/letsencrypt/ /etc/letsencrypt/

View File

@ -3,8 +3,8 @@
############################################################################# #############################################################################
# $Id$ # $Id$
############################################################################# #############################################################################
# Log: mailcow backup # Log: Backup script (backup)
# Revision 1.1 2019/07/20 # Revision 1.0 2018/10/16
# Written by Asif Bacchus # Written by Asif Bacchus
############################################################################# #############################################################################
@ -34,7 +34,7 @@ if ($detailLevel == 0) {
elsif ($ThisLine =~ /\-- \[WARNING\] /) { elsif ($ThisLine =~ /\-- \[WARNING\] /) {
$summaryWarn++; $summaryWarn++;
} }
elsif ($ThisLine =~ /All processes completed/) { elsif ($ThisLine =~ /All processes completed successfully/) {
$summarySuccess++; $summarySuccess++;
} }
} }