Merge branch 'master' of https://github.com/jgoodman/Freeside
[freeside.git] / rt / bin / rt
1 #!/usr/bin/perl -w
2 # BEGIN BPS TAGGED BLOCK {{{
3 #
4 # COPYRIGHT:
5 #
6 # This software is Copyright (c) 1996-2014 Best Practical Solutions, LLC
7 #                                          <sales@bestpractical.com>
8 #
9 # (Except where explicitly superseded by other copyright notices)
10 #
11 #
12 # LICENSE:
13 #
14 # This work is made available to you under the terms of Version 2 of
15 # the GNU General Public License. A copy of that license should have
16 # been provided with this software, but in any event can be snarfed
17 # from www.gnu.org.
18 #
19 # This work is distributed in the hope that it will be useful, but
20 # WITHOUT ANY WARRANTY; without even the implied warranty of
21 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
22 # General Public License for more details.
23 #
24 # You should have received a copy of the GNU General Public License
25 # along with this program; if not, write to the Free Software
26 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
27 # 02110-1301 or visit their web page on the internet at
28 # http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
29 #
30 #
31 # CONTRIBUTION SUBMISSION POLICY:
32 #
33 # (The following paragraph is not intended to limit the rights granted
34 # to you to modify and distribute this software under the terms of
35 # the GNU General Public License and is only of importance to you if
36 # you choose to contribute your changes and enhancements to the
37 # community by submitting them to Best Practical Solutions, LLC.)
38 #
39 # By intentionally submitting any modifications, corrections or
40 # derivatives to this work, or any other work intended for use with
41 # Request Tracker, to Best Practical Solutions, LLC, you confirm that
42 # you are the copyright holder for those contributions and you grant
43 # Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
44 # royalty-free, perpetual, license to use, copy, create derivative
45 # works based on those contributions, and sublicense and distribute
46 # those contributions and any derivatives thereof.
47 #
48 # END BPS TAGGED BLOCK }}}
49 # Designed and implemented for Best Practical Solutions, LLC by
50 # Abhijit Menon-Sen <ams@wiw.org>
51
52 use strict;
53 use warnings;
54
55 if ( $ARGV[0] && $ARGV[0] =~ /^(?:--help|-h)$/ ) {
56     require Pod::Usage;
57     print Pod::Usage::pod2usage( { verbose => 2 } );
58     exit;
59 }
60
61 # This program is intentionally written to have as few non-core module
62 # dependencies as possible. It should stay that way.
63
64 use Cwd;
65 use LWP;
66 use Text::ParseWords;
67 use HTTP::Request::Common;
68 use HTTP::Headers;
69 use Term::ReadLine;
70 use Time::Local; # used in prettyshow
71 use File::Temp;
72
73 # strong (GSSAPI based) authentication is supported if the server does provide
74 # it and the perl modules GSSAPI and LWP::Authen::Negotiate are installed
75 # it can be suppressed by setting externalauth=0 (default is undef)
76 eval { require GSSAPI };
77 my $no_strong_auth = 'missing perl module GSSAPI';
78 if ( ! $@ ) {
79     eval {require LWP::Authen::Negotiate};
80     $no_strong_auth = $@ ? 'missing perl module LWP::Authen::Negotiate' : 0;
81 }
82
83 # We derive configuration information from hardwired defaults, dotfiles,
84 # and the RT* environment variables (in increasing order of precedence).
85 # Session information is stored in ~/.rt_sessions.
86
87 my $VERSION = 0.02;
88 my $HOME = eval{(getpwuid($<))[7]}
89            || $ENV{HOME} || $ENV{LOGDIR} || $ENV{HOMEPATH}
90            || ".";
91 my %config = (
92     (
93         debug        => 0,
94         user         => eval{(getpwuid($<))[0]} || $ENV{USER} || $ENV{USERNAME},
95         passwd       => undef,
96         server       => 'http://localhost/',
97         query        => "Status!='resolved' and Status!='rejected'",
98         orderby      => 'id',
99         queue        => undef,
100 # to protect against unlimited searches a better choice would be
101 #       queue        => 'Unknown_Queue',
102 # setting externalauth => undef will try GSSAPI auth if the corresponding perl
103 # modules are installed, externalauth => 0 is the backward compatible choice 
104         externalauth => 0,
105     ),
106     config_from_file($ENV{RTCONFIG} || ".rtrc"),
107     config_from_env()
108 );
109 my $session = Session->new("$HOME/.rt_sessions");
110 my $REST = "$config{server}/REST/1.0";
111 $no_strong_auth = 'switched off by externalauth=0'
112     if defined $config{externalauth};
113
114
115 my $prompt = 'rt> ';
116
117 sub whine;
118 sub DEBUG { warn @_ if $config{debug} >= shift }
119
120 # These regexes are used by command handlers to parse arguments.
121 # (XXX: Ask Autrijus how i18n changes these definitions.)
122
123 my $name    = '[\w.-]+';
124 my $CF_name = '[^,]+?';
125 my $field   = '(?i:[a-z][a-z0-9_-]*|C(?:ustom)?F(?:ield)?-'.$CF_name.'|CF\.\{'.$CF_name.'\})';
126 my $label   = '[^,\\/]+';
127 my $labels  = "(?:$label,)*$label";
128 my $idlist  = '(?:(?:\d+-)?\d+,)*(?:\d+-)?\d+';
129
130 # Our command line looks like this:
131 #
132 #     rt <action> [options] [arguments]
133 #
134 # We'll parse just enough of it to decide upon an action to perform, and
135 # leave the rest to per-action handlers to interpret appropriately.
136
137 my %handlers = (
138 #   handler     => [ ...aliases... ],
139     version     => ["version", "ver"],
140     shell       => ["shell"],
141     logout      => ["logout"],
142     help        => ["help", "man"],
143     show        => ["show", "cat"],
144     edit        => ["create", "edit", "new", "ed"],
145     list        => ["search", "list", "ls"],
146     comment     => ["comment", "correspond"],
147     link        => ["link", "ln"],
148     merge       => ["merge"],
149     grant       => ["grant", "revoke"],
150     take        => ["take", "steal", "untake"],
151     quit        => ["quit", "exit"],
152     setcommand  => ["del", "delete", "give", "res", "resolve",
153                     "subject"],
154 );
155
156 my %actions;
157 foreach my $fn (keys %handlers) {
158     foreach my $alias (@{ $handlers{$fn} }) {
159         $actions{$alias} = \&{"$fn"};
160     }
161 }
162
163 # Once we find and call an appropriate handler, we're done.
164
165 sub handler {
166     my $action;
167
168     push @ARGV, 'shell' if (!@ARGV);    # default to shell mode
169     shift @ARGV if ($ARGV[0] eq 'rt');    # ignore a leading 'rt'
170     if (@ARGV && exists $actions{$ARGV[0]}) {
171         $action = shift @ARGV;
172         return $actions{$action}->($action);
173     }
174     else {
175         print STDERR "rt: Unknown command '@ARGV'.\n";
176         print STDERR "rt: For help, run 'rt help'.\n";
177         return 1;
178     }
179 }
180
181 exit handler();
182
183 # Handler functions.
184 # ------------------
185 #
186 # The following subs are handlers for each entry in %actions.
187
188 sub shell {
189     $|=1;
190     my $term = Term::ReadLine->new('RT CLI');
191     while ( defined ($_ = $term->readline($prompt)) ) {
192         next if /^#/ || /^\s*$/;
193
194         @ARGV = shellwords($_);
195         handler();
196     }
197 }
198
199 sub version {
200     print "rt $VERSION\n";
201     return 0;
202 }
203
204 sub logout {
205     submit("$REST/logout") if defined $session->cookie;
206     return 0;
207 }
208
209 sub quit {
210     logout();
211     exit;
212 }
213
214 my %help;
215 sub help {
216     my ($action, $type, $rv) = @_;
217     $rv = defined $rv ? $rv : 0;
218     my $key;
219
220     # What help topics do we know about?
221     if (!%help) {
222         local $/ = undef;
223         foreach my $item (@{ Form::parse(<DATA>) }) {
224             my $title = $item->[2]{Title};
225             my @titles = ref $title eq 'ARRAY' ? @$title : $title;
226
227             foreach $title (grep $_, @titles) {
228                 $help{$title} = $item->[2]{Text};
229             }
230         }
231     }
232
233     # What does the user want help with?
234     undef $action if ($action && $actions{$action} eq \&help);
235     unless ($action || $type) {
236         # If we don't know, we'll look for clues in @ARGV.
237         foreach (@ARGV) {
238             if (exists $help{$_}) { $key = $_; last; }
239         }
240         unless ($key) {
241             # Tolerate possibly plural words.
242             foreach (@ARGV) {
243                 if ($_ =~ s/s$// && exists $help{$_}) { $key = $_; last; }
244             }
245         }
246     }
247
248     if ($type && $action) {
249         $key = "$type.$action";
250     }
251     $key ||= $type || $action || "introduction";
252
253     # Find a suitable topic to display.
254     while (!exists $help{$key}) {
255         if ($type && $action) {
256             if ($key eq "$type.$action") { $key = $action;        }
257             elsif ($key eq $action)      { $key = $type;          }
258             else                         { $key = "introduction"; }
259         }
260         else {
261             $key = "introduction";
262         }
263     }
264
265     print STDERR $help{$key}, "\n\n";
266     return $rv;
267 }
268
269 # Displays a list of objects that match some specified condition.
270
271 sub list {
272     my ($q, $type, %data);
273     my $orderby = $config{orderby};
274     
275     if ($config{orderby}) {
276          $data{orderby} = $config{orderby};
277     } 
278     my $bad = 0;
279     my $rawprint = 0;
280     my $reverse_sort = 0;
281     my $queue = $config{queue};
282
283     while (@ARGV) {
284         $_ = shift @ARGV;
285
286         if (/^-t$/) {
287             $bad = 1, last unless defined($type = get_type_argument());
288         }
289         elsif (/^-S$/) {
290             $bad = 1, last unless get_var_argument(\%data);
291         }
292         elsif (/^-o$/) {
293             $data{'orderby'} = shift @ARGV;
294         }
295         elsif (/^-([isl])$/) {
296             $data{format} = $1;
297             $rawprint = 1;
298         }
299         elsif (/^-q$/) {
300             $queue = shift @ARGV;
301         }
302         elsif (/^-r$/) {
303             $reverse_sort = 1;
304         }
305         elsif (/^-f$/) {
306             if ($ARGV[0] !~ /^(?:(?:$field,)*$field)$/) {
307                 whine "No valid field list in '-f $ARGV[0]'.";
308                 $bad = 1; last;
309             }
310             $data{fields} = shift @ARGV;
311             $data{format} = 's' if ! $data{format};
312             $rawprint = 1;
313         }
314         elsif (!defined $q && !/^-/) {
315             $q = $_;
316         }
317         else {
318             my $datum = /^-/ ? "option" : "argument";
319             whine "Unrecognised $datum '$_'.";
320             $bad = 1; last;
321         }
322     }
323     if ( ! $rawprint and ! exists $data{format} ) {
324         $data{format} = 'l';
325     }
326     if ( $reverse_sort and $data{orderby} =~ /^-/ ) {
327         $data{orderby} =~ s/^-/+/;
328     } elsif ($reverse_sort) {
329         $data{orderby} =~ s/^\+?(.*)/-$1/;
330     }
331
332     if (!defined $q) {
333         $q = $config{query}; 
334     }
335     
336     $q =~ s/^#//; # get rid of leading hash
337     if ($q =~ /^\d+$/) {
338         # only digits, must be an id, formulate a correct query
339         $q = "id=$q" if $q =~ /^\d+$/;
340     } else {
341         # a string only, take it as an owner or requestor (quoting done later)
342         $q = "(Owner=$q or Requestor like $q) and $config{query}"
343              if $q =~ /^[\w\-]+$/;
344         # always add a query for a specific queue or (comma separated) queues
345         $queue =~ s/,/ or Queue=/g if $queue;
346         $q .= " and (Queue=$queue)" if $queue and $q and $q !~ /Queue\s*=/i
347             and $q !~ /id\s*=/i;
348     }
349     # correctly quote strings in a query
350     $q =~ s/(=|like\s)\s*([^'\d\s]\S*)\b/$1\'$2\'/g;
351
352     $type ||= "ticket";
353     unless ($type && defined $q) {
354         my $item = $type ? "query string" : "object type";
355         whine "No $item specified.";
356         $bad = 1;
357     }
358     #return help("list", $type) if $bad;
359     return suggest_help("list", $type, $bad) if $bad;
360
361     print "Query:$q\n" if ! $rawprint;
362     my $r = submit("$REST/search/$type", { query => $q, %data });
363     if ( $rawprint ) {
364         print $r->content;
365     } else {
366         my $forms = Form::parse($r->content);
367         prettylist ($forms);
368     }
369     return 0;
370 }
371
372 # Displays selected information about a single object.
373
374 sub show {
375     my ($type, @objects, %data);
376     my $slurped = 0;
377     my $bad = 0;
378     my $rawprint = 0;
379     my $histspec;
380
381     while (@ARGV) {
382         $_ = shift @ARGV;
383         s/^#// if /^#\d+/; # get rid of leading hash
384         if (/^-t$/) {
385             $bad = 1, last unless defined($type = get_type_argument());
386         }
387         elsif (/^-S$/) {
388             $bad = 1, last unless get_var_argument(\%data);
389         }
390         elsif (/^-([isl])$/) {
391             $data{format} = $1;
392             $rawprint = 1;
393         }
394         elsif (/^-$/ && !$slurped) {
395             chomp(my @lines = <STDIN>);
396             foreach (@lines) {
397                 unless (is_object_spec($_, $type)) {
398                     whine "Invalid object on STDIN: '$_'.";
399                     $bad = 1; last;
400                 }
401                 push @objects, $_;
402             }
403             $slurped = 1;
404         }
405         elsif (/^-f$/) {
406             if ($ARGV[0] !~ /^(?:(?:$field,)*$field)$/) {
407                 whine "No valid field list in '-f $ARGV[0]'.";
408                 $bad = 1; last;
409             }
410             $data{fields} = shift @ARGV;
411             # option f requires short raw listing format
412             $data{format} = 's';
413             $rawprint = 1;
414         }
415         elsif (/^\d+$/ and my $spc2 = is_object_spec("ticket/$_", $type)) {
416             push @objects, $spc2;
417             $histspec = is_object_spec("ticket/$_/history", $type);
418         }
419         elsif (/^\d+\// and my $spc3 = is_object_spec("ticket/$_", $type)) {
420             push @objects, $spc3;
421             $rawprint = 1 if $_ =~ /\/content$/;
422         }
423         elsif (my $spec = is_object_spec($_, $type)) {
424             push @objects, $spec;
425             $rawprint = 1 if $_ =~ /\/content$/ or $_ =~ /\/links/ or $_ !~ /^ticket/;
426         }
427         else {
428             my $datum = /^-/ ? "option" : "argument";
429             whine "Unrecognised $datum '$_'.";
430             $bad = 1; last;
431         }
432     }
433     if ( ! $rawprint ) {
434         push @objects, $histspec if $histspec;
435         $data{format} = 'l' if ! exists $data{format};
436     }
437
438     unless (@objects) {
439         whine "No objects specified.";
440         $bad = 1;
441     }
442     #return help("show", $type) if $bad;
443     return suggest_help("show", $type, $bad) if $bad;
444
445     my $r = submit("$REST/show", { id => \@objects, %data });
446     my $c = $r->content;
447     # if this isn't a text reply, remove the trailing newline so we
448     # don't corrupt things like tarballs when people do
449     # show ticket/id/attachments/id/content > foo.tar.gz
450     if ($r->content_type !~ /^text\//) {
451         chomp($c);
452         $rawprint = 1;
453     }
454     if ( $rawprint ) {
455         print $c;
456     } else {
457         # I do not know how to get more than one form correctly returned
458         $c =~ s!^RT/[\d\.]+ 200 Ok$!--!mg;
459         my $forms = Form::parse($c);
460         prettyshow ($forms);
461     }
462     return 0;
463 }
464
465 # To create a new object, we ask the server for a form with the defaults
466 # filled in, allow the user to edit it, and send the form back.
467 #
468 # To edit an object, we must ask the server for a form representing that
469 # object, make changes requested by the user (either on the command line
470 # or interactively via $EDITOR), and send the form back.
471
472 sub edit {
473     my ($action) = @_;
474     my (%data, $type, @objects);
475     my ($cl, $text, $edit, $input, $output, $content_type);
476
477     use vars qw(%set %add %del);
478     %set = %add = %del = ();
479     my $slurped = 0;
480     my $bad = 0;
481     
482     while (@ARGV) {
483         $_ = shift @ARGV;
484         s/^#// if /^#\d+/; # get rid of leading hash
485
486         if    (/^-e$/) { $edit = 1 }
487         elsif (/^-i$/) { $input = 1 }
488         elsif (/^-o$/) { $output = 1 }
489         elsif (/^-ct$/) { $content_type = shift @ARGV }
490         elsif (/^-t$/) {
491             $bad = 1, last unless defined($type = get_type_argument());
492         }
493         elsif (/^-S$/) {
494             $bad = 1, last unless get_var_argument(\%data);
495         }
496         elsif (/^-$/ && !($slurped || $input)) {
497             chomp(my @lines = <STDIN>);
498             foreach (@lines) {
499                 unless (is_object_spec($_, $type)) {
500                     whine "Invalid object on STDIN: '$_'.";
501                     $bad = 1; last;
502                 }
503                 push @objects, $_;
504             }
505             $slurped = 1;
506         }
507         elsif (/^set$/i) {
508             my $vars = 0;
509
510             while (@ARGV && $ARGV[0] =~ /^($field)([+-]?=)(.*)$/s) {
511                 my ($key, $op, $val) = ($1, $2, $3);
512                 my $hash = ($op eq '=') ? \%set : ($op =~ /^\+/) ? \%add : \%del;
513
514                 vpush($hash, lc $key, $val);
515                 shift @ARGV;
516                 $vars++;
517             }
518             unless ($vars) {
519                 whine "No variables to set.";
520                 $bad = 1; last;
521             }
522             $cl = $vars;
523         }
524         elsif (/^(?:add|del)$/i) {
525             my $vars = 0;
526             my $hash = ($_ eq "add") ? \%add : \%del;
527
528             while (@ARGV && $ARGV[0] =~ /^($field)=(.*)$/s) {
529                 my ($key, $val) = ($1, $2);
530
531                 vpush($hash, lc $key, $val);
532                 shift @ARGV;
533                 $vars++;
534             }
535             unless ($vars) {
536                 whine "No variables to set.";
537                 $bad = 1; last;
538             }
539             $cl = $vars;
540         }
541         elsif (/^\d+$/ and my $spc2 = is_object_spec("ticket/$_", $type)) {
542             push @objects, $spc2;
543         }
544         elsif (my $spec = is_object_spec($_, $type)) {
545             push @objects, $spec;
546         }
547         else {
548             my $datum = /^-/ ? "option" : "argument";
549             whine "Unrecognised $datum '$_'.";
550             $bad = 1; last;
551         }
552     }
553
554     if ($action =~ /^ed(?:it)?$/) {
555         unless (@objects) {
556             whine "No objects specified.";
557             $bad = 1;
558         }
559     }
560     else {
561         if (@objects) {
562             whine "You shouldn't specify objects as arguments to $action.";
563             $bad = 1;
564         }
565         unless ($type) {
566             whine "What type of object do you want to create?";
567             $bad = 1;
568         }
569         @objects = ("$type/new") if defined($type);
570     }
571     #return help($action, $type) if $bad;
572     return suggest_help($action, $type, $bad) if $bad;
573
574     # We need a form to make changes to. We usually ask the server for
575     # one, but we can avoid that if we are fed one on STDIN, or if the
576     # user doesn't want to edit the form by hand, and the command line
577     # specifies only simple variable assignments.  We *should* get a
578     # form if we're creating a new ticket, so that the default values
579     # get filled in properly.
580
581     my @new_objects = grep /\/new$/, @objects;
582
583     if ($input) {
584         local $/ = undef;
585         $text = <STDIN>;
586     }
587     elsif ($edit || %add || %del || !$cl || @new_objects) {
588         my $r = submit("$REST/show", { id => \@objects, format => 'l' });
589         $text = $r->content;
590     }
591
592     # If any changes were specified on the command line, apply them.
593     if ($cl) {
594         if ($text) {
595             # We're updating forms from the server.
596             my $forms = Form::parse($text);
597
598             foreach my $form (@$forms) {
599                 my ($c, $o, $k, $e) = @$form;
600                 my ($key, $val);
601
602                 next if ($e || !@$o);
603
604                 local %add = %add;
605                 local %del = %del;
606                 local %set = %set;
607
608                 # Make changes to existing fields.
609                 foreach $key (@$o) {
610                     if (exists $add{lc $key}) {
611                         $val = delete $add{lc $key};
612                         vpush($k, $key, $val);
613                         $k->{$key} = vsplit($k->{$key}) if $val =~ /[,\n]/;
614                     }
615                     if (exists $del{lc $key}) {
616                         $val = delete $del{lc $key};
617                         my %val = map {$_=>1} @{ vsplit($val) };
618                         $k->{$key} = vsplit($k->{$key});
619                         @{$k->{$key}} = grep {!exists $val{$_}} @{$k->{$key}};
620                     }
621                     if (exists $set{lc $key}) {
622                         $k->{$key} = delete $set{lc $key};
623                     }
624                 }
625                 
626                 # Then update the others.
627                 foreach $key (keys %set) { vpush($k, $key, $set{$key}) }
628                 foreach $key (keys %add) {
629                     vpush($k, $key, $add{$key});
630                     $k->{$key} = vsplit($k->{$key});
631                 }
632                 push @$o, (keys %add, keys %set);
633             }
634
635             $text = Form::compose($forms);
636         }
637         else {
638             # We're rolling our own set of forms.
639             my @forms;
640             foreach (@objects) {
641                 my ($type, $ids, $args) =
642                     m{^($name)/($idlist|$labels)(?:(/.*))?$}o;
643
644                 $args ||= "";
645                 foreach my $obj (expand_list($ids)) {
646                     my %set = (%set, id => "$type/$obj$args");
647                     push @forms, ["", [keys %set], \%set];
648                 }
649             }
650             $text = Form::compose(\@forms);
651         }
652     }
653
654     if ($output) {
655         print $text;
656         return 0;
657     }
658
659     my @files;
660     @files = @{ vsplit($set{'attachment'}) } if exists $set{'attachment'};
661
662     my $synerr = 0;
663
664 EDIT:
665     # We'll let the user edit the form before sending it to the server,
666     # unless we have enough information to submit it non-interactively.
667     if ( $type && $type eq 'ticket' && $text !~ /^Content-Type:/m ) {
668         $text .= "Content-Type: $content_type\n"
669             if $content_type and $content_type ne "text/plain";
670     }
671
672     if ($edit || (!$input && !$cl)) {
673         my ($newtext) = vi_form_while(
674             $text,
675             sub {
676                 my ($text, $form) = @_;
677                 return 1 unless exists $form->[2]{'Attachment'};
678
679                 foreach my $f ( @{ vsplit($form->[2]{'Attachment'}) } ) {
680                     return (0, "File '$f' doesn't exist") unless -f $f;
681                 }
682                 @files = @{ vsplit($form->[2]{'Attachment'}) };
683                 return 1;
684             },
685         );
686         return $newtext unless $newtext;
687         # We won't resubmit a bad form unless it was changed.
688         $text = ($synerr && $newtext eq $text) ? undef : $newtext;
689     }
690
691     delete @data{ grep /^attachment_\d+$/, keys %data };
692     my $i = 1;
693     foreach my $file (@files) {
694         $data{"attachment_$i"} = bless([ $file ], "Attachment");
695         $i++;
696     }
697
698     if ($text) {
699         my $r = submit("$REST/edit", {content => $text, %data});
700         if ($r->code == 409) {
701             # If we submitted a bad form, we'll give the user a chance
702             # to correct it and resubmit.
703             if ($edit || (!$input && !$cl)) {
704                 my $content = $r->content . "\n";
705                 $content =~ s/^(?!#)/#     /mg;
706                 $text = $content . $text;
707                 $synerr = 1;
708                 goto EDIT;
709             }
710             else {
711                 print $r->content;
712                 return 0;
713             }
714         }
715         print $r->content;
716     }
717     return 0;
718 }
719
720 # handler for special edit commands. A valid edit command is constructed and
721 # further work is delegated to the edit handler
722
723 sub setcommand {
724     my ($action) = @_;
725     my ($id, $bad, $what);
726     if ( @ARGV ) {
727         $_ = shift @ARGV;
728         $id = $1 if (m|^(?:ticket/)?($idlist)$|);
729     }
730     if ( ! $id ) {
731         $bad = 1;
732         whine "No ticket number specified.";
733     }
734     if ( @ARGV ) {
735         if ($action eq 'subject') {
736             my $subject = '"'.join (" ", @ARGV).'"';
737             @ARGV = ();
738             $what = "subject=$subject";
739         } elsif ($action eq 'give') {
740             my $owner = shift @ARGV;
741             $what = "owner=$owner";
742         }
743     } else {
744         if ( $action eq 'delete' or $action eq 'del' ) {
745             $what = "status=deleted";
746         } elsif ($action eq 'resolve' or $action eq 'res' ) {
747             $what = "status=resolved";
748         } elsif ($action eq 'take' ) {
749             $what = "owner=$config{user}";
750         } elsif ($action eq 'untake') {
751             $what = "owner=Nobody";
752         }
753     }
754     if (@ARGV) {
755         $bad = 1;
756         whine "Extraneous arguments for action $action: @ARGV.";
757     }
758     if ( ! $what ) {
759         $bad = 1;
760         whine "unrecognized action $action.";
761     }
762     return help("edit", undef, $bad) if $bad;
763     @ARGV = ( $id, "set", $what );
764     print "Executing: rt edit @ARGV\n";
765     return edit("edit");
766 }
767
768 # We roll "comment" and "correspond" into the same handler.
769
770 sub comment {
771     my ($action) = @_;
772     my (%data, $id, @files, @bcc, @cc, $msg, $content_type, $wtime, $edit);
773     my $bad = 0;
774
775     while (@ARGV) {
776         $_ = shift @ARGV;
777
778         if (/^-e$/) {
779             $edit = 1;
780         }
781         elsif (/^-(?:[abcmw]|ct)$/) {
782             unless (@ARGV) {
783                 whine "No argument specified with $_.";
784                 $bad = 1; last;
785             }
786
787             if (/-a/) {
788                 unless (-f $ARGV[0] && -r $ARGV[0]) {
789                     whine "Cannot read attachment: '$ARGV[0]'.";
790                     return 0;
791                 }
792                 push @files, shift @ARGV;
793             }
794             elsif (/-ct/) {
795                 $content_type = shift @ARGV;
796             }
797             elsif (/-([bc])/) {
798                 my $a = $_ eq "-b" ? \@bcc : \@cc;
799                 @$a = split /\s*,\s*/, shift @ARGV;
800             }
801             elsif (/-m/) {
802                 $msg = shift @ARGV;
803                 if ( $msg =~ /^-$/ ) {
804                     undef $msg;
805                     while (<STDIN>) { $msg .= $_ }
806                 }
807             }
808             elsif (/-w/) { $wtime = shift @ARGV }
809         }
810         elsif (!$id && m|^(?:ticket/)?($idlist)$|) {
811             $id = $1;
812         }
813         else {
814             my $datum = /^-/ ? "option" : "argument";
815             whine "Unrecognised $datum '$_'.";
816             $bad = 1; last;
817         }
818     }
819
820     unless ($id) {
821         whine "No object specified.";
822         $bad = 1;
823     }
824     #return help($action, "ticket") if $bad;
825     return suggest_help($action, "ticket") if $bad;
826
827     my $form = [
828         "",
829         [ "Ticket", "Action", "Cc", "Bcc", "Attachment", "TimeWorked", "Content-Type", "Text" ],
830         {
831             Ticket     => $id,
832             Action     => $action,
833             Cc         => [ @cc ],
834             Bcc        => [ @bcc ],
835             Attachment => [ @files ],
836             TimeWorked => $wtime || '',
837             'Content-Type' => $content_type || 'text/plain',
838             Text       => $msg || '',
839             Status => ''
840         }
841     ];
842
843     my $text = Form::compose([ $form ]);
844
845     if ($edit || !$msg) {
846         my ($tmp) = vi_form_while(
847             $text,
848             sub {
849                 my ($text, $form) = @_;
850                 foreach my $f ( @{ vsplit($form->[2]{'Attachment'}) } ) {
851                     return (0, "File '$f' doesn't exist") unless -f $f;
852                 }
853                 @files = @{ vsplit($form->[2]{'Attachment'}) };
854                 return 1;
855             },
856         );
857         return $tmp unless $tmp;
858         $text = $tmp;
859     }
860
861     my $i = 1;
862     foreach my $file (@files) {
863         $data{"attachment_$i"} = bless([ $file ], "Attachment");
864         $i++;
865     }
866     $data{content} = $text;
867
868     my $r = submit("$REST/ticket/$id/comment", \%data);
869     print $r->content;
870     return 0;
871 }
872
873 # Merge one ticket into another.
874
875 sub merge {
876     my @id;
877     my $bad = 0;
878
879     while (@ARGV) {
880         $_ = shift @ARGV;
881         s/^#// if /^#\d+/; # get rid of leading hash
882
883         if (/^\d+$/) {
884             push @id, $_;
885         }
886         else {
887             whine "Unrecognised argument: '$_'.";
888             $bad = 1; last;
889         }
890     }
891
892     unless (@id == 2) {
893         my $evil = @id > 2 ? "many" : "few";
894         whine "Too $evil arguments specified.";
895         $bad = 1;
896     }
897     #return help("merge", "ticket") if $bad;
898     return suggest_help("merge", "ticket", $bad) if $bad;
899
900     my $r = submit("$REST/ticket/$id[0]/merge/$id[1]");
901     print $r->content;
902     return 0;
903 }
904
905 # Link one ticket to another.
906
907 sub link {
908     my ($bad, $del, %data) = (0, 0, ());
909     my $type;
910
911     my %ltypes = map { lc $_ => $_ } qw(DependsOn DependedOnBy RefersTo
912                                         ReferredToBy HasMember MemberOf);
913
914     while (@ARGV && $ARGV[0] =~ /^-/) {
915         $_ = shift @ARGV;
916
917         if (/^-d$/) {
918             $del = 1;
919         }
920         elsif (/^-t$/) {
921             $bad = 1, last unless defined($type = get_type_argument());
922         }
923         else {
924             whine "Unrecognised option: '$_'.";
925             $bad = 1; last;
926         }
927     }
928     
929     $type = "ticket" unless $type; # default type to tickets
930     
931     if (@ARGV == 3) {
932         my ($from, $rel, $to) = @ARGV;
933         if (($type eq "ticket") && ( ! exists $ltypes{lc $rel})) {
934             whine "Invalid link '$rel' for type $type specified.";
935             $bad = 1;
936         }
937         %data = (id => $from, rel => $rel, to => $to, del => $del);
938     }
939     else {
940         my $bad = @ARGV < 3 ? "few" : "many";
941         whine "Too $bad arguments specified.";
942         $bad = 1;
943     }
944     return suggest_help("link", $type, $bad) if $bad;
945  
946     my $r = submit("$REST/$type/link", \%data);
947     print $r->content;
948     return 0;
949 }
950
951 # Take/steal a ticket
952 sub take {
953     my ($cmd) = @_;
954     my ($bad, %data) = (0, ());
955
956     my $id;
957
958     # get the ticket id
959     if (@ARGV == 1) {
960         ($id) = @ARGV;
961         unless ($id =~ /^\d+$/) {
962             whine "Invalid ticket ID $id specified.";
963             $bad = 1;
964         }
965         my $form = [
966             "",
967             [ "Ticket", "Action" ],
968             {
969                 Ticket => $id,
970                 Action => $cmd,
971                 Status => '',
972             }
973         ];
974
975         my $text = Form::compose([ $form ]);
976         $data{content} = $text;
977     }
978     else {
979         $bad = @ARGV < 1 ? "few" : "many";
980         whine "Too $bad arguments specified.";
981         $bad = 1;
982     }
983     return suggest_help("take", "ticket", $bad) if $bad;
984
985     my $r = submit("$REST/ticket/$id/take", \%data);
986     print $r->content;
987     return 0;
988 }
989
990 # Grant/revoke a user's rights.
991
992 sub grant {
993     my ($cmd) = @_;
994
995     whine "$cmd is unimplemented.";
996     return 1;
997 }
998
999 # Client <-> Server communication.
1000 # --------------------------------
1001 #
1002 # This function composes and sends an HTTP request to the RT server, and
1003 # interprets the response. It takes a request URI, and optional request
1004 # data (a string, or a reference to a set of key-value pairs).
1005
1006 sub submit {
1007     my ($uri, $content) = @_;
1008     my ($req, $data);
1009     my $ua = LWP::UserAgent->new(agent => "RT/3.0b", env_proxy => 1);
1010     my $h = HTTP::Headers->new;
1011
1012     # Did the caller specify any data to send with the request?
1013     $data = [];
1014     if (defined $content) {
1015         unless (ref $content) {
1016             # If it's just a string, make sure LWP handles it properly.
1017             # (By pretending that it's a file!)
1018             $content = [ content => [undef, "", Content => $content] ];
1019         }
1020         elsif (ref $content eq 'HASH') {
1021             my @data;
1022             foreach my $k (keys %$content) {
1023                 if (ref $content->{$k} eq 'ARRAY') {
1024                     foreach my $v (@{ $content->{$k} }) {
1025                         push @data, $k, $v;
1026                     }
1027                 }
1028                 else { push @data, $k, $content->{$k} }
1029             }
1030             $content = \@data;
1031         }
1032         $data = $content;
1033     }
1034
1035     # Should we send authentication information to start a new session?
1036     my $how = $config{server} =~ /^https/ ? 'over SSL' : 'unencrypted';
1037     (my $server = $config{server}) =~ s/^.*\/\/([^\/]+)\/?/$1/;
1038     if ($config{externalauth}) {
1039         $h->authorization_basic($config{user}, $config{passwd} || read_passwd() );
1040         print "   Password will be sent to $server $how\n",
1041               "   Press CTRL-C now if you do not want to continue\n"
1042             if ! $config{passwd};
1043     } elsif ( $no_strong_auth ) {
1044         if (!defined $session->cookie) {
1045             print "   Strong encryption not available, $no_strong_auth\n",
1046                   "   Password will be sent to $server $how\n",
1047                   "   Press CTRL-C now if you do not want to continue\n"
1048                 if ! $config{passwd};
1049             push @$data, ( user => $config{user} );
1050             push @$data, ( pass => $config{passwd} || read_passwd() );
1051         }
1052     }
1053
1054     # Now, we construct the request.
1055     if (@$data) {
1056         $req = POST($uri, $data, Content_Type => 'form-data');
1057     }
1058     else {
1059         $req = GET($uri);
1060     }
1061     $session->add_cookie_header($req);
1062     if ($config{externalauth}) {
1063         $req->header(%$h);
1064     }
1065
1066     # Then we send the request and parse the response.
1067     DEBUG(3, $req->as_string);
1068     my $res = $ua->request($req);
1069     DEBUG(3, $res->as_string);
1070
1071     if ($res->is_success) {
1072         # The content of the response we get from the RT server consists
1073         # of an HTTP-like status line followed by optional header lines,
1074         # a blank line, and arbitrary text.
1075
1076         my ($head, $text) = split /\n\n/, $res->content, 2;
1077         my ($status, @headers) = split /\n/, $head;
1078         $text =~ s/\n*$/\n/ if ($text);
1079
1080         # "RT/3.0.1 401 Credentials required"
1081         if ($status !~ m#^RT/\d+(?:\S+) (\d+) ([\w\s]+)$#) {
1082             warn "rt: Malformed RT response from $config{server}.\n";
1083             warn "(Rerun with RTDEBUG=3 for details.)\n" if $config{debug} < 3;
1084             exit -1;
1085         }
1086
1087         # Our caller can pretend that the server returned a custom HTTP
1088         # response code and message. (Doing that directly is apparently
1089         # not sufficiently portable and uncomplicated.)
1090         $res->code($1);
1091         $res->message($2);
1092         $res->content($text);
1093         $session->update($res) if ($res->is_success || $res->code != 401);
1094
1095         if (!$res->is_success) {
1096             # We can deal with authentication failures ourselves. Either
1097             # we sent invalid credentials, or our session has expired.
1098             if ($res->code == 401) {
1099                 my %d = @$data;
1100                 if (exists $d{user}) {
1101                     warn "rt: Incorrect username or password.\n";
1102                     exit -1;
1103                 }
1104                 elsif ($req->header("Cookie")) {
1105                     # We'll retry the request with credentials, unless
1106                     # we only wanted to logout in the first place.
1107                     $session->delete;
1108                     return submit(@_) unless $uri eq "$REST/logout";
1109                 }
1110             }
1111             # Conflicts should be dealt with by the handler and user.
1112             # For anything else, we just die.
1113             elsif ($res->code != 409) {
1114                 warn "rt: ", $res->content;
1115                 #exit;
1116             }
1117         }
1118     }
1119     else {
1120         warn "rt: Server error: ", $res->message, " (", $res->code, ")\n";
1121         exit -1;
1122     }
1123
1124     return $res;
1125 }
1126
1127 # Session management.
1128 # -------------------
1129 #
1130 # Maintains a list of active sessions in the ~/.rt_sessions file.
1131 {
1132     package Session;
1133     my ($s, $u);
1134
1135     # Initialises the session cache.
1136     sub new {
1137         my ($class, $file) = @_;
1138         my $self = {
1139             file => $file || "$HOME/.rt_sessions",
1140             sids => { }
1141         };
1142        
1143         # The current session is identified by the currently configured
1144         # server and user.
1145         ($s, $u) = @config{"server", "user"};
1146
1147         bless $self, $class;
1148         $self->load();
1149
1150         return $self;
1151     }
1152
1153     # Returns the current session cookie.
1154     sub cookie {
1155         my ($self) = @_;
1156         my $cookie = $self->{sids}{$s}{$u};
1157         return defined $cookie ? "RT_SID_$cookie" : undef;
1158     }
1159
1160     # Deletes the current session cookie.
1161     sub delete {
1162         my ($self) = @_;
1163         delete $self->{sids}{$s}{$u};
1164     }
1165
1166     # Adds a Cookie header to an outgoing HTTP request.
1167     sub add_cookie_header {
1168         my ($self, $request) = @_;
1169         my $cookie = $self->cookie();
1170
1171         $request->header(Cookie => $cookie) if defined $cookie;
1172     }
1173
1174     # Extracts the Set-Cookie header from an HTTP response, and updates
1175     # session information accordingly.
1176     sub update {
1177         my ($self, $response) = @_;
1178         my $cookie = $response->header("Set-Cookie");
1179
1180         if (defined $cookie && $cookie =~ /^RT_SID_(.[^;,\s]+=[0-9A-Fa-f]+);/) {
1181             $self->{sids}{$s}{$u} = $1;
1182         }
1183     }
1184
1185     # Loads the session cache from the specified file.
1186     sub load {
1187         my ($self, $file) = @_;
1188         $file ||= $self->{file};
1189
1190         open( my $handle, '<', $file ) or return 0;
1191
1192         $self->{file} = $file;
1193         my $sids = $self->{sids} = {};
1194         while (<$handle>) {
1195             chomp;
1196             next if /^$/ || /^#/;
1197             next unless m#^https?://[^ ]+ \w+ [^;,\s]+=[0-9A-Fa-f]+$#;
1198             my ($server, $user, $cookie) = split / /, $_;
1199             $sids->{$server}{$user} = $cookie;
1200         }
1201         return 1;
1202     }
1203
1204     # Writes the current session cache to the specified file.
1205     sub save {
1206         my ($self, $file) = shift;
1207         $file ||= $self->{file};
1208
1209         open( my $handle, '>', "$file" ) or return 0;
1210
1211         my $sids = $self->{sids};
1212         foreach my $server (keys %$sids) {
1213             foreach my $user (keys %{ $sids->{$server} }) {
1214                 my $sid = $sids->{$server}{$user};
1215                 if (defined $sid) {
1216                     print $handle "$server $user $sid\n";
1217                 }
1218             }
1219         }
1220         close($handle);
1221         chmod 0600, $file;
1222         return 1;
1223     }
1224
1225     sub DESTROY {
1226         my $self = shift;
1227         $self->save;
1228     }
1229 }
1230
1231 # Form handling.
1232 # --------------
1233 #
1234 # Forms are RFC822-style sets of (field, value) specifications with some
1235 # initial comments and interspersed blank lines allowed for convenience.
1236 # Sets of forms are separated by --\n (in a cheap parody of MIME).
1237 #
1238 # Each form is parsed into an array with four elements: commented text
1239 # at the start of the form, an array with the order of keys, a hash with
1240 # key/value pairs, and optional error text if the form syntax was wrong.
1241
1242 # Returns a reference to an array of parsed forms.
1243 sub Form::parse {
1244     my $state = 0;
1245     my @forms = ();
1246     my @lines = split /\n/, $_[0] if $_[0];
1247     my ($c, $o, $k, $e) = ("", [], {}, "");
1248
1249     LINE:
1250     while (@lines) {
1251         my $line = shift @lines;
1252
1253         next LINE if $line eq '';
1254
1255         if ($line eq '--') {
1256             # We reached the end of one form. We'll ignore it if it was
1257             # empty, and store it otherwise, errors and all.
1258             if ($e || $c || @$o) {
1259                 push @forms, [ $c, $o, $k, $e ];
1260                 $c = ""; $o = []; $k = {}; $e = "";
1261             }
1262             $state = 0;
1263         }
1264         elsif ($state != -1) {
1265             if ($state == 0 && $line =~ /^#/) {
1266                 # Read an optional block of comments (only) at the start
1267                 # of the form.
1268                 $state = 1;
1269                 $c = $line;
1270                 while (@lines && $lines[0] =~ /^#/) {
1271                     $c .= "\n".shift @lines;
1272                 }
1273                 $c .= "\n";
1274             }
1275             elsif ($state <= 1 && $line =~ /^($field):(?:\s+(.*))?$/) {
1276                 # Read a field: value specification.
1277                 my $f  = $1;
1278                 my @v  = ($2 || ());
1279
1280                 # Read continuation lines, if any.
1281                 while (@lines && ($lines[0] eq '' || $lines[0] =~ /^\s+/)) {
1282                     push @v, shift @lines;
1283                 }
1284                 pop @v while (@v && $v[-1] eq '');
1285
1286                 # Strip longest common leading indent from text.
1287                 my $ws = "";
1288                 foreach my $ls (map {/^(\s+)/} @v[1..$#v]) {
1289                     $ws = $ls if (!$ws || length($ls) < length($ws));
1290                 }
1291                 s/^$ws// foreach @v;
1292
1293                 push(@$o, $f) unless exists $k->{$f};
1294                 vpush($k, $f, join("\n", @v));
1295
1296                 $state = 1;
1297             }
1298             elsif ($line !~ /^#/) {
1299                 # We've found a syntax error, so we'll reconstruct the
1300                 # form parsed thus far, and add an error marker. (>>)
1301                 $state = -1;
1302                 $e = Form::compose([[ "", $o, $k, "" ]]);
1303                 $e.= $line =~ /^>>/ ? "$line\n" : ">> $line\n";
1304             }
1305         }
1306         else {
1307             # We saw a syntax error earlier, so we'll accumulate the
1308             # contents of this form until the end.
1309             $e .= "$line\n";
1310         }
1311     }
1312     push(@forms, [ $c, $o, $k, $e ]) if ($e || $c || @$o);
1313
1314     foreach my $l (keys %$k) {
1315         $k->{$l} = vsplit($k->{$l}) if (ref $k->{$l} eq 'ARRAY');
1316     }
1317
1318     return \@forms;
1319 }
1320
1321 # Returns text representing a set of forms.
1322 sub Form::compose {
1323     my ($forms) = @_;
1324     my @text;
1325
1326     foreach my $form (@$forms) {
1327         my ($c, $o, $k, $e) = @$form;
1328         my $text = "";
1329
1330         if ($c) {
1331             $c =~ s/\n*$/\n/;
1332             $text = "$c\n";
1333         }
1334         if ($e) {
1335             $text .= $e;
1336         }
1337         elsif ($o) {
1338             my @lines;
1339
1340             foreach my $key (@$o) {
1341                 my ($line, $sp);
1342                 my $v = $k->{$key};
1343                 my @values = ref $v eq 'ARRAY' ? @$v : $v;
1344
1345                 $sp = " "x(length("$key: "));
1346                 $sp = " "x4 if length($sp) > 16;
1347
1348                 foreach $v (@values) {
1349                     if ($v =~ /\n/) {
1350                         $v =~ s/^/$sp/gm;
1351                         $v =~ s/^$sp//;
1352
1353                         if ($line) {
1354                             push @lines, "$line\n\n";
1355                             $line = "";
1356                         }
1357                         elsif (@lines && $lines[-1] !~ /\n\n$/) {
1358                             $lines[-1] .= "\n";
1359                         }
1360                         push @lines, "$key: $v\n\n";
1361                     }
1362                     elsif ($line &&
1363                            length($line)+length($v)-rindex($line, "\n") >= 70)
1364                     {
1365                         $line .= ",\n$sp$v";
1366                     }
1367                     else {
1368                         $line = $line ? "$line,$v" : "$key: $v";
1369                     }
1370                 }
1371
1372                 $line = "$key:" unless @values;
1373                 if ($line) {
1374                     if ($line =~ /\n/) {
1375                         if (@lines && $lines[-1] !~ /\n\n$/) {
1376                             $lines[-1] .= "\n";
1377                         }
1378                         $line .= "\n";
1379                     }
1380                     push @lines, "$line\n";
1381                 }
1382             }
1383
1384             $text .= join "", @lines;
1385         }
1386         else {
1387             chomp $text;
1388         }
1389         push @text, $text;
1390     }
1391
1392     return join "\n--\n\n", @text;
1393 }
1394
1395 # Configuration.
1396 # --------------
1397
1398 # Returns configuration information from the environment.
1399 sub config_from_env {
1400     my %env;
1401
1402     foreach my $k (qw(EXTERNALAUTH DEBUG USER PASSWD SERVER QUERY ORDERBY)) {
1403
1404         if (exists $ENV{"RT$k"}) {
1405             $env{lc $k} = $ENV{"RT$k"};
1406         }
1407     }
1408
1409     return %env;
1410 }
1411
1412 # Finds a suitable configuration file and returns information from it.
1413 sub config_from_file {
1414     my ($rc) = @_;
1415
1416     if ($rc =~ m#^/#) {
1417         # We'll use an absolute path if we were given one.
1418         return parse_config_file($rc);
1419     }
1420     else {
1421         # Otherwise we'll use the first file we can find in the current
1422         # directory, or in one of its (increasingly distant) ancestors.
1423
1424         my @dirs = split /\//, cwd;
1425         while (@dirs) {
1426             my $file = join('/', @dirs, $rc);
1427             if (-r $file) {
1428                 return parse_config_file($file);
1429             }
1430
1431             # Remove the last directory component each time.
1432             pop @dirs;
1433         }
1434
1435         # Still nothing? We'll fall back to some likely defaults.
1436         for ("$HOME/$rc", "/opt/rt3/local/etc/rt.conf", "/etc/rt.conf") {
1437             return parse_config_file($_) if (-r $_);
1438         }
1439     }
1440
1441     return ();
1442 }
1443
1444 # Makes a hash of the specified configuration file.
1445 sub parse_config_file {
1446     my %cfg;
1447     my ($file) = @_;
1448     local $_; # $_ may be aliased to a constant, from line 1163
1449
1450     open( my $handle, '<', $file ) or return;
1451
1452     while (<$handle>) {
1453         chomp;
1454         next if (/^#/ || /^\s*$/);
1455
1456         if (/^(externalauth|user|passwd|server|query|orderby|queue)\s+(.*)\s?$/) {
1457             $cfg{$1} = $2;
1458         }
1459         else {
1460             die "rt: $file:$.: unknown configuration directive.\n";
1461         }
1462     }
1463
1464     return %cfg;
1465 }
1466
1467 # Helper functions.
1468 # -----------------
1469
1470 sub whine {
1471     my $sub = (caller(1))[3];
1472     $sub =~ s/^main:://;
1473     warn "rt: $sub: @_\n";
1474     return 0;
1475 }
1476
1477 sub read_passwd {
1478     eval 'require Term::ReadKey';
1479     if ($@) {
1480         die "No password specified (and Term::ReadKey not installed).\n";
1481     }
1482
1483     print "Password: ";
1484     Term::ReadKey::ReadMode('noecho');
1485     chomp(my $passwd = Term::ReadKey::ReadLine(0));
1486     Term::ReadKey::ReadMode('restore');
1487     print "\n";
1488
1489     return $passwd;
1490 }
1491
1492 sub vi_form_while {
1493     my $text = shift;
1494     my $cb = shift;
1495
1496     my $error = 0;
1497     my ($c, $o, $k, $e);
1498     do {
1499         my $ntext = vi($text);
1500         return undef if ($error && $ntext eq $text);
1501
1502         $text = $ntext;
1503
1504         my $form = Form::parse($text);
1505         $error = 0;
1506         ($c, $o, $k, $e) = @{ $form->[0] };
1507         if ( $e ) {
1508             $error = 1;
1509             $c = "# Syntax error.";
1510             goto NEXT;
1511         }
1512         elsif (!@$o) {
1513             return 0;
1514         }
1515
1516         my ($status, $msg) = $cb->( $text, [$c, $o, $k, $e] );
1517         unless ( $status ) {
1518             $error = 1;
1519             $c = "# $msg";
1520         }
1521
1522     NEXT:
1523         $text = Form::compose([[$c, $o, $k, $e]]);
1524     } while ($error);
1525
1526     return $text;
1527 }
1528
1529 sub vi {
1530     my ($text) = @_;
1531     my $editor = $ENV{EDITOR} || $ENV{VISUAL} || "vi";
1532
1533     local $/ = undef;
1534
1535     my $handle = File::Temp->new;
1536     print $handle $text;
1537     close($handle);
1538
1539     system($editor, $handle->filename) && die "Couldn't run $editor.\n";
1540
1541     open( $handle, '<', $handle->filename ) or die "$handle: $!\n";
1542     $text = <$handle>;
1543     close($handle);
1544
1545     return $text;
1546 }
1547
1548 # Add a value to a (possibly multi-valued) hash key.
1549 sub vpush {
1550     my ($hash, $key, $val) = @_;
1551     my @val = ref $val eq 'ARRAY' ? @$val : $val;
1552
1553     if (exists $hash->{$key}) {
1554         unless (ref $hash->{$key} eq 'ARRAY') {
1555             my @v = $hash->{$key} ne '' ? $hash->{$key} : ();
1556             $hash->{$key} = \@v;
1557         }
1558         push @{ $hash->{$key} }, @val;
1559     }
1560     else {
1561         $hash->{$key} = $val;
1562     }
1563 }
1564
1565 # "Normalise" a hash key that's known to be multi-valued.
1566 sub vsplit {
1567     my ($val) = @_;
1568     my ($word, @words);
1569     my @values = ref $val eq 'ARRAY' ? @$val : $val;
1570
1571     foreach my $line (map {split /\n/} @values) {
1572         # XXX: This should become a real parser, Ã  la Text::ParseWords.
1573         $line =~ s/^\s+//;
1574         $line =~ s/\s+$//;
1575         my ( $a, $b ) = split /\s*,\s*/, $line, 2;
1576
1577         while ($a) {
1578             no warnings 'uninitialized';
1579             if ( $a =~ /^'/ ) {
1580                 my $s = $a;
1581                 while ( $a !~ /'$/ || (   $a !~ /(\\\\)+'$/
1582                             && $a =~ /(\\)+'$/ )) {
1583                     ( $a, $b ) = split /\s*,\s*/, $b, 2;
1584                     $s .= ',' . $a;
1585                 }
1586                 push @words, $s;
1587             }
1588             elsif ( $a =~ /^q\{/ ) {
1589                 my $s = $a;
1590                 while ( $a !~ /\}$/ ) {
1591                     ( $a, $b ) =
1592                       split /\s*,\s*/, $b, 2;
1593                     $s .= ',' . $a;
1594                 }
1595                 $s =~ s/^q\{/'/;
1596                 $s =~ s/\}/'/;
1597                 push @words, $s;
1598             }
1599             else {
1600                 push @words, $a;
1601             }
1602             ( $a, $b ) = split /\s*,\s*/, $b, 2;
1603         }
1604
1605
1606     }
1607
1608     return \@words;
1609 }
1610
1611 # WARN: this code is duplicated in lib/RT/Interface/REST.pm
1612 # change both functions at once
1613 sub expand_list {
1614     my ($list) = @_;
1615
1616     my @elts;
1617     foreach (split /\s*,\s*/, $list) {
1618         push @elts, /^(\d+)-(\d+)$/? ($1..$2): $_;
1619     }
1620
1621     return map $_->[0], # schwartzian transform
1622         sort {
1623             defined $a->[1] && defined $b->[1]?
1624                 # both numbers
1625                 $a->[1] <=> $b->[1]
1626                 :!defined $a->[1] && !defined $b->[1]?
1627                     # both letters
1628                     $a->[2] cmp $b->[2]
1629                     # mix, number must be first
1630                     :defined $a->[1]? -1: 1
1631         }
1632         map [ $_, (defined( /^(\d+)$/ )? $1: undef), lc($_) ],
1633         @elts;
1634 }
1635
1636 sub get_type_argument {
1637     my $type;
1638
1639     if (@ARGV) {
1640         $type = shift @ARGV;
1641         unless ($type =~ /^[A-Za-z0-9_.-]+$/) {
1642             # We want whine to mention our caller, not us.
1643             @_ = ("Invalid type '$type' specified.");
1644             goto &whine;
1645         }
1646     }
1647     else {
1648         @_ = ("No type argument specified with -t.");
1649         goto &whine;
1650     }
1651
1652     $type =~ s/s$//; # "Plural". Ugh.
1653     return $type;
1654 }
1655
1656 sub get_var_argument {
1657     my ($data) = @_;
1658
1659     if (@ARGV) {
1660         my $kv = shift @ARGV;
1661         if (my ($k, $v) = $kv =~ /^($field)=(.*)$/) {
1662             push @{ $data->{$k} }, $v;
1663         }
1664         else {
1665             @_ = ("Invalid variable specification: '$kv'.");
1666             goto &whine;
1667         }
1668     }
1669     else {
1670         @_ = ("No variable argument specified with -S.");
1671         goto &whine;
1672     }
1673 }
1674
1675 sub is_object_spec {
1676     my ($spec, $type) = @_;
1677
1678     $spec =~ s|^(?:$type/)?|$type/| if defined $type;
1679     return $spec if ($spec =~ m{^$name/(?:$idlist|$labels)(?:/.*)?$}o);
1680     return 0;
1681 }
1682
1683 sub suggest_help {
1684     my ($action, $type, $rv) = @_;
1685
1686     print STDERR "rt: For help, run 'rt help $action'.\n" if defined $action;
1687     print STDERR "rt: For help, run 'rt help $type'.\n" if defined $type;
1688     return $rv;
1689 }
1690
1691 sub str2time {
1692     # simplified procedure for parsing date, avoid loading Date::Parse
1693     my %month = (Jan => 0, Feb => 1, Mar => 2, Apr => 3, May =>  4, Jun =>  5,
1694                  Jul => 6, Aug => 7, Sep => 8, Oct => 9, Nov => 10, Dec => 11);
1695     $_ = shift;
1696     my ($mon, $day, $hr, $min, $sec, $yr, $monstr);
1697     if ( /(\w{3})\s+(\d\d?)\s+(\d\d):(\d\d):(\d\d)\s+(\d{4})/ ) {
1698         ($monstr, $day, $hr, $min, $sec, $yr) = ($1, $2, $3, $4, $5, $6);
1699         $mon = $month{$monstr} if exists $month{$monstr};
1700     } elsif ( /(\d{4})-(\d\d)-(\d\d)\s+(\d\d):(\d\d):(\d\d)/ ) {
1701         ($yr, $mon, $day, $hr, $min, $sec) = ($1, $2-1, $3, $4, $5, $6);
1702     }
1703     if ( $yr and defined $mon and $day and defined $hr and defined $sec ) {
1704         return timelocal($sec,$min,$hr,$day,$mon,$yr);
1705     } else {
1706         print "Unknown date format in parsedate: $_\n";
1707         return undef;
1708     }
1709 }
1710
1711 sub date_diff {
1712     my ($old, $new) = @_;
1713     $new = time() if ! $new;
1714     $old = str2time($old) if $old !~ /^\d+$/;
1715     $new = str2time($new) if $new !~ /^\d+$/;
1716     return "???" if ! $old or ! $new;
1717
1718     my %seconds = (min => 60,
1719                    hr  => 60*60,
1720                    day => 60*60*24,
1721                    wk  => 60*60*24*7,
1722                    mth => 60*60*24*30,
1723                    yr  => 60*60*24*365);
1724
1725     my $diff = $new - $old;
1726     my $what = 'sec';
1727     my $howmuch = $diff;
1728     for ( sort {$seconds{$a} <=> $seconds{$b}} keys %seconds) {
1729         last if $diff < $seconds{$_};
1730         $what = $_;
1731         $howmuch = int($diff/$seconds{$_});
1732     }
1733     return "$howmuch $what";
1734 }
1735
1736 sub prettyshow {
1737     my $forms = shift;
1738     my ($form) = grep { exists $_->[2]->{Queue} } @$forms;
1739     my $k = $form->[2];
1740     # dates are in local time zone
1741     if ( $k ) {
1742         print "Date: $k->{Created}\n";
1743         print "From: $k->{Requestors}\n";
1744         print "Cc: $k->{Cc}\n" if $k->{Cc};
1745         print "X-AdminCc: $k->{AdminCc}\n" if $k->{AdminCc};
1746         print "X-Queue: $k->{Queue}\n";
1747         print "Subject: [rt #$k->{id}] $k->{Subject}\n\n";
1748     }
1749     # dates in these attributes are in GMT and will be converted
1750     foreach my $form (@$forms) {
1751         my ($c, $o, $k, $e) = @$form;
1752         next if ! $k->{id} or exists $k->{Queue};
1753         if ( exists $k->{Created} ) {
1754             my ($y,$m,$d,$hh,$mm,$ss) = ($k->{Created} =~ /(\d\d\d\d)-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)/);
1755             $m--;
1756             my $created = localtime(timegm($ss,$mm,$hh,$d,$m,$y));
1757             if ( exists $k->{Description} ) {
1758                 print "===> $k->{Description} on $created\n";
1759             }
1760         }
1761         print "$k->{Content}\n" if exists $k->{Content} and
1762                                    $k->{Content} !~ /to have no content$/ and
1763                                    ($k->{Type}||'') ne 'EmailRecord';
1764         print "$k->{Attachments}\n" if exists $k->{Attachments} and
1765                                    $k->{Attachments};
1766     }
1767 }
1768
1769 sub prettylist {
1770     my $forms = shift;
1771     my $heading = "Ticket Owner Queue    Age   Told Status Requestor Subject\n";
1772     $heading .= '-' x 80 . "\n";
1773     my (@open, @me);
1774     foreach my $form (@$forms) {
1775         my ($c, $o, $k, $e) = @$form;
1776         next if ! $k->{id};
1777         print $heading if $heading;
1778         $heading = '';
1779         my $id = $k->{id};
1780         $id =~ s!^ticket/!!;
1781         my $owner = $k->{Owner} eq 'Nobody' ? '' : $k->{Owner};
1782         $owner = substr($owner, 0, 5);
1783         my $queue = substr($k->{Queue}, 0, 5);
1784         my $subject = substr($k->{Subject}, 0, 30);
1785         my $age = date_diff($k->{Created});
1786         my $told = $k->{Told} eq 'Not set' ? '' : date_diff($k->{Told});
1787         my $status = substr($k->{Status}, 0, 6);
1788         my $requestor = substr($k->{Requestors}, 0, 9);
1789         my $line = sprintf "%6s %5s %5s %6s %6s %-6s %-9s %-30s\n",
1790             $id, $owner, $queue, $age, $told, $status, $requestor, $subject;
1791         if ( $k->{Owner} eq 'Nobody' ) {
1792             push @open, $line;
1793         } elsif ($k->{Owner} eq $config{user} ) {
1794             push @me, $line;
1795         } else {
1796             print $line;
1797         }
1798     }
1799     print "No matches found\n" if $heading;
1800     printf "========== my %2d open tickets ==========\n", scalar @me if @me;
1801     print @me if @me;
1802     printf "========== %2d unowned tickets ==========\n", scalar @open if @open;
1803     print @open if @open;
1804 }
1805
1806 __DATA__
1807
1808 Title: intro
1809 Title: introduction
1810 Text:
1811
1812     This is a command-line interface to RT 3.0 or newer.
1813
1814     It allows you to interact with an RT server over HTTP, and offers an
1815     interface to RT's functionality that is better-suited to automation
1816     and integration with other tools.
1817
1818     In general, each invocation of this program should specify an action
1819     to perform on one or more objects, and any other arguments required
1820     to complete the desired action.
1821
1822     For more information:
1823
1824         - rt help usage         (syntax information)
1825         - rt help objects       (how to specify objects)
1826         - rt help actions       (a list of possible actions)
1827         - rt help types         (a list of object types)
1828
1829         - rt help config        (configuration details)
1830         - rt help examples      (a few useful examples)
1831         - rt help topics        (a list of help topics)
1832
1833 --
1834
1835 Title: usage
1836 Title: syntax
1837 Text:
1838
1839     Syntax:
1840
1841         rt <action> [options] [arguments]
1842       or
1843         rt shell
1844
1845     Each invocation of this program must specify an action (e.g. "edit",
1846     "create"), options to modify behaviour, and other arguments required
1847     by the specified action. (For example, most actions expect a list of
1848     numeric object IDs to act upon.)
1849
1850     The details of the syntax and arguments for each action are given by
1851     "rt help <action>". Some actions may be referred to by more than one
1852     name ("create" is the same as "new", for example).  
1853
1854     You may also call "rt shell", which will give you an 'rt>' prompt at
1855     which you can issue commands of the form "<action> [options] 
1856     [arguments]".  See "rt help shell" for details.
1857
1858     Objects are identified by a type and an ID (which can be a name or a
1859     number, depending on the type). For some actions, the object type is
1860     implied (you can only comment on tickets); for others, the user must
1861     specify it explicitly. See "rt help objects" for details.
1862
1863     In syntax descriptions, mandatory arguments that must be replaced by
1864     appropriate value are enclosed in <>, and optional arguments are
1865     indicated by [] (for example, <action> and [options] above).
1866
1867     For more information:
1868
1869         - rt help objects       (how to specify objects)
1870         - rt help actions       (a list of actions)
1871         - rt help types         (a list of object types)
1872         - rt help shell         (how to use the shell)
1873
1874 --
1875
1876 Title: conf
1877 Title: config
1878 Title: configuration
1879 Text:
1880
1881     This program has two major sources of configuration information: its
1882     configuration files, and the environment.
1883
1884     The program looks for configuration directives in a file named .rtrc
1885     (or $RTCONFIG; see below) in the current directory, and then in more
1886     distant ancestors, until it reaches /. If no suitable configuration
1887     files are found, it will also check for ~/.rtrc, /opt/rt3/local/etc/rt.conf
1888     and /etc/rt.conf.
1889
1890     Configuration directives:
1891
1892         The following directives may occur, one per line:
1893
1894         - server <URL>          URL to RT server.
1895         - user <username>       RT username.
1896         - passwd <passwd>       RT user's password.
1897         - query <RT Query>      Default RT Query for list action
1898         - orderby <order>       Default RT order for list action
1899         - queue <queuename>     Default RT Queue for list action
1900         - externalauth <0|1>    Use HTTP Basic authentication
1901          explicitely setting externalauth to 0 inhibits also GSSAPI based
1902          authentication, if LWP::Authen::Negotiate (and GSSAPI) is installed
1903
1904         Blank and #-commented lines are ignored.
1905
1906     Sample configuration file contents:
1907
1908          server  https://rt.somewhere.com/
1909          # more than one queue can be given (by adding a query expression)
1910          queue helpdesk or queue=support
1911          query Status != resolved and Owner=myaccount
1912
1913
1914     Environment variables:
1915
1916         The following environment variables override any corresponding
1917         values defined in configuration files:
1918
1919         - RTUSER
1920         - RTPASSWD
1921         - RTEXTERNALAUTH
1922         - RTSERVER
1923         - RTDEBUG       Numeric debug level. (Set to 3 for full logs.)
1924         - RTCONFIG      Specifies a name other than ".rtrc" for the
1925                         configuration file.
1926         - RTQUERY       Default RT Query for rt list
1927         - RTORDERBY     Default order for rt list
1928
1929 --
1930
1931 Title: objects
1932 Text:
1933
1934     Syntax:
1935
1936         <type>/<id>[/<attributes>]
1937
1938     Every object in RT has a type (e.g. "ticket", "queue") and a numeric
1939     ID. Some types of objects can also be identified by name (like users
1940     and queues). Furthermore, objects may have named attributes (such as
1941     "ticket/1/history").
1942
1943     An object specification is like a path in a virtual filesystem, with
1944     object types as top-level directories, object IDs as subdirectories,
1945     and named attributes as further subdirectories.
1946
1947     A comma-separated list of names, numeric IDs, or numeric ranges can
1948     be used to specify more than one object of the same type. Note that
1949     the list must be a single argument (i.e., no spaces). For example,
1950     "user/root,1-3,5,7-10,ams" is a list of ten users; the same list
1951     can also be written as "user/ams,root,1,2,3,5,7,8-10".
1952     
1953     If just a number is given as object specification it will be
1954     interpreted as ticket/<number>
1955
1956     Examples:
1957
1958         1                   # the same as ticket/1
1959         ticket/1
1960         ticket/1/attachments
1961         ticket/1/attachments/3
1962         ticket/1/attachments/3/content
1963         ticket/1-3/links
1964         ticket/1-3,5-7/history
1965
1966         user/ams
1967
1968     For more information:
1969
1970         - rt help <action>      (action-specific details)
1971         - rt help <type>        (type-specific details)
1972
1973 --
1974
1975 Title: actions
1976 Title: commands
1977 Text:
1978
1979     You can currently perform the following actions on all objects:
1980
1981         - list          (list objects matching some condition)
1982         - show          (display object details)
1983         - edit          (edit object details)
1984         - create        (create a new object)
1985
1986     Each type may define actions specific to itself; these are listed in
1987     the help item about that type.
1988
1989     For more information:
1990
1991         - rt help <action>      (action-specific details)
1992         - rt help types         (a list of possible types)
1993
1994     The following actions on tickets are also possible:
1995
1996         - comment       Add comments to a ticket
1997         - correspond    Add comments to a ticket
1998         - merge         Merge one ticket into another
1999         - link          Link one ticket to another
2000         - take          Take a ticket (steal and untake are possible as well)
2001
2002     For several edit set subcommands that are frequently used abbreviations
2003     have been introduced. These abbreviations are:
2004
2005         - delete or del  delete a ticket           (edit set status=deleted)
2006         - resolve or res resolve a ticket          (edit set status=resolved)
2007         - subject        change subject of ticket  (edit set subject=string)
2008         - give           give a ticket to somebody (edit set owner=user)
2009
2010 --
2011
2012 Title: types
2013 Text:
2014
2015     You can currently operate on the following types of objects:
2016
2017         - tickets
2018         - users
2019         - groups
2020         - queues
2021
2022     For more information:
2023
2024         - rt help <type>        (type-specific details)
2025         - rt help objects       (how to specify objects)
2026         - rt help actions       (a list of possible actions)
2027
2028 --
2029
2030 Title: ticket
2031 Text:
2032
2033     Tickets are identified by a numeric ID.
2034
2035     The following generic operations may be performed upon tickets:
2036
2037         - list
2038         - show
2039         - edit
2040         - create
2041
2042     In addition, the following ticket-specific actions exist:
2043
2044         - link
2045         - merge
2046         - comment
2047         - correspond
2048         - take
2049         - steal
2050         - untake
2051         - give
2052         - resolve
2053         - delete
2054         - subject
2055
2056     Attributes:
2057
2058         The following attributes can be used with "rt show" or "rt edit"
2059         to retrieve or edit other information associated with tickets:
2060
2061         links                      A ticket's relationships with others.
2062         history                    All of a ticket's transactions.
2063         history/type/<type>        Only a particular type of transaction.
2064         history/id/<id>            Only the transaction of the specified id.
2065         attachments                A list of attachments.
2066         attachments/<id>           The metadata for an individual attachment.
2067         attachments/<id>/content   The content of an individual attachment.
2068
2069 --
2070
2071 Title: user
2072 Title: group
2073 Text:
2074
2075     Users and groups are identified by name or numeric ID.
2076
2077     The following generic operations may be performed upon them:
2078
2079         - list
2080         - show
2081         - edit
2082         - create
2083
2084 --
2085
2086 Title: queue
2087 Text:
2088
2089     Queues are identified by name or numeric ID.
2090
2091     Currently, they can be subjected to the following actions:
2092
2093         - show
2094         - edit
2095         - create
2096
2097 --
2098
2099 Title: subject
2100 Text:
2101
2102     Syntax:
2103
2104         rt subject <id> <new subject text>
2105
2106     Change the subject of a ticket whose ticket id is given.
2107
2108 --
2109
2110 Title: give
2111 Text:
2112
2113     Syntax:
2114
2115         rt give <id> <accountname>
2116
2117     Give a ticket whose ticket id is given to another user.
2118
2119 --
2120
2121 Title: steal
2122 Text:
2123
2124         rt steal <id> 
2125
2126     Steal a ticket whose ticket id is given, i.e. set the owner to myself.
2127
2128 --
2129
2130 Title: take
2131 Text:
2132
2133     Syntax:
2134
2135         rt take <id>
2136
2137     Take a ticket whose ticket id is given, i.e. set the owner to myself.
2138
2139 --
2140
2141 Title: untake
2142 Text:
2143
2144     Syntax:
2145
2146         rt untake <id>
2147
2148     Untake a ticket whose ticket id is given, i.e. set the owner to Nobody.
2149
2150 --
2151
2152 Title: resolve
2153 Title: res
2154 Text:
2155
2156     Syntax:
2157
2158         rt resolve <id>
2159
2160     Resolves a ticket whose ticket id is given.
2161
2162 --
2163
2164 Title: delete
2165 Title: del
2166 Text:
2167
2168     Syntax:
2169
2170         rt delete <id>
2171
2172     Deletes a ticket whose ticket id is given.
2173
2174 --
2175
2176 Title: logout
2177 Text:
2178
2179     Syntax:
2180
2181         rt logout
2182
2183     Terminates the currently established login session. You will need to
2184     provide authentication credentials before you can continue using the
2185     server. (See "rt help config" for details about authentication.)
2186
2187 --
2188
2189 Title: ls
2190 Title: list
2191 Title: search
2192 Text:
2193
2194     Syntax:
2195
2196         rt <ls|list|search> [options] "query string"
2197
2198     Displays a list of objects matching the specified conditions.
2199     ("ls", "list", and "search" are synonyms.)
2200
2201     Conditions are expressed in the SQL-like syntax used internally by
2202     RT. (For more information, see "rt help query".) The query string
2203     must be supplied as one argument.
2204
2205     (Right now, the server doesn't support listing anything but tickets.
2206     Other types will be supported in future; this client will be able to
2207     take advantage of that support without any changes.)
2208
2209     Options:
2210
2211         The following options control how much information is displayed
2212         about each matching object:
2213
2214         -i             Numeric IDs only. (Useful for |rt edit -; see examples.)
2215         -s             Short description.
2216         -l             Longer description.
2217         -f <field[s]   Display only the fields listed and the ticket id
2218
2219         In addition,
2220         
2221         -o +/-<field>  Orders the returned list by the specified field.
2222         -r             reversed order (useful if a default was given)
2223         -q queue[s]    restricts the query to the queue[s] given
2224                        multiple queues are separated by comma
2225         -S var=val     Submits the specified variable with the request.
2226         -t type        Specifies the type of object to look for. (The
2227                        default is "ticket".)
2228
2229     Examples:
2230
2231         rt ls "Priority > 5 and Status=new"
2232         rt ls -o +Subject "Priority > 5 and Status=new"
2233         rt ls -o -Created "Priority > 5 and Status=new"
2234         rt ls -i "Priority > 5"|rt edit - set status=resolved
2235         rt ls -t ticket "Subject like '[PATCH]%'"
2236         rt ls -q systems
2237         rt ls -f owner,subject
2238
2239 --
2240
2241 Title: show
2242 Text:
2243
2244     Syntax:
2245
2246         rt show [options] <object-ids>
2247
2248     Displays details of the specified objects.
2249
2250     For some types, object information is further classified into named
2251     attributes (for example, "1-3/links" is a valid ticket specification
2252     that refers to the links for tickets 1-3). Consult "rt help <type>"
2253     and "rt help objects" for further details.
2254
2255     If only a number is given it will be interpreted as the objects
2256     ticket/number and ticket/number/history
2257
2258     This command writes a set of forms representing the requested object
2259     data to STDOUT.
2260
2261     Options:
2262
2263         The following options control how much information is displayed
2264         about each matching object:
2265
2266         Without any formatting options prettyprinted output is generated.
2267         Giving any of the two options below reverts to raw output.
2268         -s      Short description (history and attachments only).
2269         -l      Longer description (history and attachments only).
2270
2271         In addition,
2272         -               Read IDs from STDIN instead of the command-line.
2273         -t type         Specifies object type.
2274         -f a,b,c        Restrict the display to the specified fields.
2275         -S var=val      Submits the specified variable with the request.
2276
2277     Examples:
2278
2279         rt show -t ticket -f id,subject,status 1-3
2280         rt show ticket/3/attachments/29
2281         rt show ticket/3/attachments/29/content
2282         rt show ticket/1-3/links
2283         rt show ticket/3/history
2284         rt show -l ticket/3/history
2285         rt show -t user 2
2286         rt show 2
2287
2288 --
2289
2290 Title: new
2291 Title: edit
2292 Title: create
2293 Text:
2294
2295     Syntax:
2296
2297         rt edit [options] <object-ids> set field=value [field=value] ...
2298                                        add field=value [field=value] ...
2299                                        del field=value [field=value] ...
2300
2301     Edits information corresponding to the specified objects.
2302
2303     A purely numeric object id nnn is translated into ticket/nnn
2304
2305     If, instead of "edit", an action of "new" or "create" is specified,
2306     then a new object is created. In this case, no numeric object IDs
2307     may be specified, but the syntax and behaviour remain otherwise
2308     unchanged.
2309
2310     This command typically starts an editor to allow you to edit object
2311     data in a form for submission. If you specified enough information
2312     on the command-line, however, it will make the submission directly.
2313
2314     The command line may specify field-values in three different ways.
2315     "set" sets the named field to the given value, "add" adds a value
2316     to a multi-valued field, and "del" deletes the corresponding value.
2317     Each "field=value" specification must be given as a single argument.
2318
2319     For some types, object information is further classified into named
2320     attributes (for example, "1-3/links" is a valid ticket specification
2321     that refers to the links for tickets 1-3). These attributes may also
2322     be edited. Consult "rt help <type>" and "rt help object" for further
2323     details.
2324
2325     Options:
2326
2327         -       Read numeric IDs from STDIN instead of the command-line.
2328                 (Useful with rt ls ... | rt edit -; see examples below.)
2329         -i      Read a completed form from STDIN before submitting.
2330         -o      Dump the completed form to STDOUT instead of submitting.
2331         -e      Allows you to edit the form even if the command-line has
2332                 enough information to make a submission directly.
2333         -S var=val
2334                 Submits the specified variable with the request.
2335         -t type Specifies object type.
2336         -ct content-type Specifies content type of message(tickets only).
2337
2338     Examples:
2339
2340         # Interactive (starts $EDITOR with a form).
2341         rt edit ticket/3
2342         rt create -t ticket
2343         rt create -t ticket -ct text/html
2344
2345         # Non-interactive.
2346         rt edit ticket/1-3 add cc=foo@example.com set priority=3 due=tomorrow
2347         rt ls -t tickets -i 'Priority > 5' | rt edit - set status=resolved
2348         rt edit ticket/4 set priority=3 owner=bar@example.com \
2349                          add cc=foo@example.com bcc=quux@example.net
2350         rt create -t ticket set subject='new ticket' priority=10 \
2351                             add cc=foo@example.com
2352
2353 --
2354
2355 Title: comment
2356 Title: correspond
2357 Text:
2358
2359     Syntax:
2360
2361         rt <comment|correspond> [options] <ticket-id>
2362
2363     Adds a comment (or correspondence) to the specified ticket (the only
2364     difference being that comments aren't sent to the requestors.)
2365
2366     This command will typically start an editor and allow you to type a
2367     comment into a form. If, however, you specified all the necessary
2368     information on the command line, it submits the comment directly.
2369
2370     (See "rt help forms" for more information about forms.)
2371
2372     Options:
2373
2374         -m <text>       Specify comment text.
2375         -ct <content-type> Specify content-type of comment text.
2376         -a <file>       Attach a file to the comment. (May be used more
2377                         than once to attach multiple files.)
2378         -c <addrs>      A comma-separated list of Cc addresses.
2379         -b <addrs>      A comma-separated list of Bcc addresses.
2380         -w <time>       Specify the time spent working on this ticket.
2381         -e              Starts an editor before the submission, even if
2382                         arguments from the command line were sufficient.
2383
2384     Examples:
2385
2386         rt comment -m 'Not worth fixing.' -a stddisclaimer.h 23
2387
2388 --
2389
2390 Title: merge
2391 Text:
2392
2393     Syntax:
2394
2395         rt merge <from-id> <to-id>
2396
2397     Merges the first ticket specified into the second ticket specified.
2398
2399 --
2400
2401 Title: link
2402 Text:
2403
2404     Syntax:
2405
2406         rt link [-d] <id-A> <link> <id-B>
2407
2408     Creates (or, with -d, deletes) a link between the specified tickets.
2409     The link can (irrespective of case) be any of:
2410
2411         DependsOn/DependedOnBy:     A depends upon B (or vice versa).
2412         RefersTo/ReferredToBy:      A refers to B (or vice versa).
2413         MemberOf/HasMember:         A is a member of B (or vice versa).
2414
2415     To view a ticket's links, use "rt show ticket/3/links". (See
2416     "rt help ticket" and "rt help show".)
2417
2418     Options:
2419
2420         -d      Deletes the specified link.
2421
2422     Examples:
2423
2424         rt link 2 dependson 3
2425         rt link -d 4 referredtoby 6     # 6 no longer refers to 4
2426
2427 --
2428
2429 Title: query
2430 Text:
2431
2432     RT uses an SQL-like syntax to specify object selection constraints.
2433     See the <RT:...> documentation for details.
2434     
2435     (XXX: I'm going to have to write it, aren't I?)
2436
2437     Until it exists here a short description of important constructs:
2438
2439     The two simple forms of query expressions are the constructs
2440     Attribute like Value and
2441     Attribute = Value or Attribute != Value
2442
2443     Whether attributes can be matched using like or using = is built into RT.
2444     The attributes id, Queue, Owner Priority and Status require the = or !=
2445     tests.
2446
2447     If Value is a string it must be quoted and may contain the wildcard
2448     character %. If the string does not contain white space, the quoting
2449     may however be omitted, it will be added automatically when parsing
2450     the input.
2451
2452     Simple query expressions can be combined using and, or and parentheses
2453     can be used to group expressions.
2454
2455     As a special case a standalone string (which would not form a correct
2456     query) is transformed into (Owner='string' or Requestor like 'string%')
2457     and added to the default query, i.e. the query is narrowed down.
2458
2459     If no Queue=name clause is contained in the query, a default clause
2460     Queue=$config{queue} is added.
2461
2462     Examples:
2463     Status!='resolved' and Status!='rejected'
2464     (Owner='myaccount' or Requestor like 'myaccount%') and Status!='resolved'
2465
2466 --
2467
2468 Title: form
2469 Title: forms
2470 Text:
2471
2472     This program uses RFC822 header-style forms to represent object data
2473     in a form that's suitable for processing both by humans and scripts.
2474
2475     A form is a set of (field, value) specifications, with some initial
2476     commented text and interspersed blank lines allowed for convenience.
2477     Field names may appear more than once in a form; a comma-separated
2478     list of multiple field values may also be specified directly.
2479     
2480     Field values can be wrapped as in RFC822, with leading whitespace.
2481     The longest sequence of leading whitespace common to all the lines
2482     is removed (preserving further indentation). There is no limit on
2483     the length of a value.
2484
2485     Multiple forms are separated by a line containing only "--\n".
2486
2487     (XXX: A more detailed specification will be provided soon. For now,
2488     the server-side syntax checking will suffice.)
2489
2490 --
2491
2492 Title: topics
2493 Text:
2494
2495     Syntax:
2496
2497         rt help <topic>
2498
2499     Get help on any of the following subjects:
2500
2501         - tickets, users, groups, queues.
2502         - show, edit, ls/list/search, new/create.
2503
2504         - query                                 (search query syntax)
2505         - forms                                 (form specification)
2506
2507         - objects                               (how to specify objects)
2508         - types                                 (a list of object types)
2509         - actions/commands                      (a list of actions)
2510         - usage/syntax                          (syntax details)
2511         - conf/config/configuration             (configuration details)
2512         - examples                              (a few useful examples)
2513
2514 --
2515
2516 Title: example
2517 Title: examples
2518 Text:
2519
2520     some useful examples
2521
2522     All the following list requests will be restricted to the default queue.
2523     That can be changed by adding the option -q queuename
2524
2525     List all tickets that are not rejected/resolved
2526         rt ls
2527     List all tickets that are new and do not have an owner
2528         rt ls "status=new and owner=nobody"
2529     List all tickets which I have sent or of which I am the owner
2530         rt ls myaccount
2531     List all attributes for the ticket 6977 (ls -l instead of ls)
2532         rt ls -l 6977
2533     Show the content of ticket 6977
2534         rt show 6977
2535     Show all attributes in the ticket and in the history of the ticket
2536         rt show -l 6977
2537     Comment a ticket (mail is sent to all queue watchers, i.e. AdminCc's)
2538         rt comment 6977
2539         This will open an editor and lets you add text (attribute Text:)
2540         Other attributes may be changed as well, but usually don't do that.
2541     Correspond a ticket (like comment, but mail is also sent to requestors)
2542         rt correspond 6977
2543     Edit a ticket (generic change, interactive using the editor)
2544         rt edit 6977
2545     Change the owner of a ticket non interactively
2546         rt edit 6977 set owner=myaccount
2547         or
2548         rt give 6977 account
2549         or
2550         rt take 6977
2551     Change the status of a ticket
2552         rt edit 6977 set status=resolved
2553         or
2554         rt resolve 6977
2555     Change the status of all tickets I own to resolved !!!
2556         rt ls -i owner=myaccount | rt edit - set status=resolved
2557
2558 --
2559
2560 Title: shell
2561 Text:
2562
2563     Syntax:
2564
2565         rt shell
2566
2567     Opens an interactive shell, at which you can issue commands of 
2568     the form "<action> [options] [arguments]".
2569
2570     To exit the shell, type "quit" or "exit".
2571
2572     Commands can be given at the shell in the same form as they would 
2573     be given at the command line without the leading 'rt' invocation.
2574
2575     Example:
2576         $ rt shell
2577         rt> create -t ticket set subject='new' add cc=foo@example.com
2578         # Ticket 8 created.
2579         rt> quit
2580         $
2581
2582 --
2583
2584 Title: take
2585 Title: untake
2586 Title: steal
2587 Text:
2588
2589     Syntax:
2590
2591         rt <take|untake|steal> <ticket-id>
2592
2593     Sets the owner of the specified ticket to the current user, 
2594     assuming said user has the bits to do so, or releases the 
2595     ticket.  
2596     
2597     'Take' is used on tickets which are not currently owned 
2598     (Owner: Nobody), 'steal' is used on tickets which *are* 
2599     currently owned, and 'untake' is used to "release" a ticket 
2600     (reset its Owner to Nobody).  'Take' cannot be used on
2601     tickets which are currently owned.
2602
2603     Example:
2604         alice$ rt create -t ticket set subject="New ticket"
2605         # Ticket 7 created.
2606         alice$ rt take 7
2607         # Owner changed from Nobody to alice
2608         alice$ su bob
2609         bob$ rt steal 7
2610         # Owner changed from alice to bob
2611         bob$ rt untake 7
2612         # Owner changed from bob to Nobody
2613
2614 --
2615
2616 Title: quit
2617 Title: exit
2618 Text:
2619
2620     Use "quit" or "exit" to leave the shell.  Only valid within shell 
2621     mode.
2622
2623     Example:
2624         $ rt shell
2625         rt> quit
2626         $
2627
2628 __END__
2629
2630 =head1 NAME
2631
2632 rt - command-line interface to RT 3.0 or newer
2633
2634 =head1 SYNOPSIS
2635
2636     rt help
2637
2638 =head1 DESCRIPTION
2639
2640 This script allows you to interact with an RT server over HTTP, and offers an
2641 interface to RT's functionality that is better-suited to automation and
2642 integration with other tools.
2643
2644 In general, each invocation of this program should specify an action to
2645 perform on one or more objects, and any other arguments required to complete
2646 the desired action.
2647