From Request Tracker Wiki
Revision as of 10:44, 27 January 2014 by (talk)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to navigation Jump to search


The code below didn't worked out of the box for me. I did some corrections, the code is available at: https://github.com/zito/rt-action-loopin. I'm have prepared it for RT 4.2.2.


We had a set of users who, when using our RT, would like to say they were "looping in" folks when they responded to tickets. This was a common practice in their regular mail, where the Cc list would be extended with new addresses and everyone would be sure to use Reply All for the latest message. After explaining repeatedly that RT doesn't work like that, I decided to find a way to make it work like that.

I started with the existing AddWatchersOnCorrespond. That action itsef points out there are security problems with letting people send mail with additional addresses that then are automagically added as watchers, so this action takes some care to limit who is allowed to loop addresses into the ticket. The addresses authorized include:

  • Requestor
  • Cc and AdminCc addresses

Additionally, there is a lookaside file with rules to permit addresses to loop in others within a specific domain. The permitted list can include specific authorized addresses or anyone at a domain.

RT::Action::LoopIn Module

package RT::Action::LoopIn;

# SEE: http://wiki.bestpractical.com/view/AddWatchersOnCorrespond

# This scrip will add new watchers based on message headers, but
# only if the actor is authorized (is a requestor, cc, or admincc).

use strict;

use base qw(RT::Action);

my $scrip = 'Scrip:LoopIn';

# {{{ sub Describe
   sub Describe  {
      my $self = shift;
      return (ref $self . " add new watchers from to/cc list if actor is authorized.");
   # }}}

# {{{ sub Prepare
   sub Prepare  {
      # nothing to prepare
      return 1;
   # }}}

sub Commit {
   my $self = shift;
   my $Transaction = $self->TransactionObj;
   my $ActorAddr = $Transaction->CreatorObj->EmailAddress;
   my $Queue = $self->TicketObj->QueueObj;
   my $Ticket = $self->TicketObj;
   my $Id = $self->TicketObj->id;
   my @Authorized;
   my @Unauthorized;

   # assume it is NOT valid to loop in additional addresses
   my $loopin_authorized = 0;

   $RT::Logger->debug("$scrip: about to check if creator is authorized");

   # load email alias file, if present
   my %loopauth;
   my $loopauthfile = RT->Config->Get('WM_LoopAuth');
   if ($loopauthfile and my $fh = IO::File->new($loopauthfile)) {
      while (<$fh>) {
         s/^\s*//;        # strip leading whitespace
         s/\s*$//;        # strip trailing whitespace
         next if /^$/;    # skip blank lines
         next if /^#/;    # skip comment lines
         if (/^equiv\s+(\S+)\s+(\S+)$/i) {
            $loopauth{equiv}{lc($1)} = $2;
         elsif (/^domain\s+(\S+)\s+(\S+)$/i) {
            $loopauth{domain}{lc($1)}->{lc($2)} = 1;
         else {
            $RT::Logger->error("$scrip: unknown loopauth directive: $_");

   # if actor is a requestor, cc or admincc, loopin is authorized
   if (my $Creator = $Transaction->CreatorObj) {
      my $Principal = $Creator->PrincipalId if $Creator->Id;
      $RT::Logger->debug("$scrip: creator principal ID: #$Principal");
      if (($Queue->IsCc($Principal) or
           $Queue->IsAdminCc($Principal) or
           $Ticket->IsCc($Principal) or
           $Ticket->IsAdminCc($Principal) or
          )) {
         $loopin_authorized = 1;
         $RT::Logger->debug("$scrip: $ActorAddr is authorized to loop in additional watchers");

   $RT::Logger->debug("$scrip: about to extract candidate address list");

   # extract a list of to and cc addresses associated with this transaction
   for my $h (qw(To Cc)) {
      my $header = $Transaction->Attachments->First->GetHeader($h);
      for my $addrObj (Mail::Address->parse($header)) {
         # extract and normalize email address
         my $addr = lc $RT::Nobody->UserObj->CanonicalizeEmailAddress($addrObj->address);

         # ignore the specific addresses for this queue:
         next if lc $Queue->CorrespondAddress eq $addr or lc $Queue->CommentAddress eq $addr;

         # ignore any email address that looks like one for ANY of our queues:
         next if RT::EmailParser::IsRTAddress(, $addr);

         # normalize address if equivalence is defined
         if (defined($loopauth{equiv}{$addr})) {
            $RT::Logger->debug("$scrip: normalizing $addr to $loopauth{equiv}{$addr}");
            $addr = $loopauth{equiv}{$addr};

         # ignore any email address that is already a watcher
         my $User = RT::User->new($RT::SystemUser);
         $User->LoadByEmail($addr);     # NOT LoadOrCreateByEmail
         my $Principal = $User->PrincipalId if $User->Id;
         next if ($Queue->IsCc($Principal) or
                  $Queue->IsAdminCc($Principal) or
                  $Ticket->IsCc($Principal) or
                  $Ticket->IsAdminCc($Principal) or

         # extend additional watchers list if authorized
         if ($loopin_authorized or domainauth($loopauth{domain}, $ActorAddr, $addr)) {
            $RT::Logger->debug("$scrip: Ticket #$Id correspondence contains header - $h: $addr");
            push @Authorized, $addr;
         else {
            push @Unauthorized, $addr;

   my $comment = "";

   if (@Unauthorized) {
      $comment .= "$ActorAddr made an unauthorized attempt to loop in the following:\n " . join("\n ", @Unauthorized) . "\n";

   $RT::Logger->debug("$scrip: about to add candidate addresses as watchers");

   # add authorized candidates not already listed as watchers
   if (@Authorized) {
      my @looped;       # list of looped addresses
      my @failed;       # list of failed addresses
      for my $addr (@Authorized) {
         my $User = RT::User->new($RT::SystemUser);
         my $Principal = $User->PrincipalId if $User->Id;
         # add the new watcher and check for errors
         my ($ret, $msg) = $Ticket->AddWatcher(
            Type  => 'Cc',
            Email => $addr,
            PrincipalId => $Principal,
         if ($ret) {
            $RT::Logger->info("$scrip: New watcher added to ticket #$Id: $addr (#$Principal)");
            push(@looped, $addr);
         } else {
            $RT::Logger->error("$scrip: Failed to add new watcher to ticket #$Id: $addr (#$Principal) - $msg");
            push(@failed, $addr);
      if (@looped) {
         $comment .= "$ActorAddr successfully looped in the following:\n  " . join("\n  ", @looped) . "\n";
      if (@failed) {
         $comment .= "$ActorAddr failed to loop in the following:\n  " . join("\n  ", @failed) . "\n";

   $Ticket->Comment(Content => $comment) if $comment;

sub domainauth {
   my $domainauth = shift;
   my $actor = shift;
   my $loopin = shift;
   my $loopin_domain;
   my $actor_domain;

   $RT::Logger->debug("$scrip: checking domainauth for $actor looping in $loopin");
   ($loopin_domain = $loopin) =~ s/^[^@]+@//;
   ($actor_domain = $actor) =~ s/^[^@]+@//;
   $RT::Logger->debug("$scrip: actor domain: $actor_domain, loopin domain: $loopin_domain");
   return 0 unless defined $domainauth;
   return 0 unless defined $domainauth->{$loopin_domain};
   return 1 if $domainauth->{lc($loopin_domain)}->{lc($actor)};
   return 1 if $domainauth->{lc($loopin_domain)}->{'*@'.lc($actor_domain)};
   return 0;

eval "require RT::Action::LoopIn_Vendor";
die $@ if ($@ && $@ !~ qr{^Can't locate RT/Action/LoopIn_Vendor.pm});
eval "require RT::Action::LoopIn_Local";
die $@ if ($@ && $@ !~ qr{^Can't locate RT/Action/LoopIn_Local.pm});

return 1;

LoopIn External Configuration

Additional rules can be defined for controlling access to LoopIn. The filename is defined in RT_SiteConfig.pm as follows:

Set($WM_LoopAuth, "/opt/rt3/etc/loopauth.cfg");

Note that the variable name includes a WM_ prefix for "Willing Minds", since I don't want to stomp on anything that might be defined by Best Practical or others later.

A sample copy of this file with directive documentation is included below.

# loopin address equivalences
# This defines address normalization rules to avoid adding duplicate 
# watchers.
# Format:
# If ORIGADDR is being looped in, it will be normalized to NORMADDR
# before the existing watcher list is checked.

# normalize foo.com users with example.com addresses
equiv   user1@example.com   user1@foo.com
equiv   user2@example.com   user2@foo.com

# loopin domain permissions
# Format:
# If fromaddr includes To or Cc addresses that is in DOMAIN not on the
# ticket, then those addresses will be looped into the ticket.  The
# special case '*@domain' format may be used for FROMADDR to indicate
# that anyone in the domain is authorized.

# anyone at foo.com can loop in foo.com addresses
domain  foo.com    *@foo.com

# joe@bar.com can loop in baz.com addresses
domain  baz.com    joe@bar.com