mu-wizard

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

muw (12191B)


      1 #!/bin/sh -e
      2 
      3 out() { printf '%s\n' "$@" >&2 ;}
      4 err() { printf 'err: %s\n' "$@" >&2 ;}
      5 die() { err "$@"; exit 1 ;}
      6 prompt() {
      7     # The first argument is used as the prompt, and will print as 'prompt: '.
      8     # Any other arguments will be printed before prompt if they exist.
      9     prompt=$1 ans=
     10     [ "$2" ] && { shift 1; out "$@" ;}
     11     printf '%s: ' "$prompt" >&2
     12     read -r ans || return
     13     printf %s "$ans"
     14 }
     15 prompt_required() {
     16     promptout=''; promptout=$(prompt "$@")
     17     until [ "$promptout" ]; do
     18         out "This is required."
     19         promptout=$(prompt "$@")
     20     done
     21     printf %s "$promptout"
     22 }
     23 prompt_noecho() {
     24     # This is the same with the prompt() function except that it doesn't echo
     25     # the user input to the terminal. It can be used to ask for passwords and
     26     # other secret information.
     27     stty=$(stty -g); stty -echo
     28     prompt "$@"; printf '\n' >&2
     29     stty "$stty"
     30 }
     31 
     32 getbut() {
     33     # This function can be used to get a single button input. No arguments
     34     # necessary.
     35     stty=$(stty -g); stty -icanon -echo
     36     dd bs=1 count=1 2>/dev/null
     37     stty "$stty"
     38 }
     39 
     40 yesno() {
     41     # Function to ask the user yes/no questions. Returns 0 if the button
     42     # received is Y/y, Returns 1 if the button received is N/n, and returns
     43     # 2 if any other button is received.
     44     printf '%s\n' "$@"; printf 'y/n: '
     45     ans=$(getbut)
     46     printf '%s\n' "$ans" >&2
     47     case "$ans" in Y|y) return 0;; N|n) return 1; esac; return 2
     48 }
     49 
     50 delete() {
     51     rm -f "$accountdir/$title.el"
     52     sed_i "/^IMAPStore $title-remote\$/,/^# End profile\$/d" "$HOME/.mbsyncrc"
     53     sed_i "/^account $title\$/,/^# End profile\$/d" "$config_home/msmtp/config"
     54     rm -f /tmp/mbsync-boxes
     55 }
     56 
     57 contains() {
     58     # Check whether a list contains the given value
     59     # Example: 'contains "a" "a b c d e f"' returns 0
     60     case " $2 " in *" $1 "*) return 0; esac; return 1
     61 }
     62 
     63 sed_i() {
     64     # 'sed -i' like function without actually running it. This can be only used
     65     # for a single file, and the filename should always come last.
     66     eval "[ -f \$$# ] || return 1"
     67     sed "$@" > _; for file; do :; done; mv _ "$file"
     68 }
     69 
     70 get_profiles() {
     71     # Function to get all available profiles
     72     unset profiles
     73     for profile in "$accountdir"/*.el; do
     74         [ -f "$profile" ] || continue
     75         profile=${profile##*/}
     76         profiles="${profile%.el} $profiles"
     77     done
     78 }
     79 
     80 msmtp_header() {
     81     mkdir -p "$config_home/msmtp" \
     82              "${XDG_CACHE_HOME:=$HOME/.cache}/msmtp"
     83     cat <<EOF > "$config_home/msmtp/config"
     84 defaults
     85 auth on
     86 tls on
     87 tls_trust_file $sslcert
     88 logfile $XDG_CACHE_HOME/msmtp/msmtp.log
     89 EOF
     90 }
     91 
     92 pm_ask() {
     93     case ${pass_prog##*/} in
     94         pass) pass insert "$pmt"
     95               password_command="pass show $pmt" ;;
     96         pash) trap 'delete; rm -f _' EXIT INT
     97               sed 's/yn "Gen/false "Gen/g' "$(command -v pash)" >_
     98               sh _ add "$pmt"
     99               rm -f _; trap delete EXIT INT
    100               password_command="pash show $pmt" ;;
    101         pm)   pass=$(prompt_noecho "Enter your password")
    102               pass2=$(prompt_noecho "Enter your password again")
    103               if [ "$pass" = "$pass2" ]; then
    104                   pm add "$pmt" <<EOF
    105 $pass
    106 EOF
    107                   password_command="pm show $pmt"
    108               else
    109                   err "Passwords don't match"
    110                   return 1
    111               fi
    112     esac
    113 }
    114 
    115 pm_del() {
    116     case ${pass_prog##*/} in
    117         pass) pass rm -f "$pmt" ;;
    118         pash) yes | pash del "$pmt" ;;
    119         pm)   pm del "$pmt"
    120     esac >/dev/null 2>&1 ||:
    121 }
    122 
    123 test_connection() {
    124     mkdir -p "$maildir/$title"
    125 
    126     # Since we are "Flattening" the inbox structure by replacing '/' with '.',
    127     # we need to also replace it in the 'mbsync -l' output by hand. See the
    128     # Flatten section on mbsync(1).
    129     { mbsync -l "$title" || die "Log-on not successful." \
    130           "It seems that either you have inputted the wrong password or server settings," \
    131           "or there are requirements for your account out of the control of mu-wizard."
    132     } | sed 's|/|.|g' > /tmp/mbsync-boxes
    133 
    134     while read -r dir; do mkdir -p "$maildir/$title/${dir#/}"; done < /tmp/mbsync-boxes
    135 
    136     drafts=$(grep -i drafts /tmp/mbsync-boxes | sed 1q)
    137     trash=$(grep  -i trash  /tmp/mbsync-boxes | sed 1q)
    138     inbox=$(grep  -i inbox  /tmp/mbsync-boxes | sed 1q)
    139     sent=$(grep   -i sent   /tmp/mbsync-boxes | sed 1q)
    140 
    141     rm -f /tmp/mbsync-boxes
    142 }
    143 
    144 get_domains() {
    145     if [ -f "$confdir/domains.csv" ]; then
    146         { [ -f "$sharedir/domains.csv" ] && cat "$sharedir/domains.csv"
    147           cat "$confdir/domains.csv" ;} | sort -uo "$confdir/domains.csv"
    148     else
    149         if [ -f "$sharedir/domains.csv" ]; then
    150             cat "$sharedir/domains.csv"; fi > "$confdir/domains.csv"
    151     fi
    152 }
    153 
    154 set_mbsync() {
    155     cat <<EOF >> "$HOME/.mbsyncrc"
    156 IMAPStore $title-remote
    157 Host $imap
    158 Port $iport
    159 User $login
    160 PassCmd "$password_command"
    161 SSLType $ssltype
    162 CertificateFile $sslcert
    163 
    164 MaildirStore $title-local
    165 Subfolders Verbatim
    166 Path ~/.local/share/mail/$title/
    167 Inbox ~/.local/share/mail/$title/INBOX
    168 Flatten .
    169 
    170 Channel $title
    171 Expunge Both
    172 Master :$title-remote:
    173 Slave  :$title-local:
    174 Patterns * !"[Gmail]/All Mail"
    175 Create Both
    176 SyncState *
    177 MaxMessages ${maxmes:-0}
    178 ExpireUnread no
    179 # End profile
    180 
    181 EOF
    182 }
    183 
    184 set_msmtp() {
    185     [ -f "$config_home/msmtp/config" ] || msmtp_header
    186 
    187     cat <<EOF >> "$config_home/msmtp/config"
    188 account $title
    189 host $smtp
    190 port $sport
    191 from $fulladdr
    192 user $login
    193 passwordeval "$password_command"
    194 $starttlsoff
    195 # End profile
    196 
    197 EOF
    198 }
    199 
    200 set_accountlisp() {
    201     cat <<EOF > "$accountdir/$title.el"
    202 (add-to-list 'mu4e-contexts
    203 (make-mu4e-context
    204   :name "$title"
    205   :match-func (lambda (msg)
    206                 (when msg
    207                   (mu4e-message-contact-field-matches msg
    208                      :to "$fulladdr")))
    209   :vars '((user-mail-address                . "$fulladdr")
    210           (message-sendmail-extra-arguments . ("-a" "$title"))
    211           (mu4e-sent-messages-behavior      . $msg_behavior)
    212           (mu4e-drafts-folder               . "/$title/${drafts:=Drafts}")
    213           (mu4e-trash-folder                . "/$title/${trash:=Trash}")
    214           (mu4e-sent-folder                 . "/$title/${sent:=Sent}")
    215           (mu4e-maildir-shortcuts           . (("/$title/${inbox:-INBOX}" . ?i)
    216                                                ("/$title/$drafts"         . ?d)
    217                                                ("/$title/${sent:-Sent}" . ?s)))
    218           (user-full-name                   . "$realname"))))
    219 EOF
    220 
    221 }
    222 
    223 get_information() {
    224     for file in /etc/ssl/certs/ca-certificates.crt /etc/pki/tls/certs/ca-bundle.crt \
    225                 /etc/certificates/cert.pem /etc/ssl/ca-bundle.pem /etc/ssl/cert.pem \
    226                 /etc/pki/tls/cacert.pem /usr/share/ca-certificates; do
    227         [ -r "$file" ] || continue
    228         sslcert=$file; break
    229     done
    230 
    231     fulladdr=$(prompt_required Email \
    232                "Insert the email address that you want to configure for mu4e")
    233 
    234     # Check the override directory for possible alterations of the
    235     # configuration. If the override is found on the configuration dirctory, it
    236     # will be used instead.
    237     for file in "$confdir/overrides/"* "$sharedir/overrides/"*; do
    238         [ -f "$sharedir/overrides/${fulladdr##*@}" ] && {
    239             domain=${fulladdr##*@}
    240             # shellcheck disable=1090
    241             . "$sharedir/overrides/$domain"
    242             break
    243         }
    244     done
    245 
    246     [ "$domain" ] || while IFS=, read -r domain imap iport smtp sport; do
    247         case "$domain" in "${fulladdr##*@}") break; esac
    248     done < "$confdir/domains.csv"
    249 
    250     if [ "$domain" = "${fulladdr##*@}" ]; then
    251         out "Your domain information was found!'" \
    252             "The following information will be used:" ""\
    253             "Domain: $domain" \
    254             "IMAP:   $imap:$iport" \
    255             "SMTP:   $smtp:$sport"
    256     else
    257         imap=$(prompt Server "Insert the IMAP server for your email provider" \
    258             "(excluding the port number)")
    259 
    260         iport=$(prompt Port \
    261             "What is your server's IMAP port number? (Usually 993)")
    262 
    263         smtp=$(prompt Server "Insert the SMTP server for your email provider" \
    264             "(excluding the port number)")
    265 
    266         sport=$(prompt Port \
    267             "What is your server's SMTP port number? (Usually 587)")
    268     fi
    269 
    270 
    271     realname=$(prompt "Real name" \
    272         "Enter your full name you want to be identified on this account.")
    273 
    274     title=$(prompt_required "Account name" \
    275         "Enter a short, one-word identifier for this email account that will" \
    276         "distinguish them from any other accounts you add")
    277 
    278     while contains "$title" "$profiles"; do
    279         out "The title '$title' is already used."
    280         title=$(prompt_required "Account name" \
    281         "Enter a short, one-word identifier for this email account that will" \
    282         "distinguish them from any other accounts you add")
    283     done
    284 
    285     login=$(prompt "Login" \
    286         "If your account has a special username different from your address," \
    287         "insert it now. Otherwise leave this blank.")
    288 
    289     # If login is empty use fulladdr
    290     [ "$login" ] || login="$fulladdr"
    291 
    292     maxmes=$(prompt "Maximum messages" \
    293         "If you want to limit the number of messages kept offline to a number," \
    294         "enter it below. Otherwise leave this blank.")
    295 
    296     case "$sport" in 465) starttlsoff="tls_starttls off"; esac
    297 }
    298 
    299 main() {
    300     pass_prog=${MUW_PWM:-$(command -v pass || command -v pash || command -v pm)} ||
    301         die "No applicable password manager found."
    302     command -v mbsync >/dev/null || die "mbsync must be installed for mu-wizard to work."
    303 
    304     sharedir=/usr/share/mu-wizard
    305     ssltype=IMAPS
    306     msg_behavior=sent
    307 
    308     mkdir -p "${config_home:=${XDG_CONFIG_HOME:-$HOME/.config}}" \
    309              "${confdir:=$config_home/mu4e}" \
    310              "${accountdir:=$confdir/accounts}" \
    311              "${maildir:=$HOME/.local/share/mail}"
    312 
    313     get_profiles
    314 
    315     case "$1" in
    316         l|list) for profile in $profiles; do out "  $profile"; done ;;
    317 
    318         a|add)
    319             get_domains
    320             get_information
    321             trap delete INT EXIT
    322             pmt="mu-wizard-$title"
    323             while :; do pm_del; pm_ask && break; done
    324             set_mbsync
    325             test_connection
    326             set_msmtp
    327             set_accountlisp
    328             [ -f "$confdir/mu4e-config.el" ] || cp "$sharedir/mu4e-config.el" "$confdir/mu4e-config.el"
    329 
    330             # We add user's personal domain information to the config directory,
    331             # so even if the user doesn't want to share the information in a git
    332             # repository, they can have it themselves when they may need it.
    333 
    334             printf '%s,%s,%s,%s,%s' \
    335                    "${fulladdr##*@}" "$imap" "$iport" "$smtp" "$sport" >> "$confdir/domains.csv"
    336             trap - INT EXIT
    337             out "All done. You can now run 'mbsync $title' in order to sync this account."
    338             ;;
    339 
    340         d|delete)
    341             out "Pick a profile to be deleted"
    342             for profile in $profiles; do out "  $profile"; done
    343             read -r title
    344             contains "$title" "$profiles" || die "Profile '$title' doesn't exist."
    345             yesno "Are you sure you want to delete '$title'?" && delete
    346             ;;
    347         p|purge)
    348             yesno "Are you sure you want to delete all account data?" || exit 1
    349             rm -rf "$HOME/.mbsyncrc" "$confdir" "$config_home/msmtp"
    350             ;;
    351         s|share)
    352             # Output the share directory and exit.
    353             printf '%s\n' "$sharedir" ;;
    354         ''|--help|-h)
    355             out "usage: ${0##*/} [action]" \
    356                 "mu-wizard, auto-configure email accounts for mu4e" \
    357                 "  Options:" \
    358                 "    [a]dd:    Add and autoconfigure an email address" \
    359                 "    [d]elete: Pick an account to delete" \
    360                 "    [l]ist:   List configured accounts" \
    361                 "    [p]urge:  Purge all configuration"
    362                 "    [s]hare:  See your share directory" "" \
    363                 "NOTE: Once at least one account is added, you can run" \
    364                 "'mbsync -a' to begin downloading mail."
    365             ;;
    366         *) die "Unknown action '$1'"
    367     esac
    368 
    369 }
    370 
    371 main "$@"