521 lines
17 KiB
Bash
Executable File
521 lines
17 KiB
Bash
Executable File
#!/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 \"<pipe-message>goobook add<return>\" \"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.*<F$fNumber>.*" $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 <F$fNumber> '<sync-mailbox><enter-command>source ${muttHome/#$HOME/\~}/$emailAddress<enter><change-folder>!<enter>'" >> "$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
|