--- /dev/null
+Revision history for Perl extension App::Lastmsg.
+
+0.001 2016-12-31T19:50+02:00
+ - Initial release
+
\ No newline at end of file
--- /dev/null
+Changes
+Makefile.PL
+MANIFEST
+README
+lastmsg
+t/App-Lastmsg.t
+t/inbox
+t/lastmsg.config
+t/sent
+lib/App/Lastmsg.pm
--- /dev/null
+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'
+ }
+ }
+);
--- /dev/null
+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.
+
+
--- /dev/null
+#!/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
--- /dev/null
+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
--- /dev/null
+#!/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
--- /dev/null
+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!
--- /dev/null
+---
+inbox: inbox
+sent: sent
+track:
+ user1: user1@example.com
+ user2:
+ - user2@example.com
+ - user2@example.org
+ user3:
+ - user3@example.com
--- /dev/null
+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!