#!/usr/bin/env bash # This script has the lofty goal of becoming a full configuration utility for mutt. # Written by Storm Dragon: https://social.stormdragon.tk/storm # Written by Michael Taboada: https://2mb.social/mwtab # Contributions by Kyle: https://kyle.tk # Released under the terms of the WTFPL: http://wtfpl.net # Settings to improve accessibility of dialog. export DIALOGOPTS='--insecure --no-lines --visit-items' # Array of command line arguments declare -A command=( [h]="Show this help information." [t]="Test mode. Use ~/mutt_test instead of ~/.mutt for configuration." ) # Variables muttHome="${HOME}/.mutt" testMode=false # Process command line arguments # Convert the keys of the associative array to a format usable by getopts args="${!command[*]}" args="${args//[[:space:]]/}" while getopts "${args}" i ; do case "$i" in h) help ;; t) testMode=true muttHome="${HOME}/mutt_test" ;; esac done # Functions help() { echo "fleacollar.sh" echo "Released under the terms of the WTFPL: http://wtfpl.net" echo -e "A configuration utility for mutt.\n" echo -e "Usage:\n" echo "With no arguments, run the configuration utility." for i in "${!command[@]}" ; do echo "-${i}: ${command[${i}]}" done | sort exit 0 } check_dependancies() { local dep for dep in dialog mutt w3m openssl ; do if ! command -v $dep &> /dev/null ; then echo "$dep is not installed. Please install $dep and run this script again." exit 1 fi done } inputbox() { # Returns: text entered by the user # Args 1, Instructions for box. # args: 2 initial text (optional) dialog --backtitle "Enter text and press enter." \ --inputbox "$1" 0 0 "$2" --stdout } menulist() { # Args: minimum group 2, multiples of 2, "tag" "choice" # returns: selected tag local menuList ifs="$IFS" IFS=$'\n' dialog --backtitle "Use the up and down arrow keys to find the option you want, then press enter to select it." \ --no-tags \ --menu "Please select one" 0 0 0 $@ --stdout IFS="$ifs" } msgbox() { # Returns: None # Shows the provided message on the screen with an ok button. dialog --msgbox "$*" 0 0 } passwordbox() { # Returns: text entered by the user # Args 1, Instructions for box. # args: 2 initial text (optional) dialog --backtitle "Enter text and press enter." \ --passwordbox "$1" 0 0 "$2" --stdout } yesno() { # Returns: Yes or No # Args: Question to user. # Called in if $(yesno) == "Yes" # Or variable=$(yesno) dialog --backtitle "Press 'Enter' for \"yes\" or 'Escape' for \"no\"." --yesno "$*" 0 0 --stdout if [[ $? -eq 0 ]]; then echo "Yes" else echo "No" fi } # Function to encrypt a file with OpenSSL encrypt_file() { # $1 = input file to encrypt # $2 = output file local passphrase # If master password file doesn't exist, create it if [[ ! -f "$muttHome/.master_password" ]]; then passphrase="" passphraseCompare="-" while [[ "${passphrase}" != "${passphraseCompare}" ]] ; do passphrase="$(passwordbox "Please create a master password for encrypting your email credentials:")" passphraseCompare="$(passwordbox "Please re-enter the master password:")" if [[ "${passphrase}" != "${passphraseCompare}" ]]; then msgbox "Passphrase does not match, please retry." fi done # Store a hash of the master password for verification later echo "$(echo "$passphrase" | openssl dgst -sha256 -hex | cut -d ' ' -f 2)" > "$muttHome/.master_password" else # Ask for existing master password passphrase="$(passwordbox "Enter your master password to encrypt this email configuration:")" # Verify the password local stored_hash="$(cat "$muttHome/.master_password")" local input_hash="$(echo "$passphrase" | openssl dgst -sha256 -hex | cut -d ' ' -f 2)" if [[ "$stored_hash" != "$input_hash" ]]; then msgbox "Incorrect master password. Please try again." encrypt_file "$1" "$2" return fi fi # Encrypt the file with AES-256-CBC openssl enc -aes-256-cbc -salt -a -pbkdf2 -pass pass:"$passphrase" -in "$1" -out "$2" } # Function to decrypt a file decrypt_file() { # $1 = encrypted file # $2 = output file (temporary) local passphrase # If the master password file doesn't exist, we can't decrypt if [[ ! -f "$muttHome/.master_password" ]]; then echo "ERROR: No master password set" return 1 fi # Ask for the master password passphrase="$(passwordbox "Enter your master password to decrypt email configuration:")" # Verify the password local stored_hash="$(cat "$muttHome/.master_password")" local input_hash="$(echo "$passphrase" | openssl dgst -sha256 -hex | cut -d ' ' -f 2)" if [[ "$stored_hash" != "$input_hash" ]]; then msgbox "Incorrect master password. Please try again." decrypt_file "$1" "$2" return fi # Decrypt the file openssl enc -aes-256-cbc -d -a -pbkdf2 -pass pass:"$passphrase" -in "$1" -out "$2" 2>/dev/null } initialize_directory() { mkdir -p "${muttHome}/cache/bodies" mkdir -p "${muttHome}/scripts" if ! [ -f "$muttHome/aliases" ]; then touch "$muttHome/aliases" fi # Copy the add_address.sh script if ! [ -f "$muttHome/scripts/add_address.sh" ]; then cp "files/add_address.sh" "$muttHome/scripts/add_address.sh" chmod 700 "$muttHome/scripts/add_address.sh" fi if ! [ -f "$muttHome/mailcap" ]; then cp "scripts/mailcap" "$muttHome/mailcap" fi # Copy the gpg.rc file if it exists in files directory if [[ -f "files/gpg.rc" ]] && ! [[ -f "$muttHome/gpg.rc" ]]; then cp "files/gpg.rc" "$muttHome/gpg.rc" echo "# Note: GPG signing/encryption is available but not configured." >> "$muttHome/gpg.rc" echo "# You can manually set up GPG for email if needed." >> "$muttHome/gpg.rc" fi # Create or update macro file cp "files/macros" "$muttHome/macros" # Create basic muttrc if ! [ -f "$muttHome/muttrc" ]; then # Find desired editor x=0 for i in nano vim ; do unset editorPath editorPath="$(command -v $i)" if [[ -n "$editorPath" ]]; then editors[$x]="$editorPath" ((x++)) fi done echo "Select editor for email composition:" i="$(menulist ${EDITOR} ${EDITOR} $(for i in "${editors[@]##*/}" ; do echo "$i";echo "$i";done))" if [[ -z "$i" ]]; then exit 0 fi # Copy base muttrc cp "files/muttrc" "$muttHome/muttrc" # Set editor in muttrc case "$i" in "nano") sed -i "1s|^|set editor = '$i +7 -r 72'\n|" "$muttHome/muttrc" ;; "vim") sed -i "1s|^|set editor = \"vim -c 'set spell spelllang=${LANG::2}'\"\n|" "$muttHome/muttrc" ;; esac # Replace muttHome in muttrc sed -i "s|MUTTHOME|${muttHome}|g" "$muttHome/muttrc" fi # Create the decrypt helper script create_decrypt_helper } create_decrypt_helper() { # Create a helper script to decrypt files cat > "$muttHome/scripts/decrypt.sh" << 'EOF' #!/bin/bash # Script to decrypt an encrypted email configuration file # Usage: decrypt.sh encrypted_file temp_output_file if [[ $# -ne 2 ]]; then echo "Usage: $0 encrypted_file temp_output_file" exit 1 fi ENCRYPTED_FILE="$1" OUTPUT_FILE="$2" MASTER_FILE="$(dirname "$ENCRYPTED_FILE")/.master_password" if [[ ! -f "$ENCRYPTED_FILE" ]]; then echo "Error: Encrypted file not found" exit 1 fi if [[ ! -f "$MASTER_FILE" ]]; then echo "Error: Master password file not found" exit 1 fi # Function to decrypt decrypt() { local stored_hash="$(cat "$MASTER_FILE")" local passphrase local attempt=0 while [ $attempt -lt 3 ]; do passphrase="$(dialog --passwordbox "Enter master password to decrypt:" 0 0 --stdout)" # Check password hash local input_hash="$(echo "$passphrase" | openssl dgst -sha256 -hex | cut -d ' ' -f 2)" if [[ "$stored_hash" = "$input_hash" ]]; then # Decrypt the file openssl enc -aes-256-cbc -d -a -pbkdf2 -pass pass:"$passphrase" -in "$ENCRYPTED_FILE" -out "$OUTPUT_FILE" 2>/dev/null return 0 else dialog --msgbox "Incorrect password. Please try again." 0 0 ((attempt++)) fi done echo "Too many failed attempts." return 1 } # Create temp directory if it doesn't exist mkdir -p "$(dirname "$OUTPUT_FILE")" # Decrypt the file decrypt # Make sure the output file is readable if [[ -f "$OUTPUT_FILE" ]]; then chmod 600 "$OUTPUT_FILE" else echo "Error: Failed to decrypt file" exit 1 fi exit 0 EOF # Make script executable chmod 700 "$muttHome/scripts/decrypt.sh" } add_email_address() { read -p "Please enter your email address: " emailAddress if ! [[ "$emailAddress" =~ .*@.*\..* ]]; then read -p "This appears to be an invalid email address. Continue anyway? (y/n) " continue if [[ "${continue^}" != "Y" ]]; then exit 0 fi fi if [[ -f "$muttHome/$emailAddress" ]] || [[ -f "$muttHome/$emailAddress.ssl" ]]; then read -p "This email address already exists. Overwrite the existing settings? (y/n) " continue if [[ "${continue^}" != "Y" ]]; then exit 0 else sed -i "/$emailAddress/d" "$muttHome/muttrc" rm -f "$muttHome/$emailAddress" "$muttHome/$emailAddress.ssl" fi fi # Create a temporary file for configuration configFile="$muttHome/$emailAddress" # Write basic config echo "set realname=\"$(inputbox "Enter your name as you want it to appear in emails:")\"" > "$configFile" echo "set from=\"$emailAddress\"" >> "$configFile" echo "set use_from = \"yes\"" >> "$configFile" echo "set hostname=${emailAddress##*@}" >> "$configFile" # Configure account based on email domain case "$emailAddress" in *gmail.com) configure_gmail "$emailAddress" ;; *hotmail.com | *outlook.com | *live.com) configure_hotmail "$emailAddress" ;; *) continue="$(yesno "Is this a gmail account?")" if [[ "$continue" == "Yes" ]]; then configure_gmail "$emailAddress" else configure_generic "$emailAddress" fi esac # Get password for email emailPassword="$(passwordbox "Please enter the password for $emailAddress:")" # Add password to the config file echo "set imap_pass=\"$emailPassword\"" >> "$configFile" echo "set smtp_pass=\"$emailPassword\"" >> "$configFile" # Add source for aliases echo "source ~/${muttHome#/home/*/}/aliases" >> "$configFile" # Add folder-hook echo "folder-hook .*$emailAddress/ 'source ${muttHome/#$HOME/\~}/$emailAddress.ssl'" >> "$configFile" # Encrypt the configuration file encrypt_file "$configFile" "$configFile.ssl" # Create a wrapper configuration file that sources the encrypted file echo "# Encrypted email configuration for $emailAddress" > "$configFile" echo "# The real configuration is in $emailAddress.ssl (encrypted)" >> "$configFile" echo "" >> "$configFile" echo "# This command will decrypt the configuration when needed" >> "$configFile" echo "source \"| $muttHome/scripts/decrypt.sh $muttHome/$emailAddress.ssl /tmp/mutt-$emailAddress-$USER\"" >> "$configFile" # Add keybinding in muttrc add_keybinding # Clean up rm -f "/tmp/mutt-$emailAddress-$USER" 2>/dev/null msgbox "Email address added, press enter to continue." } configure_gmail() { # Copy the Gmail template and replace placeholders cp "files/gmail.template" "$muttHome/$1.tmp" # Replace email placeholder in the template sed -i "s|EMAIL_ADDRESS|$1|g" "$muttHome/$1.tmp" sed -i "s|USERNAME|${1%@*}|g" "$muttHome/$1.tmp" # Append the template to the existing config cat "$muttHome/$1.tmp" >> "$muttHome/$1" rm "$muttHome/$1.tmp" # Handle goobook integration if available unset continue if command -v goobook &> /dev/null ; then read -p "Goobook is installed, would you like to use it as your addressbook for the account $1? " continue if [[ "${continue^}" = "Y" ]]; then echo "set query_command=\"goobook query %s\"" >> "$muttHome/$1" echo "macro index,pager a \"goobook add\" \"add sender to google contacts\"" >> "$muttHome/$1" fi fi } configure_hotmail() { # Copy the Hotmail template and replace placeholders cp "files/hotmail.template" "$muttHome/$1.tmp" # Replace email placeholder in the template sed -i "s|EMAIL_ADDRESS|$1|g" "$muttHome/$1.tmp" # Append the template to the existing config cat "$muttHome/$1.tmp" >> "$muttHome/$1" rm "$muttHome/$1.tmp" } configure_generic() { # Break the email address into its components: local userName="${1%%@*}" local hostName="${1##*@}" local imapHost local imapUser local imapPort local smtpHost local smtpUser local smtpPort # Gather input read -p "Enter imap host: " -e -i imap.$hostName imapHost read -p "Enter imap user: " -e -i $1 imapUser read -p "Enter imap port: " -e -i 993 imapPort read -p "Enter smtp host: " -e -i smtp.$hostName smtpHost read -p "Enter smtp user: " -e -i $userName smtpUser read -p "Enter smtp port: " -e -i 587 smtpPort # Copy the generic template cp "files/generic.template" "$muttHome/$1.tmp" # Replace placeholders sed -i "s|IMAP_HOST|$imapHost|g" "$muttHome/$1.tmp" sed -i "s|IMAP_USER|$imapUser|g" "$muttHome/$1.tmp" sed -i "s|IMAP_PORT|$imapPort|g" "$muttHome/$1.tmp" sed -i "s|SMTP_HOST|$smtpHost|g" "$muttHome/$1.tmp" sed -i "s|SMTP_USER|$smtpUser|g" "$muttHome/$1.tmp" sed -i "s|SMTP_PORT|$smtpPort|g" "$muttHome/$1.tmp" # Append the template to the existing config cat "$muttHome/$1.tmp" >> "$muttHome/$1" rm "$muttHome/$1.tmp" # Add any extra settings read -p "Enter extra settings, one line at a time, just press enter when done: " extraSettings while [ "$extraSettings" != "" ]; do echo "$extraSettings" >> "$muttHome/$1" read extraSettings done } add_keybinding() { # Here we search for previous keybinding local fNumber=1 while : ; do grep "^[[:space:]m]acro.*index.*.*" $muttHome/muttrc &> /dev/null || break # fNumber is now the currently open keybinding. ((fNumber++)) # fNumber was taken, so increment it. done # Bind key FfNumber to the mail account. echo "macro generic,index 'source ${muttHome/#$HOME/\~}/$emailAddress!'" >> "$muttHome/muttrc" echo "mail account $emailAddress bound to F$fNumber." if ! grep "^source.*@.*\..*" "$muttHome/muttrc" &> /dev/null ; then continue="$(yesno "Make $emailAddress the default account?")" if [[ "$continue" = "Yes" ]]; then echo "source ${muttHome/#$HOME/\~}/$emailAddress" >> "$muttHome/muttrc" fi fi } new_contact() { contactName="$(inputbox "Enter the contact's first and last name..")" if [[ -z "$contactName" ]]; then exit 0 fi contactEmail="$(inputbox "Enter the email address for $contactName")" if [[ -z "$contactEmail" ]]; then exit 0 fi contactAlias="${contactName,,}" contactAlias="${contactAlias// /-}" if grep "<$contactEmail>\| $contactAlias " "$muttHome/aliases" &> /dev/null ; then [[ "$(yesno "This email address already exists in your contacts. Continue anyway?")" != "Yes" ]] && exit 0 fi echo "alias $contactAlias $contactName <$contactEmail>" >> "$muttHome/aliases" sort -u "$muttHome/aliases" -o "$muttHome/aliases" msgbox "$contactName added to your address book." } # This is the main loop of the program # Call functions to be ran every time the script is ran. check_dependancies initialize_directory # If in test mode, display a notification if [[ "$testMode" = true ]]; then echo "Running in test mode. Using $muttHome for configuration." fi # Let's make a mainmenu variable to hold all the options for the select loop. mainmenu=("Add Email Address" "New Contact" "Exit") while : ; do i="$(IFS=$'\n';menulist $(for i in "${mainmenu[@]}" ; do echo "$i";echo "$i";done))" [[ -z "$i" ]] && continue functionName="${i,,}" functionName="${functionName// /_}" functionName="${functionName/exit/exit 0}" $functionName done exit 0