#!/usr/bin/perl -w # # notmuch-mutt - notmuch (of a) helper for Mutt # # Copyright: © 2011-2012 Stefano Zacchiroli # License: GNU General Public License (GPL), version 3 or above # # See the bottom of this file for more documentation. # A manpage can be obtained by running "pod2man notmuch-mutt > notmuch-mutt.1" use strict; use warnings; use File::Path; use Getopt::Long qw(:config no_getopt_compat); use Mail::Header; use Mail::Box::Maildir; use Pod::Usage; use String::ShellQuote; use Term::ReadLine; use Digest::SHA; my $xdg_cache_dir = "$ENV{HOME}/.cache"; $xdg_cache_dir = $ENV{XDG_CACHE_HOME} if $ENV{XDG_CACHE_HOME}; my $cache_dir = "$xdg_cache_dir/notmuch/mutt"; # create an empty maildir (if missing) or empty an existing maildir" sub empty_maildir($) { my ($maildir) = (@_); rmtree($maildir) if (-d $maildir); my $folder = new Mail::Box::Maildir(folder => $maildir, create => 1); $folder->close(); } # search($maildir, $remove_dups, $query) # search mails according to $query with notmuch; store results in $maildir sub search($$$) { my ($maildir, $remove_dups, $query) = @_; my $dup_option = ""; $query = shell_quote($query); if ($remove_dups) { $dup_option = "--duplicate=1"; } empty_maildir($maildir); system("notmuch search --output=files $dup_option $query" . " | sed -e 's: :\\\\ :g'" . " | xargs --no-run-if-empty ln -s -t $maildir/cur/"); } sub prompt($$) { my ($text, $default) = @_; my $query = ""; my $term = Term::ReadLine->new( "notmuch-mutt" ); my $histfile = "$cache_dir/history"; $term->ornaments( 0 ); $term->unbind_key( ord( "\t" ) ); $term->MinLine( 3 ); $histfile = $ENV{MUTT_NOTMUCH_HISTFILE} if $ENV{MUTT_NOTMUCH_HISTFILE}; $term->ReadHistory($histfile) if (-r $histfile); while (1) { chomp($query = $term->readline($text, $default)); if ($query eq "?") { system("man", "notmuch-search-terms"); } else { $term->WriteHistory($histfile); return $query; } } } sub get_message_id() { my $mid = undef; my $in_header = 1; my @raw_header = (); my $sha = Digest::SHA->new(1); # SHA1 hash of the whole mail while () { # compute SHA1 as we go push(@raw_header, $_) if $in_header; # cache header lines if ($_ =~ /^$/) { # end of header, parse it and look for Message-ID $in_header = 0; my $head = Mail::Header->new(\@raw_header); $mid = $head->get("message-id") or undef; if ($mid) { $mid =~ /^<(.*)>$/; # get message-id value $mid = $1; last; # stop hashing } } $sha->add($_); # update hash } # If no message-id was found, generate one-id in the same way that # notmuch would do. # See: http://git.notmuchmail.org/git/notmuch/blob/HEAD:/lib/sha1.c $mid ||= "notmuch-sha1-".$sha->hexdigest; return $mid; } sub search_action($$$@) { my ($interactive, $results_dir, $remove_dups, @params) = @_; if (! $interactive) { search($results_dir, $remove_dups, join(' ', @params)); } else { my $query = prompt("search ('?' for man): ", join(' ', @params)); if ($query ne "") { search($results_dir, $remove_dups, $query); } } } sub thread_action($$@) { my ($results_dir, $remove_dups, @params) = @_; my $mid = get_message_id(); if (! defined $mid) { empty_maildir($results_dir); die "notmuch-mutt: cannot find Message-Id, abort.\n"; } my $search_cmd = 'notmuch search --output=threads ' . shell_quote("id:$mid"); my $tid = `$search_cmd`; # get thread id chomp($tid); search($results_dir, $remove_dups, $tid); } sub tag_action(@) { my $mid = get_message_id(); defined $mid or die "notmuch-mutt: cannot find Message-Id, abort.\n"; system("notmuch", "tag", @_, "--", "id:$mid"); } sub die_usage() { my %podflags = ( "verbose" => 1, "exitval" => 2 ); pod2usage(%podflags); } sub main() { mkpath($cache_dir) unless (-d $cache_dir); my $results_dir = "$cache_dir/results"; my $interactive = 0; my $help_needed = 0; my $remove_dups = 0; my $getopt = GetOptions( "h|help" => \$help_needed, "o|output-dir=s" => \$results_dir, "p|prompt" => \$interactive, "r|remove-dups" => \$remove_dups); if (! $getopt || $#ARGV < 0) { die_usage() }; my ($action, @params) = ($ARGV[0], @ARGV[1..$#ARGV]); foreach my $param (@params) { $param =~ s/folder:=/folder:/g; } if ($help_needed) { die_usage(); } elsif ($action eq "search" && $#ARGV == 0 && ! $interactive) { print STDERR "Error: no search term provided\n\n"; die_usage(); } elsif ($action eq "search") { search_action($interactive, $results_dir, $remove_dups, @params); } elsif ($action eq "thread") { thread_action($results_dir, $remove_dups, @params); } elsif ($action eq "tag") { tag_action(@params); } else { die_usage(); } } main(); __END__ =head1 NAME notmuch-mutt - notmuch (of a) helper for Mutt =head1 SYNOPSIS =over =item B [I