fleacollar/fleacollar.sh
2025-04-10 08:13:44 -04:00

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