#!/usr/bin/perl
use strict;
use warnings;
use Tk;
use Tk::ROText;
use Tk::BrowseEntry;
use File::Basename;
use Time::Local;
use POSIX;
use Getopt::Std;
use subs qw(logmsg errmsg);
our ($opt_c, $opt_r, $opt_v);
getopt('c');
$opt_c = "$ENV{HOME}/.mboxw/config.pl" unless ($opt_c);
die "No such file $opt_c" unless (-s $opt_c);
require $opt_c;
my $my_folder = dirname $opt_c;
our $soundplayer;
our $new_mail;
our $meeting;
our $mail_dimension;
our $mail_x_pos;
our $mail_y_pos;
our $cal_x_pos;
our $cal_y_pos;
our $inbox;
our $m_wait;
our $c_wait;
our $f_wait;
our $gcalcli;
our $snooze_interval;
our $reminder;
our $lockfile;
our $cachefile;
our %calendar;
our $nag_tm;
our $logfile;
our $log_gag;
our $log_roll_size;
our $log_roll_compress;
our $log_roll_retain;
our $renum_flag;
my %Mbox;
my $cutoff;
my %AckHead;
my %e_time;
my %e_stat;
my %AckAppt;
my %Gcal;
my %Snooze;
my $next_nag;
my $ymd = POSIX::strftime("%y%m%d", localtime);
my $localhost = `hostname`;
$localhost =~ s/\s+$//;
my $inbox_mnum = 0;
my %Month = (
Jan => 1,
Feb => 2,
Mar => 3,
Apr => 4,
May => 5,
Jun => 6,
Jul => 7,
Aug => 8,
Sep => 9,
Oct => 10,
Nov => 11,
Dec => 12,
);
sub logmsg {
my ($msg,$level) = @_;
$level = 0 unless ($level);
return if ($level < $log_gag);
$msg =~ s/\s+$//;
$msg = sprintf "%s %s", POSIX::strftime("%Y-%m-%d %H:%M", localtime), $msg;
if ($logfile) {
open LOG,">>$logfile" || errmsg "Cannot open: $logfile - $!";
print LOG "$msg\n";
close LOG;
print "$msg\n" if ($opt_v);
} else {
print "$msg\n";
}
}
sub errmsg {
logmsg $_[0];
die "ERROR: $_[0]\n";
}
sub flag_ok {
open CLICK, ">$my_folder/click.$$" || errmsg "$!";
print CLICK "D\n";
close CLICK;
exit;
}
sub flag_snooze {
open CLICK, ">$my_folder/click.$$" || errmsg "$!";
print CLICK "Z\n";
close CLICK;
exit;
}
sub appointment {
my ($a_time) = @_;
system "$soundplayer $meeting >/dev/null 2>&1 &";
my $due = int(($a_time - time)/60);
my $status;
if ( $due >= 0 ) {
$status = "Due in $due minutes";
} else {
$status = sprintf "Overdue by %d minutes", $due * -1;
}
my $mw = MainWindow->new(-title => "Appointment");
$mw->geometry("+$cal_x_pos+$cal_y_pos");
my $msg = sprintf "%s %s", POSIX::strftime("%H:%M", localtime), $Gcal{$a_time};
my $label = $mw -> Label(-text=>$msg) -> pack();
my $due_status = $mw -> Label(-text=>$status) -> pack();
my $button1 = $mw -> Button(-text => "Dismiss",
-command => \&flag_ok)
-> pack();
my $button2 = $mw -> Button(-text => "Snooze",
-command => \&flag_snooze)
-> pack();
MainLoop;
}
my $spam_details;
sub flag_acknowledged {
open CLICK, ">$my_folder/click.$$" || errmsg "$!";
print CLICK "A\n";
close CLICK;
exit;
}
sub flag_acknowledged_all {
open CLICK, ">$my_folder/click.$$" || errmsg "$!";
print CLICK "*\n";
close CLICK;
exit;
}
sub flag_spam {
my $msg_folder = "/foo/bar";
open CLICK, ">$my_folder/click.$$" || errmsg "$!";
print CLICK "S\n";
close CLICK;
my $app_owner = `whoami`;
$app_owner =~ s/\s+$//;
my $stat_msg = basename $inbox;
$stat_msg = "$msg_folder/$app_owner.$stat_msg.status";
open MSG,">>$stat_msg" || errmsg "Cannot open: $stat_msg - $!";
print MSG "$spam_details\tS\n";
close MSG;
exit;
}
sub you_got_mail {
my ($key,@args) = @_;
my @headers;
foreach my $tag (qw ( From To Cc Subject Date X-Spam-Status ) ) {
foreach (@args) { push @headers,$_ if (/^$tag: /i) }
}
push @headers,"","===","";
foreach (@_) {push @headers,$_
unless ( /^From: /i || /^To: /i || /^Cc: /i ||
/^Subject: /i || /^Date: /i || /^X-Spam-Status: /i)
}
system "$soundplayer $new_mail >/dev/null 2>&1 &";
my $mw = MainWindow->new(-title => "You've Got Mail!");
$mw->geometry( $mail_dimension . "+$mail_x_pos+$mail_y_pos");
my $f = $mw->Frame(-relief => 'ridge', -borderwidth => 2)
->pack(-fill => 'x');
my $t = $mw->Scrolled('ROText', -wrap => 'none', -scrollbars => 'se')
->pack(-expand => 1, -fill => 'both');
$t->tagConfigure('bold', -font => "System 11 bold");
$t->tagConfigure('normal', -font => "System 11");
while (@headers) {
my $x = shift @headers;
my ($tag,$content) = ("","");
if ( $x =~ /^(\S+:)/) {
($tag,$content) = ($1,$');
$t->insert('end', $tag, 'bold');
$t->insert('end', "$content\n", 'normal');
} else {
$t->insert('end', "$x\n", 'normal');
}
}
$f->Button(-text => ' *SPAM* ', -command => \&flag_spam)
->pack(-side => 'bottom');
$f->Button(-text => ' Dismiss ', -command => \&flag_acknowledged)
->pack(-side => 'bottom');
$f->Button(-text => 'Dismiss All', -command => \&flag_acknowledged_all)
->pack(-side => 'bottom');
$spam_details = "$key\t$e_time{$key}";
MainLoop;
}
sub stop_nagging {
open CLICK, ">$my_folder/click.$$" || errmsg "$!";
print CLICK "FIRST\n";
close CLICK;
exit;
}
sub stop_nagging_all {
open CLICK, ">$my_folder/click.$$" || errmsg "$!";
print CLICK "ALL\n";
close CLICK;
exit;
}
sub nagging_alert {
system "$soundplayer $new_mail >/dev/null 2>&1 &";
my $mw = MainWindow->new(-title => "You've Got Mail");
$mw->geometry("+$mail_x_pos+$mail_y_pos");
my $f = $mw->Frame(-relief => 'ridge', -borderwidth => 2)
->pack(-fill => 'x');
my $t = $mw->Scrolled('ROText', -wrap => 'none', -scrollbars => 'se')
->pack(-expand => 1, -fill => 'both');
$t->tagConfigure('bold', -font => "System 11 bold");
$t->tagConfigure('normal', -font => "System 11");
foreach my $key (sort{$e_time{$a}<=>$e_time{$b}} keys %AckHead){
my $arrayref = $AckHead{$key};
next unless ($e_stat{$key} eq "K");
my ($from,$subject) = ("","");
foreach my $h (@$arrayref) {
$from = $' if ($h =~ /^From: /i);
$subject = $' if ($h =~ /^Subject: /i);
last if ($from && $subject);
}
$subject =~ s/^\s+//;
$from =~ s/^\s+//;
if ($from =~ /^(.*)\</) {
my $from_readable = $1;
$from_readable =~ s/\s+$//;
$from = $from_readable if ($from_readable);
}
$from =~ s/\s+$//;
$from =~ s/^["']//;
$from =~ s/["']$//;
$t->insert('end', sprintf ("%-30.30s ",$from), 'bold');
$t->insert('end', sprintf ("%-50.50s\n",$subject), 'normal');
}
$f->Button(-text => ' Dismiss ', -command => \&stop_nagging)
->pack(-side => 'bottom');
$f->Button(-text => 'Dismiss All', -command => \&stop_nagging_all)
->pack(-side => 'bottom');
MainLoop;
}
sub read_cache {
my $now = time;
foreach my $key (qw ( msize mtime last_ack last_write ) ) {
$Mbox{$key} = 0 unless ($Mbox{$key});
}
return unless (-s $cachefile);
logmsg "reading cachefile";
open CACHE,$cachefile || errmsg "Cannot open $cachefile";
$cutoff = $now -(86400 * 7);
my $tag;
while (<CACHE>) {
s/\s+$//;
next unless ($_);
if (/\[(\S+)\]$/) {
$tag = $1;
last if ($tag eq "mbox_cache");
next;
}
next unless $tag;;
next if ($tag eq "last_ack" && $Mbox{last_ack} > $_);
$Mbox{$tag} = $_;
}
while (<CACHE>) {
s/\s+$//;
last if (/^\[gcal\]$/);
next unless ($_);
my ($n, $msg_id, $time, $status) = split(/\t/,$_);
next unless ($time && $status);
my $key = "$n\t$msg_id";
next if ($time < $cutoff);
$e_time{$key} = $time;
$e_stat{$key} = $status;
}
while (<CACHE>) {
s/\s+$//;
next unless ($_);
my ($a_time, $details, $status) = split(/\t/,$_);
next if (($now - $a_time) > 86400 );
$Gcal{$a_time} = $details;
$AckAppt{$a_time} = $status;
logmsg sprintf("item: %s %s %s", $status,
POSIX::strftime("%H:%M", localtime($a_time)), $details);
}
}
sub write_cache {
$Mbox{last_write} = time;
logmsg "writing cachefile";
open CACHE,">$cachefile" || errmsg "Cannot open $cachefile";
foreach my $key (qw ( msize mtime last_ack last_write) ) {
print CACHE "[$key]\n$Mbox{$key}\n";
}
print CACHE "[mbox_cache]\n";
foreach my $key (sort{$e_time{$a}<=>$e_time{$b}} keys %e_time){
print CACHE "$key\t$e_time{$key}\t$e_stat{$key}\n";
}
print CACHE "[gcal]\n";
foreach my $a_time (sort { $a<=>$b } keys %Gcal) {
my $status = "N";
$status = $AckAppt{$a_time} if ($AckAppt{$a_time});
print CACHE "$a_time\t$Gcal{$a_time}\t$status\n";
}
close CACHE || errmsg "Cannot close $cachefile";
}
sub read_headers {
my %Header;
my @headers;
$cutoff = time -(86400 * 7);
my ($tag, $content) = ("","");
open INBOX,$inbox || errmsg "Cannot open $inbox";
$inbox_mnum = 0;
my $timestamp;
my $key = "";
%AckHead = ();
my %Src;
if ( $renum_flag) {
foreach my $k (keys %e_time) {
next unless ($e_stat{$k} && $e_stat{$k} eq "A");
my $tstamp = $e_time{$k};
my ($m,$from) = split (/\t/,$k);
$Src{"$from\t$tstamp"} = "A";
}
}
logmsg "reading headers from $inbox";
while (<INBOX>) {
s/\s+$//;
if ( $key ) {
if ( $_) {
if ( /^(\S+:)\s+/ ) {
($tag, $content) = ($1, $');
push (@headers, $_);
$Header{$tag} = $content;
} else {
$_ =~ s/^\s+//;
$headers[$#headers] .= " $_";
}
} else {
my $from = (split(/\t/,$key))[1];
my $status = "N";
$status = $Header{"Status:"} if ($Header{"Status:"});
$status = "O" if ($status eq "RO");
$status = "R" if ( $status eq "RO" && $Header{"X-Status:"} eq "A");
if ( $status eq "N") {
if ( $e_time{$key} && $e_time{$key} == $timestamp ) {
$status = $e_stat{$key} if ($e_stat{$key} =~ /[AK]/);
} elsif ($renum_flag && $Src{"$from\t$timestamp"} ) {
$status = $Src{"$from\t$timestamp"};
logmsg "inbox: $inbox_mnum from: $from already acknoledged, status: $status";
} else {
logmsg "inbox: $inbox_mnum from: $from";
}
$e_time{$key} = $timestamp;
$e_stat{$key} = $status;
my @array = @headers;
$AckHead{$key} = \@array;
} else {
delete $e_time{$key} if ($e_time{$key});
delete $e_stat{$key} if ($e_stat{$key});
}
$key = $tag = "";
%Header = ();
@headers = ();
}
} elsif ( /^From\s+(\S+)\s+(\w+)\s+(\w+)\s+(\d+)\s+(\d+):\s*(\d+):\s*(\d+)\s+(\d+)/) {
my ($from,$wday,$Mth,$d,$h,$m,$s,$y) = ($1,$2,$3,$4,$5,$6,$7,$8);
my $mth = $Month{$Mth};
$inbox_mnum++;
$timestamp = timelocal ($s,$m,$h,$d,$mth-1,$y);
next if ($timestamp < $cutoff);
$key = sprintf "%d\t%s", $inbox_mnum,$from;
$tag = "From";
$content = $_;
$content =~ s/^From\s+//;
$Header{$tag} = $content;
}
}
close INBOX;
}
sub get_gcal_agenda {
my $start = POSIX::strftime("%Y-%m-%d", localtime (time - 86400) );
my $end = POSIX::strftime("%Y-%m-%d", localtime (time + 86400) );
my $a_date = POSIX::strftime("%a %b %d", localtime);
my $l = 0;
my $now = time;
my %agenda;
foreach my $cal(keys %calendar) {
my $today = 0;
my @item = `$gcalcli --nc --cal '$calendar{$cal}' agenda $start $end 2>&1`;
if ($?) {
logmsg join (' ',"No connection with Gcal. ",@item);
return;
} else {
foreach (@item) {
s/\s+$//;
next unless ($_ && length($_) > 18);
$today++ if (substr($_, 0, 10) eq $a_date);
my ($h,$m) = split(/:/,substr($_, 12,5));
$h += 12 if (substr($_, 17, 2) eq "pm" && $h < 12);
$h = 0 if ($h == 12 && substr($_, 17, 2) eq "am");
my @day = localtime (time - 86400);
if ($today) {
@day = localtime;
}
$day[0] = 0;
$day[1] = $m;
$day[2] = $h;
my $a_time = timelocal @day;
next if ($today == 0 && ($now - $a_time) > 14400);
my $detail = "$cal: " . substr($_, 21);
if ( $agenda{$a_time} ) {
next if ($agenda{$a_time} eq $detail);
$agenda{$a_time} .= " / $detail";
} else {
$agenda{$a_time} = $detail;
}
}
}
}
foreach my $a_time (sort {$a<=>$b} keys %agenda){
next if ($Gcal{$a_time} && $Gcal{$a_time} eq $agenda{$a_time});
if ($Gcal{$a_time}) {
logmsg sprintf("updating agenda item: %s %s",
POSIX::strftime("%H:%M", localtime($a_time)),
$agenda{$a_time});
} else {
logmsg sprintf("new agenda item: %s %s",
POSIX::strftime("%H:%M", localtime($a_time)),
$agenda{$a_time});
}
$Gcal{$a_time} = $agenda{$a_time};
}
foreach my $a_time (sort {$a<=>$b} keys %Gcal){
next if ($agenda{$a_time});
logmsg sprintf("removing agenda item: %s %s",
POSIX::strftime("%H:%M", localtime($a_time)),
$Gcal{$a_time});
delete $Gcal{$a_time};
delete $AckAppt{$a_time} if ($AckAppt{$a_time});
delete $Snooze{$a_time} if ($Snooze{$a_time});
}
}
sub process_mail_response {
my ($click_file,$key) = @_;
if (open CLICK,$click_file) {
my $status = (<CLICK>);
close CLICK;
$status =~ s/\s+$//;
if ( $status eq 'FIRST' || $status eq 'ALL' || $status eq '*') {
foreach my $key (sort{$e_time{$a}<=>$e_time{$b}} keys %AckHead){
next unless ($e_stat{$key} eq "K" || $status eq '*');
$e_stat{$key} = "A";
delete $AckHead{$key};
last if ($status eq 'FIRST');
}
} else {
$e_stat{$key} = $status;
$Mbox{last_ack} = $e_time{$key};
delete $AckHead{$key};
}
unlink $click_file;
} else {
logmsg "could not open click file: $click_file - $!",99;
}
return;
}
sub mail_response_wait {
my ($child, $key) = @_;
my $wait = $f_wait + 1;
my $click_file = "$my_folder/click.$child";
while ( $wait-- > 0 ) {
if (-s $click_file) {
process_mail_response($click_file,$key);
return;
}
sleep 1;
}
kill 15, $child;
logmsg "no response from child process: $child, terminated",1;
if ($key) {
$e_stat{$key} = "K";
} else {
$next_nag = time + $nag_tm;
}
}
sub gcal_response_wait {
my ($child, $a_time) = @_;
my $wait = $f_wait + 1;
my $click_file = "$my_folder/click.$child";
while ( $wait-- > 0 ) {
if (-s $click_file) {
if (open CLICK,$click_file) {
my $status = (<CLICK>);
close CLICK;
$status =~ s/\s+$//;
$AckAppt{$a_time} = $status;
$Snooze{$a_time} = time if ( $status eq "Z");
unlink $click_file;
} else {
logmsg "could not open click file: $click_file - $!",99;
}
return;
}
sleep 1;
}
kill 15, $child;
logmsg "no response from child process: $child, terminated",2;
$AckAppt{$a_time} = "Z";
$Snooze{$a_time} = time;
}
sub init {
if (@ARGV) {
$opt_v = 1;
if ( $ARGV[0] =~ /^term$/i) {
errmsg "Can't terminate running process without a lockfile"
unless ($lockfile && -s $lockfile);
open LOCK,$lockfile || errmsg "Cannot open: $lockfile - $!";
my %Lock;
my $tag;
while(<LOCK>) {
s/\s+$//;
if (/^\[(\w+)\]$/) {
$tag = $1;
next;
}
errmsg "Cannot fathom file: $lockfile" unless ($tag);
$Lock{$tag} = $_;
}
errmsg "Cannot terminate process on host: $Lock{HOSTNAME}"
unless ( $Lock{HOSTNAME} eq $localhost);
errmsg "Cannot find active PID"
unless ($Lock{PID} && $Lock{PID} =~ /^\d+$/);
kill 15,$Lock{PID};
unlink $lockfile;
logmsg "PID $Lock{PID} terminated.";
}
exit;
}
if ($logfile) {
if ($log_roll_size && (-f $logfile) && $log_roll_size < (-s $logfile)) {
rename $logfile,"$logfile.$ymd";
if ($log_roll_compress) {
$log_roll_retain = $log_roll_compress * 2 unless ($log_roll_compress);
foreach my $l(glob "$logfile.[0-9]*") {
if ($l =~ /\.gz$/) {
unlink $l if ((-M $l) > $log_roll_retain);
} else {
system "gzip -f $l" if ((-M $l) > $log_roll_compress);
}
}
}
}
open STDERR,">>$logfile";
}
logmsg "$0 commenced, PID: $$",2;
errmsg "Bad f_wait: $f_wait" unless ($f_wait && $f_wait =~ /^\d+$/ && $f_wait > 0);
errmsg "Bad m_wait: $m_wait" unless ($m_wait && $m_wait =~ /^\d+$/ && $m_wait > 0);
errmsg "Bad c_wait: $c_wait" unless ($c_wait && $c_wait =~ /^\d+$/ && $c_wait > 0);
$m_wait *= 60;
$c_wait *= 60;
if ($nag_tm && $nag_tm =~ /^\d+$/ && $nag_tm > 0) {
$nag_tm *= 60;
$next_nag = time + $nag_tm;
} else {
$nag_tm = $next_nag = 0;
}
$log_gag = 0 unless ($log_gag && $log_gag =~ /^\d+$/);
$log_gag = 3 if ($log_gag > 3);
if ( $lockfile && -f $lockfile) {
logmsg "found lock: $lockfile",2;
my $wait = int(($m_wait/60) * 5);
my $stale = $wait * 60;
my @stat;
while ($wait) {
@stat = stat $lockfile;
last if ((time - $stat[9]) > $stale);
sleep 60;
$wait--;
logmsg "waiting on lock: $lockfile ... giving up in $wait minutes",2;
}
@stat = stat $lockfile;
if ((time - $stat[9]) < $stale){
logmsg "gave up waiting on lock: $lockfile -- $0 terminating ...",99;
exit;
}
logmsg "removing stale lock: $lockfile",2;
}
if ($lockfile) {
open LOCK,">$lockfile" || errmsg "Cannot open file: $lockfile - $!";
print LOCK "[HOSTNAME]\n$localhost\n[PID]\n$$\n";
close LOCK;
}
$cachefile = "$my_folder/mboxw_data.cache" unless ($cachefile);
read_cache();
get_gcal_agenda();
}
init();
while (1) {
sleep $m_wait;
my $kill_count = 0;
my @stat = stat $inbox;
unless (@stat) {
logmsg "ERROR: Cannot stat $inbox",99;
next;
}
system "touch $lockfile" if ($lockfile);
unless ($stat[7] == $Mbox{msize} && $stat[9] == $Mbox{mtime} ) {
read_headers();
$Mbox{msize} = $stat[7];
$Mbox{mtime} = $stat[9];
foreach my $key (keys %e_time){
delete $e_time{$key} if ((split (/\t/,$key))[0] > $inbox_mnum);
}
}
foreach my $key (sort{$e_time{$a}<=>$e_time{$b}} keys %AckHead){
my $arrayref = $AckHead{$key};
if ($e_stat{$key} eq "K") {
$kill_count++;
next;
}
next unless ($e_stat{$key} eq "N");
my $child = fork;
if ($child) {
mail_response_wait($child, $key);
@stat = stat $inbox;
last unless ($stat[7] == $Mbox{msize} && $stat[9] == $Mbox{mtime} );
} else {
you_got_mail($key,@$arrayref);
exit;
}
}
foreach my $a_time (sort {$a<=>$b} keys %Gcal){
$AckAppt{$a_time} = "N" unless ($AckAppt{$a_time});
next if ( $AckAppt{$a_time} eq "D");
my $now = time;
if ( $AckAppt{$a_time} eq "Z") {
$Snooze{$a_time} = 0 unless ($Snooze{$a_time});
if ( ($now - $Snooze{$a_time} ) > $snooze_interval * 60) {
$Snooze{$a_time} = $now;
} else {
next;
}
}
next if (($a_time - $now) > ($reminder * 60) );
my $child = fork;
if ($child) {
gcal_response_wait($child, $a_time);
} else {
appointment($a_time);
exit;
}
}
if ( (time - $Mbox{last_write}) > $c_wait) {
logmsg "refreshing calendar info",2;
get_gcal_agenda();
write_cache();
$Mbox{last_write} = time;
}
if ( $nag_tm && $kill_count && (time > $next_nag) ) {
logmsg "nagging alert";
my $child = fork;
if ($child) {
mail_response_wait($child);
} else {
nagging_alert();
exit;
}
}
}