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