Tired of being locked into a single default program for your email attachments in Neomutt? This tutorial will guide you through creating an intelligent file-open-chooser that dynamically reads your system’s mimeapps.list configuration. With a single keypress, you can select from all associated applications to open any attachment, bringing the flexibility of a file manager like Ranger directly into your email client.

Changelog

DateChange
2025-08-04Initial version of the tutorial published.

1. Prerequisites

Before we begin, ensure your system has the necessary software and configuration in place.

1.1. Required Software

This script relies on a few common command-line utilities.

# Arch Linux Installation
sudo pacman -S neomutt rofi file

# Optional: dmenu as a fallback for rofi
sudo pacman -S dmenu

1.2. Existing Configuration

You should already have:

  • A working Neomutt setup (e.g., one configured with mutt-wizard).
  • A populated ~/.config/mimeapps.list file with your preferred application associations.

2. The Core Script: file-open-chooser

This script is the heart of our new functionality. It identifies an attachment’s file type, finds all associated programs from your mimeapps.list, and presents them in a selection menu.

ℹ️ BEYOND NEOMUTT

While we focus on Neomutt here, this script is a full-featured, system-wide file opener. It reads your mimeapps.list and works in any context where files can be piped via stdin - terminal, other mail clients, RSS readers, or custom scripts.

Step 1: Create the Script’s Directory and File

To keep your scripts organized, we will create a dedicated folder and the script file within it.

# Create the subdirectory for our project
mkdir -p ~/Scripts/file-open-chooser

# Create the script file itself
touch ~/Scripts/file-open-chooser/file-open-chooser.sh

Now, open the file ~/Scripts/file-open-chooser/file-open-chooser.sh and paste the entire content below into it.

#!/bin/bash
set -euo pipefail

# Create a temporary file to hold the attachment content piped from Neomutt
tempfile=$(mktemp)
cat > "$tempfile"

# Determine the MIME type of the temporary file
mimetype=$(file --mime-type -b "$tempfile")

# --- Function to extract available programs from mimeapps.list ---
get_programs_for_mime() {
    local mime="$1"
    
    # Read from both [Default Applications] and [Added Associations] sections
    for section in "Default Applications" "Added Associations"; do
        if grep -q "^\[$section\]" ~/.config/mimeapps.list 2>/dev/null; then
            # Use awk to find and print programs for the given MIME type
            apps=$(awk -v section="$section" -v mime="$mime" '
                /^\[.*\]/ { current_section = $0; gsub(/[\[\]]/, "", current_section) }
                current_section == section && $0 ~ "^" mime "=" {
                    sub("^" mime "=", "")
                    gsub(/;/, "\n")
                    print
                }' ~/.config/mimeapps.list)
            
            if [ -n "$apps" ]; then
                echo "$apps"
            fi
        fi
    done
    
    # Always include a "Default" option to use the system's xdg-open
    echo "Default"
}

# --- Function to convert a .desktop file name to a human-readable application name ---
desktop_to_name() {
    local desktop="$1"
    local desktop_file=""
    
    # Search for the .desktop file in standard system locations
    for dir in ~/.local/share/applications /usr/share/applications /usr/local/share/applications; do
        if [ -f "$dir/$desktop" ]; then
            desktop_file="$dir/$desktop"
            break
        fi
    done
    
    if [ -n "$desktop_file" ]; then
        # Extract the 'Name=' field from the .desktop file
        name=$(grep "^Name=" "$desktop_file" | head -1 | cut -d'=' -f2-)
        echo "${name:-$desktop}" # Fallback to the filename if Name is not found
    else
        echo "$desktop"
    fi
}

# --- Main Logic ---

# Collect a unique, sorted list of programs
programs=$(get_programs_for_mime "$mimetype" | sort -u)
display_programs=""

# Build the display list with readable names
while IFS= read -r program; do
    if [ "$program" = "Default" ]; then
        display_programs="$display_programs$program\n"
    else
        readable_name=$(desktop_to_name "$program")
        display_programs="$display_programs$readable_name ($program)\n"
    fi
done <<< "$programs"

# Show the selection menu using rofi, dmenu, or a terminal prompt as fallback
choice="" # Initialize variable
if command -v rofi >/dev/null; then
    choice=$(echo -e "$display_programs" | rofi -dmenu -p "Open $mimetype with:")
elif command -v dmenu >/dev/null; then
    choice=$(echo -e "$display_programs" | dmenu -p "Open $mimetype with:")
else
    echo "Choose program to open $mimetype file:"
    echo -e "$display_programs" | nl
    read -p "Enter choice number: " num
    choice=$(echo -e "$display_programs" | sed -n "${num}p")
fi

# Exit if the user cancelled the selection (e.g., by pressing Esc in rofi)
if [ -z "$choice" ]; then
    rm -f "$tempfile"
    exit 0
fi

# Execute the chosen program
if [ "$choice" = "Default" ]; then
    setsid xdg-open "$tempfile" >/dev/null 2>&1 &
else
    # Extract the .desktop filename from the selection (e.g., from "Okular (org.kde.okular.desktop)")
    desktop_file=$(echo "$choice" | sed 's/.*(\(.*\))/\1/')
    if [ -n "$desktop_file" ] && [ "$desktop_file" != "$choice" ]; then
        # Use gtk-launch for .desktop files, as it's the proper way to launch them
        setsid gtk-launch "$desktop_file" "$tempfile" >/dev/null 2>&1 &
    else
        # Fallback to xdg-open if something went wrong
        setsid xdg-open "$tempfile" >/dev/null 2>&1 &
    fi
fi

# Clean up the temporary file after a delay to give the program time to open it
(sleep 60; rm -f "$tempfile") &

Step 2: Make the Script Executable

Your shell needs permission to run the script file.

chmod +x ~/Scripts/file-open-chooser/file-open-chooser.sh

3. Neomutt Integration

Now, let’s teach Neomutt our new trick. We will bind the o key (for “open”) in the attachment view to execute our script directly.

Add the following line to your Neomutt configuration file (e.g., ~/.config/mutt/muttrc):

# Add a macro to the attachment menu (key 'o') to pipe the
# attachment to our script by calling it with its full path.
macro attach o "<pipe-entry>bash $HOME/Scripts/file-open-chooser/file-open-chooser.sh<enter>" "Choose program to open attachment"
ℹ️ WHY 'BASH $HOME/...'?

By explicitly calling bash, we ensure the script is executed with the Bash interpreter. Using $HOME instead of ~ is a good practice inside configurations, as it’s more robustly expanded in different contexts.


4. Usage in Neomutt

Your new workflow is simple and efficient:

  1. Navigate to an email with an attachment.
  2. Press v to open the attachment view.
  3. Use the arrow keys to select the desired attachment.
  4. Press o (our new macro).
  5. A Rofi (or dmenu) window will appear, listing all compatible programs.
  6. Select a program, and the file will open.

Example Workflows:

  • PDF Attachmento → Choose between Okular, Zathura, Evince…
  • Image Attachmento → Choose between Viewnior, GIMP, Inkscape…
  • Video Attachmento → Choose between MPV, VLC…

5. Customization

To add a new program to the list for a specific file type, simply edit your ~/.config/mimeapps.list file. The changes are picked up by the script immediately, with no need to restart anything!

Example [Added Associations] section:

[Added Associations]
application/pdf=org.kde.okular.desktop;org.pwmt.zathura.desktop;org.gnome.Evince.desktop;
image/jpeg=viewnior.desktop;gimp.desktop;

Another Useful Keybinding

You can still define a separate shortcut to always use the default system application without seeing the menu.

# Add this to your muttrc. 'O' (Shift+o) will open with the default handler.
macro attach O "<pipe-entry>xdg-open<enter>" "Open with default application"

6. Troubleshooting

If things don’t work as expected, here are some common issues and their solutions.

  <colgroup>
    
      <col width="40%">
    
      <col width="60%">
    
  </colgroup>


<thead><tr><th>Problem</th><th>Solution</th></tr></thead><tbody>
Permission denied errorThe script is likely not executable. Run chmod +x ~/Scripts/file-open-chooser/file-open-chooser.sh again to be sure.
File not found errorDouble-check the path in your Neomutt macro. Make sure it exactly matches the location of your script. Verify with ls -l ~/Scripts/file-open-chooser/file-open-chooser.sh.
Rofi/dmenu does not startTest rofi directly by running rofi -show run. If it fails, check for error messages. Ensure it’s installed. The script should fall back to a terminal prompt if rofi is missing.
Incorrect or no programs listedTest the MIME type detection with file --mime-type -b /path/to/some/file.pdf. Check your ~/.config/mimeapps.list to ensure the associations for that MIME type are correct.

7. Conclusion

By implementing this file-open-chooser, you’ve significantly enhanced Neomutt’s capabilities. Your setup now:

  • ✅ Leverages your existing mimeapps.list configuration without duplication.
  • ✅ Works universally with any file type.
  • ✅ Provides flexible application choices with a single keypress.
  • ✅ Integrates elegantly with modern tools like Rofi.
  • ✅ Is robust, with fallbacks for different system configurations.
  • ✅ Is easy to set up with a direct, simple configuration.

You now have the same power and flexibility to open files in your email client as you do in your file manager.

📚NEOMUTT DOCS: EXTERNAL PROGRAMS 🚀ROFI ON GITHUB