mutt to org-contacts

· 609 words · 3 minute read

Part of making the whole plaintext CRM thing work involves capture: Getting contact information into the database. I made a capture template for beorg to make it easier to capture a contact on mobile, but I have a bunch of recent email contacts I’ve wanted to add, too, and no easy way to get them out of mutt.

For all of its talents, mutt doesn’t choose to expose any message variables to its macro functionality. If you want to extract a sender’s name or email address or whatever, the message has to be piped. I put something like that together to add senders to a mutt score file, and there are a few other things out there in the mutt ecosystem, like lbdb, that capture email addresses. lbdb could have been great for this, but its whole point is capture to a database of its own.

I’ve been poking around with getting ChatGPT to write little utilities for me, so I put this problem to it, asking for a Ruby script that could pipe a message through formail to extract the needed name and email address.

It took a second prompt – initially it just wrote the code to process the input without specifying the input – but it produced something workable to get the two pieces of data out, and I added an org-contacts template as a HERE doc the script writes into a mutt_contacts.org file. I could have sent it straight to contacts.org but prefer to automate into buffer files to keep the chance of conflicts, sync or otherwise, to a minimum.

The accompanying mutt macro looks like this:

macro index,pager .oc "|~/.mutt/org_contact.rb\n"

I tap .oc when positioned on a message and it pipes into the script.

I’d like to extend the macro to include a quick note, but for now it just adds the NOTES todo state and schedules the entry a day out to remind me to put something in there soon: The new entries turn up in my org-mode agenda for processing and re-filing into my contacts.org file.

And it worked well enough: Once I set it up I was able to run through and add nine or ten new contacts in a few seconds, then visit them in org-mode and refile them all.

And yeah …still holding the line against adding an MUA to Emacs proper. I just don’t want to do the whole OfflineIMAP thing or similar, and I am still keeping things relatively simple. Adding org-caldav today was a small step in the wrong direction, but I like having calendar entries in my org agenda, it syncs relatively quickly once it does the initial download, and it has no system dependencies.

#!/usr/bin/env ruby
require 'open3'
require 'date'
contacts_file = "~/org/mutt_contacts.org"
message_contents = $stdin.read
# Define the command to extract headers with formail
command = 'formail -X From: -X Sender: -X Reply-To: -x To: -x Cc: -x Bcc:'

# Execute the command and capture the output
output, status = Open3.capture2(command, stdin_data: message_contents)

if status.success?
  email = nil
  name = nil
end
  # Parse the output and extract the email and name from the From, Sender, and Reply-To headers
  output.lines.each do |line|
    case line
    when /^From:\s*(.*)$/i, /^Sender:\s*(.*)$/i, /^Reply-To:\s*(.*)$/i
      from = $1.strip

      # Extract the email and name from the From, Sender, or Reply-To header
      if from =~ /(.+?)\s*<(.+?)>/
        email = $2
        name = $1.strip
      else
        email = from
        name = "No Name Found"
      end

      # Found the email and name, so break the loop
      break
    end
  end

org_template = <<~TEMPLATE

  ** NOTES #{name} :mutt:
  SCHEDULED: #{(Date.today + 1).strftime("%Y-%m-%d")}
  :PROPERTIES:
  :EMAIL: #{email}
  :PHONE:
  :BIRTHDAY:
  :CONTACTED: #{Date.today.strftime("%Y-%m-%d")}
  :END:

TEMPLATE

File.open(contacts_file, "a") { |f| f.write org_template}