RT/FM The RT Hackers Guide Not logged in. [Login]

[Article listing] [Search]


This documentation tree is intended to be a guide on what can be done with RT beyond the standard installation. A lot of the articles within will be taken from the RT mailing lists (rt-devel, rt-users), and will have varying degrees of technicality.

RT Schema


Behind the scenes, RT uses a schema autogenerating mechanism (DBIx::DBschema) to translate the etc/schema.pm file (distribution tarball) into things that your database of choice can use. As such, this file tends to be the authoritative reference, and is where this reference is taken from.

Graphical Schema Courtesy of XS4ALL.

RT 2.0.x Schema - Table Relationships


This uses Tables, and may not render properly in all browsers. My apologies in advance
Tickets Table

The Tickets table holds, well, Tickets. The API in RT::Ticket covers handling individual Tickets, whilst the API in RT::Tickets covers handling groupings of Tickets. For more information on extra things in an RT::Ticket, see the detailed schema below.

There are four main linkages between the Tickets Table and other Tables, being the Transactions, Watchers and Users tables.

Linkage: Tickets.Queue = Queue.id Linkage: Transaction.Ticket = Ticket.id Linkage: Watchers.Value = Ticket.id
Where Watchers.Scope = Ticket
Linkage: Tickets.Owner = User.id
Queue Table

This contains most of the information about various Queues that are configured in your system.

Transactions Table

This contains meta-information about actions which have been performed on this ticket. This includes incoming Comments or Correspondence, but the actual data is kept in the Attachments table, which is further linked below.

Watchers Table

This is.. an odd beast, and is only in the 2.0.x series. It contains references to people that are Watchers on a given ticket, such as the original Requestor, additional requestors (etc). In most cases, the Watcher has an 'Owner', which is a reference to a User object.

Users Table

This contains Users, and and has suitable fields to contain a biography about a person.

Linkage: Attachments.TransactionId = Transaction.id Linkage: Watchers.Owner = Users.id

RT 2.0.x Schema: Queues


A 'Queue' in RT is the basis of your workflow. Reuqests are organised into seperate Queues, which may be different departments, or different sections within a given department, or different aspects of your workflow. The important thing is that the Queue is used to group similar Requests together.

The methods available from an RT::Queue object that correspond to table fields are as follows:

All of the above methods have a '->SetName()' equivilant, which allow you to change the data stored in the field if your current User has the relevant access rights. The RT::Queue object also has other, more indirect methods, which ease the drama of crossreferencing information from other tables. They are as follows:

RT 2.0.x Schema: Scrips


'Scrips' => { 'columns' => [ 'id', 'serial', '', '', '', '', 'ScripCondition', 'integer', 'NULL', '', '', '', #Foreign key ScripConditions::id 'ScripAction', 'integer', 'NULL', '', '', '', #Foreign key ScripActions::id 'Stage', 'varchar', 'NULL', '32','','', #What stage does this scrip #Happen in. for now, everything is 'TransactionCreate', 'Queue', 'integer', 'NULL', '', '', '', #Foreign key Queues::id 'Template', 'integer', 'NULL', '', '', '', #Foreign key Templates::id 'Creator', 'integer', 'NULL', '', '', '', 'Created', 'timestamp', 'NULL', '', '', '', 'LastUpdatedBy', 'integer', 'NULL', '', '', '', 'LastUpdated', 'timestamp', 'NULL', '', '', '', ], 'primary_key' => 'id', 'index' => [ ], }, 'unique' => [ ],

RT 2.0.x Schema: ScripActions


'ScripActions' => { 'columns' => [ 'id', 'serial', '', '', '', '', 'Name', 'varchar', 'NULL', '255', '', '', # Alias 'Description', 'varchar', 'NULL', '255', '', '', #Textual description 'ExecModule', 'varchar', 'NULL', '60', '', '', #This calles RT::Action::___ 'Argument', 'varchar', 'NULL', '255', '', '', #We can pass a single argument #to the scrip. sometimes, it's who to send mail to. 'Creator', 'integer', 'NULL', '', '', '', 'Created', 'timestamp', 'NULL', '', '', '', 'LastUpdatedBy', 'integer', 'NULL', '', '', '', 'LastUpdated', 'timestamp', 'NULL', '', '', '', ], 'primary_key' => 'id', 'unique' => [ ], 'index' => [ ], },

RT 2.0.x Schema: ScripConditions


'ScripConditions' => { 'columns' => [ 'id', 'serial', '', '', '', '', 'Name', 'varchar', 'NULL', '255', '', '', # Alias 'Description', 'varchar', 'NULL', '255', '', '', #Textual description 'ExecModule', 'varchar', 'NULL', '60', '', '', #This calles RT::Condition:: 'Argument', 'varchar', 'NULL', '255', '', '', #We can pass a single argument #to the scrip. sometimes, it's who to send mail to. 'ApplicableTransTypes', 'varchar', 'NULL', '60', '', '',#Transaction types this scrip # acts on. comma or / delimited is just great. 'Creator', 'integer', 'NULL', '', '', '', 'Created', 'timestamp', 'NULL', '', '', '', 'LastUpdatedBy', 'integer', 'NULL', '', '', '', 'LastUpdated', 'timestamp', 'NULL', '', '', '', ], 'primary_key' => 'id', 'unique' => [ ], 'index' => [ ], },

RT 2.0.x Schema: Templates


'Templates' => { 'columns' => [ 'id', 'serial', '', '', '', '', 'Queue', 'integer', 'NOT NULL', '', '0', '', 'Name', 'varchar', '', '40', '', '', 'Description', 'varchar', 'NULL', '120', '', '', 'Type', 'varchar', 'NULL', '16', '','', 'Language', 'varchar', 'NULL', '16', '', '', 'TranslationOf', 'integer', 'NULL', '', '', '', 'Content', 'blob', 'NULL', '', '', '', 'LastUpdated', 'timestamp', 'NULL', '', '', '', 'LastUpdatedBy', 'integer', 'NULL', '', '', '', 'Creator', 'integer', 'NULL', '', '', '', 'Created', 'timestamp', 'NULL', '', '', '', ], 'primary_key' => 'id', 'unique' => [ [''] ], 'index' => [ ], },

RT 2.0.x Schema: Tickets


The Tickets table has the following fields and Methods: Next update - other method in RT::Ticket and RT::Tickets.

RT 2.0.x Schema: Transactions


'Transactions' => { 'columns' => [ 'id', 'serial', '', '', '', '', 'EffectiveTicket', 'integer', 'NULL', '', '', '', 'Ticket', 'integer', 'NULL', '', '', '', #Foreign key Ticket::id 'TimeTaken', 'integer', 'NULL', '', '', '', #Time spent on this trans in min 'Type', 'varchar', 'NULL', '20', '', '', 'Field', 'varchar', 'NULL', '40', '', '', #If it's a "Set" transaction, what #field was set. 'OldValue', 'varchar', 'NULL', '255', '', '', 'NewValue', 'varchar', 'NULL', '255', '', '', 'Data', 'varchar', 'NULL', '100', '', '', 'Creator', 'integer', 'NULL', '', '', '', 'Created', 'timestamp', 'NULL', '', '', '', ], 'primary_key' => 'id', 'unique' => [ ], 'index' => [ ['Ticket'], ['EffectiveTicket'] ], },

RT 2.0.x Schema: Attachments


'Attachments' => { 'columns' => [ 'id', 'serial', '', '', '', '', 'TransactionId', 'integer', '', '', '', '', #Foreign key Transactions::Id 'Parent', 'integer', 'NULL', '', '', '', # Attachments::Id 'MessageId', 'varchar', 'NULL', '160', '', '', #RFC822 messageid, if any 'Subject', 'varchar', 'NULL', '255', '', '', 'Filename', 'varchar', 'NULL', '255', '', '', 'ContentType', 'varchar', 'NULL', '80', '', '', 'ContentEncoding', 'varchar', 'NULL', '80', '', '', 'Content', 'long varbinary', 'NULL', '', '', '', 'Headers', 'long varbinary', 'NULL', '', '', '', 'Creator', 'integer', 'NULL', '', '', '', 'Created', 'timestamp', 'NULL', '', '', '', ], 'primary_key' => 'id', 'unique' => [ ], 'index' => [ ['Parent'], ['TransactionId'], ['Parent', 'TransactionId'] ], },

RT 2.0.x Schema: Keywords


'Keywords' => { 'columns' => [ 'id', 'serial', '', '', '', '', 'Name', 'varchar', 'NOT NULL', '255', '', '', 'Description', 'varchar', 'NULL', '255', '', '', 'Parent', 'integer', 'NULL', '', '', '', 'Disabled', 'int2', '','','0','', ], 'primary_key' => 'id', 'unique' => [ [ 'Name', 'Parent' ] ], 'index' => [ [ 'Name', ], [ 'Parent' ] ], },

RT 2.0.x Schema: ObjectKeywords


'ObjectKeywords' => { 'columns' => [ 'id', 'serial', '', '', '', '', 'Keyword', 'integer', 'NOT NULL', '', '', '', 'KeywordSelect', 'integer', 'NOT NULL', '', '', '', 'ObjectType', 'varchar', 'NOT NULL', '32', '', '', 'ObjectId', 'integer', 'NOT NULL', '', '', '', ], 'primary_key' => 'id', 'unique' => [ [ 'ObjectId', 'ObjectType','KeywordSelect', 'Keyword' ] ], 'index' => [ [ 'ObjectId', 'ObjectType' ] , ['Keyword'] ], },

RT 2.0.x Schema: KeywordSelects


'KeywordSelects' => { 'columns' => [ 'id', 'serial', '', '', '', '', 'Name','varchar','NULL','255','','', 'Keyword', 'integer', 'NULL', '', '', '', 'Single', 'integer', 'NULL', '', '', '', 'Depth', 'integer', 'NOT NULL', '', 0, '', 'ObjectType', 'varchar', 'NOT NULL', '32', '', '', 'ObjectField', 'varchar', 'NULL', '32', '', '', 'ObjectValue', 'varchar', 'NULL', '255', '', '', 'Disabled', 'int2', '','','0','', ], 'primary_key' => 'id', 'unique' => [ [ ] ], 'index' => [ [ 'Keyword' ], [ 'ObjectType', 'ObjectField', 'ObjectValue'] ], },

RT 2.0.x Schema: Watchers


'Watchers' => { 'columns' => [ 'id', 'serial', '', '', '', '', 'Type', 'varchar', 'NULL', '16', '', '', 'Scope', 'varchar', 'NULL', '16', '', '', 'Value', 'integer', 'NULL', '', '', '', 'Email', 'varchar', 'NULL', '255', '', '', 'Quiet', 'integer', 'NULL', '', '', '', 'Owner', 'integer', 'NULL', '', '', '', 'Creator', 'integer', 'NULL', '', '', '', 'Created', 'timestamp', 'NULL', '', '', '', 'LastUpdatedBy', 'integer', 'NULL', '', '', '', 'LastUpdated', 'timestamp', 'NULL', '', '', '', ], 'primary_key' => 'id', 'unique' => [ ], 'index' => [ ['Scope','Value','Type','Owner'] ], },

RT 2.0.x Schema: Users


'Users' => { 'columns' => [ 'id', 'serial', '', '', '', '', 'Name', 'varchar', '', '120', '', '', 'Password', 'varchar', 'NULL', '40', '', '', 'Comments', 'blob', 'NULL', '', '', '', 'Signature', 'blob', 'NULL', '', '', '', 'EmailAddress', 'varchar', 'NULL', '120', '', '', 'FreeformContactInfo', 'blob', 'NULL', '', '', '', 'Organization', 'varchar', 'NULL', '200', '', '', 'Privileged', 'integer', 'NULL', '', '', '', 'RealName', 'varchar', 'NULL', '120', '', '', 'Nickname', 'varchar', 'NULL', '16', '', '', 'Lang', 'varchar', 'NULL', '16', '', '', 'EmailEncoding', 'varchar', 'NULL', '16', '', '', 'WebEncoding', 'varchar', 'NULL', '16', '', '', 'ExternalContactInfoId', 'varchar', 'NULL', '100', '', '', 'ContactInfoSystem', 'varchar', 'NULL', '30', '', '', 'ExternalAuthId', 'varchar', 'NULL', '100', '', '', 'AuthSystem', 'varchar', 'NULL', '30', '', '', 'Gecos', 'varchar', 'NULL', '16', '', '', 'HomePhone', 'varchar', 'NULL', '30', '', '', 'WorkPhone', 'varchar', 'NULL', '30', '', '', 'MobilePhone', 'varchar', 'NULL', '30', '', '', 'PagerPhone', 'varchar', 'NULL', '30', '', '', 'Address1', 'varchar', 'NULL', '200', '', '', 'Address2', 'varchar', 'NULL', '200', '', '', 'City', 'varchar', 'NULL', '100', '', '', 'State', 'varchar', 'NULL', '100', '', '', 'Zip', 'varchar', 'NULL', '16', '', '', 'Country', 'varchar', 'NULL', '50', '', '', 'Creator', 'integer', 'NULL', '', '', '', 'Created', 'timestamp', 'NULL', '', '', '', 'LastUpdatedBy', 'integer', 'NULL', '', '', '', 'LastUpdated', 'timestamp', 'NULL', '', '', '', 'Disabled', 'int2', '','','0','', ], 'primary_key' => 'id', 'unique' => [ ['Name'] ], 'index' => [ ['Name'], ['id', 'EmailAddress'], ['EmailAddress'] ], },

RT 2.0.x Schema: Links


'Links' => { 'columns' => [ 'id', 'serial', '', '', '', '', 'Base', 'varchar', 'NULL', '240', '', '', 'Target', 'varchar', 'NULL', '240', '', '', 'Type', 'varchar', '', '20', '', '', 'LocalTarget', 'integer', 'NULL', '', '', '', 'LocalBase', 'integer', 'NULL', '', '', '', 'LastUpdatedBy', 'integer', 'NULL', '', '', '', 'LastUpdated', 'timestamp', 'NULL', '', '', '', 'Creator', 'integer', 'NULL', '', '', '', 'Created', 'timestamp', 'NULL', '', '', '', ], 'primary_key' => 'id', 'unique' => [ ['Base', 'Target', 'Type'] ], 'index' => [ ], },

RT 2.0.x Schema: Groups


This table stores the various Groups of Users.

Table Name: Groups
Columns:


RT 2.0.x Schema: GroupMembers


'GroupMembers' => { 'columns' => [ 'id', 'serial', '', '', '', '', 'GroupId', 'integer', 'NULL', '', '', '', #foreign key, Groups::id 'UserId', 'integer', 'NULL', '', '', '', #foreign key, Users::id ], 'primary_key' => 'id', 'unique' => [ ['GroupId', 'UserId'] ], 'index' => [ ], },

RT 2.0.x Schema: ACL


This table stores various ACLs (Access Control Lists), controlling who can get access to RT.

Table Name: ACL
Columns:

The available methods in RT::ACL are

Extending RT



Japanese


For RT 2.0.x, Ben Gertzfield has provided a patch to RT to handle the EUC-JP and ISO-2022-JP character sets as follows:

Mon, 04 Mar 2002 18:12:12 +0900

I have patched Request Tracker 2.0.11 to work with Japanese, and both
email and web work correctly now.  Outgoing emails and incoming emails
are both supported in Japanese.

This patch does not deal with any languages other than Japanese and
English, and in fact will probably break if
German/French/Russian/whatever messages are sent.

This patch requires Jcode.pm from http://openlab.ring.gr.jp/Jcode/ to
be installed.  It stores all data in the DB in EUC-JP, and converts
incoming emails from ISO-2022-JP to EUC-JP.  The subject line is
correctly MIME decoded/encoded with Base64 then converted, as well.

You will want to tell your web server to serve all pages for RT as
EUC-JP.  In my apache httpd.conf, I used:

AddDefaultCharset euc-jp

The patch is available at:

http://nausicaa.interq.or.jp/rt/rt-japanese.patch

Patch follows, against Request Tracker version 2.0.11.

diff -ruN rt-2-0-11.orig/lib/RT/Action/SendEmail.pm rt-2-0-11/lib/RT/Action/SendEmail.pm
--- rt-2-0-11.orig/lib/RT/Action/SendEmail.pm	Wed Nov  7 08:04:17 2001
+++ rt-2-0-11/lib/RT/Action/SendEmail.pm	Mon Mar  4 17:25:25 2002
@@ -2,6 +2,8 @@
 # Copyright 2000  Jesse Vincent  and Tobias Brox 
# Released under the terms of the GNU Public License
 
+use Jcode;
+
 package RT::Action::SendEmail;
 require RT::Action::Generic;
 
@@ -98,6 +100,19 @@
     
 
     $MIMEObj->make_singlepart;
+
+    if (Jcode::getcode($MIMEObj->as_string) ne 'ascii') {
+        $MIMEObj->head->set('content-type', 'text/plain; charset=iso-2022-jp');
+	my $body_r = $MIMEObj->bodyhandle->open("r");
+	my $body;
+	while (defined($_ = $body_r->getline)) {
+          $body .= Jcode->new($_)->iso_2022_jp;
+        }
+	$body_r->close;
+	my $body_w = $MIMEObj->bodyhandle->open("w");
+	$body_w->print($body);
+	$body_w->close;
+    }
     
     
     #If we don't have any recipients to send to, don't send a message;
@@ -292,6 +307,11 @@
   unless ($self->TemplateObj->MIMEObj->head->get('From')) {
       my $friendly_name=$self->TransactionObj->CreatorObj->RealName;
       # TODO: this "via RT" should really be site-configurable.
+      # hack for Japanese - che@debian.org
+      if (Jcode::getcode($friendly_name) ne 'ascii') {
+        $friendly_name = Jcode->new($friendly_name)->mime_encode();
+      }
+
       $self->SetHeader('From', "$friendly_name via RT <$replyto>");
   }
   
@@ -396,7 +416,8 @@
 
 sub SetSubject {
   my $self = shift;
-  unless ($self->TemplateObj->MIMEObj->head->get('Subject')) {
+  my $subj = $self->TemplateObj->MIMEObj->head->get('Subject'); 
+  if (not $subj) {
     my $message=$self->TransactionObj->Message;
     my $ticket=$self->TicketObj->Id;
     
@@ -423,11 +444,21 @@
     
     $subject =~ s/(\r\n|\n|\s)/ /gi;
 
+    # hack for Japanese
+    if (Jcode::getcode($subject) ne 'ascii') {
+      $subject = Jcode->new($subject)->mime_encode();
+    }
+
     chomp $subject;
     $self->SetHeader('Subject',$subject);
-    
-    }
-  return($subject);
+    return $subject;
+  } elsif (Jcode::getcode($subj) ne 'ascii') {
+    my $newsubj = Jcode->new($subject)->mime_encode();
+    $self->SetHeader('Subject', $newsubj);
+    return $newsubj;
+  } else {
+    return $subj;
+  }
 }
 # }}}
 
diff -ruN rt-2-0-11.orig/lib/RT/Interface/Email.pm rt-2-0-11/lib/RT/Interface/Email.pm
--- rt-2-0-11.orig/lib/RT/Interface/Email.pm	Wed Nov  7 08:04:52 2001
+++ rt-2-0-11/lib/RT/Interface/Email.pm	Mon Mar  4 17:33:58 2002
@@ -6,6 +6,7 @@
 use strict;
 use Mail::Address;
 use MIME::Entity;
+use Jcode;
 
 BEGIN {
     use Exporter ();
@@ -244,8 +245,14 @@
     # }}} (temporary directory)
 
     #Ok. now that we're set up, let's get the stdin.
+
+    # Hack for Japanese.
+    my $body = '';
+    while (defined(my $line = )) {
+        $body .= Jcode->new($line)->euc; 
+    }
     my $entity;
-    unless ($entity = $parser->read(\*STDIN)) {
+    unless ($entity = $parser->parse_data($body)) {
 	die "couldn't parse MIME stream";
     }
     #Now we've got a parsed mime object. 
@@ -254,10 +261,15 @@
     my $head = $entity->head;
     
     # TODO - information about the charset is lost here!
-    $head->decode;
+    #$head->decode;
+    foreach my $header (qw(Subject To From Cc)) {
+        if ($head->count($header)) {
+            $head->replace($header, 
+	       Jcode->new($head->get($header))->mime_decode->euc);
+	}
+    }
 
     return ($entity, $head);
-
 }
 # }}}
 

Understanding Scrips



Just what is a 'Scrip' ?


You've read the definition in the Administration guide, being 'a powerful system for implementing local business logic, called 'Scrips'. (The 'Scrips' system is a combination of a 'script' system and a 'subscription' system)'

Now heres where we get into the nitty-gritty. A Scrip is the common name given to two dynamically loaded sections of code each time a transaction occurs. Each Queue can have its own collection of Scrips to apply when a transaction occurs on a Ticket within that queue, in addition to the global queue.

Each Scrip has two parts, the ScripCondition and the ScripAction. Each time a transaction occurs, every ScripCondition (of every global and queue-specific Scrip) is executed to see if the Scrip's ScripAction should be executed. It is essential to note that a given Scrip uses a ScripCondition and a ScripAction. ScripConditions and ScripActions are not tied to a specific Scrip.

The RT distribution is supplied with several Scrips which send E-mail base on Templates, but this is certainly not the only use for Scrips.

More detail on how the Scrip system is implemented is given in the Under the Hood section.

Where do these ScripCondition and ScripAction files go?


When installing an add-on ScripCondition or ScripAction, you will need to do two things before you can make use of it.

Firstly, copy the perl library file (the foobar.pm file) to the appropriate place in the RT hierarchy. For a ScripCondition, this is the 'Condition' subdirectory of your RT's lib directory. (eg, if your RT libraries are in /usr/local/rt2/lib/RT, then your ScripCondition file should be copied into /usr/local/rt2/lib/RT/Condition/ ). For a ScripAction, this is in the 'Action' subdirectory of your RT's lib directory. (If you've forgotten what you've downloaded, look for the 'package RT::...;' line in the foobar.pm file. If its 'RT::Condition::...;', then its a ScripCondition, it its 'RT::Action::...;', its a ScripAction. If its something else, you're on your own ;))

In both cases, make sure that the user which runs RT's mailgate, and the user which runs RT's Web Interface can access the file.

Secondly, you need to make an entry in the RT database giving details of the ScripCondtion or ScripAction. Most addons have a nifty insert perl script which you can execute. If you don't have one of these, I'd highly advise that you find one, edit it as appropriate, and execute that rather than messing around with hand SQL INSERT statements.

After that, you will be able to refer to your new ScripCondition or ScripActions when in the RT Administration interface.

ScripConditions


The following ScripConditions have been contributed back into RT. Please note that this is not a complete list.

Crontab - Perform certain Scrips at certain times of day.


Applicable to: RT 2.0.x
Short description: This will give the ability to perform certain conditions at certain times of the day, utilising a format very similar to crontab(5). This refers to the time when a transaction is triggered, not that a Scrip will be executed at a particular time.
Full Readme and Database Insertion Script
Download Crontab.pm

IncomingEmail - Did this transaction come in via email or Web?


Applicable To: RT 2.0.x
Short description: fire if the transaction originated via Email
Full Readme and Database insertion Script
Download IncomingEmail.pm

OwnerChange - decide whether the Owner of a ticket is being changed.


Applicable to: RT 2.0.x
Short Description: This ScripCondition fires if the ownership of a ticket is being changed. Useful for using with the NotifyOldOwner ScripAction
Full Readme and Database insertion script
Download OwnerChange.pm

ScripActions


The following ScripActions have been contributed back into RT. Note that this is not a complete list.

AutoAssign - Automatically assign incoming tickets to a random person


Applicable to: RT 2.0.x
Short description: This just grabs the watchers for the queue, takes the users who are allowed to own tickets, and randomly gives it to somebody.
Full Readme and Database Insertion Script
Download AutoAssign.pm

NotifyOldOwner - Tell the previous owner when their ticket is taken away from them


Applicable to: RT 2.0.x
Short description: This Action can notify both the previous Owner and the New Owner of a ticket when Ownership is changed (give or steal).
Full Readme and Database insert script
Download NotifyOldOwner.pm

Web Interface



Short URLs to retrieve Ticket numbers and Queues.


Smylers has written an interesting hack to Apache (1.3+) using the mod_rewrite module.

The end effect is that your tickets can be retrieved by using http://rt.example.com/12345 and your queues can be retrieved by using http://rt.example.com/your-queue-name .

The additions needed to your RT's VirtualHost configuration are:

RewriteEngine On
# Treat a single number as a ticket number:
RewriteRule ^/([0-9]+)$ /Ticket/Display.html?id=$1 [L,R]
# Treat anything else that doesn't look like a file or directory as 
# a queue name:
RewriteRule ^/([^/.]+)$ /Search/Listing.html?ShowQueue=$1 [L,R]

You'll also need to add the following to webrt/Search/Listing.html:

  if (my $QueueName = $ARGS{ShowQueue})
  {
    $QueueName =~ tr/-_/  /;
    my $Queue = RT::Queue->new($session{CurrentUser});
    if ($Queue->Load($QueueName))
    {
      %ARGS =
      (
        ValueOfQueue => $Queue->Id,
        ValueOfStatus => 'open',
        ValueOfStatus => 'new',
        StatusOp => '=',
        QueueOp => '=',
        NewSearch => '1',
      );
    }
  }

The original posting to rt-users is available here

Michael Brown <mbrown@fensystems.co.uk> has tweaked the above "queue" RewriteRule so that no changes to Search/Listing are required:-

RewriteRule ^([^/.]+)$ /rt/Search/Listing.html?NewSearch=1&TicketsSortBy=Status&TicketsSortOrder=ASC&QueueOp==&ValueOfQueue=$1 [L,R]

Obviously there's a lot of room for fiddling here. Further permutations are left as an exercise for the reader.


Under the Hood



Tutorial on Creating ScripConditions


This documents how to create a basic ScripCondition yourself.  It uses a
comparitive minimum of features, and gives a rough overview of what
features you can use.  *Most* of what is described is also applicable to
ScripActions, with the exception that a ScripAction has two invoked
methods, 'Prepare()' and 'Commit()'.

Overview:

For a Scrip to be executed, a number of factors determine RT's choice,
before RT even loads the code behind a Scrip.

Firstly, the Scrip must either be Global, or specific to a Queue.

Secondly, the Transaction being created must be one of those described in
the ScripCondition's ApplicableTransType field.

Thirdly, the ScripCondition's 'IsApplicable()' method must return true.
This is what we're going to cover.

If the ScripCondition's IsApplicable returns true, RT then proceeds to
load the appropriate ScripAction, Prepare() it, and if that was
successful, Commit() it.  ( The dry bit that does this is in
lib/RT/Transaction.pm - Create ).

All of the above (except for the public methods() ) are described in the
SQL tables concerning Scrips.  Lets have a look there:

SQL references; Scrips;

In the Scrips table, RT looks at the following:


mysql> select * from Scrips;
+----+----------------+-------------+-------------------+-------+----------+---------+---------+---------------+-------------+
| id | ScripCondition | ScripAction | Stage             | Queue | Template | Creator | Created | LastUpdatedBy | LastUpdated |
+----+----------------+-------------+-------------------+-------+----------+---------+---------+---------------+-------------+
|  1 |              1 |           1 | TransactionCreate |     2 |        1 |    NULL | NULL    |          NULL | NULL        |
[snip]

Firstly, we have the id field of the Scrip.  Secondly, we have the ids for
the ScripCondition and ScripAction.  These are used as indexes in the
ScripConditions and ScripActions tables.

Next, we have the stage of the Scrip.  Currently RT has only one Stage,
being the TransactionCreate, but has the possibility of being extended.

Then we have the Queue identifier (again, used as an index in the Queues
table) and the Template identifier (index into Templates).  The remaining
fields are merly informational, and we can ignore them in this context.

SQL References; ScripConditions:

The next step is to look at the ScripCondition.  This can be done
selecting the same identifier from the ScripConditions table as is
mentioned in the Condition (see the Scrips table):

mysql> select * from ScripConditions where id = '1';
+----+----------+--------------------------+----------------+----------+----------------------+---------+---------------------+---------------+---------------------+
| id | Name     | Description              | ExecModule     | Argument | ApplicableTransTypes | Creator | Created             | LastUpdatedBy | LastUpdated         |
+----+----------+--------------------------+----------------+----------+----------------------+---------+---------------------+---------------+---------------------+
|  1 | OnCreate | When a ticket is created | AnyTransaction | NULL     | Create               |       1 | 2001-12-23 21:25:18 |             1 | 2001-12-23 21:25:18 |
+----+----------+--------------------------+----------------+----------+----------------------+---------+---------------------+---------------+---------------------+

Again, we have the identifier of the ScripCondition.  We also have a
descriptive Name, and an expanded Description.  Next, we have an odd field
named 'ExecModule', being the real magic behind Scrips, and covered
further on.  Next we have an optional Argument, and a list of applicable

Transactions types.  The remaining fields are again, informational and not
covered here.

In this case, RT will load this Scrip when a Transaction is Created, hence
the name 'OnCreate' (descriptive only) and 'Create' in
ApplicableTransTypes (used by RT).

Loading a ScripCondition:

Now that we've read the SQL table to know what the ScripCondition is and
we're in an Applicable Transaction Type, the next task is to load it.
This is done via the LoadCondition() method in lib/RT/ScripCondition.pm,
aka:

	$self->ExecModule =~ /^(\w+)$/;	# match the word
	my $module = $1;		# store the word
	my $type = "RT::Condition::" . $module;	# Prepare the word

	# Use the word
	eval "require $type" || die "Require of $type failed.\n$@\n";

The above retrieves the name of the ExecModule from the database
('AnyTransaction'), and tries to 'require' it as
RT::Condition::AnyTransaction.  Replacing the '::'s with '/' indicates
that RT is reading in lib/RT/Condition/AnyTransaction.pm .

( Note that RT is also doing some other tricks to ensure that if RT is
  unable to load a ScripCondition, RT doesn't completely die.  Having bad
  code during testing is ok and won't badly affect your installation. )



Examining a ScripCondition:

Looking at our example Condition (lib/RT/Condition/AnyTransaction.pm), we
can see that its pretty simple.

The essential part of it is the following:

	# Declare what we are:
	package RT::Condition::AnyTransaction;

	# Read in the Generic RT Goodness
	require RT::Condition::Generic;

	# Load the Generic RT Goodness into our namespace
	@ISA = qw(RT::Condition::Generic);

The above lines (note that I've put in extra comments for clarity) do most
of the footwork behind a ScripCondition.  We'll cover some of what
RT::Condition::Generic does for you later.

Next in the code is the public method, 'IsApplicable()'.  Back in
lib/RT/Transaction.pm, you can see that RT calls the 'IsApplicable()'
method to see if RT should execute the ScripAction.  In AnyTransaction,
IsApplicable is extremely simple:

	sub IsApplicable {
		# Get our OO $self
		my $self = shift;

		# This routine returns true in any case.
		return(1);
	}

Writing our Own ScripCondition:

In this example, we're going to create our own ScripCondition.  As this is

a step-by-step example, we're not going to do anything complicated, but we
are going to do something useful.

In a typical RT install, a Scrip is supplied that reads something like:

	OnCreate AutoreplyToRequestors with template Autoreply

This gives an Autoreply each time that a transaction is created, normally
via Email.  Wouldn't it be useful to give a different AutoReply if a
request came in outside Office Hours?

So, we're going to create a ScripCondition that checks what time it is,
and we'll eventually end up with:

	OnCreateOfficeHours AutoreplyToRequestors with template Autoreply-hours
	OnCreateOutsideHours AutoreplyToRequestors with template Autoreply-outside

Sounds easy, right?

The Right Place to Put ScripConditions:

The correct place to put ScripConditions is in the 'Condition'
subdirectory of your RT's library, ie
'lib/RT/Condition/MyScripCondition.pm'.  Personally, I'm a great believer
of maintaining a seperate place for localisations, and I have the
following setup:

	/home/rt2/etc/site_scrips/Condition/MyLocalCondition.pm
	/home/rt2/lib/RT/Condition/MyLocalCondition.pm --symlink-to-->
		/home/rt2/etc/site_scrips/Condition/MyLocalCondition.pm

( This is so that when I upgrade, I just have to recreate the symbolic
  links, and not my localisations )

Everyone has their own method, however RT must be able to load

MyLocalCondition.pm from 'lib/RT/Condition/MyLocalCondition.pm'.

Example ScripCondition; Crontab.pm:

Bearing the above in mind, we're going to create our own ScripCondition
named 'Crontab.pm'.  Although we're going to appear to end up with 2
ScripConditions, we only need one ExecModule (RT::Condition::Crontab.pm).
Later, we'll be creating two ScripConditions that will have the same
ExecModule, but will differ in their Names, Descriptions and Arguments.

( Code Re-use is Good for you )

Firstly, we set up ourselves:

	# ScripCondition: Crontab.pm
	# IsApplicable returns true if the current time is within the
	# range specified by the Argument, undef otherwise.

	# Bruce Campbell says to always give yourself Credit, and
	# to use RCS (man rcs):
	# $Id$

	# Who are we?
	package RT::Condition::Crontab;

	# Bring all the RT Goodness into our lives.
	require RT::Condition::Generic;

	# and our namespace
	@ISA = qw(RT::Condition::Generic);

	# We're going to use this later.
	use Set::Crontab;

Now we've got our preparations out of the way, we can do some work.
Remember that the only public method is the 'IsApplicable()' one:


	# Override RT::Condition::Generic's IsApplicable method:

	sub IsApplicable {
		# Get our OO $self.
		my $self = shift;

		# Prepare our default return value
		my $retval = undef;

Now what?  We need to get the current time.  We'll use RT::Date for this.

		my $cur_time = new RT::Date( $RT::Nobody );
		$cur_time->SetToNow();

Next, we need to find out the time ranges that we're valid for.  For this,
we'll use the Argument that will be stored in the ScripConditions table
later.  We'll use the standard crontab specifiers as they're simple, and a
'man crontab' will give the usage.

To allow for multiple time ranges in one ScripCondition, we'll use the ':'
character, so:

		my @time_ranges = split( ':', $self->Argument );

Huh?  Where do we get the Argument from?  Its imported from
RT::Condition::Generic, as a helper method for getting values from
SQL concerning the current ScripCondition, Transaction (etc).

Next, we need to see if 'Now' is within any of the ranges given above.
For this, we need to use the nice Set::Crontab module from CPAN.  Do a
"perl -MCPAN -e'install Set::Crontab'" to install this.

		# Prepare 'now'.
		# We'll use localtime on the Unix() method of RT::Date
		my @nowlist = localtime( $cur_time->Unix );


		# Build up a compare list.
		my @wantlist = ();

		# Names are not supported in Set::Crontab
		$wantlist[0] = $nowlist[1];	# minute 0-59
		$wantlist[1] = $nowlist[2];	# hour 0-23
		$wantlist[2] = $nowlist[3];	# day of month 1-31
		$wantlist[3] = $nowlist[4] + 1;	# month 1-12
		$wantlist[4] = $nowlist[6];	# day of week 0-7

We also need to set up the known ranges for cron

		# Known ranges
		my @known_ranges = (
				[0..59],        # minute 0-59
				[0..23],        # hour 0-23
				[1..31],        # day of month 1-31
				[1..12],        # month 1-12
				[0..7],         # day of week 0-7
				);


We loop through the various @time_ranges that we've found.

		foreach my $this_range ( @time_ranges ){

			# Remove leading or trailing space (we split() on that later)
			$this_range =~ s/^\s*//g;
			$this_range =~ s/\s*$//g;

			# How many of the fields have we matched so far?
			my $matched = 0;

			# Split apart the fields based on whitespace.
			my @this_list = split( /\s+/, $this_range );

And we also loop through each field in the time range.

			# See if each field is in within range.
			my $loop = 0;
			while( ( $loop < scalar @wantlist ) &&
				( $loop < scalar @this_list ) ){
				my $tst = Set::Crontab->new(
					$this_list[$loop],
					$known_ranges[$loop] );
				if( $tst->contains(
					$wantlist[$loop] ){
					$matched++;
				}

Always remember to keep moving forward.

				# Increment the loop
				$loop++;

			}

Now we check to see whether we matched them all.  If we didn't, its
obviously not this time range.

			# Did we match them all?
			if( ( $matched == $loop ) &&
				( $loop == scalar @wantlist ) ){
				# We did.  Set our return value.
				$retval++;
			}
		}

Finally, we return with our results.

		# retval is undef when declared, and only incremented
		# if a time value matches.
		return( $retval );

	}

The very last thing we need in Crontab.pm is to return true for the
'require'.  So, we put:

	1;

At this point, it might be best to grab the working version from:

	http://www.fsck.com/pub/contrib/2.0/

( Note, I'm not sure where Jesse is going to put it )

Inserting a ScripCondition into the Database; Template:

Now that we have our Code to execute the ScripCondition, we need to Create
the Condition in the SQL database.  With RT2.0.x, there is an interface
for Creating Scrips, but no easy method of calling it from the Web or
command line interfaces.

Instead, we're going to write a perl program to do it for us.  Save this
as 'insert_condition_Crontab_Template.pl':

	#!/usr/bin/perl -w

Remember to change these two variables as needed.

	# Where is our RT library?
	use lib "!!CHANGE_ME_TO_RT_LIB_DIRECTORY!!";

	# Where is our configuration?
	use lib "!!CHANGE_ME_TO_RT_ETC_DIRECTORY!!";

	# Get our base configuration (config.pm)
	use config;


	# pretty errors.
	use Carp;

	# use a few more things related to RT
	use RT::Handle;
	use RT::User;
	use RT::CurrentUser;

	# Connect to the Database (low level)
	$RT::Handle = new RT::Handle( $RT::DatabaseType );
	$RT::Handle->Connect;

	#Put together a current user object so we can create a User object
	my $CurrentUser = new RT::CurrentUser();

	#now that we bootstrapped that little bit, we can use the standard RT cli
	# helpers  to do what we need

	use RT::Interface::CLI  qw(CleanEnv LoadConfig DBConnect
        	                   GetCurrentUser GetMessageContent);

	#Clean out all the nasties from the environment
	CleanEnv();

	#Load etc/config.pm and drop privs
	LoadConfig();

	#Connect to the database and get RT::SystemUser and RT::Nobody loaded
	DBConnect();

	# Load the System User
	$CurrentUser->LoadByName('RT_System');

At this point, RT has connected to the database as the System User. From
here, we need to define what we put into the database:

	# The Argument for Crontab.pm is a ':' seperated list of

	# crontab-like entries.  See 'man crontab' (5) for a full
	# description of crontab configuration.
	my @ScripConditions = (
			       {
				Name => 'OnCreateHours',
				Description => 'Matches at any time',
				ApplicableTransTypes => 'Create',
				Argument => '* * * * *',
				ExecModule => 'Crontab',
			       },
			      );

And finally, we need to put it into the database:

	# The above is the user changable stuff.  Now create a Scrip with
	# the supplied data
	print "Creating ScripConditions...";

	use RT::ScripCondition;
	for $item (@ScripConditions) {
	    my $new_entry = new RT::ScripCondition($CurrentUser);
	    my $return = $new_entry->Create(%$item);
	    print $return.".";
	}

	print "done.\n";

	$RT::Handle->Disconnect();

	1;

Save that file as 'insert_condition_Crontab_Template.pl' (I like to keep it
these in /home/rt2/etc/site_scrips/notes/ myself, but you may have
different ideas of course), and proceed to the next step.

This should also be available from the same directory on www.fsck.com that
you found Crontab.pm.

Inserting a ScripCondition into the Database; Specifics:

Now that we have a Template for creating a ScripCondition, we're going to
use this to create two ScripConditions, 'OnCreateOfficeHours' and
'OnCreateOutsideHours' as suggested earlier.

To do this, make two copies of the Template as
'insert_condition_OnCreateOfficeHours.pl' and
'insert_condition_OnCreateOutsideHours.pl'.  Note that the names of the
files is arbitary, but simply helps to remember what you're dealing with.

Within the OfficeHours one, replace the Name, Description and Argument as
follows:

		Name => 'OnCreateOfficeHours',
		Description => 'Fires when a Create is done within Office \
				Hours',
		Argument => '* 9-16 * * 1-5',

Change as appropriate if your office hours are outside the sterotypical
9-5.  For example, the following describes 8:45am to 17:34pm:

		Argument => '45-59 8 * * 1-5:* 9-16 * * 1-5:0-15 17 * * 1-5',

Within the OutsideHours one, replace the Name, Description and Argument as
follows:

		Name => 'OnCreateOutsideHours',
		Description => 'Fires when a Create is done outside Office \
				Hours',
		Argument => '* 0-8 * * 1-5:* 17-23 * * 1-5:* * * * 0,6',

All that you need to do now is to run these perl programs (but not the
Template one), and you've created two new ScripConditions.


Using your New ScripConditions:

Using these ScripConditions is as easy as using any normal ScripCondition.
Firstly, you need to create two new Templates, 'Autoreply-hours' and
'Autoreply-outside' with appropriate texts, ie:

	Autoreply-hours:

		Greetings,

		This message has been automatically generated in response
		to the creation of a trouble ticket regarding:
			"{$Ticket->Subject()}" .

		We are on duty now, so you should receive a reply within
		the next hour.

		(etc)

	Autoreply-outside

		Greetings,

		This message has been automatically generated in response
		to the creation of a trouble ticket regarding:
			"{$Ticket->Subject()}" .

		Our office hours are between 9am and 5pm weekdays, and
		our local timezone is CET (GMT+0100).  We will answer your
		query on the next working day.

		(etc)

Finally, you need to create the Scrips using the Templates and the
Conditions that we've written, as:

	OnCreateOfficeHours AutoreplyToRequestors with template Autoreply-hours

	OnCreateOutsideHours AutoreplyToRequestors with template Autoreply-outside

Thats all there is to it.  Really.

Other things from RT::Condition::Generic:

The Generic module for ScripConditions handles all of the setup for that you
need in order to create your own ScripCondition.  By 'require'ing it, you
can just override what you need, normally just the 'IsApplicable' method.

The useful methods in Generic are the Argument() which we've seen above,
TicketObj() which gives you access to the current Ticket object, and
TransactionObj() which gives you access to the current Transaction object.

Both of these can be referenced from within
RT::Condition::MyLocalCondition as $self->TicketObj or
$self->TransactionObj.  Because they are references to Ticket and
Transaction objects, you can do neat things like:

	if( $self->TransactionObj->CreatorObj->EmailAddress() =~ /magic/ ){

At this point, the best method to learn is to use some examples.  Plenty
of examples can be found on the Rt distribution site, at
http://www.fsck.com/pub/rt/contrib/2.0 .  As a guideline for understanding
the code, always remember that '$self' refers to something within
ourselves (man perlboot).

If you see a reference that you don't get, search through the other files
that have been 'require'd or 'use'd until you find it.  Sometimes you will
have to go back into the code to find out where something is defined.

For example, RT::Condition::Generic uses $self->{'TransactionObj'}.  This
gets loaded by RT::ScripCondition->LoadCondition(), which is handed the
Transaction Object when called.  The only place LoadCondition() is called
is within RT::Transaction->Create(), which passes a copy of itself.
phew.  (theres no easy way of describing it.  Use grep over the whole
lib/RT tree if you have to)

Installing RT without setgid Perl or mod_perl


[ This was originally posted to rt-users ]
[ TODO - break into component parts ]

From roth@XXXX.XXX Wed Apr 24 11:39:12 2002
Date: Thu, 18 Apr 2002 18:00:33 -0500
From: Mark D. Roth 
To: rt-users@lists.fsck.com
Subject: [rt-users] RFC: RT install w/o setgid Perl or mod_perl

I've only recently discovered RT and joined the list, so I apologize
in advance if any of this is an FAQ.  I didn't see anything similar in
the list archives, and I thought people might be interested in how I
have RT (2.0.13) set up.  I'd also like to hear any suggestions that
anyone may have on how to improve my configuration.


Background Information
----------------------

My group's primary function is to run systems that host central
services such as mail, LDAP, news, web servers, etc.  We don't run the
applications that run on our systems; we just provide the underlying
platform on which our customers install and manage their
applications.

As a result of this division of responsibilities, we've developed a
lot of infrastructure to allow our customers to control as much of
their software and data as possible, even though they don't have root
access.  In particular, we create a unique pseudo-user for each
application, and we try to install everything related to the
application in the pseudo-user's home directory.  The customer then
su's to the pseudo-user to manage the service.

For example, for our LDAP service, we created a pseudo-user named
"ldap" with home directory "/services/ldap".  All of the LDAP server
software, directory data, and support scripts are owned by this user
and installed in its home directory.

For web-based services, we've established this convention for
URL-to-directory mappings:

   DIRECTORY                        URL
   ----------------------------------------------------------------
   ~pseudouser/public_html/http     http://hostname/pseudouser/
   ~pseudouser/public_html/https    https://hostname/pseudouser/

Rather than just using an Apache "Alias" directive to establish this
mapping, we use mod_rewrite to rewrite the requested URL internally
using ~pseudouser notation.  This allows us to use Apache's suEXEC
mechanism to invoke CGI scripts as the appropriate pseudo-user, rather
than as user httpd.


Goals for RT Installation
-------------------------

Given that we already have infrastructure in place for running
applications as pseudo-users, I wanted to use the same model for my RT
installation.  In particular, I had two main goals:

 1. Install RT under a single pseudo-user account and run every part
    of it as that user.  (I created a pseudo-user named "rt" with a
    home directory of "/services/rt" for this purpose.)

 2. Use my site's existing perl and apache packages instead of
    creating special builds just for RT.  (This means no set[ug]id
    perl scripts and no mod_perl.)

These goals presented several interesting challenges.


Installation Process
--------------------

Because the RT installation procedure assumes that you'll be running
different parts as different users, it includes commands that chown
various files, so it must be run as root.  After closely scrutinizing
what it was going to do, I ran it as root, but quickly chown'ed all of
the installed files to user rt.  I also removed all of the set[ug]id
bits.


RT Mail Gateway
---------------

Since rt-mailgate could not be a setgid script, I needed to find a way
to run it as user "rt".  The RT docs say to run it directly from
sendmail's aliases file, as follows:

  sysmgr: "|/services/rt/rt2/bin/rt-mailgate --queue sysmgr --action correspond"

That would result in rt-mailgate being run as user daemon.  To get
around this, I changed the alias to point to the rt user:

  sysmgr: rt

Then, as user rt, I use procmail to run rt-mailgate with the
appropriate options depending on what address the message was sent to.
Thus, rt-mailgate gets run as user rt.


RT Web Interface
----------------

The next problem was getting the web interface to run as user rt
instead of as user httpd.  In this case, even though I didn't want to
use mod_perl in the first place, the fact that I couldn't worked out
in my favor, since FastCGI can be configured to use Apache's suEXEC
mechanism to run external programs as alternate users.

The first problem I ran into in setting up FastCGI was a mod_perl
dependency problem in mason_handler.fcgi.  Commenting out this line
seemed to fix it:

   use HTML::Mason::ApacheHandler;

(I hope commenting this out doesn't have any bad side-effects, but I
haven't noticed any problems so far.  Is this a bug, or is there
something weird about my setup?)

Next, in order to be processed by our mod_rewrite rules and suEXEC,
the FastCGI handler had to be installed in ~rt/public_html/https.
However, since I had installed RT in ~rt/rt2, I solved this problem
with a hard link.  (The down-side of this is that I'll probably need
to recreate the hard link when I upgrade RT.)

Finally, I needed to create a symlink ~rt/public_html/https/images ->
~rt/rt2/WebRT/html/images so that the images would be accessible.

Here are the relevant snippets from httpd.conf:

--------------------------------------------------------------------------
LoadModule fastcgi_module            /usr/local/libexec/mod_fastcgi.so

AddModule mod_fastcgi.c


    AddHandler fastcgi-script .fcgi
    FastCgiWrapper /usr/local/sbin/suexec
    FastCgiIpcDir /var/lib/httpd


    
        RewriteEngine on

        ### Allow the translated subrequest to pass straight through.
        RewriteCond %{IS_SUBREQ}                =true
        RewriteRule ^/~rt/https/ -       [L,PT]

        ### In case the rewritten URI leaks out to the browser,
        ### redirect it back to the advertised location.
        RewriteRule ^/~(rt)(/https)?(/(.*))?$      /$1/$4    [R=permanent,L]

        ### Redirect to add trailing '/' for directory names.
        RewriteRule ^/(rt)$           /$1/      [R=permanent,L]

        ### Handle requests for the advertised URIs by translating them
        ### and passing them through in a subrequest.
        RewriteRule ^/rt/images(/(.*))?$        /~rt/https/images/$2     [L,PT]
        RewriteRule ^/rt/(.*)$  /~rt/https/mason_handler.fcgi/$1         [L,PT]
    
--------------------------------------------------------------------------

Running the RT web interface as user rt eliminated the need for the
WebRT/data and WebRT/sessiondata directories to be owned by user
httpd.


Conclusion and Suggestions
--------------------------

There were a lot of issues that needed to be identified and addressed
in order to install RT under its own pseudo-user.  However, once this
was done, the procedure was actually fairly easy.

Based on this experience, here are a few off-the-cuff suggestions, for
whatever they're worth:

 * Change the installation procedure to use autoconf.  This would make
   it easier to install different parts of RT in different locations
   and with different options.  It would also make it easier to build
   distributable packages of RT.

 * Change the installation procedure not to assume that different
   files and directories need to have different ownerships.  This
   would allow RT to be installed by a user other than root.

 * Improve the documentation.  In particular, the installation
   instructions need to be more precise and more detailed.


Well, I think that's it for now...  Please let me know if you have any
questions, comments, or suggestions.  Thanks!

-- 
Mark D. Roth 
http://www.feep.net/~roth/

_______________________________________________
rt-users mailing list
rt-users@lists.fsck.com
http://lists.fsck.com/mailman/listinfo/rt-users

Interesting Perlisms


placeholder

Reading and Writing to a pipe without blocking


#!/usr/bin/perl
# This will run a command, pipe stuff into it, collect the output, and NOT BLOCK whilst doing so.  Also has a timeout.
# Stuff for open2
use IPC::Open2;
# Stuff for waitpid
use POSIX ":sys_wait_h";
# Stuff for setting NONBLOCK on handles.
use Fcntl qw(F_GETFL F_SETFL O_NONBLOCK);

# Dummy commands.
my @Commands = ( "sort", "-u"  );

# Open the command with an input and output handle.  Save the process
# id of this.
$pid = open2(\*Reader, \*Writer, join( ' ', @Commands ) ) || die "boing";

my @foo = ();

# Dummy text to spit out.
open( INPUT, "dummy.input.file" );
my $input = "";
while( my $line =  ){
        chomp( $line );
        # push @foo, $line;
        $input .= $line . "\n";
}
close( INPUT );

my $timeout = 5;

# Get the flags.
my $flags = fcntl(Writer, F_GETFL, 0)
       or die "Can't get flags for the socket: $!\n";
# set NONBLOCK on the Writer.
$flags = fcntl(Writer, F_SETFL, $flags | O_NONBLOCK)
       or die "Can't set flags on the socket: $!\n";
$flags = fcntl(Reader, F_GETFL, 0)
       or die "Can't get flags for the socket: $!\n";
# set NONBLOCK on the Reader.
$flags = fcntl(Reader, F_SETFL, $flags | O_NONBLOCK)
       or die "Can't set flags on the socket: $!\n";

my $bytes_written = 0;
my $bytes_wrote = 0;
my $max_bytes = length( $input );
# Use 80 or the length.
my $chunk_size = ( 1024 < $max_bytes ) ? 1024 : $max_bytes;
my $output = "";
my $stillgoing = $timeout;

my $last_success = time;

while( $stillgoing ){

        # Attempt to write to the chunk size using syswrite
        my $these_bytes = undef;

        if( $bytes_written < $max_bytes ){
                my $this_chunk = ( ( $max_bytes - $bytes_written ) > $chunk_size ) ? $chunk_size : ( $max_bytes - $bytes_written );

                print "Using $this_chunk\n";
                $these_bytes = syswrite( Writer, $input, $this_chunk, $bytes_written );
        }
        if( defined( $these_bytes ) ){
                $bytes_written += $these_bytes;
                print "Wrote $these_bytes - $bytes_written\n";
                $last_success = time;
                if( $bytes_written >= $max_bytes ){
                        close( Writer );
                }
        }else{
                print "Could not write? - $bytes_written\n";
        }


        # Attempt to read from the input.
        my $this_output = "";
        $these_bytes = sysread( Reader, $this_output, $chunk_size );
        if( defined( $these_bytes ) ){
                $bytes_read += $these_bytes;
                print "Read $these_bytes - $bytes_read\n";
                if( $these_bytes == 0 ){
                        # end of file?
                        $stillgoing--;
                }
                $last_success = time;
        }else{
                print "Could not read? - $bytes_read\n";
                # if( eof( Reader ) && eof( Writer ) ){
                #       $stillgoing--;
                # }
        }

        unless( time < ( $last_success + $timeout ) ){
                # Timeout.  Send a TERM signal
                print STDERR "Timeout ($timeout, $stillgoing)\n";
                if( waitpid( $pid, WNOHANG ) == 0 ){
                        print STDERR "Timeout and process running, sending TERM\n";
                        kill( SIGTERM, $pid );
                }
                print STDERR "Timeout2 ($timeout, $stillgoing)\n";
                $stillgoing--;
        }
}
close( Writer );
close( Reader );
print "Finished?\n";

# Reap the child.
if( waitpid( $pid, WNOHANG ) == 0 ){
        # process is still running.  die.
        print STDERR "Process still running, sending kill\n";
        kill( SIGKILL, $pid );
}

Running RT from procmail, without using setuid/suid/setgid/sgid


Running RT from procmail, without using setuid/suid/setgid/sgid

This snippet below is what I use on my systems to run RT without relying on suid bits. The essential magic is to use procmail's magic based on the ownership of a file in /etc/procmailrcs/ (see procmail man page) to become the RT user.


aliases file:
	# Run rt2 as the rt2 user, not as root.
	test-rt2:           "| /usr/bin/procmail -m /etc/procmailrcs/rt2-test"

/etc/procmailrcs/rt2-test:

	# This file MUST be owned by the rt2 user.  See '-m' description
	# in the procmail man page.
	HOME=/home/rt2
	INCLUDERC=$HOME/.procmailrc-test

	# are we still here after including?  Dump it to someone
	:0
	! joe_bloggs@example.com

( The above two snippets gets procmail running, as the rt2 user, a
  procmail file for the specific queue.  I really really dislike relying
  on setuid stuff. )

/home/rt2/.procmailrc-test

	# This file does any filtering/mail manipulations/etc before
	# calling rt-mailgate.

	HOME=/home/rt2
	LOGFILE=log/procmail.log
	VERBOSE=no

	# Go away you naughty annoying spammer
	:0
	* ^From:.*badguy@example.com
	/dev/null

	# Place mailfiltering here
	:0 f
	| /home/rt2/bin/my_custom_filter_program

	# Accept the mail into mailgate.  We { } enclose it incase we want
	# a last-ditch filter.
	:0
	{
	  # Use a lockfile to ensure only one rt-mailgate process.
	  :0 :.procmail.lock
	  | /home/rt2/bin/rt-mailgate --queue TEST --action correspond

	  # Something went wrong.  Dump it to someone
	  :0e
	  ! joe.bloggs@example.com
	}

Contributing Back into RT


When extending RT for your own circumstances, please spare a moment to think about all of the other people who have also extended RT and provided their changes as patches back into the core RT, or as add-ons to the RT system. Unless your work is under an excessively strict Non-Disclosure Agreement, there should be nothing to stop you from contributing to RT and getting that warm inner glow of having helped someone else.

Firstly, decide whether you need to reinvent the wheel. Many things have been previously decide, some have been implemented and others have been dropped as a bad idea. Research is always important. If you have a good idea, chances are that you're not the only one, and it might even be mentioned within RT/FM (that 'Search' tab up the top is what you're looking for).

Secondly, keep track of your work. If you are fiddling with the core of RT, then please take the time to use a good Version Control System so you can understand what you have done, and can back out of any changes. Common, but still going strong, are 'rcs' and 'cvs'. Newer ones are 'ageis', 'bitkeeper' and a host of others.

Thirdly, make sure that you can provide a reference point. For changes to RT's core, this means a context 'diff' against a particular RT version, the later the better. Always remember to mention which version of RT you are using as a baseline.

Fourthly, you can always ask for help.

Fifthly, try to do as much testing as possible. If you cannot test your changes (huh?), make sure that you explicitly say 'This is untested code and may not work'. Someone else may fix it for you if they also see a need.

Sixthly, tell Jesse about it. Preferably, this is in the form of a complete description of what your change should do, what else is needed, your base version, and of course the caveats and security risks inherent in your change. Remember to use RT's own tracking system for this (using, curiously enough, RT), or a posting to the rt-devel@lists.fsck.com mailing list.