Initial commit 0.001
authorMarius Gavrilescu <marius@ieval.ro>
Sat, 31 Dec 2016 17:50:03 +0000 (19:50 +0200)
committerMarius Gavrilescu <marius@ieval.ro>
Sat, 31 Dec 2016 17:50:03 +0000 (19:50 +0200)
Changes [new file with mode: 0644]
MANIFEST [new file with mode: 0644]
Makefile.PL [new file with mode: 0644]
README [new file with mode: 0644]
lastmsg [new file with mode: 0755]
lib/App/Lastmsg.pm [new file with mode: 0644]
t/App-Lastmsg.t [new file with mode: 0644]
t/inbox [new file with mode: 0644]
t/lastmsg.config [new file with mode: 0644]
t/sent [new file with mode: 0644]

diff --git a/Changes b/Changes
new file mode 100644 (file)
index 0000000..9ebc0cd
--- /dev/null
+++ b/Changes
@@ -0,0 +1,5 @@
+Revision history for Perl extension App::Lastmsg.
+
+0.001 2016-12-31T19:50+02:00
+ - Initial release
\ No newline at end of file
diff --git a/MANIFEST b/MANIFEST
new file mode 100644 (file)
index 0000000..2dc728f
--- /dev/null
+++ b/MANIFEST
@@ -0,0 +1,10 @@
+Changes
+Makefile.PL
+MANIFEST
+README
+lastmsg
+t/App-Lastmsg.t
+t/inbox
+t/lastmsg.config
+t/sent
+lib/App/Lastmsg.pm
diff --git a/Makefile.PL b/Makefile.PL
new file mode 100644 (file)
index 0000000..4e95e6a
--- /dev/null
@@ -0,0 +1,22 @@
+use ExtUtils::MakeMaker;
+use 5.014000;
+
+WriteMakefile(
+       NAME              => 'App::Lastmsg',
+       VERSION_FROM      => 'lib/App/Lastmsg.pm',
+       ABSTRACT_FROM     => 'lib/App/Lastmsg.pm',
+       AUTHOR            => 'Marius Gavrilescu <marius@ieval.ro>',
+       MIN_PERL_VERSION  => '5.14.0',
+       LICENSE           => 1,
+       PREREQ_PM         => {
+               qw/Config::Auto  0
+                  Date::Parse   0
+                  Email::Folder 0/,
+       },
+       META_MERGE        => {
+               dynamic_config => 0,
+               resources      => {
+                       repository => 'https://git.ieval.ro/?p=app-lastmsg.git'
+               }
+       }
+);
diff --git a/README b/README
new file mode 100644 (file)
index 0000000..0e045ea
--- /dev/null
+++ b/README
@@ -0,0 +1,34 @@
+App-Lastmsg version 0.001
+=========================
+
+lastmsg reads your mail folders looking for emails you exchanged with
+a given set of people. Then for each person in the set it prints the
+time you last received an email from or sent an email to them and the
+email address used.
+
+INSTALLATION
+
+To install this module type the following:
+
+   perl Makefile.PL
+   make
+   make test
+   make install
+
+DEPENDENCIES
+
+This module requires these other modules and libraries:
+
+* Config::Auto
+* Date::Parse
+* Email::Folder
+
+COPYRIGHT AND LICENCE
+
+Copyright (C) 2016 by Marius Gavrilescu
+
+This library is free software; you can redistribute it and/or modify
+it under the same terms as Perl itself, either Perl version 5.24.1 or,
+at your option, any later version of Perl 5 you may have available.
+
+
diff --git a/lastmsg b/lastmsg
new file mode 100755 (executable)
index 0000000..7f818b7
--- /dev/null
+++ b/lastmsg
@@ -0,0 +1,124 @@
+#!/usr/bin/perl
+use 5.014000;
+use strict;
+use warnings;
+
+use App::Lastmsg;
+
+App::Lastmsg::run;
+
+__END__
+
+=encoding utf-8
+
+=head1 NAME
+
+lastmsg - last(1) semblance for your inbox
+
+=head1 SYNOPSIS
+
+  # in ~/.lastmsgrc
+  inbox:
+    - /home/MGV/mail/inbox
+    - /home/MGV/mail/folder
+  sent:
+    - /home/MGV/mail/sent
+  track:
+    bestfriend:
+      - best@friend.com
+      - best.friend@freemail.com
+    someguy: SOMEGUY@cpan.org
+    nobody:
+      - nobody@example.com
+
+  # in your shell
+  mgv@somehost ~ $ lastmsg
+  bestfriend best@friend.com  Sat 31 Dec 2016 12:34:56 EET
+  someguy    SOMEGUY@cpan.org Thu 20 Nov 2016 12:00:00 EET
+  nobody                      NOT FOUND
+
+=head1 DESCRIPTION
+
+lastmsg reads your mail folders looking for emails you exchanged with
+a given set of people. Then for each person in the set it prints the
+time you last received an email from or sent an email to them and the
+email address used.
+
+The script takes no arguments (the settings are taken from a
+configuration file), and it prints a three-column table where the
+first column is the ID of a person, the second column is the email
+address last used (empty if you've never exchanged an email with that
+person), and the last column is the date of last contact (or the
+string C<NOT FOUND> if you've never exchanged an email). The rows are
+sorted by date of last contact (with the most recently contacted
+people at the top), and the people that you've never exchanged an
+email with are at the end.
+
+The configuration is in YAML format. Three keys are recognised:
+
+=over
+
+=item B<inbox>
+
+The path(s) to your inbox and other incoming mail folders (a single
+string or a list of strings). The C<From> field of these emails is
+scanned.
+
+If not provided, it defaults to F</var/mail/$ENV{USER}> and
+F<$ENV{HOME}/Maildir/>.
+
+B<NOTE:> See L<Email::FolderType> for information on how the type of a
+folder is identified. In short, the suffix of the folder is analyzed:
+If F</>, the format is Maildir. If F</.>, the format is MH. If F<//>,
+the format is Ezmlm. Otherwise, some heuristics are checked and the
+fallback is Mbox.
+
+=item B<sent>
+
+The path(s) to your sent and other outgoing mail folders (a single
+string or a list of strings). The C<To>, C<Cc>, and C<Bcc> fields of
+these emails are scanned.
+
+If not provided, it default to an empty list. See B<NOTE:> above for
+information on how the type of a folder is identified.
+
+=item B<track>
+
+A hash of people to track. Each entry represents a person. The key is
+the ID of that person (used for display), and the value is the email
+address of that person or a list of email addresses of that person.
+
+If not provided, the script will die with an error.
+
+=back
+
+The configuration file can be named F<lastmsgconfig>,
+F<lastmsg.config>, F<lastmsgrc>, or F<.lastmsgrc> and can be placed in
+the current directory, in your home directory, in F</etc/>, and in
+F</usr/local/etc/>. See L<Config::Auto> for more information.
+
+=head1 ENVIRONMENT
+
+The only recognised environment variable is B<LASTMSG_DEBUG>, which if
+set to a true value causes the script to emit a lot of information
+about what it is doing.
+
+=head1 TODO
+
+Should handle IRC and IM logs as well, not just emails. Should have
+better tests.
+
+=head1 AUTHOR
+
+Marius Gavrilescu, E<lt>marius@ieval.roE<gt>
+
+=head1 COPYRIGHT AND LICENSE
+
+Copyright (C) 2016 by Marius Gavrilescu
+
+This library is free software; you can redistribute it and/or modify
+it under the same terms as Perl itself, either Perl version 5.24.1 or,
+at your option, any later version of Perl 5 you may have available.
+
+
+=cut
diff --git a/lib/App/Lastmsg.pm b/lib/App/Lastmsg.pm
new file mode 100644 (file)
index 0000000..827cd36
--- /dev/null
@@ -0,0 +1,143 @@
+package App::Lastmsg;
+
+use 5.014000;
+use strict;
+use warnings;
+
+use Config::Auto;
+$Config::Auto::DisablePerl = 1;
+use Date::Parse;
+use Email::Folder;
+use List::Util qw/max/;
+use POSIX qw/strftime/;
+
+our $OUTPUT_FILEHANDLE = \*STDOUT;
+our $VERSION = '0.001';
+
+our @DEFAULT_INBOX = (
+       "/var/mail/$ENV{USER}",
+       "$ENV{HOME}/Maildir/",
+);
+
+sub run {
+       my $config = Config::Auto->new(format => 'yaml')->parse;
+       die "No configuration file found\n" unless $config;
+       die "No addresses to track listed in config\n" unless $config->{track};
+
+       $config->{inbox} //= [];
+       $config->{sent}  //= [];
+       $config->{inbox} = [$config->{inbox}] unless ref $config->{inbox};
+       $config->{sent}  = [$config->{sent}]  unless ref $config->{sent};
+       $config->{inbox} = \@DEFAULT_INBOX unless @{$config->{inbox}};
+
+       my %track = %{$config->{track}};
+       my %addr_to_id = map {
+               my $id = $_;
+               my $track = $track{$id};
+               $track = [$track] unless ref $track;
+               map { $_ => $id } @$track
+       } keys %track;
+
+       my (%lastmsg, %lastaddr);
+
+       my $process_message = sub {
+               my ($msg, @people) = @_;
+               for my $addr (@people) {
+                       ($addr) = $addr =~ /<\s*(.+)\s*>/ if $addr =~ /</;
+                       $addr =~ s/^\s+//;
+                       $addr =~ s/\s+$//;
+                       my $id = $addr_to_id{$addr};
+                       next unless $id;
+                       my $date = str2time $msg->header_raw('Date');
+                       if (!exists $lastmsg{$id} || $lastmsg{$id} < $date) {
+                               $lastmsg{$id} = $date;
+                               $lastaddr{$id} = $addr;
+                       }
+               }
+       };
+
+       for my $folder (@{$config->{inbox}}) {
+               next unless -e $folder;
+               say "Scanning $folder (inbox)" if $ENV{LASTMSG_DEBUG};
+               my $folder = Email::Folder->new($folder);
+               while (my $msg = $folder->next_message) {
+                       my ($from) = grep { /^from$/i } $msg->header_names;
+                       $from = $msg->header_raw($from);
+                       if ($ENV{LASTMSG_DEBUG}) {
+                               my $mid = grep { /^message-id$/i } $msg->header_names;
+                               say 'Processing ', $msg->header_raw('Message-ID'),
+                                 " from $from" if $ENV{LASTMSG_DEBUG};
+                       }
+                       $process_message->($msg, $from);
+               }
+       }
+
+       for my $folder (@{$config->{sent}}) {
+               next unless -e $folder;
+               say "Scanning $folder (sent)" if $ENV{LASTMSG_DEBUG};
+               my $folder = Email::Folder->new($folder);
+               while (my $msg = $folder->next_message) {
+                       my @hdrs = grep { /^(?:to|cc|bcc)$/i } $msg->header_names;
+                       my @people;
+                       for my $hdr (@hdrs) {
+                               @people = (@people, split /,/, $msg->header_raw($hdr));
+                       }
+                       if ($ENV{LASTMSG_DEBUG}) {
+                               my $mid = grep { /^message-id$/i } $msg->header_names;
+                               say 'Processing ', $msg->header_raw($mid),
+                                 ' sent to ', join ',', @people if $ENV{LASTMSG_DEBUG};
+                       }
+                       $process_message->($msg, @people);
+               }
+       }
+
+       my $idlen   = max map { length } keys %track;
+       my $addrlen = max map { length } values %lastaddr;
+
+       for (sort { $lastmsg{$b} <=> $lastmsg{$a} } keys %lastmsg) {
+               my $time = strftime '%c', localtime $lastmsg{$_};
+               printf $OUTPUT_FILEHANDLE "%-${idlen}s %-${addrlen}s %s\n", $_, $lastaddr{$_}, $time;
+       }
+
+       for (grep { !exists $lastmsg{$_} } sort keys %track) {
+               printf $OUTPUT_FILEHANDLE "%-${idlen}s %-${addrlen}s NOT FOUND\n", $_, ''
+       }
+}
+
+1;
+__END__
+
+=encoding utf-8
+
+=head1 NAME
+
+App::Lastmsg - last(1) semblance for your inbox
+
+=head1 SYNOPSIS
+
+  use App::Lastmsg;
+  App::Lastmsg::run
+
+=head1 DESCRIPTION
+
+This module contains the implementation of the L<lastmsg(1)> script.
+See that script's documentation for information on what it does.
+
+=head1 SEE ALSO
+
+L<lastmsg>
+
+=head1 AUTHOR
+
+Marius Gavrilescu, E<lt>marius@ieval.roE<gt>
+
+=head1 COPYRIGHT AND LICENSE
+
+Copyright (C) 2016 by Marius Gavrilescu
+
+This library is free software; you can redistribute it and/or modify
+it under the same terms as Perl itself, either Perl version 5.24.1 or,
+at your option, any later version of Perl 5 you may have available.
+
+
+=cut
diff --git a/t/App-Lastmsg.t b/t/App-Lastmsg.t
new file mode 100644 (file)
index 0000000..a5eca29
--- /dev/null
@@ -0,0 +1,29 @@
+#!/usr/bin/perl
+use strict;
+use warnings;
+
+use Test::More tests => 2;
+BEGIN { use_ok('App::Lastmsg') };
+
+my $real_strftime = \&POSIX::strftime;
+
+{
+       no warnings 'redefine';
+       *POSIX::strftime = sub {
+               my ($format, @rest) = @_;
+               $real_strftime->('%s', @rest)
+       };
+}
+
+chdir 't';
+my $out = '';
+open my $fh, '>', \$out or die "$!";
+$App::Lastmsg::OUTPUT_FILEHANDLE = $fh;
+$0 = 'lastmsg';
+App::Lastmsg::run;
+
+is $out, <<'EOF', 'output is correct';
+user2 user2@example.org Sat 31 Dec 2016 13:10:00 EET
+user1 user1@example.com Fri 30 Dec 2016 15:44:00 EET
+user3                   NOT FOUND
+EOF
diff --git a/t/inbox b/t/inbox
new file mode 100644 (file)
index 0000000..8781b79
--- /dev/null
+++ b/t/inbox
@@ -0,0 +1,29 @@
+From user1@example.com Sat Dec 31 10:01:02 2016
+Envelope-to: MGV@cpan.org
+Date: Sat, 31 Dec 2016 10:00:00 2016
+From: User 1 <user1@example.com> (Comment)
+Message-ID: <mail1@example.com>
+To: MGV@cpan.org
+Subject: Test email
+
+This is a test email.
+
+From user1@example.com Sat Dec 31 9:12:34 2016
+Envelope-to: MGV@cpan.org
+Date: Sat, 31 Dec 2016 9:00:00 2016
+From: user1@example.com
+Message-ID: <mail2@example.com>
+To: MGV@cpan.org
+Subject: Older test email
+
+This is a test email, sent an hour before the one above.
+
+From user2@example.com Sat Dec 31 12:35:00 2016
+Envelope-to: MGV@cpan.org
+Date: Sat, 31 Dec 2016 12:34:56 2016
+From: user2@example.com
+Message-ID: <mail3@example.com>
+To: MGV@cpan.org
+Subject: Hello
+
+Hi MGV!
diff --git a/t/lastmsg.config b/t/lastmsg.config
new file mode 100644 (file)
index 0000000..52f3987
--- /dev/null
@@ -0,0 +1,10 @@
+---
+inbox: inbox
+sent: sent
+track:
+  user1: user1@example.com
+  user2:
+    - user2@example.com
+    - user2@example.org
+  user3:
+    - user3@example.com
diff --git a/t/sent b/t/sent
new file mode 100644 (file)
index 0000000..c79915d
--- /dev/null
+++ b/t/sent
@@ -0,0 +1,9 @@
+From nobody Sat Dec 31 13:10:10 2016
+From: Marius Gavrilescu <MGV@cpan.org>
+To: Random User <rando@example.com> (Very random), User 2 <user2@example.org>
+Subject: Re: Hi
+References: <mail3@example.com>
+Message-ID: <33r32r32igf432g@localhost.localdomain>
+Date: Sat, 31 Dec 2016 13:10:00 +0200
+
+Hi User 2!
This page took 0.019023 seconds and 4 git commands to generate.