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