#!/usr/local/bin/perl -w

# RT3 reminder script
#
# This script will remind people about tickets they have not responded to
# or dealt with in a reasonable length of time. In its normal mode it will
# tell ticket owners about tickets which have exceeded a given priority
# (see the %pri hash below). It also has the ability to tell ticket owners
# about all their open or new tickets (-a), without or with stalled
# tickets (-A).
#
# This is best used with another script to escalate priorities automatically.
#
# The intended use is to run with no flags daily, with the -a flag weekly
# and with the -A flag monthly. Or similar.
#
# Written by T.D.Bishop@kent.ac.uk, July 2003.
# Updated for multiple queues, March 2004.
#   (using patches sent by jtucker@cyberfuse.com, October 2003)
# Updated for RT 3.2.x, November 2004.
#   (using patches sent by petter.reinholdtsen@usit.uio.no, August 2004)
# Updated to only display tickets with a passed or near start date, March 2005.
#   (using patches sent by ray@sysdev.oucs.ox.ac.uk)
# Updated to display string instead of empty subject, June 2005.
#   (using patches sent by pavel.ruzicka@i.cz)
# Whole bunch of updates, June 2005.
#   (using patches sent by Petter Reinholdtsen <pere@hungry.com>).
# Updates to remove warnings with RT 3.8.0, July 2008.
#
# $Id: rt-remind,v 1.9 2009/09/21 14:19:21 tdb Exp $


### Configuration

# Global mappings of users to addresses
# This overrides addresses in the RT database
# The most useful case is setting an address for Nobody
#
# Queue specific overrides can be done by using "queue,user"
my(%map) = (
    'Nobody' => 'YOU@YOURDOMAIN',
    'root' => 'YOU@YOURDOMAIN',
    'queue1,Nobody' => 'ANOTHER@YOURDOMAIN',
);

my(@queues); # do not delete this

# Queues to operate on (default is all)
#@queues = qw( queue1 queue2 );

# Whether to merge the tickets for all queues into one email per person
# (as opposed to sending an email per person per queue)
my($mergequeues) = 0;

# Each state can have a priority associated with it.
# When a ticket in that state passes the given priority it is added to
# the list to be sent out.
my(%pri) = (
    'new' => 2,
    'open' => 7,
);

# The length at which lines will be truncated. 78, or thereabouts looks
# best for most people. Setting to 0 will stop lines being truncated.
my($linelen) = 78;

# How many days in the future a ticket's start date should be before
# we ignore it (unless using -A).
my($futuredays) = 7*24*60*60;

# Location of RT3's libs
use lib ("/usr/local/packages/rt/lib", "/usr/local/packages/rt/local/lib");

# Address emails should originate from
my($from) = 'RT Reminder <YOU@YOURDOMAIN>';

# String to use when there is no subject
my($nosubject) = "(No subject)";

# charset to use in emails
my($charset) = "ISO-8859-15";

### Code

use strict;
use Carp;

my($showall) = 0;
my($showmost) = 0;

my($debug) = 0; # Enable debugging

# Process command line arguments
foreach(@ARGV) {
    if (/^-a$/) {
        $showmost = 1;
    }
    elsif (/^-A$/) {
        $showall = 1;
    }
    elsif (/^-d$/) {
        $debug = 1;
    }
    else {
        print STDERR "Usage: rt-remind [-a] [-A] [-h]\n";
        print STDERR "   -a  show all open or new tickets\n";
        print STDERR "   -A  show all open, new, stalled or future tickets\n";
        print STDERR "   -d  debug mode - do not send any emails\n";
        print STDERR "   -h  show this help\n";
        exit 1;
    }
}

# Pull in the RT stuff
package RT;
use RT::Interface::CLI qw(CleanEnv GetCurrentUser GetMessageContent loc);

# Clean our the environment
CleanEnv();

# Load the RT configuration
RT::LoadConfig();

# Initialise RT
RT::Init();

# Drop any setgid permissions we might have
#RT::DropSetGIDPermissions();

use RT::Date;
use RT::Queue;
use RT::Queues;
use RT::Tickets;

# Path to sendmail and flags. Need to set this after RT::Init()
my($sendmail) = RT->Config->Get('SendmailPath') . " " . RT->Config->Get('SendmailArguments');

my(%TicketStore) = ();

if(!@queues) {
    my $queues = new RT::Queues($RT::SystemUser);
    $queues->LimitToEnabled();
    foreach my $queue (@{$queues->ItemsArrayRef()}) {
        push @queues, $queue->Name;
    }
}

foreach my $queuename (@queues) {
    # Load in the queue
    my $queue = new RT::Queue($RT::SystemUser);
    $queue->Load($queuename);

    my $now = new RT::Date($RT::SystemUser);
    $now->SetToNow();

    # Get hold of the tickets we're after, sorted by priority
    my $tickets = new RT::Tickets($RT::SystemUser);
    $tickets->LimitStatus(VALUE => 'new');
    $tickets->LimitStatus(VALUE => 'open');
    $tickets->LimitStatus(VALUE => 'stalled') if $showall;
    $tickets->LimitQueue(VALUE => $queue->Id);
    $tickets->OrderBy(FIELD => 'Priority', ORDER => 'DESC');
    my $user = RT::User->new($RT::SystemUser);

    %TicketStore = () unless $mergequeues;

    # Sort tickets in to lists for each owner
    while (my $Ticket = $tickets->Next) {
        $user->Load($Ticket->Owner);

        my(@emails) = &getContacts($Ticket, $user, $queuename);
        foreach my $email (@emails) {
            push @{$TicketStore{$email}}, $Ticket;
        }
    }

    &doMessages($queuename, %TicketStore) unless $mergequeues;
}

&doMessages("All", %TicketStore) if $mergequeues;

# Disconnect before we finish off
$RT::Handle->Disconnect();
exit 0;


sub getGroupMemberAddresses {
    my ($groupname) = @_;

    print "Looking up members of group '$groupname'\n" if ($debug);

    use RT::Group;
    my @emails = ();
    my ($Group) = new RT::Group($RT::SystemUser);
    $Group->LoadUserDefinedGroup($groupname);
    if (defined $Group->Id && $Group->MembersObj->Count != 0) {
        my ($UserMembers) = $Group->MembersObj;

        # XXX Should we limit it to users, or recursively expand
        # groups as well?  Only listing first order users for now.
        # $GroupMembers->LimitToGroups();
        $UserMembers->LimitToUsers();

        while (my $member = $UserMembers->Next()) {
            my ($address) = $member->MemberObj->Object->EmailAddress;
            if ($address) {
                my ($realname) = $member->MemberObj->Object->RealName;
                my ($a) = "\"$realname\" <$address>";
                print "  Found '$a'\n" if $debug;
                push(@emails, $a);
            }
        }
        return join(",", @emails) if @emails;
    }
    return undef;
}

#
# Look up who to send reminder to for a given ticket. The mail
# address is constructed based on ticket owner and queue name.
#
sub getContacts {
    my ($ticket, $owner, $queuename) = @_;

    # Try various methods to look up the contacts

    # Look for queue-specific mapping
    my($email) = $map{$queuename.",".$owner->Name};

    # Check the <queuename>-owners RT group
    if (!defined $email) {
        $email = &getGroupMemberAddresses("$queuename-owners");
    }

    # Look for mapping (non queue-specific)
    if (!defined $email) {
        $email = $map{$owner->Name};
    }

    # Look at ticket owner
    if (!defined $email) {
        my $address = $owner->EmailAddress;
        if (defined $address) {
            my $realname = $owner->RealName;
            $email = "\"$realname\" <$address>";
        }
    }

    # If we still can't find anyone, try mapping for root
    if (!defined $email) {
        $email = $map{'root'};
    }

    return split(/\s*,\s*/, $email);
}

# Subroutine to generate messages
sub doMessages() {
    my($queuename, %TicketStore) = @_;
    foreach my $email (sort keys %TicketStore) {
        my($t) =  $TicketStore{$email};
        &doOwner($email, $queuename, @$t);
    }
}

# Subroutine to generate a messages for a given owner and set of tickets.
sub doOwner() {
    my($email, $queuename, @tickets) = @_;
    my($subject);
    if($queuename eq "All") {
        $subject = "Outstanding RT tickets";
    }
    else {
        $subject = "Outstanding RT tickets for queue '$queuename'";
    }
    my($msg);
    $msg .= "Content-Type: text/plain; charset=\"$charset\"\n";
    $msg .= "From: $from\n";
    $msg .= "To: $email\n";
    $msg .= "Subject: $subject\n";
    $msg .= "\n";
    if($showall) {
        $msg .= "This is a summary of all open, new, stalled or future tickets assigned to:\n";
    }
    elsif($showmost) {
        $msg .= "This is a summary of all open or new tickets assigned to:\n";
    }
    else {
        $msg .= "These tickets are now considered important and are assigned to:\n";
    }
    $msg .= "\n";
    $msg .= "  $email\n";
    $msg .= "\n";
    # Generate heading
    $msg .= sprintf "%5s  %-7s %3s  %-13s %-30s\n", "Id", "Status", "Pri", "Created", "Subject";
    # We might not want to print the message if there are no tickets
    my($printmsg) = 0;
    # Look through tickets, sorted by priority, highest first
    foreach my $Ticket (sort {$b->Priority <=> $a->Priority} @tickets) {
        my($starts) = new RT::Date($RT::SystemUser);
        $starts->Set(Format => 'sql', Value => $Ticket->Starts);
        # Only add ticket if it's over the priority, or we're in -a or -A
        # AND the ticket start date has passed or is in the next
        # $futuredays days, or we're in -A
        my($showfuture) = 1;
        if(defined $starts->Diff(time)) {
            $showfuture = $starts->Diff(time) <= $futuredays;
        }
        if(($Ticket->Priority > $pri{$Ticket->Status} || $showmost || $showall)
          && ($showfuture || $showall)) {
            $printmsg = 1;
            # Use our own date formatting routine
            my($date) = &formatDate($Ticket->CreatedObj->Unix);
            my($subject) = $Ticket->Subject ? $Ticket->Subject : $nosubject;
            my($line) = sprintf "%5d  %-7s %3d  %-13s %-30s", $Ticket->Id, $Ticket->Status, $Ticket->Priority, $date, $subject;
            # Truncate lines if required
            if($linelen) {
                $line = substr($line, 0, $linelen);
            }
            $msg .= "$line\n";
        }
    }
    $msg .= "\n" . RT->Config->Get('WebURL') . "\n";
    # Send the message
    if($printmsg) {
        if ($debug) {
            print "====== Would call '$sendmail' with this input:\n";
            print "$msg\n\n";
        }
        else {
            open(SENDMAIL, "|$sendmail") || die "Error sending mail: $!";
            print SENDMAIL $msg;
            close(SENDMAIL);
        }
    }
}

# Formats a date like: Thu 10-07-03
# Designed to be consice yet useful
sub formatDate() {
    my($unixtime) = @_;
    my(@days) = ( "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" );
    # Return an empty string if we haven't been given a time
    return "" if $unixtime <= 0;
    my($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime($unixtime);
    return sprintf "%s %02d-%02d-%02d", $days[$wday], $mday, $mon+1, $year%100;
}
