import rt 3.8.7
[freeside.git] / rt / bin / rt
index 3440d9e..9554a93 100755 (executable)
--- a/rt/bin/rt
+++ b/rt/bin/rt
@@ -2,8 +2,8 @@
 # BEGIN BPS TAGGED BLOCK {{{
 # 
 # COPYRIGHT:
-#  
-# This software is Copyright (c) 1996-2009 Best Practical Solutions, LLC 
+# 
+# This software is Copyright (c) 1996-2009 Best Practical Solutions, LLC
 #                                          <jesse@bestpractical.com>
 # 
 # (Except where explicitly superseded by other copyright notices)
@@ -58,7 +58,19 @@ use Cwd;
 use LWP;
 use Text::ParseWords;
 use HTTP::Request::Common;
+use HTTP::Headers;
 use Term::ReadLine;
+use Time::Local; # used in prettyshow
+
+# strong (GSSAPI based) authentication is supported if the server does provide
+# it and the perl modules GSSAPI and LWP::Authen::Negotiate are installed
+# it can be suppressed by setting externalauth=0 (default is undef)
+eval { require GSSAPI };
+my $no_strong_auth = 'missing perl module GSSAPI';
+if ( ! $@ ) {
+    eval {require LWP::Authen::Negotiate};
+    $no_strong_auth = $@ ? 'missing perl module LWP::Authen::Negotiate' : 0;
+}
 
 # We derive configuration information from hardwired defaults, dotfiles,
 # and the RT* environment variables (in increasing order of precedence).
@@ -70,18 +82,27 @@ my $HOME = eval{(getpwuid($<))[7]}
            || ".";
 my %config = (
     (
-        debug   => 0,
-        user    => eval{(getpwuid($<))[0]} || $ENV{USER} || $ENV{USERNAME},
-        passwd  => undef,
-        server  => 'http://localhost/',
-        query   => undef,
-        orderby => undef,
+        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',
+# setting externalauth => undef will try GSSAPI auth if the corresponding perl
+# modules are installed, externalauth => 0 is the backward compatible choice 
+        externalauth => 0,
     ),
     config_from_file($ENV{RTCONFIG} || ".rtrc"),
     config_from_env()
 );
 my $session = new Session("$HOME/.rt_sessions");
 my $REST = "$config{server}/REST/1.0";
+$no_strong_auth = 'switched off by externalauth=0'
+    if defined $config{externalauth};
+
 
 my $prompt = 'rt> ';
 
@@ -91,11 +112,12 @@ 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_-]|\s+)*)';
-my $label  = '[a-zA-Z0-9@_.+-]+';
-my $labels = "(?:$label,)*$label";
-my $idlist = '(?:(?:\d+-)?\d+,)*(?:\d+-)?\d+';
+my $name    = '[\w.-]+';
+my $CF_name = '[\sa-z0-9_ :()/-]+';
+my $field   = '(?i:[a-z][a-z0-9_-]*|C(?:ustom)?F(?:ield)?-'.$CF_name.'|CF\.\{'.$CF_name.'\})';
+my $label   = '[a-zA-Z0-9@_.+-]+';
+my $labels  = "(?:$label,)*$label";
+my $idlist  = '(?:(?:\d+-)?\d+,)*(?:\d+-)?\d+';
 
 # Our command line looks like this:
 #
@@ -119,6 +141,8 @@ my %handlers = (
     grant       => ["grant", "revoke"],
     take        => ["take", "steal", "untake"],
     quit        => ["quit", "exit"],
+    setcommand  => ["del", "delete", "give", "res", "resolve",
+                    "subject"],
 );
 
 my %actions;
@@ -137,16 +161,16 @@ sub handler {
     shift @ARGV if ($ARGV[0] eq 'rt');    # ignore a leading 'rt'
     if (@ARGV && exists $actions{$ARGV[0]}) {
         $action = shift @ARGV;
-        $actions{$action}->($action);
+        return $actions{$action}->($action);
     }
     else {
         print STDERR "rt: Unknown command '@ARGV'.\n";
         print STDERR "rt: For help, run 'rt help'.\n";
+        return 1;
     }
 }
 
-handler();
-exit;
+exit handler();
 
 # Handler functions.
 # ------------------
@@ -166,10 +190,12 @@ sub shell {
 
 sub version {
     print "rt $VERSION\n";
+    return 0;
 }
 
 sub logout {
     submit("$REST/logout") if defined $session->cookie;
+    return 0;
 }
 
 sub quit {
@@ -179,7 +205,8 @@ sub quit {
 
 my %help;
 sub help {
-    my ($action, $type) = @_;
+    my ($action, $type, $rv) = @_;
+    $rv = defined $rv ? $rv : 0;
     my $key;
 
     # What help topics do we know about?
@@ -228,6 +255,7 @@ sub help {
     }
 
     print STDERR $help{$key}, "\n\n";
+    return $rv;
 }
 
 # Displays a list of objects that match some specified condition.
@@ -240,6 +268,9 @@ sub list {
          $data{orderby} = $config{orderby};
     } 
     my $bad = 0;
+    my $rawprint = 0;
+    my $reverse_sort = 0;
+    my $queue = $config{queue};
 
     while (@ARGV) {
         $_ = shift @ARGV;
@@ -255,6 +286,13 @@ sub list {
         }
         elsif (/^-([isl])$/) {
             $data{format} = $1;
+            $rawprint = 1;
+        }
+        elsif (/^-q$/) {
+            $queue = shift @ARGV;
+        }
+        elsif (/^-r$/) {
+            $reverse_sort = 1;
         }
         elsif (/^-f$/) {
             if ($ARGV[0] !~ /^(?:(?:$field,)*$field)$/) {
@@ -262,6 +300,8 @@ sub list {
                 $bad = 1; last;
             }
             $data{fields} = shift @ARGV;
+            $data{format} = 's' if ! $data{format};
+            $rawprint = 1;
         }
         elsif (!defined $q && !/^-/) {
             $q = $_;
@@ -272,10 +312,35 @@ sub list {
             $bad = 1; last;
         }
     }
+    if ( ! $rawprint and ! exists $data{format} ) {
+        $data{format} = 'l';
+    }
+    if ( $reverse_sort and $data{orderby} =~ /^-/ ) {
+        $data{orderby} =~ s/^-/+/;
+    } elsif ($reverse_sort) {
+        $data{orderby} =~ s/^\+?(.*)/-$1/;
+    }
+
     if (!defined $q) {
         $q = $config{query}; 
     }
     
+    $q =~ s/^#//; # get rid of leading hash
+    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;
+
     $type ||= "ticket";
     unless ($type && defined $q) {
         my $item = $type ? "query string" : "object type";
@@ -283,10 +348,17 @@ sub list {
         $bad = 1;
     }
     #return help("list", $type) if $bad;
-    return suggest_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 });
-    print $r->content;
+    if ( $rawprint ) {
+        print $r->content;
+    } else {
+        my $forms = Form::parse($r->content);
+        prettylist ($forms);
+    }
+    return 0;
 }
 
 # Displays selected information about a single object.
@@ -295,10 +367,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());
         }
@@ -307,6 +381,7 @@ sub show {
         }
         elsif (/^-([isl])$/) {
             $data{format} = $1;
+            $rawprint = 1;
         }
         elsif (/^-$/ && !$slurped) {
             chomp(my @lines = <STDIN>);
@@ -325,9 +400,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 $_ !~ /^ticket/;
         }
         else {
             my $datum = /^-/ ? "option" : "argument";
@@ -335,13 +422,17 @@ 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 suggest_help("show", $type) if $bad;
+    return suggest_help("show", $type, $bad) if $bad;
 
     my $r = submit("$REST/show", { id => \@objects, %data });
     my $c = $r->content;
@@ -350,8 +441,17 @@ sub show {
     # 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);
     }
-    print $c;
+    return 0;
 }
 
 # To create a new object, we ask the server for a form with the defaults
@@ -373,6 +473,7 @@ sub edit {
     
     while (@ARGV) {
         $_ = shift @ARGV;
+        s/^#// if /^#\d+/; # get rid of leading hash
 
         if    (/^-e$/) { $edit = 1 }
         elsif (/^-i$/) { $input = 1 }
@@ -397,7 +498,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;
 
@@ -415,7 +516,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);
@@ -428,6 +529,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;
         }
@@ -453,10 +557,10 @@ 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 suggest_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
@@ -540,7 +644,7 @@ sub edit {
 
     if ($output) {
         print $text;
-        return;
+        return 0;
     }
 
     my $synerr = 0;
@@ -566,11 +670,60 @@ EDIT:
             }
             else {
                 print $r->content;
-                return;
+                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.
@@ -595,7 +748,7 @@ sub comment {
             if (/-a/) {
                 unless (-f $ARGV[0] && -r $ARGV[0]) {
                     whine "Cannot read attachment: '$ARGV[0]'.";
-                    return;
+                    return 0;
                 }
                 push @files, shift @ARGV;
             }
@@ -665,7 +818,7 @@ sub comment {
                 goto NEXT;
             }
             elsif (!@$o) {
-                return;
+                return 0;
             }
             @files = @{ vsplit($k->{Attachment}) };
 
@@ -683,6 +836,7 @@ sub comment {
 
     my $r = submit("$REST/ticket/$id/comment", \%data);
     print $r->content;
+    return 0;
 }
 
 # Merge one ticket into another.
@@ -693,6 +847,7 @@ sub merge {
 
     while (@ARGV) {
         $_ = shift @ARGV;
+        s/^#// if /^#\d+/; # get rid of leading hash
 
         if (/^\d+$/) {
             push @id, $_;
@@ -709,16 +864,19 @@ sub merge {
         $bad = 1;
     }
     #return help("merge", "ticket") if $bad;
-    return suggest_help("merge", "ticket") if $bad;
+    return suggest_help("merge", "ticket", $bad) if $bad;
 
     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);
 
@@ -728,21 +886,26 @@ 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.";
+            whine "Invalid $type ID '$bad' specified.";
             $bad = 1;
         }
-        unless (exists $ltypes{lc $rel}) {
-            whine "Invalid link '$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);
@@ -752,11 +915,11 @@ sub link {
         whine "Too $bad arguments specified.";
         $bad = 1;
     }
-    #return help("link", "ticket") if $bad;
-    return suggest_help("link", "ticket") if $bad;
-
-    my $r = submit("$REST/ticket/link", \%data);
+    return suggest_help("link", $type, $bad) if $bad;
+    my $r = submit("$REST/$type/link", \%data);
     print $r->content;
+    return 0;
 }
 
 # Take/steal a ticket
@@ -791,10 +954,11 @@ sub take {
         whine "Too $bad arguments specified.";
         $bad = 1;
     }
-    return suggest_help("take", "ticket") if $bad;
+    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.
@@ -807,6 +971,7 @@ sub grant {
     }
 
     $revoke = 1 if $cmd->{action} eq 'revoke';
+    return 0;
 }
 
 # Client <-> Server communication.
@@ -820,6 +985,7 @@ sub submit {
     my ($uri, $content) = @_;
     my ($req, $data);
     my $ua = new LWP::UserAgent(agent => "RT/3.0b", env_proxy => 1);
+    my $h = HTTP::Headers->new;
 
     # Did the caller specify any data to send with the request?
     $data = [];
@@ -845,9 +1011,22 @@ sub submit {
     }
 
     # Should we send authentication information to start a new session?
-    if (!defined $session->cookie) {
-        push @$data, ( user => $config{user} );
-        push @$data, ( pass => $config{passwd} || read_passwd() );
+    my $how = $config{server} =~ /^https/ ? 'over SSL' : 'unencrypted';
+    (my $server = $config{server}) =~ s/^.*\/\/([^\/]+)\/?/$1/;
+    if ($config{externalauth}) {
+        $h->authorization_basic($config{user}, $config{passwd} || read_passwd() );
+        print "   Password will be sent to $server $how\n",
+              "   Press CTRL-C now if you do not want to continue\n"
+            if ! $config{passwd};
+    } elsif ( $no_strong_auth ) {
+        if (!defined $session->cookie) {
+            print "   Strong encryption not available, $no_strong_auth\n",
+                  "   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() );
+        }
     }
 
     # Now, we construct the request.
@@ -858,6 +1037,9 @@ sub submit {
         $req = GET($uri);
     }
     $session->add_cookie_header($req);
+    if ($config{externalauth}) {
+        $req->header(%$h);
+    }
 
     # Then we send the request and parse the response.
     DEBUG(3, $req->as_string);
@@ -874,7 +1056,7 @@ sub submit {
         $text =~ s/\n*$/\n/ if ($text);
 
         # "RT/3.0.1 401 Credentials required"
-       if ($status !~ m#^RT/\d+(?:\S+) (\d+) ([\w\s]+)$#) {
+        if ($status !~ m#^RT/\d+(?:\S+) (\d+) ([\w\s]+)$#) {
             warn "rt: Malformed RT response from $config{server}.\n";
             warn "(Rerun with RTDEBUG=3 for details.)\n" if $config{debug} < 3;
             exit -1;
@@ -1043,7 +1225,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:
@@ -1199,7 +1381,8 @@ sub Form::compose {
 sub config_from_env {
     my %env;
 
-    foreach my $k ("DEBUG", "USER", "PASSWD", "SERVER", "QUERY", "ORDERBY") {
+    foreach my $k (qw(EXTERNALAUTH DEBUG USER PASSWD SERVER QUERY ORDERBY)) {
+
         if (exists $ENV{"RT$k"}) {
             $env{lc $k} = $ENV{"RT$k"};
         }
@@ -1251,7 +1434,7 @@ sub parse_config_file {
             chomp;
             next if (/^#/ || /^\s*$/);
 
-            if (/^(user|passwd|server|query|orderby)\s+(.*)\s?$/) {
+            if (/^(externalauth|user|passwd|server|query|orderby|queue)\s+(.*)\s?$/) {
                 $cfg{$1} = $2;
             }
             else {
@@ -1270,7 +1453,7 @@ sub whine {
     my $sub = (caller(1))[3];
     $sub =~ s/^main:://;
     warn "rt: $sub: @_\n";
-    return;
+    return 0;
 }
 
 sub read_passwd {
@@ -1331,7 +1514,37 @@ sub vsplit {
         # XXX: This should become a real parser, à la Text::ParseWords.
         $line =~ s/^\s+//;
         $line =~ s/\s+$//;
-        push @words, split /\s*,\s*/, $line;
+        my ( $a, $b ) = split /,/, $line, 2;
+
+        while ($a) {
+            no warnings 'uninitialized';
+            if ( $a =~ /^'/ ) {
+                my $s = $a;
+                while ( $a !~ /'$/ || (   $a !~ /(\\\\)+'$/
+                            && $a =~ /(\\)+'$/ )) {
+                    ( $a, $b ) = split /,/, $b, 2;
+                    $s .= ',' . $a;
+                }
+                push @words, $s;
+            }
+            elsif ( $a =~ /^q{/ ) {
+                my $s = $a;
+                while ( $a !~ /}$/ ) {
+                    ( $a, $b ) =
+                      split /,/, $b, 2;
+                    $s .= ',' . $a;
+                }
+                $s =~ s/^q{/'/;
+                $s =~ s/}/'/;
+                push @words, $s;
+            }
+            else {
+                push @words, $a;
+            }
+            ( $a, $b ) = split /,/, $b, 2;
+        }
+
+
     }
 
     return \@words;
@@ -1406,14 +1619,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) = @_;
+    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__
@@ -1511,9 +1840,21 @@ Text:
         - 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
+        - externalauth <0|1>    Use HTTP Basic authentication
+         explicitely setting externalauth to 0 inhibits also GSSAPI based
+         authentication, if LWP::Authen::Negotiate (and GSSAPI) is installed
 
         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
@@ -1521,6 +1862,7 @@ Text:
 
         - RTUSER
         - RTPASSWD
+        - RTEXTERNALAUTH
         - RTSERVER
         - RTDEBUG       Numeric debug level. (Set to 3 for full logs.)
         - RTCONFIG      Specifies a name other than ".rtrc" for the
@@ -1552,8 +1894,12 @@ Text:
     "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-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
@@ -1591,6 +1937,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
@@ -1629,6 +1991,13 @@ Text:
         - merge
         - comment
         - correspond
+        - take
+        - steal
+        - untake
+        - give
+        - resolve
+        - delete
+        - subject
 
     Attributes:
 
@@ -1687,6 +2056,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:
 
@@ -1725,24 +2171,30 @@ Text:
         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
 
 --
 
@@ -1760,16 +2212,28 @@ 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.
         -S var=val      Submits the specified variable with the request.
-        -v              Verbose display
+
     Examples:
 
         rt show -t ticket -f id,subject,status 1-3
@@ -1777,8 +2241,9 @@ Text:
         rt show ticket/3/attachments/29/content
         rt show ticket/1-3/links
         rt show ticket/3/history
-        rt show -v ticket/3/history
+        rt show -l ticket/3/history
         rt show -t user 2
+        rt show 2
 
 --
 
@@ -1795,6 +2260,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
@@ -1834,7 +2301,7 @@ Text:
         rt create -t ticket
 
         # 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
@@ -1930,6 +2397,35 @@ Text:
     
     (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
@@ -1984,10 +2480,43 @@ Title: example
 Title: examples
 Text:
 
-    This section will be filled in with useful examples, once it becomes
-    more clear what examples may be useful.
-
-    For the moment, please consult examples provided with each action.
+    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
 
 --