In my last post, I mentioned the usage of customized query_command
to look
up contact email address. Some might wonder why I would want to do this despite
there are many mature tools, such as abook and lbdb, available.
The reason is simple. I just do not like to install extra any extra packages for mutt contact management. Having a few scripts safely backed-up to github and providing the same functionality, is much easier to maintain.
lbdb has a script to be used in mutt sendmail
command; it collects
recipients' addresses from email header, and puts them in a database, which can
later be queried with predefined query_command
. This was the way I used the
most to manage contacts, and all the ones I care about are saved automatically
when the first time an email was sent out to them.
It turns out this is not at all difficult to be rolled out in ruby.
First of all, the contact_query
script. I have added ldap search
functionality to it as well.
#!/usr/bin/env ruby
# coding: utf-8
#Description:
# remember mutt outgoing recipients as well as from whom I have read
# their emails
require 'mail'
require 'yaml'
MUTT_DIR = "#{ENV['HOME']}/.mutt"
CONTACTS_FILE = File.join(MUTT_DIR, "contacts.yml")
LDAP_ENV_FILE = File.join(MUTT_DIR, ".env")
require 'dotenv' rescue abort "Please install net-ldap gem!"
Dotenv.load!(LDAP_ENV_FILE)
module Contacts
def self.save_mail_contacts(mail_body, fields=[ :from ], with_timestamp = true )
a = LocalContacts.new
a.from_mail(mail_body, options[:fetch], options[:time]).save
end
def self.query(filter)
agents = constants.select{|c| Class === const_get(c)}.map{|c| const_get(c) }
agents.map{|c| c.new.query(filter) }.inject(&:+)
end
def self.query_print(filter)
res = query(filter)
puts
res.each do |email, name, detail|
puts "#{email}\t#{name || ' '}\t#{detail || ' '}"
end
end
do_ldap = false
if ENV['LDAP_BASEDN']
begin
require 'net/ldap' # ruby-net-ldap gem needed
do_ldap = true
rescue
nil
end
end
class LDAPContacts
def initialize
@ldap = Net::LDAP.new :host => ENV['LDAP_SERVER'],
:port => ENV['LDAP_SERVER_PORT'],
:base => ENV['LDAP_BASEDN'],
:encryption => :simple_tls,
:auth => { :method => :simple,
:username => ENV['LDAP_BINDDN'],
:password => ENV['LDAP_PASSWORD'] }
end
def query(filter)
f = Net::LDAP::Filter.eq( "cn", "*#{filter}*" ) | Net::LDAP::Filter.eq( "mail", "*#{filter}*" )
extra_fields = ENV['LDAP_EXTRA_FIELDS'] ?
ENV['LDAP_EXTRA_FIELDS'].downcase.split.map(&:to_sym) :
[ :telephonenumber ]
fields = %w( cn mail ) + extra_fields
res = @ldap.search( filter: f, attributes: fields )
res.map{|entry|
[ entry.mail.first, # mail
entry.cn.first, # name
extra_fields.map{|i| entry[i].first.to_s }.compact.join(" | ") ] # extra info
}
end
end if do_ldap
class LocalContacts
def initialize(file = CONTACTS_FILE)
@file = file
@contacts = File.file?(@file) ? YAML.load_file(@file) : {}
end
def from_mail(mail_text, fields = [:from], with_timestamp = true)
m = Mail.new(mail_text)
new_contacts = fields.collect{|f| m[f]}.compact.collect{|f|
[f.addresses, f.display_names].transpose
}.inject(&:+).uniq
new_contacts = Hash[new_contacts.collect{|a,n| [a, {:name => n}.delete_if{|k,v| v.nil? }] } ]
new_contacts.each{|_,v| v[:last] = Time.now} if with_timestamp
new_contacts.each_key do |a|
@contacts[a] ? contacts[a].merge(new_contacts[a]) : ( contacts[a] = new_contacts[a] )
end
self
end
def save
open(@file, 'w'){|f| f.puts @file.to_yaml}
end
def query(filter)
@contacts.select{|k, v| k + v[:name].to_s =~ /#{filter}/i }. # select matching results
sort{|a,b| a.last[:last].to_s <=> b.last[:last].to_s }. # order by last contact date, desceding
map{|a,h| [a, h[:name], h[:last]] }
end
end
end
if __FILE__ == $0
require 'optparse'
options = {}
OptionParser.new do |opts|
opts.banner = "Usage: #{$0} [options] [STRING]"
options[:fetch] = nil
opts.on('-f','--fetch FIELDS','Fetch addresses from email, FIELDS should be comma separated') { |fields|
options[:fetch] = fields.downcase.split(',').map(&:to_sym)
}
opts.on('-t','--time',
'Toggle if timestamps should be added to record',
'Records with timestamps always stand before those without.'
) { options[:time] = true }
opts.on('-p','--print',
'Toggle printing of the original message',
'Useful when this script is used as mutt :display_filter '
) { options[:print] = true }
end.parse!
if options[:fetch] # fetch
mail_body = ARGF.read
Contacts.save_mail_contacts(mail_body, options[:fetch], options[:time])
puts mail_body if options[:print]
else # query
Contacts.query_print(ARGV.first)
end
end
Then the do_sendmail
script. Here I also added an ugly attachment checking.
#!/bin/bash
export PATH=~/.mutt:$PATH
## Save msg in file to re-use it for multiple tests.
t=`mktemp -t mutt_message.XXXXXX` || exit 3
cat > "$t"
# save email addresses in contacts.yml
contact_query -f to,cc,bcc -t "$t"
## Attachment keywords that the message body will be searched for:
KEYWORDS='attach|patch|附件|文件'
## Define test for multipart message.
function multipart() {
grep -q '^Content-Type: multipart' "$t"
}
## Define test for keyword search.
function word-attach() {
grep -v '^>' "$t" | grep -E -i -q "$KEYWORDS"
}
## define a dialog to confirm sending
function ask() {
# # try to get stdin/stdout/stderr from parent process
pid=$$
tty=$(ps -o tty= -p $pid)
while ! [[ $tty = *t* ]]; do # at least contain a *t* for pts/tty/etc
pid=$(ps -o ppid= -p $pid)
tty=$(ps -o tty= -p $pid)
done
TTY=/dev/$tty
# ps -p $pid |grep nonono
dialog --defaultno \
--title "Attachment Missing" \
--yesno "There is no attachment found.\nDo you still want to send anyway?" 0 0 < $TTY > $TTY
}
# FINAL DECISION:
if multipart || ! word-attach || ask ; then
exit_status=$?
# send email
"$@" < "$t"
else
echo "No file was attached but a search of the message text suggests there should be one."
exit_status=1
fi
rm -f "$t"
exit $exit_status
Here this line acts like lbdb-fetchaddr
and collects recipients' email addresses.
contact_query -f to,cc,bcc -t "$t"
Time to enable these two scripts in .muttrc
set query_command = "~/.mutt/contact_query"
set sendmail = "~/.mutt/do_sendmail msmtp"
Now in mutt, any recipients in an outgoing message will be saved in
~/.mutt/contacts.yml
, which is friendlier than lbdb’s binary database format.
And pressing Ctrl-t
will complete email addresses according to saved
addresses.
Update: now I split query script in to smaller ones in a folder named ~/.mutt/contacts
and use run-parts
to call them like advised here. You can find updated code in my github dotfiles repository.