#!/bin/sh # -*- cperl -*- case $* in (''|--*) exec perl -x "$0" "$@" ;; (???*) ;; (*) exit 0 ;; esac grep -aiF "$*" "${XDG_CONFIG_HOME:-$HOME/.config}/nottoomuch/addresses.active" case $? in 0|1) exit 0; esac exit $? # $ nottoomuch-addresses.sh $ # # Created: Thu 27 Oct 2011 17:38:46 EEST too # Last modified: Sat 29 Mar 2014 17:12:14 +0200 too # Add this to your notmuch elisp configuration file: # # (require 'notmuch-address) # (setq notmuch-address-command "/path/to/nottoomuch-addresses.sh") # (notmuch-address-message-insinuate) # Documentation at the end. Encoding: utf-8. #!perl # line 24 # - 24 -^ # HISTORY # # Version 2.2 2014-03-29 15:12:14 UTC # * In case there is both {phrase} and (comment) in an email address, # append comment to the phrase. This will make more duplicates to be # removed. Now there can be: # # "phrase" # "phrase (comment)" # (comment) # * In case email address is in form "someuser@somehost" # i.e. the phrase is exactly the same as
, phrase is dropped. # # Version 2.1 2012-02-22 14:58:58 UTC # * Fixed a bug where decoding matching but unknown or malformed =?...?=- # encoded parts in email addresses lead to infinite loop. # # Version 2.0 2012-01-14 03:45:00 UTC # * Added regexp-based ignores using /regexp/[i] syntax in ignore file. # * Changed addresses file header to v4; 'addresses' file now contains all # found addresses plus some metainformation added at the end of the file. # Filtered (by ignores) address list is now in new 'addresses.active' # file and the fgrep code at the beginning now uses this "active" file. # Addresses file with header v2 and v3 are supported for reading. # * Encoded address content is now recursively decoded. # # Version 1.6 2011-12-29 06:42:42 UTC # * Fixed 'encoded-text' recognition and concatenations, and underscore # to space replacements. Now quite RFC 2047 "compliant". # # Version 1.5 2011-12-22 20:20:32 UTC # * Changed search to exit with zero value (also) if no match found. # * Changed addresses file header (v3) to use \t as separator. Addresses # file containing previous version header (v2) can also be read. # * Removed outdated information about sorting in ASCII order. # # Version 1.4 2011-12-14 19:24:28 UTC # * Changed to run notmuch search --sort=newest-first --output=files ... # (instead of notmuch show ...) and read headers from files internally. # * Fixed away joining uninitialized $phrase value to address line. # # Version 1.3 2011-12-12 15:41:05 UTC # * Changed to store/show addresses in 'newest first' order. # * Changed addresses file header to force address file rebuild. # # Version 1.2 2011-12-06 18:00:00 UTC # * Changed search work case-insensitively -- grep(1) does it locale-aware. # * Changed this program execute from /bin/sh (wrapper). # # Version 1.1 2011-12-02 17:11:33 UTC # * Removed Naïve assumption that no-one runs update on 'dumb' terminal. # * Check address database file first line whether it is known to us. # # Thanks to Bart Bunting for providing a good bug report. # # Version 1.0 2011-11-30 20:56:10 UTC # * Initial release. use 5.8.1; use strict; use warnings; use Encode qw/encode_utf8 find_encoding/; use MIME::Base64 'decode_base64'; use MIME::QuotedPrint 'decode_qp'; no encoding; my $configdir = ($ENV{XDG_CONFIG_HOME}||$ENV{HOME}.'/.config').'/nottoomuch'; my $adbpath = $configdir . '/addresses'; my $ignpath = $configdir . '/addresses.ignore'; my $actpath = $configdir . '/addresses.active'; unless (@ARGV) { require Pod::Usage; Pod::Usage::pod2usage( -verbose => 0, -exitval => 0 ); exit 1; } if ($ARGV[0] eq '--help') { $SIG{__DIE__} = sub { $SIG{__DIE__} = 'DEFAULT'; require Pod::Usage; Pod::Usage::pod2usage( -verbose => 2, -exitval => 0, -noperldoc => 1 ); exit 1; }; require Pod::Perldoc; $SIG{__DIE__} = 'DEFAULT'; # in case PAGER is not set, perldoc runs /usr/bin/perl -isr ... if ( ($ENV{PAGER} || '') eq 'less') { $ENV{LESS} .= 'R' if ($ENV{LESS} || '') !~ /[rR]/; } @ARGV = ( $0 ); exit ( Pod::Perldoc->run() ); } my @list; if ($ARGV[0] eq '--update') { sub mkdirs($); sub mkdirs($) { die "'$_[0]': not a (writable) directory\n" if -e $_[0]; return if mkdir $_[0]; # no mode: 0777 & ~umask used local $_ = $_[0]; mkdirs $_ if s|/?[^/]+$|| and $_; mkdir $_[0] or die "Cannot create '$_[0]': $!\n"; } mkdirs $configdir unless -d $configdir; unlink $adbpath if defined $ARGV[1] and $ARGV[1] eq '--rebuild'; my ($sstr, $acount) = (0, 0); if (-s $adbpath) { die "Cannot open '$adbpath': $!\n" unless open I, '<', $adbpath; sysread I, $_, 18; # new header: "v4/dd/dd/dd/dd/dd\n" where / == '\t' (but match also v2) if (/^v[234]\s(\d\d)\s(\d\d)\s(\d\d)\s(\d\d)\s(\d\d)\n$/) { $sstr = "$1$2$3$4$5" - 86400 * 7; # one week extra to (re)look. $sstr = 0 if $sstr < 0; } close I if $sstr == 0; } if ($sstr > 0) { print "Updating '$adbpath', since $sstr.\n"; $sstr .= '..'; } else { print "Creating '$adbpath'. This may take some time...\n"; $sstr = '*'; } my (%ign_hash, @ign_relist); if (-f $ignpath) { die "Cannot open '$ignpath': $!\n" unless open J, '<', $ignpath; while () { next if /^\s*#/; if (m|^/(.*)/(\w*)\s*$|) { if ($2 eq 'i') { push @ign_relist, qr/$1/i; } else { push @ign_relist, qr/$1/; } } else { s/\s+$/\n/; $ign_hash{$_} = 1; } } close J; } my $sometime = time; die "Cannot open '$adbpath.new': $!\n" unless open O, '>', $adbpath.'.new'; die "Cannot open '$actpath.new': $!\n" unless open A, '>', $actpath.'.new'; $_ = $sometime; s/(..)\B/$1\t/g; # FYI: s/..\B\K/\t/g requires perl 5.10. print O "v4\t$_\n"; # The following code block is from Email::Address, almost verbatim. # The reasons to snip code I instead of just 'use Email::Address' are: # 1) Some systems ship Mail::Address instead of Email::Address # 2) Every user doesn't have ability to install Email::Address # --8<----8<----8<----8<----8<----8<----8<----8<----8<----8<----8<-- ## no critic RequireUseWarnings # support pre-5.6 #$VERSION = '1.889'; my $COMMENT_NEST_LEVEL = 2; my $CTL = q{\x00-\x1F\x7F}; my $special = q{()<>\\[\\]:;@\\\\,."}; my $text = qr/[^\x0A\x0D]/; my $quoted_pair = qr/\\$text/; my $ctext = qr/(?>[^()\\]+)/; my ($ccontent, $comment) = (q{})x2; for (1 .. $COMMENT_NEST_LEVEL) { $ccontent = qr/$ctext|$quoted_pair|$comment/; $comment = qr/\s*\((?:\s*$ccontent)*\s*\)\s*/; } my $cfws = qr/$comment|\s+/; my $atext = qq/[^$CTL$special\\s]/; my $atom = qr/$cfws*$atext+$cfws*/; my $dot_atom_text = qr/$atext+(?:\.$atext+)*/; my $dot_atom = qr/$cfws*$dot_atom_text$cfws*/; my $qtext = qr/[^\\"]/; my $qcontent = qr/$qtext|$quoted_pair/; my $quoted_string = qr/$cfws*"$qcontent+"$cfws*/; my $word = qr/$atom|$quoted_string/; # XXX: This ($phrase) used to just be: my $phrase = qr/$word+/; It was changed # to resolve bug 22991, creating a significant slowdown. Given current speed # problems. Once 16320 is resolved, this section should be dealt with. # -- rjbs, 2006-11-11 #my $obs_phrase = qr/$word(?:$word|\.|$cfws)*/; # XXX: ...and the above solution caused endless problems (never returned) when # examining this address, now in a test: # admin+=E6=96=B0=E5=8A=A0=E5=9D=A1_Weblog-- ATAT --test.socialtext.com # So we disallow the hateful CFWS in this context for now. Of modern mail # agents, only Apple Web Mail 2.0 is known to produce obs-phrase. # -- rjbs, 2006-11-19 my $simple_word = qr/$atom|\.|\s*"$qcontent+"\s*/; my $obs_phrase = qr/$simple_word+/; my $phrase = qr/$obs_phrase|(?:$word+)/; my $local_part = qr/$dot_atom|$quoted_string/; my $dtext = qr/[^\[\]\\]/; my $dcontent = qr/$dtext|$quoted_pair/; my $domain_literal = qr/$cfws*\[(?:\s*$dcontent)*\s*\]$cfws*/; my $domain = qr/$dot_atom|$domain_literal/; my $display_name = $phrase; my $addr_spec = qr/$local_part\@$domain/; my $angle_addr = qr/$cfws*<$addr_spec>$cfws*/; my $name_addr = qr/$display_name?$angle_addr/; my $mailbox = qr/(?:$name_addr|$addr_spec)$comment*/; # --8<----8<----8<----8<----8<----8<----8<----8<----8<----8<----8<-- # In this particular purpose the cache code used in... my %seen; # ...Email::Address is "replaced" by %seen & %hash. my %hash; my $ptime = $sometime + 5; my $addrcount = 0; $| = 1; open P, '-|', qw/notmuch search --sort=newest-first --output=files/, $sstr; while (

) { chomp; open M, '<', $_ or next; while () { last if /^\s*$/; next unless s/^(From|To|Cc|Bcc):\s+//i; s/\s+$//; my @a = ( $_ ); while () { # XXX leaks to body in case empty line is found in this loop... # XXX Note that older code leaked to mail body always... if (s/^\s+// or s/^(From|To|Cc|Bcc):\s+/,/i) { s/\s+$//; push @a, $_; next; } last; } $_ = join ' ', @a; if (time > $ptime) { my $c = qw(/ - \ |)[int ($ptime / 5) % 4]; print $c, ' active addresses gathered: ', $addrcount, "\r"; $ptime += 5; } # The parse function from Email::Address heavily modified # to fit ok in this particular purpose. New bugs are mine! # --8<----8<----8<----8<----8<----8<----8<----8<----8<----8<----8<-- s/[ \t]+/ /g; s/\?= =\?/\?==\?/g; my (@mailboxes) = (/$mailbox/go); L: foreach (@mailboxes) { next if $seen{$_}; $seen{$_} = 1; my @comments = /($comment)/go; s/$comment//go if @comments; my ($user, $host); ($user, $host) = ($1, $2) if s/<($local_part)\@($domain)>//o; if (! defined($user) || ! defined($host)) { s/($local_part)\@($domain)//o; ($user, $host) = ($1, $2); } sub decode_substring ($) { my $t = lc $2; my $s; if ($t eq 'b') { $s = decode_base64($3); } elsif ($t eq 'q') { $s = decode_qp($3); } else { $_[0] = 0; return "=?$1?$2?$3?="; } $s =~ tr/_/ /; return $s if lc $1 eq 'utf-8'; my $o = find_encoding($1); $_[0] = 0, return "=?$1?$2?$3?=" unless ref $o; return encode_utf8($o->decode($s)); } sub decode_data () { my $loopmax = 5; while ( s{ =\?([^?]+)\?(\w)\?(.*?)\?= } { decode_substring($loopmax) }gex ) { last if --$loopmax <= 0; }; } my @phrase = /($display_name)/o; decode_data foreach (@phrase); for ( @phrase, $host, $user, @comments ) { next unless defined $_; s/^[\s'"]+//; ## additions 20111123 too s/[\s'"]+$//; ## additions 20111123 too $_ = undef unless length $_; } # here we want to have email address always // 20111123 too next unless defined $user and defined $host; my $userhost = lc "<$user\@$host>"; #my $userhost = "<$user\@$host>"; @comments = grep { defined or return 0; decode_data; 1; } @comments; # "trim" phrase, if equals to user@host after trimming, drop it. if (defined $phrase[0]) { #$phrase[0] =~s/\A"(.+)"\z/$1/; $phrase[0] =~ tr/\\//d; ## 20111124 too $phrase[0] =~ s/\"/\\"/g; @phrase = () if lc "<$phrase[0]>" eq $userhost; } # In case we would have {phrase} (comment), # make that "{phrase} (comment)" ... if (defined $phrase[0]) { if (@comments) { $phrase[0] = qq/"$phrase[0] / . join(' ', @comments) . '"'; @comments = (); } else { $phrase[0] = qq/"$phrase[0]"/; } } else { @phrase = (); } $_ = join(' ', @phrase, $userhost, @comments) . "\n"; next if defined $hash{$_}; print O $_; $hash{$_} = 1; next if defined $ign_hash{$_}; foreach my $re (@ign_relist) { next L if $_ =~ $re; } print A $_; $addrcount++; } # --8<----8<----8<----8<----8<----8<----8<----8<----8<----8<----8<-- } close M; } undef %seen; close P; my $oldaddrcount = 0; if ($sstr ne '*') { L: while () { last if /^---/; next if defined $hash{$_}; print O $_; next if defined $ign_hash{$_}; foreach my $re (@ign_relist) { next L if $_ =~ $re; } print A $_; $addrcount++; } while () { $oldaddrcount = ($1 + 0), next if /^active:\s+(\d+)\s*$/; } close I; } print O "---\n"; print O "active: ", $addrcount, "\n"; close O; close A; undef %hash; #link $adbpath, $adbpath . '.' . $sometime; rename $adbpath . '.new', $adbpath or die "Cannot rename '$adbpath.new' to '$adbpath': $!\n"; rename $actpath . '.new', $actpath or die "Cannot rename '$actpath.new' to '$actpath': $!\n"; if ($oldaddrcount or $sstr eq '*') { $sometime = time - $sometime; my $new = $addrcount - $oldaddrcount; print "Added $new active addresses in $sometime seconds.\n"; } print "Total number of active addresses: $addrcount.\n"; exit 0; } die "$0: '$ARGV[0]': unknown option.\n"; __END__ =encoding utf8 =head1 NAME nottoomuch-addresses.sh -- address completion/matching (for notmuch) =head1 SYNOPSIS nottoomuch-addresses.sh ( --update [--rebuild] | ) B for more help =head1 VERSION 2.2 (2014-03-29) =head1 OPTIONS =head2 B<--update> This option is used to initially create the "address database" for searches to be done, and then incrementally update it with new addresses that are available in mails received since last update. In case you want to rebuild the database from scratch, add B<--rebuild> after --update on command line. This is necessary if some of the new emails received have Date: header point too much in the past (one week before last update). Update used emails Date: information to go through new emails to be checked for new addresses with one week's overlap. Other reason for rebuild could be enhancements in new versions of this program which change the email format in database. =head2 In case no option argument is given on command line, the command line arguments are used as fixed search string. Search goes through all email addresses in database and outputs every address (separated by newline) where a substring match with the given search string is found. No wildcard of regular expression matching is used. Search is not done unless there is at least 3 octets in search string. =head1 IGNORE FILE Some of the addresses collected may be valid but those still seems to be noisy junk. One may additionally want to just hide some email addresses. When running B<--update> the output shows the path of address database file (usually C<$HOME/.config/nottoomuch/addresses>). If there is file C in the same directory that file is read as newline-separated list of addresses which are not to be included in address database file. Use your text editor to open both of these files. Then move address lines to be ignored from B to B. After saving these 2 files the moved addresses will not reappear in B file again. Version 2.0 of nottoomuch-addresses.sh supports regular expressions in ignore file. Lines in format I or I defines (perl) Is which are used to match email addresses for ignoring. The I format makes regular expression case-insensitive -- although this is only applied to characters in ranges I and I. Remember that I and I provides same set of matching lines. =head1 LICENSE This program uses code from Email::Address perl module. Inclusion of that makes it easy to define license for the whole of this code base: Terms of Perl itself a) the GNU General Public License as published by the Free Software Foundation; either version 1, or (at your option) any later version, or b) the "Artistic License" =head1 SEE ALSO L, L =head1 AUTHOR Tomi Ollila -- too ät iki piste fi =head1 ACKNOWLEDGMENTS This program uses code from Email::Address, Copyright (c) by Casey West and maintained by Ricardo Signes. Thank you. All new bugs are mine, though.