import of rt 3.0.9 RT_3_0_9
authorivan <ivan>
Thu, 11 Mar 2004 02:05:38 +0000 (02:05 +0000)
committerivan <ivan>
Thu, 11 Mar 2004 02:05:38 +0000 (02:05 +0000)
165 files changed:
rt/Changelog
rt/Makefile
rt/Makefile.in
rt/README
rt/README.Oracle [new file with mode: 0644]
rt/UPGRADING [new file with mode: 0644]
rt/autom4te.cache/output.0
rt/autom4te.cache/traces.0
rt/bin/mason_handler.fcgi
rt/bin/mason_handler.fcgi.in
rt/bin/mason_handler.scgi
rt/bin/mason_handler.scgi.in
rt/bin/mason_handler.svc
rt/bin/mason_handler.svc.in
rt/bin/rt
rt/bin/rt-crontool
rt/bin/rt-crontool.in
rt/bin/rt-mailgate
rt/bin/rt-mailgate.in
rt/bin/rt.in [new file with mode: 0644]
rt/bin/webmux.pl
rt/bin/webmux.pl.in
rt/config.layout
rt/config.log
rt/config.status
rt/configure
rt/configure.ac
rt/docs/rt3-schema-relationships.dot [new file with mode: 0644]
rt/etc/RT_Config.pm
rt/etc/RT_Config.pm.in
rt/etc/acl.Informix [new file with mode: 0644]
rt/etc/acl.Oracle
rt/etc/constraints.mysql
rt/etc/drop.Informix [new file with mode: 0644]
rt/etc/drop.Oracle [new file with mode: 0644]
rt/etc/initialdata
rt/etc/schema.Informix [new file with mode: 0644]
rt/etc/schema.Oracle
rt/etc/schema.Pg
rt/etc/schema.SQLite
rt/etc/schema.mysql
rt/html/Admin/Elements/EditCustomField
rt/html/Admin/Elements/EditCustomFieldValues
rt/html/Admin/Elements/EditCustomFields
rt/html/Admin/Elements/EditScrip
rt/html/Admin/Elements/EditScrips
rt/html/Admin/Elements/SelectGroups
rt/html/Admin/Elements/SelectRights
rt/html/Admin/Elements/SelectStage [new file with mode: 0644]
rt/html/Admin/Queues/CustomFields.html
rt/html/Admin/Queues/index.html
rt/html/Admin/Users/Modify.html
rt/html/Admin/Users/index.html
rt/html/Approvals/Display.html
rt/html/Elements/Callback
rt/html/Elements/MessageBox
rt/html/Elements/MyTickets
rt/html/Elements/SelectLang [new file with mode: 0644]
rt/html/Elements/SelectStatus
rt/html/Elements/SelectWatcherType
rt/html/Elements/SetupSessionCookie
rt/html/Elements/SimpleSearch
rt/html/NoAuth/webrt.css
rt/html/REST/1.0/Forms/queue/default [new file with mode: 0644]
rt/html/REST/1.0/Forms/queue/ns [new file with mode: 0644]
rt/html/REST/1.0/Forms/ticket/attachments [new file with mode: 0644]
rt/html/REST/1.0/Forms/ticket/default [new file with mode: 0644]
rt/html/REST/1.0/Forms/ticket/history [new file with mode: 0644]
rt/html/REST/1.0/Forms/ticket/links [new file with mode: 0644]
rt/html/REST/1.0/Forms/user/default [new file with mode: 0644]
rt/html/REST/1.0/Forms/user/ns [new file with mode: 0644]
rt/html/REST/1.0/NoAuth/mail-gateway
rt/html/REST/1.0/autohandler [new file with mode: 0644]
rt/html/REST/1.0/dhandler [new file with mode: 0644]
rt/html/REST/1.0/logout [new file with mode: 0644]
rt/html/REST/1.0/search/dhandler [new file with mode: 0644]
rt/html/REST/1.0/search/ticket [new file with mode: 0644]
rt/html/REST/1.0/ticket/comment [new file with mode: 0644]
rt/html/REST/1.0/ticket/link [new file with mode: 0644]
rt/html/REST/1.0/ticket/merge [new file with mode: 0644]
rt/html/Search/Bulk.html
rt/html/Search/Elements/PickRestriction
rt/html/SelfService/Display.html
rt/html/SelfService/Elements/MyRequests
rt/html/SelfService/Update.html
rt/html/Ticket/Attachment/dhandler
rt/html/Ticket/Create.html
rt/html/Ticket/Display.html
rt/html/Ticket/Elements/AddWatchers
rt/html/Ticket/Elements/EditCustomField
rt/html/Ticket/Elements/EditLinks
rt/html/Ticket/Elements/EditPeople
rt/html/Ticket/Elements/ShowAttachments
rt/html/Ticket/Elements/ShowDates
rt/html/Ticket/Elements/ShowHistory
rt/html/Ticket/Elements/ShowMessageStanza
rt/html/Ticket/Elements/ShowPeople
rt/html/Ticket/Elements/ShowTransaction
rt/html/Ticket/Elements/Tabs
rt/html/Ticket/Modify.html
rt/html/Ticket/ModifyAll.html
rt/html/Ticket/ModifyPeople.html
rt/html/Ticket/Update.html
rt/html/User/Prefs.html
rt/html/autohandler
rt/html/index.html
rt/lib/RT.pm
rt/lib/RT.pm.in
rt/lib/RT/Action/AutoOpen.pm
rt/lib/RT/Action/Autoreply.pm
rt/lib/RT/Action/CreateTickets.pm
rt/lib/RT/Action/SendEmail.pm
rt/lib/RT/Attachment_Overlay.pm
rt/lib/RT/Base.pm
rt/lib/RT/CachedGroupMember_Overlay.pm
rt/lib/RT/CachedGroupMembers_Overlay.pm
rt/lib/RT/CurrentUser.pm
rt/lib/RT/CustomField_Overlay.pm
rt/lib/RT/EmailParser.pm
rt/lib/RT/GroupMember_Overlay.pm
rt/lib/RT/Group_Overlay.pm
rt/lib/RT/Groups_Overlay.pm
rt/lib/RT/Handle.pm
rt/lib/RT/I18N.pm
rt/lib/RT/I18N/cs.pm
rt/lib/RT/I18N/de.po
rt/lib/RT/I18N/it.po [new file with mode: 0644]
rt/lib/RT/I18N/ru.po
rt/lib/RT/I18N/zh_cn.po
rt/lib/RT/I18N/zh_tw.po
rt/lib/RT/Interface/Email.pm
rt/lib/RT/Interface/REST.pm [new file with mode: 0644]
rt/lib/RT/Interface/Web.pm
rt/lib/RT/Principal_Overlay.pm
rt/lib/RT/Queue_Overlay.pm
rt/lib/RT/Record.pm
rt/lib/RT/ScripAction_Overlay.pm
rt/lib/RT/Scrip_Overlay.pm
rt/lib/RT/Scrips_Overlay.pm
rt/lib/RT/StyleGuide.pod [new file with mode: 0644]
rt/lib/RT/Template_Overlay.pm
rt/lib/RT/Ticket_Overlay.pm
rt/lib/RT/Tickets_Overlay.pm
rt/lib/RT/Tickets_Overlay_SQL.pm
rt/lib/RT/Transaction_Overlay.pm
rt/lib/RT/URI.pm
rt/lib/RT/URI/fsck_com_rt.pm
rt/lib/RT/User_Overlay.pm
rt/lib/RT/Users_Overlay.pm
rt/lib/t/02regression.t
rt/lib/t/02regression.t.in
rt/lib/t/03web.pl
rt/lib/t/03web.pl.in
rt/lib/t/04_send_email.pl
rt/lib/t/04_send_email.pl.in
rt/lib/t/data/crashes-file-based-parser [new file with mode: 0644]
rt/lib/t/data/multipart-report [new file with mode: 0644]
rt/lib/t/data/notes-uuencoded [new file with mode: 0644]
rt/sbin/extract-message-catalog
rt/sbin/factory
rt/sbin/license_tag
rt/sbin/rt-setup-database
rt/sbin/rt-setup-database.in
rt/sbin/rt-test-dependencies
rt/sbin/rt-test-dependencies.in

index d8d73fd..0f6bd10 100644 (file)
@@ -2,7 +2,7 @@
 
 
 Project "rt.3", Branch 0                                                 Page 1
-Change Log                                             Sat Jul 12 04:24:41 2003
+Change Log                                             Fri Feb 13 12:31:27 2004
 
 rt.3.D000, C0, jesse, Thu Mar 13 20:43:23 2003, RT: Request Tracker, branch 3.0.
     RT: Request Tracker, branch 3.0.
@@ -207,6 +207,1633 @@ rt.3.D000, C0, jesse, Thu Mar 13 20:43:23 2003, RT: Request Tracker, branch 3.0.
      199    150          README updates to indicate deprecated dependencies
      200    151          Debugging framework cleanup
      201    152          Bumping version to 3.0.4
+     195    153          #3042: Make max inline body size configurable
+     196    154          #3029 - better warning message on improper perms on mail in
+     202    155          Initial commit of new commandline client support code
+     203    156          More updates to the commandline client
+     205    157          Removing ancient cli code that was accidentally added to the
+                 repository
+     206    158          Extended ACL edit routines to make it easier to use generic
+                 routines in 3rd party apps
+      24    159          Certain ACL checks could fail on postgres due to a marshalling
+                 bug
+     209    160          #1751: update second page in Bulk update
+     208    161          #1651: URIs not escaped in ticket display
+     210    162          A couple of fixes to better deal with creation of 'blank'
+                 ticket requestors
+     207    163          regression tests: use $RT::WebPath and RT_LIB_PATH
+     211    164          Requestor searches had an extra join that they didn't need
+     212    165          License tagger was tagging Makefile, not Makefile.in.
+                 Reconfigured.
+     213    166          Bumping to 3.0.5pre1
+     215    167          Merging internationalization fixes from ourinternet
+     216    168          #2692: make $Domain an argument for SelectGroups
+     219    169          #2855: User_Overlay and Template_Overlay fixes
+     220    170          #2989: regexp changes for Subject and loop-detection
+     221    171          3158: user can delete only with DeleteTicket right
+     222    172          fixes for the importer
+     223    173          Adding the RT coding style guide to the distribution
+     225    174          One I18N 'fix' from ourinternet tainted attachment data,
+                 breaking tests
+     226    175          Code to catch execution problems within RT's web app server
+                 was made more robust
+      34    176          Failed user creation didn't always properly roll-back the
+                 database
+     227    177          [fsck.com #2378] personal permissions for installation
+     228    178          #3199: normalize custom fields searching syntax - Global CF's
+                 previously didn't allow the { }
+     229    179          #3201: Perform more clever joining to enhance custom field
+                 search results
+      40    180          #3200 - AND MultipleSelect CFs together -  OR all other CFs
+                 together.
+      42    181          Bumping version to 3.0.5-pre2
+      41    182          #3022: Update to German translation
+      43    183          #3068: Better setting of Due dates via the web ui
+      44    184          #3131: Preliminary support for Oracle from Brook Schonfield
+      45    185          #3152: Updated russian .po file
+      46    186          #2792: When finding out if someone is a queue watcher, check
+                 groups recursively
+      47    187          Bumping to 3.0.5pre3
+      49    188          Dependencies updated; performance and memory usage fixes for
+                 ticket creation memory usage
+     231    189          #3237: Queue-specific templates with the same name as global
+                 templates will now override the globals for queue-related
+                 scrips
+      54    190          #3279: Make fsck.com-rt: URIs case insensitive
+     230    191          #3230: Parser patch to make watchers searches more efficient
+     218    192          #2955: wrapping in messagebox
+     232    193          Old relationship update transactions weren't properly
+                 displayed
+     237    194          #2672: custom field values ordering
+     235    195          #2653: Email.pm patch
+     238    196          #3114: allow longer subject lines for postgres
+     233    197          #3242: cannonicalize addresses in comments
+     236    198          #3278: occasional internal server error in RT.pm
+     239    199          #3309: switch lines in User/Prefs.html
+     250    200          #3329: Email.pm patch
+     252    201          #2687: add ticket subject to resolved template
+     255    202          #2268: align fields in User/Prefs.html
+     256    203          #2160: clarify that box deletes scrips
+     259    204          #2773: don't allow searching for deleted tickets
+     257    205          #2700: configurable home page ticket list length
+     258    206          #2409: colons after labels in Create.html
+     261    207          #3240: DeleteWatcher, not DelWatcher
+     262    208          #3143: Italian translation
+     251    209          #2617: custom field ordering
+     260    210          #2558: allow access to CFs with no name
+     263    211          #3281: form actions must not be paths
+     266    212          #2693: show proper id in menu after creation
+     265    213          #3118: change default unset mail address
+     267    214          #3324: Apache::DBI must be 0.92 or newer
+     253    215          Fixing improperly applied custom field editing patches
+     268    216          Bumping version to 3.0.5pre4
+     269    217          #3341: edit comments in SiteConfig
+     271    218          #3012: vertical alignment in Ticket/Elements/ShowPeople
+     270    219          #3349: umlauts aren't correct in subject
+     272    220          #3236: allow attachments without other txn contents
+     273    221          #3105: CreateTickets doesn't set ticket type
+     275    222          #3384: recursive merge patches
+     276    223          #3354: an additional fix for avoiding the morning bug
+     277    224          #3114: increase subject length in non-Postgres dbs
+     278    225          Fixes to attempt to stop mysql 'morning bugs' with mysql
+     280    226          Post 3.0.5pre3 - sometimes silently losing mail. fixed a
+                 possible bug, improved testing
+     281    227          More explicit warning about a lack of perl 5.8
+     282    228          fixing the new testdeps thing
+     279    229          #2651: localize die/warn handlers
+      64    230          Bumping to 3.0.5pre5
+     283    231          #3399: Message parsing fails for some types of report
+     285    232          Better handling of apparently bogus email; rationalize mail
+                 gateway error codes
+     286    233          Bumping to 3.0.5pre6
+     287    234          Bumping to 3.0.5RC1
+     288    235          Patches to the cli from ams
+     289    236          Custom field values couldn't be set to '0'; README updated for
+                 apache2
+     290    237          RT 3.0.5
+     291    238          Fixing a couple bugs related to display of links
+     292    239          fixing a multiple-signature-inclusion bug
+     293    240          Bumping to 3.0.6RC1
+     295    241          Updated documentation for RT CLI tool
+     296    242          Bumping to RT 3.0.6
+     297    243          Conditionalizing Text::Quoted display, so as to avoid utf8
+                 crashes
+     299    244          Merging bugfixes from ourinternet
+     300    245          A bunch of postgres correctness fixes
+     301    246          #2346: Resolving a deprecation warning
+      84    247          #3981: Ticket creation syntax fix
+     298    248          bps #1032: SelfService fixes
+     302    249          #3889: add/del fixes
+     303    250          #3822: Fix for cli bug (Inapropriate use of arrayref)
+     305    251          #3807: CLI example updates
+     306    252          #3907: New default templates for user, ticket and queue
+     308    253          More reference weakening
+     307    254          User objects weren't always destroyed, due to a circular
+                 reference
+     310    255          Slightly better debugging on failure to send mail
+      94    256          Deep recursion issue on localization handle; missing language
+                 selector
+     313    257          Adding back missing SelectLang
+     311    258          #3566: EditCustomField supports Default for FreeformSingle
+     312    259          Initial Informix port from akso.de
+     316    260          #3765: TicketsSQL is case-sensitive
+     317    261          #3613: searching on NOT LIKE
+     318    262          #3877: don't strip multi-line headers
+     319    263          #3551: Create values corrupted when adding new files
+     321    264          #3993: warn when installing with mod_perl2
+     320    265          #4087: allow non-ISO dates in transaction searches
+     322    266          #3439: custom fields patch
+     323    267          #3855: require Locale::Maketext::Lexicon 0.31
+     325    268          #3856: /REST/1.0/search/tickets should work in UTC
+     326    269          #3827: regression shouldn't drop db
+     327    270          #3801: note when transaction content is ellided
+     328    271          #3751: ParseNewMessageForTicketCcs
+     332    272          #3776: new indices
+     329    273          #3674: autohandler patch
+     336    274          New schema relationships diagram in .dot format
+     337    275          more work on the schema diagram
+     330    276          #3601: SelectRights patch
+     331    277          #3583: set Last Contacted date
+     333    278          #4088: Postgres performance improvements
+     335    279          fix attachment links in base RT
+     338    280          Updating storable dependency, to keep redhat 9 users from
+                 hurting themselves
+     339    281          Small fix to ticket searching to cut down on # of joins needed
+     104    282          Merging ourinternet's changes relative to 3.0.7pre2; UPGRADING
+                 update; postgres installation fixes
+     350    283          Bumping version to 3.0.7pre3
+     351    284          CLI changes
+     352    285          CLI usage updates
+     353    286          Bumping to 3.0.7rc1
+     361    287          Bumping to 3.0.7; Updated DBIx::SearchBuilder dependency
+     362    288          Fixes to RT 3.0.7 upgrade instructions; Bumping to 3.0.7_01
+     355    289          Minor cleanups to RT cli tool
+     356    290          Fixup to rt-setup-database tool for local schema
+     357    291          Display.html takes TicketObj; Update.html uses it
+     358    292          localization for link text, not whole link
+     359    293          ProcessTicketCustomFieldUpdates takes TicketObj
+     360    294          Transaction batching
+     363    295          protect against reentrancy in Ticket::DESTROY
+     365    296          FastCGI fix to make the CLI work
+     366    297          #4415: Fix for imporeting merged tickets
+     368    298          Regression and upgrade cleanups
+     367    299          Ticket creation and updates via the Web UI were sometimes
+                 encoded wrong
+     369    300          CLI tool should pass through orderby arg
+     370    301          Bumping to 3.0.8pre1
+     371    302          none
+     372    303          Decode uuencoded attachments
+     373    304          Fixing next/prev ticket navigation (#4461) it sometimes
+                 disappeared.
+     375    305          Ticket counts became inaccurate after repeated web searches
+     376    306          Certain non-western From: headers were being mangled
+     377    307          Numerous CLI improvements
+     378    308          Bumping to 3.0.8pre2
+     379    309          Switching to new I18NSafe lowercasing behaviour for Pg
+     380    310          Adding a new index doubles my performance on /index.html
+     381    311          Importing fixes from ourinternet
+     382    312          Researching email corruption
+     197    313          Fixing CreaetTickets documentation
+     383    314          #4572: Fix searching on links
+     385    315          #4554: callbacks should be ordered
+     386    316          #4552 arguments for AddCustomFieldValues
+     387    317          #4455: Better handling of bad link URIs
+     388    318          #3725: Making CLI display newly created tickets ids
+     389    319          #3736: Backport RT 3.1 'inplace' layout
+     390    320          #3813: the CreateTickets scripaction couldn't handle
+                 customfields
+     391    321          #3608: search by ccs and adminccs in addition to requestors
+     114    322          #3066: crontool docs
+     393    323          #4711: search ON Dates
+     392    324          order SelfService tickets by numeric id
+     395    325          Bumping to 3.0.8RC1
+     396    326          AutoOpen should set correct type for Status transaction
+     397    327          Fixing apparent SQL error on logout. Actually bug in
+                 localization
+      21    328          #2587: turn off autocomplete in RT's search box
+     309    329          #3660: add a 'timeout' flag to the rt-mailgate
+     315    330          #3608: fixing quicksearch to work with new watcher search
+     398    331          Allow RT::CurrentUser to load objects based on RT::User
+                 objects passed in; Backported a fix to the Language Selector
+                 for non-traditional languages; bumped to 3.0.8
+     399    332          Searching for role groups generated queries that were way too
+                 complex
+      48    333          First cut at new oracle code from Netzah
+     501    334          Adding UTF8 support for oracle; Oracle install instructions;
+                 searchbuilder dep bumped; bumping to 3.0.9pre2
+     502    335          Deleted tickets should never be found in searches
+     503    336          #5212  Unicode issues with incoming mail with a charset or
+                 encoding of UTF-8 (caps)
+     500    337          Reversing our no-cache pragmas, since IE can't cope with them
+                 over SSL
+     506    338          A couple of perf fixes from autrijus which _require_
+                 SearchBuilder 0.97. Reduces long ticket display by 30%
+     508    339          Bumping to 3.0.9pre3
+     509    340          RT now uses progressive rendering by default AND no longer
+                 blocks the load of the CSS sheet; 3.0.9pre4
+     511    341          Optimizing column loads from autrijus' new column load patches
+     512    342          Bumping to 3.0.9pre5
+     513    343          More performance work listing ticket Attachments
+     515    344          Turning off autoflush for ticket attachments so we can a
+                 content type
+     516    345          #5178: Use less verbose html for ticket history
+     517    346          Bumping to 3.0.9pre6
+     518    347          Improved next/prev handling for merged tickets
+     519    348          Bumping to 3.0.9 - Noting preference for perl 5.8.3
+
+  rt.3.0.D348, C519, jesse, Fri Feb 13 12:30:42 2004, Bumping to 3.0.9 - Noting
+  preference for perl 5.8.3
+      none
+
+  rt.3.0.D347, C518, jesse, Thu Feb 12 00:21:35 2004, Improved next/prev
+  handling for merged tickets
+      From: Jesse <jesse@bitsy>
+      Date: Thu Feb 12 00:20:40 2004
+
+      none
+
+  rt.3.0.D346, C517, jesse, Tue Feb 10 00:22:11 2004, Bumping to 3.0.9pre6
+      From: Jesse <jesse@bitsy>
+      Date: Tue Feb 10 00:21:24 2004
+      Warning: the original change was in the 'being_integrated' state
+
+      none
+
+  rt.3.0.D345, C516, jesse, Tue Feb 10 00:19:50 2004, #5178: Use less verbose
+  html for ticket history
+      From: Jesse <jesse@bitsy>
+      Date: Tue Feb 10 00:18:57 2004
+
+      none
+
+  rt.3.0.D344, C515, jesse, Tue Feb 10 00:07:41 2004, Turning off autoflush for
+  ticket attachments so we can a content type
+      From: Jesse <jesse@bitsy>
+      Date: Tue Feb 10 00:02:40 2004
+
+      none
+
+  rt.3.0.D343, C513, jesse, Mon Feb  9 23:48:40 2004, More performance work
+  listing ticket Attachments
+      From: Jesse <jesse@bitsy>
+      Date: Mon Feb  9 23:48:07 2004
+
+      none
+
+  rt.3.0.D342, C512, jesse, Fri Feb  6 02:05:13 2004, Bumping to 3.0.9pre5
+      none
+
+  rt.3.0.D341, C511, jesse, Thu Feb  5 23:11:20 2004, Optimizing column loads
+  from autrijus' new column load patches
+      none
+
+  rt.3.0.D340, C509, jesse, Wed Feb  4 17:56:57 2004, RT now uses progressive
+  rendering by default AND no longer blocks the load of the CSS sheet; 3.0.9pre4
+      none
+
+  rt.3.0.D339, C508, jesse, Wed Feb  4 16:42:08 2004, Bumping to 3.0.9pre3
+      none
+
+  rt.3.0.D338, C506, jesse, Wed Feb  4 15:06:36 2004, A couple of perf fixes
+  from autrijus which _require_ SearchBuilder 0.97. Reduces long ticket display
+  by 30%
+      none
+
+  rt.3.0.D337, C500, jesse, Sun Feb  1 15:01:39 2004, Reversing our no-cache
+  pragmas, since IE can't cope with them over SSL
+      none
+
+  rt.3.0.D336, C503, jesse, Wed Jan 28 19:56:55 2004, #5212  Unicode issues with
+  incoming mail with a charset or encoding of UTF-8 (caps)
+      none
+
+  rt.3.0.D335, C502, jesse, Wed Jan 28 19:24:48 2004, Deleted tickets should
+  never be found in searches
+      none
+
+  rt.3.0.D334, C501, jesse, Thu Jan  8 15:21:02 2004, Adding UTF8 support for
+  oracle; Oracle install instructions; searchbuilder dep bumped; bumping to
+  3.0.9pre2
+      none
+
+  rt.3.0.D333, C48, jesse, Sun Jan  4 23:18:16 2004, First cut at new oracle
+  code from Netzah
+      From: Jesse <jesse@bitsy>
+      Date: Sun Jan  4 23:17:21 2004
+
+      none
+
+  rt.3.0.D332, C399, jesse, Sat Jan  3 15:57:44 2004, Searching for role groups
+  generated queries that were way too complex
+      From: Jesse <jesse@bitsy>
+      Date: Sat Jan  3 15:57:50 2004
+
+      none
+
+  rt.3.0.D331, C398, jesse, Fri Jan  2 16:22:47 2004, Allow RT::CurrentUser to
+  load objects based on RT::User objects passed in; Backported a fix to the
+  Language Selector for non-traditional languages; bumped to 3.0.8
+      From: Jesse <jesse@bitsy>
+      Date: Fri Jan  2 16:22:55 2004
+
+      none
+
+  rt.3.0.D330, C315, jesse, Fri Jan  2 15:10:37 2004, #3608: fixing quicksearch
+  to work with new watcher search
+      From: Jesse Vincent <jesse@hostname>
+      Date: Sat Dec 13 15:31:06 2003
+
+      none
+
+  rt.3.0.D329, C309, jesse, Fri Jan  2 15:10:02 2004, #3660: add a 'timeout'
+  flag to the rt-mailgate
+      From: Jesse Vincent <jesse@hostname>
+      Date: Sat Dec 13 02:12:05 2003
+
+      none
+
+  rt.3.0.D328, C21, jesse, Fri Jan  2 15:09:18 2004, #2587: turn off
+  autocomplete in RT's search box
+      From: Jesse Vincent <jesse@hostname>
+      Date: Sat Dec 13 01:57:52 2003
+
+      none
+
+  rt.3.0.D327, C397, jesse, Tue Dec 30 16:19:24 2003, Fixing apparent SQL error
+  on logout. Actually bug in localization
+      none
+
+  rt.3.0.D326, C396, leira, Fri Dec 19 01:11:25 2003, AutoOpen should set
+  correct type for Status transaction
+      From: Linda L. Julien <leira@starsong.org>
+      Date: Thu Dec 18 23:56:15 2003
+      Warning: the original change was in the 'awaiting_integration' state
+
+      none
+
+  rt.3.0.D325, C395, jesse, Wed Dec 17 14:12:38 2003, Bumping to 3.0.8RC1
+      From: Jesse <jesse@bitsy>
+      Date: Thu Dec 18 15:00:12 2003
+
+      none
+
+  rt.3.0.D324, C392, leira, Wed Dec 17 14:09:47 2003, order SelfService tickets
+  by numeric id
+      From: Linda Julien <leira@hawthorn.local.>
+      Date: Mon Dec 15 04:21:15 2003
+      Warning: the original change was in the 'awaiting_integration' state
+
+      none
+
+  rt.3.0.D323, C393, jesse, Mon Dec 15 19:28:46 2003, #4711: search ON Dates
+      From: Jesse <jesse@bitsy>
+      Date: Tue Dec 16 20:30:27 2003
+      Warning: the original change was in the 'being_developed' state
+
+      none
+
+  rt.3.0.D322, C114, jesse, Sat Dec 13 01:32:42 2003, #3066: crontool docs
+      From: Jesse Vincent <jesse@hostname>
+      Date: Sat Dec 13 00:38:04 2003
+
+      none
+
+  rt.3.0.D321, C391, jesse, Sat Dec 13 01:32:12 2003, #3608: search by ccs and
+  adminccs in addition to requestors
+      From: Jesse Vincent <jesse@hostname>
+      Date: Sat Dec 13 01:24:33 2003
+      Warning: the original change was in the 'being_developed' state
+
+      none
+
+  rt.3.0.D320, C390, jesse, Sat Dec 13 01:29:24 2003, #3813: the CreateTickets
+  scripaction couldn't handle customfields
+      From: Jesse Vincent <jesse@hostname>
+      Date: Sat Dec 13 01:22:56 2003
+
+      none
+
+  rt.3.0.D319, C389, jesse, Sat Dec 13 01:28:24 2003, #3736: Backport RT 3.1
+  'inplace' layout
+      From: Jesse Vincent <jesse@hostname>
+      Date: Sat Dec 13 01:15:39 2003
+
+      none
+
+  rt.3.0.D318, C388, jesse, Sat Dec 13 01:28:13 2003, #3725: Making CLI display
+  newly created tickets ids
+      From: Jesse Vincent <jesse@hostname>
+      Date: Sat Dec 13 01:12:20 2003
+
+      none
+
+  rt.3.0.D317, C387, jesse, Sat Dec 13 01:27:59 2003, #4455: Better handling of
+  bad link URIs
+      From: Jesse Vincent <jesse@hostname>
+      Date: Fri Dec 12 23:33:09 2003
+
+      none
+
+  rt.3.0.D316, C386, jesse, Sat Dec 13 01:27:41 2003, #4552 arguments for
+  AddCustomFieldValues
+      From: Jesse Vincent <jesse@hostname>
+      Date: Fri Dec 12 23:23:08 2003
+
+      none
+
+  rt.3.0.D315, C385, jesse, Sat Dec 13 01:27:17 2003, #4554: callbacks should be
+  ordered
+      From: Jesse Vincent <jesse@hostname>
+      Date: Fri Dec 12 23:14:57 2003
+
+      none
+
+  rt.3.0.D314, C383, jesse, Sat Dec 13 01:27:06 2003, #4572: Fix searching on
+  links
+      From: Jesse Vincent <jesse@hostname>
+      Date: Fri Dec 12 23:11:24 2003
+
+      none
+
+  rt.3.0.D313, C197, jesse, Fri Dec 12 23:18:59 2003, Fixing CreaetTickets
+  documentation
+      From: Jesse Vincent <jesse@localhost>
+      Date: Tue Jul  8 19:59:48 2003
+
+      none
+
+  rt.3.0.D312, C382, jesse, Thu Dec 11 12:58:27 2003, Researching email
+  corruption
+      From: Jesse <jesse@bitsy>
+      Date: Fri Dec 12 14:02:37 2003
+
+      none
+
+  rt.3.0.D311, C381, jesse, Mon Dec  8 03:06:53 2003, Importing fixes from
+  ourinternet
+      From: Jesse <jesse@bitsy>
+      Date: Tue Dec  9 04:10:35 2003
+
+      none
+
+  rt.3.0.D310, C380, jesse, Mon Dec  8 02:52:37 2003, Adding a new index doubles
+  my performance on /index.html
+      From: Jesse <jesse@bitsy>
+      Date: Tue Dec  9 03:53:40 2003
+
+      none
+
+  rt.3.0.D309, C379, jesse, Sat Dec  6 21:06:31 2003, Switching to new I18NSafe
+  lowercasing behaviour for Pg
+      From: Jesse <jesse@bitsy>
+      Date: Sun Dec  7 22:11:09 2003
+
+      none
+
+  rt.3.0.D308, C378, jesse, Fri Dec  5 16:51:11 2003, Bumping to 3.0.8pre2
+      From: Jesse <jesse@bitsy>
+      Date: Sat Dec  6 17:43:47 2003
+
+      none
+
+  rt.3.0.D307, C377, leira, Fri Dec  5 16:45:20 2003, Numerous CLI improvements
+      From: Linda Julien <leira@hawthorn.local.>
+      Date: Fri Dec  5 03:09:04 2003
+      Warning: the original change was in the 'awaiting_integration' state
+
+      none
+
+  rt.3.0.D306, C376, jesse, Wed Dec  3 01:15:23 2003, Certain non-western From:
+  headers were being mangled
+      From: Jesse Vincent <jesse@hostname>
+      Date: Wed Dec  3 01:14:25 2003
+
+      none
+
+  rt.3.0.D305, C375, jesse, Tue Dec  2 19:05:41 2003, Ticket counts became
+  inaccurate after repeated web searches
+      From: Jesse Vincent <jesse@hostname>
+      Date: Tue Dec  2 19:03:18 2003
+
+      none
+
+  rt.3.0.D304, C373, jesse, Tue Dec  2 18:36:05 2003, Fixing next/prev ticket
+  navigation (#4461) it sometimes disappeared.
+      From: Jesse Vincent <jesse@hostname>
+      Date: Tue Dec  2 18:34:07 2003
+
+      none
+
+  rt.3.0.D303, C372, jesse, Thu Nov 27 12:28:56 2003, Decode uuencoded
+  attachments
+      From: Jesse <jesse@bitsy>
+      Date: Fri Nov 28 13:32:25 2003
+
+      none
+
+  rt.3.0.D302, C371, jesse, Thu Nov 27 02:00:59 2003, none
+      From: Jesse <jesse@bitsy>
+      Date: Fri Nov 28 03:05:10 2003
+
+      none
+
+  rt.3.0.D301, C370, jesse, Tue Nov 25 22:24:45 2003, Bumping to 3.0.8pre1
+      none
+
+  rt.3.0.D300, C369, leira, Fri Nov 21 12:30:08 2003, CLI tool should pass
+  through orderby arg
+      From: Linda Julien <leira@hawthorn.local.>
+      Date: Fri Nov 21 12:22:20 2003
+      Warning: the original change was in the 'awaiting_integration' state
+
+      none
+
+  rt.3.0.D299, C367, jesse, Fri Nov 21 00:24:44 2003, Ticket creation and
+  updates via the Web UI were sometimes encoded wrong
+      From: Jesse <jesse@bitsy>
+      Date: Sat Nov 22 01:17:10 2003
+
+      none
+
+  rt.3.0.D298, C368, jesse, Fri Nov 21 00:21:32 2003, Regression and upgrade
+  cleanups
+      From: Jesse <jesse@bitsy>
+      Date: Sat Nov 22 01:20:08 2003
+
+      none
+
+  rt.3.0.D297, C366, jesse, Thu Nov 20 17:44:16 2003, #4415: Fix for imporeting
+  merged tickets
+      From: Jesse <jesse@bitsy>
+      Date: Fri Nov 21 18:47:53 2003
+
+      none
+
+  rt.3.0.D296, C365, jesse, Thu Nov 20 17:22:51 2003, FastCGI fix to make the
+  CLI work
+      From: Jesse <jesse@bitsy>
+      Date: Fri Nov 21 18:24:18 2003
+
+      none
+
+  rt.3.0.D295, C363, leira, Thu Nov 20 17:21:32 2003, protect against reentrancy
+  in Ticket::DESTROY
+      From: Linda L. Julien <leira@starsong.org>
+      Date: Wed Nov 19 15:45:47 2003
+      Warning: the original change was in the 'awaiting_integration' state
+
+      none
+
+  rt.3.0.D294, C360, leira, Mon Nov 17 23:28:48 2003, Transaction batching
+      From: Linda Julien <leira@hawthorn.local.>
+      Date: Mon Nov 17 18:52:37 2003
+      Warning: the original change was in the 'being_developed' state
+
+      From: Jesse <jesse@bitsy>
+      Date: Sun Nov 16 18:10:00 2003
+      Warning: the original change was in the 'being_developed' state
+
+      none
+
+  rt.3.0.D293, C359, leira, Mon Nov 17 23:24:21 2003,
+  ProcessTicketCustomFieldUpdates takes TicketObj
+      From: Linda Julien <leira@hawthorn.local.>
+      Date: Mon Nov 17 17:59:28 2003
+      Warning: the original change was in the 'awaiting_integration' state
+
+      none
+
+  rt.3.0.D292, C358, leira, Mon Nov 17 23:21:05 2003, localization for link
+  text, not whole link
+      From: Linda Julien <leira@hawthorn.local.>
+      Date: Mon Nov 17 17:33:58 2003
+      Warning: the original change was in the 'awaiting_integration' state
+
+      none
+
+  rt.3.0.D291, C357, leira, Mon Nov 17 23:20:32 2003, Display.html takes
+  TicketObj; Update.html uses it
+      From: Linda Julien <leira@hawthorn.local.>
+      Date: Mon Nov 17 17:19:05 2003
+      Warning: the original change was in the 'awaiting_integration' state
+
+      none
+
+  rt.3.0.D290, C356, leira, Mon Nov 17 23:16:11 2003, Fixup to rt-setup-database
+  tool for local schema
+      From: Linda Julien <leira@hawthorn.local.>
+      Date: Mon Nov 17 15:30:46 2003
+      Warning: the original change was in the 'awaiting_integration' state
+
+      From: Jesse <jesse@bitsy>
+      Date: Sun Nov 16 18:09:16 2003
+      Warning: the original change was in the 'being_developed' state
+
+      none
+
+  rt.3.0.D289, C355, leira, Mon Nov 17 23:14:05 2003, Minor cleanups to RT cli
+  tool
+      From: Linda Julien <leira@hawthorn.local.>
+      Date: Mon Nov 17 14:59:54 2003
+      Warning: the original change was in the 'awaiting_integration' state
+
+      From: Jesse <jesse@bitsy>
+      Date: Sun Nov 16 18:09:22 2003
+      Warning: the original change was in the 'being_developed' state
+
+      none
+
+  rt.3.0.D288, C362, jesse, Mon Nov 17 22:53:56 2003, Fixes to RT 3.0.7 upgrade
+  instructions; Bumping to 3.0.7_01
+      From: Jesse <jesse@bitsy>
+      Date: Tue Nov 18 23:56:48 2003
+
+      none
+
+  rt.3.0.D287, C361, jesse, Mon Nov 17 19:30:11 2003, Bumping to 3.0.7; Updated
+  DBIx::SearchBuilder dependency
+      From: Jesse <jesse@bitsy>
+      Date: Tue Nov 18 20:25:24 2003
+      Warning: the original change was in the 'being_developed' state
+
+      none
+
+  rt.3.0.D286, C353, jesse, Thu Nov 13 03:11:35 2003, Bumping to 3.0.7rc1
+      From: Jesse <jesse@bitsy>
+      Date: Fri Nov 14 04:13:34 2003
+
+      none
+
+  rt.3.0.D285, C352, jesse, Thu Nov 13 02:59:36 2003, CLI usage updates
+      From: Linda Julien <leira@hawthorn.local.>
+      Date: Thu Nov 13 02:58:17 2003
+      Warning: the original change was in the 'awaiting_integration' state
+
+      none
+
+  rt.3.0.D284, C351, leira, Thu Nov 13 02:41:21 2003, CLI changes
+      From: Linda Julien <leira@hawthorn.local.>
+      Date: Thu Nov 13 02:36:17 2003
+      Warning: the original change was in the 'awaiting_integration' state
+
+      none
+
+  rt.3.0.D283, C350, jesse, Mon Nov 10 01:26:11 2003, Bumping version to
+  3.0.7pre3
+      From: Jesse Vincent <jesse@hostname>
+      Date: Mon Nov 10 01:24:27 2003
+
+      none
+
+  rt.3.0.D282, C104, jesse, Mon Nov 10 01:16:01 2003, Merging ourinternet's
+  changes relative to 3.0.7pre2; UPGRADING update; postgres installation fixes
+      From: Jesse Vincent <jesse@hostname>
+      Date: Mon Nov 10 01:10:08 2003
+
+      none
+
+  rt.3.0.D281, C339, jesse, Thu Nov  6 21:11:46 2003, Small fix to ticket
+  searching to cut down on # of joins needed
+      From: Jesse Vincent <jesse@hostname>
+      Date: Thu Nov  6 21:11:21 2003
+
+      none
+
+  rt.3.0.D280, C338, jesse, Thu Nov  6 21:05:17 2003, Updating storable
+  dependency, to keep redhat 9 users from hurting themselves
+      From: Jesse Vincent <jesse@hostname>
+      Date: Thu Nov  6 20:53:44 2003
+
+      none
+
+  rt.3.0.D279, C335, leira, Tue Nov  4 21:11:39 2003, fix attachment links in
+  base RT
+      From: Linda L. Julien <leira@starsong.org>
+      Date: Tue Nov  4 16:42:55 2003
+      Warning: the original change was in the 'awaiting_integration' state
+
+      none
+
+  rt.3.0.D278, C333, leira, Tue Nov  4 21:10:18 2003, #4088: Postgres
+  performance improvements
+      From: Linda L. Julien <leira@starsong.org>
+      Date: Tue Nov  4 16:25:57 2003
+      Warning: the original change was in the 'awaiting_integration' state
+
+      none
+
+  rt.3.0.D277, C331, leira, Tue Nov  4 21:08:27 2003, #3583: set Last Contacted
+  date
+      From: Linda L. Julien <leira@starsong.org>
+      Date: Mon Nov  3 13:58:57 2003
+      Warning: the original change was in the 'awaiting_integration' state
+
+      none
+
+  rt.3.0.D276, C330, leira, Tue Nov  4 21:06:14 2003, #3601: SelectRights patch
+      From: Linda L. Julien <leira@starsong.org>
+      Date: Sun Nov  2 22:03:23 2003
+      Warning: the original change was in the 'awaiting_integration' state
+
+      none
+
+  rt.3.0.D275, C337, jesse, Tue Nov  4 18:18:39 2003, more work on the schema
+  diagram
+      From: Jesse Vincent <jesse@hostname>
+      Date: Tue Nov  4 18:16:53 2003
+
+      none
+
+  rt.3.0.D274, C336, jesse, Tue Nov  4 18:18:24 2003, New schema relationships
+  diagram in .dot format
+      From: Jesse Vincent <jesse@hostname>
+      Date: Tue Nov  4 17:51:10 2003
+
+      none
+
+  rt.3.0.D273, C329, leira, Tue Nov  4 17:52:14 2003, #3674: autohandler patch
+      From: Linda L. Julien <leira@starsong.org>
+      Date: Sun Nov  2 21:49:43 2003
+
+      none
+
+  rt.3.0.D272, C332, leira, Tue Nov  4 17:29:08 2003, #3776: new indices
+      From: Linda L. Julien <leira@starsong.org>
+      Date: Tue Nov  4 15:25:39 2003
+      Warning: the original change was in the 'awaiting_integration' state
+
+      none
+
+  rt.3.0.D271, C328, leira, Sun Nov  2 23:07:51 2003, #3751:
+  ParseNewMessageForTicketCcs
+      From: Linda L. Julien <leira@starsong.org>
+      Date: Sun Nov  2 21:16:07 2003
+
+      none
+
+  rt.3.0.D270, C327, leira, Sun Nov  2 22:49:39 2003, #3801: note when
+  transaction content is ellided
+      From: Linda L. Julien <leira@starsong.org>
+      Date: Sun Nov  2 20:44:58 2003
+      Warning: the original change was in the 'awaiting_integration' state
+
+      none
+
+  rt.3.0.D269, C326, leira, Sun Nov  2 22:31:42 2003, #3827: regression
+  shouldn't drop db
+      From: Linda L. Julien <leira@starsong.org>
+      Date: Sun Nov  2 20:17:48 2003
+      Warning: the original change was in the 'awaiting_integration' state
+
+      none
+
+  rt.3.0.D268, C325, leira, Sun Nov  2 21:15:22 2003, #3856: /REST/1.0/search/
+  tickets should work in UTC
+      From: Linda L. Julien <leira@starsong.org>
+      Date: Sun Nov  2 19:57:19 2003
+      Warning: the original change was in the 'awaiting_integration' state
+
+      none
+
+  rt.3.0.D267, C323, leira, Sun Nov  2 21:14:48 2003, #3855: require
+  Locale::Maketext::Lexicon 0.31
+      From: Linda L. Julien <leira@starsong.org>
+      Date: Sun Nov  2 19:36:37 2003
+      Warning: the original change was in the 'awaiting_integration' state
+
+      none
+
+  rt.3.0.D266, C322, leira, Sun Nov  2 21:14:18 2003, #3439: custom fields patch
+      From: Linda L. Julien <leira@starsong.org>
+      Date: Sun Nov  2 19:11:11 2003
+      Warning: the original change was in the 'awaiting_integration' state
+
+      none
+
+  rt.3.0.D265, C320, leira, Sun Nov  2 20:00:45 2003, #4087: allow non-ISO dates
+  in transaction searches
+      From: Linda L. Julien <leira@starsong.org>
+      Date: Sun Nov  2 18:28:45 2003
+      Warning: the original change was in the 'awaiting_integration' state
+
+      none
+
+  rt.3.0.D264, C321, leira, Sun Nov  2 19:59:21 2003, #3993: warn when
+  installing with mod_perl2
+      From: Linda L. Julien <leira@starsong.org>
+      Date: Sun Nov  2 18:47:57 2003
+      Warning: the original change was in the 'awaiting_integration' state
+
+      none
+
+  rt.3.0.D263, C319, leira, Sun Nov  2 18:51:11 2003, #3551: Create values
+  corrupted when adding new files
+      From: Linda L. Julien <leira@starsong.org>
+      Date: Sun Nov  2 17:36:44 2003
+      Warning: the original change was in the 'awaiting_integration' state
+
+      none
+
+  rt.3.0.D262, C318, leira, Sun Nov  2 18:50:09 2003, #3877: don't strip multi-
+  line headers
+      From: Linda L. Julien <leira@starsong.org>
+      Date: Sun Nov  2 16:32:21 2003
+      Warning: the original change was in the 'awaiting_integration' state
+
+      none
+
+  rt.3.0.D261, C317, leira, Sun Nov  2 18:49:15 2003, #3613: searching on NOT
+  LIKE
+      From: Linda L. Julien <leira@starsong.org>
+      Date: Fri Oct 31 01:39:10 2003
+      Warning: the original change was in the 'awaiting_integration' state
+
+      none
+
+  rt.3.0.D260, C316, leira, Fri Oct 31 11:59:11 2003, #3765: TicketsSQL is case-
+  sensitive
+      From: Linda L. Julien <leira@starsong.org>
+      Date: Thu Oct 30 16:46:33 2003
+      Warning: the original change was in the 'awaiting_integration' state
+
+      none
+
+  rt.3.0.D259, C312, jesse, Fri Oct 31 11:58:43 2003, Initial Informix port from
+  akso.de
+      From: Jesse Vincent <jesse@hostname>
+      Date: Thu Oct 30 13:03:54 2003
+
+      none
+
+  rt.3.0.D258, C311, leira, Fri Oct 31 11:57:33 2003, #3566: EditCustomField
+  supports Default for FreeformSingle
+      From: Linda L. Julien <leira@starsong.org>
+      Date: Fri Oct 24 16:48:36 2003
+      Warning: the original change was in the 'awaiting_integration' state
+
+      none
+
+  rt.3.0.D257, C313, jesse, Thu Oct 30 15:33:45 2003, Adding back missing
+  SelectLang
+      From: Jesse Vincent <jesse@hostname>
+      Date: Thu Oct 30 15:31:35 2003
+      Warning: the original change was in the 'being_developed' state
+
+      none
+
+  rt.3.0.D256, C94, jesse, Tue Oct 28 16:02:50 2003, Deep recursion issue on
+  localization handle; missing language selector
+      From: Jesse Vincent <jesse@hostname>
+      Date: Tue Oct 28 16:01:40 2003
+
+      none
+
+  rt.3.0.D255, C310, jesse, Wed Oct 22 00:33:40 2003, Slightly better debugging
+  on failure to send mail
+      From: Jesse Vincent <jesse@hostname>
+      Date: Wed Oct 22 00:25:35 2003
+
+      none
+
+  rt.3.0.D254, C307, jesse, Tue Oct 21 23:43:39 2003, User objects weren't
+  always destroyed, due to a circular reference
+      From: Jesse Vincent <jesse@hostname>
+      Date: Mon Oct 20 19:02:08 2003
+
+      none
+
+  rt.3.0.D253, C308, jesse, Tue Oct 21 23:35:50 2003, More reference weakening
+      From: Jesse Vincent <jesse@hostname>
+      Date: Tue Oct 21 23:34:11 2003
+
+      none
+
+  rt.3.0.D252, C306, jesse, Sun Oct 19 21:06:33 2003, #3907: New default
+  templates for user, ticket and queue
+      From: Jesse Vincent <jesse@hostname>
+      Date: Sun Oct 19 21:05:48 2003
+
+      none
+
+  rt.3.0.D251, C305, jesse, Sun Oct 19 21:00:11 2003, #3807: CLI example updates
+      From: Jesse Vincent <jesse@hostname>
+      Date: Sun Oct 19 20:59:14 2003
+
+      none
+
+  rt.3.0.D250, C303, jesse, Sun Oct 19 20:55:45 2003, #3822: Fix for cli bug
+  (Inapropriate use of arrayref)
+      From: Jesse Vincent <jesse@hostname>
+      Date: Sun Oct 19 20:54:22 2003
+
+      none
+
+  rt.3.0.D249, C302, jesse, Sun Oct 19 20:55:16 2003, #3889: add/del fixes
+      From: Jesse Vincent <jesse@hostname>
+      Date: Sun Oct 19 20:25:21 2003
+
+      none
+
+  rt.3.0.D248, C298, leira, Sun Oct 19 20:23:35 2003, bps #1032: SelfService
+  fixes
+      From: Linda Julien <leira@hawthorn.local.>
+      Date: Wed Oct  8 18:39:07 2003
+      Warning: the original change was in the 'awaiting_integration' state
+
+      none
+
+  rt.3.0.D247, C84, jesse, Sun Oct 19 20:23:21 2003, #3981: Ticket creation
+  syntax fix
+      From: Jesse Vincent <jesse@hostname>
+      Date: Sun Oct 19 20:20:46 2003
+
+      none
+
+  rt.3.0.D246, C301, jesse, Sun Oct 19 20:11:09 2003, #2346: Resolving a
+  deprecation warning
+      From: Jesse Vincent <jesse@hostname>
+      Date: Sun Oct 19 20:10:25 2003
+
+      none
+
+  rt.3.0.D245, C300, jesse, Thu Oct 16 20:03:36 2003, A bunch of postgres
+  correctness fixes
+      From: Jesse Vincent <jesse@hostname>
+      Date: Thu Oct 16 20:00:27 2003
+
+      none
+
+  rt.3.0.D244, C299, jesse, Thu Oct 16 20:02:08 2003, Merging bugfixes from
+  ourinternet
+      From: Jesse Vincent <jesse@Jesse-Vincents-Computer.local.>
+      Date: Tue Oct 14 12:29:53 2003
+
+      none
+
+  rt.3.0.D243, C297, jesse, Wed Oct  8 11:47:30 2003, Conditionalizing
+  Text::Quoted display, so as to avoid utf8 crashes
+      From: Jesse Vincent <jesse@hostname>
+      Date: Wed Oct  8 16:46:31 2003
+
+      none
+
+  rt.3.0.D242, C296, jesse, Thu Sep 25 16:28:46 2003, Bumping to RT 3.0.6
+      From: Jesse Vincent <jesse@Jesse-Vincents-Computer.local.>
+      Date: Thu Sep 25 16:27:53 2003
+
+      none
+
+  rt.3.0.D241, C295, jesse, Thu Sep 25 15:13:08 2003, Updated documentation for
+  RT CLI tool
+      From: Jesse Vincent <jesse@Jesse-Vincents-Computer.local.>
+      Date: Thu Sep 25 15:12:46 2003
+
+      none
+
+  rt.3.0.D240, C293, jesse, Mon Sep 22 16:21:10 2003, Bumping to 3.0.6RC1
+      From: Jesse Vincent <jesse@localhost>
+      Date: Mon Sep 22 16:20:02 2003
+
+      none
+
+  rt.3.0.D239, C292, jesse, Mon Sep 22 16:21:00 2003, fixing a multiple-
+  signature-inclusion bug
+      From: Jesse Vincent <jesse@localhost>
+      Date: Mon Sep 22 15:29:40 2003
+
+      none
+
+  rt.3.0.D238, C291, jesse, Mon Sep 22 14:46:52 2003, Fixing a couple bugs
+  related to display of links
+      From: Jesse Vincent <jesse@localhost>
+      Date: Mon Sep 22 14:44:12 2003
+
+      none
+
+  rt.3.0.D237, C290, jesse, Mon Sep  8 14:18:29 2003, RT 3.0.5
+      From: Jesse Vincent <jesse@localhost>
+      Date: Mon Sep  8 14:12:19 2003
+
+      none
+
+  rt.3.0.D236, C289, jesse, Mon Sep  8 13:47:05 2003, Custom field values
+  couldn't be set to '0'; README updated for apache2
+      From: Jesse Vincent <jesse@localhost>
+      Date: Mon Sep  8 13:45:41 2003
+
+      none
+
+  rt.3.0.D235, C288, jesse, Sat Sep  6 01:52:41 2003, Patches to the cli from
+  ams
+      From: Jesse Vincent <jesse@localhost>
+      Date: Sat Sep  6 01:50:41 2003
+
+      none
+
+  rt.3.0.D234, C287, jesse, Tue Sep  2 18:16:15 2003, Bumping to 3.0.5RC1
+      From: Jesse Vincent <jesse@localhost>
+      Date: Tue Sep  2 18:15:51 2003
+
+      none
+
+  rt.3.0.D233, C286, jesse, Fri Aug 29 21:10:44 2003, Bumping to 3.0.5pre6
+      From: Jesse Vincent <jesse@localhost>
+      Date: Fri Aug 29 18:01:40 2003
+
+      none
+
+  rt.3.0.D232, C285, jesse, Fri Aug 29 21:10:24 2003, Better handling of
+  apparently bogus email; rationalize mail gateway error codes
+      From: Jesse Vincent <jesse@localhost>
+      Date: Fri Aug 29 18:01:29 2003
+
+      none
+
+  rt.3.0.D231, C283, jesse, Fri Aug 29 16:26:30 2003, #3399: Message parsing
+  fails for some types of report
+      From: Jesse Vincent <jesse@localhost>
+      Date: Fri Aug 29 16:25:33 2003
+
+      none
+
+  rt.3.0.D230, C64, jesse, Thu Aug 28 17:40:29 2003, Bumping to 3.0.5pre5
+      From: Jesse Vincent <jesse@localhost>
+      Date: Thu Aug 28 17:32:38 2003
+
+      none
+
+  rt.3.0.D229, C279, leira, Thu Aug 28 17:39:37 2003, #2651: localize die/warn
+  handlers
+      From: Linda Julien <leira@hawthorn.local.>
+      Date: Tue Aug 26 15:38:12 2003
+      Warning: the original change was in the 'awaiting_integration' state
+
+      none
+
+  rt.3.0.D228, C282, jesse, Thu Aug 28 17:38:28 2003, fixing the new testdeps
+  thing
+      From: Jesse Vincent <jesse@localhost>
+      Date: Thu Aug 28 17:38:07 2003
+
+      none
+
+  rt.3.0.D227, C281, jesse, Thu Aug 28 17:34:18 2003, More explicit warning
+  about a lack of perl 5.8
+      From: Jesse Vincent <jesse@localhost>
+      Date: Thu Aug 28 17:30:47 2003
+
+      none
+
+  rt.3.0.D226, C280, jesse, Thu Aug 28 17:33:15 2003, Post 3.0.5pre3 - sometimes
+  silently losing mail. fixed a possible bug, improved testing
+      From: Jesse Vincent <jesse@localhost>
+      Date: Thu Aug 28 17:09:51 2003
+
+      none
+
+  rt.3.0.D225, C278, jesse, Tue Aug 26 15:29:24 2003, Fixes to attempt to stop
+  mysql 'morning bugs' with mysql
+      From: Jesse Vincent <jesse@localhost>
+      Date: Tue Aug 26 15:28:20 2003
+
+      none
+
+  rt.3.0.D224, C277, leira, Tue Aug 26 15:04:20 2003, #3114: increase subject
+  length in non-Postgres dbs
+      From: Linda Julien <leira@hawthorn.local.>
+      Date: Tue Aug 26 14:51:25 2003
+      Warning: the original change was in the 'awaiting_integration' state
+
+      none
+
+  rt.3.0.D223, C276, leira, Tue Aug 26 15:03:17 2003, #3354: an additional fix
+  for avoiding the morning bug
+      From: Linda Julien <leira@hawthorn.local.>
+      Date: Mon Aug 25 17:58:27 2003
+      Warning: the original change was in the 'awaiting_integration' state
+
+      none
+
+  rt.3.0.D222, C275, leira, Tue Aug 26 15:02:16 2003, #3384: recursive merge
+  patches
+      From: Linda Julien <leira@hawthorn.local.>
+      Date: Mon Aug 25 17:02:00 2003
+      Warning: the original change was in the 'awaiting_integration' state
+
+      none
+
+  rt.3.0.D221, C273, leira, Mon Aug 25 16:33:53 2003, #3105: CreateTickets
+  doesn't set ticket type
+      From: Linda Julien <leira@hawthorn.local.>
+      Date: Mon Aug 25 16:30:06 2003
+      Warning: the original change was in the 'awaiting_integration' state
+
+      none
+
+  rt.3.0.D220, C272, leira, Mon Aug 25 15:53:13 2003, #3236: allow attachments
+  without other txn contents
+      From: Linda Julien <leira@hawthorn.local.>
+      Date: Mon Aug 25 15:47:52 2003
+      Warning: the original change was in the 'awaiting_integration' state
+
+      none
+
+  rt.3.0.D219, C270, leira, Mon Aug 25 15:13:31 2003, #3349: umlauts aren't
+  correct in subject
+      From: Linda Julien <leira@hawthorn.local.>
+      Date: Mon Aug 25 13:55:52 2003
+      Warning: the original change was in the 'awaiting_integration' state
+
+      none
+
+  rt.3.0.D218, C271, leira, Mon Aug 25 15:13:05 2003, #3012: vertical alignment
+  in Ticket/Elements/ShowPeople
+      From: Linda Julien <leira@hawthorn.local.>
+      Date: Mon Aug 25 14:08:04 2003
+      Warning: the original change was in the 'awaiting_integration' state
+
+      none
+
+  rt.3.0.D217, C269, leira, Mon Aug 25 14:45:12 2003, #3341: edit comments in
+  SiteConfig
+      From: Linda Julien <leira@hawthorn.local.>
+      Date: Mon Aug 25 13:38:51 2003
+      Warning: the original change was in the 'awaiting_integration' state
+
+      none
+
+  rt.3.0.D216, C268, jesse, Sun Aug 24 17:16:04 2003, Bumping version to
+  3.0.5pre4
+      From: Jesse Vincent <jesse@localhost>
+      Date: Sun Aug 24 17:15:36 2003
+
+      none
+
+  rt.3.0.D215, C253, jesse, Sun Aug 24 17:12:37 2003, Fixing improperly applied
+  custom field editing patches
+      From: Jesse Vincent <jesse@localhost>
+      Date: Sun Aug 24 17:11:40 2003
+
+      none
+
+  rt.3.0.D214, C267, leira, Sun Aug 24 14:57:37 2003, #3324: Apache::DBI must be
+  0.92 or newer
+      From: Linda Julien <leira@hawthorn.local.>
+      Date: Fri Aug 22 14:40:27 2003
+      Warning: the original change was in the 'awaiting_integration' state
+
+      none
+
+  rt.3.0.D213, C265, leira, Fri Aug 22 13:30:09 2003, #3118: change default
+  unset mail address
+      From: Linda Julien <leira@hawthorn.local.>
+      Date: Fri Aug 22 12:13:22 2003
+      Warning: the original change was in the 'awaiting_integration' state
+
+      none
+
+  rt.3.0.D212, C266, leira, Fri Aug 22 13:29:46 2003, #2693: show proper id in
+  menu after creation
+      From: Linda Julien <leira@hawthorn.local.>
+      Date: Fri Aug 22 12:39:53 2003
+      Warning: the original change was in the 'awaiting_integration' state
+
+      none
+
+  rt.3.0.D211, C263, leira, Fri Aug 22 01:18:00 2003, #3281: form actions must
+  not be paths
+      From: Linda Julien <leira@hawthorn.local.>
+      Date: Fri Aug 22 01:12:07 2003
+      Warning: the original change was in the 'awaiting_integration' state
+
+      none
+
+  rt.3.0.D210, C260, leira, Fri Aug 22 01:13:07 2003, #2558: allow access to CFs
+  with no name
+      From: Linda Julien <leira@hawthorn.local.>
+      Date: Thu Aug 21 22:36:35 2003
+      Warning: the original change was in the 'awaiting_integration' state
+
+      none
+
+  rt.3.0.D209, C251, leira, Fri Aug 22 01:07:34 2003, #2617: custom field
+  ordering
+      From: Linda Julien <leira@hawthorn.local.>
+      Date: Wed Aug 20 17:33:22 2003
+      Warning: the original change was in the 'awaiting_integration' state
+
+      none
+
+  rt.3.0.D208, C262, leira, Fri Aug 22 01:00:21 2003, #3143: Italian translation
+      From: Linda Julien <leira@hawthorn.local.>
+      Date: Fri Aug 22 00:52:11 2003
+      Warning: the original change was in the 'awaiting_integration' state
+
+      none
+
+  rt.3.0.D207, C261, leira, Thu Aug 21 23:31:22 2003, #3240: DeleteWatcher, not
+  DelWatcher
+      From: Linda Julien <leira@hawthorn.local.>
+      Date: Thu Aug 21 23:22:40 2003
+      Warning: the original change was in the 'awaiting_integration' state
+
+      none
+
+  rt.3.0.D206, C258, leira, Thu Aug 21 23:07:01 2003, #2409: colons after labels
+  in Create.html
+      From: Linda Julien <leira@hawthorn.local.>
+      Date: Thu Aug 21 17:23:04 2003
+      Warning: the original change was in the 'awaiting_integration' state
+
+      none
+
+  rt.3.0.D205, C257, leira, Thu Aug 21 23:06:12 2003, #2700: configurable home
+  page ticket list length
+      From: Linda Julien <leira@hawthorn.local.>
+      Date: Thu Aug 21 15:08:27 2003
+      Warning: the original change was in the 'awaiting_integration' state
+
+      none
+
+  rt.3.0.D204, C259, leira, Thu Aug 21 22:37:27 2003, #2773: don't allow
+  searching for deleted tickets
+      From: Linda Julien <leira@hawthorn.local.>
+      Date: Thu Aug 21 22:15:43 2003
+      Warning: the original change was in the 'awaiting_integration' state
+
+      none
+
+  rt.3.0.D203, C256, leira, Thu Aug 21 14:09:59 2003, #2160: clarify that box
+  deletes scrips
+      From: Linda Julien <leira@hawthorn.local.>
+      Date: Thu Aug 21 13:53:00 2003
+      Warning: the original change was in the 'awaiting_integration' state
+
+      none
+
+  rt.3.0.D202, C255, leira, Thu Aug 21 14:09:27 2003, #2268: align fields in
+  User/Prefs.html
+      From: Linda Julien <leira@hawthorn.local.>
+      Date: Thu Aug 21 13:21:00 2003
+      Warning: the original change was in the 'awaiting_integration' state
+
+      none
+
+  rt.3.0.D201, C252, leira, Wed Aug 20 18:13:50 2003, #2687: add ticket subject
+  to resolved template
+      From: Linda Julien <leira@hawthorn.local.>
+      Date: Wed Aug 20 17:58:22 2003
+      Warning: the original change was in the 'awaiting_integration' state
+
+      none
+
+  rt.3.0.D200, C250, leira, Wed Aug 20 13:33:40 2003, #3329: Email.pm patch
+      From: Linda Julien <leira@hawthorn.local.>
+      Date: Wed Aug 20 13:19:58 2003
+      Warning: the original change was in the 'awaiting_integration' state
+
+      none
+
+  rt.3.0.D199, C239, leira, Tue Aug 19 23:05:34 2003, #3309: switch lines in
+  User/Prefs.html
+      From: Linda Julien <leira@hawthorn.local.>
+      Date: Tue Aug 19 22:59:28 2003
+      Warning: the original change was in the 'awaiting_integration' state
+
+      none
+
+  rt.3.0.D198, C236, leira, Tue Aug 19 22:47:07 2003, #3278: occasional internal
+  server error in RT.pm
+      From: Linda Julien <leira@hawthorn.local.>
+      Date: Tue Aug 19 22:44:16 2003
+      Warning: the original change was in the 'awaiting_integration' state
+
+      none
+
+  rt.3.0.D197, C233, leira, Tue Aug 19 22:28:05 2003, #3242: cannonicalize
+  addresses in comments
+      From: Linda Julien <leira@hawthorn.local.>
+      Date: Tue Aug 19 22:21:57 2003
+      Warning: the original change was in the 'awaiting_integration' state
+
+      none
+
+  rt.3.0.D196, C238, leira, Tue Aug 19 22:07:37 2003, #3114: allow longer
+  subject lines for postgres
+      From: Linda Julien <leira@hawthorn.local.>
+      Date: Tue Aug 19 22:01:02 2003
+      Warning: the original change was in the 'awaiting_integration' state
+
+      none
+
+  rt.3.0.D195, C235, leira, Tue Aug 19 21:54:16 2003, #2653: Email.pm patch
+      From: Linda Julien <leira@hawthorn.local.>
+      Date: Tue Aug 19 19:57:12 2003
+      Warning: the original change was in the 'awaiting_integration' state
+
+      none
+
+  rt.3.0.D194, C237, leira, Tue Aug 19 21:51:11 2003, #2672: custom field values
+  ordering
+      From: Linda Julien <leira@hawthorn.local.>
+      Date: Tue Aug 19 21:19:44 2003
+      Warning: the original change was in the 'awaiting_integration' state
+
+      none
+
+  rt.3.0.D193, C232, jesse, Mon Aug 18 22:16:09 2003, Old relationship update
+  transactions weren't properly displayed
+      From: Jesse Vincent <jesse@localhost>
+      Date: Mon Aug 18 22:14:33 2003
+
+      none
+
+  rt.3.0.D192, C218, leira, Thu Aug 14 13:37:04 2003, #2955: wrapping in
+  messagebox
+      From: Linda Julien <leira@hawthorn.local.>
+      Date: Wed Aug 13 17:28:03 2003
+      Warning: the original change was in the 'awaiting_integration' state
+
+      none
+
+  rt.3.0.D191, C230, jesse, Tue Aug 12 02:54:11 2003, #3230: Parser patch to
+  make watchers searches more efficient
+      From: Jesse Vincent <jesse@localhost>
+      Date: Mon Aug 11 14:41:00 2003
+
+      none
+
+  rt.3.0.D190, C54, jesse, Tue Aug 12 02:49:27 2003, #3279: Make fsck.com-rt:
+  URIs case insensitive
+      From: Jesse Vincent <jesse@localhost>
+      Date: Mon Aug 11 15:25:07 2003
+
+      none
+
+  rt.3.0.D189, C231, jesse, Tue Aug 12 02:47:19 2003, #3237: Queue-specific
+  templates with the same name as global templates will now override the globals
+  for queue-related scrips
+      From: Jesse Vincent <jesse@localhost>
+      Date: Mon Aug 11 15:04:37 2003
+
+      none
+
+  rt.3.0.D188, C49, jesse, Fri Aug  8 01:57:57 2003, Dependencies updated;
+  performance and memory usage fixes for ticket creation memory usage
+      From: Jesse Vincent <jesse@localhost>
+      Date: Fri Aug  8 01:54:16 2003
+
+      none
+
+  rt.3.0.D187, C47, jesse, Mon Aug  4 23:20:33 2003, Bumping to 3.0.5pre3
+      From: Jesse Vincent <jesse@localhost>
+      Date: Mon Aug  4 23:20:16 2003
+
+      none
+
+  rt.3.0.D186, C46, jesse, Mon Aug  4 19:15:22 2003, #2792: When finding out if
+  someone is a queue watcher, check groups recursively
+      From: Jesse Vincent <jesse@localhost>
+      Date: Mon Aug  4 19:14:50 2003
+
+      none
+
+  rt.3.0.D185, C45, jesse, Mon Aug  4 18:55:13 2003, #3152: Updated russian .po
+  file
+      From: Jesse Vincent <jesse@localhost>
+      Date: Mon Aug  4 18:50:35 2003
+
+      none
+
+  rt.3.0.D184, C44, jesse, Mon Aug  4 18:27:40 2003, #3131: Preliminary support
+  for Oracle from Brook Schonfield
+      From: Jesse Vincent <jesse@localhost>
+      Date: Mon Aug  4 18:26:08 2003
+
+      none
+
+  rt.3.0.D183, C43, jesse, Mon Aug  4 16:19:07 2003, #3068: Better setting of
+  Due dates via the web ui
+      From: Jesse Vincent <jesse@localhost>
+      Date: Mon Aug  4 16:18:50 2003
+
+      none
+
+  rt.3.0.D182, C41, jesse, Mon Aug  4 16:02:01 2003, #3022: Update to German
+  translation
+      From: Jesse Vincent <jesse@localhost>
+      Date: Mon Aug  4 16:01:08 2003
+
+      none
+
+  rt.3.0.D181, C42, jesse, Thu Jul 31 13:34:50 2003, Bumping version to 3.0.5-
+  pre2
+      From: Jesse Vincent <jesse@localhost>
+      Date: Thu Jul 31 13:28:35 2003
+
+      none
+
+  rt.3.0.D180, C40, jesse, Tue Jul 29 02:24:22 2003, #3200 - AND MultipleSelect
+  CFs together -  OR all other CFs together.
+      From: Jesse Vincent <jesse@localhost>
+      Date: Tue Jul 29 02:21:50 2003
+
+      none
+
+  rt.3.0.D179, C229, jesse, Tue Jul 29 02:09:06 2003, #3201: Perform more clever
+  joining to enhance custom field search results
+      From: Jesse Vincent <jesse@localhost>
+      Date: Tue Jul 29 02:06:22 2003
+
+      none
+
+  rt.3.0.D178, C228, jesse, Tue Jul 29 01:17:12 2003, #3199: normalize custom
+  fields searching syntax - Global CF's previously didn't allow the { }
+      From: Jesse Vincent <jesse@localhost>
+      Date: Tue Jul 29 01:16:28 2003
+
+      none
+
+  rt.3.0.D177, C227, jesse, Mon Jul 28 01:39:28 2003, [fsck.com #2378] personal
+  permissions for installation
+      From: Robert <rspier@bear>
+      Date: Sun Jul 27 22:19:40 2003
+      Warning: the original change was in the 'awaiting_integration' state
+
+      install as current user option for configure
+
+  rt.3.0.D176, C34, jesse, Mon Jul 28 00:00:12 2003, Failed user creation didn't
+  always properly roll-back the database
+      From: Jesse Vincent <jesse@localhost>
+      Date: Sun Jul 27 23:59:16 2003
+
+      none
+
+  rt.3.0.D175, C226, jesse, Sun Jul 27 23:52:28 2003, Code to catch execution
+  problems within RT's web app server was made more robust
+      From: Jesse Vincent <jesse@localhost>
+      Date: Sun Jul 27 23:51:50 2003
+
+      none
+
+  rt.3.0.D174, C225, jesse, Sun Jul 27 23:44:44 2003, One I18N 'fix' from
+  ourinternet tainted attachment data, breaking tests
+      From: Jesse Vincent <jesse@localhost>
+      Date: Sun Jul 27 23:44:17 2003
+
+      none
+
+  rt.3.0.D173, C223, jesse, Sun Jul 27 17:16:20 2003, Adding the RT coding style
+  guide to the distribution
+      From: Jesse Vincent <jesse@localhost>
+      Date: Sun Jul 27 17:15:32 2003
+
+      none
+
+  rt.3.0.D172, C222, jesse, Fri Jul 25 19:50:19 2003, fixes for the importer
+      From: Jesse Vincent <jesse@localhost>
+      Date: Fri Jul 25 19:49:06 2003
+
+      none
+
+  rt.3.0.D171, C221, leira, Fri Jul 25 14:02:49 2003, 3158: user can delete only
+  with DeleteTicket right
+      From: Linda Julien <leira@hawthorn.local.>
+      Date: Fri Jul 25 02:44:41 2003
+      Warning: the original change was in the 'being_developed' state
+
+      none
+
+  rt.3.0.D170, C220, leira, Fri Jul 25 14:01:38 2003, #2989: regexp changes for
+  Subject and loop-detection
+      From: Linda Julien <leira@hawthorn.local.>
+      Date: Fri Jul 25 02:02:18 2003
+      Warning: the original change was in the 'awaiting_integration' state
+
+      none
+
+  rt.3.0.D169, C219, leira, Fri Jul 25 13:52:47 2003, #2855: User_Overlay and
+  Template_Overlay fixes
+      From: Linda Julien <leira@hawthorn.local.>
+      Date: Fri Jul 25 01:08:37 2003
+      Warning: the original change was in the 'awaiting_integration' state
+
+      none
+
+  rt.3.0.D168, C216, leira, Fri Jul 25 13:45:37 2003, #2692: make $Domain an
+  argument for SelectGroups
+      From: Linda Julien <leira@hawthorn.local.>
+      Date: Thu Jul 24 23:31:35 2003
+      Warning: the original change was in the 'awaiting_integration' state
+
+      none
+
+  rt.3.0.D167, C215, jesse, Thu Jul 24 23:25:01 2003, Merging
+  internationalization fixes from ourinternet
+      From: Jesse Vincent <jesse@localhost>
+      Date: Thu Jul 24 23:22:46 2003
+
+      none
+
+  rt.3.0.D166, C213, jesse, Thu Jul 24 18:39:57 2003, Bumping to 3.0.5pre1
+      From: Jesse Vincent <jesse@localhost>
+      Date: Thu Jul 24 18:38:14 2003
+
+      none
+
+  rt.3.0.D165, C212, jesse, Wed Jul 23 18:06:41 2003, License tagger was tagging
+  Makefile, not Makefile.in. Reconfigured.
+      From: Jesse Vincent <jesse@localhost>
+      Date: Wed Jul 23 18:03:14 2003
+
+      none
+
+  rt.3.0.D164, C211, jesse, Wed Jul 23 15:23:11 2003, Requestor searches had an
+  extra join that they didn't need
+      From: Jesse Vincent <jesse@localhost>
+      Date: Wed Jul 23 15:22:34 2003
+
+      none
+
+  rt.3.0.D163, C207, leira, Tue Jul 22 03:10:34 2003, regression tests: use
+  $RT::WebPath and RT_LIB_PATH
+      From: Linda L. Julien <leira@starsong.org>
+      Date: Mon Jul 21 18:12:56 2003
+      Warning: the original change was in the 'awaiting_integration' state
+
+      none
+
+  rt.3.0.D162, C210, jesse, Tue Jul 22 02:52:01 2003, A couple of fixes to
+  better deal with creation of 'blank' ticket requestors
+      From: Jesse Vincent <jesse@localhost>
+      Date: Tue Jul 22 02:50:09 2003
+
+      none
+
+  rt.3.0.D161, C208, leira, Tue Jul 22 01:56:30 2003, #1651: URIs not escaped in
+  ticket display
+      From: Linda Julien <leira@hawthorn.local.>
+      Date: Mon Jul 21 18:33:24 2003
+      Warning: the original change was in the 'awaiting_integration' state
+
+      none
+
+  rt.3.0.D160, C209, leira, Tue Jul 22 01:54:49 2003, #1751: update second page
+  in Bulk update
+      From: Linda Julien <leira@hawthorn.local.>
+      Date: Mon Jul 21 20:54:02 2003
+      Warning: the original change was in the 'awaiting_integration' state
+
+      none
+
+  rt.3.0.D159, C24, jesse, Fri Jul 18 18:59:13 2003, Certain ACL checks could
+  fail on postgres due to a marshalling bug
+      From: Jesse Vincent <jesse@localhost>
+      Date: Fri Jul 18 17:06:39 2003
+
+      none
+
+  rt.3.0.D158, C206, jesse, Fri Jul 18 18:58:22 2003, Extended ACL edit routines
+  to make it easier to use generic routines in 3rd party apps
+      From: Jesse Vincent <jesse@localhost>
+      Date: Fri Jul 18 17:04:15 2003
+
+      none
+
+  rt.3.0.D157, C205, jesse, Mon Jul 14 02:51:28 2003, Removing ancient cli code
+  that was accidentally added to the repository
+      From: Jesse Vincent <jesse@localhost>
+      Date: Sun Jul 13 23:47:51 2003
+
+      none
+
+  rt.3.0.D156, C203, jesse, Sun Jul 13 22:35:44 2003, More updates to the
+  commandline client
+      From: Jesse Vincent <jesse@localhost>
+      Date: Sun Jul 13 19:32:10 2003
+
+      none
+
+  rt.3.0.D155, C202, jesse, Sun Jul 13 21:33:58 2003, Initial commit of new
+  commandline client support code
+      From: Jesse Vincent <jesse@localhost>
+      Date: Sun Jul 13 18:33:29 2003
+
+      none
+
+  rt.3.0.D154, C196, jesse, Sun Jul 13 18:06:09 2003, #3029 - better warning
+  message on improper perms on mail in
+      From: Jesse Vincent <jesse@localhost>
+      Date: Tue Jul  8 16:59:59 2003
+
+      none
+
+  rt.3.0.D153, C195, jesse, Sun Jul 13 18:05:41 2003, #3042: Make max inline
+  body size configurable
+      From: Jesse Vincent <jesse@localhost>
+      Date: Tue Jul  8 16:55:28 2003
+
+      none
 
   rt.3.0.D152, C201, jesse, Sat Jul 12 02:41:34 2003, Bumping version to 3.0.4
       From: Jesse Vincent <jesse@localhost>
index 6447221..0895874 100644 (file)
@@ -1,20 +1,19 @@
 # BEGIN LICENSE BLOCK
 # 
-# Copyright (c) 1996-2002 Jesse Vincent <jesse@bestpractical.com>
+# Copyright (c) 1996-2003 Jesse Vincent <jesse@bestpractical.com>
 # 
 # (Except where explictly superceded by other copyright notices)
 # 
 # This work is made available to you under the terms of Version 2 of
 # the GNU General Public License. A copy of that license should have
 # been provided with this software, but in any event can be snarfed
-# from www.gnu.org
+# from www.gnu.org.
 # 
 # This work is distributed in the hope that it will be useful, but
 # WITHOUT ANY WARRANTY; without even the implied warranty of
 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 # General Public License for more details.
 # 
-# 
 # Unless otherwise specified, all modifications, corrections or
 # extensions to this work which alter its source code become the
 # property of Best Practical Solutions, LLC when submitted for
@@ -22,8 +21,6 @@
 # 
 # 
 # END LICENSE BLOCK
-
-
 #
 # DO NOT HAND-EDIT the file named 'Makefile'. This file is autogenerated.
 # Have a look at "configure" and "Makefile.in" instead
@@ -39,7 +36,7 @@ SITE_CONFIG_FILE              =       $(CONFIG_FILE_PATH)/RT_SiteConfig.pm
 
 RT_VERSION_MAJOR       =       3
 RT_VERSION_MINOR       =       0
-RT_VERSION_PATCH       =       4
+RT_VERSION_PATCH       =       9
 
 RT_VERSION =   $(RT_VERSION_MAJOR).$(RT_VERSION_MINOR).$(RT_VERSION_PATCH)
 TAG       =    rt-$(RT_VERSION_MAJOR)-$(RT_VERSION_MINOR)-$(RT_VERSION_PATCH)
@@ -101,8 +98,8 @@ RT_MODPERL_HANDLER   =       $(RT_BIN_PATH)/webmux.pl
 RT_FASTCGI_HANDLER     =       $(RT_BIN_PATH)/mason_handler.fcgi
 # RT_WIN32_FASTCGI_HANDLER is the mason handler script for FastCGI
 RT_WIN32_FASTCGI_HANDLER       =       $(RT_BIN_PATH)/mason_handler.svc
-# RT's admin CLI
-RT_CLI_ADMIN_BIN       =       $(RT_BIN_PATH)/rtadmin
+# RT's CLI
+RT_CLI_BIN             =       $(RT_BIN_PATH)/rt
 # RT's mail gateway
 RT_MAILGATE_BIN                =       $(RT_BIN_PATH)/rt-mailgate
 # RT's cron tool
@@ -115,6 +112,7 @@ SETGID_BINARIES             =       $(DESTDIR)/$(RT_FASTCGI_HANDLER) \
 
 BINARIES               =       $(DESTDIR)/$(RT_MODPERL_HANDLER) \
                                $(DESTDIR)/$(RT_MAILGATE_BIN) \
+                               $(DESTDIR)/$(RT_CLI_BIN) \
                                $(DESTDIR)/$(RT_CRON_BIN) \
                                $(SETGID_BINARIES)
 SYSTEM_BINARIES                =       $(DESTDIR)/$(RT_SBIN_PATH)/
@@ -128,6 +126,7 @@ SYSTEM_BINARIES             =       $(DESTDIR)/$(RT_SBIN_PATH)/
 # DB_TYPE defines what sort of database RT trys to talk to
 # "mysql" is known to work.
 # "Pg" is known to work
+# "Informix" is known to work
 
 DB_TYPE                        =       mysql
 
@@ -138,7 +137,8 @@ DB_TYPE                     =       mysql
 
 # For mysql, you probably want 'root'
 # For Pg, you probably want 'postgres' 
-# For oracle, you want 'system'
+# For Oracle, you want 'system'
+# For Informix, you want 'informix'
 
 DB_DBA                 =       root
 
@@ -211,7 +211,7 @@ upgrade-instruct:
        @echo "    $(RT_SBIN_PATH)/rt-setup-database --action insert --datafile etc/upgrade/<version>"
 
 
-upgrade: dirs upgrade-noclobber upgrade-instruct
+upgrade: config-install dirs files-install fixperms upgrade-instruct
 
 upgrade-noclobber: config-install libs-install html-install bin-install local-install doc-install fixperms
 
@@ -312,13 +312,16 @@ config-install:
 test: 
        $(PERL) -Ilib lib/t/00smoke.t
 
-regression-nosetgid-quiet: config-install dirs files-install libs-install sbin-install bin-install regression-instruct regression-reset-db  testify-pods fixperms-nosetgid apachectl
+regression-install: config-install
+       $(PERL) -pi -e 's/Set\(\$$DatabaseName.*\);/Set\(\$$DatabaseName, "rt3regression"\);/' $(DESTDIR)/$(CONFIG_FILE)
+
+regression-nosetgid-quiet: regression-install dirs files-install libs-install sbin-install bin-install regression-instruct regression-reset-db  testify-pods fixperms-nosetgid apachectl
        $(PERL) sbin/regression_harness
 
-regression-nosetgid: config-install dirs files-install libs-install sbin-install bin-install regression-instruct regression-reset-db  testify-pods fixperms-nosetgid apachectl
+regression-nosetgid: regression-install dirs files-install libs-install sbin-install bin-install regression-instruct regression-reset-db  testify-pods fixperms-nosetgid apachectl
        $(PERL) lib/t/02regression.t
 
-regression: config-install dirs files-install libs-install sbin-install bin-install regression-instruct regression-reset-db  testify-pods apachectl
+regression: regression-install dirs files-install libs-install sbin-install bin-install regression-instruct regression-reset-db  testify-pods fixperms apachectl
        $(PERL) lib/t/02regression.t
 
 regression-quiet:
@@ -397,7 +400,9 @@ bin-install:
        -cp -rp \
                bin/rt-mailgate \
                bin/mason_handler.fcgi \
+               bin/mason_handler.scgi \
                bin/mason_handler.svc \
+               bin/rt \
                bin/webmux.pl \
                bin/rt-crontool \
                $(DESTDIR)/$(RT_BIN_PATH)
index 245ec5e..c3eabc6 100644 (file)
@@ -1,20 +1,19 @@
 # BEGIN LICENSE BLOCK
 # 
-# Copyright (c) 1996-2002 Jesse Vincent <jesse@bestpractical.com>
+# Copyright (c) 1996-2003 Jesse Vincent <jesse@bestpractical.com>
 # 
 # (Except where explictly superceded by other copyright notices)
 # 
 # This work is made available to you under the terms of Version 2 of
 # the GNU General Public License. A copy of that license should have
 # been provided with this software, but in any event can be snarfed
-# from www.gnu.org
+# from www.gnu.org.
 # 
 # This work is distributed in the hope that it will be useful, but
 # WITHOUT ANY WARRANTY; without even the implied warranty of
 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 # General Public License for more details.
 # 
-# 
 # Unless otherwise specified, all modifications, corrections or
 # extensions to this work which alter its source code become the
 # property of Best Practical Solutions, LLC when submitted for
@@ -22,8 +21,6 @@
 # 
 # 
 # END LICENSE BLOCK
-
-
 #
 # DO NOT HAND-EDIT the file named 'Makefile'. This file is autogenerated.
 # Have a look at "configure" and "Makefile.in" instead
@@ -101,8 +98,8 @@ RT_MODPERL_HANDLER   =       $(RT_BIN_PATH)/webmux.pl
 RT_FASTCGI_HANDLER     =       $(RT_BIN_PATH)/mason_handler.fcgi
 # RT_WIN32_FASTCGI_HANDLER is the mason handler script for FastCGI
 RT_WIN32_FASTCGI_HANDLER       =       $(RT_BIN_PATH)/mason_handler.svc
-# RT's admin CLI
-RT_CLI_ADMIN_BIN       =       $(RT_BIN_PATH)/rtadmin
+# RT's CLI
+RT_CLI_BIN             =       $(RT_BIN_PATH)/rt
 # RT's mail gateway
 RT_MAILGATE_BIN                =       $(RT_BIN_PATH)/rt-mailgate
 # RT's cron tool
@@ -115,6 +112,7 @@ SETGID_BINARIES             =       $(DESTDIR)/$(RT_FASTCGI_HANDLER) \
 
 BINARIES               =       $(DESTDIR)/$(RT_MODPERL_HANDLER) \
                                $(DESTDIR)/$(RT_MAILGATE_BIN) \
+                               $(DESTDIR)/$(RT_CLI_BIN) \
                                $(DESTDIR)/$(RT_CRON_BIN) \
                                $(SETGID_BINARIES)
 SYSTEM_BINARIES                =       $(DESTDIR)/$(RT_SBIN_PATH)/
@@ -128,6 +126,7 @@ SYSTEM_BINARIES             =       $(DESTDIR)/$(RT_SBIN_PATH)/
 # DB_TYPE defines what sort of database RT trys to talk to
 # "mysql" is known to work.
 # "Pg" is known to work
+# "Informix" is known to work
 
 DB_TYPE                        =       @DB_TYPE@
 
@@ -138,7 +137,8 @@ DB_TYPE                     =       @DB_TYPE@
 
 # For mysql, you probably want 'root'
 # For Pg, you probably want 'postgres' 
-# For oracle, you want 'system'
+# For Oracle, you want 'system'
+# For Informix, you want 'informix'
 
 DB_DBA                 =       @DB_DBA@
 
@@ -211,7 +211,7 @@ upgrade-instruct:
        @echo "    $(RT_SBIN_PATH)/rt-setup-database --action insert --datafile etc/upgrade/<version>"
 
 
-upgrade: dirs upgrade-noclobber upgrade-instruct
+upgrade: config-install dirs files-install fixperms upgrade-instruct
 
 upgrade-noclobber: config-install libs-install html-install bin-install local-install doc-install fixperms
 
@@ -312,13 +312,16 @@ config-install:
 test: 
        $(PERL) -Ilib lib/t/00smoke.t
 
-regression-nosetgid-quiet: config-install dirs files-install libs-install sbin-install bin-install regression-instruct regression-reset-db  testify-pods fixperms-nosetgid apachectl
+regression-install: config-install
+       $(PERL) -pi -e 's/Set\(\$$DatabaseName.*\);/Set\(\$$DatabaseName, "rt3regression"\);/' $(DESTDIR)/$(CONFIG_FILE)
+
+regression-nosetgid-quiet: regression-install dirs files-install libs-install sbin-install bin-install regression-instruct regression-reset-db  testify-pods fixperms-nosetgid apachectl
        $(PERL) sbin/regression_harness
 
-regression-nosetgid: config-install dirs files-install libs-install sbin-install bin-install regression-instruct regression-reset-db  testify-pods fixperms-nosetgid apachectl
+regression-nosetgid: regression-install dirs files-install libs-install sbin-install bin-install regression-instruct regression-reset-db  testify-pods fixperms-nosetgid apachectl
        $(PERL) lib/t/02regression.t
 
-regression: config-install dirs files-install libs-install sbin-install bin-install regression-instruct regression-reset-db  testify-pods apachectl
+regression: regression-install dirs files-install libs-install sbin-install bin-install regression-instruct regression-reset-db  testify-pods fixperms apachectl
        $(PERL) lib/t/02regression.t
 
 regression-quiet:
@@ -397,7 +400,9 @@ bin-install:
        -cp -rp \
                bin/rt-mailgate \
                bin/mason_handler.fcgi \
+               bin/mason_handler.scgi \
                bin/mason_handler.svc \
+               bin/rt \
                bin/webmux.pl \
                bin/rt-crontool \
                $(DESTDIR)/$(RT_BIN_PATH)
index 7c5e4d4..7188f09 100755 (executable)
--- a/rt/README
+++ b/rt/README
@@ -21,6 +21,7 @@
 # 
 # 
 # END LICENSE BLOCK
+
 RT is an enterprise-grade issue tracking system. It allows
 organizations to keep track of their to-do lists, who is working
 on which tasks, what's already been done, and when tasks were
@@ -36,22 +37,22 @@ up and use.
 REQUIRED PACKAGES:
 ------------------
 
-o   Perl 5.8.0 or later (http://www.perl.com).
+o   Perl 5.8.3 or later (http://www.perl.com).
 
        (If you intend to use the FastCGI or SpeedyCGI support, you 
         need to make sure that perl has been built with support for 
         setgid perl scripts.)`
 
+    Perl versions prior to 5.8.3 contain bugs that could result in data
+    corruption. We recommend strongly that you use 5.8.3 or newer.
+
     Perl 5.6.1 is currently deprecated and will be officially desupported
     in a future release
 
 o   A DB backend; MySQL is recommended ( http://www.mysql.com ) 
         Currently supported:  Mysql 4.0.13 or later. 
                               Postgres 7.2 or later.
-
-                              Mysql 3.23.46 or newer with support for InnoDB 
-                             is currently deprecated and will be officially
-                             desupported in a future release.
+                              Oracle 9iR2.
 
 o   Apache version 1.3.x or 2.x (http://httpd.apache.org) 
     with mod_perl -- (http://perl.apache.org ) 
@@ -119,7 +120,7 @@ http://www.bestpractical.com/rt
    perl sbin/rt-test-dependencies \ 
                 --with-<databasename> --with-<web-environment>
 
-        databasename is one of: mysql, postgres
+        databasename is one of: mysql, postgres, oracle
         web-environment is one of: fastcgi, modperl1, modperl2
 
 3.2   If there are unsatisfied dependencies, install them by hand or run:
@@ -151,6 +152,10 @@ http://www.bestpractical.com/rt
 
 5b  FOR UPGRADING: (Within the RT 3.0.x series)
 
+
+        Read through the UPGRADING document included in this distribution.
+        It may contain important instructions for updating your database
+
         As root, type: 
                 make upgrade     (replace "make" with the local name for 
                                   Make, if you need to)
@@ -160,6 +165,14 @@ http://www.bestpractical.com/rt
         
         It may then instruct you to update your RT system database objects 
 
+5c  FOR UPGRADING: (From RT 2.0.x)
+
+    Download the RT2 to RT3 migration tools from:
+
+    http://bestpractical.com/pub/rt/devel/rt2-to-rt3.tar.gz
+
+    Follow the included instructions.
+
 6   Edit etc/RT_SiteConfig.pm in your RT installation directory, by specifying
     any values you need to change from the defaults in etc/RT_Config.pm
 
@@ -192,31 +205,20 @@ Apache
     DocumentRoot /opt/rt3/share/html
     AddDefaultCharset UTF-8
 
-    # this line applies to Apache2+mod_perl2 only
+    # these four lines apply to Apache2+mod_perl2 only: {{{
+    PerlSetVar MasonArgsMethod CGI
     PerlModule Apache2 Apache::compat
+    RewriteEngine On
+    RewriteRule ^(.*)/$ $1/index.html
+    # }}}
 
     PerlModule Apache::DBI
     PerlRequire /opt/rt3/bin/webmux.pl
 
-    # this section applies to Apache 1 only
     <Location />
         SetHandler perl-script
         PerlHandler RT::Mason
     </Location>
-
-    # this section applies to Apache2+mod_perl2 only
-    <FilesMatch "\.html$">
-        SetHandler perl-script
-        PerlHandler RT::Mason
-    </FilesMatch>
-    <LocationMatch "/Attachment/">
-        SetHandler perl-script
-        PerlHandler RT::Mason
-    </LocationMatch>
-    <LocationMatch "/REST/">
-        SetHandler perl-script
-        PerlHandler RT::Mason
-    </LocationMatch>
 </VirtualHost>
 
 
diff --git a/rt/README.Oracle b/rt/README.Oracle
new file mode 100644 (file)
index 0000000..41bec82
--- /dev/null
@@ -0,0 +1,37 @@
+In order to install RT with Oracle, the database must first be
+prepared.  Ports of RT to other databases will automatically create
+the RT schema.  This is not done for Oracle because most sites wishing
+to deploy RT on Oracle will have choose to make specific configuration
+of the RT user, for example to select the appropriate tablespace or to
+set up a resource profile.  The RT user must have appropriate
+privileges similar to the resource and connect roles and must have the
+"query rewrite" system privilege.
+  Here is an example of commands to create an RT user called "RT" with
+a password of "rt".
+
+  create user rt identified by rt default tablespace users temporary
+    tablespace temp;
+  grant resource, connect, query rewrite to rt;
+
+
+RT should not run its schema creation as the Oracle DBA; instead the
+schema creation should be run as the RT user.  To accomplish this set
+the --with-rt-dba configuration parameter to the RT user, not to the
+Oracle DBA.  As an example, the following might be appropriate to
+configure RT for the example.com Oracle database.
+
+  ./configure --prefix /usr/local/rt --with-db-type=Oracle \
+        --with-db-dba=rt --with-db-database=example.com \
+    --with-db-rt-user=rt \
+        --with-db-rt-pass=rt 
+
+
+As with all databases it is important to analyze the Schema and get
+current statistics after any significant dataset change.  Oracle's
+cost-based optimizer can provide particularly bad performance when the
+schema statistics are significantly inaccurate.  To analyze the schema
+of a user called rt, execute the following from withing Sqlplus.
+
+  execute dbms_utility.analyze_schema( 'RT', 'estimate');
+
+
diff --git a/rt/UPGRADING b/rt/UPGRADING
new file mode 100644 (file)
index 0000000..4306eb6
--- /dev/null
@@ -0,0 +1,64 @@
+UPGRADING
+
+
+*******
+WARNING
+*******
+
+
+Before making any changes to your database, always ensure that you have a 
+complete current backup. If you don't have a current backup, you could 
+accidentally damage your database and lose data or worse.
+
+
+
+Look for the 
+
+
+----------------------------------------------------------------------
+
+3.0.7
+=====
+
+All Databases
+-------------
+
+If you are upgrading from versions between 3.0.0 and 3.0.7, inclusive,
+you might find improved performance by adding the following index to
+your database:
+
+CREATE INDEX Links4 ON Links(Type,LocalBase);
+
+
+
+3.0.6
+=====
+
+
+All Databases
+-------------
+
+If you are upgrading from versions between 3.0.0 and 3.0.6, inclusive,
+you might find improved performance by adding the following indices to
+your database:
+
+   CREATE INDEX TicketCustomFieldValues1 ON TicketCustomFieldValues (CustomField,Ticket,Content); 
+   CREATE INDEX TicketCustomFieldValues2 ON TicketCustomFieldValues (CustomField,Ticket); 
+
+   CREATE INDEX CustomFieldValues1 ON CustomFieldValues (CustomField);
+
+
+
+Postgres
+--------
+
+If you have a Postgres database, the following changes to your
+database can improve performance:
+
+  ALTER TABLE groups rename instance to instance1;
+  ALTER TABLE groups add instance int;
+  UPDATE groups SET instance = instance1::text::int where btrim(instance1) <> '';
+  ALTER TABLE groups drop column instance1;
+
+
+
index 51a8aaf..3d27db9 100644 (file)
@@ -1,7 +1,7 @@
 @%:@! /bin/sh
 @%:@ From configure.ac Revision: 1.1 .
 @%:@ Guess values for system-dependent variables and create Makefiles.
-@%:@ Generated by GNU Autoconf 2.53 for RT 3.0.4.
+@%:@ Generated by GNU Autoconf 2.53 for RT 3.0.9.
 @%:@
 @%:@ Report bugs to <rt-3.0-bugs@fsck.com>.
 @%:@ 
@@ -257,8 +257,8 @@ SHELL=${CONFIG_SHELL-/bin/sh}
 # Identity of this package.
 PACKAGE_NAME='RT'
 PACKAGE_TARNAME='rt'
-PACKAGE_VERSION='3.0.4'
-PACKAGE_STRING='RT 3.0.4'
+PACKAGE_VERSION='3.0.9'
+PACKAGE_STRING='RT 3.0.9'
 PACKAGE_BUGREPORT='rt-3.0-bugs@fsck.com'
 
 ac_unique_file="lib/RT.pm.in"
@@ -711,7 +711,7 @@ if test "$ac_init_help" = "long"; then
   # Omit some internal or obsolete options to make the list less imposing.
   # This message is too long to be a string in the A/UX 3.1 sh.
   cat <<_ACEOF
-\`configure' configures RT 3.0.4 to adapt to many kinds of systems.
+\`configure' configures RT 3.0.9 to adapt to many kinds of systems.
 
 Usage: $0 [OPTION]... [VAR=VALUE]...
 
@@ -768,7 +768,7 @@ fi
 
 if test -n "$ac_init_help"; then
   case $ac_init_help in
-     short | recursive ) echo "Configuration of RT 3.0.4:";;
+     short | recursive ) echo "Configuration of RT 3.0.9:";;
    esac
   cat <<\_ACEOF
 
@@ -786,8 +786,8 @@ Optional Packages:
   --with-bin-owner=OWNER  user that will own rt binaries (default root)
   --with-libs-owner=OWNER user that will own RT libraries (default root)
   --with-libs-group=GROUP group that will own rt binaries (default bin)
-  --with-db-type=TYPE     sort of database RT will use (default: mysql) (mysql
-                          and Pg are valid)
+  --with-db-type=TYPE     sort of database RT will use (default: mysql)
+                          (mysql, Pg, Oracle and Informix are valid)
   --with-db-host=HOSTNAME FQDN of database server (default: localhost)
   --with-db-port=PORT     port on which the database listens on
   --with-db-rt-host=HOSTNAME 
@@ -802,6 +802,7 @@ Optional Packages:
                           password for database user (default: rt_pass)
   --with-web-user=USER    user the web server runs as (default: www)
   --with-web-group=GROUP  group the web server runs as (default: www)
+  --with-my-user-group    set all users and groups to current user/group
 
 Some influential environment variables:
   PERL        Perl interpreter command
@@ -872,7 +873,7 @@ fi
 test -n "$ac_init_help" && exit 0
 if $ac_init_version; then
   cat <<\_ACEOF
-RT configure 3.0.4
+RT configure 3.0.9
 generated by GNU Autoconf 2.53
 
 Copyright 1992, 1993, 1994, 1995, 1996, 1998, 1999, 2000, 2001, 2002
@@ -887,7 +888,7 @@ cat >&5 <<_ACEOF
 This file contains any messages produced by compilers while
 running configure, to aid debugging if configure makes a mistake.
 
-It was created by RT $as_me 3.0.4, which was
+It was created by RT $as_me 3.0.9, which was
 generated by GNU Autoconf 2.53.  Invocation command line was
 
   $ $0 $@
@@ -1171,7 +1172,7 @@ rt_version_major=3
 
 rt_version_minor=0
 
-rt_version_patch=4
+rt_version_patch=9
 
 test "x$rt_version_major" = 'x' && rt_version_major=0
 test "x$rt_version_minor" = 'x' && rt_version_minor=0
@@ -1703,13 +1704,21 @@ if test "${with_db_type+set}" = set; then
 else
   DB_TYPE=mysql
 fi; 
-if test "$DB_TYPE" != 'mysql' -a "$DB_TYPE" != 'Pg' -a "$DB_TYPE" != 'SQLite'; then
-       { { echo "$as_me:$LINENO: error: Only Pg and mysql are valid db types" >&5
-echo "$as_me: error: Only Pg and mysql are valid db types" >&2;}
+if test "$DB_TYPE" != 'mysql' -a "$DB_TYPE" != 'Pg' -a "$DB_TYPE" != 'SQLite' -a "$DB_TYPE" != 'Oracle' -a "$DB_TYPE" != 'Informix' ; then
+       { { echo "$as_me:$LINENO: error: Only Oracle, Informix, Pg and mysql are valid db types" >&5
+echo "$as_me: error: Only Oracle, Informix, Pg and mysql are valid db types" >&2;}
    { (exit 1); exit 1; }; }
 fi
 
 
+if test "$DB_TYPE" = 'Oracle'; then
+       test "x$ORACLE_HOME" = 'x' && { { echo "$as_me:$LINENO: error: Please declare the ORACLE_HOME environment variable" >&5
+echo "$as_me: error: Please declare the ORACLE_HOME environment variable" >&2;}
+   { (exit 1); exit 1; }; }
+       ORACLE_ENV_PREF="\$ENV{'ORACLE_HOME'} = '$ORACLE_HOME';"
+fi
+
+
 
 # Check whether --with-db-host or --without-db-host was given.
 if test "${with_db_host+set}" = set; then
@@ -1800,6 +1809,19 @@ else
 fi; 
 
 
+my_group=$(groups|cut -f1 -d' ')
+
+# Check whether --with-my-user-group or --without-my-user-group was given.
+if test "${with_my_user_group+set}" = set; then
+  withval="$with_my_user_group"
+  RTGROUP=$my_group
+            BIN_OWNER=$USER
+            LIBS_OWNER=$USER
+            LIBS_GROUP=$my_group
+            WEB_USER=$USER
+            WEB_GROUP=$my_group
+fi; 
+
 
 RT_VERSION_MAJOR=${rt_version_major}
 
@@ -1848,7 +1870,7 @@ RT_LOG_PATH=${exp_logfiledir}
 
 
 
-ac_config_files="$ac_config_files sbin/rt-setup-database sbin/rt-test-dependencies Makefile etc/RT_Config.pm lib/RT.pm lib/t/00smoke.t lib/t/01harness.t lib/t/02regression.t lib/t/03web.pl lib/t/04_send_email.pl bin/mason_handler.fcgi bin/mason_handler.scgi bin/mason_handler.svc bin/rt-commit-handler bin/rt-crontool bin/rt-mailgate bin/webmux.pl"
+ac_config_files="$ac_config_files sbin/rt-setup-database sbin/rt-test-dependencies Makefile etc/RT_Config.pm lib/RT.pm lib/t/00smoke.t lib/t/01harness.t lib/t/02regression.t lib/t/03web.pl lib/t/04_send_email.pl bin/mason_handler.fcgi bin/mason_handler.scgi bin/mason_handler.svc bin/rt-commit-handler bin/rt-crontool bin/rt-mailgate bin/rt bin/webmux.pl"
 
 cat >confcache <<\_ACEOF
 # This file is a shell script that caches the results of configure
@@ -2206,7 +2228,7 @@ _ASBOX
 } >&5
 cat >&5 <<_CSEOF
 
-This file was extended by RT $as_me 3.0.4, which was
+This file was extended by RT $as_me 3.0.9, which was
 generated by GNU Autoconf 2.53.  Invocation command line was
 
   CONFIG_FILES    = $CONFIG_FILES
@@ -2260,7 +2282,7 @@ _ACEOF
 
 cat >>$CONFIG_STATUS <<_ACEOF
 ac_cs_version="\\
-RT config.status 3.0.4
+RT config.status 3.0.9
 configured by $0, generated by GNU Autoconf 2.53,
   with options \\"`echo "$ac_configure_args" | sed 's/[\\""\`\$]/\\\\&/g'`\\"
 
@@ -2363,6 +2385,7 @@ do
   "bin/rt-commit-handler" ) CONFIG_FILES="$CONFIG_FILES bin/rt-commit-handler" ;;
   "bin/rt-crontool" ) CONFIG_FILES="$CONFIG_FILES bin/rt-crontool" ;;
   "bin/rt-mailgate" ) CONFIG_FILES="$CONFIG_FILES bin/rt-mailgate" ;;
+  "bin/rt" ) CONFIG_FILES="$CONFIG_FILES bin/rt" ;;
   "bin/webmux.pl" ) CONFIG_FILES="$CONFIG_FILES bin/webmux.pl" ;;
   *) { { echo "$as_me:$LINENO: error: invalid argument: $ac_config_target" >&5
 echo "$as_me: error: invalid argument: $ac_config_target" >&2;}
@@ -2487,6 +2510,7 @@ s,@BIN_OWNER@,$BIN_OWNER,;t t
 s,@LIBS_OWNER@,$LIBS_OWNER,;t t
 s,@LIBS_GROUP@,$LIBS_GROUP,;t t
 s,@DB_TYPE@,$DB_TYPE,;t t
+s,@ORACLE_ENV_PREF@,$ORACLE_ENV_PREF,;t t
 s,@DB_HOST@,$DB_HOST,;t t
 s,@DB_PORT@,$DB_PORT,;t t
 s,@DB_RT_HOST@,$DB_RT_HOST,;t t
index 962f400..f132762 100644 (file)
@@ -52,7 +52,7 @@ m4trace:configure.ac:9: -1- AC_SUBST([ECHO_T])
 m4trace:configure.ac:9: -1- AC_SUBST([LIBS])
 m4trace:configure.ac:14: -1- AC_SUBST([rt_version_major], [3])
 m4trace:configure.ac:16: -1- AC_SUBST([rt_version_minor], [0])
-m4trace:configure.ac:18: -1- AC_SUBST([rt_version_patch], [4])
+m4trace:configure.ac:18: -1- AC_SUBST([rt_version_patch], [9])
 m4trace:configure.ac:24: -1- AC_PROG_INSTALL
 m4trace:configure.ac:24: -1- AC_SUBST([INSTALL_PROGRAM])
 m4trace:configure.ac:24: -1- AC_SUBST([INSTALL_SCRIPT])
@@ -104,38 +104,39 @@ m4trace:configure.ac:57: -1- AC_SUBST([BIN_OWNER])
 m4trace:configure.ac:65: -1- AC_SUBST([LIBS_OWNER])
 m4trace:configure.ac:73: -1- AC_SUBST([LIBS_GROUP])
 m4trace:configure.ac:84: -1- AC_SUBST([DB_TYPE])
-m4trace:configure.ac:92: -1- AC_SUBST([DB_HOST])
-m4trace:configure.ac:100: -1- AC_SUBST([DB_PORT])
-m4trace:configure.ac:108: -1- AC_SUBST([DB_RT_HOST])
-m4trace:configure.ac:116: -1- AC_SUBST([DB_DBA])
-m4trace:configure.ac:124: -1- AC_SUBST([DB_DATABASE])
-m4trace:configure.ac:132: -1- AC_SUBST([DB_RT_USER])
-m4trace:configure.ac:140: -1- AC_SUBST([DB_RT_PASS])
-m4trace:configure.ac:148: -1- AC_SUBST([WEB_USER])
-m4trace:configure.ac:156: -1- AC_SUBST([WEB_GROUP])
-m4trace:configure.ac:163: -1- AC_SUBST([RT_VERSION_MAJOR], [${rt_version_major}])
-m4trace:configure.ac:164: -1- AC_SUBST([RT_VERSION_MINOR], [${rt_version_minor}])
-m4trace:configure.ac:165: -1- AC_SUBST([RT_VERSION_PATCH], [${rt_version_patch}])
-m4trace:configure.ac:168: -1- AC_SUBST([RT_PATH], [${exp_prefix}])
-m4trace:configure.ac:169: -1- AC_SUBST([RT_DOC_PATH], [${exp_manualdir}])
-m4trace:configure.ac:170: -1- AC_SUBST([RT_LOCAL_PATH], [${exp_customdir}])
-m4trace:configure.ac:171: -1- AC_SUBST([RT_LIB_PATH], [${exp_libdir}])
-m4trace:configure.ac:172: -1- AC_SUBST([RT_ETC_PATH], [${exp_sysconfdir}])
-m4trace:configure.ac:173: -1- AC_SUBST([CONFIG_FILE_PATH], [${exp_sysconfdir}])
-m4trace:configure.ac:174: -1- AC_SUBST([RT_BIN_PATH], [${exp_bindir}])
-m4trace:configure.ac:175: -1- AC_SUBST([RT_SBIN_PATH], [${exp_sbindir}])
-m4trace:configure.ac:176: -1- AC_SUBST([RT_VAR_PATH], [${exp_localstatedir}])
-m4trace:configure.ac:177: -1- AC_SUBST([RT_MAN_PATH], [${exp_mandir}])
-m4trace:configure.ac:178: -1- AC_SUBST([MASON_DATA_PATH], [${exp_masonstatedir}])
-m4trace:configure.ac:179: -1- AC_SUBST([MASON_SESSION_PATH], [${exp_sessionstatedir}])
-m4trace:configure.ac:180: -1- AC_SUBST([MASON_HTML_PATH], [${exp_htmldir}])
-m4trace:configure.ac:181: -1- AC_SUBST([LOCAL_ETC_PATH], [${exp_custometcdir}])
-m4trace:configure.ac:182: -1- AC_SUBST([MASON_LOCAL_HTML_PATH], [${exp_customhtmldir}])
-m4trace:configure.ac:183: -1- AC_SUBST([LOCAL_LEXICON_PATH], [${exp_customlexdir}])
-m4trace:configure.ac:184: -1- AC_SUBST([LOCAL_LIB_PATH], [${exp_customlibdir}])
-m4trace:configure.ac:185: -1- AC_SUBST([DESTDIR], [${exp_prefix}])
-m4trace:configure.ac:186: -1- AC_SUBST([RT_LOG_PATH], [${exp_logfiledir}])
-m4trace:configure.ac:208: -1- AC_CONFIG_FILES([
+m4trace:configure.ac:91: -1- AC_SUBST([ORACLE_ENV_PREF])
+m4trace:configure.ac:99: -1- AC_SUBST([DB_HOST])
+m4trace:configure.ac:107: -1- AC_SUBST([DB_PORT])
+m4trace:configure.ac:115: -1- AC_SUBST([DB_RT_HOST])
+m4trace:configure.ac:123: -1- AC_SUBST([DB_DBA])
+m4trace:configure.ac:131: -1- AC_SUBST([DB_DATABASE])
+m4trace:configure.ac:139: -1- AC_SUBST([DB_RT_USER])
+m4trace:configure.ac:147: -1- AC_SUBST([DB_RT_PASS])
+m4trace:configure.ac:155: -1- AC_SUBST([WEB_USER])
+m4trace:configure.ac:163: -1- AC_SUBST([WEB_GROUP])
+m4trace:configure.ac:182: -1- AC_SUBST([RT_VERSION_MAJOR], [${rt_version_major}])
+m4trace:configure.ac:183: -1- AC_SUBST([RT_VERSION_MINOR], [${rt_version_minor}])
+m4trace:configure.ac:184: -1- AC_SUBST([RT_VERSION_PATCH], [${rt_version_patch}])
+m4trace:configure.ac:187: -1- AC_SUBST([RT_PATH], [${exp_prefix}])
+m4trace:configure.ac:188: -1- AC_SUBST([RT_DOC_PATH], [${exp_manualdir}])
+m4trace:configure.ac:189: -1- AC_SUBST([RT_LOCAL_PATH], [${exp_customdir}])
+m4trace:configure.ac:190: -1- AC_SUBST([RT_LIB_PATH], [${exp_libdir}])
+m4trace:configure.ac:191: -1- AC_SUBST([RT_ETC_PATH], [${exp_sysconfdir}])
+m4trace:configure.ac:192: -1- AC_SUBST([CONFIG_FILE_PATH], [${exp_sysconfdir}])
+m4trace:configure.ac:193: -1- AC_SUBST([RT_BIN_PATH], [${exp_bindir}])
+m4trace:configure.ac:194: -1- AC_SUBST([RT_SBIN_PATH], [${exp_sbindir}])
+m4trace:configure.ac:195: -1- AC_SUBST([RT_VAR_PATH], [${exp_localstatedir}])
+m4trace:configure.ac:196: -1- AC_SUBST([RT_MAN_PATH], [${exp_mandir}])
+m4trace:configure.ac:197: -1- AC_SUBST([MASON_DATA_PATH], [${exp_masonstatedir}])
+m4trace:configure.ac:198: -1- AC_SUBST([MASON_SESSION_PATH], [${exp_sessionstatedir}])
+m4trace:configure.ac:199: -1- AC_SUBST([MASON_HTML_PATH], [${exp_htmldir}])
+m4trace:configure.ac:200: -1- AC_SUBST([LOCAL_ETC_PATH], [${exp_custometcdir}])
+m4trace:configure.ac:201: -1- AC_SUBST([MASON_LOCAL_HTML_PATH], [${exp_customhtmldir}])
+m4trace:configure.ac:202: -1- AC_SUBST([LOCAL_LEXICON_PATH], [${exp_customlexdir}])
+m4trace:configure.ac:203: -1- AC_SUBST([LOCAL_LIB_PATH], [${exp_customlibdir}])
+m4trace:configure.ac:204: -1- AC_SUBST([DESTDIR], [${exp_prefix}])
+m4trace:configure.ac:205: -1- AC_SUBST([RT_LOG_PATH], [${exp_logfiledir}])
+m4trace:configure.ac:228: -1- AC_CONFIG_FILES([
                 sbin/rt-setup-database
                  sbin/rt-test-dependencies
                  Makefile
@@ -152,5 +153,6 @@ m4trace:configure.ac:208: -1- AC_CONFIG_FILES([
                 bin/rt-commit-handler
                 bin/rt-crontool
                 bin/rt-mailgate
+                bin/rt
                 bin/webmux.pl
                 ])
index 431eccb..93d1f88 100755 (executable)
@@ -27,7 +27,7 @@ use strict;
 use File::Basename;
 require ('/opt/rt3/bin/webmux.pl');
 
-my $h = &RT::Interface::Web::NewCGIHandler();
+my $h = &RT::Interface::Web::NewCGIHandler(@RT::MasonParameters);
 
 # Enter CGI::Fast mode, which should also work as a vanilla CGI script.
 require CGI::Fast;
@@ -44,11 +44,25 @@ while ( my $cgi = CGI::Fast->new ) {
     $ENV{'ENV'}    = '' if defined $ENV{'ENV'};
     $ENV{'IFS'}    = '' if defined $ENV{'IFS'};
 
-    unless ($h->interp->comp_exists($cgi->path_info)) {
-       $cgi->path_info($cgi->path_info . "/index.html");
+    RT::ConnectToDatabase();
+
+    if ( ( !$h->interp->comp_exists( $cgi->path_info ) )
+        && ( $h->interp->comp_exists( $cgi->path_info . "/index.html" ) ) ) {
+        $cgi->path_info( $cgi->path_info . "/index.html" );
+    }
+
+    eval { $h->handle_cgi_object($cgi); };
+    if ($@) {
+        $RT::Logger->crit($@);
+    }
+
+
+    if ($RT::Handle->TransactionDepth) {
+        $RT::Handle->ForceRollback;
+        $RT::Logger->crit("Transaction not committed. Usually indicates a software fault. Data loss may have occurred") ;
     }
-    $h->handle_cgi_object($cgi);
-    # _should_ always be tied
+
+
 }
 
 1;
index e932bfc..a009663 100644 (file)
@@ -27,7 +27,7 @@ use strict;
 use File::Basename;
 require ('@RT_BIN_PATH@/webmux.pl');
 
-my $h = &RT::Interface::Web::NewCGIHandler();
+my $h = &RT::Interface::Web::NewCGIHandler(@RT::MasonParameters);
 
 # Enter CGI::Fast mode, which should also work as a vanilla CGI script.
 require CGI::Fast;
@@ -44,11 +44,25 @@ while ( my $cgi = CGI::Fast->new ) {
     $ENV{'ENV'}    = '' if defined $ENV{'ENV'};
     $ENV{'IFS'}    = '' if defined $ENV{'IFS'};
 
-    unless ($h->interp->comp_exists($cgi->path_info)) {
-       $cgi->path_info($cgi->path_info . "/index.html");
+    RT::ConnectToDatabase();
+
+    if ( ( !$h->interp->comp_exists( $cgi->path_info ) )
+        && ( $h->interp->comp_exists( $cgi->path_info . "/index.html" ) ) ) {
+        $cgi->path_info( $cgi->path_info . "/index.html" );
+    }
+
+    eval { $h->handle_cgi_object($cgi); };
+    if ($@) {
+        $RT::Logger->crit($@);
+    }
+
+
+    if ($RT::Handle->TransactionDepth) {
+        $RT::Handle->ForceRollback;
+        $RT::Logger->crit("Transaction not committed. Usually indicates a software fault. Data loss may have occurred") ;
     }
-    $h->handle_cgi_object($cgi);
-    # _should_ always be tied
+
+
 }
 
 1;
index 8e1135c..7774189 100755 (executable)
 use strict;
 require ('/opt/rt3/bin/webmux.pl');
 
-my $h = &RT::Interface::Web::NewCGIHandler();
+my $h = &RT::Interface::Web::NewCGIHandler(@RT::MasonParameters);
 
 require CGI;
 
 RT::Init();
 
 my $cgi = CGI->new;
-unless ($h->interp->comp_exists($cgi->path_info)) {
-    $cgi->path_info($cgi->path_info . "/index.html");
+if ( ( !$h->interp->comp_exists( $cgi->path_info ) )
+    && ( $h->interp->comp_exists( $cgi->path_info . "/index.html" ) ) ) {
+    $cgi->path_info( $cgi->path_info . "/index.html" );
 }
+
 $h->handle_cgi_object($cgi);
 
 1;
index 37d8380..614d4d4 100644 (file)
 use strict;
 require ('@RT_BIN_PATH@/webmux.pl');
 
-my $h = &RT::Interface::Web::NewCGIHandler();
+my $h = &RT::Interface::Web::NewCGIHandler(@RT::MasonParameters);
 
 require CGI;
 
 RT::Init();
 
 my $cgi = CGI->new;
-unless ($h->interp->comp_exists($cgi->path_info)) {
-    $cgi->path_info($cgi->path_info . "/index.html");
+if ( ( !$h->interp->comp_exists( $cgi->path_info ) )
+    && ( $h->interp->comp_exists( $cgi->path_info . "/index.html" ) ) ) {
+    $cgi->path_info( $cgi->path_info . "/index.html" );
 }
+
 $h->handle_cgi_object($cgi);
 
 1;
index e6d8378..c05d21e 100644 (file)
@@ -197,7 +197,7 @@ BEGIN {
 warn "Begin listening on $ENV{'FCGI_SOCKET_PATH'}\n";
 
 require CGI::Fast;
-my $h = &RT::Interface::Web::NewCGIHandler();
+my $h = &RT::Interface::Web::NewCGIHandler(@RT::MasonParameters);
 
 RT::Init();
 
index cc12c0e..0ba1f51 100644 (file)
@@ -197,7 +197,7 @@ BEGIN {
 warn "Begin listening on $ENV{'FCGI_SOCKET_PATH'}\n";
 
 require CGI::Fast;
-my $h = &RT::Interface::Web::NewCGIHandler();
+my $h = &RT::Interface::Web::NewCGIHandler(@RT::MasonParameters);
 
 RT::Init();
 
index 41220bb..d9f8a3f 100755 (executable)
--- a/rt/bin/rt
+++ b/rt/bin/rt
-#!!!PERL!! -w
-#
-# $Header: /home/cvs/cvsroot/freeside/rt/bin/Attic/rt,v 1.1 2002-08-12 06:17:07 ivan Exp $
-# RT is (c) 1996-2001 Jesse Vincent <jesse@bestpractical.com>
+#!/usr/bin/perl -w
+# BEGIN LICENSE BLOCK
+# 
+# Copyright (c) 1996-2003 Jesse Vincent <jesse@bestpractical.com>
+# 
+# (Except where explictly superceded by other copyright notices)
+# 
+# This work is made available to you under the terms of Version 2 of
+# the GNU General Public License. A copy of that license should have
+# been provided with this software, but in any event can be snarfed
+# from www.gnu.org.
+# 
+# This work is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# General Public License for more details.
+# 
+# Unless otherwise specified, all modifications, corrections or
+# extensions to this work which alter its source code become the
+# property of Best Practical Solutions, LLC when submitted for
+# inclusion in the work.
+# 
+# 
+# END LICENSE BLOCK
 
 use strict;
-use Carp;
-use Getopt::Long;
 
-use lib "!!RT_LIB_PATH!!";
-use lib "!!RT_ETC_PATH!!";
+# This program is intentionally written to have as few non-core module
+# dependencies as possible. It should stay that way.
+
+use Cwd;
+use LWP;
+use HTTP::Request::Common;
+
+# We derive configuration information from hardwired defaults, dotfiles,
+# and the RT* environment variables (in increasing order of precedence).
+# Session information is stored in ~/.rt_sessions.
+
+my $VERSION = 0.02;
+my $HOME = eval{(getpwuid($<))[7]}
+           || $ENV{HOME} || $ENV{LOGDIR} || $ENV{HOMEPATH}
+           || ".";
+my %config = (
+    (
+        debug   => 0,
+        user    => eval{(getpwuid($<))[0]} || $ENV{USER} || $ENV{USERNAME},
+        passwd  => undef,
+        server  => 'http://localhost/rt/',
+    ),
+    config_from_file($ENV{RTCONFIG} || ".rtrc"),
+    config_from_env()
+);
+my $session = new Session("$HOME/.rt_sessions");
+my $REST = "$config{server}/REST/1.0";
+
+sub whine;
+sub DEBUG { warn @_ if $config{debug} >= shift }
+
+# These regexes are used by command handlers to parse arguments.
+# (XXX: Ask Autrijus how i18n changes these definitions.)
+
+my $name   = '[\w.-]+';
+my $field  = '[a-zA-Z][a-zA-Z0-9_-]*';
+my $label  = '[a-zA-Z0-9@_.+-]+';
+my $labels = "(?:$label,)*$label";
+my $idlist = '(?:(?:\d+-)?\d+,)*(?:\d+-)?\d+';
+
+# Our command line looks like this:
+#
+#     rt <action> [options] [arguments]
+#
+# We'll parse just enough of it to decide upon an action to perform, and
+# leave the rest to per-action handlers to interpret appropriately.
+
+my %handlers = (
+#   handler     => [ ...aliases... ],
+    version     => ["version", "ver"],
+    logout      => ["logout"],
+    help        => ["help", "man"],
+    show        => ["show", "cat"],
+    edit        => ["create", "edit", "new", "ed"],
+    list        => ["search", "list", "ls"],
+    comment     => ["comment", "correspond"],
+    link        => ["link", "ln"],
+    merge       => ["merge"],
+    grant       => ["grant", "revoke"],
+);
+
+# Once we find and call an appropriate handler, we're done.
+
+my (%actions, $action);
+foreach my $fn (keys %handlers) {
+    foreach my $alias (@{ $handlers{$fn} }) {
+        $actions{$alias} = \&{"$fn"};
+    }
+}
+if (@ARGV && exists $actions{$ARGV[0]}) {
+    $action = shift @ARGV;
+}
+$actions{$action || "help"}->($action || ());
+exit;
 
-use RT::Interface::CLI  qw(CleanEnv LoadConfig DBConnect 
-                          GetCurrentUser GetMessageContent);
+# Handler functions.
+# ------------------
+#
+# The following subs are handlers for each entry in %actions.
 
-#Clean out all the nasties from the environment
-CleanEnv();
+sub version {
+    print "rt $VERSION\n";
+}
 
-#Load etc/config.pm and drop privs
-LoadConfig();
+sub logout {
+    submit("$REST/logout") if defined $session->cookie;
+}
 
-#Connect to the database and get RT::SystemUser and RT::Nobody loaded
-DBConnect();
+sub help {
+    my ($action, $type) = @_;
+    my (%help, $key);
 
-#Drop setgid permissions
-RT::DropSetGIDPermissions();
+    # What help topics do we know about?
+    local $/ = undef;
+    foreach my $item (@{ Form::parse(<DATA>) }) {
+        my $title = $item->[2]{Title};
+        my @titles = ref $title eq 'ARRAY' ? @$title : $title;
 
-#Get the current user all loaded
-my $CurrentUser = GetCurrentUser();
+        foreach $title (grep $_, @titles) {
+            $help{$title} = $item->[2]{Text};
+        }
+    }
 
-unless ($CurrentUser->Id) {
-       print "No RT user found. Please consult your RT administrator.\n";
-       exit(1);
+    # What does the user want help with?
+    undef $action if ($action && $actions{$action} eq \&help);
+    unless ($action || $type) {
+        # If we don't know, we'll look for clues in @ARGV.
+        foreach (@ARGV) {
+            if (exists $help{$_}) { $key = $_; last; }
+        }
+        unless ($key) {
+            # Tolerate possibly plural words.
+            foreach (@ARGV) {
+                if ($_ =~ s/s$// && exists $help{$_}) { $key = $_; last; }
+            }
+        }
+    }
+
+    if ($type && $action) {
+        $key = "$type.$action";
+    }
+    $key ||= $type || $action || "introduction";
+
+    # Find a suitable topic to display.
+    while (!exists $help{$key}) {
+        if ($type && $action) {
+            if ($key eq "$type.$action") { $key = $action;        }
+            elsif ($key eq $action)      { $key = $type;          }
+            else                         { $key = "introduction"; }
+        }
+        else {
+            $key = "introduction";
+        }
+    }
+
+    print STDERR $help{$key}, "\n\n";
 }
 
+# Displays a list of objects that match some specified condition.
 
-# {{{ commandline flags 
-
-my ( @id,
-     @limit_queue,
-     @limit_status,
-     @limit_owner,
-     @limit_priority,
-     @limit_final_priority,
-     @limit_requestor,
-     @limit_subject,
-     @limit_body,
-     @limit_created,
-     @limit_resolved,
-     @limit_lastupdated,
-     @limit_dependson,
-     @limit_dependedonby,
-     @limit_memberof,
-     @limit_hasmember,
-     @limit_refersto,
-     @limit_referredtoby,
-     @limit_keyword,
-     
-     @limit_due,
-     @limit_starts,
-     @limit_started,
-     $limit_first,
-     $limit_rows,
-     $history,
-     $summary,
-     $create,
-     @requestors,
-     @cc,
-     @admincc,
-     $status,
-     $subject,
-     $owner,
-     $steal,
-     $queue,
-     $time_left,
-     $priority,
-     $final_priority,
-     $due,
-     $starts,
-     $started,
-     $contacted,
-     $comment,
-     $reply,
-     $source,
-     $edit,
-     @dependson,
-     @memberof, 
-     @refersto,
-     $mergeinto,
-     @keywords,
-     $time_taken,
-     $verbose,
-     $debug,
-   $help,
-   $version);
-
-# }}}
-
-# Set defaults for cli args
-
-$edit = 1; # Assume the user wants to edit replies and comments 
-           # unless they specify --noedit
-
-# {{{    args
-
-my @args =("id=s" => \@id,
-          "limit-queue=s" => \@limit_queue,
-          "limit-status=s" => \@limit_status,
-          "limit-owner=s" => \@limit_owner,
-          "limit-priority=s" => \@limit_priority,
-          "limit-final-priority=s" => \@limit_final_priority,
-          "limit-requestor=s" => \@limit_requestor,
-          "limit-subject=s" => \@limit_subject,
-          "limit-body=s",      \@limit_body,
-          "limit-created=s" => \@limit_created,
-          "limit-due=s" =>     \@limit_due,
-          "limit-last-updated=s" => \@limit_lastupdated,
-          "limit-keyword=s" => \@limit_keyword,
-
-          "limit-member-of=s" => \@limit_memberof,
-          "limit-has-member=s" => \@limit_hasmember,
-          "limit-depended-on-by=s" => \@limit_dependedonby,
-          "limit-depends-on=s" => \@limit_dependson,
-          "limit-referred-to-by=s" => \@limit_referredtoby,
-          "limit-refers-to=s" => \@limit_refersto,
-
-          "limit-starts=s" => \@limit_starts,
-          "limit-started=s" => \@limit_started,
-          "limit-first=i" => \$limit_first,
-          "limit-rows=i" => \$limit_rows,
-          "history|show" => \$history,
-          "summary:s" => \$summary,
-          "create" => \$create,
-          "keywords=s" => \@keywords,
-          "requestor|requestors=s" => \@requestors,
-          "cc=s" => \@cc,
-          "admincc=s" => \@admincc,
-          "status=s" => \$status,
-          "subject=s" => \$subject,
-          "owner=s" => \$owner,
-          "steal" => \$steal,
-          "queue=s" => \$queue,
-
-          
-          "priority=i" => \$priority,
-          "final-priority=i" => \$final_priority,
-          "due=s" => \$due,
-          "starts=s" => \$starts,
-          "started=s" => \$started,
-          "contacted=s" => \$contacted,
-          "comment", \$comment,
-          "reply|respond", \$reply,
-          "source=s" => \$source,
-          "edit!" => \$edit,
-          "depends-on=s" => \@dependson,
-          "member-of=s" => \@memberof, 
-          "merge-into=s" => \$mergeinto, 
-          "refers-to=s" => \@refersto,
-          "time-left=i" => \$time_left,
-          "time-taken=i" => \$time_taken,
-          "verbose+" => \$verbose,
-          "debug" => \$debug,
-          "version" => \$version,
-          "help|h|usage" => \$help
-         );
-
-# }}}
-
-
-
-GetOptions(@args);
-
-print join(':',@keywords);
-# {{{ If they want it, print a usage message and get out
-
-if ($help) {
-
-
-print <<EOUSAGE;
-
-Limit the set of records returned:
-
---id=[first][-][last]
-  Specify a single ticket, a range, or to start with (n-) or end with (-n)
-a specific ticket.
-  
-  --limit-queue=<queue>
-         --limit-status=[!](new|open|stalled|resolved)
-
-         --limit-owner=[!]<userid>
-         --limit-priority=[starts][-][ends]
-         --limit-final-priority=[starts][-][ends]
-           starts is less than ends
-         --limit-requestor=[!]<userid>|<email>
-         --limit-subject=[!]<text>
-         --limit-body=[!]<text>
-         --limit-keyword=[!]<select>/<keyword>
-        
-       Links
-          --limit-member-of=<ticketid>
-          --limit-has-member=<ticketid>
-          --limit-refers-to=<ticketid>
-          --limit-referred-to-by=<ticketid>
-          --limit-depends-on=<ticketid>
-          --limit-depended-on-by=<ticketid>
+sub list {
+    my ($q, $type, %data, $orderby);
+    my $bad = 0;
 
+    while (@ARGV) {
+        $_ = shift @ARGV;
 
-       Dates
-         --limit-created=[starts][-][ends]
-         --limit-due=[starts][-][ends]
-         --limit-starts=[starts][-][ends]
-         --limit-started=[starts][-][ends]
-          --limit-resolved=[starts][-][ends]
-          --limit-last-updated=[starts][-][ends]
-           starts and ends are dates.  starts can not be less than ends
+        if (/^-t$/) {
+            $bad = 1, last unless defined($type = get_type_argument());
+        }
+        elsif (/^-S$/) {
+            $bad = 1, last unless get_var_argument(\%data);
+        }
+        elsif (/^-o$/) {
+            $orderby = shift @ARGV;
+        }
+        elsif (/^-([isl])$/) {
+            $data{format} = $1;
+        }
+        elsif (/^-f$/) {
+            if ($ARGV[0] !~ /^(?:(?:$field,)*$field)$/) {
+                whine "No valid field list in '-f $ARGV[0]'.";
+                $bad = 1; last;
+            }
+            $data{fields} = shift @ARGV;
+        }
+        elsif (!defined $q && !/^-/) {
+            $q = $_;
+        }
+        else {
+            my $datum = /^-/ ? "option" : "argument";
+            whine "Unrecognised $datum '$_'.";
+            $bad = 1; last;
+        }
+    }
 
-         --limit-first=<first row returned>
-         --limit-rows=<row count>
+    $type ||= "ticket";
+    unless ($type && defined $q) {
+        my $item = $type ? "query string" : "object type";
+        whine "No $item specified.";
+        $bad = 1;
+    }
+    return help("list", $type) if $bad;
 
-         --history | --show
-            show a history of the tickets found
+    my $r = submit("$REST/search/$type", { query => $q, %data, orderby => $orderby || "" });
+    print $r->content;
+}
 
-         --summary [format-string]
-             show a listing-style summary of the tickets found. If format string
-             is ommitted, uses \$RT_SUMMARY_FORMAT or an internal default
-            
+# Displays selected information about a single object.
 
-             #TODO: doc summary 
-             format: <atom>%<format>
-             atom:   <name><size>
-             size: <integer>
-             name:  (grep for # {{{ attribs for the array of ok values)
+sub show {
+    my ($type, @objects, %data);
+    my $slurped = 0;
+    my $bad = 0;
 
+    while (@ARGV) {
+        $_ = shift @ARGV;
 
-         --create
-            create a new ticket. Any attributes that you can modify on an existing ticket
-            can also be used for ticket creation.
+        if (/^-t$/) {
+            $bad = 1, last unless defined($type = get_type_argument());
+        }
+        elsif (/^-S$/) {
+            $bad = 1, last unless get_var_argument(\%data);
+        }
+        elsif (/^-([isl])$/) {
+            $data{format} = $1;
+        }
+        elsif (/^-$/ && !$slurped) {
+            chomp(my @lines = <STDIN>);
+            foreach (@lines) {
+                unless (is_object_spec($_, $type)) {
+                    whine "Invalid object on STDIN: '$_'.";
+                    $bad = 1; last;
+                }
+                push @objects, $_;
+            }
+            $slurped = 1;
+        }
+        elsif (/^-f$/) {
+            if ($ARGV[0] !~ /^(?:(?:$field,)*$field)$/) {
+                whine "No valid field list in '-f $ARGV[0]'.";
+                $bad = 1; last;
+            }
+            $data{fields} = shift @ARGV;
+        }
+        elsif (my $spec = is_object_spec($_, $type)) {
+            push @objects, $spec;
+        }
+        else {
+            my $datum = /^-/ ? "option" : "argument";
+            whine "Unrecognised $datum '$_'.";
+            $bad = 1; last;
+        }
+    }
 
+    unless (@objects) {
+        whine "No objects specified.";
+        $bad = 1;
+    }
+    return help("show", $type) if $bad;
 
+    my $r = submit("$REST/show", { id => \@objects, %data });
+    print $r->content;
+}
 
-Attributes
-  Basics
-         --status=<new|open|stalled|resolved|dead>
-           sets status
-          --subject=<subject>
-           sets subject
-          --owner=<userid>
-           set owner to 
-           --steal
-           Become the owner, even if someone else owns the ticket
-          --queue=<queueid>
-           set queue to
-          
-          --priority=<int>
-         
-           --final-priority=<int>
+# To create a new object, we ask the server for a form with the defaults
+# filled in, allow the user to edit it, and send the form back.
+#
+# To edit an object, we must ask the server for a form representing that
+# object, make changes requested by the user (either on the command line
+# or interactively via $EDITOR), and send the form back.
+
+sub edit {
+    my ($action) = @_;
+    my (%data, $type, @objects);
+    my ($cl, $text, $edit, $input, $output);
+
+    use vars qw(%set %add %del);
+    %set = %add = %del = ();
+    my $slurped = 0;
+    my $bad = 0;
+    
+    while (@ARGV) {
+        $_ = shift @ARGV;
+
+        if    (/^-e$/) { $edit = 1 }
+        elsif (/^-i$/) { $input = 1 }
+        elsif (/^-o$/) { $output = 1 }
+        elsif (/^-t$/) {
+            $bad = 1, last unless defined($type = get_type_argument());
+        }
+        elsif (/^-S$/) {
+            $bad = 1, last unless get_var_argument(\%data);
+        }
+        elsif (/^-$/ && !($slurped || $input)) {
+            chomp(my @lines = <STDIN>);
+            foreach (@lines) {
+                unless (is_object_spec($_, $type)) {
+                    whine "Invalid object on STDIN: '$_'.";
+                    $bad = 1; last;
+                }
+                push @objects, $_;
+            }
+            $slurped = 1;
+        }
+        elsif (/^set$/i) {
+            my $vars = 0;
+
+            while (@ARGV && $ARGV[0] =~ /^($field)([+-]?=)(.*)$/) {
+                my ($key, $op, $val) = ($1, $2, $3);
+                my $hash = ($op eq '=') ? \%set : ($op =~ /^\+/) ? \%add : \%del;
+
+                vpush($hash, lc $key, $val);
+                shift @ARGV;
+                $vars++;
+            }
+            unless ($vars) {
+                whine "No variables to set.";
+                $bad = 1; last;
+            }
+            $cl = $vars;
+        }
+        elsif (/^(?:add|del)$/i) {
+            my $vars = 0;
+            my $hash = ($_ eq "add") ? \%add : \%del;
+
+            while (@ARGV && $ARGV[0] =~ /^($field)=(.*)$/) {
+                my ($key, $val) = ($1, $2);
+
+                vpush($hash, lc $key, $val);
+                shift @ARGV;
+                $vars++;
+            }
+            unless ($vars) {
+                whine "No variables to set.";
+                $bad = 1; last;
+            }
+            $cl = $vars;
+        }
+        elsif (my $spec = is_object_spec($_, $type)) {
+            push @objects, $spec;
+        }
+        else {
+            my $datum = /^-/ ? "option" : "argument";
+            whine "Unrecognised $datum '$_'.";
+            $bad = 1; last;
+        }
+    }
 
-  Watchers
-         --requestors=[+|-]<userid|email address>
-          add or remove this user as a ticket requestor 
-         --cc=[+|-]<userid|email address>
-          add or remove this user as a ticket cc
-         --admincc=[+|-]<userid|email address>
-          add or remove this user as a ticket admincc
+    if ($action =~ /^ed(?:it)?$/) {
+        unless (@objects) {
+            whine "No objects specified.";
+            $bad = 1;
+        }
+    }
+    else {
+        if (@objects) {
+            whine "You shouldn't specify objects as arguments to $action.";
+            $bad = 1;
+        }
+        unless ($type) {
+            whine "What type of object do you want to create?";
+            $bad = 1;
+        }
+        @objects = ("$type/new");
+    }
+    return help($action, $type) if $bad;
 
-       (When creating tickets, just leave off the + or - )
+    # We need a form to make changes to. We usually ask the server for
+    # one, but we can avoid that if we are fed one on STDIN, or if the
+    # user doesn't want to edit the form by hand, and the command line
+    # specifies only simple variable assignments.
 
-  Keywords
-         --keywords[+|-]<keyword_select>/<keyword>
-          Add or remove a keyword.
+    if ($input) {
+        local $/ = undef;
+        $text = <STDIN>;
+    }
+    elsif ($edit || %add || %del || !$cl) {
+        my $r = submit("$REST/show", { id => \@objects, format => 'l' });
+        $text = $r->content;
+    }
 
+    # If any changes were specified on the command line, apply them.
+    if ($cl) {
+        if ($text) {
+            # We're updating forms from the server.
+            my $forms = Form::parse($text);
+
+            foreach my $form (@$forms) {
+                my ($c, $o, $k, $e) = @$form;
+                my ($key, $val);
+
+                next if ($e || !@$o);
+
+                local %add = %add;
+                local %del = %del;
+                local %set = %set;
+
+                # Make changes to existing fields.
+                foreach $key (@$o) {
+                    if (exists $add{lc $key}) {
+                        $val = delete $add{lc $key};
+                        vpush($k, $key, $val);
+                        $k->{$key} = vsplit($k->{$key}) if $val =~ /[,\n]/;
+                    }
+                    if (exists $del{lc $key}) {
+                        $val = delete $del{lc $key};
+                        my %val = map {$_=>1} @{ vsplit($val) };
+                        $k->{$key} = vsplit($k->{$key});
+                        @{$k->{$key}} = grep {!exists $val{$_}} @{$k->{$key}};
+                    }
+                    if (exists $set{lc $key}) {
+                        $k->{$key} = delete $set{lc $key};
+                    }
+                }
+                
+                # Then update the others.
+                foreach $key (keys %set) { vpush($k, $key, $set{$key}) }
+                foreach $key (keys %add) {
+                    vpush($k, $key, $add{$key});
+                    $k->{$key} = vsplit($k->{$key});
+                }
+                push @$o, (keys %add, keys %set);
+            }
+
+            $text = Form::compose($forms);
+        }
+        else {
+            # We're rolling our own set of forms.
+            my @forms;
+            foreach (@objects) {
+                my ($type, $ids, $args) =
+                    m{^($name)/($idlist|$labels)(?:(/.*))?$}o;
+
+                $args ||= "";
+                foreach my $obj (expand_list($ids)) {
+                    my %set = (%set, id => "$type/$obj$args");
+                    push @forms, ["", [keys %set], \%set];
+                }
+            }
+            $text = Form::compose(\@forms);
+        }
+    }
 
+    if ($output) {
+        print $text;
+        exit;
+    }
 
-  Dates
-          --due=<date>
-          --starts=<date>
-          --started=<date>
-          --contacted=<date>
+    my $synerr = 0;
 
-          --time-left=<int>
-            
-          --time-taken=<int>
+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 ($edit || (!$input && !$cl)) {
+        my $newtext = vi($text);
+        # We won't resubmit a bad form unless it was changed.
+        $text = ($synerr && $newtext eq $text) ? undef : $newtext;
+    }
 
+    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;
+                $synerr = 1;
+                goto EDIT;
+            }
+            else {
+                print $r->content;
+                exit -1;
+            }
+        }
+        print $r->content;
+    }
+}
 
-   Link related manipulation:
+# We roll "comment" and "correspond" into the same handler.
 
-          --depends-on=[+|-]<ticketid>
-          --member-of=[+|-]<ticketid>
-          --refers-to=[+|-]<ticketid>
-           --merge-into=<ticketid>
+sub comment {
+    my ($action) = @_;
+    my (%data, $id, @files, @bcc, @cc, $msg, $wtime, $edit);
+    my $bad = 0;
 
-Comments and replies
+    while (@ARGV) {
+        $_ = shift @ARGV;
 
-          --comment
-          --reply|respond
-            --source <path>
-                Specify the path to the source file for this ticket update
+        if (/^-e$/) {
+            $edit = 1;
+        }
+        elsif (/^-[abcmw]$/) {
+            unless (@ARGV) {
+                whine "No argument specified with $_.";
+                $bad = 1; last;
+            }
+
+            if (/-a/) {
+                unless (-f $ARGV[0] && -r $ARGV[0]) {
+                    whine "Cannot read attachment: '$ARGV[0]'.";
+                    exit -1;
+                }
+                push @files, shift @ARGV;
+            }
+            elsif (/-([bc])/) {
+                my $a = $_ eq "-b" ? \@bcc : \@cc;
+                @$a = split /\s*,\s*/, shift @ARGV;
+            }
+            elsif (/-m/) { $msg = shift @ARGV }
+            elsif (/-w/) { $wtime = shift @ARGV }
+        }
+        elsif (!$id && m|^(?:ticket/)?($idlist)$|) {
+            $id = $1;
+        }
+        else {
+            my $datum = /^-/ ? "option" : "argument";
+            whine "Unrecognised $datum '$_'.";
+            $bad = 1; last;
+        }
+    }
 
-             --noedit
-                Don't invoke \$EDITOR to edit the content of this update
+    unless ($id) {
+        whine "No object specified.";
+        $bad = 1;
+    }
+    return help($action, "ticket") if $bad;
+
+    my $form = [
+        "",
+        [ "Ticket", "Action", "Cc", "Bcc", "Attachment", "TimeWorked", "Text" ],
+        {
+            Ticket     => $id,
+            Action     => $action,
+            Cc         => [ @cc ],
+            Bcc        => [ @bcc ],
+            Attachment => [ @files ],
+            TimeWorked => $wtime || '',
+            Text       => $msg || '',
+        }
+    ];
+
+    my $text = Form::compose([ $form ]);
+
+    if ($edit || !$msg) {
+        my $error = 0;
+        my ($c, $o, $k, $e);
+
+        do {
+            my $ntext = vi($text);
+            exit if ($error && $ntext eq $text);
+            $text = $ntext;
+            $form = Form::parse($text);
+            $error = 0;
+
+            ($c, $o, $k, $e) = @{ $form->[0] };
+            if ($e) {
+                $error = 1;
+                $c = "# Syntax error.";
+                goto NEXT;
+            }
+            elsif (!@$o) {
+                exit;
+            }
+            @files = @{ vsplit($k->{Attachment}) };
+
+        NEXT:
+            $text = Form::compose([[$c, $o, $k, $e]]);
+        } while ($error);
+    }
 
+    my $i = 1;
+    foreach my $file (@files) {
+        $data{"attachment_$i"} = bless([ $file ], "Attachment");
+        $i++;
+    }
+    $data{content} = $text;
 
+    my $r = submit("$REST/ticket/comment/$id", \%data);
+    print $r->content;
+}
 
+# Merge one ticket into another.
 
-   Condiments
+sub merge {
+    my @id;
+    my $bad = 0;
 
-          --verbose
-          --debug
-          --version
-          --help|h|usage
-             You're reading it.
+    while (@ARGV) {
+        $_ = shift @ARGV;
 
-EOUSAGE
+        if (/^\d+$/) {
+            push @id, $_;
+        }
+        else {
+            whine "Unrecognised argument: '$_'.";
+            $bad = 1; last;
+        }
+    }
 
-    exit(0);
-}
+    unless (@id == 2) {
+        my $evil = @id > 2 ? "many" : "few";
+        whine "Too $evil arguments specified.";
+        $bad = 1;
+    }
+    return help("merge", "ticket") if $bad;
 
-# Print version, and leave
-if ($version) {
-       print "RT $RT::VERSION for $RT::rtname. Copyright 1996-2001 Jesse Vincent <jesse\@fsck.com>\n";
-       exit(0);
+    my $r = submit("$REST/ticket/merge/$id[0]", {into => $id[1]});
+    print $r->content;
 }
 
-# }}}
-
-# {{{ Validate any options that were passed in. normalize them.
+# Link one ticket to another.
 
-#if a queue was specified
-if ($queue) {
-    # make sure that $queue is a valid queue and load it into $queue_obj
-}
+sub link {
+    my ($bad, $del, %data) = (0, 0, ());
+    my %ltypes = map { lc $_ => $_ } qw(DependsOn DependedOnBy RefersTo
+                                        ReferredToBy HasMember MemberOf);
 
-#For each date in: $due, $starts, $started
+    while (@ARGV && $ARGV[0] =~ /^-/) {
+        $_ = shift @ARGV;
 
-# load up an RT::Date object and parse it into a normalized form
-# if it can't parse it, log an error and null out the variable
+        if (/^-d$/) {
+            $del = 1;
+        }
+        else {
+            whine "Unrecognised option: '$_'.";
+            $bad = 1; last;
+        }
+    }
 
-# }}}
+    if (@ARGV == 3) {
+        my ($from, $rel, $to) = @ARGV;
+        if ($from !~ /^\d+$/ || $to !~ /^\d+$/) {
+            my $bad = $from =~ /^\d+$/ ? $to : $from;
+            whine "Invalid ticket ID '$bad' specified.";
+            $bad = 1;
+        }
+        unless (exists $ltypes{lc $rel}) {
+            whine "Invalid relationship '$rel' specified.";
+            $bad = 1;
+        }
+        %data = (id => $from, rel => $rel, to => $to, del => $del);
+    }
+    else {
+        my $bad = @ARGV < 3 ? "few" : "many";
+        whine "Too $bad arguments specified.";
+        $bad = 1;
+    }
+    return help("link", "ticket") if $bad;
 
-# {{{ Check if we're creating, if so, create the ticket and be done
+    my $r = submit("$REST/ticket/link", \%data);
+    print $r->content;
+}
 
-if ($create) {
-    $RT::Logger->debug("Creating a new ticket");
+# Grant/revoke a user's rights.
 
-    #Make sure the current user can create tickets in this queue
-    
-    #Make sure that the owner specified can own tickets in this queue
+sub grant {
+    my ($cmd) = @_;
 
+    my $revoke = 0;
+    while (@ARGV) {
+    }
 
-           
-    my $linesref = GetMessageContent( Edit => $edit, Source => $source,
-                                     CurrentUser => $CurrentUser
-                                   );
-    
-    require MIME::Entity;
-    my $MIMEObj;
-    
-    if ($linesref) {
-       $MIMEObj = MIME::Entity->build(Data => $linesref);
-    }  
-    
-    use RT::Ticket;
-    my $Ticket=new RT::Ticket($CurrentUser);
-    my ($ticket, $trans, $msg) =
-      $Ticket->Create(Queue => $queue,
-                     Owner => $owner,
-                     Status => $status || 'new' ,
-                     Subject => $subject,
-                     Requestor => \@requestors,
-                     Cc => \@cc,
-                     AdminCc => \@admincc,
-                     Due => $due,
-                     Starts => $starts,
-                     Started => $started,
-                     TimeLeft => $time_left,
-                     InitialPriority => $priority,
-                     FinalPriority => $final_priority,
-                     MIMEObj => $MIMEObj
-                    );
-    print $msg . "\n";
+    $revoke = 1 if $cmd->{action} eq 'revoke';
 }
 
-# }}}
-
-else {
-    #Apply restrictions
-    use RT::Tickets;
-    my $Tickets = new RT::Tickets($CurrentUser);
-    
-    # {{{ Limit our search
-    my $value;                 #to use when iterating through restrictions
-    my $queue_id;              #to use when limiting by keyword
-    
-    # {{{ limit on id
-
-    foreach $value (@id) {
-       if ($value =~ /^(\d+)$/) {
-           $Tickets->LimitId ( VALUE => $1,
-                               OPERATOR => '=');
-       }       
-       elsif ($value =~ /^(\d*)\D?(\d*)$/) {
-           my $start = $1;
-           my $end = $2;
-           $Tickets->LimitId(
-                             VALUE => "$start",
-                             OPERATOR => '>=') if ($start);
-           $Tickets->LimitId(
-                             VALUE => "$end",
-                             OPERATOR => '<=') if ($end);
-       }       
+# Client <-> Server communication.
+# --------------------------------
+#
+# This function composes and sends an HTTP request to the RT server, and
+# interprets the response. It takes a request URI, and optional request
+# data (a string, or a reference to a set of key-value pairs).
+
+sub submit {
+    my ($uri, $content) = @_;
+    my ($req, $data);
+    my $ua = new LWP::UserAgent(agent => "RT/3.0b", env_proxy => 1);
+
+    # Did the caller specify any data to send with the request?
+    $data = [];
+    if (defined $content) {
+        unless (ref $content) {
+            # If it's just a string, make sure LWP handles it properly.
+            # (By pretending that it's a file!)
+            $content = [ content => [undef, "", Content => $content] ];
+        }
+        elsif (ref $content eq 'HASH') {
+            my @data;
+            foreach my $k (keys %$content) {
+                if (ref $content->{$k} eq 'ARRAY') {
+                    foreach my $v (@{ $content->{$k} }) {
+                        push @data, $k, $v;
+                    }
+                }
+                else { push @data, $k, $content->{$k} }
+            }
+            $content = \@data;
+        }
+        $data = $content;
     }
 
+    # Should we send authentication information to start a new session?
+    if (!defined $session->cookie) {
+        push @$data, ( user => $config{user} );
+        push @$data, ( pass => $config{passwd} || read_passwd() );
+    }
 
-    # }}}
-    
-    # {{{ limit on status
-
-    foreach $value (@limit_status) {
-       if ($value =~ /^(=|!=|!|)(.*)$/) {
-           my $op = $1;
-           my $val = $2;
-                
-
-           $op = ParseBooleanOp($op);
-           $Tickets->LimitStatus(VALUE => "$val",
-                                 OPERATOR => "$op");
-       }       
+    # Now, we construct the request.
+    if (@$data) {
+        $req = POST($uri, $data, Content_Type => 'form-data');
     }
+    else {
+        $req = GET($uri);
+    }
+    $session->add_cookie_header($req);
+
+    # Then we send the request and parse the response.
+    DEBUG(3, $req->as_string);
+    my $res = $ua->request($req);
+    DEBUG(3, $res->as_string);
+
+    if ($res->is_success) {
+        # The content of the response we get from the RT server consists
+        # of an HTTP-like status line followed by optional header lines,
+        # a blank line, and arbitrary text.
+
+        my ($head, $text) = split /\n\n/, $res->content, 2;
+        my ($status, @headers) = split /\n/, $head;
+        $text =~ s/\n*$/\n/;
+
+        # "RT/3.0.1 401 Credentials required"
+        if ($status !~ m#^RT/\d+(?:\.\d+)+(?:-?\w+)? (\d+) ([\w\s]+)$#) {
+            warn "rt: Malformed RT response from $config{server}.\n";
+            warn "(Rerun with RTDEBUG=3 for details.)\n" if $config{debug} < 3;
+            exit -1;
+        }
 
-    # }}}
-
-
-
-    # {{{ limit on queue
-    foreach $value (@limit_queue) {
-       if ($value =~ /^(\W?)(.*?)$/i) {
-           my $op = $1;
-           my $val = $2;
-               
-           $op = ParseBooleanOp($op);
-
-           my $queue_obj = new RT::Queue($RT::SystemUser);
-               
-           unless ($queue_obj->Load($val)) {
-               $RT::Logger->debug("Queue '$val' not found");
-               print STDERR "Queue '$val' not found\n";        
-               exit(-1);
-           }
-           $RT::Logger->debug ("Limiting queue to $op ".$queue_obj->Name);
-           $Tickets->LimitQueue(VALUE => $queue_obj->Name,
-                                OPERATOR => $op);
-           $queue_id=$queue_obj->id;
-       }       
-    }  
-
-    # {{{ limit on keyword
-    foreach $value (@limit_keyword) {
-       if ($value =~ /^(\W?)(.*?)\/(.*)$/i) {
-           my $op = $1;
-           my $select = $2;
-           my $keyword = $3;
-
-           $op = ParseBooleanOp($op);
-
-           # load the keyword select
-           my $keyselect = RT::KeywordSelect->new($RT::SystemUser);
-           unless ($keyselect->LoadByName(Name=>$select, Queue=>$queue_id)) {
-               $RT::Logger->debug("KeywordSelect '$select' not found");
-               print STDERR "KeywordSelect '$select' not fount\n";
-               exit(-1);
-           }
-
-           # load the keyword
-           my $k = RT::Keyword->new($RT::SystemUser);
-           unless ($k->LoadByNameAndParentId($keyword, $keyselect->Keyword)) {
-               $RT::Logger->debug("Keyword '$keyword' not found");
-               print STDERR "Keyword '$keyword' not found\n";
-               exit(-1);
-           }
-           $Tickets->LimitKeyword(OPERATOR => $op,
-                                  KEYWORDSELECT => $keyselect->id,
-                                  KEYWORD => $k->id);
-           $RT::Logger->debug ("Limiting keyword to $op ".$k->Path);
-       }
+        # Our caller can pretend that the server returned a custom HTTP
+        # response code and message. (Doing that directly is apparently
+        # not sufficiently portable and uncomplicated.)
+        $res->code($1);
+        $res->message($2);
+        $res->content($text);
+        $session->update($res) if ($res->is_success || $res->code != 401);
+
+        if (!$res->is_success) {
+            # We can deal with authentication failures ourselves. Either
+            # we sent invalid credentials, or our session has expired.
+            if ($res->code == 401) {
+                my %d = @$data;
+                if (exists $d{user}) {
+                    warn "rt: Incorrect username or password.\n";
+                    exit -1;
+                }
+                elsif ($req->header("Cookie")) {
+                    # We'll retry the request with credentials, unless
+                    # we only wanted to logout in the first place.
+                    $session->delete;
+                    return submit(@_) unless $uri eq "$REST/logout";
+                }
+            }
+            # Conflicts should be dealt with by the handler and user.
+            # For anything else, we just die.
+            elsif ($res->code != 409) {
+                warn "rt: ", $res->content;
+                exit;
+            }
+        }
     }
-    # }}}
-    # {{{ limit on owner
-    foreach $value (@limit_owner) {
-       if ($value =~ /^(\W?)(.*?)$/i) {
-           my $op = $1;
-           my $val = $2;
-               
-           $op = ParseBooleanOp($op);
-
-           my $user_obj = new RT::User($RT::SystemUser);
-               
-           unless ($user_obj->Load($val)) {
-               $RT::Logger->debug("User '$val' not found");
-               print STDERR "User '$val' not found\n"; 
-               exit(-1);
-           }
-           $val = $user_obj->id();
-               
-           $RT::Logger->debug ("Limiting owner to $op $val");
-           $Tickets->LimitOwner(VALUE => "$val",
-                                OPERATOR => "$op");
-       }       
-    }  
-    # }}}
-    # {{{ limt on priority
-
-    foreach $value (@limit_priority) {
-       my ($start, $end) = ParseRange($value);
-       if ($start == $end) {
-           $Tickets->LimitPriority( VALUE => $start,
-                                    OPERATOR => '=');
-       } elsif ($start) {
-           $Tickets->LimitPriority( VALUE => $start,
-                                    OPERATOR => '>=');
-       } elsif ($end) {
-           $Tickets->LimitPriority( VALUE => $end,
-                                    OPERATOR => '<=');
-       }       
-           
+    else {
+        warn "rt: Server error: ", $res->message, " (", $res->code, ")\n";
+        exit -1;
     }
-    foreach $value (@limit_final_priority) {
-       my ($start, $end) = ParseRange($value);
-       if ($start == $end) {
-           $Tickets->LimitFinalPriority( VALUE => $start,
-                                         OPERATOR => '=');
-       } elsif ($start) {
-           $Tickets->LimitFinalPriority( VALUE => $start,
-                                         OPERATOR => '>=');
-       } elsif ($end) {
-           $Tickets->LimitFinalPriority( VALUE => $end,
-                                         OPERATOR => '<=');
-       }       
+
+    return $res;
+}
+
+# Session management.
+# -------------------
+#
+# Maintains a list of active sessions in the ~/.rt_sessions file.
+{
+    package Session;
+    my ($s, $u);
+
+    # Initialises the session cache.
+    sub new {
+        my ($class, $file) = @_;
+        my $self = {
+            file => $file || "$HOME/.rt_sessions",
+            sids => { }
+        };
+       
+        # The current session is identified by the currently configured
+        # server and user.
+        ($s, $u) = @config{"server", "user"};
+
+        bless $self, $class;
+        $self->load();
+
+        return $self;
     }
-    # }}}
-
-    foreach $value (@limit_requestor) {
-       if ($value =~ /^(\W?)(.*?)$/i) {
-           my $op = $1;
-           my $val = $2;
-               
-           $op = ParseBooleanOp($op);
-           $Tickets->LimitRequestor(VALUE => $val,
-                                    OPERATOR => $op );
-       }
-           
+
+    # Returns the current session cookie.
+    sub cookie {
+        my ($self) = @_;
+        my $cookie = $self->{sids}{$s}{$u};
+        return defined $cookie ? "RT_SID=$cookie" : undef;
     }
-    foreach $value (@limit_subject) {
-       
-       if ($value =~ /^(\W?)(.*?)$/i) {
-           my $op = $1;
-           my $val = $2;
-           
-           $op = ParseLikeOp($op);
-           
-           $Tickets->LimitSubject(VALUE => $val,
-                                  OPERATOR => $op );
-           }
+
+    # Deletes the current session cookie.
+    sub delete {
+        my ($self) = @_;
+        delete $self->{sids}{$s}{$u};
     }
-    
-    foreach $value (@limit_body) {
-       if ($value =~ /^(\W?)(.*?)$/i) {
-           my $op = $1;
-           my $val = $2;
-           
-           $op = ParseLikeOp($op);
-           
-               $Tickets->LimitBody(VALUE => $val,
-                                   OPERATOR => $op );
-       }       
-       
+
+    # Adds a Cookie header to an outgoing HTTP request.
+    sub add_cookie_header {
+        my ($self, $request) = @_;
+        my $cookie = $self->cookie();
+
+        $request->header(Cookie => $cookie) if defined $cookie;
     }
-    
-    
-    
-    # Dates
-    foreach my $date (@limit_created) {
-       my ($start, $end) = ParseDateRange($date);
-       $Tickets->LimitCreated ( VALUE => $start,
-                                OPERATOR => '>=' ) if ($start);
-       $Tickets->LimitCreated ( VALUE => $end,
-                                OPERATOR => '<=' ) if ($end);
+
+    # Extracts the Set-Cookie header from an HTTP response, and updates
+    # session information accordingly.
+    sub update {
+        my ($self, $response) = @_;
+        my $cookie = $response->header("Set-Cookie");
+
+        if (defined $cookie && $cookie =~ /^RT_SID=([0-9A-Fa-f]+);/) {
+            $self->{sids}{$s}{$u} = $1;
+        }
     }
 
-    foreach my $date (@limit_due) {
-       my ($start, $end) = ParseDateRange($date);
-       $Tickets->LimitDue ( VALUE => $start,
-                                OPERATOR => '>=' ) if ($start);
-       $Tickets->LimitDue ( VALUE => $end,
-                                OPERATOR => '<=' ) if ($end);
+    # Loads the session cache from the specified 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+ [0-9A-Fa-f]+$#;
+                my ($server, $user, $cookie) = split / /, $_;
+                $sids->{$server}{$user} = $cookie;
+            }
+            return 1;
+        };
+        return 0;
     }
 
-    foreach my $date (@limit_starts) {
-       my ($start, $end) = ParseDateRange($date);
-       $Tickets->LimitStarts ( VALUE => $start,
-                                OPERATOR => '>=' ) if ($start);
-       $Tickets->LimitStarts ( VALUE => $end,
-                                OPERATOR => '<=' ) if ($end);
+    # 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";
+                    }
+                }
+            }
+            close(F);
+            chmod 0600, $file;
+            return 1;
+        };
+        return 0;
     }
 
-    foreach my $date (@limit_started) {
-       my ($start, $end) = ParseDateRange($date);
-       $Tickets->LimitStarted ( VALUE => $start,
-                                OPERATOR => '>=' ) if ($start);
-       $Tickets->LimitStarted ( VALUE => $end,
-                                OPERATOR => '<=' ) if ($end);
+    sub DESTROY {
+        my $self = shift;
+        $self->save;
     }
+}
 
-    foreach my $date (@limit_resolved) {
-       my ($start, $end) = ParseDateRange($date);
-       $Tickets->LimitResolved ( VALUE => $start,
-                                OPERATOR => '>=' ) if ($start);
-       $Tickets->LimitResolved ( VALUE => $end,
-                                OPERATOR => '<=' ) if ($end);
+# Form handling.
+# --------------
+#
+# Forms are RFC822-style sets of (field, value) specifications with some
+# initial comments and interspersed blank lines allowed for convenience.
+# Sets of forms are separated by --\n (in a cheap parody of MIME).
+#
+# Each form is parsed into an array with four elements: commented text
+# at the start of the form, an array with the order of keys, a hash with
+# key/value pairs, and optional error text if the form syntax was wrong.
+
+# Returns a reference to an array of parsed forms.
+sub Form::parse {
+    my $state = 0;
+    my @forms = ();
+    my @lines = split /\n/, $_[0];
+    my ($c, $o, $k, $e) = ("", [], {}, "");
+
+    LINE:
+    while (@lines) {
+        my $line = shift @lines;
+
+        next LINE if $line eq '';
+
+        if ($line eq '--') {
+            # We reached the end of one form. We'll ignore it if it was
+            # empty, and store it otherwise, errors and all.
+            if ($e || $c || @$o) {
+                push @forms, [ $c, $o, $k, $e ];
+                $c = ""; $o = []; $k = {}; $e = "";
+            }
+            $state = 0;
+        }
+        elsif ($state != -1) {
+            if ($state == 0 && $line =~ /^#/) {
+                # Read an optional block of comments (only) at the start
+                # of the form.
+                $state = 1;
+                $c = $line;
+                while (@lines && $lines[0] =~ /^#/) {
+                    $c .= "\n".shift @lines;
+                }
+                $c .= "\n";
+            }
+            elsif ($state <= 1 && $line =~ /^($field):(?:\s+(.*))?$/) {
+                # Read a field: value specification.
+                my $f  = $1;
+                my @v  = ($2 || ());
+
+                # Read continuation lines, if any.
+                while (@lines && ($lines[0] eq '' || $lines[0] =~ /^\s+/)) {
+                    push @v, shift @lines;
+                }
+                pop @v while (@v && $v[-1] eq '');
+
+                # Strip longest common leading indent from text.
+                my $ws = "";
+                foreach my $ls (map {/^(\s+)/} @v[1..$#v]) {
+                    $ws = $ls if (!$ws || length($ls) < length($ws));
+                }
+                s/^$ws// foreach @v;
+
+                push(@$o, $f) unless exists $k->{$f};
+                vpush($k, $f, join("\n", @v));
+
+                $state = 1;
+            }
+            elsif ($line !~ /^#/) {
+                # We've found a syntax error, so we'll reconstruct the
+                # form parsed thus far, and add an error marker. (>>)
+                $state = -1;
+                $e = Form::compose([[ "", $o, $k, "" ]]);
+                $e.= $line =~ /^>>/ ? "$line\n" : ">> $line\n";
+            }
+        }
+        else {
+            # We saw a syntax error earlier, so we'll accumulate the
+            # contents of this form until the end.
+            $e .= "$line\n";
+        }
     }
+    push(@forms, [ $c, $o, $k, $e ]) if ($e || $c || @$o);
 
-    foreach my $date (@limit_lastupdated) {
-       my ($start, $end) = ParseDateRange($date);
-       $Tickets->LimitLastUpdated( VALUE => $start,
-                                OPERATOR => '>=' ) if ($start);
-       $Tickets->LimitLastUpdated ( VALUE => $end,
-                                OPERATOR => '<=' ) if ($end);
+    foreach my $l (keys %$k) {
+        $k->{$l} = vsplit($k->{$l}) if (ref $k->{$l} eq 'ARRAY');
     }
 
-    foreach my $link (@limit_memberof) {
-       $Tickets->LimitMemberOf($link);
-    }  
+    return \@forms;
+}
 
-    foreach my $link (@limit_hasmember) {
-       $Tickets->LimitHasMember($link);
-    }  
+# Returns text representing a set of forms.
+sub Form::compose {
+    my ($forms) = @_;
+    my @text;
 
-    foreach my $link (@limit_dependson) {
-       $Tickets->LimitDependsOn($link);
-    }  
+    foreach my $form (@$forms) {
+        my ($c, $o, $k, $e) = @$form;
+        my $text = "";
 
-    foreach my $link (@limit_dependedonby) {
-       $Tickets->LimitDependedOnBy($link);
+        if ($c) {
+            $c =~ s/\n*$/\n/;
+            $text = "$c\n";
+        }
+        if ($e) {
+            $text .= $e;
+        }
+        elsif ($o) {
+            my @lines;
+
+            foreach my $key (@$o) {
+                my ($line, $sp);
+                my $v = $k->{$key};
+                my @values = ref $v eq 'ARRAY' ? @$v : $v;
+
+                $sp = " "x(length("$key: "));
+                $sp = " "x4 if length($sp) > 16;
+
+                foreach $v (@values) {
+                    if ($v =~ /\n/) {
+                        $v =~ s/^/$sp/gm;
+                        $v =~ s/^$sp//;
+
+                        if ($line) {
+                            push @lines, "$line\n\n";
+                            $line = "";
+                        }
+                        elsif (@lines && $lines[-1] !~ /\n\n$/) {
+                            $lines[-1] .= "\n";
+                        }
+                        push @lines, "$key: $v\n\n";
+                    }
+                    elsif ($line &&
+                           length($line)+length($v)-rindex($line, "\n") >= 70)
+                    {
+                        $line .= ",\n$sp$v";
+                    }
+                    else {
+                        $line = $line ? "$line, $v" : "$key: $v";
+                    }
+                }
+
+                $line = "$key:" unless @values;
+                if ($line) {
+                    if ($line =~ /\n/) {
+                        if (@lines && $lines[-1] !~ /\n\n$/) {
+                            $lines[-1] .= "\n";
+                        }
+                        $line .= "\n";
+                    }
+                    push @lines, "$line\n";
+                }
+            }
+
+            $text .= join "", @lines;
+        }
+        else {
+            chomp $text;
+        }
+        push @text, $text;
     }
-    foreach my $link (@limit_refersto) {
-       $Tickets->LimitRefersTo($link);
-    }  
-    
-    foreach my $link (@limit_referredtoby) {
-       $Tickets->LimitReferredToBy($link);
-    }  
 
-    
-    if ($limit_first){
-    }
-    if ($limit_rows){
-    }
+    return join "\n--\n\n", @text;
+}
 
-# }}}
-    
-    # {{{ Iterate through all tickets we found
+# Configuration.
+# --------------
 
+# Returns configuration information from the environment.
+sub config_from_env {
+    my %env;
 
-    my ($format, $titles, $code);
-    
-    #Set up the summary format if we need to
-    if (defined $summary) {
-       my $format_string = $summary || $ENV{'RT_SUMMARY_FORMAT'} || "%id4%status4%queue7%subject40%requestor16";
+    foreach my $k ("DEBUG", "USER", "PASSWD", "SERVER") {
+        if (exists $ENV{"RT$k"}) {
+            $env{lc $k} = $ENV{"RT$k"};
+        }
+    }
 
-       ($format, $titles, $code) = BuildListingFormat($format_string);
-        printf "$format\n", eval "$titles";
-   }   
+    return %env;
+}
 
+# Finds a suitable configuration file and returns information from it.
+sub config_from_file {
+    my ($rc) = @_;
 
-    while (my $Ticket = $Tickets->Next()) {
-       $RT::Logger->debug ("Now working on ticket ". $Ticket->id);
-    
-       #Run through all the ticket modifications we might want to do
-       #TODO: these are all insufficiently lazy and should be replaced with some 
-       # nice foreaches.
-
-
-       # {{{ deal with watchers
-       
-       # add / delete requestors
-       foreach $value (@requestors) {
-           if ($value =~ /^(\W?)(.*)$/) {
-               my $op = $1;
-               my $addr = $2;
-               
-               $Ticket->AddRequestor(Email => $addr) if ($op eq '+');
-               $Ticket->DeleteRequestor( $addr) if ($op eq '-');
-           }   
-       }
-       
-       # add / delete ccs
-       foreach $value (@cc) {
-           if ($value =~ /^(\W?)(.*)$/) {
-               my $op = $1;
-               my $addr = $2;
-               $Ticket->AddCc(Email => $addr) if ($op eq '+');
-               $Ticket->DeleteCc($addr) if ($op eq '-');
-           }   
-       }       
-       
-       # add / delete adminccs
-        $RT::Logger->debug("Looking at admin ccs");
-       foreach $value (@admincc) {
-           if ($value =~ /^(\W?)(.*)$/) {
-               my $op = $1;
-               my $addr = $2;
-               $Ticket->AddAdminCc(Email => $addr) if ($op eq '+');
-               $Ticket->DeleteAdminCc($addr) if ($op eq '-');
-           }   
-       }       
-
-       # }}}
-       
-       # {{{ Deal with ticket keywords
-
-       my $KeywordSelects = $Ticket->QueueObj->KeywordSelects();
-        $RT::Logger->debug ("Looking at keywords");
-       foreach $value (@keywords) {
-           $RT::Logger->debug("Looking at --keyword=$value");
-           if ($value =~ /^(\W?)(.*?)\/(.*)$/) {
-               my $op = $1;
-               my $select = $2;
-               my $keyword = $3;
-               
-               $RT::Logger->debug("Going to $op Keyword $select / $keyword");  
-               while (my $ks = $KeywordSelects->Next) {
-                    $RT::Logger->debug("$select is select ".$ks->Name." is found");
-                   next unless ($ks->Name =~ /$select/i);
-                   $RT::Logger->debug ("Found a match for $select\n"); 
-                   my $kids = $ks->KeywordObj->Descendents;
-    
-                    my ($kid);
-                   foreach $kid (keys %{$kids}) {
-                        $RT::Logger->debug("Now comparing $keyword with ".$kids->{$kid}. "\n");
-                       next unless ($kids->{$kid} =~ /^$keyword$/i);
-                       $RT::Logger->debug("Going to $op $select / $keyword (".$kids->{$kid} .")");     
-                       $Ticket->DeleteKeyword(KeywordSelect => $ks->id,
-                                           Keyword => $kid) if ($op eq '-');
-                       
-                       $Ticket->AddKeyword(KeywordSelect => $ks->id,
-                                           Keyword => $kid) if ($op eq '+');
-                   }
-                   
-               }
-           }
-       }
-       # }}}
-       
-       # {{{ deal with links
-
-       # Deal with merging {
-       if ($mergeinto) {
-               my ($trans, $msg) =$Ticket->MergeInto($mergeinto);
-               print $msg."\n";
-       }       
-       # add /delete depends-ons
-
-       foreach my $value (@dependson) {
-           if ($value =~ /^(\W?)(.*)$/) {
-               my $op = $1;
-               my $ticket = $2;
-               if (!$op or ($op eq '+')) {
-                   my ($trans, $msg) =
-                     $Ticket->AddLink(Type => 'DependsOn', Target => $ticket);
-                   print $msg."\n";
-               }
-               elsif ($op eq '-') {
-                   my ($trans, $msg) = 
-                     $Ticket->DeleteLink(Type => 'DependsOn', Target => $ticket);
-                   print $msg."\n";
-               }
-
-           }
-       }
-       # add /delete member-of
-       foreach my $value (@memberof) {
-           if ($value =~ /^(\W?)(.*)$/) {
-               my $op = $1;
-               my $ticket = $2;
-               if ($op eq '+') {
-                   my ($trans, $msg) =
-                     $Ticket->AddLink(Type => 'MemberOf', Target => $ticket);
-                   print $msg;
-               }
-               elsif ($op eq '-') {
-                   my ($trans, $msg) = 
-                     $Ticket->DeleteLink(Type => 'MemberOf', Target => $ticket);
-                   print $msg;
-               }
-
-           }
-       }       
-       # add / delete refers-to
-               foreach my $value (@refersto) {
-           if ($value =~ /^(\W?)(.*)$/) {
-               my $op = $1;
-               my $ticket = $2;
-               if ($op eq '+') {
-                   my ($trans, $msg) =
-                     $Ticket->AddLink(Type => 'RefersTo', Target => $ticket);
-                   print $msg;
-               }
-               elsif ($op eq '-') {
-                   my ($trans, $msg) = 
-                     $Ticket->DeleteLink(Type => 'RefersTo', Target => $ticket);
-                   print $msg;
-               }
-
-           }
-       }
-
-       # }}}
-       
-       # {{{ deal with dates
-       
-       #set due 
-       if ($due) {
-           my $iso = ParseDateToISO($due);
-           if ($iso) {
-               $RT::Logger->debug("Setting due date to $iso ($due)");
-               my ($trans, $msg) = 
-                 $Ticket->SetDue($iso);
-               print $msg;
-           }
-           else {
-               print "Due date '$due' could not be parsed";
-           }
-       }
-
-       #set starts
-       if ($starts) {
-           my $iso = ParseDateToISO($due);
-           if ($iso) {
-               my ($trans, $msg) = 
-                 $Ticket->SetStarts($iso);
-               print $msg."\n";
-           }
-           else {
-               print "Starts date '$starts' could not be parsed";
-           }
-       }
-       #set started
-               if ($started) {
-           my $iso = ParseDateToISO($started);
-           if ($iso) {
-               my ($trans, $msg) = 
-                 $Ticket->SetStarted($iso);
-               print $msg."\n";
-           }
-           else {
-               print "Started date '$started' could not be parsed";
-           }
-       }
-       #set contacted
-               if ($contacted) {
-           my $iso = ParseDateToISO($contacted);
-           if ($iso) {
-               my ($trans, $msg) = 
-                 $Ticket->SetContacted($iso);
-               print $msg."\n";
-           }
-           else {
-               print "Contacted date '$contacted' could not be parsed";
-           }
-       }
-
-    # }}}
-       
-       # {{{ set other attributes
-
-       #Set subject
-       if ($subject) {
-           my ($trans, $msg) = $Ticket->SetSubject($subject);
-           print $msg."\n";
-       }
-       
-       #Set priority
-       if ($priority) {
-           my ($trans, $msg) = 
-             $Ticket->SetPriority($priority);
-           print $msg."\n";
-       }
-       
-       #Set final priority
-       if ($final_priority) {
-           my ($trans, $msg) =
-             $Ticket->SetFinalPriority($final_priority);
-           print $msg."\n";
-       }
-
-       #Set status
-       if ($status) {
-           my ($trans, $msg) = 
-             $Ticket->SetStatus($status);
-           print $msg."\n";
-       }
-       
-       #Set time left
-       if ($time_left) {
-           my ($trans, $msg) = 
-             $Ticket->SetTimeLeft($time_left);
-           print $msg."\n";
-       }
-
-       #Set time_taken 
-       if ($time_taken) {
-           my ($trans, $msg) = 
-             $Ticket->SetTimeTaken($time_taken);
-           print $msg."\n";
-       }
-       
-       #Set owner
-       if ($owner) {
-           my ($trans, $msg) =
-             $Ticket->SetOwner($owner);
-           print $msg."\n";
-       }
-
-        # Steal
-        if ($steal) {
-                my ($trans, $msg) =
-                 $Ticket->Steal();
-                 print $msg . "\n";
+    if ($rc =~ m#^/#) {
+        # We'll use an absolute path if we were given one.
+        return parse_config_file($rc);
+    }
+    else {
+        # Otherwise we'll use the first file we can find in the current
+        # directory, or in one of its (increasingly distant) ancestors.
+
+        my @dirs = split /\//, cwd;
+        while (@dirs) {
+            my $file = join('/', @dirs, $rc);
+            if (-r $file) {
+                return parse_config_file($file);
+            }
+
+            # Remove the last directory component each time.
+            pop @dirs;
+        }
+
+        # Still nothing? We'll fall back to some likely defaults.
+        for ("$HOME/$rc", "/etc/rt.conf") {
+            return parse_config_file($_) if (-r $_);
         }
-       #Set queue 
-       if ($queue) {
-           my ($trans, $msg) = 
-             $Ticket->SetQueue($queue);
-           print $msg."\n";
-       }
-
-    # }}}
-       
-
-
-       # {{{ Perform ticket comments/replies
-       if ($reply) {
-           $RT::Logger->debug("Replying to ticket ".$Ticket->Id);
-           
-           my $linesref = GetMessageContent( Edit => $edit, Source => $source,
-                                            CurrentUser => $CurrentUser
-                                          );
-           
-           #TODO build this entity
-           require MIME::Entity;
-           my $MIMEObj = MIME::Entity->build(Data => $linesref);
-           
-           $Ticket->Correspond( MIMEObj => $MIMEObj ,
-                                TimeTaken => $time_taken);
-       }       
-       
-       elsif ($comment) {
-           $RT::Logger->debug("Commenting on ticket ".$Ticket->Id);
-       
-           my $linesref =GetMessageContent(Edit => $edit, Source => $source,
-                                           CurrentUser => $CurrentUser);
-           #TODO build this entity
-           require MIME::Entity;
-           my $MIMEObj = MIME::Entity->build(Data => $linesref);
-           
-           $Ticket->Comment( MIMEObj => $MIMEObj,
-                             TimeTaken => $time_taken);
-       }
-
-    # }}}
-       
-       # {{{ Display whatever we need to display
-
-       # {{{ Display a full ticket listing and history
-       if ($history) {
-           #Display the history
-           $RT::Logger->debug("Show history for ".$Ticket->id);
-           
-           if ($Ticket->CurrentUserHasRight("ShowTicket")) {
-               &ShowSummary($Ticket);
-               print "\n";
-               &ShowHistory($Ticket);
-           }
-           else {
-               print "You don't have permission to view that ticket.\n";
-           }
-       }       
-
-       # }}}
-       
-       # {{{ Display a summary if we need to
-       if (defined $summary) {
-           $RT::Logger->debug ("Show ticket summary with format $format");
-           
-           printf $format."\n", eval $code;
-           
-       }       
-       # }}}
-
-       # }}}
-       
     }
 
-    # }}}
-    
+    return ();
 }
 
+# Makes a hash of the specified configuration file.
+sub parse_config_file {
+    my %cfg;
+    my ($file) = @_;
+
+    open(CFG, $file) && do {
+        while (<CFG>) {
+            chomp;
+            next if (/^#/ || /^\s*$/);
+
+            if (/^(user|passwd|server)\s+([^ ]+)$/) {
+                $cfg{$1} = $2;
+            }
+            else {
+                die "rt: $file:$.: unknown configuration directive.\n";
+            }
+        }
+    };
 
-$RT::Handle->Disconnect();
+    return %cfg;
+}
 
+# Helper functions.
+# -----------------
 
+sub whine {
+    my $sub = (caller(1))[3];
+    $sub =~ s/^main:://;
+    warn "rt: $sub: @_\n";
+    return;
+}
 
+sub read_passwd {
+    eval 'require Term::ReadKey';
+    if ($@) {
+        die "No password specified (and Term::ReadKey not installed).\n";
+    }
 
+    print "Password: ";
+    Term::ReadKey::ReadMode('noecho');
+    chomp(my $passwd = Term::ReadKey::ReadLine(0));
+    Term::ReadKey::ReadMode('restore');
+    print "\n";
 
+    return $passwd;
+}
 
+sub vi {
+    my ($text) = @_;
+    my $file = "/tmp/rt.form.$$";
+    my $editor = $ENV{EDITOR} || $ENV{VISUAL} || "vi";
 
-# {{{ sub ParseBooleanOp
+    local *F;
+    local $/ = undef;
 
-=head2 ParseBooleanOp
+    open(F, ">$file") || die "$file: $!\n"; print F $text; close(F);
+    system($editor, $file) && die "Couldn't run $editor.\n";
+    open(F, $file) || die "$file: $!\n"; $text = <F>; close(F);
+    unlink($file);
 
-  Takes an option modifier. returns the apropriate SQL operator.
-  If it's handed ! or -, returns !=.  Otherwise returns =.
+    return $text;
+}
 
-=cut
+# Add a value to a (possibly multi-valued) hash key.
+sub vpush {
+    my ($hash, $key, $val) = @_;
+    my @val = ref $val eq 'ARRAY' ? @$val : $val;
 
-sub ParseBooleanOp {
-    
-    my $op = shift;
-    
-    #so that !new limits to not new, etc
-    if ($op =~ /^(\!|-)/) {
-       $op = "!=";
+    if (exists $hash->{$key}) {
+        unless (ref $hash->{$key} eq 'ARRAY') {
+            my @v = $hash->{$key} ne '' ? $hash->{$key} : ();
+            $hash->{$key} = \@v;
+        }
+        push @{ $hash->{$key} }, @val;
     }
     else {
-       $op = "=";
+        $hash->{$key} = $val;
     }
-    
-    return($op);
 }
 
-# }}}
+# "Normalise" a hash key that's known to be multi-valued.
+sub vsplit {
+    my ($val) = @_;
+    my ($word, @words);
+    my @values = ref $val eq 'ARRAY' ? @$val : $val;
+
+    foreach my $line (map {split /\n/} @values) {
+        # XXX: This should become a real parser, à la Text::ParseWords.
+        $line =~ s/^\s+//;
+        $line =~ s/\s+$//;
+        push @words, split /\s*,\s*/, $line;
+    }
+
+    return \@words;
+}
+
+sub expand_list {
+    my ($list) = @_;
+    my ($elt, @elts, %elts);
 
-# {{{ sub ParseLikeOp
-=head2 ParseLikeOp
+    foreach $elt (split /,/, $list) {
+        if ($elt =~ /^(\d+)-(\d+)$/) { push @elts, ($1..$2) }
+        else                         { push @elts, $elt }
+    }
 
-  Takes an option modifier. returns the apropriate SQL operator.
-  If it's handed ! or -, returns NOT  LIKE.  Otherwise returns LIKE
+    @elts{@elts}=();
+    return sort {$a<=>$b} keys %elts;
+}
 
-=cut
+sub get_type_argument {
+    my $type;
 
-sub ParseLikeOp {
-    
-    my $op = shift;
-    
-    #so that !new limits to not new, etc
-    if ($op =~ /^(\!|-)/) {
-       $op = "NOT LIKE";
+    if (@ARGV) {
+        $type = shift @ARGV;
+        unless ($type =~ /^[A-Za-z0-9_.-]+$/) {
+            # We want whine to mention our caller, not us.
+            @_ = ("Invalid type '$type' specified.");
+            goto &whine;
+        }
     }
     else {
-       $op = "LIKE";
+        @_ = ("No type argument specified with -t.");
+        goto &whine;
     }
-    
-    return($op);
+
+    $type =~ s/s$//; # "Plural". Ugh.
+    return $type;
 }
-# }}}
 
-# {{{ sub ParseDateToISO
+sub get_var_argument {
+    my ($data) = @_;
 
-=head2 ParseDateToISO
+    if (@ARGV) {
+        my $kv = shift @ARGV;
+        if (my ($k, $v) = $kv =~ /^($field)=(.*)$/) {
+            push @{ $data->{$k} }, $v;
+        }
+        else {
+            @_ = ("Invalid variable specification: '$kv'.");
+            goto &whine;
+        }
+    }
+    else {
+        @_ = ("No variable argument specified with -S.");
+        goto &whine;
+    }
+}
 
-Takes a date in an arbitrary format.
-Returns an ISO date and time in GMT
+sub is_object_spec {
+    my ($spec, $type) = @_;
 
-=cut
+    $spec =~ s|^(?:$type/)?|$type/| if defined $type;
+    return $spec if ($spec =~ m{^$name/(?:$idlist|$labels)(?:/.*)?$}o);
+    return;
+}
 
-sub ParseDateToISO {
-    my $date = shift;
+__DATA__
 
-       my $date_obj = new RT::Date($CurrentUser);
-       $date_obj->Set( Format => 'unknown',
-                       Value => $date
-                     );
-       return ($date_obj->ISO);
-}
+Title: intro
+Title: introduction
+Text:
 
-# }}}
+    ** THIS IS AN UNSUPPORTED PREVIEW RELEASE **
+    ** PLEASE REPORT BUGS TO rt-bugs@fsck.com **
 
-# {{{ sub ParseDateRange
+    This is a command-line interface to RT 3.
 
-=head2 ParseDateRange [RANGE]
+    It allows you to interact with an RT server over HTTP, and offers an
+    interface to RT's functionality that is better-suited to automation
+    and integration with other tools.
 
-Takes a range of dates of the form [<date>][-][<date>] and returns 
-starting and ending dates (as ISOs) If a date is specified as neither a starting nor ending 
-date, we parse it it as "midnight tonight to midnight tomorrow"
+    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.
 
-=cut
+    For more information:
 
-sub ParseDateRange {
-    my $in = shift;
-    my ($start, $end);
-    
-    
-    use RT::Date;
-    my $start_obj = new RT::Date($CurrentUser);
-    my $end_obj = new RT::Date($CurrentUser);
-    
-    if ($in =~ /^(.*?)-(.*?)$/) {
-       $start = $1;
-       $end = $2;
-
-       if ($start) {
-           $start_obj->Set(Format => 'unknown', 
-                           Value => $start);
-       }
-       if ($end) {
-           $end_obj->Set(Format => 'unknown', 
-                         Value => $end);
-       }
-    }
-    else {
-       $start = $in;
-       $end = $in;
-
-       $start_obj->Set(Format => 'unknown', 
-                       Value => $start);
-       
-       $end_obj->Set(Format => 'unknown', 
-                     Value => $end);
-       
-       $start_obj->SetToMidnight();
-       $end_obj->SetToMidnight();
-       $end_obj->AddDay();
-    }  
-    
-    if ($start) {
-       $start = $start_obj->ISO;
-    }
-    if ($end) {
-       $end = $end_obj->ISO;
-    }
+        - rt help actions       (a list of possible actions)
+        - rt help objects       (how to specify objects)
+        - rt help usage         (syntax information)
 
-    return ($start, $end);
-}
+        - rt help config        (configuration details)
+        - rt help examples      (a few useful examples)
+        - rt help topics        (a list of help topics)
 
-# }}}
+--
 
-# {{{ ParseRange
-=head2 ParseRange [RANGE]
+Title: usage
+Title: syntax
+Text:
 
-Takes a range of the form [<int>][-][<int>] and returns 
-a first and a last value. If the - is omitted, both $start and $end are the same.
-=cut
+    Syntax:
 
-sub ParseRange {
-    my $in = shift;
-    my ($start, $end);
-    
-    if ($in =~ /(.*?)-(.*?)/) {
-       $start = $1;
-       $end = $2;
-    }
-    else {
-       $start = $in;
-       $end = $in;
-    }  
-    
-    return ($start, $end);
-    
+        rt <action> [options] [arguments]
 
-    
-}
+    Each invocation of this program must specify an action (e.g. "edit",
+    "create"), options to modify behaviour, and other arguments required
+    by the specified action. (For example, most actions expect a list of
+    numeric object IDs to act upon.)
 
-# }}}
-         
-# {{{ sub ShowSummary 
-
-sub ShowSummary  {
-    my $Ticket = shift;
-
-
-    print <<EOFORM;
-Serial Number: @{[$Ticket->Id]}   Status:@{[$Ticket->Status]} Worked: @{[$Ticket->TimeWorked]} minutes  Queue:@{[$Ticket->QueueObj->Name]}
-      Subject: @{[$Ticket->Subject]}
-   Requestors: @{[$Ticket->RequestorsAsString]}
-           Cc: @{[$Ticket->CcAsString]}
-     Admin Cc: @{[$Ticket->AdminCcAsString]}
-        Owner: @{[$Ticket->OwnerObj->Name]}
-     Priority: @{[$Ticket->Priority]} / @{[$Ticket->FinalPriority]}
-          Due: @{[$Ticket->DueAsString]}
-      Created: @{[$Ticket->CreatedAsString]} (@{[$Ticket->AgeAsString]})
- Last Contact: @{[$Ticket->ToldAsString]} (@{[$Ticket->LongSinceToldAsString]})
-  Last Update: @{[$Ticket->LastUpdatedAsString]} by @{[$Ticket->LastUpdatedByObj->Name]}
-                
-EOFORM
-
-my $selects = $Ticket->QueueObj->KeywordSelects();
-    #get the keyword selects
-    print "Keywords:\n";
-    while (my $select = $selects->Next) {
-       print "\t" .$select->Name .": ";
-       my $keys = $Ticket->KeywordsObj($select->id);   
-       while (my $key = $keys->Next) {
-           print $key->KeywordObj->RelativePath($select->KeywordObj) . "  ";
-           
-       }       
-       print "\n";
-    }
+    The details of the syntax and arguments for each action are given by
+    "rt help <action>". Some actions may be referred to by more than one
+    name ("create" is the same as "new", for example).  
+
+    Objects are identified by a type and an ID (which can be a name or a
+    number, depending on the type). For some actions, the object type is
+    implied (you can only comment on tickets); for others, the user must
+    specify it explicitly. See "rt help objects" for details.
+
+    In syntax descriptions, mandatory arguments that must be replaced by
+    appropriate value are enclosed in <>, and optional arguments are
+    indicated by [] (for example, <action> and [options] above).
+
+    For more information:
+
+        - rt help objects       (how to specify objects)
+        - rt help actions       (a list of actions)
+        - rt help types         (a list of object types)
+
+--
+
+Title: conf
+Title: config
+Title: configuration
+Text:
+
+    This program has two major sources of configuration information: its
+    configuration files, and the environment.
+
+    The program looks for configuration directives in a file named .rtrc
+    (or $RTCONFIG; see below) in the current directory, and then in more
+    distant ancestors, until it reaches /. If no suitable configuration
+    files are found, it will also check for ~/.rtrc and /etc/rt.conf.
+
+    Configuration directives:
+
+        The following directives may occur, one per line:
+
+        - server <URL>          URL to RT server.
+        - user <username>       RT username.
+        - passwd <passwd>       RT user's password.
+
+        Blank and #-commented lines are ignored.
+
+    Environment variables:
+
+        The following environment variables override any corresponding
+        values defined in configuration files:
+
+        - RTUSER
+        - RTPASSWD
+        - RTSERVER
+        - RTDEBUG       Numeric debug level. (Set to 3 for full logs.)
+        - RTCONFIG      Specifies a name other than ".rtrc" for the
+                        configuration file.
+
+--
+
+Title: objects
+Text:
+
+    Syntax:
+
+        <type>/<id>[/<attributes>]
+
+    Every object in RT has a type (e.g. "ticket", "queue") and a numeric
+    ID. Some types of objects can also be identified by name (like users
+    and queues). Furthermore, objects may have named attributes (such as
+    "ticket/1/history").
+
+    An object specification is like a path in a virtual filesystem, with
+    object types as top-level directories, object IDs as subdirectories,
+    and named attributes as further subdirectories.
+
+    A comma-separated list of names, numeric IDs, or numeric ranges can
+    be used to specify more than one object of the same type. Note that
+    the list must be a single argument (i.e., no spaces). For example,
+    "user/root,1-3,5,7-10,ams" is a list of ten users; the same list
+    can also be written as "user/ams,root,1,2,3,5,7,8-20".
     
-#iterate through the keyword selects.
-#print the keyword select and all the related keywords
+    Examples:
 
+        ticket/1
+        ticket/1/attachments
+        ticket/1/attachments/3
+        ticket/1/attachments/3/content
+        ticket/1-3/links
+        ticket/1-3,5-7/history
 
+        user/ams
+        user/ams/rights
+        user/ams,rai,1/rights
 
-#TODO: finish link  descriptions
-print "Dependencies: \n";
-   while (my $l=$Ticket->DependedOnBy->Next) {
-       print $l->BaseObj->id," (",$l->BaseObj->Subject,") ",$l->Type," this ticket\n";
-   }
-   while (my $l=$Ticket->DependsOn->Next) {
-       print "This ticket ",$l->Type," ",$l->TargetObj->Id," (",$l->TargetObj->Subject,")\n";
-   }
-}
+    For more information:
 
-# }}}
-
-# {{{ sub ShowHistory 
-sub ShowHistory  {
-    my $Ticket = shift;
-    my $Transaction;    
-    my $Transactions = $Ticket->Transactions;
-
-    while ($Transaction = $Transactions->Next) {
-      &ShowTransaction($Transaction);
-    }   
-  }
-# }}}
-
-# {{{ sub ShowTransaction 
-sub ShowTransaction  {
-  my $transaction = shift;
-  
-print <<EOFORM;
-==========================================================================
-Date: @{[$transaction->CreatedAsString]} (@{[$transaction->TimeTaken]} minutes)
-@{[$transaction->Description]}
-EOFORM
-    ;
-  my $attachments=$transaction->Attachments();
-  while (my $message=$attachments->Next) {
-    print <<EOFORM;
---------------------------------------------------------------------------
-@{[$message->Headers]}
-EOFORM
-
-    if ($message->ContentType =~ m{^(text/plain|message|text$)}) {
-       print $message->Content;
-    } else {
-       print $message->ContentType, " not shown";
-    }
-  }
-  print "\n";
-  return();
-}
-# }}}
-
-
-# {{{ sub BuildListingFormat
-
-sub BuildListingFormat {
-    my $format_string = shift;
-
-    my ($id, @format, @code, @titles);
-    my ($field,$titles,$length, $format);
-
-    my $code = "";
-
-    # {{{ attribs
-    my $attribs = { id => { chars => '4',
-                           justify => 'r',
-                           title => 'id',
-                           value => '$Ticket->id',
-                         },
-                   
-                   queue => { chars => '8',
-                              justify => 'l',
-                              title => 'Queue',
-                              value => '$Ticket->QueueObj->Name' 
-                            },
-                   subject => { chars => '30',
-                                justify => 'l',
-                                title => 'Subject',
-                                value => '$Ticket->Subject',
-                              },
-                   priority => { chars => '2',
-                                 justify => 'r',
-                                 title => 'Pri',
-                                 value => '$Ticket->Priority',
-                               },
-                   final_priority => {  chars => '2',
-                                        justify => 'r',
-                                        title => 'Fin',
-                                        value => '$Ticket->FinalPriority',
-                                     },
-                   time_worked => { chars => '6',
-                                    justify => 'r',
-                                    title => 'Worked',
-                                    value => '$Ticket->TimeWorked',
-                                  },
-                   time_left => { chars => '5',
-                                  justify => 'r',
-                                  title => 'Left',
-                                  value => '$Ticket->TimeLeft',
-                              
-                                },
-               
-                   status => {  chars => '6',
-                                justify => 'r',
-                                title => 'Status',
-                                value => '$Ticket->Status',
-                             },
-                   owner => {  chars => '10',
-                               justify => 'r',
-                               title => 'Owner',
-                               value => '$Ticket->OwnerObj->Name'
-                            },
-                   requestor => {  chars => '10',
-                                   justify => 'r',
-                                   title => 'Requestor',
-                                   value => '$Ticket->RequestorsAsString'
-                                },
-                   created => {  chars => '12',
-                                 justify => 'r',
-                                 title => 'Created',
-                                 value => '$Ticket->CreatedAsString'
-                              },
-                   updated => {  chars => '12',
-                                 justify => 'r',
-                                 title => 'Updated',
-                                 value => '$Ticket->LastUpdatedAsString'
-                              },
-                   due => {  chars => '12',
-                             justify => 'r',
-                             title => 'Due',
-                             value => '$Ticket->DueAsString'
-                          },
-                   told => {  chars => '12',
-                              justify => 'r',
-                              title => 'Told',
-                              value => '$Ticket->ToldAsString'
-                           },
-               
-               
-               
-                 };
-
-    # }}}
+        - rt help <action>      (action-specific details)
+        - rt help <type>        (type-specific details)
+
+--
+
+Title: actions
+Title: commands
+Text:
+
+    You can currently perform the following actions on all objects:
+
+        - list          (list objects matching some condition)
+        - show          (display object details)
+        - edit          (edit object details)
+        - create        (create a new object)
+
+    Each type may define actions specific to itself; these are listed in
+    the help item about that type.
+
+    For more information:
+
+        - rt help <action>      (action-specific details)
+        - rt help types         (a list of possible types)
+
+--
+
+Title: types
+Text:
+
+    You can currently operate on the following types of objects:
+
+        - tickets
+        - users
+        - groups
+        - queues
+
+    For more information:
+
+        - rt help <type>        (type-specific details)
+        - rt help objects       (how to specify objects)
+        - rt help actions       (a list of possible actions)
+
+--
+
+Title: ticket
+Text:
+
+    Tickets are identified by a numeric ID.
+
+    The following generic operations may be performed upon tickets:
+
+        - list
+        - show
+        - edit
+        - create
+
+    In addition, the following ticket-specific actions exist:
+
+        - link
+        - merge
+        - comment
+        - correspond
+
+    Attributes:
+
+        The following attributes can be used with "rt show" or "rt edit"
+        to retrieve or edit other information associated with tickets:
+
+        links                      A ticket's relationships with others.
+        history                    All of a ticket's transactions.
+        history/type/<type>        Only a particular type of transaction.
+        history/id/<id>            Only the transaction of the specified id.
+        attachments                A list of attachments.
+        attachments/<id>           The metadata for an individual attachment.
+        attachments/<id>/content   The content of an individual attachment.
+
+--
+
+Title: user
+Title: group
+Text:
+
+    Users and groups are identified by name or numeric ID.
+
+    The following generic operations may be performed upon them:
+
+        - list
+        - show
+        - 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
+Text:
+
+    Queues are identified by name or numeric ID.
+
+    Currently, they can be subjected to the following actions:
+
+        - show
+        - edit
+        - create
+
+--
+
+Title: logout
+Text:
+
+    Syntax:
+
+        rt logout
+
+    Terminates the currently established login session. You will need to
+    provide authentication credentials before you can continue using the
+    server. (See "rt help config" for details about authentication.)
+
+--
+
+Title: ls
+Title: list
+Title: search
+Text:
+
+    Syntax:
+
+        rt <ls|list|search> [options] "query string"
+
+    Displays a list of objects matching the specified conditions.
+    ("ls", "list", and "search" are synonyms.)
+
+    Conditions are expressed in the SQL-like syntax used internally by
+    RT3. (For more information, see "rt help query".) The query string
+    must be supplied as one argument.
+
+    (Right now, the server doesn't support listing anything but tickets.
+    Other types will be supported in future; this client will be able to
+    take advantage of that support without any changes.)
+
+    Options:
+
+        The following options control how much information is displayed
+        about each matching object:
+
+        -i      Numeric IDs only. (Useful for |rt edit -; see examples.)
+        -s      Short description.
+        -l      Longer description.
+
+        In addition,
+        
+        -o +/-<field>   Orders the returned list by the specified field.
+        -S var=val      Submits the specified variable with the request.
+        -t type         Specifies the type of object to look for. (The
+                        default is "ticket".)
+
+    Examples:
+
+        rt ls "Priority > 5 and Status='new'"
+        rt ls -o +Subject "Priority > 5 and Status='new'"
+        rt ls -o -Created "Priority > 5 and Status='new'"
+        rt ls -i "Priority > 5"|rt edit - set status=resolved
+        rt ls -t ticket "Subject like '[PATCH]%'"
+
+--
+
+Title: show
+Text:
+
+    Syntax:
+
+        rt show [options] <object-ids>
+
+    Displays details of the specified objects.
+
+    For some types, object information is further classified into named
+    attributes (for example, "1-3/links" is a valid ticket specification
+    that refers to the links for tickets 1-3). Consult "rt help <type>"
+    and "rt help objects" for further details.
+
+    This command writes a set of forms representing the requested object
+    data to STDOUT.
+
+    Options:
+
+        -               Read IDs from STDIN instead of the command-line.
+        -t type         Specifies object type.
+        -f a,b,c        Restrict the display to the specified fields.
+        -S var=val      Submits the specified variable with the request.
+
+    Examples:
+
+        rt show -t ticket -f id,subject,status 1-3
+        rt show ticket/3/attachments/29
+        rt show ticket/3/attachments/29/content
+        rt show ticket/1-3/links
+        rt show -t user 2
+
+--
+
+Title: new
+Title: edit
+Title: create
+Text:
+
+    Syntax:
+
+        rt edit [options] <object-ids> set field=value [field=value] ...
+                                       add field=value [field=value] ...
+                                       del field=value [field=value] ...
+
+    Edits information corresponding to the specified objects.
+
+    If, instead of "edit", an action of "new" or "create" is specified,
+    then a new object is created. In this case, no numeric object IDs
+    may be specified, but the syntax and behaviour remain otherwise
+    unchanged.
+
+    This command typically starts an editor to allow you to edit object
+    data in a form for submission. If you specified enough information
+    on the command-line, however, it will make the submission directly.
+
+    The command line may specify field-values in three different ways.
+    "set" sets the named field to the given value, "add" adds a value
+    to a multi-valued field, and "del" deletes the corresponding value.
+    Each "field=value" specification must be given as a single argument.
+
+    For some types, object information is further classified into named
+    attributes (for example, "1-3/links" is a valid ticket specification
+    that refers to the links for tickets 1-3). These attributes may also
+    be edited. Consult "rt help <type>" and "rt help object" for further
+    details.
+
+    Options:
+
+        -       Read numeric IDs from STDIN instead of the command-line.
+                (Useful with rt ls ... | rt edit -; see examples below.)
+        -i      Read a completed form from STDIN before submitting.
+        -o      Dump the completed form to STDOUT instead of submitting.
+        -e      Allows you to edit the form even if the command-line has
+                enough information to make a submission directly.
+        -S var=val
+                Submits the specified variable with the request.
+        -t type Specifies object type.
+
+    Examples:
+
+        # Interactive (starts $EDITOR with a form).
+        rt edit ticket/3
+        rt create -t ticket
+
+        # Non-interactive.
+        rt edit ticket/1-3 add cc=foo@example.com set priority=3
+        rt ls -t tickets -i 'Priority > 5' | rt edit - set status=resolved
+        rt edit ticket/4 set priority=3 owner=bar@example.com \
+                         add cc=foo@example.com bcc=quux@example.net
+        rt create -t ticket subject='new ticket' priority=10 \
+                            add cc=foo@example.com
+
+--
+
+Title: comment
+Title: correspond
+Text:
+
+    Syntax:
+
+        rt <comment|correspond> [options] <ticket-id>
+
+    Adds a comment (or correspondence) to the specified ticket (the only
+    difference being that comments aren't sent to the requestors.)
+
+    This command will typically start an editor and allow you to type a
+    comment into a form. If, however, you specified all the necessary
+    information on the command line, it submits the comment directly.
+
+    (See "rt help forms" for more information about forms.)
+
+    Options:
+
+        -m <text>       Specify comment text.
+        -a <file>       Attach a file to the comment. (May be used more
+                        than once to attach multiple files.)
+        -c <addrs>      A comma-separated list of Cc addresses.
+        -b <addrs>      A comma-separated list of Bcc addresses.
+        -w <time>       Specify the time spent working on this ticket.
+        -e              Starts an editor before the submission, even if
+                        arguments from the command line were sufficient.
+
+    Examples:
+
+        rt comment -t 'Not worth fixing.' -a stddisclaimer.h 23
+
+--
+
+Title: merge
+Text:
+
+    Syntax:
+
+        rt merge <from-id> <to-id>
+
+    Merges the two specified tickets.
+
+--
+
+Title: link
+Text:
+
+    Syntax:
+
+        rt link [-d] <id-A> <relationship> <id-B>
+
+    Creates (or, with -d, deletes) a link between the specified tickets.
+    The relationship can (irrespective of case) be any of:
+
+        DependsOn/DependedOnBy:     A depends upon B (or vice versa).
+        RefersTo/ReferredToBy:      A refers to B (or vice versa).
+        MemberOf/HasMember:         A is a member of B (or vice versa).
+
+    To view a ticket's relationships, use "rt show ticket/3/links". (See
+    "rt help ticket" and "rt help show".)
+
+    Options:
+
+        -d      Deletes the specified link.
+
+    Examples:
+
+        rt link 2 dependson 3
+        rt link -d 4 referredtoby 6     # 6 no longer refers to 4
+
+--
+
+Title: grant
+Title: revoke
+Text:
+
+--
+
+Title: query
+Text:
+
+    RT3 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?)
 
-    foreach $field (split ('%',$format_string)) {
-       
-       if ($field =~ /^(\D*?)(\d*?)$/) {
-           $id = $1;
-           $length = $2;
-       }
-       else {  
-           $RT::Logger->debug ("Error parsing $field\n");
-       }
-       if ($length) {
-           push (@format, "%".$length.".".$length."s ");
-           
-           push (@code,  $attribs->{"$id"}->{'value'});
-                 
-           push (@titles, "'". $attribs->{"$id"}->{title}. "'");
-       }
-       
-       
-    }
-     $code = join (',', @code);
-     $format = join (" ", @format);
-     $titles = join (', ', @titles);
+--
+
+Title: form
+Title: forms
+Text:
+
+    This program uses RFC822 header-style forms to represent object data
+    in a form that's suitable for processing both by humans and scripts.
+
+    A form is a set of (field, value) specifications, with some initial
+    commented text and interspersed blank lines allowed for convenience.
+    Field names may appear more than once in a form; a comma-separated
+    list of multiple field values may also be specified directly.
     
-  
-    return ($format, $titles, $code);
-}
+    Field values can be wrapped as in RFC822, with leading whitespace.
+    The longest sequence of leading whitespace common to all the lines
+    is removed (preserving further indentation). There is no limit on
+    the length of a value.
+
+    Multiple forms are separated by a line containing only "--\n".
+
+    (XXX: A more detailed specification will be provided soon. For now,
+    the server-side syntax checking will suffice.)
+
+--
+
+Title: topics
+Text:
+
+    Use "rt help <topic>" for help on any of the following subjects:
+
+        - tickets, users, groups, queues.
+        - show, edit, ls/list/search, new/create.
+
+        - query                                 (search query syntax)
+        - forms                                 (form specification)
+
+        - objects                               (how to specify objects)
+        - types                                 (a list of object types)
+        - actions/commands                      (a list of actions)
+        - usage/syntax                          (syntax details)
+        - conf/config/configuration             (configuration details)
+        - examples                              (a few useful examples)
+
+--
 
-# }}}
+Title: example
+Title: examples
+Text:
 
+    This section will be filled in with useful examples, once it becomes
+    more clear what examples may be useful.
 
+    For the moment, please consult examples provided with each action.
 
-1;
+--
index ede874a..cdbc3cb 100644 (file)
@@ -197,7 +197,7 @@ sub help {
       )
       . "\n\n";
 
-    print " sbin/cron_shim \\\n";
+    print " bin/rt-cron-tool \\\n";
     print
       "  --search RT::Search::ActiveTicketsInQueue  --search-arg general \\\n";
     print
@@ -205,6 +205,16 @@ sub help {
     print "  --action RT::Action::SetPriority --action-arg 99 \\\n";
     print "  --verbose\n";
 
+    print "\n";
+    print loc("Escalate tickets");
+    print "rt-crontool \\\n";
+    print " --search RT::Search::ActiveTicketsInQueue  --search-arg thequeuename \\\n";
+    print " --action RT::Action::EscalatePriority \\\n";
+
+
 
     exit(0);
 }
index 73b80aa..8ecc718 100644 (file)
@@ -197,7 +197,7 @@ sub help {
       )
       . "\n\n";
 
-    print " sbin/cron_shim \\\n";
+    print " bin/rt-cron-tool \\\n";
     print
       "  --search RT::Search::ActiveTicketsInQueue  --search-arg general \\\n";
     print
@@ -205,6 +205,16 @@ sub help {
     print "  --action RT::Action::SetPriority --action-arg 99 \\\n";
     print "  --verbose\n";
 
+    print "\n";
+    print loc("Escalate tickets");
+    print "rt-crontool \\\n";
+    print " --search RT::Search::ActiveTicketsInQueue  --search-arg thequeuename \\\n";
+    print " --action RT::Action::EscalatePriority \\\n";
+
+
 
     exit(0);
 }
index b304436..8af8002 100755 (executable)
@@ -1,26 +1,26 @@
 #!/usr/bin/perl -w
 # BEGIN LICENSE BLOCK
-#
+# 
 # Copyright (c) 1996-2003 Jesse Vincent <jesse@bestpractical.com>
-#
+# 
 # (Except where explictly superceded by other copyright notices)
-#
+# 
 # This work is made available to you under the terms of Version 2 of
 # the GNU General Public License. A copy of that license should have
 # been provided with this software, but in any event can be snarfed
 # from www.gnu.org.
-#
+# 
 # This work is distributed in the hope that it will be useful, but
 # WITHOUT ANY WARRANTY; without even the implied warranty of
 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 # General Public License for more details.
-#
+# 
 # Unless otherwise specified, all modifications, corrections or
 # extensions to this work which alter its source code become the
 # property of Best Practical Solutions, LLC when submitted for
 # inclusion in the work.
-#
-#
+# 
+# 
 # END LICENSE BLOCK
 
 =head1 NAME
@@ -31,10 +31,25 @@ rt-mailgate - Mail interface to RT3.
 
 use RT::I18N;
 
+# Make sure that when we call the mailgate wrong, it tempfails
+
+ok(open(MAIL, "|/opt/rt3/bin/rt-mailgate --url http://bad.address"), "Opened the mailgate - The error below is expected - $@");
+print MAIL <<EOF;
+From: root\@localhost
+To: rt\@example.com
+Subject: This is a test of new ticket creation
+
+Foob!
+EOF
+close (MAIL);
+
+# Check the return value
+is ( $? >> 8, 75, "The error message above is expected The mail gateway exited with a failure. yay");
+
 
 # {{{ Test new ticket creation by root who is privileged and superuser
 
-ok(open(MAIL, "|/opt/rt3/bin/rt-mailgate --url http://localhost/ --queue general --action correspond"), "Opened the mailgate - $@");
+ok(open(MAIL, "|/opt/rt3/bin/rt-mailgate --url http://localhost".$RT::WebPath."/ --queue general --action correspond"), "Opened the mailgate - $@");
 print MAIL <<EOF;
 From: root\@localhost
 To: rt\@example.com
@@ -45,6 +60,9 @@ Foob!
 EOF
 close (MAIL);
 
+#Check the return value
+is ($? >> 8, 0, "The mail gateway exited normally. yay");
+
 use RT::Tickets;
 my $tickets = RT::Tickets->new($RT::SystemUser);
 $tickets->OrderBy(FIELD => 'id', ORDER => 'DESC');
@@ -59,7 +77,7 @@ ok ($tick->Subject eq 'This is a test of new ticket creation', "Created the tick
 
 # {{{This is a test of new ticket creation as an unknown user
 
-ok(open(MAIL, "|/opt/rt3/bin/rt-mailgate --url http://localhost/ --queue general --action correspond"), "Opened the mailgate - $@");
+ok(open(MAIL, "|/opt/rt3/bin/rt-mailgate --url http://localhost".$RT::WebPath."/ --queue general --action correspond"), "Opened the mailgate - $@");
 print MAIL <<EOF;
 From: doesnotexist\@example.com
 To: rt\@example.com
@@ -69,6 +87,8 @@ Blah!
 Foob!
 EOF
 close (MAIL);
+#Check the return value
+is ($? >> 8, 0, "The mail gateway exited normally. yay");
 
 $tickets = RT::Tickets->new($RT::SystemUser);
 $tickets->OrderBy(FIELD => 'id', ORDER => 'DESC');
@@ -94,7 +114,7 @@ ok ($val, "Granted everybody the right to create tickets - $msg");
 
 sleep(60); # gotta sleep so the remote process' ACL cache times out
 
-ok(open(MAIL, "|/opt/rt3/bin/rt-mailgate --url http://localhost/ --queue general --action correspond"), "Opened the mailgate - $@");
+ok(open(MAIL, "|/opt/rt3/bin/rt-mailgate --url http://localhost".$RT::WebPath."/ --queue general --action correspond"), "Opened the mailgate - $@");
 print MAIL <<EOF;
 From: doesnotexist\@example.com
 To: rt\@example.com
@@ -104,6 +124,8 @@ Blah!
 Foob!
 EOF
 close (MAIL);
+#Check the return value
+is ($? >> 8, 0, "The mail gateway exited normally. yay");
 
 
 $tickets = RT::Tickets->new($RT::SystemUser);
@@ -126,7 +148,7 @@ ok( $u->Id != 0, " user does not exist and was created by ticket submission");
 #ok ($val, "Granted everybody the right to create tickets - $msg");
 #sleep(60); # gotta sleep so the remote process' ACL cache times out
 
-ok(open(MAIL, "|/opt/rt3/bin/rt-mailgate --url http://localhost/ --queue general --action correspond"), "Opened the mailgate - $@");
+ok(open(MAIL, "|/opt/rt3/bin/rt-mailgate --url http://localhost".$RT::WebPath."/ --queue general --action correspond"), "Opened the mailgate - $@");
 print MAIL <<EOF;
 From: doesnotexist-2\@example.com
 To: rt\@example.com
@@ -136,6 +158,8 @@ Blah!
 Foob!
 EOF
 close (MAIL);
+#Check the return value
+is ($? >> 8, 0, "The mail gateway exited normally. yay");
 
 $u = RT::User->new($RT::SystemUser);
 $u->Load('doesnotexist-2@example.com');
@@ -148,7 +172,7 @@ ok( $u->Id == 0, " user does not exist and was not created by ticket corresponde
 ok ($val, "Granted everybody the right to reply to  tickets - $msg");
 sleep(60); # gotta sleep so the remote process' ACL cache times out
 
-ok(open(MAIL, "|/opt/rt3/bin/rt-mailgate --url http://localhost/ --queue general --action correspond"), "Opened the mailgate - $@");
+ok(open(MAIL, "|/opt/rt3/bin/rt-mailgate --url http://localhost".$RT::WebPath."/ --queue general --action correspond"), "Opened the mailgate - $@");
 print MAIL <<EOF;
 From: doesnotexist-2\@example.com
 To: rt\@example.com
@@ -158,6 +182,8 @@ Blah!
 Foob!
 EOF
 close (MAIL);
+#Check the return value
+is ($? >> 8, 0, "The mail gateway exited normally. yay");
 
 
 $u = RT::User->new($RT::SystemUser);
@@ -173,7 +199,7 @@ ok( $u->Id != 0, " user exists and was created by ticket correspondence submissi
 #ok ($val, "Granted everybody the right to create tickets - $msg");
 #sleep(60); # gotta sleep so the remote process' ACL cache times out
 
-ok(open(MAIL, "|/opt/rt3/bin/rt-mailgate --url http://localhost/ --queue general --action comment"), "Opened the mailgate - $@");
+ok(open(MAIL, "|/opt/rt3/bin/rt-mailgate --url http://localhost".$RT::WebPath."/ --queue general --action comment"), "Opened the mailgate - $@");
 print MAIL <<EOF;
 From: doesnotexist-3\@example.com
 To: rt\@example.com
@@ -184,6 +210,9 @@ Foob!
 EOF
 close (MAIL);
 
+#Check the return value
+is ($? >> 8, 0, "The mail gateway exited normally. yay");
+
 $u = RT::User->new($RT::SystemUser);
 $u->Load('doesnotexist-3@example.com');
 ok( $u->Id == 0, " user does not exist and was not created by ticket comment submission");
@@ -196,7 +225,7 @@ ok( $u->Id == 0, " user does not exist and was not created by ticket comment sub
 ok ($val, "Granted everybody the right to reply to  tickets - $msg");
 sleep(60); # gotta sleep so the remote process' ACL cache times out
 
-ok(open(MAIL, "|/opt/rt3/bin/rt-mailgate --url http://localhost/ --queue general --action comment"), "Opened the mailgate - $@");
+ok(open(MAIL, "|/opt/rt3/bin/rt-mailgate --url http://localhost".$RT::WebPath."/ --queue general --action comment"), "Opened the mailgate - $@");
 print MAIL <<EOF;
 From: doesnotexist-3\@example.com
 To: rt\@example.com
@@ -207,6 +236,8 @@ Foob!
 EOF
 close (MAIL);
 
+#Check the return value
+is ($? >> 8, 0, "The mail gateway exited normally. yay");
 
 $u = RT::User->new($RT::SystemUser);
 $u->Load('doesnotexist-3@example.com');
@@ -227,17 +258,20 @@ my $entity = MIME::Entity->build( From => 'root@localhost',
                                 Data => ['This is a test of a binary attachment']);
 
 # currently in lib/t/autogen
-$entity->attach(Path => '../../../html/NoAuth/images/spacer.gif', 
+$entity->attach(Path => '/opt/rt3/share/html/NoAuth/images/spacer.gif', 
                 Type => 'image/gif',
                 Encoding => 'base64');
 
 # Create a ticket with a binary attachment
-ok(open(MAIL, "|/opt/rt3/bin/rt-mailgate --url http://localhost/ --queue general --action correspond"), "Opened the mailgate - $@");
+ok(open(MAIL, "|/opt/rt3/bin/rt-mailgate --url http://localhost".$RT::WebPath."/ --queue general --action correspond"), "Opened the mailgate - $@");
 
 $entity->print(\*MAIL);
 
 close (MAIL);
 
+#Check the return value
+is ($? >> 8, 0, "The mail gateway exited normally. yay");
+
 my $tickets = RT::Tickets->new($RT::SystemUser);
 $tickets->OrderBy(FIELD => 'id', ORDER => 'DESC');
 $tickets->Limit(FIELD => 'id', OPERATOR => '>', VALUE => '0');
@@ -273,7 +307,7 @@ use LWP::UserAgent;
 # Grab the binary attachment via the web ui
 my $ua      = LWP::UserAgent->new();
 
-my $full_url = "http://localhost/Ticket/Attachment/".$attachment->TransactionId."/".$attachment->id."/spacer.gif?&user=root&pass=password";
+my $full_url = "http://localhost".$RT::WebPath."/Ticket/Attachment/".$attachment->TransactionId."/".$attachment->id."/spacer.gif?&user=root&pass=password";
 my $r = $ua->get( $full_url);
 
 
@@ -286,7 +320,7 @@ is($file, $r->content, 'The attachment isn\'t screwed up in download');
 
 # {{{ Simple I18N testing
 
-ok(open(MAIL, "|/opt/rt3/bin/rt-mailgate --url http://localhost/ --queue general --action correspond"), "Opened the mailgate - $@");
+ok(open(MAIL, "|/opt/rt3/bin/rt-mailgate --url http://localhost".$RT::WebPath."/ --queue general --action correspond"), "Opened the mailgate - $@");
                                                                          
 print MAIL <<EOF;
 From: root\@localhost
@@ -301,6 +335,9 @@ bye
 EOF
 close (MAIL);
 
+#Check the return value
+is ($? >> 8, 0, "The mail gateway exited normally. yay");
+
 my $unitickets = RT::Tickets->new($RT::SystemUser);
 $unitickets->OrderBy(FIELD => 'id', ORDER => 'DESC');
 $unitickets->Limit(FIELD => 'id', OPERATOR => '>', VALUE => '0');
@@ -317,7 +354,7 @@ is ($unitick->Transactions->First->Content, $unitick->Transactions->First->Attac
 ok($unitick->Transactions->First->Attachments->First->Content =~ /$unistring/i, $unitick->Id." appears to be unicode ". $unitick->Transactions->First->Attachments->First->Id);
 # supposedly I18N fails on the second message sent in.
 
-ok(open(MAIL, "|/opt/rt3/bin/rt-mailgate --url http://localhost/ --queue general --action correspond"), "Opened the mailgate - $@");
+ok(open(MAIL, "|/opt/rt3/bin/rt-mailgate --url http://localhost".$RT::WebPath."/ --queue general --action correspond"), "Opened the mailgate - $@");
                                                                          
 print MAIL <<EOF;
 From: root\@localhost
@@ -332,6 +369,9 @@ bye
 EOF
 close (MAIL);
 
+#Check the return value
+is ($? >> 8, 0, "The mail gateway exited normally. yay");
+
 my $tickets2 = RT::Tickets->new($RT::SystemUser);
 $tickets2->OrderBy(FIELD => 'id', ORDER => 'DESC');
 $tickets2->Limit(FIELD => 'id', OPERATOR => '>', VALUE => '0');
@@ -367,7 +407,7 @@ use LWP::UserAgent;
 use constant EX_TEMPFAIL => 75;
 
 my %opts;
-GetOptions( \%opts, "queue=s", "action=s", "url=s", "jar=s", "help", "debug", "extension=s" );
+GetOptions( \%opts, "queue=s", "action=s", "url=s", "jar=s", "help", "debug", "extension=s", "timeout=i" );
 
 if ( $opts{help} ) {
     require Pod::Usage;
@@ -381,17 +421,18 @@ for (qw(url)) {
 }
 
 undef $/;
-my $message = <>;
 my $ua      = LWP::UserAgent->new();
 $ua->cookie_jar( { file => $opts{jar} } );
 
 my %args = (
     queue   => $opts{queue},
     action  => $opts{action},
-    message => $message,
     SessionType => 'REST',    # Surpress login box
 );
 
+# Read the message in from STDIN
+$args{'message'} = <>;
+
 
 if ($opts{'extension'}) {
         $args{$opts{'extension'}} = $ENV{'EXTENSION'};
@@ -404,6 +445,7 @@ warn "Connecting to $full_url" if $opts{'debug'};
 
 
 
+$ua->timeout(exists($opts{'timeout'}) ? $opts{'timeout'} : 180);
 my $r = $ua->post( $full_url, {%args} );
 check_failure($r);
 
@@ -414,7 +456,7 @@ if ( $content !~ /^(ok|not ok)/ ) {
 
     # It's not the server's fault if the mail is bogus. We just want to know that
     # *something* came out of the server.
-    die <<EOF
+    warn <<EOF;
 RT server error.
 
 The RT server which handled your email did not behave as expected. It
@@ -423,8 +465,13 @@ said:
 $content
 EOF
 
+exit EX_TEMPFAIL;
+
 }
 
+exit;
+
+
 sub check_failure {
     my $r = shift;
     return if $r->is_success();
@@ -455,7 +502,11 @@ Usual invocation (from MTA):
 
     rt-mailgate --action (correspond|comment) --queue queuename
                 --url http://your.rt.server/
-                [ --extension (queue|action|ticket)
+                [ --debug ]
+                [ --extension (queue|action|ticket) ]
+                [ --timeout seconds ]
+
+
 
 See C<man rt-mailgate> for more.
 
@@ -486,6 +537,16 @@ submitted to will be set to the value of $EXTENSION. By specifying
 is related to.  "action" will allow the user to specify either "comment" or
 "correspond" in the address extension.
 
+=item C<--debug> OPTIONAL
+
+Print debugging output to standard error
+
+
+=item C<--timeout> OPTIONAL
+
+Configure the timeout for posting the message to the web server.  The
+default timeout is 3 minutes (180 seconds).
+
 
 =head1 DESCRIPTION
 
index 304fcbc..2ddb604 100644 (file)
@@ -1,26 +1,26 @@
 #!@PERL@ -w
 # BEGIN LICENSE BLOCK
-#
+# 
 # Copyright (c) 1996-2003 Jesse Vincent <jesse@bestpractical.com>
-#
+# 
 # (Except where explictly superceded by other copyright notices)
-#
+# 
 # This work is made available to you under the terms of Version 2 of
 # the GNU General Public License. A copy of that license should have
 # been provided with this software, but in any event can be snarfed
 # from www.gnu.org.
-#
+# 
 # This work is distributed in the hope that it will be useful, but
 # WITHOUT ANY WARRANTY; without even the implied warranty of
 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 # General Public License for more details.
-#
+# 
 # Unless otherwise specified, all modifications, corrections or
 # extensions to this work which alter its source code become the
 # property of Best Practical Solutions, LLC when submitted for
 # inclusion in the work.
-#
-#
+# 
+# 
 # END LICENSE BLOCK
 
 =head1 NAME
@@ -31,10 +31,25 @@ rt-mailgate - Mail interface to RT3.
 
 use RT::I18N;
 
+# Make sure that when we call the mailgate wrong, it tempfails
+
+ok(open(MAIL, "|@RT_BIN_PATH@/rt-mailgate --url http://bad.address"), "Opened the mailgate - The error below is expected - $@");
+print MAIL <<EOF;
+From: root\@localhost
+To: rt\@example.com
+Subject: This is a test of new ticket creation
+
+Foob!
+EOF
+close (MAIL);
+
+# Check the return value
+is ( $? >> 8, 75, "The error message above is expected The mail gateway exited with a failure. yay");
+
 
 # {{{ Test new ticket creation by root who is privileged and superuser
 
-ok(open(MAIL, "|@RT_BIN_PATH@/rt-mailgate --url http://localhost/ --queue general --action correspond"), "Opened the mailgate - $@");
+ok(open(MAIL, "|@RT_BIN_PATH@/rt-mailgate --url http://localhost".$RT::WebPath."/ --queue general --action correspond"), "Opened the mailgate - $@");
 print MAIL <<EOF;
 From: root\@localhost
 To: rt\@example.com
@@ -45,6 +60,9 @@ Foob!
 EOF
 close (MAIL);
 
+#Check the return value
+is ($? >> 8, 0, "The mail gateway exited normally. yay");
+
 use RT::Tickets;
 my $tickets = RT::Tickets->new($RT::SystemUser);
 $tickets->OrderBy(FIELD => 'id', ORDER => 'DESC');
@@ -59,7 +77,7 @@ ok ($tick->Subject eq 'This is a test of new ticket creation', "Created the tick
 
 # {{{This is a test of new ticket creation as an unknown user
 
-ok(open(MAIL, "|@RT_BIN_PATH@/rt-mailgate --url http://localhost/ --queue general --action correspond"), "Opened the mailgate - $@");
+ok(open(MAIL, "|@RT_BIN_PATH@/rt-mailgate --url http://localhost".$RT::WebPath."/ --queue general --action correspond"), "Opened the mailgate - $@");
 print MAIL <<EOF;
 From: doesnotexist\@example.com
 To: rt\@example.com
@@ -69,6 +87,8 @@ Blah!
 Foob!
 EOF
 close (MAIL);
+#Check the return value
+is ($? >> 8, 0, "The mail gateway exited normally. yay");
 
 $tickets = RT::Tickets->new($RT::SystemUser);
 $tickets->OrderBy(FIELD => 'id', ORDER => 'DESC');
@@ -94,7 +114,7 @@ ok ($val, "Granted everybody the right to create tickets - $msg");
 
 sleep(60); # gotta sleep so the remote process' ACL cache times out
 
-ok(open(MAIL, "|@RT_BIN_PATH@/rt-mailgate --url http://localhost/ --queue general --action correspond"), "Opened the mailgate - $@");
+ok(open(MAIL, "|@RT_BIN_PATH@/rt-mailgate --url http://localhost".$RT::WebPath."/ --queue general --action correspond"), "Opened the mailgate - $@");
 print MAIL <<EOF;
 From: doesnotexist\@example.com
 To: rt\@example.com
@@ -104,6 +124,8 @@ Blah!
 Foob!
 EOF
 close (MAIL);
+#Check the return value
+is ($? >> 8, 0, "The mail gateway exited normally. yay");
 
 
 $tickets = RT::Tickets->new($RT::SystemUser);
@@ -126,7 +148,7 @@ ok( $u->Id != 0, " user does not exist and was created by ticket submission");
 #ok ($val, "Granted everybody the right to create tickets - $msg");
 #sleep(60); # gotta sleep so the remote process' ACL cache times out
 
-ok(open(MAIL, "|@RT_BIN_PATH@/rt-mailgate --url http://localhost/ --queue general --action correspond"), "Opened the mailgate - $@");
+ok(open(MAIL, "|@RT_BIN_PATH@/rt-mailgate --url http://localhost".$RT::WebPath."/ --queue general --action correspond"), "Opened the mailgate - $@");
 print MAIL <<EOF;
 From: doesnotexist-2\@example.com
 To: rt\@example.com
@@ -136,6 +158,8 @@ Blah!
 Foob!
 EOF
 close (MAIL);
+#Check the return value
+is ($? >> 8, 0, "The mail gateway exited normally. yay");
 
 $u = RT::User->new($RT::SystemUser);
 $u->Load('doesnotexist-2@example.com');
@@ -148,7 +172,7 @@ ok( $u->Id == 0, " user does not exist and was not created by ticket corresponde
 ok ($val, "Granted everybody the right to reply to  tickets - $msg");
 sleep(60); # gotta sleep so the remote process' ACL cache times out
 
-ok(open(MAIL, "|@RT_BIN_PATH@/rt-mailgate --url http://localhost/ --queue general --action correspond"), "Opened the mailgate - $@");
+ok(open(MAIL, "|@RT_BIN_PATH@/rt-mailgate --url http://localhost".$RT::WebPath."/ --queue general --action correspond"), "Opened the mailgate - $@");
 print MAIL <<EOF;
 From: doesnotexist-2\@example.com
 To: rt\@example.com
@@ -158,6 +182,8 @@ Blah!
 Foob!
 EOF
 close (MAIL);
+#Check the return value
+is ($? >> 8, 0, "The mail gateway exited normally. yay");
 
 
 $u = RT::User->new($RT::SystemUser);
@@ -173,7 +199,7 @@ ok( $u->Id != 0, " user exists and was created by ticket correspondence submissi
 #ok ($val, "Granted everybody the right to create tickets - $msg");
 #sleep(60); # gotta sleep so the remote process' ACL cache times out
 
-ok(open(MAIL, "|@RT_BIN_PATH@/rt-mailgate --url http://localhost/ --queue general --action comment"), "Opened the mailgate - $@");
+ok(open(MAIL, "|@RT_BIN_PATH@/rt-mailgate --url http://localhost".$RT::WebPath."/ --queue general --action comment"), "Opened the mailgate - $@");
 print MAIL <<EOF;
 From: doesnotexist-3\@example.com
 To: rt\@example.com
@@ -184,6 +210,9 @@ Foob!
 EOF
 close (MAIL);
 
+#Check the return value
+is ($? >> 8, 0, "The mail gateway exited normally. yay");
+
 $u = RT::User->new($RT::SystemUser);
 $u->Load('doesnotexist-3@example.com');
 ok( $u->Id == 0, " user does not exist and was not created by ticket comment submission");
@@ -196,7 +225,7 @@ ok( $u->Id == 0, " user does not exist and was not created by ticket comment sub
 ok ($val, "Granted everybody the right to reply to  tickets - $msg");
 sleep(60); # gotta sleep so the remote process' ACL cache times out
 
-ok(open(MAIL, "|@RT_BIN_PATH@/rt-mailgate --url http://localhost/ --queue general --action comment"), "Opened the mailgate - $@");
+ok(open(MAIL, "|@RT_BIN_PATH@/rt-mailgate --url http://localhost".$RT::WebPath."/ --queue general --action comment"), "Opened the mailgate - $@");
 print MAIL <<EOF;
 From: doesnotexist-3\@example.com
 To: rt\@example.com
@@ -207,6 +236,8 @@ Foob!
 EOF
 close (MAIL);
 
+#Check the return value
+is ($? >> 8, 0, "The mail gateway exited normally. yay");
 
 $u = RT::User->new($RT::SystemUser);
 $u->Load('doesnotexist-3@example.com');
@@ -227,17 +258,20 @@ my $entity = MIME::Entity->build( From => 'root@localhost',
                                 Data => ['This is a test of a binary attachment']);
 
 # currently in lib/t/autogen
-$entity->attach(Path => '../../../html/NoAuth/images/spacer.gif', 
+$entity->attach(Path => '@MASON_HTML_PATH@/NoAuth/images/spacer.gif', 
                 Type => 'image/gif',
                 Encoding => 'base64');
 
 # Create a ticket with a binary attachment
-ok(open(MAIL, "|@RT_BIN_PATH@/rt-mailgate --url http://localhost/ --queue general --action correspond"), "Opened the mailgate - $@");
+ok(open(MAIL, "|@RT_BIN_PATH@/rt-mailgate --url http://localhost".$RT::WebPath."/ --queue general --action correspond"), "Opened the mailgate - $@");
 
 $entity->print(\*MAIL);
 
 close (MAIL);
 
+#Check the return value
+is ($? >> 8, 0, "The mail gateway exited normally. yay");
+
 my $tickets = RT::Tickets->new($RT::SystemUser);
 $tickets->OrderBy(FIELD => 'id', ORDER => 'DESC');
 $tickets->Limit(FIELD => 'id', OPERATOR => '>', VALUE => '0');
@@ -273,7 +307,7 @@ use LWP::UserAgent;
 # Grab the binary attachment via the web ui
 my $ua      = LWP::UserAgent->new();
 
-my $full_url = "http://localhost/Ticket/Attachment/".$attachment->TransactionId."/".$attachment->id."/spacer.gif?&user=root&pass=password";
+my $full_url = "http://localhost".$RT::WebPath."/Ticket/Attachment/".$attachment->TransactionId."/".$attachment->id."/spacer.gif?&user=root&pass=password";
 my $r = $ua->get( $full_url);
 
 
@@ -286,7 +320,7 @@ is($file, $r->content, 'The attachment isn\'t screwed up in download');
 
 # {{{ Simple I18N testing
 
-ok(open(MAIL, "|@RT_BIN_PATH@/rt-mailgate --url http://localhost/ --queue general --action correspond"), "Opened the mailgate - $@");
+ok(open(MAIL, "|@RT_BIN_PATH@/rt-mailgate --url http://localhost".$RT::WebPath."/ --queue general --action correspond"), "Opened the mailgate - $@");
                                                                          
 print MAIL <<EOF;
 From: root\@localhost
@@ -301,6 +335,9 @@ bye
 EOF
 close (MAIL);
 
+#Check the return value
+is ($? >> 8, 0, "The mail gateway exited normally. yay");
+
 my $unitickets = RT::Tickets->new($RT::SystemUser);
 $unitickets->OrderBy(FIELD => 'id', ORDER => 'DESC');
 $unitickets->Limit(FIELD => 'id', OPERATOR => '>', VALUE => '0');
@@ -317,7 +354,7 @@ is ($unitick->Transactions->First->Content, $unitick->Transactions->First->Attac
 ok($unitick->Transactions->First->Attachments->First->Content =~ /$unistring/i, $unitick->Id." appears to be unicode ". $unitick->Transactions->First->Attachments->First->Id);
 # supposedly I18N fails on the second message sent in.
 
-ok(open(MAIL, "|@RT_BIN_PATH@/rt-mailgate --url http://localhost/ --queue general --action correspond"), "Opened the mailgate - $@");
+ok(open(MAIL, "|@RT_BIN_PATH@/rt-mailgate --url http://localhost".$RT::WebPath."/ --queue general --action correspond"), "Opened the mailgate - $@");
                                                                          
 print MAIL <<EOF;
 From: root\@localhost
@@ -332,6 +369,9 @@ bye
 EOF
 close (MAIL);
 
+#Check the return value
+is ($? >> 8, 0, "The mail gateway exited normally. yay");
+
 my $tickets2 = RT::Tickets->new($RT::SystemUser);
 $tickets2->OrderBy(FIELD => 'id', ORDER => 'DESC');
 $tickets2->Limit(FIELD => 'id', OPERATOR => '>', VALUE => '0');
@@ -367,7 +407,7 @@ use LWP::UserAgent;
 use constant EX_TEMPFAIL => 75;
 
 my %opts;
-GetOptions( \%opts, "queue=s", "action=s", "url=s", "jar=s", "help", "debug", "extension=s" );
+GetOptions( \%opts, "queue=s", "action=s", "url=s", "jar=s", "help", "debug", "extension=s", "timeout=i" );
 
 if ( $opts{help} ) {
     require Pod::Usage;
@@ -381,17 +421,18 @@ for (qw(url)) {
 }
 
 undef $/;
-my $message = <>;
 my $ua      = LWP::UserAgent->new();
 $ua->cookie_jar( { file => $opts{jar} } );
 
 my %args = (
     queue   => $opts{queue},
     action  => $opts{action},
-    message => $message,
     SessionType => 'REST',    # Surpress login box
 );
 
+# Read the message in from STDIN
+$args{'message'} = <>;
+
 
 if ($opts{'extension'}) {
         $args{$opts{'extension'}} = $ENV{'EXTENSION'};
@@ -404,6 +445,7 @@ warn "Connecting to $full_url" if $opts{'debug'};
 
 
 
+$ua->timeout(exists($opts{'timeout'}) ? $opts{'timeout'} : 180);
 my $r = $ua->post( $full_url, {%args} );
 check_failure($r);
 
@@ -414,7 +456,7 @@ if ( $content !~ /^(ok|not ok)/ ) {
 
     # It's not the server's fault if the mail is bogus. We just want to know that
     # *something* came out of the server.
-    die <<EOF
+    warn <<EOF;
 RT server error.
 
 The RT server which handled your email did not behave as expected. It
@@ -423,8 +465,13 @@ said:
 $content
 EOF
 
+exit EX_TEMPFAIL;
+
 }
 
+exit;
+
+
 sub check_failure {
     my $r = shift;
     return if $r->is_success();
@@ -455,7 +502,11 @@ Usual invocation (from MTA):
 
     rt-mailgate --action (correspond|comment) --queue queuename
                 --url http://your.rt.server/
-                [ --extension (queue|action|ticket)
+                [ --debug ]
+                [ --extension (queue|action|ticket) ]
+                [ --timeout seconds ]
+
+
 
 See C<man rt-mailgate> for more.
 
@@ -486,6 +537,16 @@ submitted to will be set to the value of $EXTENSION. By specifying
 is related to.  "action" will allow the user to specify either "comment" or
 "correspond" in the address extension.
 
+=item C<--debug> OPTIONAL
+
+Print debugging output to standard error
+
+
+=item C<--timeout> OPTIONAL
+
+Configure the timeout for posting the message to the web server.  The
+default timeout is 3 minutes (180 seconds).
+
 
 =head1 DESCRIPTION
 
diff --git a/rt/bin/rt.in b/rt/bin/rt.in
new file mode 100644 (file)
index 0000000..90369b5
--- /dev/null
@@ -0,0 +1,1816 @@
+#!@PERL@ -w
+# BEGIN LICENSE BLOCK
+# 
+# Copyright (c) 1996-2003 Jesse Vincent <jesse@bestpractical.com>
+# 
+# (Except where explictly superceded by other copyright notices)
+# 
+# This work is made available to you under the terms of Version 2 of
+# the GNU General Public License. A copy of that license should have
+# been provided with this software, but in any event can be snarfed
+# from www.gnu.org.
+# 
+# This work is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# General Public License for more details.
+# 
+# Unless otherwise specified, all modifications, corrections or
+# extensions to this work which alter its source code become the
+# property of Best Practical Solutions, LLC when submitted for
+# inclusion in the work.
+# 
+# 
+# END LICENSE BLOCK
+
+use strict;
+
+# This program is intentionally written to have as few non-core module
+# dependencies as possible. It should stay that way.
+
+use Cwd;
+use LWP;
+use HTTP::Request::Common;
+
+# We derive configuration information from hardwired defaults, dotfiles,
+# and the RT* environment variables (in increasing order of precedence).
+# Session information is stored in ~/.rt_sessions.
+
+my $VERSION = 0.02;
+my $HOME = eval{(getpwuid($<))[7]}
+           || $ENV{HOME} || $ENV{LOGDIR} || $ENV{HOMEPATH}
+           || ".";
+my %config = (
+    (
+        debug   => 0,
+        user    => eval{(getpwuid($<))[0]} || $ENV{USER} || $ENV{USERNAME},
+        passwd  => undef,
+        server  => 'http://localhost/rt/',
+    ),
+    config_from_file($ENV{RTCONFIG} || ".rtrc"),
+    config_from_env()
+);
+my $session = new Session("$HOME/.rt_sessions");
+my $REST = "$config{server}/REST/1.0";
+
+sub whine;
+sub DEBUG { warn @_ if $config{debug} >= shift }
+
+# These regexes are used by command handlers to parse arguments.
+# (XXX: Ask Autrijus how i18n changes these definitions.)
+
+my $name   = '[\w.-]+';
+my $field  = '[a-zA-Z][a-zA-Z0-9_-]*';
+my $label  = '[a-zA-Z0-9@_.+-]+';
+my $labels = "(?:$label,)*$label";
+my $idlist = '(?:(?:\d+-)?\d+,)*(?:\d+-)?\d+';
+
+# Our command line looks like this:
+#
+#     rt <action> [options] [arguments]
+#
+# We'll parse just enough of it to decide upon an action to perform, and
+# leave the rest to per-action handlers to interpret appropriately.
+
+my %handlers = (
+#   handler     => [ ...aliases... ],
+    version     => ["version", "ver"],
+    logout      => ["logout"],
+    help        => ["help", "man"],
+    show        => ["show", "cat"],
+    edit        => ["create", "edit", "new", "ed"],
+    list        => ["search", "list", "ls"],
+    comment     => ["comment", "correspond"],
+    link        => ["link", "ln"],
+    merge       => ["merge"],
+    grant       => ["grant", "revoke"],
+);
+
+# Once we find and call an appropriate handler, we're done.
+
+my (%actions, $action);
+foreach my $fn (keys %handlers) {
+    foreach my $alias (@{ $handlers{$fn} }) {
+        $actions{$alias} = \&{"$fn"};
+    }
+}
+if (@ARGV && exists $actions{$ARGV[0]}) {
+    $action = shift @ARGV;
+}
+$actions{$action || "help"}->($action || ());
+exit;
+
+# Handler functions.
+# ------------------
+#
+# The following subs are handlers for each entry in %actions.
+
+sub version {
+    print "rt $VERSION\n";
+}
+
+sub logout {
+    submit("$REST/logout") if defined $session->cookie;
+}
+
+sub help {
+    my ($action, $type) = @_;
+    my (%help, $key);
+
+    # What help topics do we know about?
+    local $/ = undef;
+    foreach my $item (@{ Form::parse(<DATA>) }) {
+        my $title = $item->[2]{Title};
+        my @titles = ref $title eq 'ARRAY' ? @$title : $title;
+
+        foreach $title (grep $_, @titles) {
+            $help{$title} = $item->[2]{Text};
+        }
+    }
+
+    # What does the user want help with?
+    undef $action if ($action && $actions{$action} eq \&help);
+    unless ($action || $type) {
+        # If we don't know, we'll look for clues in @ARGV.
+        foreach (@ARGV) {
+            if (exists $help{$_}) { $key = $_; last; }
+        }
+        unless ($key) {
+            # Tolerate possibly plural words.
+            foreach (@ARGV) {
+                if ($_ =~ s/s$// && exists $help{$_}) { $key = $_; last; }
+            }
+        }
+    }
+
+    if ($type && $action) {
+        $key = "$type.$action";
+    }
+    $key ||= $type || $action || "introduction";
+
+    # Find a suitable topic to display.
+    while (!exists $help{$key}) {
+        if ($type && $action) {
+            if ($key eq "$type.$action") { $key = $action;        }
+            elsif ($key eq $action)      { $key = $type;          }
+            else                         { $key = "introduction"; }
+        }
+        else {
+            $key = "introduction";
+        }
+    }
+
+    print STDERR $help{$key}, "\n\n";
+}
+
+# Displays a list of objects that match some specified condition.
+
+sub list {
+    my ($q, $type, %data, $orderby);
+    my $bad = 0;
+
+    while (@ARGV) {
+        $_ = shift @ARGV;
+
+        if (/^-t$/) {
+            $bad = 1, last unless defined($type = get_type_argument());
+        }
+        elsif (/^-S$/) {
+            $bad = 1, last unless get_var_argument(\%data);
+        }
+        elsif (/^-o$/) {
+            $orderby = shift @ARGV;
+        }
+        elsif (/^-([isl])$/) {
+            $data{format} = $1;
+        }
+        elsif (/^-f$/) {
+            if ($ARGV[0] !~ /^(?:(?:$field,)*$field)$/) {
+                whine "No valid field list in '-f $ARGV[0]'.";
+                $bad = 1; last;
+            }
+            $data{fields} = shift @ARGV;
+        }
+        elsif (!defined $q && !/^-/) {
+            $q = $_;
+        }
+        else {
+            my $datum = /^-/ ? "option" : "argument";
+            whine "Unrecognised $datum '$_'.";
+            $bad = 1; last;
+        }
+    }
+
+    $type ||= "ticket";
+    unless ($type && defined $q) {
+        my $item = $type ? "query string" : "object type";
+        whine "No $item specified.";
+        $bad = 1;
+    }
+    return help("list", $type) if $bad;
+
+    my $r = submit("$REST/search/$type", { query => $q, %data, orderby => $orderby || "" });
+    print $r->content;
+}
+
+# Displays selected information about a single object.
+
+sub show {
+    my ($type, @objects, %data);
+    my $slurped = 0;
+    my $bad = 0;
+
+    while (@ARGV) {
+        $_ = shift @ARGV;
+
+        if (/^-t$/) {
+            $bad = 1, last unless defined($type = get_type_argument());
+        }
+        elsif (/^-S$/) {
+            $bad = 1, last unless get_var_argument(\%data);
+        }
+        elsif (/^-([isl])$/) {
+            $data{format} = $1;
+        }
+        elsif (/^-$/ && !$slurped) {
+            chomp(my @lines = <STDIN>);
+            foreach (@lines) {
+                unless (is_object_spec($_, $type)) {
+                    whine "Invalid object on STDIN: '$_'.";
+                    $bad = 1; last;
+                }
+                push @objects, $_;
+            }
+            $slurped = 1;
+        }
+        elsif (/^-f$/) {
+            if ($ARGV[0] !~ /^(?:(?:$field,)*$field)$/) {
+                whine "No valid field list in '-f $ARGV[0]'.";
+                $bad = 1; last;
+            }
+            $data{fields} = shift @ARGV;
+        }
+        elsif (my $spec = is_object_spec($_, $type)) {
+            push @objects, $spec;
+        }
+        else {
+            my $datum = /^-/ ? "option" : "argument";
+            whine "Unrecognised $datum '$_'.";
+            $bad = 1; last;
+        }
+    }
+
+    unless (@objects) {
+        whine "No objects specified.";
+        $bad = 1;
+    }
+    return help("show", $type) if $bad;
+
+    my $r = submit("$REST/show", { id => \@objects, %data });
+    print $r->content;
+}
+
+# To create a new object, we ask the server for a form with the defaults
+# filled in, allow the user to edit it, and send the form back.
+#
+# To edit an object, we must ask the server for a form representing that
+# object, make changes requested by the user (either on the command line
+# or interactively via $EDITOR), and send the form back.
+
+sub edit {
+    my ($action) = @_;
+    my (%data, $type, @objects);
+    my ($cl, $text, $edit, $input, $output);
+
+    use vars qw(%set %add %del);
+    %set = %add = %del = ();
+    my $slurped = 0;
+    my $bad = 0;
+    
+    while (@ARGV) {
+        $_ = shift @ARGV;
+
+        if    (/^-e$/) { $edit = 1 }
+        elsif (/^-i$/) { $input = 1 }
+        elsif (/^-o$/) { $output = 1 }
+        elsif (/^-t$/) {
+            $bad = 1, last unless defined($type = get_type_argument());
+        }
+        elsif (/^-S$/) {
+            $bad = 1, last unless get_var_argument(\%data);
+        }
+        elsif (/^-$/ && !($slurped || $input)) {
+            chomp(my @lines = <STDIN>);
+            foreach (@lines) {
+                unless (is_object_spec($_, $type)) {
+                    whine "Invalid object on STDIN: '$_'.";
+                    $bad = 1; last;
+                }
+                push @objects, $_;
+            }
+            $slurped = 1;
+        }
+        elsif (/^set$/i) {
+            my $vars = 0;
+
+            while (@ARGV && $ARGV[0] =~ /^($field)([+-]?=)(.*)$/) {
+                my ($key, $op, $val) = ($1, $2, $3);
+                my $hash = ($op eq '=') ? \%set : ($op =~ /^\+/) ? \%add : \%del;
+
+                vpush($hash, lc $key, $val);
+                shift @ARGV;
+                $vars++;
+            }
+            unless ($vars) {
+                whine "No variables to set.";
+                $bad = 1; last;
+            }
+            $cl = $vars;
+        }
+        elsif (/^(?:add|del)$/i) {
+            my $vars = 0;
+            my $hash = ($_ eq "add") ? \%add : \%del;
+
+            while (@ARGV && $ARGV[0] =~ /^($field)=(.*)$/) {
+                my ($key, $val) = ($1, $2);
+
+                vpush($hash, lc $key, $val);
+                shift @ARGV;
+                $vars++;
+            }
+            unless ($vars) {
+                whine "No variables to set.";
+                $bad = 1; last;
+            }
+            $cl = $vars;
+        }
+        elsif (my $spec = is_object_spec($_, $type)) {
+            push @objects, $spec;
+        }
+        else {
+            my $datum = /^-/ ? "option" : "argument";
+            whine "Unrecognised $datum '$_'.";
+            $bad = 1; last;
+        }
+    }
+
+    if ($action =~ /^ed(?:it)?$/) {
+        unless (@objects) {
+            whine "No objects specified.";
+            $bad = 1;
+        }
+    }
+    else {
+        if (@objects) {
+            whine "You shouldn't specify objects as arguments to $action.";
+            $bad = 1;
+        }
+        unless ($type) {
+            whine "What type of object do you want to create?";
+            $bad = 1;
+        }
+        @objects = ("$type/new");
+    }
+    return help($action, $type) if $bad;
+
+    # We need a form to make changes to. We usually ask the server for
+    # one, but we can avoid that if we are fed one on STDIN, or if the
+    # user doesn't want to edit the form by hand, and the command line
+    # specifies only simple variable assignments.
+
+    if ($input) {
+        local $/ = undef;
+        $text = <STDIN>;
+    }
+    elsif ($edit || %add || %del || !$cl) {
+        my $r = submit("$REST/show", { id => \@objects, format => 'l' });
+        $text = $r->content;
+    }
+
+    # If any changes were specified on the command line, apply them.
+    if ($cl) {
+        if ($text) {
+            # We're updating forms from the server.
+            my $forms = Form::parse($text);
+
+            foreach my $form (@$forms) {
+                my ($c, $o, $k, $e) = @$form;
+                my ($key, $val);
+
+                next if ($e || !@$o);
+
+                local %add = %add;
+                local %del = %del;
+                local %set = %set;
+
+                # Make changes to existing fields.
+                foreach $key (@$o) {
+                    if (exists $add{lc $key}) {
+                        $val = delete $add{lc $key};
+                        vpush($k, $key, $val);
+                        $k->{$key} = vsplit($k->{$key}) if $val =~ /[,\n]/;
+                    }
+                    if (exists $del{lc $key}) {
+                        $val = delete $del{lc $key};
+                        my %val = map {$_=>1} @{ vsplit($val) };
+                        $k->{$key} = vsplit($k->{$key});
+                        @{$k->{$key}} = grep {!exists $val{$_}} @{$k->{$key}};
+                    }
+                    if (exists $set{lc $key}) {
+                        $k->{$key} = delete $set{lc $key};
+                    }
+                }
+                
+                # Then update the others.
+                foreach $key (keys %set) { vpush($k, $key, $set{$key}) }
+                foreach $key (keys %add) {
+                    vpush($k, $key, $add{$key});
+                    $k->{$key} = vsplit($k->{$key});
+                }
+                push @$o, (keys %add, keys %set);
+            }
+
+            $text = Form::compose($forms);
+        }
+        else {
+            # We're rolling our own set of forms.
+            my @forms;
+            foreach (@objects) {
+                my ($type, $ids, $args) =
+                    m{^($name)/($idlist|$labels)(?:(/.*))?$}o;
+
+                $args ||= "";
+                foreach my $obj (expand_list($ids)) {
+                    my %set = (%set, id => "$type/$obj$args");
+                    push @forms, ["", [keys %set], \%set];
+                }
+            }
+            $text = Form::compose(\@forms);
+        }
+    }
+
+    if ($output) {
+        print $text;
+        exit;
+    }
+
+    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 ($edit || (!$input && !$cl)) {
+        my $newtext = vi($text);
+        # We won't resubmit a bad form unless it was changed.
+        $text = ($synerr && $newtext eq $text) ? undef : $newtext;
+    }
+
+    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;
+                $synerr = 1;
+                goto EDIT;
+            }
+            else {
+                print $r->content;
+                exit -1;
+            }
+        }
+        print $r->content;
+    }
+}
+
+# We roll "comment" and "correspond" into the same handler.
+
+sub comment {
+    my ($action) = @_;
+    my (%data, $id, @files, @bcc, @cc, $msg, $wtime, $edit);
+    my $bad = 0;
+
+    while (@ARGV) {
+        $_ = shift @ARGV;
+
+        if (/^-e$/) {
+            $edit = 1;
+        }
+        elsif (/^-[abcmw]$/) {
+            unless (@ARGV) {
+                whine "No argument specified with $_.";
+                $bad = 1; last;
+            }
+
+            if (/-a/) {
+                unless (-f $ARGV[0] && -r $ARGV[0]) {
+                    whine "Cannot read attachment: '$ARGV[0]'.";
+                    exit -1;
+                }
+                push @files, shift @ARGV;
+            }
+            elsif (/-([bc])/) {
+                my $a = $_ eq "-b" ? \@bcc : \@cc;
+                @$a = split /\s*,\s*/, shift @ARGV;
+            }
+            elsif (/-m/) { $msg = shift @ARGV }
+            elsif (/-w/) { $wtime = shift @ARGV }
+        }
+        elsif (!$id && m|^(?:ticket/)?($idlist)$|) {
+            $id = $1;
+        }
+        else {
+            my $datum = /^-/ ? "option" : "argument";
+            whine "Unrecognised $datum '$_'.";
+            $bad = 1; last;
+        }
+    }
+
+    unless ($id) {
+        whine "No object specified.";
+        $bad = 1;
+    }
+    return help($action, "ticket") if $bad;
+
+    my $form = [
+        "",
+        [ "Ticket", "Action", "Cc", "Bcc", "Attachment", "TimeWorked", "Text" ],
+        {
+            Ticket     => $id,
+            Action     => $action,
+            Cc         => [ @cc ],
+            Bcc        => [ @bcc ],
+            Attachment => [ @files ],
+            TimeWorked => $wtime || '',
+            Text       => $msg || '',
+        }
+    ];
+
+    my $text = Form::compose([ $form ]);
+
+    if ($edit || !$msg) {
+        my $error = 0;
+        my ($c, $o, $k, $e);
+
+        do {
+            my $ntext = vi($text);
+            exit if ($error && $ntext eq $text);
+            $text = $ntext;
+            $form = Form::parse($text);
+            $error = 0;
+
+            ($c, $o, $k, $e) = @{ $form->[0] };
+            if ($e) {
+                $error = 1;
+                $c = "# Syntax error.";
+                goto NEXT;
+            }
+            elsif (!@$o) {
+                exit;
+            }
+            @files = @{ vsplit($k->{Attachment}) };
+
+        NEXT:
+            $text = Form::compose([[$c, $o, $k, $e]]);
+        } while ($error);
+    }
+
+    my $i = 1;
+    foreach my $file (@files) {
+        $data{"attachment_$i"} = bless([ $file ], "Attachment");
+        $i++;
+    }
+    $data{content} = $text;
+
+    my $r = submit("$REST/ticket/comment/$id", \%data);
+    print $r->content;
+}
+
+# Merge one ticket into another.
+
+sub merge {
+    my @id;
+    my $bad = 0;
+
+    while (@ARGV) {
+        $_ = shift @ARGV;
+
+        if (/^\d+$/) {
+            push @id, $_;
+        }
+        else {
+            whine "Unrecognised argument: '$_'.";
+            $bad = 1; last;
+        }
+    }
+
+    unless (@id == 2) {
+        my $evil = @id > 2 ? "many" : "few";
+        whine "Too $evil arguments specified.";
+        $bad = 1;
+    }
+    return help("merge", "ticket") if $bad;
+
+    my $r = submit("$REST/ticket/merge/$id[0]", {into => $id[1]});
+    print $r->content;
+}
+
+# Link one ticket to another.
+
+sub link {
+    my ($bad, $del, %data) = (0, 0, ());
+    my %ltypes = map { lc $_ => $_ } qw(DependsOn DependedOnBy RefersTo
+                                        ReferredToBy HasMember MemberOf);
+
+    while (@ARGV && $ARGV[0] =~ /^-/) {
+        $_ = shift @ARGV;
+
+        if (/^-d$/) {
+            $del = 1;
+        }
+        else {
+            whine "Unrecognised option: '$_'.";
+            $bad = 1; last;
+        }
+    }
+
+    if (@ARGV == 3) {
+        my ($from, $rel, $to) = @ARGV;
+        if ($from !~ /^\d+$/ || $to !~ /^\d+$/) {
+            my $bad = $from =~ /^\d+$/ ? $to : $from;
+            whine "Invalid ticket ID '$bad' specified.";
+            $bad = 1;
+        }
+        unless (exists $ltypes{lc $rel}) {
+            whine "Invalid relationship '$rel' specified.";
+            $bad = 1;
+        }
+        %data = (id => $from, rel => $rel, to => $to, del => $del);
+    }
+    else {
+        my $bad = @ARGV < 3 ? "few" : "many";
+        whine "Too $bad arguments specified.";
+        $bad = 1;
+    }
+    return help("link", "ticket") if $bad;
+
+    my $r = submit("$REST/ticket/link", \%data);
+    print $r->content;
+}
+
+# Grant/revoke a user's rights.
+
+sub grant {
+    my ($cmd) = @_;
+
+    my $revoke = 0;
+    while (@ARGV) {
+    }
+
+    $revoke = 1 if $cmd->{action} eq 'revoke';
+}
+
+# Client <-> Server communication.
+# --------------------------------
+#
+# This function composes and sends an HTTP request to the RT server, and
+# interprets the response. It takes a request URI, and optional request
+# data (a string, or a reference to a set of key-value pairs).
+
+sub submit {
+    my ($uri, $content) = @_;
+    my ($req, $data);
+    my $ua = new LWP::UserAgent(agent => "RT/3.0b", env_proxy => 1);
+
+    # Did the caller specify any data to send with the request?
+    $data = [];
+    if (defined $content) {
+        unless (ref $content) {
+            # If it's just a string, make sure LWP handles it properly.
+            # (By pretending that it's a file!)
+            $content = [ content => [undef, "", Content => $content] ];
+        }
+        elsif (ref $content eq 'HASH') {
+            my @data;
+            foreach my $k (keys %$content) {
+                if (ref $content->{$k} eq 'ARRAY') {
+                    foreach my $v (@{ $content->{$k} }) {
+                        push @data, $k, $v;
+                    }
+                }
+                else { push @data, $k, $content->{$k} }
+            }
+            $content = \@data;
+        }
+        $data = $content;
+    }
+
+    # Should we send authentication information to start a new session?
+    if (!defined $session->cookie) {
+        push @$data, ( user => $config{user} );
+        push @$data, ( pass => $config{passwd} || read_passwd() );
+    }
+
+    # Now, we construct the request.
+    if (@$data) {
+        $req = POST($uri, $data, Content_Type => 'form-data');
+    }
+    else {
+        $req = GET($uri);
+    }
+    $session->add_cookie_header($req);
+
+    # Then we send the request and parse the response.
+    DEBUG(3, $req->as_string);
+    my $res = $ua->request($req);
+    DEBUG(3, $res->as_string);
+
+    if ($res->is_success) {
+        # The content of the response we get from the RT server consists
+        # of an HTTP-like status line followed by optional header lines,
+        # a blank line, and arbitrary text.
+
+        my ($head, $text) = split /\n\n/, $res->content, 2;
+        my ($status, @headers) = split /\n/, $head;
+        $text =~ s/\n*$/\n/;
+
+        # "RT/3.0.1 401 Credentials required"
+        if ($status !~ m#^RT/\d+(?:\.\d+)+(?:-?\w+)? (\d+) ([\w\s]+)$#) {
+            warn "rt: Malformed RT response from $config{server}.\n";
+            warn "(Rerun with RTDEBUG=3 for details.)\n" if $config{debug} < 3;
+            exit -1;
+        }
+
+        # Our caller can pretend that the server returned a custom HTTP
+        # response code and message. (Doing that directly is apparently
+        # not sufficiently portable and uncomplicated.)
+        $res->code($1);
+        $res->message($2);
+        $res->content($text);
+        $session->update($res) if ($res->is_success || $res->code != 401);
+
+        if (!$res->is_success) {
+            # We can deal with authentication failures ourselves. Either
+            # we sent invalid credentials, or our session has expired.
+            if ($res->code == 401) {
+                my %d = @$data;
+                if (exists $d{user}) {
+                    warn "rt: Incorrect username or password.\n";
+                    exit -1;
+                }
+                elsif ($req->header("Cookie")) {
+                    # We'll retry the request with credentials, unless
+                    # we only wanted to logout in the first place.
+                    $session->delete;
+                    return submit(@_) unless $uri eq "$REST/logout";
+                }
+            }
+            # Conflicts should be dealt with by the handler and user.
+            # For anything else, we just die.
+            elsif ($res->code != 409) {
+                warn "rt: ", $res->content;
+                exit;
+            }
+        }
+    }
+    else {
+        warn "rt: Server error: ", $res->message, " (", $res->code, ")\n";
+        exit -1;
+    }
+
+    return $res;
+}
+
+# Session management.
+# -------------------
+#
+# Maintains a list of active sessions in the ~/.rt_sessions file.
+{
+    package Session;
+    my ($s, $u);
+
+    # Initialises the session cache.
+    sub new {
+        my ($class, $file) = @_;
+        my $self = {
+            file => $file || "$HOME/.rt_sessions",
+            sids => { }
+        };
+       
+        # The current session is identified by the currently configured
+        # server and user.
+        ($s, $u) = @config{"server", "user"};
+
+        bless $self, $class;
+        $self->load();
+
+        return $self;
+    }
+
+    # Returns the current session cookie.
+    sub cookie {
+        my ($self) = @_;
+        my $cookie = $self->{sids}{$s}{$u};
+        return defined $cookie ? "RT_SID=$cookie" : undef;
+    }
+
+    # Deletes the current session cookie.
+    sub delete {
+        my ($self) = @_;
+        delete $self->{sids}{$s}{$u};
+    }
+
+    # Adds a Cookie header to an outgoing HTTP request.
+    sub add_cookie_header {
+        my ($self, $request) = @_;
+        my $cookie = $self->cookie();
+
+        $request->header(Cookie => $cookie) if defined $cookie;
+    }
+
+    # Extracts the Set-Cookie header from an HTTP response, and updates
+    # session information accordingly.
+    sub update {
+        my ($self, $response) = @_;
+        my $cookie = $response->header("Set-Cookie");
+
+        if (defined $cookie && $cookie =~ /^RT_SID=([0-9A-Fa-f]+);/) {
+            $self->{sids}{$s}{$u} = $1;
+        }
+    }
+
+    # Loads the session cache from the specified 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+ [0-9A-Fa-f]+$#;
+                my ($server, $user, $cookie) = split / /, $_;
+                $sids->{$server}{$user} = $cookie;
+            }
+            return 1;
+        };
+        return 0;
+    }
+
+    # 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";
+                    }
+                }
+            }
+            close(F);
+            chmod 0600, $file;
+            return 1;
+        };
+        return 0;
+    }
+
+    sub DESTROY {
+        my $self = shift;
+        $self->save;
+    }
+}
+
+# Form handling.
+# --------------
+#
+# Forms are RFC822-style sets of (field, value) specifications with some
+# initial comments and interspersed blank lines allowed for convenience.
+# Sets of forms are separated by --\n (in a cheap parody of MIME).
+#
+# Each form is parsed into an array with four elements: commented text
+# at the start of the form, an array with the order of keys, a hash with
+# key/value pairs, and optional error text if the form syntax was wrong.
+
+# Returns a reference to an array of parsed forms.
+sub Form::parse {
+    my $state = 0;
+    my @forms = ();
+    my @lines = split /\n/, $_[0];
+    my ($c, $o, $k, $e) = ("", [], {}, "");
+
+    LINE:
+    while (@lines) {
+        my $line = shift @lines;
+
+        next LINE if $line eq '';
+
+        if ($line eq '--') {
+            # We reached the end of one form. We'll ignore it if it was
+            # empty, and store it otherwise, errors and all.
+            if ($e || $c || @$o) {
+                push @forms, [ $c, $o, $k, $e ];
+                $c = ""; $o = []; $k = {}; $e = "";
+            }
+            $state = 0;
+        }
+        elsif ($state != -1) {
+            if ($state == 0 && $line =~ /^#/) {
+                # Read an optional block of comments (only) at the start
+                # of the form.
+                $state = 1;
+                $c = $line;
+                while (@lines && $lines[0] =~ /^#/) {
+                    $c .= "\n".shift @lines;
+                }
+                $c .= "\n";
+            }
+            elsif ($state <= 1 && $line =~ /^($field):(?:\s+(.*))?$/) {
+                # Read a field: value specification.
+                my $f  = $1;
+                my @v  = ($2 || ());
+
+                # Read continuation lines, if any.
+                while (@lines && ($lines[0] eq '' || $lines[0] =~ /^\s+/)) {
+                    push @v, shift @lines;
+                }
+                pop @v while (@v && $v[-1] eq '');
+
+                # Strip longest common leading indent from text.
+                my $ws = "";
+                foreach my $ls (map {/^(\s+)/} @v[1..$#v]) {
+                    $ws = $ls if (!$ws || length($ls) < length($ws));
+                }
+                s/^$ws// foreach @v;
+
+                push(@$o, $f) unless exists $k->{$f};
+                vpush($k, $f, join("\n", @v));
+
+                $state = 1;
+            }
+            elsif ($line !~ /^#/) {
+                # We've found a syntax error, so we'll reconstruct the
+                # form parsed thus far, and add an error marker. (>>)
+                $state = -1;
+                $e = Form::compose([[ "", $o, $k, "" ]]);
+                $e.= $line =~ /^>>/ ? "$line\n" : ">> $line\n";
+            }
+        }
+        else {
+            # We saw a syntax error earlier, so we'll accumulate the
+            # contents of this form until the end.
+            $e .= "$line\n";
+        }
+    }
+    push(@forms, [ $c, $o, $k, $e ]) if ($e || $c || @$o);
+
+    foreach my $l (keys %$k) {
+        $k->{$l} = vsplit($k->{$l}) if (ref $k->{$l} eq 'ARRAY');
+    }
+
+    return \@forms;
+}
+
+# Returns text representing a set of forms.
+sub Form::compose {
+    my ($forms) = @_;
+    my @text;
+
+    foreach my $form (@$forms) {
+        my ($c, $o, $k, $e) = @$form;
+        my $text = "";
+
+        if ($c) {
+            $c =~ s/\n*$/\n/;
+            $text = "$c\n";
+        }
+        if ($e) {
+            $text .= $e;
+        }
+        elsif ($o) {
+            my @lines;
+
+            foreach my $key (@$o) {
+                my ($line, $sp);
+                my $v = $k->{$key};
+                my @values = ref $v eq 'ARRAY' ? @$v : $v;
+
+                $sp = " "x(length("$key: "));
+                $sp = " "x4 if length($sp) > 16;
+
+                foreach $v (@values) {
+                    if ($v =~ /\n/) {
+                        $v =~ s/^/$sp/gm;
+                        $v =~ s/^$sp//;
+
+                        if ($line) {
+                            push @lines, "$line\n\n";
+                            $line = "";
+                        }
+                        elsif (@lines && $lines[-1] !~ /\n\n$/) {
+                            $lines[-1] .= "\n";
+                        }
+                        push @lines, "$key: $v\n\n";
+                    }
+                    elsif ($line &&
+                           length($line)+length($v)-rindex($line, "\n") >= 70)
+                    {
+                        $line .= ",\n$sp$v";
+                    }
+                    else {
+                        $line = $line ? "$line, $v" : "$key: $v";
+                    }
+                }
+
+                $line = "$key:" unless @values;
+                if ($line) {
+                    if ($line =~ /\n/) {
+                        if (@lines && $lines[-1] !~ /\n\n$/) {
+                            $lines[-1] .= "\n";
+                        }
+                        $line .= "\n";
+                    }
+                    push @lines, "$line\n";
+                }
+            }
+
+            $text .= join "", @lines;
+        }
+        else {
+            chomp $text;
+        }
+        push @text, $text;
+    }
+
+    return join "\n--\n\n", @text;
+}
+
+# Configuration.
+# --------------
+
+# Returns configuration information from the environment.
+sub config_from_env {
+    my %env;
+
+    foreach my $k ("DEBUG", "USER", "PASSWD", "SERVER") {
+        if (exists $ENV{"RT$k"}) {
+            $env{lc $k} = $ENV{"RT$k"};
+        }
+    }
+
+    return %env;
+}
+
+# Finds a suitable configuration file and returns information from it.
+sub config_from_file {
+    my ($rc) = @_;
+
+    if ($rc =~ m#^/#) {
+        # We'll use an absolute path if we were given one.
+        return parse_config_file($rc);
+    }
+    else {
+        # Otherwise we'll use the first file we can find in the current
+        # directory, or in one of its (increasingly distant) ancestors.
+
+        my @dirs = split /\//, cwd;
+        while (@dirs) {
+            my $file = join('/', @dirs, $rc);
+            if (-r $file) {
+                return parse_config_file($file);
+            }
+
+            # Remove the last directory component each time.
+            pop @dirs;
+        }
+
+        # Still nothing? We'll fall back to some likely defaults.
+        for ("$HOME/$rc", "/etc/rt.conf") {
+            return parse_config_file($_) if (-r $_);
+        }
+    }
+
+    return ();
+}
+
+# Makes a hash of the specified configuration file.
+sub parse_config_file {
+    my %cfg;
+    my ($file) = @_;
+
+    open(CFG, $file) && do {
+        while (<CFG>) {
+            chomp;
+            next if (/^#/ || /^\s*$/);
+
+            if (/^(user|passwd|server)\s+([^ ]+)$/) {
+                $cfg{$1} = $2;
+            }
+            else {
+                die "rt: $file:$.: unknown configuration directive.\n";
+            }
+        }
+    };
+
+    return %cfg;
+}
+
+# Helper functions.
+# -----------------
+
+sub whine {
+    my $sub = (caller(1))[3];
+    $sub =~ s/^main:://;
+    warn "rt: $sub: @_\n";
+    return;
+}
+
+sub read_passwd {
+    eval 'require Term::ReadKey';
+    if ($@) {
+        die "No password specified (and Term::ReadKey not installed).\n";
+    }
+
+    print "Password: ";
+    Term::ReadKey::ReadMode('noecho');
+    chomp(my $passwd = Term::ReadKey::ReadLine(0));
+    Term::ReadKey::ReadMode('restore');
+    print "\n";
+
+    return $passwd;
+}
+
+sub vi {
+    my ($text) = @_;
+    my $file = "/tmp/rt.form.$$";
+    my $editor = $ENV{EDITOR} || $ENV{VISUAL} || "vi";
+
+    local *F;
+    local $/ = undef;
+
+    open(F, ">$file") || die "$file: $!\n"; print F $text; close(F);
+    system($editor, $file) && die "Couldn't run $editor.\n";
+    open(F, $file) || die "$file: $!\n"; $text = <F>; close(F);
+    unlink($file);
+
+    return $text;
+}
+
+# Add a value to a (possibly multi-valued) hash key.
+sub vpush {
+    my ($hash, $key, $val) = @_;
+    my @val = ref $val eq 'ARRAY' ? @$val : $val;
+
+    if (exists $hash->{$key}) {
+        unless (ref $hash->{$key} eq 'ARRAY') {
+            my @v = $hash->{$key} ne '' ? $hash->{$key} : ();
+            $hash->{$key} = \@v;
+        }
+        push @{ $hash->{$key} }, @val;
+    }
+    else {
+        $hash->{$key} = $val;
+    }
+}
+
+# "Normalise" a hash key that's known to be multi-valued.
+sub vsplit {
+    my ($val) = @_;
+    my ($word, @words);
+    my @values = ref $val eq 'ARRAY' ? @$val : $val;
+
+    foreach my $line (map {split /\n/} @values) {
+        # XXX: This should become a real parser, à la Text::ParseWords.
+        $line =~ s/^\s+//;
+        $line =~ s/\s+$//;
+        push @words, split /\s*,\s*/, $line;
+    }
+
+    return \@words;
+}
+
+sub expand_list {
+    my ($list) = @_;
+    my ($elt, @elts, %elts);
+
+    foreach $elt (split /,/, $list) {
+        if ($elt =~ /^(\d+)-(\d+)$/) { push @elts, ($1..$2) }
+        else                         { push @elts, $elt }
+    }
+
+    @elts{@elts}=();
+    return sort {$a<=>$b} keys %elts;
+}
+
+sub get_type_argument {
+    my $type;
+
+    if (@ARGV) {
+        $type = shift @ARGV;
+        unless ($type =~ /^[A-Za-z0-9_.-]+$/) {
+            # We want whine to mention our caller, not us.
+            @_ = ("Invalid type '$type' specified.");
+            goto &whine;
+        }
+    }
+    else {
+        @_ = ("No type argument specified with -t.");
+        goto &whine;
+    }
+
+    $type =~ s/s$//; # "Plural". Ugh.
+    return $type;
+}
+
+sub get_var_argument {
+    my ($data) = @_;
+
+    if (@ARGV) {
+        my $kv = shift @ARGV;
+        if (my ($k, $v) = $kv =~ /^($field)=(.*)$/) {
+            push @{ $data->{$k} }, $v;
+        }
+        else {
+            @_ = ("Invalid variable specification: '$kv'.");
+            goto &whine;
+        }
+    }
+    else {
+        @_ = ("No variable argument specified with -S.");
+        goto &whine;
+    }
+}
+
+sub is_object_spec {
+    my ($spec, $type) = @_;
+
+    $spec =~ s|^(?:$type/)?|$type/| if defined $type;
+    return $spec if ($spec =~ m{^$name/(?:$idlist|$labels)(?:/.*)?$}o);
+    return;
+}
+
+__DATA__
+
+Title: intro
+Title: introduction
+Text:
+
+    ** THIS IS AN UNSUPPORTED PREVIEW RELEASE **
+    ** PLEASE REPORT BUGS TO rt-bugs@fsck.com **
+
+    This is a command-line interface to RT 3.
+
+    It allows you to interact with an RT server over HTTP, and offers an
+    interface to RT's functionality that is better-suited to automation
+    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.
+
+    For more information:
+
+        - rt help actions       (a list of possible actions)
+        - rt help objects       (how to specify objects)
+        - rt help usage         (syntax information)
+
+        - rt help config        (configuration details)
+        - rt help examples      (a few useful examples)
+        - rt help topics        (a list of help topics)
+
+--
+
+Title: usage
+Title: syntax
+Text:
+
+    Syntax:
+
+        rt <action> [options] [arguments]
+
+    Each invocation of this program must specify an action (e.g. "edit",
+    "create"), options to modify behaviour, and other arguments required
+    by the specified action. (For example, most actions expect a list of
+    numeric object IDs to act upon.)
+
+    The details of the syntax and arguments for each action are given by
+    "rt help <action>". Some actions may be referred to by more than one
+    name ("create" is the same as "new", for example).  
+
+    Objects are identified by a type and an ID (which can be a name or a
+    number, depending on the type). For some actions, the object type is
+    implied (you can only comment on tickets); for others, the user must
+    specify it explicitly. See "rt help objects" for details.
+
+    In syntax descriptions, mandatory arguments that must be replaced by
+    appropriate value are enclosed in <>, and optional arguments are
+    indicated by [] (for example, <action> and [options] above).
+
+    For more information:
+
+        - rt help objects       (how to specify objects)
+        - rt help actions       (a list of actions)
+        - rt help types         (a list of object types)
+
+--
+
+Title: conf
+Title: config
+Title: configuration
+Text:
+
+    This program has two major sources of configuration information: its
+    configuration files, and the environment.
+
+    The program looks for configuration directives in a file named .rtrc
+    (or $RTCONFIG; see below) in the current directory, and then in more
+    distant ancestors, until it reaches /. If no suitable configuration
+    files are found, it will also check for ~/.rtrc and /etc/rt.conf.
+
+    Configuration directives:
+
+        The following directives may occur, one per line:
+
+        - server <URL>          URL to RT server.
+        - user <username>       RT username.
+        - passwd <passwd>       RT user's password.
+
+        Blank and #-commented lines are ignored.
+
+    Environment variables:
+
+        The following environment variables override any corresponding
+        values defined in configuration files:
+
+        - RTUSER
+        - RTPASSWD
+        - RTSERVER
+        - RTDEBUG       Numeric debug level. (Set to 3 for full logs.)
+        - RTCONFIG      Specifies a name other than ".rtrc" for the
+                        configuration file.
+
+--
+
+Title: objects
+Text:
+
+    Syntax:
+
+        <type>/<id>[/<attributes>]
+
+    Every object in RT has a type (e.g. "ticket", "queue") and a numeric
+    ID. Some types of objects can also be identified by name (like users
+    and queues). Furthermore, objects may have named attributes (such as
+    "ticket/1/history").
+
+    An object specification is like a path in a virtual filesystem, with
+    object types as top-level directories, object IDs as subdirectories,
+    and named attributes as further subdirectories.
+
+    A comma-separated list of names, numeric IDs, or numeric ranges can
+    be used to specify more than one object of the same type. Note that
+    the list must be a single argument (i.e., no spaces). For example,
+    "user/root,1-3,5,7-10,ams" is a list of ten users; the same list
+    can also be written as "user/ams,root,1,2,3,5,7,8-20".
+    
+    Examples:
+
+        ticket/1
+        ticket/1/attachments
+        ticket/1/attachments/3
+        ticket/1/attachments/3/content
+        ticket/1-3/links
+        ticket/1-3,5-7/history
+
+        user/ams
+        user/ams/rights
+        user/ams,rai,1/rights
+
+    For more information:
+
+        - rt help <action>      (action-specific details)
+        - rt help <type>        (type-specific details)
+
+--
+
+Title: actions
+Title: commands
+Text:
+
+    You can currently perform the following actions on all objects:
+
+        - list          (list objects matching some condition)
+        - show          (display object details)
+        - edit          (edit object details)
+        - create        (create a new object)
+
+    Each type may define actions specific to itself; these are listed in
+    the help item about that type.
+
+    For more information:
+
+        - rt help <action>      (action-specific details)
+        - rt help types         (a list of possible types)
+
+--
+
+Title: types
+Text:
+
+    You can currently operate on the following types of objects:
+
+        - tickets
+        - users
+        - groups
+        - queues
+
+    For more information:
+
+        - rt help <type>        (type-specific details)
+        - rt help objects       (how to specify objects)
+        - rt help actions       (a list of possible actions)
+
+--
+
+Title: ticket
+Text:
+
+    Tickets are identified by a numeric ID.
+
+    The following generic operations may be performed upon tickets:
+
+        - list
+        - show
+        - edit
+        - create
+
+    In addition, the following ticket-specific actions exist:
+
+        - link
+        - merge
+        - comment
+        - correspond
+
+    Attributes:
+
+        The following attributes can be used with "rt show" or "rt edit"
+        to retrieve or edit other information associated with tickets:
+
+        links                      A ticket's relationships with others.
+        history                    All of a ticket's transactions.
+        history/type/<type>        Only a particular type of transaction.
+        history/id/<id>            Only the transaction of the specified id.
+        attachments                A list of attachments.
+        attachments/<id>           The metadata for an individual attachment.
+        attachments/<id>/content   The content of an individual attachment.
+
+--
+
+Title: user
+Title: group
+Text:
+
+    Users and groups are identified by name or numeric ID.
+
+    The following generic operations may be performed upon them:
+
+        - list
+        - show
+        - 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
+Text:
+
+    Queues are identified by name or numeric ID.
+
+    Currently, they can be subjected to the following actions:
+
+        - show
+        - edit
+        - create
+
+--
+
+Title: logout
+Text:
+
+    Syntax:
+
+        rt logout
+
+    Terminates the currently established login session. You will need to
+    provide authentication credentials before you can continue using the
+    server. (See "rt help config" for details about authentication.)
+
+--
+
+Title: ls
+Title: list
+Title: search
+Text:
+
+    Syntax:
+
+        rt <ls|list|search> [options] "query string"
+
+    Displays a list of objects matching the specified conditions.
+    ("ls", "list", and "search" are synonyms.)
+
+    Conditions are expressed in the SQL-like syntax used internally by
+    RT3. (For more information, see "rt help query".) The query string
+    must be supplied as one argument.
+
+    (Right now, the server doesn't support listing anything but tickets.
+    Other types will be supported in future; this client will be able to
+    take advantage of that support without any changes.)
+
+    Options:
+
+        The following options control how much information is displayed
+        about each matching object:
+
+        -i      Numeric IDs only. (Useful for |rt edit -; see examples.)
+        -s      Short description.
+        -l      Longer description.
+
+        In addition,
+        
+        -o +/-<field>   Orders the returned list by the specified field.
+        -S var=val      Submits the specified variable with the request.
+        -t type         Specifies the type of object to look for. (The
+                        default is "ticket".)
+
+    Examples:
+
+        rt ls "Priority > 5 and Status='new'"
+        rt ls -o +Subject "Priority > 5 and Status='new'"
+        rt ls -o -Created "Priority > 5 and Status='new'"
+        rt ls -i "Priority > 5"|rt edit - set status=resolved
+        rt ls -t ticket "Subject like '[PATCH]%'"
+
+--
+
+Title: show
+Text:
+
+    Syntax:
+
+        rt show [options] <object-ids>
+
+    Displays details of the specified objects.
+
+    For some types, object information is further classified into named
+    attributes (for example, "1-3/links" is a valid ticket specification
+    that refers to the links for tickets 1-3). Consult "rt help <type>"
+    and "rt help objects" for further details.
+
+    This command writes a set of forms representing the requested object
+    data to STDOUT.
+
+    Options:
+
+        -               Read IDs from STDIN instead of the command-line.
+        -t type         Specifies object type.
+        -f a,b,c        Restrict the display to the specified fields.
+        -S var=val      Submits the specified variable with the request.
+
+    Examples:
+
+        rt show -t ticket -f id,subject,status 1-3
+        rt show ticket/3/attachments/29
+        rt show ticket/3/attachments/29/content
+        rt show ticket/1-3/links
+        rt show -t user 2
+
+--
+
+Title: new
+Title: edit
+Title: create
+Text:
+
+    Syntax:
+
+        rt edit [options] <object-ids> set field=value [field=value] ...
+                                       add field=value [field=value] ...
+                                       del field=value [field=value] ...
+
+    Edits information corresponding to the specified objects.
+
+    If, instead of "edit", an action of "new" or "create" is specified,
+    then a new object is created. In this case, no numeric object IDs
+    may be specified, but the syntax and behaviour remain otherwise
+    unchanged.
+
+    This command typically starts an editor to allow you to edit object
+    data in a form for submission. If you specified enough information
+    on the command-line, however, it will make the submission directly.
+
+    The command line may specify field-values in three different ways.
+    "set" sets the named field to the given value, "add" adds a value
+    to a multi-valued field, and "del" deletes the corresponding value.
+    Each "field=value" specification must be given as a single argument.
+
+    For some types, object information is further classified into named
+    attributes (for example, "1-3/links" is a valid ticket specification
+    that refers to the links for tickets 1-3). These attributes may also
+    be edited. Consult "rt help <type>" and "rt help object