dotfiles

configuration files for shell, text editor, graphical environment, etc.
git clone git://src.adamsgaard.dk/dotfiles # fast
git clone https://src.adamsgaard.dk/dotfiles.git # slow
Log | Files | Refs | README | LICENSE Back to index

notmuch-mutt (8396B)


      1 #!/usr/bin/env perl
      2 #
      3 # notmuch-mutt - notmuch (of a) helper for Mutt
      4 #
      5 # Copyright: © 2011-2015 Stefano Zacchiroli <zack@upsilon.cc>
      6 # License: GNU General Public License (GPL), version 3 or above
      7 #
      8 # See the bottom of this file for more documentation.
      9 # A manpage can be obtained by running "pod2man notmuch-mutt > notmuch-mutt.1"
     10 
     11 use strict;
     12 use warnings;
     13 
     14 use File::Path;
     15 use Getopt::Long qw(:config no_getopt_compat);
     16 use Mail::Header;
     17 use Mail::Box::Maildir;
     18 use Pod::Usage;
     19 use String::ShellQuote;
     20 use Term::ReadLine;
     21 use Digest::SHA;
     22 
     23 
     24 my $xdg_cache_dir = "$ENV{HOME}/.cache";
     25 $xdg_cache_dir = $ENV{XDG_CACHE_HOME} if $ENV{XDG_CACHE_HOME};
     26 my $cache_dir = "$xdg_cache_dir/notmuch/mutt";
     27 
     28 
     29 # create an empty maildir (if missing) or empty an existing maildir"
     30 sub empty_maildir($) {
     31     my ($maildir) = (@_);
     32     rmtree($maildir) if (-d $maildir);
     33     my $folder = new Mail::Box::Maildir(folder => $maildir,
     34 					create => 1);
     35     $folder->close();
     36 }
     37 
     38 # search($maildir, $remove_dups, $query)
     39 # search mails according to $query with notmuch; store results in $maildir
     40 sub search($$$) {
     41     my ($maildir, $remove_dups, $query) = @_;
     42     my $dup_option = "";
     43 
     44     $query = shell_quote($query);
     45 
     46     if ($remove_dups) {
     47       $dup_option = "--duplicate=1";
     48     }
     49 
     50     empty_maildir($maildir);
     51     #system("notmuch search --output=files $dup_option $query"
     52     #. " | sed -e 's: :\\\\ :g'"
     53     #. " | xargs -r -I searchoutput ln -s searchoutput $maildir/cur/");
     54     system("notmuch search --output=files $dup_option $query"
     55 	   . " | sed -e 's: :\\\\ :g'"
     56 	   . " | xargs -I searchoutput ln -s searchoutput $maildir/cur/");
     57 }
     58 
     59 sub prompt($$) {
     60     my ($text, $default) = @_;
     61     my $query = "";
     62     my $term = Term::ReadLine->new( "notmuch-mutt" );
     63     my $histfile = "$cache_dir/history";
     64 
     65     $term->ornaments( 0 );
     66     $term->unbind_key( ord( "\t" ) );
     67     $term->MinLine( 3 );
     68     $histfile = $ENV{MUTT_NOTMUCH_HISTFILE} if $ENV{MUTT_NOTMUCH_HISTFILE};
     69     $term->ReadHistory($histfile) if (-r $histfile);
     70     while (1) {
     71 	chomp($query = $term->readline($text, $default));
     72 	if ($query eq "?") {
     73 	    system("man", "notmuch-search-terms");
     74 	} else {
     75 	    $term->WriteHistory($histfile);
     76 	    return $query;
     77 	}
     78     }
     79 }
     80 
     81 sub get_message_id() {
     82     my $mid = undef;
     83     my @headers = ();
     84 
     85     while (<STDIN>) {  # collect header lines in @headers
     86 	push(@headers, $_);
     87 	last if $_ =~ /^$/;
     88     }
     89     my $head = Mail::Header->new(\@headers);
     90     $mid = $head->get("message-id") or undef;
     91 
     92     if ($mid) {  # Message-ID header found
     93 	$mid =~ /^<(.*)>$/;  # extract message id
     94 	$mid = $1;
     95     } else {  # Message-ID header not found, synthesize a message id
     96 	      # based on SHA1, as notmuch would do.  See:
     97 	      # https://git.notmuchmail.org/git/notmuch/blob/HEAD:/lib/sha1.c
     98 	my $sha = Digest::SHA->new(1);
     99 	$sha->add($_) foreach(@headers);
    100 	$sha->addfile(\*STDIN);
    101 	$mid = 'notmuch-sha1-' . $sha->hexdigest;
    102     }
    103 
    104     return $mid;
    105 }
    106 
    107 sub search_action($$$@) {
    108     my ($interactive, $results_dir, $remove_dups, @params) = @_;
    109 
    110     if (! $interactive) {
    111 	search($results_dir, $remove_dups, join(' ', @params));
    112     } else {
    113 	my $query = prompt("search ('?' for man): ", join(' ', @params));
    114 	if ($query ne "") {
    115 	    search($results_dir, $remove_dups, $query);
    116 	}
    117     }
    118 }
    119 
    120 sub thread_action($$@) {
    121     my ($results_dir, $remove_dups, @params) = @_;
    122 
    123     my $mid = get_message_id();
    124     if (! defined $mid) {
    125 	empty_maildir($results_dir);
    126 	die "notmuch-mutt: cannot find Message-Id, abort.\n";
    127     }
    128     my $search_cmd = 'notmuch search --output=threads ' . shell_quote("id:$mid");
    129     my $tid = `$search_cmd`;	# get thread id
    130     chomp($tid);
    131 
    132     search($results_dir, $remove_dups, $tid);
    133 }
    134 
    135 sub tag_action(@) {
    136     my $mid = get_message_id();
    137     defined $mid or die "notmuch-mutt: cannot find Message-Id, abort.\n";
    138 
    139     system("notmuch", "tag", @_, "--", "id:$mid");
    140 }
    141 
    142 sub die_usage() {
    143     my %podflags = ( "verbose" => 1,
    144 		    "exitval" => 2 );
    145     pod2usage(%podflags);
    146 }
    147 
    148 sub main() {
    149     mkpath($cache_dir) unless (-d $cache_dir);
    150 
    151     my $results_dir = "$cache_dir/results";
    152     my $interactive = 0;
    153     my $help_needed = 0;
    154     my $remove_dups = 0;
    155 
    156     my $getopt = GetOptions(
    157 	"h|help" => \$help_needed,
    158 	"o|output-dir=s" => \$results_dir,
    159 	"p|prompt" => \$interactive,
    160 	"r|remove-dups" => \$remove_dups);
    161     if (! $getopt || $#ARGV < 0) { die_usage() };
    162     my ($action, @params) = ($ARGV[0], @ARGV[1..$#ARGV]);
    163 
    164     foreach my $param (@params) {
    165       $param =~ s/folder:=/folder:/g;
    166     }
    167 
    168     if ($help_needed) {
    169 	die_usage();
    170     } elsif ($action eq "search" && $#ARGV == 0 && ! $interactive) {
    171 	print STDERR "Error: no search term provided\n\n";
    172 	die_usage();
    173     } elsif ($action eq "search") {
    174 	search_action($interactive, $results_dir, $remove_dups, @params);
    175     } elsif ($action eq "thread") {
    176 	thread_action($results_dir, $remove_dups, @params);
    177     } elsif ($action eq "tag") {
    178 	tag_action(@params);
    179     } else {
    180 	die_usage();
    181     }
    182 }
    183 
    184 main();
    185 
    186 __END__
    187 
    188 =head1 NAME
    189 
    190 notmuch-mutt - notmuch (of a) helper for Mutt
    191 
    192 =head1 SYNOPSIS
    193 
    194 =over
    195 
    196 =item B<notmuch-mutt> [I<OPTION>]... search [I<SEARCH-TERM>]...
    197 
    198 =item B<notmuch-mutt> [I<OPTION>]... thread < I<MAIL>
    199 
    200 =item B<notmuch-mutt> [I<OPTION>]... tag [I<TAGS>]... < I<MAIL>
    201 
    202 =back
    203 
    204 =head1 DESCRIPTION
    205 
    206 notmuch-mutt is a frontend to the notmuch mail indexer capable of populating
    207 a maildir with search results.
    208 
    209 =head1 OPTIONS
    210 
    211 =over 4
    212 
    213 =item -o DIR
    214 
    215 =item --output-dir DIR
    216 
    217 Store search results as (symlink) messages under maildir DIR. Beware: DIR will
    218 be overwritten. (Default: F<~/.cache/notmuch/mutt/results/>)
    219 
    220 =item -p
    221 
    222 =item --prompt
    223 
    224 Instead of using command line search terms, prompt the user for them (only for
    225 "search").
    226 
    227 =item -r
    228 
    229 =item --remove-dups
    230 
    231 Remove emails with duplicate message-ids from search results.  (Passes
    232 --duplicate=1 to notmuch search command.)  Note this can hide search
    233 results if an email accidentally or maliciously uses the same message-id
    234 as a different email.
    235 
    236 =item -h
    237 
    238 =item --help
    239 
    240 Show usage information and exit.
    241 
    242 =back
    243 
    244 =head1 INTEGRATION WITH MUTT
    245 
    246 notmuch-mutt can be used to integrate notmuch with the Mutt mail user agent
    247 (unsurprisingly, given the name). To that end, you should define macros like
    248 the following in your Mutt configuration (usually one of: F<~/.muttrc>,
    249 F</etc/Muttrc>, or a configuration snippet under F</etc/Muttrc.d/>):
    250 
    251     macro index <F8> \
    252     "<enter-command>set my_old_pipe_decode=\$pipe_decode my_old_wait_key=\$wait_key nopipe_decode nowait_key<enter>\
    253     <shell-escape>notmuch-mutt -r --prompt search<enter>\
    254     <change-folder-readonly>`echo ${XDG_CACHE_HOME:-$HOME/.cache}/notmuch/mutt/results`<enter>\
    255     <enter-command>set pipe_decode=\$my_old_pipe_decode wait_key=\$my_old_wait_key<enter>" \
    256           "notmuch: search mail"
    257 
    258     macro index <F9> \
    259     "<enter-command>set my_old_pipe_decode=\$pipe_decode my_old_wait_key=\$wait_key nopipe_decode nowait_key<enter>\
    260     <pipe-message>notmuch-mutt -r thread<enter>\
    261     <change-folder-readonly>`echo ${XDG_CACHE_HOME:-$HOME/.cache}/notmuch/mutt/results`<enter>\
    262     <enter-command>set pipe_decode=\$my_old_pipe_decode wait_key=\$my_old_wait_key<enter>" \
    263           "notmuch: reconstruct thread"
    264 
    265     macro index <F6> \
    266     "<enter-command>set my_old_pipe_decode=\$pipe_decode my_old_wait_key=\$wait_key nopipe_decode nowait_key<enter>\
    267     <pipe-message>notmuch-mutt tag -- -inbox<enter>\
    268     <enter-command>set pipe_decode=\$my_old_pipe_decode wait_key=\$my_old_wait_key<enter>" \
    269           "notmuch: remove message from inbox"
    270 
    271 The first macro (activated by <F8>) prompts the user for notmuch search terms
    272 and then jump to a temporary maildir showing search results. The second macro
    273 (activated by <F9>) reconstructs the thread corresponding to the current mail
    274 and show it as search results. The third macro (activated by <F6>) removes the
    275 tag C<inbox> from the current message; by changing C<-inbox> this macro may be
    276 customised to add or remove tags appropriate to the users notmuch work-flow.
    277 
    278 To keep notmuch index current you should then periodically run C<notmuch
    279 new>. Depending on your local mail setup, you might want to do that via cron,
    280 as a hook triggered by mail retrieval, etc.
    281 
    282 =head1 SEE ALSO
    283 
    284 mutt(1), notmuch(1)
    285 
    286 =head1 AUTHOR
    287 
    288 Copyright: (C) 2011-2012 Stefano Zacchiroli <zack@upsilon.cc>
    289 
    290 License: GNU General Public License (GPL), version 3 or higher
    291 
    292 =cut