Skip to content

Instantly share code, notes, and snippets.

@dennisse
Forked from john2x/offlineimap-notify.sh
Last active October 26, 2018 10:02
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save dennisse/47a51a5938acf3595dc65393c53853f9 to your computer and use it in GitHub Desktop.
Save dennisse/47a51a5938acf3595dc65393c53853f9 to your computer and use it in GitHub Desktop.
mbsync-notify.sh
#!/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