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