22d30d01e5a9f1897ec15fa05d1992686c02c8bc
[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: 600 seconds or 10 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 Pod::Usage;
79 use Mail::IMAPTalk;
80 use MIME::Base64;
81
82 use lib "$FindBin::Bin/../lib";
83
84 use Display;
85 use Logger;
86 use Speak;
87 use TimeUtils;
88 use Utils;
89
90 local $0 = "$FindBin::Script " . join ' ', @ARGV;
91
92 my $defaultIMAPServer = 'defaria.com';
93 my $IMAP;
94 my %unseen;
95 my $log;
96
97 my @greetings = (
98   'Incoming message',
99   'You have received a new message',
100   'Hey I found this in your inbox',
101   'For some unknown reason this guy send you a message',
102   'Did you know you just got a message',
103   'Potential spam',
104   'You received a communique',
105   'I was looking in your inbox and found a message',
106   'Not sure you want to hear this message',
107   'Good news',
108   "What's this? A new message",
109 );
110
111 my $icon    = '/home/andrew/.icons/Thunderbird.jpg';
112 my $timeout = 5 * 1000;
113
114 my %opts = (
115   usage       => sub { pod2usage },
116   help        => sub { pod2usage(-verbose => 2)},
117   verbose     => sub { set_verbose },
118   debug       => sub { set_debug },
119   daemon      => 1,
120   timeout     => 600, # 10 minutes
121   username    => $ENV{USER},
122   password    => $ENV{PASSWORD},
123   imap        => $defaultIMAPServer,
124 );
125
126 sub notify($) {
127   my ($msg) = @_;
128
129   my $cmd = "notify-send -i $icon -t $timeout '$msg'";
130
131   Execute $cmd;
132
133   return;
134 } # notify
135
136 sub interrupted {
137   if (get_debug) {
138     notify 'Turning off debugging';
139     set_debug 0;
140   } else {
141     notify ('Turning on debugging');
142     set_debug 1;
143   } # if
144
145   return;
146 } # interrupted
147
148 sub Connect2IMAP;
149
150 #sub restart {
151 #  $log->dbug("Re-establishing connection to $opts{imap} as $opts{username}");
152 #
153 #  Connect2IMAP;
154 #
155 #  goto MONITORMAIL;
156 #} # restart
157
158 $SIG{USR1} = \&interrupted;
159 #$SIG{USR2} = \&restart;
160
161 sub unseenMsgs() {
162   $IMAP->select('inbox') or
163     $log->err("Unable to select inbox: " . get_last_error(), 1);
164
165   return map { $_=> 0 } @{$IMAP->search('not', 'seen')};
166 } # unseenMsgs 
167
168 sub Connect2IMAP() {
169   $log->dbug("Connecting to $opts{imap} as $opts{username}");
170
171   # Destroy any old connections
172   undef $IMAP;
173
174   $IMAP = Mail::IMAPTalk->new(
175     Server      => $opts{imap},
176     Username    => $opts{username},
177     Password    => $opts{password},
178     UseSSL      => $opts{usessl},
179     UseBlocking => $opts{useblocking},
180   ) or $log->err("Unable to connect to IMAP server $opts{imap}: $@", 1);
181
182   $log->dbug("Connected to $opts{imap} as $opts{username}");
183
184   # Focus on INBOX only
185   $IMAP->select('inbox');
186
187   # Setup %unseen to have each unseen message index set to 0 meaning not read
188   # aloud yet
189   %unseen = unseenMsgs;
190
191   return;
192 } # Connect2IMAP
193
194 sub MonitorMail() {
195 #  MONITORMAIL:
196   $log->dbug("Top of MonitorMail loop");
197
198   # First close and reselect the INBOX to get its current status
199   $IMAP->close;
200   $IMAP->select('INBOX')
201     or $log->err("Unable to select INBOX - ". $IMAP->errstr(), 1);
202
203   $log->dbug("Closed and reselected INBOX");
204   # Go through all of the unseen messages and add them to %unseen if they were
205   # not there already from a prior run and read
206   my %newUnseen = unseenMsgs;
207
208   # Now clean out any messages in %unseen that were not in the %newUnseen and
209   # marked as previously read
210   $log->dbug("Cleaning out unseen");
211   for (keys %unseen) {
212     if (defined $newUnseen{$_}) {
213       if ($unseen{$_}) {
214         delete $newUnseen{$_};
215       } # if
216     } else {
217       delete $unseen{$_}
218     } # if
219   } # for
220
221   $log->dbug("Processing new unseen messages");
222   for (keys %newUnseen) {
223     next if $unseen{$_};
224
225     my $envelope = $IMAP->fetch($_, '(envelope)');
226     my $from     = $envelope->{$_}{envelope}{From};
227     my $subject  = $envelope->{$_}{envelope}{Subject};
228        $subject //= 'Unknown subject';
229
230     # Extract the name only when the email is of the format "name <email>"
231     if ($from =~ /^"?(.*?)"?\s*\<(\S*)>/) {
232       $from = $1 if $1 ne '';
233     } # if
234
235     if ($subject =~ /=?\S+?(Q|B)\?(.+)\?=/) {
236       $subject = decode_base64($2);
237     } # if
238
239     # Google Talk doesn't like #
240     $subject =~ s/\#//g;
241
242     # Remove long strings of numbers like order numbers. They are uninteresting
243     my $longNumber = 5;
244     $subject =~ s/\s+\S*\d{$longNumber,}\S*\s*//g;
245
246     # Now speak it!
247     my $logmsg = "From $from $subject";
248
249     my $greeting = $greetings[int rand $#greetings];
250     my $msg      = "$greeting from $from... $subject";
251        $msg      =~ s/\"/\\"/g;
252
253     my $hour = (localtime)[2];
254
255     # Only announce if after 6 Am. Note this will announce up until
256     # midnight but that's ok. I want midnight to 6 Am as silent time.
257     $log->dbug("About to speak/log");
258     if ($hour >= 7) {
259       $log->dbug("Calling speak");
260       speak $msg, $log;
261       $log->msg($logmsg);
262     } else {
263       $log->msg("$logmsg [silent]");
264     } # if
265
266     $unseen{$_} = 1;
267   } # for
268
269   # Let's time things
270   my $startTime = time;
271
272   # Re-establish callback
273   $log->dbug("Evaling idle");
274   eval { $IMAP->idle(\&MonitorMail, $opts{timeout}) };
275
276   $log->err("Unable to set IMAP Idle - AS $@", 1) if $@;
277   $log->msg("IMAP Idle for $opts{name} timed out in " . howlong $startTime, time);
278
279   # If we return from idle then the server went away for some reason. With Gmail
280   # the server seems to time out around 30-40 minutes. Here we simply return
281   # back to main which will re-establish the connection and call us again.
282   unless ($IMAP->get_response_code('timeout')) {
283     my $errstr = $IMAP->get_last_error;
284
285     $log->dbug("$opts{name} went away - $errstr");
286   } # unless
287
288   return 0;
289 } # MonitorMail
290
291 END {
292   # If $log is not yet defined then the exit is not unexpected
293   if ($log) {
294     my $msg = "$FindBin::Script $opts{name} ended unexpectedly!";
295
296     speak $msg, $log;
297
298     $log->err($msg);
299   } # if
300 } # END
301
302 ## Main
303 GetOptions(
304   \%opts,
305   'usage',
306   'help',
307   'verbose',
308   'debug',
309   'daemon!',
310   'username=s',
311   'name=s',
312   'password=s',
313   'imap=s',
314   'timeout=i',
315   'usessl',
316   'useblocking',
317   'announce!',
318   'append',
319 ) || pod2usage;
320
321 unless ($opts{password}) {
322   verbose "I need $opts{username}'s password";
323   $opts{password} = GetPassword;
324 } # unless
325
326 $opts{name} //= $opts{imap};
327
328 if ($opts{username} =~ /.*\@(.*)$/) {
329   $opts{name} = $1;
330 } # if
331
332 if ($opts{daemon}) {
333   # Perl complains if we reference $DB::OUT only once
334   no warnings;
335   EnterDaemonMode unless defined $DB::OUT or get_debug;
336   use warnings;
337 } # if
338
339 $log = Logger->new(
340   path        => '/var/local/log',
341   name        => "$Logger::me.$opts{name}",
342   timestamped => 'yes',
343   append      => $opts{append},
344 );
345
346 if ($opts{username} =~ /(.*)\@/) {
347   $opts{user} = $1;
348 } else {
349   $opts{user} = $opts{username};
350 } # if
351
352 my $msg = "Now monitoring email for $opts{user}\@$opts{name}";
353
354 # Changed to loop here - better than using a goto. This kinda kills the idea of
355 # using siguser2 to interrupt announceEmail.pl to kick it into re-establishing 
356 # the connection.
357 while () {
358   Connect2IMAP;
359
360   speak $msg, $log if $opts{announce};
361
362   $log->msg($msg);
363
364   MonitorMail;
365
366   $log->dbug("$opts{name} timed out! Re-establishing connection");
367 } # while
368
369 # Should not get here
370 $log->err("Falling off the edge of $0", 1);