From 82af6c123c3cc37d8c4174dbb7e5180e067fd7dd Mon Sep 17 00:00:00 2001 From: Marius Gavrilescu Date: Sat, 31 Dec 2016 19:50:03 +0200 Subject: [PATCH 1/1] Initial commit --- Changes | 5 ++ MANIFEST | 10 ++++ Makefile.PL | 22 +++++++ README | 34 +++++++++++ lastmsg | 124 +++++++++++++++++++++++++++++++++++++++ lib/App/Lastmsg.pm | 143 +++++++++++++++++++++++++++++++++++++++++++++ t/App-Lastmsg.t | 29 +++++++++ t/inbox | 29 +++++++++ t/lastmsg.config | 10 ++++ t/sent | 9 +++ 10 files changed, 415 insertions(+) create mode 100644 Changes create mode 100644 MANIFEST create mode 100644 Makefile.PL create mode 100644 README create mode 100755 lastmsg create mode 100644 lib/App/Lastmsg.pm create mode 100644 t/App-Lastmsg.t create mode 100644 t/inbox create mode 100644 t/lastmsg.config create mode 100644 t/sent diff --git a/Changes b/Changes new file mode 100644 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 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 index 0000000..4e95e6a --- /dev/null +++ b/Makefile.PL @@ -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 ', + 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 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 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 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 + +The path(s) to your inbox and other incoming mail folders (a single +string or a list of strings). The C field of these emails is +scanned. + +If not provided, it defaults to F and +F<$ENV{HOME}/Maildir/>. + +B See L 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 + +The path(s) to your sent and other outgoing mail folders (a single +string or a list of strings). The C, C, and C fields of +these emails are scanned. + +If not provided, it default to an empty list. See B above for +information on how the type of a folder is identified. + +=item B + +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, +F, F, or F<.lastmsgrc> and can be placed in +the current directory, in your home directory, in F, and in +F. See L for more information. + +=head1 ENVIRONMENT + +The only recognised environment variable is B, 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, Emarius@ieval.roE + +=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 index 0000000..827cd36 --- /dev/null +++ b/lib/App/Lastmsg.pm @@ -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 =~ /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 script. +See that script's documentation for information on what it does. + +=head1 SEE ALSO + +L + +=head1 AUTHOR + +Marius Gavrilescu, Emarius@ieval.roE + +=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 index 0000000..a5eca29 --- /dev/null +++ b/t/App-Lastmsg.t @@ -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 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 (Comment) +Message-ID: +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: +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: +To: MGV@cpan.org +Subject: Hello + +Hi MGV! diff --git a/t/lastmsg.config b/t/lastmsg.config new file mode 100644 index 0000000..52f3987 --- /dev/null +++ b/t/lastmsg.config @@ -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 index 0000000..c79915d --- /dev/null +++ b/t/sent @@ -0,0 +1,9 @@ +From nobody Sat Dec 31 13:10:10 2016 +From: Marius Gavrilescu +To: Random User (Very random), User 2 +Subject: Re: Hi +References: +Message-ID: <33r32r32igf432g@localhost.localdomain> +Date: Sat, 31 Dec 2016 13:10:00 +0200 + +Hi User 2! -- 2.30.2