]> git.notmuchmail.org Git - notmuch/blob - contrib/notmuch-mutt/notmuch-mutt
notmuch-mutt: lookup notmuch-search-terms(7) when asked for help
[notmuch] / contrib / notmuch-mutt / notmuch-mutt
1 #!/usr/bin/perl -w
2 #
3 # notmuch-mutt - notmuch (of a) helper for Mutt
4 #
5 # Copyright: © 2011-2012 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::Internet;
17 use Mail::Box::Maildir;
18 use Pod::Usage;
19 use String::ShellQuote;
20 use Term::ReadLine;
21
22
23 my $xdg_cache_dir = "$ENV{HOME}/.cache";
24 $xdg_cache_dir = $ENV{XDG_CACHE_HOME} if $ENV{XDG_CACHE_HOME};
25 my $cache_dir = "$xdg_cache_dir/notmuch/mutt";
26
27
28 # create an empty maildir (if missing) or empty an existing maildir"
29 sub empty_maildir($) {
30     my ($maildir) = (@_);
31     rmtree($maildir) if (-d $maildir);
32     my $folder = new Mail::Box::Maildir(folder => $maildir,
33                                         create => 1);
34     $folder->close();
35 }
36
37 # search($maildir, $query)
38 # search mails according to $query with notmuch; store results in $maildir
39 sub search($$) {
40     my ($maildir, $query) = @_;
41     $query = shell_quote($query);
42
43     empty_maildir($maildir);
44     system("notmuch search --output=files $query"
45            . " | sed -e 's: :\\\\ :g'"
46            . " | xargs --no-run-if-empty ln -s -t $maildir/cur/");
47 }
48
49 sub prompt($$) {
50     my ($text, $default) = @_;
51     my $query = "";
52     my $term = Term::ReadLine->new( "notmuch-mutt" );
53     my $histfile = "$cache_dir/history";
54
55     $term->ornaments( 0 );
56     $term->unbind_key( ord( "\t" ) );
57     $term->MinLine( 3 );
58     $histfile = $ENV{MUTT_NOTMUCH_HISTFILE} if $ENV{MUTT_NOTMUCH_HISTFILE};
59     $term->ReadHistory($histfile) if (-r $histfile);
60     while (1) {
61         chomp($query = $term->readline($text, $default));
62         if ($query eq "?") {
63             system("man", "notmuch-search-terms");
64         } else {
65             $term->WriteHistory($histfile);
66             return $query;
67         }
68     }
69 }
70
71 sub get_message_id() {
72     my $mail = Mail::Internet->new(\*STDIN);
73     $mail->head->get("message-id") =~ /^<(.*)>$/;       # get message-id
74     return $1;
75 }
76
77 sub search_action($$@) {
78     my ($interactive, $results_dir, @params) = @_;
79
80     if (! $interactive) {
81         search($results_dir, join(' ', @params));
82     } else {
83         my $query = prompt("search ('?' for man): ", join(' ', @params));
84         if ($query ne "") {
85             search($results_dir,$query);
86         }
87     }
88 }
89
90 sub thread_action(@) {
91     my ($results_dir, @params) = @_;
92
93     my $mid = get_message_id();
94     my $search_cmd = 'notmuch search --output=threads ' . shell_quote("id:$mid");
95     my $tid = `$search_cmd`;    # get thread id
96     chomp($tid);
97
98     search($results_dir, $tid);
99 }
100
101 sub tag_action(@) {
102     my $mid = get_message_id();
103
104     system("notmuch tag "
105            . shell_quote(join(' ', @_))
106            . " id:$mid");
107 }
108
109 sub die_usage() {
110     my %podflags = ( "verbose" => 1,
111                     "exitval" => 2 );
112     pod2usage(%podflags);
113 }
114
115 sub main() {
116     mkpath($cache_dir) unless (-d $cache_dir);
117
118     my $results_dir = "$cache_dir/results";
119     my $interactive = 0;
120     my $help_needed = 0;
121
122     my $getopt = GetOptions(
123         "h|help" => \$help_needed,
124         "o|output-dir=s" => \$results_dir,
125         "p|prompt" => \$interactive);
126     if (! $getopt || $#ARGV < 0) { die_usage() };
127     my ($action, @params) = ($ARGV[0], @ARGV[1..$#ARGV]);
128
129     foreach my $param (@params) {
130       $param =~ s/folder:=/folder:/g;
131     }
132
133     if ($help_needed) {
134         die_usage();
135     } elsif ($action eq "search" && $#ARGV == 0 && ! $interactive) {
136         print STDERR "Error: no search term provided\n\n";
137         die_usage();
138     } elsif ($action eq "search") {
139         search_action($interactive, $results_dir, @params);
140     } elsif ($action eq "thread") {
141         thread_action($results_dir, @params);
142     } elsif ($action eq "tag") {
143         tag_action(@params);
144     } else {
145         die_usage();
146     }
147 }
148
149 main();
150
151 __END__
152
153 =head1 NAME
154
155 notmuch-mutt - notmuch (of a) helper for Mutt
156
157 =head1 SYNOPSIS
158
159 =over
160
161 =item B<notmuch-mutt> [I<OPTION>]... search [I<SEARCH-TERM>]...
162
163 =item B<notmuch-mutt> [I<OPTION>]... thread < I<MAIL>
164
165 =item B<notmuch-mutt> [I<OPTION>]... tag [I<TAGS>]... < I<MAIL>
166
167 =back
168
169 =head1 DESCRIPTION
170
171 notmuch-mutt is a frontend to the notmuch mail indexer capable of populating
172 a maildir with search results.
173
174 =head1 OPTIONS
175
176 =over 4
177
178 =item -o DIR
179
180 =item --output-dir DIR
181
182 Store search results as (symlink) messages under maildir DIR. Beware: DIR will
183 be overwritten. (Default: F<~/.cache/notmuch/mutt/results/>)
184
185 =item -p
186
187 =item --prompt
188
189 Instead of using command line search terms, prompt the user for them (only for
190 "search").
191
192 =item -h
193
194 =item --help
195
196 Show usage information and exit.
197
198 =back
199
200 =head1 INTEGRATION WITH MUTT
201
202 notmuch-mutt can be used to integrate notmuch with the Mutt mail user agent
203 (unsurprisingly, given the name). To that end, you should define macros like
204 the following in your Mutt configuration (usually one of: F<~/.muttrc>,
205 F</etc/Muttrc>, or a configuration snippet under F</etc/Muttrc.d/>):
206
207     macro index <F8> \
208           "<enter-command>unset wait_key<enter><shell-escape>notmuch-mutt --prompt search<enter><change-folder-readonly>~/.cache/notmuch/mutt/results<enter>" \
209           "notmuch: search mail"
210     macro index <F9> \
211           "<enter-command>unset wait_key<enter><pipe-message>notmuch-mutt thread<enter><change-folder-readonly>~/.cache/notmuch/mutt/results<enter><enter-command>set wait_key<enter>" \
212           "notmuch: reconstruct thread"
213     macro index <F6> \
214           "<enter-command>unset wait_key<enter><pipe-message>notmuch-mutt tag -inbox<enter>" \
215           "notmuch: remove message from inbox"
216
217 The first macro (activated by <F8>) prompts the user for notmuch search terms
218 and then jump to a temporary maildir showing search results. The second macro
219 (activated by <F9>) reconstructs the thread corresponding to the current mail
220 and show it as search results. The third macro (activated by <F6>) removes the
221 tag C<inbox> from the current message; by changing C<-inbox> this macro may be
222 customised to add or remove tags appropriate to the users notmuch work-flow.
223
224 To keep notmuch index current you should then periodically run C<notmuch
225 new>. Depending on your local mail setup, you might want to do that via cron,
226 as a hook triggered by mail retrieval, etc.
227
228 =head1 SEE ALSO
229
230 mutt(1), notmuch(1)
231
232 =head1 AUTHOR
233
234 Copyright: (C) 2011-2012 Stefano Zacchiroli <zack@upsilon.cc>
235
236 License: GNU General Public License (GPL), version 3 or higher
237
238 =cut