rt 4.2.15
[freeside.git] / rt / bin / rt
index d9f8a3f..1757d08 100755 (executable)
--- a/rt/bin/rt
+++ b/rt/bin/rt
@@ -1,36 +1,74 @@
 #!/usr/bin/perl -w
-# BEGIN LICENSE BLOCK
-# 
-# Copyright (c) 1996-2003 Jesse Vincent <jesse@bestpractical.com>
-# 
-# (Except where explictly superceded by other copyright notices)
-# 
+# BEGIN BPS TAGGED BLOCK {{{
+#
+# COPYRIGHT:
+#
+# This software is Copyright (c) 1996-2018 Best Practical Solutions, LLC
+#                                          <sales@bestpractical.com>
+#
+# (Except where explicitly superseded by other copyright notices)
+#
+#
+# LICENSE:
+#
 # This work is made available to you under the terms of Version 2 of
 # the GNU General Public License. A copy of that license should have
 # been provided with this software, but in any event can be snarfed
 # from www.gnu.org.
-# 
+#
 # This work is distributed in the hope that it will be useful, but
 # WITHOUT ANY WARRANTY; without even the implied warranty of
 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 # General Public License for more details.
-# 
-# Unless otherwise specified, all modifications, corrections or
-# extensions to this work which alter its source code become the
-# property of Best Practical Solutions, LLC when submitted for
-# inclusion in the work.
-# 
-# 
-# END LICENSE BLOCK
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301 or visit their web page on the internet at
+# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+#
+#
+# CONTRIBUTION SUBMISSION POLICY:
+#
+# (The following paragraph is not intended to limit the rights granted
+# to you to modify and distribute this software under the terms of
+# the GNU General Public License and is only of importance to you if
+# you choose to contribute your changes and enhancements to the
+# community by submitting them to Best Practical Solutions, LLC.)
+#
+# By intentionally submitting any modifications, corrections or
+# derivatives to this work, or any other work intended for use with
+# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+# you are the copyright holder for those contributions and you grant
+# Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
+# royalty-free, perpetual, license to use, copy, create derivative
+# works based on those contributions, and sublicense and distribute
+# those contributions and any derivatives thereof.
+#
+# END BPS TAGGED BLOCK }}}
+# Designed and implemented for Best Practical Solutions, LLC by
+# Abhijit Menon-Sen <ams@wiw.org>
 
 use strict;
+use warnings;
+
+if ( $ARGV[0] && $ARGV[0] =~ /^(?:--help|-h)$/ ) {
+    require Pod::Usage;
+    print Pod::Usage::pod2usage( { verbose => 2 } );
+    exit;
+}
 
 # This program is intentionally written to have as few non-core module
 # dependencies as possible. It should stay that way.
 
 use Cwd;
 use LWP;
+use Text::ParseWords;
 use HTTP::Request::Common;
+use HTTP::Headers;
+use Term::ReadLine;
+use Time::Local; # used in prettyshow
+use File::Temp;
 
 # We derive configuration information from hardwired defaults, dotfiles,
 # and the RT* environment variables (in increasing order of precedence).
@@ -42,28 +80,40 @@ my $HOME = eval{(getpwuid($<))[7]}
            || ".";
 my %config = (
     (
-        debug   => 0,
-        user    => eval{(getpwuid($<))[0]} || $ENV{USER} || $ENV{USERNAME},
-        passwd  => undef,
-        server  => 'http://localhost/rt/',
+        debug        => 0,
+        user         => eval{(getpwuid($<))[0]} || $ENV{USER} || $ENV{USERNAME},
+        passwd       => undef,
+        server       => 'http://localhost/',
+        query        => "Status!='resolved' and Status!='rejected'",
+        orderby      => 'id',
+        queue        => undef,
+# to protect against unlimited searches a better choice would be
+#       queue        => 'Unknown_Queue',
+        auth         => "rt",
     ),
     config_from_file($ENV{RTCONFIG} || ".rtrc"),
     config_from_env()
 );
-my $session = new Session("$HOME/.rt_sessions");
+
+$config{auth} = "basic" if delete $config{externalauth};
+
+my $session = Session->new("$HOME/.rt_sessions");
 my $REST = "$config{server}/REST/1.0";
 
+my $prompt = 'rt> ';
+
 sub whine;
 sub DEBUG { warn @_ if $config{debug} >= shift }
 
 # These regexes are used by command handlers to parse arguments.
 # (XXX: Ask Autrijus how i18n changes these definitions.)
 
-my $name   = '[\w.-]+';
-my $field  = '[a-zA-Z][a-zA-Z0-9_-]*';
-my $label  = '[a-zA-Z0-9@_.+-]+';
-my $labels = "(?:$label,)*$label";
-my $idlist = '(?:(?:\d+-)?\d+,)*(?:\d+-)?\d+';
+my $name    = '[\w.-]+';
+my $CF_name = '[^,]+?';
+my $field   = '(?i:[a-z][a-z0-9_-]*|C(?:ustom)?F(?:ield)?-'.$CF_name.'|CF\.\{'.$CF_name.'\})';
+my $label   = '[^,\\/]+';
+my $labels  = "(?:$label,)*$label";
+my $idlist  = '(?:(?:\d+-)?\d+,)*(?:\d+-)?\d+';
 
 # Our command line looks like this:
 #
@@ -75,6 +125,7 @@ my $idlist = '(?:(?:\d+-)?\d+,)*(?:\d+-)?\d+';
 my %handlers = (
 #   handler     => [ ...aliases... ],
     version     => ["version", "ver"],
+    shell       => ["shell"],
     logout      => ["logout"],
     help        => ["help", "man"],
     show        => ["show", "cat"],
@@ -84,47 +135,86 @@ my %handlers = (
     link        => ["link", "ln"],
     merge       => ["merge"],
     grant       => ["grant", "revoke"],
+    take        => ["take", "steal", "untake"],
+    quit        => ["quit", "exit"],
+    setcommand  => ["del", "delete", "give", "res", "resolve",
+                    "subject"],
 );
 
-# Once we find and call an appropriate handler, we're done.
-
-my (%actions, $action);
+my %actions;
 foreach my $fn (keys %handlers) {
     foreach my $alias (@{ $handlers{$fn} }) {
         $actions{$alias} = \&{"$fn"};
     }
 }
-if (@ARGV && exists $actions{$ARGV[0]}) {
-    $action = shift @ARGV;
+
+# Once we find and call an appropriate handler, we're done.
+
+sub handler {
+    my $action;
+
+    push @ARGV, 'shell' if (!@ARGV);    # default to shell mode
+    shift @ARGV if ($ARGV[0] eq 'rt');    # ignore a leading 'rt'
+    if (@ARGV && exists $actions{$ARGV[0]}) {
+        $action = shift @ARGV;
+        return $actions{$action}->($action);
+    }
+    else {
+        print STDERR "rt: Unknown command '@ARGV'.\n";
+        print STDERR "rt: For help, run 'rt help'.\n";
+        return 1;
+    }
 }
-$actions{$action || "help"}->($action || ());
-exit;
+
+exit handler();
 
 # Handler functions.
 # ------------------
 #
 # The following subs are handlers for each entry in %actions.
 
+sub shell {
+    $|=1;
+    my $term = Term::ReadLine->new('RT CLI');
+    while ( defined ($_ = $term->readline($prompt)) ) {
+        next if /^#/ || /^\s*$/;
+
+        @ARGV = shellwords($_);
+        handler();
+    }
+}
+
 sub version {
     print "rt $VERSION\n";
+    return 0;
 }
 
 sub logout {
     submit("$REST/logout") if defined $session->cookie;
+    return 0;
 }
 
+sub quit {
+    logout();
+    exit;
+}
+
+my %help;
 sub help {
-    my ($action, $type) = @_;
-    my (%help, $key);
+    my ($action, $type, $rv) = @_;
+    $rv = defined $rv ? $rv : 0;
+    my $key;
 
     # What help topics do we know about?
-    local $/ = undef;
-    foreach my $item (@{ Form::parse(<DATA>) }) {
-        my $title = $item->[2]{Title};
-        my @titles = ref $title eq 'ARRAY' ? @$title : $title;
+    if (!%help) {
+        local $/ = undef;
+        foreach my $item (@{ Form::parse(<DATA>) }) {
+            my $title = $item->[2]{Title};
+            my @titles = ref $title eq 'ARRAY' ? @$title : $title;
 
-        foreach $title (grep $_, @titles) {
-            $help{$title} = $item->[2]{Text};
+            foreach $title (grep $_, @titles) {
+                $help{$title} = $item->[2]{Text};
+            }
         }
     }
 
@@ -161,13 +251,22 @@ sub help {
     }
 
     print STDERR $help{$key}, "\n\n";
+    return $rv;
 }
 
 # Displays a list of objects that match some specified condition.
 
 sub list {
-    my ($q, $type, %data, $orderby);
+    my ($q, $type, %data);
+    my $orderby = $config{orderby};
+    
+    if ($config{orderby}) {
+         $data{orderby} = $config{orderby};
+    } 
     my $bad = 0;
+    my $rawprint = 0;
+    my $reverse_sort = 0;
+    my $queue = $config{queue};
 
     while (@ARGV) {
         $_ = shift @ARGV;
@@ -179,10 +278,17 @@ sub list {
             $bad = 1, last unless get_var_argument(\%data);
         }
         elsif (/^-o$/) {
-            $orderby = shift @ARGV;
+            $data{'orderby'} = shift @ARGV;
         }
         elsif (/^-([isl])$/) {
             $data{format} = $1;
+            $rawprint = 1;
+        }
+        elsif (/^-q$/) {
+            $queue = shift @ARGV;
+        }
+        elsif (/^-r$/) {
+            $reverse_sort = 1;
         }
         elsif (/^-f$/) {
             if ($ARGV[0] !~ /^(?:(?:$field,)*$field)$/) {
@@ -190,6 +296,8 @@ sub list {
                 $bad = 1; last;
             }
             $data{fields} = shift @ARGV;
+            $data{format} = 's' if ! $data{format};
+            $rawprint = 1;
         }
         elsif (!defined $q && !/^-/) {
             $q = $_;
@@ -200,17 +308,75 @@ sub list {
             $bad = 1; last;
         }
     }
+    if ( ! $rawprint and ! exists $data{format} ) {
+        $data{format} = 'l';
+        $data{fields} = 'subject,status,queue,created,told,owner,requestors';
+    }
+    if ( $reverse_sort and $data{orderby} =~ /^-/ ) {
+        $data{orderby} =~ s/^-/+/;
+    } elsif ($reverse_sort) {
+        $data{orderby} =~ s/^\+?(.*)/-$1/;
+    }
 
     $type ||= "ticket";
-    unless ($type && defined $q) {
+
+    if (!defined $q ) {
+        if ( $type eq 'ticket' ) {
+            $q = $config{query};
+        }
+        else {
+            $q = '';
+        }
+    }
+
+    if ( $type ne 'ticket' ) {
+        $rawprint = 1;
+    }
+
+    unless (defined $q) {
         my $item = $type ? "query string" : "object type";
         whine "No $item specified.";
         $bad = 1;
     }
-    return help("list", $type) if $bad;
 
-    my $r = submit("$REST/search/$type", { query => $q, %data, orderby => $orderby || "" });
-    print $r->content;
+    $q =~ s/^#//; # get rid of leading hash
+    if ( $type eq 'ticket' ) {
+        if ( $q =~ /^\d+$/ ) {
+
+            # only digits, must be an id, formulate a correct query
+            $q = "id=$q" if $q =~ /^\d+$/;
+        }
+        else {
+
+          # a string only, take it as an owner or requestor (quoting done later)
+            $q = "(Owner=$q or Requestor like $q) and $config{query}"
+              if $q =~ /^[\w\-]+$/;
+
+           # always add a query for a specific queue or (comma separated) queues
+            $queue =~ s/,/ or Queue=/g if $queue;
+            $q .= " and (Queue=$queue)"
+              if $queue
+                  and $q
+                  and $q !~ /Queue\s*=/i
+                  and $q !~ /id\s*=/i;
+        }
+
+        # correctly quote strings in a query
+        $q =~ s/(=|like\s)\s*([^'\d\s]\S*)\b/$1\'$2\'/g;
+    }
+
+    #return help("list", $type) if $bad;
+    return suggest_help("list", $type, $bad) if $bad;
+
+    print "Query:$q\n" if ! $rawprint;
+    my $r = submit("$REST/search/$type", { query => $q, %data });
+    if ( $rawprint ) {
+        print $r->content;
+    } else {
+        my $forms = Form::parse($r->content);
+        prettylist ($forms);
+    }
+    return 0;
 }
 
 # Displays selected information about a single object.
@@ -219,10 +385,12 @@ sub show {
     my ($type, @objects, %data);
     my $slurped = 0;
     my $bad = 0;
+    my $rawprint = 0;
+    my $histspec;
 
     while (@ARGV) {
         $_ = shift @ARGV;
-
+        s/^#// if /^#\d+/; # get rid of leading hash
         if (/^-t$/) {
             $bad = 1, last unless defined($type = get_type_argument());
         }
@@ -231,6 +399,7 @@ sub show {
         }
         elsif (/^-([isl])$/) {
             $data{format} = $1;
+            $rawprint = 1;
         }
         elsif (/^-$/ && !$slurped) {
             chomp(my @lines = <STDIN>);
@@ -249,9 +418,21 @@ sub show {
                 $bad = 1; last;
             }
             $data{fields} = shift @ARGV;
+            # option f requires short raw listing format
+            $data{format} = 's';
+            $rawprint = 1;
+        }
+        elsif (/^\d+$/ and my $spc2 = is_object_spec("ticket/$_", $type)) {
+            push @objects, $spc2;
+            $histspec = is_object_spec("ticket/$_/history", $type);
+        }
+        elsif (/^\d+\// and my $spc3 = is_object_spec("ticket/$_", $type)) {
+            push @objects, $spc3;
+            $rawprint = 1 if $_ =~ /\/content$/;
         }
         elsif (my $spec = is_object_spec($_, $type)) {
             push @objects, $spec;
+            $rawprint = 1 if $_ =~ /\/content$/ or $_ =~ /\/links/ or $_ !~ /^ticket/;
         }
         else {
             my $datum = /^-/ ? "option" : "argument";
@@ -259,15 +440,36 @@ sub show {
             $bad = 1; last;
         }
     }
+    if ( ! $rawprint ) {
+        push @objects, $histspec if $histspec;
+        $data{format} = 'l' if ! exists $data{format};
+    }
 
     unless (@objects) {
         whine "No objects specified.";
         $bad = 1;
     }
-    return help("show", $type) if $bad;
+    #return help("show", $type) if $bad;
+    return suggest_help("show", $type, $bad) if $bad;
 
     my $r = submit("$REST/show", { id => \@objects, %data });
-    print $r->content;
+    my $c = $r->content;
+    # if this isn't a text reply, remove the trailing newline so we
+    # don't corrupt things like tarballs when people do
+    # show ticket/id/attachments/id/content > foo.tar.gz
+    if ($r->content_type !~ /^text\//) {
+        chomp($c);
+        $rawprint = 1;
+    }
+    if ( $rawprint ) {
+        print $c;
+    } else {
+        # I do not know how to get more than one form correctly returned
+        $c =~ s!^RT/[\d\.]+ 200 Ok$!--!mg;
+        my $forms = Form::parse($c);
+        prettyshow ($forms);
+    }
+    return 0;
 }
 
 # To create a new object, we ask the server for a form with the defaults
@@ -280,7 +482,7 @@ sub show {
 sub edit {
     my ($action) = @_;
     my (%data, $type, @objects);
-    my ($cl, $text, $edit, $input, $output);
+    my ($cl, $text, $edit, $input, $output, $content_type);
 
     use vars qw(%set %add %del);
     %set = %add = %del = ();
@@ -289,10 +491,12 @@ sub edit {
     
     while (@ARGV) {
         $_ = shift @ARGV;
+        s/^#// if /^#\d+/; # get rid of leading hash
 
         if    (/^-e$/) { $edit = 1 }
         elsif (/^-i$/) { $input = 1 }
         elsif (/^-o$/) { $output = 1 }
+        elsif (/^-ct$/) { $content_type = shift @ARGV }
         elsif (/^-t$/) {
             $bad = 1, last unless defined($type = get_type_argument());
         }
@@ -313,7 +517,7 @@ sub edit {
         elsif (/^set$/i) {
             my $vars = 0;
 
-            while (@ARGV && $ARGV[0] =~ /^($field)([+-]?=)(.*)$/) {
+            while (@ARGV && $ARGV[0] =~ /^($field)([+-]?=)(.*)$/s) {
                 my ($key, $op, $val) = ($1, $2, $3);
                 my $hash = ($op eq '=') ? \%set : ($op =~ /^\+/) ? \%add : \%del;
 
@@ -331,7 +535,7 @@ sub edit {
             my $vars = 0;
             my $hash = ($_ eq "add") ? \%add : \%del;
 
-            while (@ARGV && $ARGV[0] =~ /^($field)=(.*)$/) {
+            while (@ARGV && $ARGV[0] =~ /^($field)=(.*)$/s) {
                 my ($key, $val) = ($1, $2);
 
                 vpush($hash, lc $key, $val);
@@ -344,6 +548,9 @@ sub edit {
             }
             $cl = $vars;
         }
+        elsif (/^\d+$/ and my $spc2 = is_object_spec("ticket/$_", $type)) {
+            push @objects, $spc2;
+        }
         elsif (my $spec = is_object_spec($_, $type)) {
             push @objects, $spec;
         }
@@ -369,20 +576,25 @@ sub edit {
             whine "What type of object do you want to create?";
             $bad = 1;
         }
-        @objects = ("$type/new");
+        @objects = ("$type/new") if defined($type);
     }
-    return help($action, $type) if $bad;
+    #return help($action, $type) if $bad;
+    return suggest_help($action, $type, $bad) if $bad;
 
     # We need a form to make changes to. We usually ask the server for
     # one, but we can avoid that if we are fed one on STDIN, or if the
     # user doesn't want to edit the form by hand, and the command line
-    # specifies only simple variable assignments.
+    # specifies only simple variable assignments.  We *should* get a
+    # form if we're creating a new ticket, so that the default values
+    # get filled in properly.
+
+    my @new_objects = grep /\/new$/, @objects;
 
     if ($input) {
         local $/ = undef;
         $text = <STDIN>;
     }
-    elsif ($edit || %add || %del || !$cl) {
+    elsif ($edit || %add || %del || !$cl || @new_objects) {
         my $r = submit("$REST/show", { id => \@objects, format => 'l' });
         $text = $r->content;
     }
@@ -451,45 +663,125 @@ sub edit {
 
     if ($output) {
         print $text;
-        exit;
+        return 0;
     }
 
+    my @files;
+    @files = @{ vsplit($set{'attachment'}) } if exists $set{'attachment'};
+
     my $synerr = 0;
 
 EDIT:
     # We'll let the user edit the form before sending it to the server,
     # unless we have enough information to submit it non-interactively.
+    if ( $type && $type eq 'ticket' && $text !~ /^Content-Type:/m ) {
+        $text .= "Content-Type: $content_type\n"
+            if $content_type and $content_type ne "text/plain";
+    }
+
     if ($edit || (!$input && !$cl)) {
-        my $newtext = vi($text);
+        my ($newtext) = vi_form_while(
+            $text,
+            sub {
+                my ($text, $form) = @_;
+                return 1 unless exists $form->[2]{'Attachment'};
+
+                foreach my $f ( @{ vsplit($form->[2]{'Attachment'}) } ) {
+                    return (0, "File '$f' doesn't exist") unless -f $f;
+                }
+                @files = @{ vsplit($form->[2]{'Attachment'}) };
+                return 1;
+            },
+        );
+        return $newtext unless $newtext;
         # We won't resubmit a bad form unless it was changed.
         $text = ($synerr && $newtext eq $text) ? undef : $newtext;
     }
 
+    delete @data{ grep /^attachment_\d+$/, keys %data };
+    my $i = 1;
+    foreach my $file (@files) {
+        $data{"attachment_$i"} = bless([ $file ], "Attachment");
+        $i++;
+    }
+
     if ($text) {
         my $r = submit("$REST/edit", {content => $text, %data});
         if ($r->code == 409) {
             # If we submitted a bad form, we'll give the user a chance
             # to correct it and resubmit.
             if ($edit || (!$input && !$cl)) {
-                $text = $r->content;
+                my $content = $r->content . "\n";
+                $content =~ s/^(?!#)/#     /mg;
+                $text = $content . $text;
                 $synerr = 1;
                 goto EDIT;
             }
             else {
                 print $r->content;
-                exit -1;
+                return 0;
             }
         }
         print $r->content;
     }
+    return 0;
+}
+
+# handler for special edit commands. A valid edit command is constructed and
+# further work is delegated to the edit handler
+
+sub setcommand {
+    my ($action) = @_;
+    my ($id, $bad, $what);
+    if ( @ARGV ) {
+        $_ = shift @ARGV;
+        $id = $1 if (m|^(?:ticket/)?($idlist)$|);
+    }
+    if ( ! $id ) {
+        $bad = 1;
+        whine "No ticket number specified.";
+    }
+    if ( @ARGV ) {
+        if ($action eq 'subject') {
+            my $subject = '"'.join (" ", @ARGV).'"';
+            @ARGV = ();
+            $what = "subject=$subject";
+        } elsif ($action eq 'give') {
+            my $owner = shift @ARGV;
+            $what = "owner=$owner";
+        }
+    } else {
+        if ( $action eq 'delete' or $action eq 'del' ) {
+            $what = "status=deleted";
+        } elsif ($action eq 'resolve' or $action eq 'res' ) {
+            $what = "status=resolved";
+        } elsif ($action eq 'take' ) {
+            $what = "owner=$config{user}";
+        } elsif ($action eq 'untake') {
+            $what = "owner=Nobody";
+        }
+    }
+    if (@ARGV) {
+        $bad = 1;
+        whine "Extraneous arguments for action $action: @ARGV.";
+    }
+    if ( ! $what ) {
+        $bad = 1;
+        whine "unrecognized action $action.";
+    }
+    return help("edit", undef, $bad) if $bad;
+    @ARGV = ( $id, "set", $what );
+    print "Executing: rt edit @ARGV\n";
+    return edit("edit");
 }
 
 # We roll "comment" and "correspond" into the same handler.
 
 sub comment {
     my ($action) = @_;
-    my (%data, $id, @files, @bcc, @cc, $msg, $wtime, $edit);
+    my (%data, $id, @files, @bcc, @cc, $msg, $content_type, $wtime, $edit);
     my $bad = 0;
+    my $status = '';
 
     while (@ARGV) {
         $_ = shift @ARGV;
@@ -497,7 +789,7 @@ sub comment {
         if (/^-e$/) {
             $edit = 1;
         }
-        elsif (/^-[abcmw]$/) {
+        elsif (/^-(?:[abcmws]|ct)$/) {
             unless (@ARGV) {
                 whine "No argument specified with $_.";
                 $bad = 1; last;
@@ -506,15 +798,27 @@ sub comment {
             if (/-a/) {
                 unless (-f $ARGV[0] && -r $ARGV[0]) {
                     whine "Cannot read attachment: '$ARGV[0]'.";
-                    exit -1;
+                    return 0;
                 }
                 push @files, shift @ARGV;
             }
+            elsif (/-ct/) {
+                $content_type = shift @ARGV;
+            }
+            elsif (/-s/) {
+                $status = shift @ARGV;
+            }
             elsif (/-([bc])/) {
                 my $a = $_ eq "-b" ? \@bcc : \@cc;
                 @$a = split /\s*,\s*/, shift @ARGV;
             }
-            elsif (/-m/) { $msg = shift @ARGV }
+            elsif (/-m/) {
+                $msg = shift @ARGV;
+                if ( $msg =~ /^-$/ ) {
+                    undef $msg;
+                    while (<STDIN>) { $msg .= $_ }
+                }
+            }
             elsif (/-w/) { $wtime = shift @ARGV }
         }
         elsif (!$id && m|^(?:ticket/)?($idlist)$|) {
@@ -531,11 +835,12 @@ sub comment {
         whine "No object specified.";
         $bad = 1;
     }
-    return help($action, "ticket") if $bad;
+    #return help($action, "ticket") if $bad;
+    return suggest_help($action, "ticket") if $bad;
 
     my $form = [
         "",
-        [ "Ticket", "Action", "Cc", "Bcc", "Attachment", "TimeWorked", "Text" ],
+        [ "Ticket", "Action", "Cc", "Bcc", "Attachment", "TimeWorked", "Content-Type", "Text" ],
         {
             Ticket     => $id,
             Action     => $action,
@@ -543,37 +848,31 @@ sub comment {
             Bcc        => [ @bcc ],
             Attachment => [ @files ],
             TimeWorked => $wtime || '',
+            'Content-Type' => $content_type || 'text/plain',
             Text       => $msg || '',
+            Status => $status
         }
     ];
+    if ($status ne '') {
+      push(@{$form->[1]}, "Status");
+    }
 
     my $text = Form::compose([ $form ]);
 
     if ($edit || !$msg) {
-        my $error = 0;
-        my ($c, $o, $k, $e);
-
-        do {
-            my $ntext = vi($text);
-            exit if ($error && $ntext eq $text);
-            $text = $ntext;
-            $form = Form::parse($text);
-            $error = 0;
-
-            ($c, $o, $k, $e) = @{ $form->[0] };
-            if ($e) {
-                $error = 1;
-                $c = "# Syntax error.";
-                goto NEXT;
-            }
-            elsif (!@$o) {
-                exit;
-            }
-            @files = @{ vsplit($k->{Attachment}) };
-
-        NEXT:
-            $text = Form::compose([[$c, $o, $k, $e]]);
-        } while ($error);
+        my ($tmp) = vi_form_while(
+            $text,
+            sub {
+                my ($text, $form) = @_;
+                foreach my $f ( @{ vsplit($form->[2]{'Attachment'}) } ) {
+                    return (0, "File '$f' doesn't exist") unless -f $f;
+                }
+                @files = @{ vsplit($form->[2]{'Attachment'}) };
+                return 1;
+            },
+        );
+        return $tmp unless $tmp;
+        $text = $tmp;
     }
 
     my $i = 1;
@@ -583,8 +882,9 @@ sub comment {
     }
     $data{content} = $text;
 
-    my $r = submit("$REST/ticket/comment/$id", \%data);
+    my $r = submit("$REST/ticket/$id/comment", \%data);
     print $r->content;
+    return 0;
 }
 
 # Merge one ticket into another.
@@ -595,6 +895,7 @@ sub merge {
 
     while (@ARGV) {
         $_ = shift @ARGV;
+        s/^#// if /^#\d+/; # get rid of leading hash
 
         if (/^\d+$/) {
             push @id, $_;
@@ -610,16 +911,20 @@ sub merge {
         whine "Too $evil arguments specified.";
         $bad = 1;
     }
-    return help("merge", "ticket") if $bad;
+    #return help("merge", "ticket") if $bad;
+    return suggest_help("merge", "ticket", $bad) if $bad;
 
-    my $r = submit("$REST/ticket/merge/$id[0]", {into => $id[1]});
+    my $r = submit("$REST/ticket/$id[0]/merge/$id[1]");
     print $r->content;
+    return 0;
 }
 
 # Link one ticket to another.
 
 sub link {
     my ($bad, $del, %data) = (0, 0, ());
+    my $type;
+
     my %ltypes = map { lc $_ => $_ } qw(DependsOn DependedOnBy RefersTo
                                         ReferredToBy HasMember MemberOf);
 
@@ -629,21 +934,21 @@ sub link {
         if (/^-d$/) {
             $del = 1;
         }
+        elsif (/^-t$/) {
+            $bad = 1, last unless defined($type = get_type_argument());
+        }
         else {
             whine "Unrecognised option: '$_'.";
             $bad = 1; last;
         }
     }
-
+    
+    $type = "ticket" unless $type; # default type to tickets
+    
     if (@ARGV == 3) {
         my ($from, $rel, $to) = @ARGV;
-        if ($from !~ /^\d+$/ || $to !~ /^\d+$/) {
-            my $bad = $from =~ /^\d+$/ ? $to : $from;
-            whine "Invalid ticket ID '$bad' specified.";
-            $bad = 1;
-        }
-        unless (exists $ltypes{lc $rel}) {
-            whine "Invalid relationship '$rel' specified.";
+        if (($type eq "ticket") && ( ! exists $ltypes{lc $rel})) {
+            whine "Invalid link '$rel' for type $type specified.";
             $bad = 1;
         }
         %data = (id => $from, rel => $rel, to => $to, del => $del);
@@ -653,10 +958,50 @@ sub link {
         whine "Too $bad arguments specified.";
         $bad = 1;
     }
-    return help("link", "ticket") if $bad;
+    return suggest_help("link", $type, $bad) if $bad;
+    my $r = submit("$REST/$type/link", \%data);
+    print $r->content;
+    return 0;
+}
 
-    my $r = submit("$REST/ticket/link", \%data);
+# Take/steal a ticket
+sub take {
+    my ($cmd) = @_;
+    my ($bad, %data) = (0, ());
+
+    my $id;
+
+    # get the ticket id
+    if (@ARGV == 1) {
+        ($id) = @ARGV;
+        unless ($id =~ /^\d+$/) {
+            whine "Invalid ticket ID $id specified.";
+            $bad = 1;
+        }
+        my $form = [
+            "",
+            [ "Ticket", "Action" ],
+            {
+                Ticket => $id,
+                Action => $cmd,
+                Status => '',
+            }
+        ];
+
+        my $text = Form::compose([ $form ]);
+        $data{content} = $text;
+    }
+    else {
+        $bad = @ARGV < 1 ? "few" : "many";
+        whine "Too $bad arguments specified.";
+        $bad = 1;
+    }
+    return suggest_help("take", "ticket", $bad) if $bad;
+
+    my $r = submit("$REST/ticket/$id/take", \%data);
     print $r->content;
+    return 0;
 }
 
 # Grant/revoke a user's rights.
@@ -664,11 +1009,8 @@ sub link {
 sub grant {
     my ($cmd) = @_;
 
-    my $revoke = 0;
-    while (@ARGV) {
-    }
-
-    $revoke = 1 if $cmd->{action} eq 'revoke';
+    whine "$cmd is unimplemented.";
+    return 1;
 }
 
 # Client <-> Server communication.
@@ -681,7 +1023,8 @@ sub grant {
 sub submit {
     my ($uri, $content) = @_;
     my ($req, $data);
-    my $ua = new LWP::UserAgent(agent => "RT/3.0b", env_proxy => 1);
+    my $ua = LWP::UserAgent->new(agent => "RT/3.0b", env_proxy => 1);
+    my $h = HTTP::Headers->new;
 
     # Did the caller specify any data to send with the request?
     $data = [];
@@ -707,7 +1050,23 @@ sub submit {
     }
 
     # Should we send authentication information to start a new session?
-    if (!defined $session->cookie) {
+    my $how = $config{server} =~ /^https/ ? 'over SSL' : 'unencrypted';
+    my($server) = $config{server} =~ m{^.*//([^/]+)};
+
+    if ($config{auth} eq "gssapi") {
+        die "GSSAPI support not available; failed to load perl module GSSAPI:\n$@\n"
+            unless eval { require GSSAPI; 1 };
+        die "GSSAPI support not available; failed to load perl module LWP::Authen::Negotiate:\n$@\n"
+            unless eval { require LWP::Authen::Negotiate; 1 };
+    } elsif ($config{auth} eq "basic") {
+        print "   Password will be sent to $server $how\n",
+              "   Press CTRL-C now if you do not want to continue\n"
+            if ! $config{passwd};
+        $h->authorization_basic($config{user}, $config{passwd} || read_passwd() );
+    } elsif ( !defined $session->cookie ) {
+        print "   Password will be sent to $server $how\n",
+              "   Press CTRL-C now if you do not want to continue\n"
+            if ! $config{passwd};
         push @$data, ( user => $config{user} );
         push @$data, ( pass => $config{passwd} || read_passwd() );
     }
@@ -720,6 +1079,7 @@ sub submit {
         $req = GET($uri);
     }
     $session->add_cookie_header($req);
+    $req->header(%$h) if %$h;
 
     # Then we send the request and parse the response.
     DEBUG(3, $req->as_string);
@@ -733,11 +1093,11 @@ sub submit {
 
         my ($head, $text) = split /\n\n/, $res->content, 2;
         my ($status, @headers) = split /\n/, $head;
-        $text =~ s/\n*$/\n/;
+        $text =~ s/\n*$/\n/ if ($text);
 
         # "RT/3.0.1 401 Credentials required"
-        if ($status !~ m#^RT/\d+(?:\.\d+)+(?:-?\w+)? (\d+) ([\w\s]+)$#) {
-            warn "rt: Malformed RT response from $config{server}.\n";
+        if ($status !~ m#^RT/\d+(?:\S+) (\d+) ([\w\s]+)$#) {
+            warn "rt: Malformed RT response from $server.\n";
             warn "(Rerun with RTDEBUG=3 for details.)\n" if $config{debug} < 3;
             exit -1;
         }
@@ -770,7 +1130,7 @@ sub submit {
             # For anything else, we just die.
             elsif ($res->code != 409) {
                 warn "rt: ", $res->content;
-                exit;
+                #exit;
             }
         }
     }
@@ -812,7 +1172,7 @@ sub submit {
     sub cookie {
         my ($self) = @_;
         my $cookie = $self->{sids}{$s}{$u};
-        return defined $cookie ? "RT_SID=$cookie" : undef;
+        return defined $cookie ? "RT_SID_$cookie" : undef;
     }
 
     # Deletes the current session cookie.
@@ -835,7 +1195,7 @@ sub submit {
         my ($self, $response) = @_;
         my $cookie = $response->header("Set-Cookie");
 
-        if (defined $cookie && $cookie =~ /^RT_SID=([0-9A-Fa-f]+);/) {
+        if (defined $cookie && $cookie =~ /^RT_SID_(.[^;,\s]+=[0-9A-Fa-f]+);/) {
             $self->{sids}{$s}{$u} = $1;
         }
     }
@@ -844,44 +1204,40 @@ sub submit {
     sub load {
         my ($self, $file) = @_;
         $file ||= $self->{file};
-        local *F;
-
-        open(F, $file) && do {
-            $self->{file} = $file;
-            my $sids = $self->{sids} = {};
-            while (<F>) {
-                chomp;
-                next if /^$/ || /^#/;
-                next unless m#^https?://[^ ]+ \w+ [0-9A-Fa-f]+$#;
-                my ($server, $user, $cookie) = split / /, $_;
-                $sids->{$server}{$user} = $cookie;
-            }
-            return 1;
-        };
-        return 0;
+
+        open( my $handle, '<', $file ) or return 0;
+
+        $self->{file} = $file;
+        my $sids = $self->{sids} = {};
+        while (<$handle>) {
+            chomp;
+            next if /^$/ || /^#/;
+            next unless m#^https?://[^ ]+ \w+ [^;,\s]+=[0-9A-Fa-f]+$#;
+            my ($server, $user, $cookie) = split / /, $_;
+            $sids->{$server}{$user} = $cookie;
+        }
+        return 1;
     }
 
     # Writes the current session cache to the specified file.
     sub save {
         my ($self, $file) = shift;
         $file ||= $self->{file};
-        local *F;
-
-        open(F, ">$file") && do {
-            my $sids = $self->{sids};
-            foreach my $server (keys %$sids) {
-                foreach my $user (keys %{ $sids->{$server} }) {
-                    my $sid = $sids->{$server}{$user};
-                    if (defined $sid) {
-                        print F "$server $user $sid\n";
-                    }
+
+        open( my $handle, '>', "$file" ) or return 0;
+
+        my $sids = $self->{sids};
+        foreach my $server (keys %$sids) {
+            foreach my $user (keys %{ $sids->{$server} }) {
+                my $sid = $sids->{$server}{$user};
+                if (defined $sid) {
+                    print $handle "$server $user $sid\n";
                 }
             }
-            close(F);
-            chmod 0600, $file;
-            return 1;
-        };
-        return 0;
+        }
+        close($handle);
+        chmod 0600, $file;
+        return 1;
     }
 
     sub DESTROY {
@@ -905,7 +1261,7 @@ sub submit {
 sub Form::parse {
     my $state = 0;
     my @forms = ();
-    my @lines = split /\n/, $_[0];
+    my @lines = split /\n/, $_[0] if $_[0];
     my ($c, $o, $k, $e) = ("", [], {}, "");
 
     LINE:
@@ -1027,7 +1383,7 @@ sub Form::compose {
                         $line .= ",\n$sp$v";
                     }
                     else {
-                        $line = $line ? "$line, $v" : "$key: $v";
+                        $line = $line ? "$line,$v" : "$key: $v";
                     }
                 }
 
@@ -1061,7 +1417,8 @@ sub Form::compose {
 sub config_from_env {
     my %env;
 
-    foreach my $k ("DEBUG", "USER", "PASSWD", "SERVER") {
+    foreach my $k (qw(EXTERNALAUTH AUTH DEBUG USER PASSWD SERVER QUERY ORDERBY)) {
+
         if (exists $ENV{"RT$k"}) {
             $env{lc $k} = $ENV{"RT$k"};
         }
@@ -1094,7 +1451,7 @@ sub config_from_file {
         }
 
         # Still nothing? We'll fall back to some likely defaults.
-        for ("$HOME/$rc", "/etc/rt.conf") {
+        for ("$HOME/$rc", "/opt/rt3/local/etc/rt.conf", "/etc/rt.conf") {
             return parse_config_file($_) if (-r $_);
         }
     }
@@ -1106,20 +1463,21 @@ sub config_from_file {
 sub parse_config_file {
     my %cfg;
     my ($file) = @_;
+    local $_; # $_ may be aliased to a constant, from line 1163
 
-    open(CFG, $file) && do {
-        while (<CFG>) {
-            chomp;
-            next if (/^#/ || /^\s*$/);
+    open( my $handle, '<', $file ) or return;
 
-            if (/^(user|passwd|server)\s+([^ ]+)$/) {
-                $cfg{$1} = $2;
-            }
-            else {
-                die "rt: $file:$.: unknown configuration directive.\n";
-            }
+    while (<$handle>) {
+        chomp;
+        next if (/^#/ || /^\s*$/);
+
+        if (/^(externalauth|auth|user|passwd|server|query|orderby|queue)\s+(.*)\s?$/) {
+            $cfg{$1} = $2;
+        }
+        else {
+            die "rt: $file:$.: unknown configuration directive.\n";
         }
-    };
+    }
 
     return %cfg;
 }
@@ -1131,7 +1489,7 @@ sub whine {
     my $sub = (caller(1))[3];
     $sub =~ s/^main:://;
     warn "rt: $sub: @_\n";
-    return;
+    return 0;
 }
 
 sub read_passwd {
@@ -1149,18 +1507,58 @@ sub read_passwd {
     return $passwd;
 }
 
+sub vi_form_while {
+    my $text = shift;
+    my $cb = shift;
+
+    my $error = 0;
+    my ($c, $o, $k, $e);
+    do {
+        my $ntext = vi($text);
+        return undef if ($error && $ntext eq $text);
+
+        $text = $ntext;
+
+        my $form = Form::parse($text);
+        $error = 0;
+        ($c, $o, $k, $e) = @{ $form->[0] };
+        if ( $e ) {
+            $error = 1;
+            $c = "# Syntax error.";
+            goto NEXT;
+        }
+        elsif (!@$o) {
+            return 0;
+        }
+
+        my ($status, $msg) = $cb->( $text, [$c, $o, $k, $e] );
+        unless ( $status ) {
+            $error = 1;
+            $c = "# $msg";
+        }
+
+    NEXT:
+        $text = Form::compose([[$c, $o, $k, $e]]);
+    } while ($error);
+
+    return $text;
+}
+
 sub vi {
     my ($text) = @_;
-    my $file = "/tmp/rt.form.$$";
     my $editor = $ENV{EDITOR} || $ENV{VISUAL} || "vi";
 
-    local *F;
     local $/ = undef;
 
-    open(F, ">$file") || die "$file: $!\n"; print F $text; close(F);
-    system($editor, $file) && die "Couldn't run $editor.\n";
-    open(F, $file) || die "$file: $!\n"; $text = <F>; close(F);
-    unlink($file);
+    my $handle = File::Temp->new;
+    print $handle $text;
+    close($handle);
+
+    system($editor, $handle->filename) && die "Couldn't run $editor.\n";
+
+    open( $handle, '<', $handle->filename ) or die "$handle: $!\n";
+    $text = <$handle>;
+    close($handle);
 
     return $text;
 }
@@ -1182,33 +1580,82 @@ sub vpush {
     }
 }
 
+# WARNING: this code is duplicated in lib/RT/Interface/REST.pm
+# If you change one, change both functions at once
 # "Normalise" a hash key that's known to be multi-valued.
 sub vsplit {
-    my ($val) = @_;
-    my ($word, @words);
-    my @values = ref $val eq 'ARRAY' ? @$val : $val;
-
-    foreach my $line (map {split /\n/} @values) {
-        # XXX: This should become a real parser, Ã  la Text::ParseWords.
-        $line =~ s/^\s+//;
-        $line =~ s/\s+$//;
-        push @words, split /\s*,\s*/, $line;
+    my ($val, $strip) = @_;
+    my @words;
+    my @values = map {split /\n/} (ref $val eq 'ARRAY' ? @$val : $val);
+
+    foreach my $line (@values) {
+        while ($line =~ /\S/) {
+            $line =~ s/^
+                       \s*   # Trim leading whitespace
+                       (?:
+                           (")   # Quoted string
+                           ((?>[^\\"]*(?:\\.[^\\"]*)*))"
+                       |
+                           (')   # Single-quoted string
+                           ((?>[^\\']*(?:\\.[^\\']*)*))'
+                       |
+                           q\{(.*?)\} # A perl-ish q{} string; this does
+                                      # no paren balancing, however, and
+                                      # only exists for back-compat
+                       |
+                           (.*?)     # Anything else, until the next comma
+                       )
+                       \s*   # Trim trailing whitespace
+                       (?:
+                           \Z  # Finish at end-of-line
+                       |
+                           ,   # Or a comma
+                       )
+                      //xs or last; # There should be no way this match
+                                    # fails, but add a failsafe to
+                                    # prevent infinite-looping if it
+                                    # somehow does.
+            my ($quote, $quoted) = ($1 ? ($1, $2) : $3 ? ($3, $4) : ('', $5 || $6));
+            # Only unquote the quote character, or the backslash -- and
+            # only if we were originally quoted..
+            if ($5) {
+                $quoted =~ s/([\\'])/\\$1/g;
+                $quote = "'";
+            }
+            if ($strip) {
+                $quoted =~ s/\\([\\$quote])/$1/g if $quote;
+                push @words, $quoted;
+            } else {
+                push @words, "$quote$quoted$quote";
+            }
+        }
     }
-
     return \@words;
 }
 
+# WARN: this code is duplicated in lib/RT/Interface/REST.pm
+# change both functions at once
 sub expand_list {
     my ($list) = @_;
-    my ($elt, @elts, %elts);
 
-    foreach $elt (split /,/, $list) {
-        if ($elt =~ /^(\d+)-(\d+)$/) { push @elts, ($1..$2) }
-        else                         { push @elts, $elt }
+    my @elts;
+    foreach (split /\s*,\s*/, $list) {
+        push @elts, /^(\d+)-(\d+)$/? ($1..$2): $_;
     }
 
-    @elts{@elts}=();
-    return sort {$a<=>$b} keys %elts;
+    return map $_->[0], # schwartzian transform
+        sort {
+            defined $a->[1] && defined $b->[1]?
+                # both numbers
+                $a->[1] <=> $b->[1]
+                :!defined $a->[1] && !defined $b->[1]?
+                    # both letters
+                    $a->[2] cmp $b->[2]
+                    # mix, number must be first
+                    :defined $a->[1]? -1: 1
+        }
+        map [ $_, (defined( /^(\d+)$/ )? $1: undef), lc($_) ],
+        @elts;
 }
 
 sub get_type_argument {
@@ -1255,7 +1702,130 @@ sub is_object_spec {
 
     $spec =~ s|^(?:$type/)?|$type/| if defined $type;
     return $spec if ($spec =~ m{^$name/(?:$idlist|$labels)(?:/.*)?$}o);
-    return;
+    return 0;
+}
+
+sub suggest_help {
+    my ($action, $type, $rv) = @_;
+
+    print STDERR "rt: For help, run 'rt help $action'.\n" if defined $action;
+    print STDERR "rt: For help, run 'rt help $type'.\n" if defined $type;
+    return $rv;
+}
+
+sub str2time {
+    # simplified procedure for parsing date, avoid loading Date::Parse
+    my %month = (Jan => 0, Feb => 1, Mar => 2, Apr => 3, May =>  4, Jun =>  5,
+                 Jul => 6, Aug => 7, Sep => 8, Oct => 9, Nov => 10, Dec => 11);
+    $_ = shift;
+    my ($mon, $day, $hr, $min, $sec, $yr, $monstr);
+    if ( /(\w{3})\s+(\d\d?)\s+(\d\d):(\d\d):(\d\d)\s+(\d{4})/ ) {
+        ($monstr, $day, $hr, $min, $sec, $yr) = ($1, $2, $3, $4, $5, $6);
+        $mon = $month{$monstr} if exists $month{$monstr};
+    } elsif ( /(\d{4})-(\d\d)-(\d\d)\s+(\d\d):(\d\d):(\d\d)/ ) {
+        ($yr, $mon, $day, $hr, $min, $sec) = ($1, $2-1, $3, $4, $5, $6);
+    }
+    if ( $yr and defined $mon and $day and defined $hr and defined $sec ) {
+        return timelocal($sec,$min,$hr,$day,$mon,$yr);
+    } else {
+        print "Unknown date format in parsedate: $_\n";
+        return undef;
+    }
+}
+
+sub date_diff {
+    my ($old, $new) = @_;
+    $new = time() if ! $new;
+    $old = str2time($old) if $old !~ /^\d+$/;
+    $new = str2time($new) if $new !~ /^\d+$/;
+    return "???" if ! $old or ! $new;
+
+    my %seconds = (min => 60,
+                   hr  => 60*60,
+                   day => 60*60*24,
+                   wk  => 60*60*24*7,
+                   mth => 60*60*24*30,
+                   yr  => 60*60*24*365);
+
+    my $diff = $new - $old;
+    my $what = 'sec';
+    my $howmuch = $diff;
+    for ( sort {$seconds{$a} <=> $seconds{$b}} keys %seconds) {
+        last if $diff < $seconds{$_};
+        $what = $_;
+        $howmuch = int($diff/$seconds{$_});
+    }
+    return "$howmuch $what";
+}
+
+sub prettyshow {
+    my $forms = shift;
+    my ($form) = grep { exists $_->[2]->{Queue} } @$forms;
+    my $k = $form->[2];
+    # dates are in local time zone
+    if ( $k ) {
+        print "Date: $k->{Created}\n";
+        print "From: $k->{Requestors}\n";
+        print "Cc: $k->{Cc}\n" if $k->{Cc};
+        print "X-AdminCc: $k->{AdminCc}\n" if $k->{AdminCc};
+        print "X-Queue: $k->{Queue}\n";
+        print "Subject: [rt #$k->{id}] $k->{Subject}\n\n";
+    }
+    # dates in these attributes are in GMT and will be converted
+    foreach my $form (@$forms) {
+        my ($c, $o, $k, $e) = @$form;
+        next if ! $k->{id} or exists $k->{Queue};
+        if ( exists $k->{Created} ) {
+            my ($y,$m,$d,$hh,$mm,$ss) = ($k->{Created} =~ /(\d\d\d\d)-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)/);
+            $m--;
+            my $created = localtime(timegm($ss,$mm,$hh,$d,$m,$y));
+            if ( exists $k->{Description} ) {
+                print "===> $k->{Description} on $created\n";
+            }
+        }
+        print "$k->{Content}\n" if exists $k->{Content} and
+                                   $k->{Content} !~ /to have no content$/ and
+                                   ($k->{Type}||'') ne 'EmailRecord';
+        print "$k->{Attachments}\n" if exists $k->{Attachments} and
+                                   $k->{Attachments};
+    }
+}
+
+sub prettylist {
+    my $forms = shift;
+    my $heading = "Ticket Owner Queue    Age   Told Status Requestor Subject\n";
+    $heading .= '-' x 80 . "\n";
+    my (@open, @me);
+    foreach my $form (@$forms) {
+        my ($c, $o, $k, $e) = @$form;
+        next if ! $k->{id};
+        print $heading if $heading;
+        $heading = '';
+        my $id = $k->{id};
+        $id =~ s!^ticket/!!;
+        my $owner = $k->{Owner} eq 'Nobody' ? '' : $k->{Owner};
+        $owner = substr($owner, 0, 5);
+        my $queue = substr($k->{Queue}, 0, 5);
+        my $subject = substr($k->{Subject}, 0, 30);
+        my $age = date_diff($k->{Created});
+        my $told = $k->{Told} eq 'Not set' ? '' : date_diff($k->{Told});
+        my $status = substr($k->{Status}, 0, 6);
+        my $requestor = substr($k->{Requestors}, 0, 9);
+        my $line = sprintf "%6s %5s %5s %6s %6s %-6s %-9s %-30s\n",
+            $id, $owner, $queue, $age, $told, $status, $requestor, $subject;
+        if ( $k->{Owner} eq 'Nobody' ) {
+            push @open, $line;
+        } elsif ($k->{Owner} eq $config{user} ) {
+            push @me, $line;
+        } else {
+            print $line;
+        }
+    }
+    print "No matches found\n" if $heading;
+    printf "========== my %2d open tickets ==========\n", scalar @me if @me;
+    print @me if @me;
+    printf "========== %2d unowned tickets ==========\n", scalar @open if @open;
+    print @open if @open;
 }
 
 __DATA__
@@ -1264,10 +1834,7 @@ Title: intro
 Title: introduction
 Text:
 
-    ** THIS IS AN UNSUPPORTED PREVIEW RELEASE **
-    ** PLEASE REPORT BUGS TO rt-bugs@fsck.com **
-
-    This is a command-line interface to RT 3.
+    This is a command-line interface to RT 3.0 or newer.
 
     It allows you to interact with an RT server over HTTP, and offers an
     interface to RT's functionality that is better-suited to automation
@@ -1279,9 +1846,10 @@ Text:
 
     For more information:
 
-        - rt help actions       (a list of possible actions)
-        - rt help objects       (how to specify objects)
         - rt help usage         (syntax information)
+        - rt help objects       (how to specify objects)
+        - rt help actions       (a list of possible actions)
+        - rt help types         (a list of object types)
 
         - rt help config        (configuration details)
         - rt help examples      (a few useful examples)
@@ -1296,6 +1864,8 @@ Text:
     Syntax:
 
         rt <action> [options] [arguments]
+      or
+        rt shell
 
     Each invocation of this program must specify an action (e.g. "edit",
     "create"), options to modify behaviour, and other arguments required
@@ -1306,6 +1876,10 @@ Text:
     "rt help <action>". Some actions may be referred to by more than one
     name ("create" is the same as "new", for example).  
 
+    You may also call "rt shell", which will give you an 'rt>' prompt at
+    which you can issue commands of the form "<action> [options] 
+    [arguments]".  See "rt help shell" for details.
+
     Objects are identified by a type and an ID (which can be a name or a
     number, depending on the type). For some actions, the object type is
     implied (you can only comment on tickets); for others, the user must
@@ -1320,6 +1894,7 @@ Text:
         - rt help objects       (how to specify objects)
         - rt help actions       (a list of actions)
         - rt help types         (a list of object types)
+        - rt help shell         (how to use the shell)
 
 --
 
@@ -1334,18 +1909,35 @@ Text:
     The program looks for configuration directives in a file named .rtrc
     (or $RTCONFIG; see below) in the current directory, and then in more
     distant ancestors, until it reaches /. If no suitable configuration
-    files are found, it will also check for ~/.rtrc and /etc/rt.conf.
+    files are found, it will also check for ~/.rtrc, /opt/rt3/local/etc/rt.conf
+    and /etc/rt.conf.
 
     Configuration directives:
 
         The following directives may occur, one per line:
 
-        - server <URL>          URL to RT server.
-        - user <username>       RT username.
-        - passwd <passwd>       RT user's password.
+        - server <URL>           URL to RT server.
+        - user <username>        RT username.
+        - passwd <passwd>        RT user's password.
+        - query <RT Query>       Default RT Query for list action
+        - orderby <order>        Default RT order for list action
+        - queue <queuename>      Default RT Queue for list action
+        - auth <rt|basic|gssapi> Method to authenticate via; "basic"
+                     means HTTP Basic authentication, "gssapi" means
+                     Kerberos credentials, if your RT is configured
+                     with $WebRemoteUserAuth.  For backwards
+                     compatibility, "externalauth 1" means "auth basic"
 
         Blank and #-commented lines are ignored.
 
+    Sample configuration file contents:
+
+         server  https://rt.somewhere.com/
+         # more than one queue can be given (by adding a query expression)
+         queue helpdesk or queue=support
+         query Status != resolved and Owner=myaccount
+
+
     Environment variables:
 
         The following environment variables override any corresponding
@@ -1353,10 +1945,13 @@ Text:
 
         - RTUSER
         - RTPASSWD
+        - RTAUTH
         - RTSERVER
         - RTDEBUG       Numeric debug level. (Set to 3 for full logs.)
         - RTCONFIG      Specifies a name other than ".rtrc" for the
                         configuration file.
+        - RTQUERY       Default RT Query for rt list
+        - RTORDERBY     Default order for rt list
 
 --
 
@@ -1380,10 +1975,14 @@ Text:
     be used to specify more than one object of the same type. Note that
     the list must be a single argument (i.e., no spaces). For example,
     "user/root,1-3,5,7-10,ams" is a list of ten users; the same list
-    can also be written as "user/ams,root,1,2,3,5,7,8-20".
+    can also be written as "user/ams,root,1,2,3,5,7,8-10".
     
+    If just a number is given as object specification it will be
+    interpreted as ticket/<number>
+
     Examples:
 
+        1                   # the same as ticket/1
         ticket/1
         ticket/1/attachments
         ticket/1/attachments/3
@@ -1392,8 +1991,6 @@ Text:
         ticket/1-3,5-7/history
 
         user/ams
-        user/ams/rights
-        user/ams,rai,1/rights
 
     For more information:
 
@@ -1421,6 +2018,22 @@ Text:
         - rt help <action>      (action-specific details)
         - rt help types         (a list of possible types)
 
+    The following actions on tickets are also possible:
+
+        - comment       Add comments to a ticket
+        - correspond    Add comments to a ticket
+        - merge         Merge one ticket into another
+        - link          Link one ticket to another
+        - take          Take a ticket (steal and untake are possible as well)
+
+    For several edit set subcommands that are frequently used abbreviations
+    have been introduced. These abbreviations are:
+
+        - delete or del  delete a ticket           (edit set status=deleted)
+        - resolve or res resolve a ticket          (edit set status=resolved)
+        - subject        change subject of ticket  (edit set subject=string)
+        - give           give a ticket to somebody (edit set owner=user)
+
 --
 
 Title: types
@@ -1459,6 +2072,13 @@ Text:
         - merge
         - comment
         - correspond
+        - take
+        - steal
+        - untake
+        - give
+        - resolve
+        - delete
+        - subject
 
     Attributes:
 
@@ -1488,20 +2108,6 @@ Text:
         - edit
         - create
 
-    In addition, the following type-specific actions exist:
-
-        - grant
-        - revoke
-
-    Attributes:
-
-        The following attributes can be used with "rt show" or "rt edit"
-        to retrieve or edit other information associated with users and
-        groups:
-
-        rights                  Global rights granted to this user.
-        rights/<queue>          Queue rights for this user.
-
 --
 
 Title: queue
@@ -1517,6 +2123,83 @@ Text:
 
 --
 
+Title: subject
+Text:
+
+    Syntax:
+
+        rt subject <id> <new subject text>
+
+    Change the subject of a ticket whose ticket id is given.
+
+--
+
+Title: give
+Text:
+
+    Syntax:
+
+        rt give <id> <accountname>
+
+    Give a ticket whose ticket id is given to another user.
+
+--
+
+Title: steal
+Text:
+
+        rt steal <id> 
+
+    Steal a ticket whose ticket id is given, i.e. set the owner to myself.
+
+--
+
+Title: take
+Text:
+
+    Syntax:
+
+        rt take <id>
+
+    Take a ticket whose ticket id is given, i.e. set the owner to myself.
+
+--
+
+Title: untake
+Text:
+
+    Syntax:
+
+        rt untake <id>
+
+    Untake a ticket whose ticket id is given, i.e. set the owner to Nobody.
+
+--
+
+Title: resolve
+Title: res
+Text:
+
+    Syntax:
+
+        rt resolve <id>
+
+    Resolves a ticket whose ticket id is given.
+
+--
+
+Title: delete
+Title: del
+Text:
+
+    Syntax:
+
+        rt delete <id>
+
+    Deletes a ticket whose ticket id is given.
+
+--
+
 Title: logout
 Text:
 
@@ -1542,37 +2225,47 @@ Text:
     Displays a list of objects matching the specified conditions.
     ("ls", "list", and "search" are synonyms.)
 
-    Conditions are expressed in the SQL-like syntax used internally by
-    RT3. (For more information, see "rt help query".) The query string
-    must be supplied as one argument.
+    The query string must be supplied as one argument.
+
+    if on tickets, query is in the SQL-like syntax used internally by
+    RT. (For more information, see "rt help query".), otherwise, query
+    is plain string with format "FIELD OP VALUE", e.g. "Name = General".
 
-    (Right now, the server doesn't support listing anything but tickets.
-    Other types will be supported in future; this client will be able to
-    take advantage of that support without any changes.)
+    if query string is absent, we limit to privileged ones on users and
+    user defined ones on groups automatically.
 
     Options:
 
         The following options control how much information is displayed
         about each matching object:
 
-        -i      Numeric IDs only. (Useful for |rt edit -; see examples.)
-        -s      Short description.
-        -l      Longer description.
+        -i             Numeric IDs only. (Useful for |rt edit -; see examples.)
+        -s             Short description.
+        -l             Longer description.
+        -f <field[s]   Display only the fields listed and the ticket id
 
         In addition,
         
-        -o +/-<field>   Orders the returned list by the specified field.
-        -S var=val      Submits the specified variable with the request.
-        -t type         Specifies the type of object to look for. (The
-                        default is "ticket".)
+        -o +/-<field>  Orders the returned list by the specified field.
+        -r             reversed order (useful if a default was given)
+        -q queue[s]    restricts the query to the queue[s] given
+                       multiple queues are separated by comma
+        -S var=val     Submits the specified variable with the request.
+        -t type        Specifies the type of object to look for. (The
+                       default is "ticket".)
 
     Examples:
 
-        rt ls "Priority > 5 and Status='new'"
-        rt ls -o +Subject "Priority > 5 and Status='new'"
-        rt ls -o -Created "Priority > 5 and Status='new'"
+        rt ls "Priority > 5 and Status=new"
+        rt ls -o +Subject "Priority > 5 and Status=new"
+        rt ls -o -Created "Priority > 5 and Status=new"
         rt ls -i "Priority > 5"|rt edit - set status=resolved
         rt ls -t ticket "Subject like '[PATCH]%'"
+        rt ls -q systems
+        rt ls -f owner,subject
+        rt ls -t queue 'Name = General'
+        rt ls -t user 'EmailAddress like foo@bar.com'
+        rt ls -t group 'Name like foo'
 
 --
 
@@ -1590,11 +2283,23 @@ Text:
     that refers to the links for tickets 1-3). Consult "rt help <type>"
     and "rt help objects" for further details.
 
+    If only a number is given it will be interpreted as the objects
+    ticket/number and ticket/number/history
+
     This command writes a set of forms representing the requested object
     data to STDOUT.
 
     Options:
 
+        The following options control how much information is displayed
+        about each matching object:
+
+        Without any formatting options prettyprinted output is generated.
+        Giving any of the two options below reverts to raw output.
+        -s      Short description (history and attachments only).
+        -l      Longer description (history and attachments only).
+
+        In addition,
         -               Read IDs from STDIN instead of the command-line.
         -t type         Specifies object type.
         -f a,b,c        Restrict the display to the specified fields.
@@ -1606,7 +2311,10 @@ Text:
         rt show ticket/3/attachments/29
         rt show ticket/3/attachments/29/content
         rt show ticket/1-3/links
+        rt show ticket/3/history
+        rt show -l ticket/3/history
         rt show -t user 2
+        rt show 2
 
 --
 
@@ -1623,6 +2331,8 @@ Text:
 
     Edits information corresponding to the specified objects.
 
+    A purely numeric object id nnn is translated into ticket/nnn
+
     If, instead of "edit", an action of "new" or "create" is specified,
     then a new object is created. In this case, no numeric object IDs
     may be specified, but the syntax and behaviour remain otherwise
@@ -1654,19 +2364,21 @@ Text:
         -S var=val
                 Submits the specified variable with the request.
         -t type Specifies object type.
+        -ct content-type Specifies content type of message(tickets only).
 
     Examples:
 
         # Interactive (starts $EDITOR with a form).
         rt edit ticket/3
         rt create -t ticket
+        rt create -t ticket -ct text/html
 
         # Non-interactive.
-        rt edit ticket/1-3 add cc=foo@example.com set priority=3
+        rt edit ticket/1-3 add cc=foo@example.com set priority=3 due=tomorrow
         rt ls -t tickets -i 'Priority > 5' | rt edit - set status=resolved
         rt edit ticket/4 set priority=3 owner=bar@example.com \
                          add cc=foo@example.com bcc=quux@example.net
-        rt create -t ticket subject='new ticket' priority=10 \
+        rt create -t ticket set subject='new ticket' priority=10 \
                             add cc=foo@example.com
 
 --
@@ -1691,17 +2403,20 @@ Text:
     Options:
 
         -m <text>       Specify comment text.
+        -ct <content-type> Specify content-type of comment text.
         -a <file>       Attach a file to the comment. (May be used more
                         than once to attach multiple files.)
         -c <addrs>      A comma-separated list of Cc addresses.
         -b <addrs>      A comma-separated list of Bcc addresses.
+        -s <status>     Set a new status for the ticket (default will
+                        leave the status unchanged)
         -w <time>       Specify the time spent working on this ticket.
         -e              Starts an editor before the submission, even if
                         arguments from the command line were sufficient.
 
     Examples:
 
-        rt comment -t 'Not worth fixing.' -a stddisclaimer.h 23
+        rt comment -m 'Not worth fixing.' -a stddisclaimer.h 23
 
 --
 
@@ -1712,7 +2427,7 @@ Text:
 
         rt merge <from-id> <to-id>
 
-    Merges the two specified tickets.
+    Merges the first ticket specified into the second ticket specified.
 
 --
 
@@ -1721,16 +2436,16 @@ Text:
 
     Syntax:
 
-        rt link [-d] <id-A> <relationship> <id-B>
+        rt link [-d] <id-A> <link> <id-B>
 
     Creates (or, with -d, deletes) a link between the specified tickets.
-    The relationship can (irrespective of case) be any of:
+    The link can (irrespective of case) be any of:
 
         DependsOn/DependedOnBy:     A depends upon B (or vice versa).
         RefersTo/ReferredToBy:      A refers to B (or vice versa).
         MemberOf/HasMember:         A is a member of B (or vice versa).
 
-    To view a ticket's relationships, use "rt show ticket/3/links". (See
+    To view a ticket's links, use "rt show ticket/3/links". (See
     "rt help ticket" and "rt help show".)
 
     Options:
@@ -1744,20 +2459,43 @@ Text:
 
 --
 
-Title: grant
-Title: revoke
-Text:
-
---
-
 Title: query
 Text:
 
-    RT3 uses an SQL-like syntax to specify object selection constraints.
+    RT uses an SQL-like syntax to specify object selection constraints.
     See the <RT:...> documentation for details.
     
     (XXX: I'm going to have to write it, aren't I?)
 
+    Until it exists here a short description of important constructs:
+
+    The two simple forms of query expressions are the constructs
+    Attribute like Value and
+    Attribute = Value or Attribute != Value
+
+    Whether attributes can be matched using like or using = is built into RT.
+    The attributes id, Queue, Owner Priority and Status require the = or !=
+    tests.
+
+    If Value is a string it must be quoted and may contain the wildcard
+    character %. If the string does not contain white space, the quoting
+    may however be omitted, it will be added automatically when parsing
+    the input.
+
+    Simple query expressions can be combined using and, or and parentheses
+    can be used to group expressions.
+
+    As a special case a standalone string (which would not form a correct
+    query) is transformed into (Owner='string' or Requestor like 'string%')
+    and added to the default query, i.e. the query is narrowed down.
+
+    If no Queue=name clause is contained in the query, a default clause
+    Queue=$config{queue} is added.
+
+    Examples:
+    Status!='resolved' and Status!='rejected'
+    (Owner='myaccount' or Requestor like 'myaccount%') and Status!='resolved'
+
 --
 
 Title: form
@@ -1787,7 +2525,11 @@ Text:
 Title: topics
 Text:
 
-    Use "rt help <topic>" for help on any of the following subjects:
+    Syntax:
+
+        rt help <topic>
+
+    Get help on any of the following subjects:
 
         - tickets, users, groups, queues.
         - show, edit, ls/list/search, new/create.
@@ -1808,9 +2550,131 @@ Title: example
 Title: examples
 Text:
 
-    This section will be filled in with useful examples, once it becomes
-    more clear what examples may be useful.
+    some useful examples
+
+    All the following list requests will be restricted to the default queue.
+    That can be changed by adding the option -q queuename
+
+    List all tickets that are not rejected/resolved
+        rt ls
+    List all tickets that are new and do not have an owner
+        rt ls "status=new and owner=nobody"
+    List all tickets which I have sent or of which I am the owner
+        rt ls myaccount
+    List all attributes for the ticket 6977 (ls -l instead of ls)
+        rt ls -l 6977
+    Show the content of ticket 6977
+        rt show 6977
+    Show all attributes in the ticket and in the history of the ticket
+        rt show -l 6977
+    Comment a ticket (mail is sent to all queue watchers, i.e. AdminCc's)
+        rt comment 6977
+        This will open an editor and lets you add text (attribute Text:)
+        Other attributes may be changed as well, but usually don't do that.
+    Correspond a ticket (like comment, but mail is also sent to requestors)
+        rt correspond 6977
+    Edit a ticket (generic change, interactive using the editor)
+        rt edit 6977
+    Change the owner of a ticket non interactively
+        rt edit 6977 set owner=myaccount
+        or
+        rt give 6977 account
+        or
+        rt take 6977
+    Change the status of a ticket
+        rt edit 6977 set status=resolved
+        or
+        rt resolve 6977
+    Change the status of all tickets I own to resolved !!!
+        rt ls -i owner=myaccount | rt edit - set status=resolved
 
-    For the moment, please consult examples provided with each action.
+--
+
+Title: shell
+Text:
+
+    Syntax:
+
+        rt shell
+
+    Opens an interactive shell, at which you can issue commands of 
+    the form "<action> [options] [arguments]".
+
+    To exit the shell, type "quit" or "exit".
+
+    Commands can be given at the shell in the same form as they would 
+    be given at the command line without the leading 'rt' invocation.
+
+    Example:
+        $ rt shell
+        rt> create -t ticket set subject='new' add cc=foo@example.com
+        # Ticket 8 created.
+        rt> quit
+        $
 
 --
+
+Title: take
+Title: untake
+Title: steal
+Text:
+
+    Syntax:
+
+        rt <take|untake|steal> <ticket-id>
+
+    Sets the owner of the specified ticket to the current user, 
+    assuming said user has the bits to do so, or releases the 
+    ticket.  
+    
+    'Take' is used on tickets which are not currently owned 
+    (Owner: Nobody), 'steal' is used on tickets which *are* 
+    currently owned, and 'untake' is used to "release" a ticket 
+    (reset its Owner to Nobody).  'Take' cannot be used on
+    tickets which are currently owned.
+
+    Example:
+        alice$ rt create -t ticket set subject="New ticket"
+        # Ticket 7 created.
+        alice$ rt take 7
+        # Owner changed from Nobody to alice
+        alice$ su bob
+        bob$ rt steal 7
+        # Owner changed from alice to bob
+        bob$ rt untake 7
+        # Owner changed from bob to Nobody
+
+--
+
+Title: quit
+Title: exit
+Text:
+
+    Use "quit" or "exit" to leave the shell.  Only valid within shell 
+    mode.
+
+    Example:
+        $ rt shell
+        rt> quit
+        $
+
+__END__
+
+=head1 NAME
+
+rt - command-line interface to RT 3.0 or newer
+
+=head1 SYNOPSIS
+
+    rt help
+
+=head1 DESCRIPTION
+
+This script allows you to interact with an RT server over HTTP, and offers an
+interface to RT's functionality that is better-suited to automation and
+integration with other tools.
+
+In general, each invocation of this program should specify an action to
+perform on one or more objects, and any other arguments required to complete
+the desired action.
+