-
-
Save dennisse/47a51a5938acf3595dc65393c53853f9 to your computer and use it in GitHub Desktop.
mbsync-notify.sh
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env bash | |
# This file: | |
# | |
# - Uses mbsync to check your email if you have a connection to the server. | |
# - Slows down syncing if you're on battery | |
# - Notifies you of new emails | |
# | |
# | |
# Usage: | |
# | |
# LOG_LEVEL=4 ./mbsync-notify.sh -a account | |
# | |
# | |
# Note: | |
# | |
# This script is meant to be run automatically. Either from cron, or launchd. | |
# | |
# This script requires that you place "Host" directly below "IMAPAccount | |
# account" in your mbsyncrc-file. Like this: | |
# | |
# IMAPAccount example | |
# Host imap.example.tld | |
# | |
# Also, this script can only sync one account at a time. Do not use it to sync | |
# all accounts at the same time. If you want to sync several accounts, run the | |
# script several times. | |
# | |
# $__skip defines number of automatic syncs to skip. If $__skip is 2, the sync | |
# will happen every third time it is automatically run. | |
# | |
# The MIT License (MIT) | |
# Copyright (c) 2017 Dennis Eriksen <https://dnns.no> | |
# | |
# Based on offlineimap-notify.sh by John Louis Del Rosario <https://github.com/john2x> | |
# | |
# Also | |
# | |
# Based on a template by BASH3 Boilerplate v2.3.0 | |
# http://bash3boilerplate.sh/#authors | |
# | |
# The MIT License (MIT) | |
# Copyright (c) 2013 Kevin van Zonneveld and contributors | |
# You are not obligated to bundle the LICENSE file with your b3bp projects as long | |
# as you leave these references intact in the header comments of your source files. | |
# Exit on error. Append "|| true" if you expect an error. | |
set -o errexit | |
# Exit on error inside any functions or subshells. | |
set -o errtrace | |
# Do not allow use of undefined vars. Use ${VAR:-} to use an undefined VAR | |
set -o nounset | |
# Catch the error in case mysqldump fails (but gzip succeeds) in `mysqldump |gzip` | |
set -o pipefail | |
# Turn on traces, useful while debugging but commented out by default | |
# set -o xtrace | |
if [[ "${BASH_SOURCE[0]}" != "${0}" ]]; then | |
__i_am_main_script="0" # false | |
if [[ "${__usage+x}" ]]; then | |
if [[ "${BASH_SOURCE[1]}" = "${0}" ]]; then | |
__i_am_main_script="1" # true | |
fi | |
__b3bp_external_usage="true" | |
__b3bp_tmp_source_idx=1 | |
fi | |
else | |
__i_am_main_script="1" # true | |
[[ "${__usage+x}" ]] && unset -v __usage | |
[[ "${__helptext+x}" ]] && unset -v __helptext | |
fi | |
# Set magic variables for current file, directory, os, etc. | |
__dir="$(cd "$(dirname "${BASH_SOURCE[${__b3bp_tmp_source_idx:-0}]}")" && pwd)" | |
__file="${__dir}/$(basename "${BASH_SOURCE[${__b3bp_tmp_source_idx:-0}]}")" | |
__base="$(basename "${__file}" .sh)" | |
# Define the environment variables (and their defaults) that this script depends on | |
LOG_LEVEL="${LOG_LEVEL:-6}" # 7 = debug -> 0 = emergency | |
NO_COLOR="${NO_COLOR:-}" # true = disable color. otherwise autodetected | |
### Functions | |
############################################################################## | |
function __b3bp_log () { | |
local log_level="${1}" | |
shift | |
# shellcheck disable=SC2034 | |
local color_debug="\x1b[35m" | |
# shellcheck disable=SC2034 | |
local color_info="\x1b[32m" | |
# shellcheck disable=SC2034 | |
local color_notice="\x1b[34m" | |
# shellcheck disable=SC2034 | |
local color_warning="\x1b[33m" | |
# shellcheck disable=SC2034 | |
local color_error="\x1b[31m" | |
# shellcheck disable=SC2034 | |
local color_critical="\x1b[1;31m" | |
# shellcheck disable=SC2034 | |
local color_alert="\x1b[1;33;41m" | |
# shellcheck disable=SC2034 | |
local color_emergency="\x1b[1;4;5;33;41m" | |
local colorvar="color_${log_level}" | |
local color="${!colorvar:-${color_error}}" | |
local color_reset="\x1b[0m" | |
if [[ "${NO_COLOR:-}" = "true" ]] || [[ "${TERM:-}" != "xterm"* ]] || [[ ! -t 2 ]]; then | |
if [[ "${NO_COLOR:-}" != "false" ]]; then | |
# Don't use colors on pipes or non-recognized terminals | |
color=""; color_reset="" | |
fi | |
fi | |
# all remaining arguments are to be printed | |
local log_line="" | |
while IFS=$'\n' read -r log_line; do | |
if [[ "${__logfile:-}" ]]; then | |
echo -e "$(date -u +"%Y-%m-%d %H:%M:%S UTC") ${color}$(printf "[%9s]" "${log_level}")${color_reset} ${log_line}" | tee -a "${__logfile}" | |
else | |
echo -e "$(date -u +"%Y-%m-%d %H:%M:%S UTC") ${color}$(printf "[%9s]" "${log_level}")${color_reset} ${log_line}" 1>&2 | |
fi | |
done <<< "${@:-}" | |
} | |
function emergency () { __b3bp_log emergency "${@}"; exit 1; } | |
function alert () { [[ "${LOG_LEVEL:-0}" -ge 1 ]] && __b3bp_log alert "${@}"; true; } | |
function critical () { [[ "${LOG_LEVEL:-0}" -ge 2 ]] && __b3bp_log critical "${@}"; true; } | |
function error () { [[ "${LOG_LEVEL:-0}" -ge 3 ]] && __b3bp_log error "${@}"; exit 1; } | |
function warning () { [[ "${LOG_LEVEL:-0}" -ge 4 ]] && __b3bp_log warning "${@}"; true; } | |
function notice () { [[ "${LOG_LEVEL:-0}" -ge 5 ]] && __b3bp_log notice "${@}"; true; } | |
function info () { [[ "${LOG_LEVEL:-0}" -ge 6 ]] && __b3bp_log info "${@}"; true; } | |
function debug () { [[ "${LOG_LEVEL:-0}" -ge 7 ]] && __b3bp_log debug "${@}"; true; } | |
function help () { | |
echo "" 1>&2 | |
echo " ${*}" 1>&2 | |
echo "" 1>&2 | |
echo " ${__usage:-No usage available}" 1>&2 | |
echo "" 1>&2 | |
if [[ "${__helptext:-}" ]]; then | |
echo " ${__helptext}" 1>&2 | |
echo "" 1>&2 | |
fi | |
exit 1 | |
} | |
function clean () { | |
sed "s/^\[/\\\[/g" | sed "s/\"/'/g" | sed 's/\!/❕ /g' | |
} | |
### Parse commandline options | |
############################################################################## | |
# Commandline options. This defines the usage page, and is used to parse cli | |
# opts & defaults from. The parsing is unforgiving so be precise in your syntax | |
# - A short option must be preset for every long option; but every short option | |
# need not have a long option | |
# - `--` is respected as the separator between options and arguments | |
# - We do not bash-expand defaults, so setting '~/app' as a default will not resolve to ${HOME}. | |
# you can use bash variables to work around this (so use ${HOME} instead) | |
# shellcheck disable=SC2015 | |
[[ "${__usage+x}" ]] || read -r -d '' __usage <<-'EOF' || true # exits non-zero when EOF encountered | |
-a --account [arg] Account to sync with mbsync. Required. | |
-c --config [arg] Your mbsyncrc-config. Default="${HOME}/.mbsyncrc" | |
-s --skip [arg] Number of syncs to skip if on battery. Default="2" | |
-m --maildir [arg] Maildir. If you want notifications. | |
-t --temp [arg] Directory for tempfile. Default="/tmp/" | |
-l --logfile [arg] Location of logfile | |
-v Enable verbose mode, print script as it is executed | |
-d --debug Enables debug mode | |
-h --help This page | |
-n --no-color Disable color output | |
-1 --one Do just one thing | |
EOF | |
# shellcheck disable=SC2015 | |
[[ "${__helptext+x}" ]] || read -r -d '' __helptext <<-'EOF' || true # exits non-zero when EOF encountered | |
--maildir activates notifications. The maildir needs to be in Maildir-format, | |
and your inbox must be found in maildir/INBOX. | |
Note that all directories must end in /. | |
EOF | |
# Translate usage string -> getopts arguments, and set $arg_<flag> defaults | |
while read -r __b3bp_tmp_line; do | |
if [[ "${__b3bp_tmp_line}" =~ ^- ]]; then | |
# fetch single character version of option string | |
__b3bp_tmp_opt="${__b3bp_tmp_line%% *}" | |
__b3bp_tmp_opt="${__b3bp_tmp_opt:1}" | |
# fetch long version if present | |
__b3bp_tmp_long_opt="" | |
if [[ "${__b3bp_tmp_line}" = *"--"* ]]; then | |
__b3bp_tmp_long_opt="${__b3bp_tmp_line#*--}" | |
__b3bp_tmp_long_opt="${__b3bp_tmp_long_opt%% *}" | |
fi | |
# map opt long name to+from opt short name | |
printf -v "__b3bp_tmp_opt_long2short_${__b3bp_tmp_long_opt//-/_}" '%s' "${__b3bp_tmp_opt}" | |
printf -v "__b3bp_tmp_opt_short2long_${__b3bp_tmp_opt}" '%s' "${__b3bp_tmp_long_opt//-/_}" | |
# check if option takes an argument | |
if [[ "${__b3bp_tmp_line}" =~ \[.*\] ]]; then | |
__b3bp_tmp_opt="${__b3bp_tmp_opt}:" # add : if opt has arg | |
__b3bp_tmp_init="" # it has an arg. init with "" | |
printf -v "__b3bp_tmp_has_arg_${__b3bp_tmp_opt:0:1}" '%s' "1" | |
elif [[ "${__b3bp_tmp_line}" =~ \{.*\} ]]; then | |
__b3bp_tmp_opt="${__b3bp_tmp_opt}:" # add : if opt has arg | |
__b3bp_tmp_init="" # it has an arg. init with "" | |
# remember that this option requires an argument | |
printf -v "__b3bp_tmp_has_arg_${__b3bp_tmp_opt:0:1}" '%s' "2" | |
else | |
__b3bp_tmp_init="0" # it's a flag. init with 0 | |
printf -v "__b3bp_tmp_has_arg_${__b3bp_tmp_opt:0:1}" '%s' "0" | |
fi | |
__b3bp_tmp_opts="${__b3bp_tmp_opts:-}${__b3bp_tmp_opt}" | |
fi | |
[[ "${__b3bp_tmp_opt:-}" ]] || continue | |
if [[ "${__b3bp_tmp_line}" =~ (^|\.\ *)Default= ]]; then | |
# ignore default value if option does not have an argument | |
__b3bp_tmp_varname="__b3bp_tmp_has_arg_${__b3bp_tmp_opt:0:1}" | |
if [[ "${!__b3bp_tmp_varname}" != "0" ]]; then | |
__b3bp_tmp_init="${__b3bp_tmp_line##*Default=}" | |
__b3bp_tmp_re='^"(.*)"$' | |
if [[ "${__b3bp_tmp_init}" =~ ${__b3bp_tmp_re} ]]; then | |
__b3bp_tmp_init="${BASH_REMATCH[1]}" | |
else | |
__b3bp_tmp_re="^'(.*)'$" | |
if [[ "${__b3bp_tmp_init}" =~ ${__b3bp_tmp_re} ]]; then | |
__b3bp_tmp_init="${BASH_REMATCH[1]}" | |
fi | |
fi | |
fi | |
fi | |
if [[ "${__b3bp_tmp_line}" =~ (^|\.\ *)Required\. ]]; then | |
# remember that this option requires an argument | |
printf -v "__b3bp_tmp_has_arg_${__b3bp_tmp_opt:0:1}" '%s' "2" | |
fi | |
printf -v "arg_${__b3bp_tmp_opt:0:1}" '%s' "${__b3bp_tmp_init}" | |
done <<< "${__usage:-}" | |
# run getopts only if options were specified in __usage | |
if [[ "${__b3bp_tmp_opts:-}" ]]; then | |
# Allow long options like --this | |
__b3bp_tmp_opts="${__b3bp_tmp_opts}-:" | |
# Reset in case getopts has been used previously in the shell. | |
OPTIND=1 | |
# start parsing command line | |
set +o nounset # unexpected arguments will cause unbound variables | |
# to be dereferenced | |
# Overwrite $arg_<flag> defaults with the actual CLI options | |
while getopts "${__b3bp_tmp_opts}" __b3bp_tmp_opt; do | |
[[ "${__b3bp_tmp_opt}" = "?" ]] && help "Invalid use of script: ${*} " | |
if [[ "${__b3bp_tmp_opt}" = "-" ]]; then | |
# OPTARG is long-option-name or long-option=value | |
if [[ "${OPTARG}" =~ .*=.* ]]; then | |
# --key=value format | |
__b3bp_tmp_long_opt=${OPTARG/=*/} | |
# Set opt to the short option corresponding to the long option | |
__b3bp_tmp_varname="__b3bp_tmp_opt_long2short_${__b3bp_tmp_long_opt//-/_}" | |
printf -v "__b3bp_tmp_opt" '%s' "${!__b3bp_tmp_varname}" | |
OPTARG=${OPTARG#*=} | |
else | |
# --key value format | |
# Map long name to short version of option | |
__b3bp_tmp_varname="__b3bp_tmp_opt_long2short_${OPTARG//-/_}" | |
printf -v "__b3bp_tmp_opt" '%s' "${!__b3bp_tmp_varname}" | |
# Only assign OPTARG if option takes an argument | |
__b3bp_tmp_varname="__b3bp_tmp_has_arg_${__b3bp_tmp_opt}" | |
printf -v "OPTARG" '%s' "${@:OPTIND:${!__b3bp_tmp_varname}}" | |
# shift over the argument if argument is expected | |
((OPTIND+=__b3bp_tmp_has_arg_${__b3bp_tmp_opt})) | |
fi | |
# we have set opt/OPTARG to the short value and the argument as OPTARG if it exists | |
fi | |
__b3bp_tmp_varname="arg_${__b3bp_tmp_opt:0:1}" | |
__b3bp_tmp_default="${!__b3bp_tmp_varname}" | |
__b3bp_tmp_value="${OPTARG}" | |
if [[ -z "${OPTARG}" ]] && [[ "${__b3bp_tmp_default}" = "0" ]]; then | |
__b3bp_tmp_value="1" | |
fi | |
printf -v "${__b3bp_tmp_varname}" '%s' "${__b3bp_tmp_value}" | |
debug "cli arg ${__b3bp_tmp_varname} = (${__b3bp_tmp_default}) -> ${!__b3bp_tmp_varname}" | |
done | |
set -o nounset # no more unbound variable references expected | |
shift $((OPTIND-1)) | |
if [[ "${1:-}" = "--" ]] ; then | |
shift | |
fi | |
fi | |
### Automatic validation of required option arguments | |
############################################################################## | |
for __b3bp_tmp_varname in ${!__b3bp_tmp_has_arg_*}; do | |
# validate only options which required an argument | |
[[ "${!__b3bp_tmp_varname}" = "2" ]] || continue | |
__b3bp_tmp_opt_short="${__b3bp_tmp_varname##*_}" | |
__b3bp_tmp_varname="arg_${__b3bp_tmp_opt_short}" | |
[[ "${!__b3bp_tmp_varname}" ]] && continue | |
__b3bp_tmp_varname="__b3bp_tmp_opt_short2long_${__b3bp_tmp_opt_short}" | |
printf -v "__b3bp_tmp_opt_long" '%s' "${!__b3bp_tmp_varname}" | |
[[ "${__b3bp_tmp_opt_long:-}" ]] && __b3bp_tmp_opt_long=" (--${__b3bp_tmp_opt_long//_/-})" | |
help "Option -${__b3bp_tmp_opt_short}${__b3bp_tmp_opt_long:-} requires an argument" | |
done | |
### Cleanup Environment variables | |
############################################################################## | |
for __tmp_varname in ${!__b3bp_tmp_*}; do | |
unset -v "${__tmp_varname}" | |
done | |
unset -v __tmp_varname | |
### Externally supplied __usage. Nothing else to do here | |
############################################################################## | |
if [[ "${__b3bp_external_usage:-}" = "true" ]]; then | |
unset -v __b3bp_external_usage | |
return | |
fi | |
### Signal trapping and backtracing | |
############################################################################## | |
#function __b3bp_cleanup_before_exit () { | |
# info "Cleaning up. Done" | |
# rm "${__tempfile}" | |
#} | |
#trap __b3bp_cleanup_before_exit EXIT | |
# requires `set -o errtrace` | |
__b3bp_err_report() { | |
local error_code | |
error_code=${?} | |
error "Error in ${__file} in function ${1} on line ${2}" | |
exit ${error_code} | |
} | |
# Uncomment the following line for always providing an error backtrace | |
# trap '__b3bp_err_report "${FUNCNAME:-.}" ${LINENO}' ERR | |
### Command-line argument switches (like -d for debugmode, -h for showing helppage) | |
############################################################################## | |
# debug mode | |
if [[ "${arg_d:?}" = "1" ]]; then | |
set -o xtrace | |
LOG_LEVEL="7" | |
# Enable error backtracing | |
trap '__b3bp_err_report "${FUNCNAME:-.}" ${LINENO}' ERR | |
fi | |
# verbose mode | |
if [[ "${arg_v:?}" = "1" ]]; then | |
set -o verbose | |
fi | |
# no color mode | |
if [[ "${arg_n:?}" = "1" ]]; then | |
NO_COLOR="true" | |
fi | |
# help mode | |
if [[ "${arg_h:?}" = "1" ]]; then | |
# Help exists with code 1 | |
help "Help using ${0}" | |
fi | |
# logfile | |
if [[ "${arg_l:-}" ]]; then | |
# check if we have write-permissions to logfile | |
touch "${arg_l}" >/dev/null 2>&1 || emergency "You can not write to ${arg_l}" | |
__logfile="${arg_l}" | |
fi | |
# Account | |
__account="${arg_a}" | |
# tempfile | |
if [[ "${arg_t:-}" ]]; then | |
if ! [[ "${arg_t: -1}" = "/" ]]; then | |
error 'All directories must end in "/"' | |
fi | |
# check if we have write-permissions to tempfile | |
touch "${arg_t}${__account}" >/dev/null 2>&1 || emergency "You can not write to ${arg_t}" | |
__tempfile="${arg_t}${__account}" | |
__tempdir="${arg_t}" | |
fi | |
# maildir | |
if [[ "${arg_m:-}" ]]; then | |
if ! [[ "${arg_m: -1}" = "/" ]]; then | |
error 'All directories must end in "/"' | |
elif ! [ -r "${arg_m}" ]; then | |
error "You can't read ${arg_m}" | |
fi | |
__maildir="${arg_m}" | |
fi | |
### Validation. Error out if the things required for your script are not present | |
############################################################################## | |
[[ "${arg_a:-}" ]] || help "You need to specify which account you want to sync." | |
[[ "${LOG_LEVEL:-}" ]] || emergency "Cannot continue without LOG_LEVEL. " | |
if ! [ -r "${arg_c}" ]; then | |
error "You can not read ${arg_c}. Please make sure you can read the mbsyncrc-file." | |
else | |
__config="${arg_c}" | |
fi | |
if ! grep -E "(Channel|Group) ${__account}" "${__config}" >/dev/null 2>&1; then | |
error "Could not find channel or group named ${__account} to sync" | |
fi | |
__num='^[0-9]+$' | |
if ! [[ "${arg_s}" =~ ${__num} ]]; then | |
error "-s requires a number" | |
elif [ "${arg_s}" -ge 255 ]; then | |
error "Come on." | |
else | |
__skip="${arg_s}" | |
fi | |
### Runtime | |
############################################################################## | |
# if we're on battery | |
if pmset -g batt | grep "Battery Power" >/dev/null 2>&1; then | |
info "We're on battery. Skipping some syncs." | |
# when we're on battery, we're supposed to skip n syncs | |
# we use the tempfile to store this. | |
if [ -f "${__tempfile}" ]; then | |
__skipped=$(cat "${__tempfile}") | |
if ! [[ "${__skipped}" =~ ${__num} ]]; then | |
notice "Your tempfile does not contain a number. It will be overwritten." | |
echo "0" > "${__tempfile}" | |
notice "Skipping sync because we're on battery." | |
exit 0 | |
elif [[ "${__skipped}" -lt "${__skip}" ]]; then | |
let "__skipped += 1" | |
echo "${__skipped}" > "${__tempfile}" | |
info "We've now skipped ${__skipped} syncs." | |
notice "Skipping sync because we're on battery." | |
exit 0 | |
else | |
info "Already skipped ${__skip} syncs. Continuing sync." | |
echo "0" > "${__tempfile}" | |
fi | |
fi | |
else | |
info "On AC. Continuing sync." | |
fi | |
# If we've gooten here, we've either skipped the right amount of sync-cycles, | |
# or we're on AC-power. Let's sync! | |
notice "Syncing ${__account}..." | |
mbsync "${__account}" | |
# if we want notifications | |
if [[ "${__maildir:-}" ]]; then | |
info "Setting up notifications" | |
__prevmsgs="${__tempdir}mbsync-notify-${__account}" | |
__newmsgs=0 | |
__sender='' | |
__subject='' | |
__maildirnew="${__maildir}INBOX/new" | |
touch "${__prevmsgs}" >/dev/null 2>&1 || error "You can not write to ${__prevmsgs}" | |
# check if the mails weren't already seen/reported before | |
# and add them to a temporary list of previously seen/reported mails | |
for __file in "${__maildirnew}"/*; do | |
if ! [ -f "${__file}" ]; then | |
continue | |
fi | |
__filename=$(basename "${__file}") | |
if ! grep -Fxq "${__filename}" "${__prevmsgs}"; then | |
debug "Found new email - ${__filename}" | |
echo "${__filename}" >> "${__prevmsgs}" | |
let "__newmsgs += 1" | |
debug "New messages: ${__newmsgs}" | |
if [ -z "${__sender}" ]; then | |
__sender="$(grep -o '^From: \(.*\)$' ${__file} | sed "s/^From: //g" | clean)" | |
fi | |
if [ -z "${__subject:-}" ]; then | |
__subject="$(grep -o '^Subject: \(.*\)$' ${__file} \ | |
| sed "s/^Subject: //g" \ | |
| perl -pe 'use MIME::Words(decode_mimewords); $_=decode_mimewords($_);' \ | |
| clean)" | |
fi | |
fi | |
done | |
if [[ "${__newmsgs}" = 0 ]]; then | |
info "No new messages found." | |
exit 0 | |
fi | |
info "Found ${__newmsgs} new messages" | |
debug '${__sender}'": ${__sender}" | |
debug '${__subject}'": ${__subject}" | |
if [[ "${__newmsgs}" -gt 1 ]]; then | |
__title="${__newmsgs} new messages for ${__account} 📬" | |
__subtitle=$(echo -e "${__sender} and others") | |
else | |
__title="New message for ${__account} ✉️ " | |
__subtitle=$(echo -e "${__sender}") | |
fi | |
__body=$(echo -e "${__subject}") | |
info "Sending notification." | |
debug "Notification:" | |
debug "${__title}" | |
debug "${__subtitle}" | |
debug "${__body}" | |
terminal-notifier -title "${__title}" -subtitle "${__subtitle}" -message "${__body}" -sender 'com.apple.Mail' | |
fi | |
info "Done." | |
exit 0 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment