Upgrade to 4.2.8
[usit-rt.git] / local / bin / rt-remind
1 #!/usr/bin/perl
2
3 # RT reminder script
4 #
5 # This script will remind people about tickets they have not responded to
6 # or dealt with in a reasonable length of time. In its normal mode it will
7 # tell ticket owners about tickets which have exceeded a given priority
8 # (see the %pri hash below). It also has the ability to tell ticket owners
9 # about all their open or new tickets (-a), without or with stalled
10 # tickets (-A).
11 #
12 # This is best used with another script to escalate priorities automatically.
13 #
14 # The intended use is to run with no flags daily, with the -a flag weekly
15 # and with the -A flag monthly. Or similar.
16 #
17 # Written by T.D.Bishop@kent.ac.uk, July 2003.
18 # Updated for multiple queues, March 2004.
19 #   (using patches sent by jtucker@cyberfuse.com, October 2003)
20 # Updated for RT 3.2.x, November 2004.
21 #   (using patches sent by petter.reinholdtsen@usit.uio.no, August 2004)
22 # Updated to only display tickets with a passed or near start date, March 2005.
23 #   (using patches sent by ray@sysdev.oucs.ox.ac.uk)
24 #
25 # Copied from <URL:http://www.cs.kent.ac.uk/people/staff/tdb/rtdev/> and
26 # modified to fit local needs.
27 #  - Petter Reinholdtsen 2004-08-31, 2005-05-19
28
29
30 ### Configuration
31
32 # Global mappings of users to addresses
33 # This overrides addresses in the RT database
34 # The most useful case is setting an address for Nobody
35 #
36 # Queue specific overrides can be done by using "queue,user"
37
38 my(%map) = (
39    'Nobody'              => 'RT Nobody <rt-drift@usit.uio.no>',
40     'root'               => 'RT root <rt-drift@usit.uio.no>',
41 );
42
43 my(@queues); # do not delete this
44
45 # Queues to operate on
46 #@queues = qw( queue1 queue2 );
47
48 # Whether to merge the tickets for all queues into one email per person
49 # (as opposed to sending an email per person per queue)
50 my($mergequeues) = 1;
51
52 # Each state can have a priority associated with it.
53 # When a ticket in that state passes the given priority it is added to
54 # the list to be sent out.
55 my(%pri) = (
56     'new'  => 40,
57     'open' => 50,
58 );
59
60 # The length at which lines will be truncated. 80, or thereabouts looks
61 # best for most people. Setting to 0 will stop lines being truncated.
62 my($linelen) = 78;
63
64 # How many days in the future a ticket's start date should be before
65 # we ignore it (unless using -A).
66 my($futuredays) = 7*24*60*60;
67
68 # Location of RT's libs and scripts
69 # Remember to change to correct path on current RT instance
70 use lib '/www/data/rt/perl/share/perl5';
71 use lib '/www/data/rt/perl/lib/perl5';
72 use lib '/www/data/rt/perl/lib64/perl5';
73 use lib ("/www/var/rt/local/lib", "/www/var/rt/lib");
74
75
76 # Address emails should originate from
77 my($from) = 'RT reminder <request-tracker@hjelp.uio.no>';
78
79 ### Code
80
81 use strict;
82 use Carp;
83 use HTML::Entities;
84
85 my($showall) = 0;
86 my($showmost) = 0;
87
88 my $debug = 0; # Enable debugging
89
90 # Process command line arguments
91 foreach(@ARGV) {
92     if (/^-a$/) {
93         $showmost = 1;
94     }
95     elsif (/^-A$/) {
96         $showall = 1;
97     }
98     elsif (/^-d$/) {
99         $debug = 1;
100     }
101     else {
102         print STDERR "Usage: rt-remind [-a] [-A] [-h]\n";
103         print STDERR "   -a  show all open or new tickets\n";
104         print STDERR "   -A  show all open, new, stalled or future tickets\n";
105         print STDERR "   -d  do not send any emails.  debug mode only\n";
106         print STDERR "   -h  show this help\n";
107         exit 1;
108     }
109 }
110
111 # Pull in the RT stuff
112 package RT;
113 use RT::Interface::CLI qw(CleanEnv);
114
115 # Clean our the environment
116 CleanEnv();
117
118 # Load the RT configuration
119 RT::LoadConfig();
120
121 # Initialise RT
122 RT::Init();
123
124 # Drop any setgid permissions we might have
125 #RT::DropSetGIDPermissions();
126
127 use RT::Date;
128 use RT::Queue;
129 use RT::Queues;
130 use RT::Tickets;
131
132 # Path to sendmail and flags.  Need to set this after RT::Init()
133 my($sendmail) = "$RT::SendmailPath $RT::SendmailArguments -t";
134
135 my(%TicketStore) = ();
136
137 if(!@queues) {
138     my $queues = new RT::Queues($RT::SystemUser);
139     $queues->LimitToEnabled();
140     foreach my $queue (@{$queues->ItemsArrayRef()}) {
141         push @queues, $queue->Name;
142     }
143 }
144
145 foreach my $queuename (@queues) {
146     # Load in the queue
147     my $queue = new RT::Queue($RT::SystemUser);
148     $queue->Load($queuename);
149
150     my $now = new RT::Date($RT::SystemUser);
151     $now->SetToNow();
152
153     # Get hold of the tickets we're after, sorted by priority
154     my $tickets = new RT::Tickets($RT::SystemUser);
155     $tickets->LimitStatus(VALUE => 'new');
156     $tickets->LimitStatus(VALUE => 'open');
157     $tickets->LimitStatus(VALUE => 'stalled') if $showall;
158     $tickets->LimitQueue(VALUE => $queue->Id);
159     $tickets->OrderBy(FIELD => 'Priority', ORDER => 'DESC');
160     my $user = RT::User->new($RT::SystemUser);
161
162     %TicketStore = () unless $mergequeues;
163
164     # Sort tickets in to lists for each owner
165     while (my $Ticket = $tickets->Next) {
166         $user->Load($Ticket->Owner);
167         my(@emails) = getContacts($Ticket, $user, $queuename);
168
169         # If ticket is over due and owned by someone other than
170         # Nobody, remind the queue owners as well.
171         if ("Nobody" ne $user->Name and
172             $Ticket->DueObj->Unix > 0 and
173             $Ticket->DueObj->Unix < time()) {
174
175             $user->Load("Nobody");
176             push(@emails, getContacts($Ticket, $user, $queuename))
177         }
178         my %gotit = ();
179         for my $address (@emails) {
180
181             push @{$TicketStore{$address}}, $Ticket
182                 unless (exists $gotit{$address});
183             $gotit{$address} = 1;
184         }
185     }
186
187     &doMessages($queuename, %TicketStore) unless $mergequeues;
188 }
189
190 &doMessages("All", %TicketStore) if $mergequeues;
191
192 # Disconnect before we finish off
193 $RT::Handle->Disconnect();
194 exit 0;
195
196 sub getGroupMemberAddresses {
197     my ($Ticket, $owner, $groupname) = @_;
198
199     print "Looking up members of group '$groupname' for ".$Ticket->id."\n"
200         if ($debug);
201
202     use RT::Group;
203     my @emails = ();
204     my $Group = new RT::Group($RT::SystemUser);
205     if ($Group->LoadUserDefinedGroup($groupname) &&
206         $Group->MembersObj->Count != 0 ) {
207
208         # Expand recursively all users in the group
209         my $UserMembers = $Group->UserMembersObj;
210
211         while (my $member = $UserMembers->Next()) {
212             my $address =  $member->EmailAddress;
213             if ($address) {
214                 my $realname = $member->RealName;
215                 my $a = "\"$realname\" <$address>";
216                 print "  Found '$a'\n" if ($debug);
217                 push(@emails, $a);
218             }
219         }
220         return join(",", @emails) if @emails;
221     }
222     return undef;
223 }
224
225 #
226 # Look up who to send reminder to for a given ticket.  The mail
227 # address is constructed based on ticket owner and queue name.
228 #
229 sub getContacts {
230     my ($Ticket, $owner, $queuename) = @_;
231
232     my($email) = $map{$queuename.",".$owner->Name};
233     print "Using contacts from \$map{$queuename,".$owner->Name."} for ".
234         $Ticket->Id."\n" if ($debug && $email);
235     if ((not defined $email) && ("Nobody" eq $owner->Name)) {
236         # check the <queuename>-owners group if the ticket have no owner
237         $email =
238             getGroupMemberAddresses($Ticket, $owner, "$queuename-owners");
239         print "Using contacts from group '$queuename-owners' for ".
240             $Ticket->Id."\n" if ($debug && $email);
241     }
242     $email = $map{$owner->Name} if not defined $email;
243     if (not defined $email) {
244         my $address = $owner->EmailAddress;
245         if ($address) {
246             my $realname = $owner->RealName;
247             $email = "\"$realname\" <$address>";
248         }
249     }
250     $email = $map{'root'} if not defined $email;
251
252     return split(/\s*,\s*/, $email);
253 }
254
255 # Subroutine to generate messages
256 sub doMessages() {
257     my($queuename, %TicketStore) = @_;
258     foreach my $email (sort keys %TicketStore) {
259         my($t) =  $TicketStore{$email};
260         &doOwner($email, $queuename, @$t);
261     }
262 }
263
264 # Subroutine to generate a messages for a given owner and set of tickets.
265 sub doOwner() {
266     my($email, $queuename, @tickets) = @_;
267     my $user = RT::User->new($RT::SystemUser);
268     my $queue = RT::Queue->new($RT::SystemUser);
269     my $subject;
270     if ("All" eq $queuename) {
271         $subject = "Pending tickets in RT";
272     } else {
273         $subject = "Pending tickets for queue '$queuename' in RT";
274     }
275     my($body,$body_html);
276         $body_html .= "<p>";
277
278     if($showall) {
279         $body .= "This is a summary of all open, new or stalled tickets assigned to:\n";
280         $body_html .= "This is a summary of all open, new or stalled tickets assigned to:<br>\n";
281     }
282     elsif($showmost) {
283         $body .= "This is a summary of all open or new tickets assigned to:\n";
284         $body_html .= "This is a summary of all open or new tickets assigned to:<br>\n";
285     }
286     else {
287         $body .= "These tickets are now considered important and are assigned to:\n";
288         $body_html .= "These tickets are now considered important and are assigned to:<br>\n";
289     }
290     $body .= "\n  $email\n\n";
291     my $email_html = $email;
292     $email_html =~ s/(\<|\>)//g;
293     $body_html .= "<br>&nbsp;&nbsp;".stripHTML($email)."<br><br>\n";
294     # Generate heading
295     $body .= sprintf "%6s %4s %3s %-8s %-8s %-10.10s %-30s\n", "Id", "Stat", "Pri", "Created", "Owner", "Queue", "Subject";
296     $body_html .= "<\p>";
297     $body_html .= "<table>\n";
298     $body_html .= "<tr style=\"background-color:lightgray\">\n";
299     $body_html .= "<td style=\"text-align:right\">Id</td><td>Stat</td><td>Pri</td><td>Created</td><td>Owner</td><td>Queue</td><td>Subject</td>\n";
300     $body_html .= "</tr>\n";
301
302     # We might not want to print the message if there are no tickets
303     my($printmsg) = 0;
304     # Look through tickets, sorted by priority, highest first
305     foreach my $Ticket (sort {$b->Priority <=> $a->Priority } @tickets) {
306         my($starts) = new RT::Date($RT::SystemUser);
307         $starts->Set(Format => 'sql', Value => $Ticket->Starts);
308         # Only add ticket if it's over the priority, or we're in -a or -A
309         # AND the ticket start date has passed or is in the next
310         # $futuredays days, or we're in -A
311         if(($Ticket->Priority > $pri{$Ticket->Status} || $showmost || $showall)
312           && ($starts->Diff(time) <= $futuredays || $showall)) {
313             $printmsg = 1;
314             # Use our own date formatting routine
315             my($date) = &formatDate($Ticket->CreatedObj->Unix);
316             $user->Load($Ticket->Owner);
317             $queue->Load($Ticket->Queue);
318             my($line) = sprintf "%6d %-4s %-3d %8s %-8s %-10.10s %-30s", $Ticket->Id, $Ticket->Status, $Ticket->Priority, $date, $user->Name, $queue->Name, $Ticket->Subject;
319             my($line_html) = sprintf "<td><a href=$RT::WebURL%d>%d</a></td><td>%s</td><td>%d</td><td>%s</td><td>%s</td><td>%s</td><td>%s</td><td>", $Ticket->Id, $Ticket->Id, $Ticket->Status, $Ticket->Priority, $date, $user->Name, $queue->Name, $Ticket->Subject;
320             # Truncate lines if required
321             if($linelen) {
322                 $line = substr($line, 0, $linelen);
323             }
324             $body .= "$line\n";
325             $body_html .= "<tr>\n";
326             $body_html .= "$line_html\n";
327             $body_html .= "</tr>\n";
328         }
329     }
330     $body_html .= "</table>";
331
332     my $boundry = "===".time()."===";
333     my ($msg);
334     $msg .= "From: $from\n";
335     $msg .= "To: $email\n";
336     $msg .= "Subject: $subject\n";
337     $msg .= "MIME-Version: 1.0\n";
338     $msg .= "Content-Type: multipart/alternative; boundary=\"$boundry\"\n";
339     $msg .= "\n";
340     $msg .= "Dersom du kan lese dette taklar ikkje epostlesaren din rfc2046.\n\n";
341     $msg .= "--$boundry\n";
342     $msg .= "Content-Type: text/plain; charset=\"iso-8859-1\"\n\n";
343     $msg .= "$body\n";
344     $msg .= "$RT::WebURL\n";
345     $msg .= "--$boundry\n";
346     $msg .= "Content-Type: text/html; charset=\"iso-8859-1\"\n\n";
347     $msg .= "<html>\n";
348     $msg .= "$body_html\n";
349     $msg .= "<p><a href=\"$RT::WebURL\">$RT::WebURL</a></p>\n";
350     $msg .= "</html>\n";
351     $msg .= "--$boundry--\n";
352     # Send the message
353     if($printmsg) {
354         if ($debug) {
355             print "====== Would call '$sendmail' with this input:\n$msg\n\n";
356         } else {
357             open(SENDMAIL, "|$sendmail") || die "Error sending mail: $!";
358             print SENDMAIL $msg;
359             close(SENDMAIL);
360         }
361     }
362 }
363
364 # Formats a date like: Thu 10-07-03
365 # Designed to be consice yet useful
366 sub formatDate() {
367     my($unixtime) = @_;
368     # Return an empty string if we haven't been given a time
369     return "" if $unixtime <= 0;
370     my($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime($unixtime);
371     return sprintf "%02d-%02d-%02d", $mday, $mon+1, $year%100;
372 }
373
374 sub stripHTML() {
375 my $s = shift;
376 $s =~ s/\>/&gt;/g;
377 $s =~ s/\</&lt;/g;
378 return $s;
379 }