mu-wizard

shell script to easily configure mu4e accounts on Emacs
git clone git://git.ckyln.com/~cem/mu-wizard
Log | Files | Refs | README | LICENSE

commit 907d305fdd8164a1e274c1dffca1f9581567e706
Author: Cem Keylan <cem@ckyln.com>
Date:   Tue, 21 Jul 2020 14:15:32 +0300

initial commit

Diffstat:
AMakefile | 22++++++++++++++++++++++
AREADME.md | 53+++++++++++++++++++++++++++++++++++++++++++++++++++++
Abin/mailsync | 31+++++++++++++++++++++++++++++++
Abin/muw | 317+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Amu4e-config.el | 40++++++++++++++++++++++++++++++++++++++++
5 files changed, 463 insertions(+), 0 deletions(-)

diff --git a/Makefile b/Makefile @@ -0,0 +1,22 @@ +PREFIX = /usr/local +BINDIR = ${PREFIX}/bin +SHAREDIR = ${PREFIX}/share +MUSHAREDIR = ${SHAREDIR}/mu-wizard + +all: + @echo "Run 'make install' to install mu-wizard." + +install: + mkdir -p ${DESTDIR}${BINDIR} ${DESTDIR}${MUSHAREDIR} + cp bin/mailsync ${DESTDIR}${BINDIR}/mailsync + chmod 755 ${DESTDIR}${BINDIR}/mailsync + sed 's|/usr/share/mu-wizard|${MUSHAREDIR}|g' < bin/muw > ${DESTDIR}${BINDIR}/muw + chmod 755 ${DESTDIR}${BINDIR}/muw + cp mu4e-config.el ${DESTDIR}${MUSHAREDIR} + chmod 644 ${DESTDIR}${MUSHAREDIR}/mu4e-config.el + +uninstall: + rm -rf ${DESTDIR}${BINDIR}/muw ${DESTDIR}${BINDIR}/mailsync \ + ${DESTDIR}${MUSHAREDIR} + +.PHONY: all install uninstall diff --git a/README.md b/README.md @@ -0,0 +1,53 @@ +mu-wizard +-------------------------------------------------------------------------------- + +Shell script to auto-configure email accounts for `mu4e` similar in function to +mutt-wizard. It uses `isync` to synchronize mail accounts, `msmtp` to send mail +and creates individual Lisp profiles for each account. It is still WIP. + + +Dependencies +-------------------------------------------------------------------------------- + +* `isync` (for offline mail storage) +* `mu` (or maildir-utils depending on your distribution) +* `msmtp` (for sending mails) +* Password manager (`pass`, `pash`, and `pm` is supported) + + + +Installation and Configuration +-------------------------------------------------------------------------------- + +In order to install clone this repository and run the following command. + + make install + +Emacs will not be loading the configurations, you will need to set it manually. +In your init file, you may choose to load the configuration in the following +ways. + +```lisp +(load-file "~/.config/mu4e/mu4e-config.el") +``` + +```lisp +(add-to-list 'load-path "~/.config/mu4e") +(require 'mu4e-config) +``` + +```lisp +(use-package mu4e-config + :after mu4e + :load-path "~/.config/mu4e") +``` + + +`Domains.csv` file +-------------------------------------------------------------------------------- + +`mu-wizard` doesn't come with a predefined `domains.csv` file, but it can use +one if it is found on `/usr/share/mu-wizard/domains.csv`. `mu-wizard` also saves +the domain information that you use when creating an account on your +configuration directory, so you don't have to retype every detail when creating +a second account with the same domain. diff --git a/bin/mailsync b/bin/mailsync @@ -0,0 +1,31 @@ +#!/bin/sh + +out() { printf '%s\n' "$@" >&2 ;} +die() { out "$@"; exit 1 ;} + +pgrep -x mbsync >/dev/null && die "mbsync is already running." + +notify() { notify-send "mu-wizard" "$2 new mail(s) in '$1' account." ;} + +[ "$1" ] || while read -r chan name; do + case "$chan" in Channel) ;; *) continue; esac + accounts="$accounts $name" +done < "$HOME/.mbsyncrc" + +for acc in ${accounts:=$*}; do + acc=${acc##*/} + mbsync "$acc" & +done; wait + +for acc in $accounts; do + newcount=$(find "$HOME/.local/share/mail/$acc/"*/new \ + -type f -newer "$HOME/.cache/.mailsynclastrun" | wc -l) + [ "$newcount" -gt 0 ] && notify "$acc" "$newcount" +done + + +# Run index update if emacs is available. +emacsclient -e "(mu4e-update-index)" >/dev/null 2>&1 || + mu index + +touch "$HOME/.cache/.mailsynclastrun" diff --git a/bin/muw b/bin/muw @@ -0,0 +1,317 @@ +#!/bin/sh + +out() { printf '%s\n' "$@" >&2 ;} +err() { printf 'err: %s\n' "$@" >&2 ;} +die() { err "$@"; exit 1 ;} +prompt() { printf '%s\n' "$1"; [ "$2" ] && printf '%s: ' "$2" ;} + +yesno() { + printf '%s\n' "$@"; printf '[y]es/[n]o: ' + read -r ans + case "$ans" in [Yy]*) return 0; esac; return 1 +} + +delete() { + rm -f "$accountdir/$title.el" + sed_i "/^IMAPStore $title-remote\$/,/^# End profile\$/d" "$HOME/.mbsyncrc" + sed_i "/^account $title\$/,/^# End profile\$/d" "$config_home/msmtp/config" + rm -f /tmp/mbsync-boxes +} + +contains() { + # Check whether a list contains the given value + # Example: 'contains "a" "a b c d e f"' returns 0 + case " $2 " in *" $1 "*) return 0; esac; return 1 +} + +sed_i() { + # 'sed -i' like function without actually running it. This can be only used + # for a single file, and the filename should always come last. + sed "$@" > _; for file; do :; done; mv _ "$file" +} + +get_profiles() { + # Function to get all available profiles + unset profiles + for profile in "$accountdir"/*.el; do + [ -f "$profile" ] || continue + profile=${profile##*/} + profiles="${profile%.el} $profiles" + done +} + +msmtp_header() { + mkdir -p "$config_home/msmtp" + cat <<EOF > "$config_home/msmtp/config" +defaults +auth on +tls on +tls_trust_file $sslcert +logfile ~/.config/msmtp/msmtp.log +EOF +} + +pm_ask() { + if [ "$(command -v pass)" ]; then + pass insert "$pmt" + password_command="pass show $pmt" + elif [ "$(command -v pash)" ]; then + # Disable 'generate password?' prompt. We need to create a file and run + # it, because the password manager needs to own the standard input. + sed 's/yn "Gen/false "Gen/g' "$(command -v pash)" >_ + sh _ add "$pmt" + rm -f _ + password_command="pash show $pmt" + elif [ "$(command -v pm)" ]; then + stty -echo + printf 'Enter your password: '; read -r pass + printf '\nEnter your password again: '; read -r pass2 + printf '\n' + stty echo + if [ "$pass" = "$pass2" ]; then + pm add "$pmt" <<EOF +$pass +EOF + password_command="pm show $pmt" + else + err "Passwords don't match" + return 1 + fi + fi +} + +pm_del() { + if [ "$(command -v pass)" ]; then + pass rm -f "$pmt" + elif [ "$(command -v pash)" ]; then + yes | pash del "$pmt" + elif [ "$(command -v pm)" ]; then + pm del "$pmt" + fi > /dev/null 2>&1 +} + +test_connection() { + mkdir -p "$maildir/$title" + + # Since we are "Flattening" the inbox structure by replacing '/' with '.', + # we need to also replace it in the 'mbsync -l' output by hand. See the + # Flatten section on mbsync(1). + { mbsync -l "$title" || die "Log-on not successful." \ + "It seems that either you have inputted the wrong password or server settings," \ + "or there are requirements for your account out of the control of mu-wizard." + } | sed 's|/|.|g' > /tmp/mbsync-boxes + + while read -r dir; do mkdir -p "$maildir/$title/${dir#/}"; done < /tmp/mbsync-boxes + + drafts=$(grep -i drafts /tmp/mbsync-boxes | sed 1q) + trash=$(grep -i trash /tmp/mbsync-boxes | sed 1q) + inbox=$(grep -i inbox /tmp/mbsync-boxes | sed 1q) + sent=$(grep -i sent /tmp/mbsync-boxes | sed 1q) + + rm -f /tmp/mbsync-boxes +} + +get_domains() { + [ -f "$confdir/domains.csv" ] && { + if [ -f "$sharedir/domains.csv" ]; then + sort -u -o "$confdir/domains.csv" "$sharedir/domains.csv" "$confdir/domains.csv" + else + sort -u -o "$confdir/domains.csv" "$confdir/domains.csv" + fi + return + } + :> "$confdir/domains.csv" + [ -f "$sharedir/domains.csv" ] && cp "$sharedir/domains.csv" "$confdir/domains.csv" +} + +set_mbsync() { + cat <<EOF >> "$HOME/.mbsyncrc" +IMAPStore $title-remote +Host $imap +Port $iport +User $login +PassCmd "$password_command" +SSLType IMAPS +CertificateFile $sslcert + +MaildirStore $title-local +Subfolders Verbatim +Path ~/.local/share/mail/$title/ +Inbox ~/.local/share/mail/$title/INBOX +Flatten . + +Channel $title +Expunge Both +Master :$title-remote: +Slave :$title-local: +Patterns * !"[Gmail]/All Mail" +Create Both +SyncState * +MaxMessages ${maxmes:-0} +ExpireUnread no +# End profile + +EOF +} + +set_msmtp() { + [ -f "$config_home/msmtp/config" ] || msmtp_header + + cat <<EOF >> "$config_home/msmtp/config" +account $title +host $smtp +port $sport +from $fulladdr +user $login +passwordeval "$password_command" +$starttlsoff +# End profile + +EOF +} + +set_accountlisp() { + cat <<EOF > "$accountdir/$title.el" +(add-to-list 'mu4e-contexts +(make-mu4e-context + :name "$title" + :match-func (lambda (msg) + (when msg + (mu4e-message-contact-field-matches msg + :to "$fulladdr"))) + :vars '((user-mail-address . "$fulladdr") + (message-sendmail-extra-arguments . ("-a" "$title")) + (mu4e-drafts-folder . "/$title/${drafts:=Drafts}") + (mu4e-trash-folder . "/$title/${trash:=Trash}") + (mu4e-maildir-shortcuts . (("/$title/${inbox:-INBOX}" . ?i) + ("/$title/$drafts" . ?d) + ("/$title/${sent:-Drafts}" . ?s))) + (user-full-name . "$realname")))) +EOF + +} + +get_information() { + for file in /etc/ssl/certs/ca-certificates.crt /etc/pki/tls/certs/ca-bundle.crt \ + /etc/certificates/cert.pem /etc/ssl/ca-bundle.pem /etc/ssl/cert.pem \ + /etc/pki/tls/cacert.pem /usr/share/ca-certificates; do + [ -e "$file" ] || continue + sslcert=$file; break + done + + prompt "Insert the email address that you want to configure for mu4e" "Email" + read -r fulladdr + + while IFS=, read -r domain imap iport smtp sport; do + case "$domain" in "${fulladdr##*@}") break; esac + done < "$confdir/domains.csv" + + if [ "$domain" = "${fulladdr##*@}" ]; then + out "Your domain information was found on '$confdir/domains.csv'!'" \ + "The following information will be used:" ""\ + "Domain: $domain" \ + "IMAP: $imap:$iport" \ + "SMTP: $smtp:$sport" + else + prompt "Insert the IMAP server for your email provider (excluding the port number)" "Server" + read -r imap + + prompt "What is your server's IMAP port number? (Usually 993)" "Port" + read -r iport + + prompt "Insert the SMTP server for your email provider (excluding the port number)" "Server" + read -r smtp + + prompt "What is your server's SMTP port number? (Usually 587 or 465)" + read -r sport + fi + + prompt "Enter the full name you want to be identified by on this account." "Real name" + read -r realname + + prompt "Enter a short, one-word identifier for this email account that will distinguish them from any other accounts you add" "Account name" + read -r title + while contains "$title" "$profiles"; do + prompt "Profile '$title' already exists, please specify a different name" "Account name" + read -r title + done + + prompt "If your account has a special username different from your address, insert it now. Otherwise leave this blank." "Login(?)" + read -r login + + # If login is empty use fulladdr + [ "$login" ] || login="$fulladdr" + + prompt "If you want to limit the number of messages kept offline to a number, enter it below. Otherwise leave this blank." "Maximum messages" + read -r maxmes + + case "$sport" in 465) starttlsoff="tls_starttls off"; esac +} + +main() { + command -v pass >/dev/null || command -v pash || command -v pm || + die "No applicable password manager found." + command -v mbsync >/dev/null || die "mbsync must be installed for mu-wizard to work." + + sharedir=/usr/share/mu-wizard + + mkdir -p "${config_home:=${XDG_CONFIG_HOME:-$HOME/.config}}" \ + "${confdir:=$config_home/mu4e}" \ + "${accountdir:=$confdir/accounts}" \ + "${maildir:=$HOME/.local/share/mail}" + + get_profiles + + case "$1" in + l|list) for profile in $profiles; do out " $profile"; done ;; + + a|add) + get_domains + get_information + trap delete INT EXIT + pmt="mu-wizard-$title" + while :; do pm_del; pm_ask && break; done + set_mbsync + test_connection + set_msmtp + set_accountlisp + [ -f "$confdir/mu4e-config.el" ] || cp "$sharedir/mu4e-config.el" "$confdir/mu4e-config.el" + + # We add user's personal domain information to the config directory, + # so even if the user doesn't want to share the information in a git + # repository, they can have it themselves when they may need it. + + printf '%s,%s,%s,%s,%s' \ + "${fulladdr##*@}" "$imap" "$iport" "$smtp" "$sport" >> "$confdir/domains.csv" + trap '' INT EXIT + out "All done. You can now run 'mbsync $title' in order to sync this account." + ;; + + d|delete) + out "Pick a profile to be deleted" + for profile in $profiles; do out " $profile"; done + read -r title + contains "$title" "$profiles" || die "Profile '$title' doesn't exist." + yesno "Are you sure you want to delete '$title'?" && delete + ;; + p|purge) + yesno "Are you sure you want to delete all account data?" || exit 1 + rm -rf "$HOME/.mbsyncrc" "$confdir" "$config_home/msmtp" + ;; + ''|--help|-h) + out "usage: ${0##*/} [action]" \ + "mu-wizard, auto-configure email accounts for mu4e" \ + " Options:" \ + " [a]dd: Add and aoutoconfigure an email address" \ + " [d]elete: Pick an account to delete" \ + " [l]ist: List configured accounts" \ + " [p]urge: Purge all configuration" "" \ + "NOTE: Once at least one account is added, you can run" \ + "'mbsync -a' to begin downloading mail." + ;; + *) die "Unknown action '$1'" + esac + +} + +main "$@" diff --git a/mu4e-config.el b/mu4e-config.el @@ -0,0 +1,40 @@ +;;; mu4e-config.el -- Configuration Options for mu4e -*- lexical-binding: t -*- + +;;; Commentary: +;; This is a template configuration for mu4e, the basic stuff. This can be +;; called from the init.el by running +;; +;; (load-file "~/.config/mu4e/mu4e-config.el") +;; +;; My knowledge of Lisp isn't great. I have been using Emacs for a few months +;; and this is my first project including Lisp. So please bear with me. I +;; would be happy to receive PRs. + +;;; Code: +(require 'mu4e nil t) +(require 'message) +(require 'sendmail) + +(defvar mu4e-get-mail-command) +(defvar mu4e-contexts) +(defvar mu4e-sent-messages-behavior) +(defvar config-home) + +(setq mu4e-get-mail-command "mbsync -a" + mail-user-agent 'mu4e-user-agent + message-send-mail-function 'message-send-mail-with-sendmail + sendmail-program (executable-find "msmtp") + mu4e-sent-messages-behavior 'delete + config-home (or (getenv "XDG_CONFIG_HOME") "~/.config")) + + +;; Make mu4e-contexts an empty list. +(setq mu4e-contexts ()) + +;; Load user accounts. +(dolist (file (directory-files + (expand-file-name "mu4e/accounts" config-home) t "\.el$" nil)) + (load file)) + +(provide 'mu4e-config) +;;; mu4e-config.el ends here