Merge branch 'master' of https://github.com/jgoodman/Freeside
[freeside.git] / rt / bin / rt
index f327b39..b87d50d 100755 (executable)
--- a/rt/bin/rt
+++ b/rt/bin/rt
@@ -3,7 +3,7 @@
 #
 # COPYRIGHT:
 #
 #
 # COPYRIGHT:
 #
-# This software is Copyright (c) 1996-2011 Best Practical Solutions, LLC
+# This software is Copyright (c) 1996-2014 Best Practical Solutions, LLC
 #                                          <sales@bestpractical.com>
 #
 # (Except where explicitly superseded by other copyright notices)
 #                                          <sales@bestpractical.com>
 #
 # (Except where explicitly superseded by other copyright notices)
 # Abhijit Menon-Sen <ams@wiw.org>
 
 use strict;
 # Abhijit Menon-Sen <ams@wiw.org>
 
 use strict;
+use warnings;
+
+if ( $ARGV[0] && $ARGV[0] =~ /^(?:--help|-h)$/ ) {
+    require Pod::Usage;
+    print Pod::Usage::pod2usage( { verbose => 2 } );
+    exit;
+}
 
 # This program is intentionally written to have as few non-core module
 # dependencies as possible. It should stay that way.
 
 # This program is intentionally written to have as few non-core module
 # dependencies as possible. It should stay that way.
@@ -61,6 +68,7 @@ use HTTP::Request::Common;
 use HTTP::Headers;
 use Term::ReadLine;
 use Time::Local; # used in prettyshow
 use HTTP::Headers;
 use Term::ReadLine;
 use Time::Local; # used in prettyshow
+use File::Temp;
 
 # strong (GSSAPI based) authentication is supported if the server does provide
 # it and the perl modules GSSAPI and LWP::Authen::Negotiate are installed
 
 # strong (GSSAPI based) authentication is supported if the server does provide
 # it and the perl modules GSSAPI and LWP::Authen::Negotiate are installed
@@ -98,7 +106,7 @@ my %config = (
     config_from_file($ENV{RTCONFIG} || ".rtrc"),
     config_from_env()
 );
     config_from_file($ENV{RTCONFIG} || ".rtrc"),
     config_from_env()
 );
-my $session = new Session("$HOME/.rt_sessions");
+my $session = Session->new("$HOME/.rt_sessions");
 my $REST = "$config{server}/REST/1.0";
 $no_strong_auth = 'switched off by externalauth=0'
     if defined $config{externalauth};
 my $REST = "$config{server}/REST/1.0";
 $no_strong_auth = 'switched off by externalauth=0'
     if defined $config{externalauth};
@@ -113,9 +121,9 @@ sub DEBUG { warn @_ if $config{debug} >= shift }
 # (XXX: Ask Autrijus how i18n changes these definitions.)
 
 my $name    = '[\w.-]+';
 # (XXX: Ask Autrijus how i18n changes these definitions.)
 
 my $name    = '[\w.-]+';
-my $CF_name = '[\sa-z0-9_ :()/-]+';
+my $CF_name = '[^,]+?';
 my $field   = '(?i:[a-z][a-z0-9_-]*|C(?:ustom)?F(?:ield)?-'.$CF_name.'|CF\.\{'.$CF_name.'\})';
 my $field   = '(?i:[a-z][a-z0-9_-]*|C(?:ustom)?F(?:ield)?-'.$CF_name.'|CF\.\{'.$CF_name.'\})';
-my $label   = '[a-zA-Z0-9@_.+-]+';
+my $label   = '[^,\\/]+';
 my $labels  = "(?:$label,)*$label";
 my $idlist  = '(?:(?:\d+-)?\d+,)*(?:\d+-)?\d+';
 
 my $labels  = "(?:$label,)*$label";
 my $idlist  = '(?:(?:\d+-)?\d+,)*(?:\d+-)?\d+';
 
@@ -179,7 +187,7 @@ exit handler();
 
 sub shell {
     $|=1;
 
 sub shell {
     $|=1;
-    my $term = new Term::ReadLine 'RT CLI';
+    my $term = Term::ReadLine->new('RT CLI');
     while ( defined ($_ = $term->readline($prompt)) ) {
         next if /^#/ || /^\s*$/;
 
     while ( defined ($_ = $term->readline($prompt)) ) {
         next if /^#/ || /^\s*$/;
 
@@ -414,7 +422,7 @@ sub show {
         }
         elsif (my $spec = is_object_spec($_, $type)) {
             push @objects, $spec;
         }
         elsif (my $spec = is_object_spec($_, $type)) {
             push @objects, $spec;
-            $rawprint = 1 if $_ =~ /\/content$/ or $_ !~ /^ticket/;
+            $rawprint = 1 if $_ =~ /\/content$/ or $_ =~ /\/links/ or $_ !~ /^ticket/;
         }
         else {
             my $datum = /^-/ ? "option" : "argument";
         }
         else {
             my $datum = /^-/ ? "option" : "argument";
@@ -464,7 +472,7 @@ sub show {
 sub edit {
     my ($action) = @_;
     my (%data, $type, @objects);
 sub edit {
     my ($action) = @_;
     my (%data, $type, @objects);
-    my ($cl, $text, $edit, $input, $output);
+    my ($cl, $text, $edit, $input, $output, $content_type);
 
     use vars qw(%set %add %del);
     %set = %add = %del = ();
 
     use vars qw(%set %add %del);
     %set = %add = %del = ();
@@ -478,6 +486,7 @@ sub edit {
         if    (/^-e$/) { $edit = 1 }
         elsif (/^-i$/) { $input = 1 }
         elsif (/^-o$/) { $output = 1 }
         if    (/^-e$/) { $edit = 1 }
         elsif (/^-i$/) { $input = 1 }
         elsif (/^-o$/) { $output = 1 }
+        elsif (/^-ct$/) { $content_type = shift @ARGV }
         elsif (/^-t$/) {
             $bad = 1, last unless defined($type = get_type_argument());
         }
         elsif (/^-t$/) {
             $bad = 1, last unless defined($type = get_type_argument());
         }
@@ -647,24 +656,54 @@ sub edit {
         return 0;
     }
 
         return 0;
     }
 
+    my @files;
+    @files = @{ vsplit($set{'attachment'}) } if exists $set{'attachment'};
+
     my $synerr = 0;
 
 EDIT:
     # We'll let the user edit the form before sending it to the server,
     # unless we have enough information to submit it non-interactively.
     my $synerr = 0;
 
 EDIT:
     # We'll let the user edit the form before sending it to the server,
     # unless we have enough information to submit it non-interactively.
+    if ( $type && $type eq 'ticket' && $text !~ /^Content-Type:/m ) {
+        $text .= "Content-Type: $content_type\n"
+            if $content_type and $content_type ne "text/plain";
+    }
+
     if ($edit || (!$input && !$cl)) {
     if ($edit || (!$input && !$cl)) {
-        my $newtext = vi($text);
+        my ($newtext) = vi_form_while(
+            $text,
+            sub {
+                my ($text, $form) = @_;
+                return 1 unless exists $form->[2]{'Attachment'};
+
+                foreach my $f ( @{ vsplit($form->[2]{'Attachment'}) } ) {
+                    return (0, "File '$f' doesn't exist") unless -f $f;
+                }
+                @files = @{ vsplit($form->[2]{'Attachment'}) };
+                return 1;
+            },
+        );
+        return $newtext unless $newtext;
         # We won't resubmit a bad form unless it was changed.
         $text = ($synerr && $newtext eq $text) ? undef : $newtext;
     }
 
         # We won't resubmit a bad form unless it was changed.
         $text = ($synerr && $newtext eq $text) ? undef : $newtext;
     }
 
+    delete @data{ grep /^attachment_\d+$/, keys %data };
+    my $i = 1;
+    foreach my $file (@files) {
+        $data{"attachment_$i"} = bless([ $file ], "Attachment");
+        $i++;
+    }
+
     if ($text) {
         my $r = submit("$REST/edit", {content => $text, %data});
         if ($r->code == 409) {
             # If we submitted a bad form, we'll give the user a chance
             # to correct it and resubmit.
             if ($edit || (!$input && !$cl)) {
     if ($text) {
         my $r = submit("$REST/edit", {content => $text, %data});
         if ($r->code == 409) {
             # If we submitted a bad form, we'll give the user a chance
             # to correct it and resubmit.
             if ($edit || (!$input && !$cl)) {
-                $text = $r->content;
+                my $content = $r->content . "\n";
+                $content =~ s/^(?!#)/#     /mg;
+                $text = $content . $text;
                 $synerr = 1;
                 goto EDIT;
             }
                 $synerr = 1;
                 goto EDIT;
             }
@@ -730,7 +769,7 @@ sub setcommand {
 
 sub comment {
     my ($action) = @_;
 
 sub comment {
     my ($action) = @_;
-    my (%data, $id, @files, @bcc, @cc, $msg, $wtime, $edit);
+    my (%data, $id, @files, @bcc, @cc, $msg, $content_type, $wtime, $edit);
     my $bad = 0;
 
     while (@ARGV) {
     my $bad = 0;
 
     while (@ARGV) {
@@ -739,7 +778,7 @@ sub comment {
         if (/^-e$/) {
             $edit = 1;
         }
         if (/^-e$/) {
             $edit = 1;
         }
-        elsif (/^-[abcmw]$/) {
+        elsif (/^-(?:[abcmw]|ct)$/) {
             unless (@ARGV) {
                 whine "No argument specified with $_.";
                 $bad = 1; last;
             unless (@ARGV) {
                 whine "No argument specified with $_.";
                 $bad = 1; last;
@@ -752,6 +791,9 @@ sub comment {
                 }
                 push @files, shift @ARGV;
             }
                 }
                 push @files, shift @ARGV;
             }
+            elsif (/-ct/) {
+                $content_type = shift @ARGV;
+            }
             elsif (/-([bc])/) {
                 my $a = $_ eq "-b" ? \@bcc : \@cc;
                 @$a = split /\s*,\s*/, shift @ARGV;
             elsif (/-([bc])/) {
                 my $a = $_ eq "-b" ? \@bcc : \@cc;
                 @$a = split /\s*,\s*/, shift @ARGV;
@@ -763,7 +805,6 @@ sub comment {
                     while (<STDIN>) { $msg .= $_ }
                 }
             }
                     while (<STDIN>) { $msg .= $_ }
                 }
             }
-
             elsif (/-w/) { $wtime = shift @ARGV }
         }
         elsif (!$id && m|^(?:ticket/)?($idlist)$|) {
             elsif (/-w/) { $wtime = shift @ARGV }
         }
         elsif (!$id && m|^(?:ticket/)?($idlist)$|) {
@@ -785,7 +826,7 @@ sub comment {
 
     my $form = [
         "",
 
     my $form = [
         "",
-        [ "Ticket", "Action", "Cc", "Bcc", "Attachment", "TimeWorked", "Text" ],
+        [ "Ticket", "Action", "Cc", "Bcc", "Attachment", "TimeWorked", "Content-Type", "Text" ],
         {
             Ticket     => $id,
             Action     => $action,
         {
             Ticket     => $id,
             Action     => $action,
@@ -793,6 +834,7 @@ sub comment {
             Bcc        => [ @bcc ],
             Attachment => [ @files ],
             TimeWorked => $wtime || '',
             Bcc        => [ @bcc ],
             Attachment => [ @files ],
             TimeWorked => $wtime || '',
+            'Content-Type' => $content_type || 'text/plain',
             Text       => $msg || '',
             Status => ''
         }
             Text       => $msg || '',
             Status => ''
         }
@@ -801,30 +843,19 @@ sub comment {
     my $text = Form::compose([ $form ]);
 
     if ($edit || !$msg) {
     my $text = Form::compose([ $form ]);
 
     if ($edit || !$msg) {
-        my $error = 0;
-        my ($c, $o, $k, $e);
-
-        do {
-            my $ntext = vi($text);
-            return if ($error && $ntext eq $text);
-            $text = $ntext;
-            $form = Form::parse($text);
-            $error = 0;
-
-            ($c, $o, $k, $e) = @{ $form->[0] };
-            if ($e) {
-                $error = 1;
-                $c = "# Syntax error.";
-                goto NEXT;
-            }
-            elsif (!@$o) {
-                return 0;
-            }
-            @files = @{ vsplit($k->{Attachment}) };
-
-        NEXT:
-            $text = Form::compose([[$c, $o, $k, $e]]);
-        } while ($error);
+        my ($tmp) = vi_form_while(
+            $text,
+            sub {
+                my ($text, $form) = @_;
+                foreach my $f ( @{ vsplit($form->[2]{'Attachment'}) } ) {
+                    return (0, "File '$f' doesn't exist") unless -f $f;
+                }
+                @files = @{ vsplit($form->[2]{'Attachment'}) };
+                return 1;
+            },
+        );
+        return $tmp unless $tmp;
+        $text = $tmp;
     }
 
     my $i = 1;
     }
 
     my $i = 1;
@@ -899,11 +930,6 @@ sub link {
     
     if (@ARGV == 3) {
         my ($from, $rel, $to) = @ARGV;
     
     if (@ARGV == 3) {
         my ($from, $rel, $to) = @ARGV;
-        if ($from !~ /^\d+$/ || $to !~ /^\d+$/) {
-            my $bad = $from =~ /^\d+$/ ? $to : $from;
-            whine "Invalid $type ID '$bad' specified.";
-            $bad = 1;
-        }
         if (($type eq "ticket") && ( ! exists $ltypes{lc $rel})) {
             whine "Invalid link '$rel' for type $type specified.";
             $bad = 1;
         if (($type eq "ticket") && ( ! exists $ltypes{lc $rel})) {
             whine "Invalid link '$rel' for type $type specified.";
             $bad = 1;
@@ -966,12 +992,8 @@ sub take {
 sub grant {
     my ($cmd) = @_;
 
 sub grant {
     my ($cmd) = @_;
 
-    my $revoke = 0;
-    while (@ARGV) {
-    }
-
-    $revoke = 1 if $cmd->{action} eq 'revoke';
-    return 0;
+    whine "$cmd is unimplemented.";
+    return 1;
 }
 
 # Client <-> Server communication.
 }
 
 # Client <-> Server communication.
@@ -984,7 +1006,7 @@ sub grant {
 sub submit {
     my ($uri, $content) = @_;
     my ($req, $data);
 sub submit {
     my ($uri, $content) = @_;
     my ($req, $data);
-    my $ua = new LWP::UserAgent(agent => "RT/3.0b", env_proxy => 1);
+    my $ua = LWP::UserAgent->new(agent => "RT/3.0b", env_proxy => 1);
     my $h = HTTP::Headers->new;
 
     # Did the caller specify any data to send with the request?
     my $h = HTTP::Headers->new;
 
     # Did the caller specify any data to send with the request?
@@ -1164,44 +1186,40 @@ sub submit {
     sub load {
         my ($self, $file) = @_;
         $file ||= $self->{file};
     sub load {
         my ($self, $file) = @_;
         $file ||= $self->{file};
-        local *F;
-
-        open(F, '<', $file) && do {
-            $self->{file} = $file;
-            my $sids = $self->{sids} = {};
-            while (<F>) {
-                chomp;
-                next if /^$/ || /^#/;
-                next unless m#^https?://[^ ]+ \w+ [^;,\s]+=[0-9A-Fa-f]+$#;
-                my ($server, $user, $cookie) = split / /, $_;
-                $sids->{$server}{$user} = $cookie;
-            }
-            return 1;
-        };
-        return 0;
+
+        open( my $handle, '<', $file ) or return 0;
+
+        $self->{file} = $file;
+        my $sids = $self->{sids} = {};
+        while (<$handle>) {
+            chomp;
+            next if /^$/ || /^#/;
+            next unless m#^https?://[^ ]+ \w+ [^;,\s]+=[0-9A-Fa-f]+$#;
+            my ($server, $user, $cookie) = split / /, $_;
+            $sids->{$server}{$user} = $cookie;
+        }
+        return 1;
     }
 
     # Writes the current session cache to the specified file.
     sub save {
         my ($self, $file) = shift;
         $file ||= $self->{file};
     }
 
     # Writes the current session cache to the specified file.
     sub save {
         my ($self, $file) = shift;
         $file ||= $self->{file};
-        local *F;
-
-        open(F, '>', $file) && do {
-            my $sids = $self->{sids};
-            foreach my $server (keys %$sids) {
-                foreach my $user (keys %{ $sids->{$server} }) {
-                    my $sid = $sids->{$server}{$user};
-                    if (defined $sid) {
-                        print F "$server $user $sid\n";
-                    }
+
+        open( my $handle, '>', "$file" ) or return 0;
+
+        my $sids = $self->{sids};
+        foreach my $server (keys %$sids) {
+            foreach my $user (keys %{ $sids->{$server} }) {
+                my $sid = $sids->{$server}{$user};
+                if (defined $sid) {
+                    print $handle "$server $user $sid\n";
                 }
             }
                 }
             }
-            close(F);
-            chmod 0600, $file;
-            return 1;
-        };
-        return 0;
+        }
+        close($handle);
+        chmod 0600, $file;
+        return 1;
     }
 
     sub DESTROY {
     }
 
     sub DESTROY {
@@ -1415,7 +1433,7 @@ sub config_from_file {
         }
 
         # Still nothing? We'll fall back to some likely defaults.
         }
 
         # Still nothing? We'll fall back to some likely defaults.
-        for ("$HOME/$rc", "local/etc/rt.conf", "/etc/rt.conf") {
+        for ("$HOME/$rc", "/opt/rt3/local/etc/rt.conf", "/etc/rt.conf") {
             return parse_config_file($_) if (-r $_);
         }
     }
             return parse_config_file($_) if (-r $_);
         }
     }
@@ -1429,19 +1447,19 @@ sub parse_config_file {
     my ($file) = @_;
     local $_; # $_ may be aliased to a constant, from line 1163
 
     my ($file) = @_;
     local $_; # $_ may be aliased to a constant, from line 1163
 
-    open(CFG, '<', $file) && do {
-        while (<CFG>) {
-            chomp;
-            next if (/^#/ || /^\s*$/);
+    open( my $handle, '<', $file ) or return;
 
 
-            if (/^(externalauth|user|passwd|server|query|orderby|queue)\s+(.*)\s?$/) {
-                $cfg{$1} = $2;
-            }
-            else {
-                die "rt: $file:$.: unknown configuration directive.\n";
-            }
+    while (<$handle>) {
+        chomp;
+        next if (/^#/ || /^\s*$/);
+
+        if (/^(externalauth|user|passwd|server|query|orderby|queue)\s+(.*)\s?$/) {
+            $cfg{$1} = $2;
+        }
+        else {
+            die "rt: $file:$.: unknown configuration directive.\n";
         }
         }
-    };
+    }
 
     return %cfg;
 }
 
     return %cfg;
 }
@@ -1471,18 +1489,58 @@ sub read_passwd {
     return $passwd;
 }
 
     return $passwd;
 }
 
+sub vi_form_while {
+    my $text = shift;
+    my $cb = shift;
+
+    my $error = 0;
+    my ($c, $o, $k, $e);
+    do {
+        my $ntext = vi($text);
+        return undef if ($error && $ntext eq $text);
+
+        $text = $ntext;
+
+        my $form = Form::parse($text);
+        $error = 0;
+        ($c, $o, $k, $e) = @{ $form->[0] };
+        if ( $e ) {
+            $error = 1;
+            $c = "# Syntax error.";
+            goto NEXT;
+        }
+        elsif (!@$o) {
+            return 0;
+        }
+
+        my ($status, $msg) = $cb->( $text, [$c, $o, $k, $e] );
+        unless ( $status ) {
+            $error = 1;
+            $c = "# $msg";
+        }
+
+    NEXT:
+        $text = Form::compose([[$c, $o, $k, $e]]);
+    } while ($error);
+
+    return $text;
+}
+
 sub vi {
     my ($text) = @_;
 sub vi {
     my ($text) = @_;
-    my $file = "/tmp/rt.form.$$";
     my $editor = $ENV{EDITOR} || $ENV{VISUAL} || "vi";
 
     my $editor = $ENV{EDITOR} || $ENV{VISUAL} || "vi";
 
-    local *F;
     local $/ = undef;
 
     local $/ = undef;
 
-    open(F, '>', $file) or die "$file: $!\n"; print F $text; close(F);
-    system($editor, $file) && die "Couldn't run $editor.\n";
-    open(F, '<', $file) or die "$file: $!\n"; $text = <F>; close(F);
-    unlink($file);
+    my $handle = File::Temp->new;
+    print $handle $text;
+    close($handle);
+
+    system($editor, $handle->filename) && die "Couldn't run $editor.\n";
+
+    open( $handle, '<', $handle->filename ) or die "$handle: $!\n";
+    $text = <$handle>;
+    close($handle);
 
     return $text;
 }
 
     return $text;
 }
@@ -1527,15 +1585,15 @@ sub vsplit {
                 }
                 push @words, $s;
             }
                 }
                 push @words, $s;
             }
-            elsif ( $a =~ /^q{/ ) {
+            elsif ( $a =~ /^q\{/ ) {
                 my $s = $a;
                 my $s = $a;
-                while ( $a !~ /}$/ ) {
+                while ( $a !~ /\}$/ ) {
                     ( $a, $b ) =
                       split /\s*,\s*/, $b, 2;
                     $s .= ',' . $a;
                 }
                     ( $a, $b ) =
                       split /\s*,\s*/, $b, 2;
                     $s .= ',' . $a;
                 }
-                $s =~ s/^q{/'/;
-                $s =~ s/}/'/;
+                $s =~ s/^q\{/'/;
+                $s =~ s/\}/'/;
                 push @words, $s;
             }
             else {
                 push @words, $s;
             }
             else {
@@ -1702,7 +1760,7 @@ sub prettyshow {
         }
         print "$k->{Content}\n" if exists $k->{Content} and
                                    $k->{Content} !~ /to have no content$/ and
         }
         print "$k->{Content}\n" if exists $k->{Content} and
                                    $k->{Content} !~ /to have no content$/ and
-                                   $k->{Type} ne 'EmailRecord';
+                                   ($k->{Type}||'') ne 'EmailRecord';
         print "$k->{Attachments}\n" if exists $k->{Attachments} and
                                    $k->{Attachments};
     }
         print "$k->{Attachments}\n" if exists $k->{Attachments} and
                                    $k->{Attachments};
     }
@@ -1826,7 +1884,7 @@ Text:
     The program looks for configuration directives in a file named .rtrc
     (or $RTCONFIG; see below) in the current directory, and then in more
     distant ancestors, until it reaches /. If no suitable configuration
     The program looks for configuration directives in a file named .rtrc
     (or $RTCONFIG; see below) in the current directory, and then in more
     distant ancestors, until it reaches /. If no suitable configuration
-    files are found, it will also check for ~/.rtrc, local/etc/rt.conf
+    files are found, it will also check for ~/.rtrc, /opt/rt3/local/etc/rt.conf
     and /etc/rt.conf.
 
     Configuration directives:
     and /etc/rt.conf.
 
     Configuration directives:
@@ -1906,8 +1964,6 @@ Text:
         ticket/1-3,5-7/history
 
         user/ams
         ticket/1-3,5-7/history
 
         user/ams
-        user/ams/rights
-        user/ams,rai,1/rights
 
     For more information:
 
 
     For more information:
 
@@ -2025,20 +2081,6 @@ Text:
         - edit
         - create
 
         - edit
         - create
 
-    In addition, the following type-specific actions exist:
-
-        - grant
-        - revoke
-
-    Attributes:
-
-        The following attributes can be used with "rt show" or "rt edit"
-        to retrieve or edit other information associated with users and
-        groups:
-
-        rights                  Global rights granted to this user.
-        rights/<queue>          Queue rights for this user.
-
 --
 
 Title: queue
 --
 
 Title: queue
@@ -2157,7 +2199,7 @@ Text:
     ("ls", "list", and "search" are synonyms.)
 
     Conditions are expressed in the SQL-like syntax used internally by
     ("ls", "list", and "search" are synonyms.)
 
     Conditions are expressed in the SQL-like syntax used internally by
-    RT3. (For more information, see "rt help query".) The query string
+    RT. (For more information, see "rt help query".) The query string
     must be supplied as one argument.
 
     (Right now, the server doesn't support listing anything but tickets.
     must be supplied as one argument.
 
     (Right now, the server doesn't support listing anything but tickets.
@@ -2291,12 +2333,14 @@ Text:
         -S var=val
                 Submits the specified variable with the request.
         -t type Specifies object type.
         -S var=val
                 Submits the specified variable with the request.
         -t type Specifies object type.
+        -ct content-type Specifies content type of message(tickets only).
 
     Examples:
 
         # Interactive (starts $EDITOR with a form).
         rt edit ticket/3
         rt create -t ticket
 
     Examples:
 
         # Interactive (starts $EDITOR with a form).
         rt edit ticket/3
         rt create -t ticket
+        rt create -t ticket -ct text/html
 
         # Non-interactive.
         rt edit ticket/1-3 add cc=foo@example.com set priority=3 due=tomorrow
 
         # Non-interactive.
         rt edit ticket/1-3 add cc=foo@example.com set priority=3 due=tomorrow
@@ -2328,6 +2372,7 @@ Text:
     Options:
 
         -m <text>       Specify comment text.
     Options:
 
         -m <text>       Specify comment text.
+        -ct <content-type> Specify content-type of comment text.
         -a <file>       Attach a file to the comment. (May be used more
                         than once to attach multiple files.)
         -c <addrs>      A comma-separated list of Cc addresses.
         -a <file>       Attach a file to the comment. (May be used more
                         than once to attach multiple files.)
         -c <addrs>      A comma-separated list of Cc addresses.
@@ -2381,16 +2426,10 @@ Text:
 
 --
 
 
 --
 
-Title: grant
-Title: revoke
-Text:
-
---
-
 Title: query
 Text:
 
 Title: query
 Text:
 
-    RT3 uses an SQL-like syntax to specify object selection constraints.
+    RT uses an SQL-like syntax to specify object selection constraints.
     See the <RT:...> documentation for details.
     
     (XXX: I'm going to have to write it, aren't I?)
     See the <RT:...> documentation for details.
     
     (XXX: I'm going to have to write it, aren't I?)
@@ -2585,3 +2624,24 @@ Text:
         $ rt shell
         rt> quit
         $
         $ rt shell
         rt> quit
         $
+
+__END__
+
+=head1 NAME
+
+rt - command-line interface to RT 3.0 or newer
+
+=head1 SYNOPSIS
+
+    rt help
+
+=head1 DESCRIPTION
+
+This script allows you to interact with an RT server over HTTP, and offers an
+interface to RT's functionality that is better-suited to automation and
+integration with other tools.
+
+In general, each invocation of this program should specify an action to
+perform on one or more objects, and any other arguments required to complete
+the desired action.
+