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