Fixed up capture
[clearscm.git] / bin / announceEmail.pl
1 #!/usr/bin/perl
2
3 =pod
4
5 =head1 NAME $RCSfile: announceEmail.pl,v $
6
7 Monitors an IMAP Server and announce incoming emails by extracting the subject
8 line and from line and then pushing that into "GoogleTalk".
9
10 =head1 VERSION
11
12 =over
13
14 =item Author
15
16 Andrew DeFaria <Andrew@DeFaria.com>
17
18 =item Revision
19
20 $Revision: 1.2 $
21
22 =item Created:
23
24 Thu Apr  4 13:40:10 MST 2019
25
26 =item Modified:
27
28 $Date: 2019/04/04 13:40:10 $
29
30 =back
31
32 =head1 SYNOPSIS
33
34  Usage: announceEmail.pl [-usa|ge] [-h|elp] [-v|erbose] [-de|bug]
35                          [-use|rname <username>] [-p|assword <password>]
36                          [-i|map <server>] [-t|imeout <secs>]
37                          [-an|nouce] [-ap|pend] [-da|emon] [-n|name <name>]
38                          [-uses|sl] [-useb|locking]
39
40  Where:
41    -usa|ge       Print this usage
42    -h|elp        Detailed help
43    -v|erbose     Verbose mode (Default: -verbose)
44    -de|bug       Turn on debugging (Default: Off)
45
46    -user|name    User name to log in with (Default: $USER)
47    -p|assword    Password to use (Default: prompted)
48    -i|map        IMAP server to talk to (Default: defaria.com)
49    -t|imeout <s> Timeout IMAP idle call (Sefault: 1200 seconds or 20 minutes)
50
51    -an|nounce    Announce startup (Default: False)
52    -ap|pend      Append to logfile (Default: Noappend)
53    -da|emon      Run in daemon mode (Default: -daemon)
54    -n|ame        Name of account (Default: imap)
55    -uses|sl      Whether or not to use SSL to connect (Default: False)
56    -useb|locking Whether to block on socket (Default: False)
57
58  Signals:
59    $SIG{USR1}:   Toggles debug option
60    $SIG{USR2}:   Reestablishes connection to IMAP server
61
62 =head1 DESCRIPTION
63
64 This script will connect to an IMAP server, login and then monitor the user's
65 INBOX. When new messages arrive it will extract the From address and Subject
66 from the message and compose a message to be used by "Google Talk" to announce
67 the email. The message will be similar to:
68
69   "<From> emailed <Subject>"
70
71 =cut
72
73 use strict;
74 use warnings;
75
76 use FindBin;
77 use Getopt::Long;
78 use Mail::IMAPTalk;
79 use MIME::Base64;
80 use Pod::Usage;
81 use Proc::ProcessTable;
82
83 use lib "$FindBin::Bin/../lib";
84
85 use Display;
86 use Logger;
87 use Speak;
88 use TimeUtils;
89 use Utils;
90
91 local $0 = "$FindBin::Script " . join ' ', @ARGV;
92
93 my $processes = Proc::ProcessTable->new;
94
95 for my $process (@{$processes->table}) {
96   if ($process->cmndline eq $0 and $process->pid != $$) { 
97     verbose "$FindBin::Script already running";
98
99     exit 0;
100   } # if
101 } # for
102
103 my $defaultIMAPServer = 'defaria.com';
104 my $IMAP;
105 my %unseen;
106 my $log;
107
108 my @greetings = (
109   'Incoming message',
110   'You have received a new message',
111   'Hey I found this in your inbox',
112   'For some unknown reason this guy send you a message',
113   'Did you know you just got a message',
114   'Potential spam',
115   'You received a communique',
116   'I was looking in your inbox and found a message',
117   'Not sure you want to hear this message',
118   'Good news',
119   "What's this? A new message",
120 );
121
122 my $icon          = '/home/andrew/.icons/Thunderbird.jpg';
123 my $notifyTimeout = 5 * 1000;
124 my $IMAPTimeout   = 20 * 60;
125
126 my %opts = (
127   usage       => sub { pod2usage },
128   help        => sub { pod2usage(-verbose => 2)},
129   verbose     => sub { set_verbose },
130   debug       => sub { set_debug },
131   daemon      => 1,
132   timeout     => $IMAPTimeout,
133   username    => $ENV{USER},
134   password    => $ENV{PASSWORD},
135   imap        => $defaultIMAPServer,
136 );
137
138 sub notify($) {
139   my ($msg) = @_;
140
141   my $cmd = "notify-send -i $icon -t $notifyTimeout '$msg'";
142
143   Execute $cmd;
144
145   return;
146 } # notify
147
148 sub interrupted {
149   if (get_debug) {
150     notify 'Turning off debugging';
151     set_debug 0;
152   } else {
153     notify ('Turning on debugging');
154     set_debug 1;
155   } # if
156
157   return;
158 } # interrupted
159
160 sub Connect2IMAP;
161 sub MonitorMail;
162
163 sub restart {
164   my $msg = "Re-establishing connection to $opts{imap} as $opts{username}";
165
166   $log->dbug($msg);
167
168   Connect2IMAP;
169
170   MonitorMail;
171 } # restart
172
173 $SIG{USR1} = \&interrupted;
174 $SIG{USR2} = \&restart;
175
176 sub unseenMsgs() {
177   $IMAP->select('inbox') or
178     $log->err("Unable to select inbox: " . get_last_error(), 1);
179
180   return map { $_=> 0 } @{$IMAP->search('not', 'seen')};
181 } # unseenMsgs 
182
183 sub Connect2IMAP() {
184   $log->dbug("Connecting to $opts{imap} as $opts{username}");
185
186   # Destroy any old connections
187   undef $IMAP;
188
189   $IMAP = Mail::IMAPTalk->new(
190     Server      => $opts{imap},
191     Username    => $opts{username},
192     Password    => $opts{password},
193     UseSSL      => $opts{usessl},
194     UseBlocking => $opts{useblocking},
195   ) or $log->err("Unable to connect to IMAP server $opts{imap}: $@", 1);
196
197   $log->dbug("Connected to $opts{imap} as $opts{username}");
198
199   # Focus on INBOX only
200   $IMAP->select('inbox');
201
202   # Setup %unseen to have each unseen message index set to 0 meaning not read
203   # aloud yet
204   %unseen = unseenMsgs;
205
206   return;
207 } # Connect2IMAP
208
209 sub MonitorMail() {
210   MONITORMAIL:
211   $log->dbug("Top of MonitorMail loop");
212
213   # First close and reselect the INBOX to get its current status
214   $IMAP->close;
215   $IMAP->select('INBOX')
216     or $log->err("Unable to select INBOX - ". $IMAP->errstr(), 1);
217
218   $log->dbug("Closed and reselected INBOX");
219   # Go through all of the unseen messages and add them to %unseen if they were
220   # not there already from a prior run and read
221   my %newUnseen = unseenMsgs;
222
223   # Now clean out any messages in %unseen that were not in the %newUnseen and
224   # marked as previously read
225   $log->dbug("Cleaning out unseen");
226   for (keys %unseen) {
227     if (defined $newUnseen{$_}) {
228       if ($unseen{$_}) {
229         delete $newUnseen{$_};
230       } # if
231     } else {
232       delete $unseen{$_}
233     } # if
234   } # for
235
236   $log->dbug("Processing new unseen messages");
237   for (keys %newUnseen) {
238     next if $unseen{$_};
239
240     my $envelope = $IMAP->fetch($_, '(envelope)');
241     my $from     = $envelope->{$_}{envelope}{From};
242     my $subject  = $envelope->{$_}{envelope}{Subject};
243        $subject //= 'Unknown subject';
244
245     # Extract the name only when the email is of the format "name <email>"
246     if ($from =~ /^"?(.*?)"?\s*\<(\S*)>/) {
247       $from = $1 if $1 ne '';
248     } # if
249
250     if ($subject =~ /=?\S+?(Q|B)\?(.+)\?=/) {
251       $subject = decode_base64($2);
252     } # if
253
254     # Google Talk doesn't like #
255     $subject =~ s/\#//g;
256
257     # Remove long strings of numbers like order numbers. They are uninteresting
258     my $longNumber = 5;
259     $subject =~ s/\s+\S*\d{$longNumber,}\S*\s*//g;
260
261     # Now speak it!
262     my $logmsg = "From $from $subject";
263
264     my $greeting = $greetings[int rand $#greetings];
265     my $msg      = "$greeting from $from... $subject";
266     my $hour     = (localtime)[2];
267
268     # Only announce if after 6 Am. Note this will announce up until
269     # midnight but that's ok. I want midnight to 6 Am as silent time.
270     $log->dbug("About to speak/log");
271     if ($hour >= 7) {
272       $log->msg($logmsg);
273       $log->dbug("Calling speak");
274       speak $msg, $log;
275     } else {
276       $log->msg("$logmsg [silent nighttime]");
277     } # if
278
279     $unseen{$_} = 1;
280   } # for
281
282   # Let's time things
283   my $startTime = time;
284
285   # Re-establish callback
286   $log->dbug("Calling IMAP->idle");
287   eval {
288     $IMAP->idle(\&MonitorMail, $opts{timeout})
289   };
290
291   my $msg = 'Returned from IMAP->idle ';
292
293   if ($@) {
294     speak($msg . $@, $log);
295   } else {
296     $log->msg($msg . 'no error');
297   } # if
298
299   # If we return from idle then the server went away for some reason. With Gmail
300   # the server seems to time out around 30-40 minutes. Here we simply reconnect
301   # to the imap server and continue to MonitorMail.
302   unless ($IMAP->get_response_code('timeout')) {
303     $msg = "IMAP Idle for $opts{name} timed out in " . howlong $startTime, time;
304
305     speak $msg;
306
307     $log->msg($msg);
308   } # unless
309
310   restart;
311 } # MonitorMail
312
313 END {
314   # If $log is not yet defined then the exit is not unexpected
315   if ($log) {
316     my $msg = "$FindBin::Script $opts{name} ending unexpectedly!";
317
318     speak $msg, $log;
319
320     $log->err($msg);
321   } # if
322 } # END
323
324 ## Main
325 GetOptions(
326   \%opts,
327   'usage',
328   'help',
329   'verbose',
330   'debug',
331   'daemon!',
332   'username=s',
333   'name=s',
334   'password=s',
335   'imap=s',
336   'timeout=i',
337   'usessl',
338   'useblocking',
339   'announce!',
340   'append',
341 ) || pod2usage;
342
343 unless ($opts{password}) {
344   verbose "I need $opts{username}'s password";
345   $opts{password} = GetPassword;
346 } # unless
347
348 $opts{name} //= $opts{imap};
349
350 if ($opts{username} =~ /.*\@(.*)$/) {
351   $opts{name} = $1;
352 } # if
353
354 if ($opts{daemon}) {
355   # Perl complains if we reference $DB::OUT only once
356   no warnings;
357   EnterDaemonMode unless defined $DB::OUT or get_debug;
358   use warnings;
359 } # if
360
361 $log = Logger->new(
362   path        => '/var/local/log',
363   name        => "$Logger::me.$opts{name}",
364   timestamped => 'yes',
365   append      => $opts{append},
366 );
367
368 Connect2IMAP;
369
370 if ($opts{username} =~ /(.*)\@/) {
371   $opts{user} = $1;
372 } else {
373   $opts{user} = $opts{username};
374 } # if
375
376 my $msg = "Now monitoring email for $opts{user}\@$opts{name}";
377
378 speak $msg, $log if $opts{announce};
379
380 $log->msg($msg);
381
382 MonitorMail;
383
384 # Should not get here
385 $log->err("Falling off the edge of $0", 1);