diff options
author | ivan <ivan> | 2009-12-31 13:16:41 +0000 |
---|---|---|
committer | ivan <ivan> | 2009-12-31 13:16:41 +0000 |
commit | b4b0c7e72d7eaee2fbfc7022022c9698323203dd (patch) | |
tree | ba4cd21399e412c32fe3737eaa8478e3271509f9 /rt/lib/RT/Action | |
parent | 2dfda73eeb3eae2d4f894099754794ef07d060dd (diff) |
import rt 3.8.7
Diffstat (limited to 'rt/lib/RT/Action')
-rw-r--r-- | rt/lib/RT/Action/AutoOpen.pm | 52 | ||||
-rwxr-xr-x | rt/lib/RT/Action/Autoreply.pm | 63 | ||||
-rw-r--r-- | rt/lib/RT/Action/CreateTickets.pm | 265 | ||||
-rw-r--r-- | rt/lib/RT/Action/EscalatePriority.pm | 11 | ||||
-rw-r--r-- | rt/lib/RT/Action/ExtractSubjectTag.pm | 103 | ||||
-rwxr-xr-x | rt/lib/RT/Action/Generic.pm | 177 | ||||
-rwxr-xr-x | rt/lib/RT/Action/LinearEscalate.pm | 279 | ||||
-rwxr-xr-x | rt/lib/RT/Action/Notify.pm | 76 | ||||
-rwxr-xr-x | rt/lib/RT/Action/NotifyAsComment.pm | 10 | ||||
-rw-r--r-- | rt/lib/RT/Action/NotifyGroup.pm | 209 | ||||
-rw-r--r-- | rt/lib/RT/Action/NotifyGroupAsComment.pm | 91 | ||||
-rw-r--r-- | rt/lib/RT/Action/RecordComment.pm | 9 | ||||
-rw-r--r-- | rt/lib/RT/Action/RecordCorrespondence.pm | 9 | ||||
-rw-r--r-- | rt/lib/RT/Action/ResolveMembers.pm | 9 | ||||
-rwxr-xr-x | rt/lib/RT/Action/SendEmail.pm | 1055 | ||||
-rw-r--r-- | rt/lib/RT/Action/SetPriority.pm | 9 | ||||
-rw-r--r-- | rt/lib/RT/Action/UserDefined.pm | 9 |
17 files changed, 1398 insertions, 1038 deletions
diff --git a/rt/lib/RT/Action/AutoOpen.pm b/rt/lib/RT/Action/AutoOpen.pm index 004ed13cc..e1cf0ae7c 100644 --- a/rt/lib/RT/Action/AutoOpen.pm +++ b/rt/lib/RT/Action/AutoOpen.pm @@ -1,8 +1,8 @@ # BEGIN BPS TAGGED BLOCK {{{ # # COPYRIGHT: -# -# This software is Copyright (c) 1996-2009 Best Practical Solutions, LLC +# +# This software is Copyright (c) 1996-2009 Best Practical Solutions, LLC # <jesse@bestpractical.com> # # (Except where explicitly superseded by other copyright notices) @@ -45,28 +45,25 @@ # those contributions and any derivatives thereof. # # END BPS TAGGED BLOCK }}} -# This Action will open the BASE if a dependent is resolved. +# This Action will open the BASE if a dependent is resolved. package RT::Action::AutoOpen; -require RT::Action::Generic; use strict; -use vars qw/@ISA/; -@ISA=qw(RT::Action::Generic); +use warnings; -#Do what we need to do and send it out. +use base qw(RT::Action); -#What does this type of Action does +=head1 DESCRIPTION -# {{{ sub Describe -sub Describe { - my $self = shift; - return (ref $self ); -} -# }}} +Opens a ticket unless it's allready open, but only unless transaction +L<RT::Transaction/IsInbound is inbound>. +Doesn't open a ticket if message's head has field C<RT-Control> with +C<no-autoopen> substring. + +=cut -# {{{ sub Prepare sub Prepare { my $self = shift; @@ -83,22 +80,21 @@ sub Prepare { return 1; } -# }}} sub Commit { my $self = shift; - my $oldstatus = $self->TicketObj->Status(); - $self->TicketObj->__Set( Field => 'Status', Value => 'open' ); - $self->TicketObj->_NewTransaction( - Type => 'Status', - Field => 'Status', - OldValue => $oldstatus, - NewValue => 'open', - Data => 'Ticket auto-opened on incoming correspondence' - ); - - - return(1); + + my $oldstatus = $self->TicketObj->Status; + $self->TicketObj->__Set( Field => 'Status', Value => 'open' ); + $self->TicketObj->_NewTransaction( + Type => 'Status', + Field => 'Status', + OldValue => $oldstatus, + NewValue => 'open', + Data => 'Ticket auto-opened on incoming correspondence' + ); + + return 1; } eval "require RT::Action::AutoOpen_Vendor"; diff --git a/rt/lib/RT/Action/Autoreply.pm b/rt/lib/RT/Action/Autoreply.pm index ea56b9f5b..3734d819a 100755 --- a/rt/lib/RT/Action/Autoreply.pm +++ b/rt/lib/RT/Action/Autoreply.pm @@ -1,8 +1,8 @@ # BEGIN BPS TAGGED BLOCK {{{ # # COPYRIGHT: -# -# This software is Copyright (c) 1996-2009 Best Practical Solutions, LLC +# +# This software is Copyright (c) 1996-2009 Best Practical Solutions, LLC # <jesse@bestpractical.com> # # (Except where explicitly superseded by other copyright notices) @@ -45,12 +45,13 @@ # those contributions and any derivatives thereof. # # END BPS TAGGED BLOCK }}} + package RT::Action::Autoreply; -require RT::Action::SendEmail; use strict; -use vars qw/@ISA/; -@ISA = qw(RT::Action::SendEmail); +use warnings; + +use base qw(RT::Action::SendEmail); =head2 Prepare @@ -95,43 +96,37 @@ Set this message\'s return address to the apropriate queue address sub SetReturnAddress { my $self = shift; - my %args = ( is_comment => 0, - @_ - ); - my $replyto; - if ($args{'is_comment'}) { - $replyto = $self->TicketObj->QueueObj->CommentAddress || - $RT::CommentAddress; - } - else { - $replyto = $self->TicketObj->QueueObj->CorrespondAddress || - $RT::CorrespondAddress; - } - - unless ($self->TemplateObj->MIMEObj->head->get('From')) { - if ($RT::UseFriendlyFromLine) { - my $friendly_name = $self->TicketObj->QueueObj->Description || + my $friendly_name; + + if (RT->Config->Get('UseFriendlyFromLine')) { + $friendly_name = $self->TicketObj->QueueObj->Description || $self->TicketObj->QueueObj->Name; - $friendly_name =~ s/"/\\"/g; - $self->SetHeader( 'From', - sprintf($RT::FriendlyFromLineFormat, - $self->MIMEEncodeString( $friendly_name, $RT::EmailOutputEncoding ), $replyto), - ); } - else { - $self->SetHeader( 'From', $replyto ); - } - } - - unless ($self->TemplateObj->MIMEObj->head->get('Reply-To')) { - $self->SetHeader('Reply-To', "$replyto"); - } + + $self->SUPER::SetReturnAddress( @_, friendly_name => $friendly_name ); } # }}} +# {{{{ sub SetRTSpecialHeaders + +=head2 SetRTSpecialHeaders + +Set the C<Auto-Generated> header to C<auto-replied>, in accordance +with RFC3834. + +=cut + +sub SetRTSpecialHeaders { + my $self = shift; + $self->SUPER::SetRTSpecialHeaders(@_); + $self->SetHeader( 'Auto-Submitted', 'auto-replied' ); +} + +# }}} + eval "require RT::Action::Autoreply_Vendor"; die $@ if ($@ && $@ !~ qr{^Can't locate RT/Action/Autoreply_Vendor.pm}); eval "require RT::Action::Autoreply_Local"; diff --git a/rt/lib/RT/Action/CreateTickets.pm b/rt/lib/RT/Action/CreateTickets.pm index 40d18d357..4883ae3a8 100644 --- a/rt/lib/RT/Action/CreateTickets.pm +++ b/rt/lib/RT/Action/CreateTickets.pm @@ -1,8 +1,8 @@ # BEGIN BPS TAGGED BLOCK {{{ # # COPYRIGHT: -# -# This software is Copyright (c) 1996-2009 Best Practical Solutions, LLC +# +# This software is Copyright (c) 1996-2009 Best Practical Solutions, LLC # <jesse@bestpractical.com> # # (Except where explicitly superseded by other copyright notices) @@ -45,13 +45,12 @@ # those contributions and any derivatives thereof. # # END BPS TAGGED BLOCK }}} + package RT::Action::CreateTickets; -require RT::Action::Generic; +use base 'RT::Action'; use strict; use warnings; -use vars qw/@ISA/; -@ISA = qw(RT::Action::Generic); use MIME::Entity; @@ -106,10 +105,12 @@ of perl inside the Text::Template using {} delimiters, but that such sections absolutely can not span a ===Create-Ticket boundary. After each ticket is created, it's stuffed into a hash called %Tickets -so as to be available during the creation of other tickets during the same -ScripAction. The hash is prepopulated with the ticket which triggered the -ScripAction as $Tickets{'TOP'}; you can also access that ticket using the -shorthand TOP. +so as to be available during the creation of other tickets during the +same ScripAction, using the key 'create-identifier', where +C<identifier> is the id you put after C<===Create-Ticket:>. The hash +is prepopulated with the ticket which triggered the ScripAction as +$Tickets{'TOP'}; you can also access that ticket using the shorthand +TOP. A simple example: @@ -164,8 +165,9 @@ A convoluted example ENDOFCONTENT ===Create-Ticket: two Subject: Manager approval + Type: approval Depended-On-By: TOP - Refers-On: {$Tickets{"approval"}->Id} + Refers-To: {$Tickets{"create-approval"}->Id} Queue: ___Approvals Content-Type: text/plain Content: @@ -236,236 +238,6 @@ Refers-To, RefersTo, refersto, refers-to and r-e-f-er-s-tO will all be treated as the same thing. -=begin testing - -ok (require RT::Action::CreateTickets); -use_ok(RT::Scrip); -use_ok(RT::Template); -use_ok(RT::ScripAction); -use_ok(RT::ScripCondition); -use_ok(RT::Ticket); - -my $approvalsq = RT::Queue->new($RT::SystemUser); -$approvalsq->Create(Name => 'Approvals'); -ok ($approvalsq->Id, "Created Approvals test queue"); - - -my $approvals = -'===Create-Ticket: approval -Queue: ___Approvals -Type: approval -AdminCc: {join ("\nAdminCc: ",@admins) } -Depended-On-By: {$Tickets{"TOP"}->Id} -Refers-To: TOP -Subject: Approval for ticket: {$Tickets{"TOP"}->Id} - {$Tickets{"TOP"}->Subject} -Due: {time + 86400} -Content-Type: text/plain -Content: Your approval is requested for the ticket {$Tickets{"TOP"}->Id}: {$Tickets{"TOP"}->Subject} -Blah -Blah -ENDOFCONTENT -===Create-Ticket: two -Subject: Manager approval. -Depended-On-By: approval -Queue: ___Approvals -Content-Type: text/plain -Content: -Your minion approved ticket {$Tickets{"TOP"}->Id}. you ok with that? -ENDOFCONTENT -'; - -ok ($approvals =~ /Content/, "Read in the approvals template"); - -my $apptemp = RT::Template->new($RT::SystemUser); -$apptemp->Create( Content => $approvals, Name => "Approvals", Queue => "0"); - -ok ($apptemp->Id); - -my $q = RT::Queue->new($RT::SystemUser); -$q->Create(Name => 'WorkflowTest'); -ok ($q->Id, "Created workflow test queue"); - -my $scrip = RT::Scrip->new($RT::SystemUser); -my ($sval, $smsg) =$scrip->Create( ScripCondition => 'On Transaction', - ScripAction => 'Create Tickets', - Template => 'Approvals', - Queue => $q->Id); -ok ($sval, $smsg); -ok ($scrip->Id, "Created the scrip"); -ok ($scrip->TemplateObj->Id, "Created the scrip template"); -ok ($scrip->ConditionObj->Id, "Created the scrip condition"); -ok ($scrip->ActionObj->Id, "Created the scrip action"); - -my $t = RT::Ticket->new($RT::SystemUser); -my($tid, $ttrans, $tmsg) = $t->Create(Subject => "Sample workflow test", - Owner => "root", - Queue => $q->Id); - -ok ($tid,$tmsg); - -my $deps = $t->DependsOn; -is ($deps->Count, 1, "The ticket we created depends on one other ticket"); -my $dependson= $deps->First->TargetObj; -ok ($dependson->Id, "It depends on a real ticket"); -unlike ($dependson->Subject, qr/{/, "The subject doesn't have braces in it. that means we're interpreting expressions"); -is ($t->ReferredToBy->Count,1, "It's only referred to by one other ticket"); -is ($t->ReferredToBy->First->BaseObj->Id,$t->DependsOn->First->TargetObj->Id, "The same ticket that depends on it refers to it."); -use RT::Action::CreateTickets; -my $action = RT::Action::CreateTickets->new( CurrentUser => $RT::SystemUser);; - -# comma-delimited templates -my $commas = <<"EOF"; -id,Queue,Subject,Owner,Content -ticket1,General,"foo, bar",root,blah -ticket2,General,foo bar,root,blah -ticket3,General,foo' bar,root,blah'boo -ticket4,General,foo' bar,,blah'boo -EOF - - -# Comma delimited templates with missing data -my $sparse_commas = <<"EOF"; -id,Queue,Subject,Owner,Requestor -ticket14,General,,,bobby -ticket15,General,,,tommy -ticket16,General,,suzie,tommy -ticket17,General,Foo "bar" baz,suzie,tommy -ticket18,General,'Foo "bar" baz',suzie,tommy -ticket19,General,'Foo bar' baz,suzie,tommy -EOF - - -# tab-delimited templates -my $tabs = <<"EOF"; -id\tQueue\tSubject\tOwner\tContent -ticket10\tGeneral\t"foo' bar"\troot\tblah' -ticket11\tGeneral\tfoo, bar\troot\tblah -ticket12\tGeneral\tfoo' bar\troot\tblah'boo -ticket13\tGeneral\tfoo' bar\t\tblah'boo -EOF - -my %expected; - -$expected{ticket1} = <<EOF; -Queue: General -Subject: foo, bar -Owner: root -Content: blah -ENDOFCONTENT -EOF - -$expected{ticket2} = <<EOF; -Queue: General -Subject: foo bar -Owner: root -Content: blah -ENDOFCONTENT -EOF - -$expected{ticket3} = <<EOF; -Queue: General -Subject: foo' bar -Owner: root -Content: blah'boo -ENDOFCONTENT -EOF - -$expected{ticket4} = <<EOF; -Queue: General -Subject: foo' bar -Owner: -Content: blah'boo -ENDOFCONTENT -EOF - -$expected{ticket10} = <<EOF; -Queue: General -Subject: foo' bar -Owner: root -Content: blah' -ENDOFCONTENT -EOF - -$expected{ticket11} = <<EOF; -Queue: General -Subject: foo, bar -Owner: root -Content: blah -ENDOFCONTENT -EOF - -$expected{ticket12} = <<EOF; -Queue: General -Subject: foo' bar -Owner: root -Content: blah'boo -ENDOFCONTENT -EOF - -$expected{ticket13} = <<EOF; -Queue: General -Subject: foo' bar -Owner: -Content: blah'boo -ENDOFCONTENT -EOF - - -$expected{'ticket14'} = <<EOF; -Queue: General -Subject: -Owner: -Requestor: bobby -EOF -$expected{'ticket15'} = <<EOF; -Queue: General -Subject: -Owner: -Requestor: tommy -EOF -$expected{'ticket16'} = <<EOF; -Queue: General -Subject: -Owner: suzie -Requestor: tommy -EOF -$expected{'ticket17'} = <<EOF; -Queue: General -Subject: Foo "bar" baz -Owner: suzie -Requestor: tommy -EOF -$expected{'ticket18'} = <<EOF; -Queue: General -Subject: Foo "bar" baz -Owner: suzie -Requestor: tommy -EOF -$expected{'ticket19'} = <<EOF; -Queue: General -Subject: 'Foo bar' baz -Owner: suzie -Requestor: tommy -EOF - - - - -$action->Parse(Content =>$commas); -$action->Parse(Content =>$sparse_commas); -$action->Parse(Content => $tabs); - -my %got; -foreach (@{ $action->{'create_tickets'} }) { - $got{$_} = $action->{'templates'}->{$_}; -} - -foreach my $id ( sort keys %expected ) { - ok(exists($got{"create-$id"}), "template exists for $id"); - is($got{"create-$id"}, $expected{$id}, "template is correct for $id"); -} - -=end testing =head1 AUTHOR @@ -541,16 +313,16 @@ sub Prepare { my $self = shift; unless ( $self->TemplateObj ) { - $RT::Logger->warning("No template object handed to $self\n"); + $RT::Logger->warning("No template object handed to $self"); } unless ( $self->TransactionObj ) { - $RT::Logger->warning("No transaction object handed to $self\n"); + $RT::Logger->warning("No transaction object handed to $self"); } unless ( $self->TicketObj ) { - $RT::Logger->warning("No ticket object handed to $self\n"); + $RT::Logger->warning("No ticket object handed to $self"); } @@ -575,7 +347,6 @@ sub CreateByTemplate { my @results; # XXX: cargo cult programming that works. i'll be back. - use bytes; local %T::Tickets = %T::Tickets; local $T::TOP = $T::TOP; @@ -637,7 +408,6 @@ sub UpdateByTemplate { my $top = shift; # XXX: cargo cult programming that works. i'll be back. - use bytes; my @results; local %T::Tickets = %T::Tickets; @@ -894,7 +664,7 @@ sub ParseLines { } ); - $RT::Logger->debug("Workflow: yielding\n$content"); + $RT::Logger->debug("Workflow: yielding $content"); if ($err) { $RT::Logger->error( "Ticket creation failed: " . $err ); @@ -990,6 +760,7 @@ sub ParseLines { TimeLeft => $args{'timeleft'}, InitialPriority => $args{'initialpriority'} || 0, FinalPriority => $args{'finalpriority'} || 0, + SquelchMailTo => $args{'squelchmailto'}, Type => $args{'type'}, ); @@ -1223,7 +994,7 @@ sub GetUpdateTemplate { my $mode = $LINKTYPEMAP{$type}->{Mode}; my $method = $LINKTYPEMAP{$type}->{Type}; - my $links; + my $links = ''; while ( my $link = $t->$method->Next ) { $links .= ", " if $links; diff --git a/rt/lib/RT/Action/EscalatePriority.pm b/rt/lib/RT/Action/EscalatePriority.pm index 46635df05..bf9de92c2 100644 --- a/rt/lib/RT/Action/EscalatePriority.pm +++ b/rt/lib/RT/Action/EscalatePriority.pm @@ -1,8 +1,8 @@ # BEGIN BPS TAGGED BLOCK {{{ # # COPYRIGHT: -# -# This software is Copyright (c) 1996-2009 Best Practical Solutions, LLC +# +# This software is Copyright (c) 1996-2009 Best Practical Solutions, LLC # <jesse@bestpractical.com> # # (Except where explicitly superseded by other copyright notices) @@ -45,6 +45,7 @@ # those contributions and any derivatives thereof. # # END BPS TAGGED BLOCK }}} + =head1 NAME RT::Action::EscalatePriority @@ -71,11 +72,9 @@ as the ticket heads toward its due date. package RT::Action::EscalatePriority; -require RT::Action::Generic; +use base 'RT::Action'; use strict; -use vars qw/@ISA/; -@ISA=qw(RT::Action::Generic); #Do what we need to do and send it out. @@ -155,7 +154,7 @@ sub Commit { my ($val, $msg) = $self->TicketObj->SetPriority($self->{'prio'}); unless ($val) { - $RT::Logger->debug($self . " $msg\n"); + $RT::Logger->debug($self . " $msg"); } } diff --git a/rt/lib/RT/Action/ExtractSubjectTag.pm b/rt/lib/RT/Action/ExtractSubjectTag.pm new file mode 100644 index 000000000..4a173ce76 --- /dev/null +++ b/rt/lib/RT/Action/ExtractSubjectTag.pm @@ -0,0 +1,103 @@ +# BEGIN BPS TAGGED BLOCK {{{ +# +# COPYRIGHT: +# +# This software is Copyright (c) 1996-2009 Best Practical Solutions, LLC +# <jesse@bestpractical.com> +# +# (Except where explicitly superseded by other copyright notices) +# +# +# LICENSE: +# +# 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. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301 or visit their web page on the internet at +# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html. +# +# +# CONTRIBUTION SUBMISSION POLICY: +# +# (The following paragraph is not intended to limit the rights granted +# to you to modify and distribute this software under the terms of +# the GNU General Public License and is only of importance to you if +# you choose to contribute your changes and enhancements to the +# community by submitting them to Best Practical Solutions, LLC.) +# +# By intentionally submitting any modifications, corrections or +# derivatives to this work, or any other work intended for use with +# Request Tracker, to Best Practical Solutions, LLC, you confirm that +# you are the copyright holder for those contributions and you grant +# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable, +# royalty-free, perpetual, license to use, copy, create derivative +# works based on those contributions, and sublicense and distribute +# those contributions and any derivatives thereof. +# +# END BPS TAGGED BLOCK }}} + +package RT::Action::ExtractSubjectTag; +use base 'RT::Action'; +use strict; + +sub Describe { + my $self = shift; + return ( ref $self ); +} + +sub Prepare { + return (1); +} + +sub Commit { + my $self = shift; + my $Transaction = $self->TransactionObj; + my $FirstAttachment = $Transaction->Attachments->First; + return 1 unless ($FirstAttachment); + + my $Ticket = $self->TicketObj; + + my $TicketSubject = $self->TicketObj->Subject; + my $origTicketSubject = $TicketSubject; + my $TransactionSubject = $FirstAttachment->Subject; + + my $match = RT->Config->Get('ExtractSubjectTagMatch'); + my $nomatch = RT->Config->Get('ExtractSubjectTagNoMatch'); + TAGLIST: while ( $TransactionSubject =~ /($match)/g ) { + my $tag = $1; + next if $tag =~ /$nomatch/; + foreach my $subject_tag ( RT->System->SubjectTag ) { + if ($tag =~ /\[\Q$subject_tag\E\s+\#(\d+)\s*\]/) { + next TAGLIST; + } + } + $TicketSubject .= " $tag" unless ( $TicketSubject =~ /\Q$tag\E/ ); + } + + $self->TicketObj->SetSubject($TicketSubject) + if ( $TicketSubject ne $origTicketSubject ); + + return (1); +} + +eval "require RT::Action::ExtractSubjectTag_Vendor"; +if ($@ && $@ !~ qr{^Can't locate RT/Action/ExtractSubjectTag_Vendor.pm}) { + die $@; +}; + +eval "require RT::Action::ExtractSubjectTag_Local"; +if ($@ && $@ !~ qr{^Can't locate RT/Action/ExtractSubjectTag_Local.pm}) { + die $@; +}; + +1; diff --git a/rt/lib/RT/Action/Generic.pm b/rt/lib/RT/Action/Generic.pm index 3232d4898..5e8ef32ce 100755 --- a/rt/lib/RT/Action/Generic.pm +++ b/rt/lib/RT/Action/Generic.pm @@ -1,8 +1,8 @@ # BEGIN BPS TAGGED BLOCK {{{ # # COPYRIGHT: -# -# This software is Copyright (c) 1996-2009 Best Practical Solutions, LLC +# +# This software is Copyright (c) 1996-2009 Best Practical Solutions, LLC # <jesse@bestpractical.com> # # (Except where explicitly superseded by other copyright notices) @@ -45,9 +45,10 @@ # those contributions and any derivatives thereof. # # END BPS TAGGED BLOCK }}} + =head1 NAME - RT::Action::Generic - a generic baseclass for RT Actions + RT::Action::Generic - deprecated, see RT::Action =head1 SYNOPSIS @@ -55,177 +56,25 @@ =head1 DESCRIPTION -=head1 METHODS - -=begin testing +This module is provided only for backwards compatibility. -ok (require RT::Action::Generic); +=head1 METHODS -=end testing =cut -package RT::Action::Generic; - use strict; -use Scalar::Util; - -use base qw/RT::Base/; - -# {{{ sub new -sub new { - my $proto = shift; - my $class = ref($proto) || $proto; - my $self = {}; - bless ($self, $class); - $self->_Init(@_); - return $self; -} -# }}} - -# {{{ sub _Init -sub _Init { - my $self = shift; - my %args = ( Argument => undef, - CurrentUser => undef, - ScripActionObj => undef, - ScripObj => undef, - TemplateObj => undef, - TicketObj => undef, - TransactionObj => undef, - Type => undef, - - @_ ); - - $self->{'Argument'} = $args{'Argument'}; - $self->CurrentUser( $args{'CurrentUser'}); - $self->{'ScripActionObj'} = $args{'ScripActionObj'}; - $self->{'ScripObj'} = $args{'ScripObj'}; - $self->{'TemplateObj'} = $args{'TemplateObj'}; - $self->{'TicketObj'} = $args{'TicketObj'}; - $self->{'TransactionObj'} = $args{'TransactionObj'}; - $self->{'Type'} = $args{'Type'}; - - Scalar::Util::weaken($self->{'ScripActionObj'}); - Scalar::Util::weaken($self->{'ScripObj'}); - Scalar::Util::weaken($self->{'TemplateObj'}); - Scalar::Util::weaken($self->{'TicketObj'}); - Scalar::Util::weaken($self->{'TransactionObj'}); - -} -# }}} - -# Access Scripwide data - -# {{{ sub Argument -sub Argument { - my $self = shift; - return($self->{'Argument'}); -} -# }}} - -# {{{ sub TicketObj -sub TicketObj { - my $self = shift; - return($self->{'TicketObj'}); -} -# }}} - -# {{{ sub TransactionObj -sub TransactionObj { - my $self = shift; - return($self->{'TransactionObj'}); -} -# }}} - -# {{{ sub TemplateObj -sub TemplateObj { - my $self = shift; - return($self->{'TemplateObj'}); -} -# }}} - -# {{{ sub ScripObj -sub ScripObj { - my $self = shift; - return($self->{'ScripObj'}); -} -# }}} - -# {{{ sub ScripActionObj -sub ScripActionObj { - my $self = shift; - return($self->{'ScripActionObj'}); -} -# }}} - -# {{{ sub Type -sub Type { - my $self = shift; - return($self->{'Type'}); -} -# }}} - - -# Scrip methods - -#Do what we need to do and send it out. - -# {{{ sub Commit -sub Commit { - my $self = shift; - return(0, $self->loc("Commit Stubbed")); -} -# }}} - - -#What does this type of Action does - -# {{{ sub Describe -sub Describe { - my $self = shift; - return $self->loc("No description for [_1]", ref $self); -} -# }}} - - -#Parse the templates, get things ready to go. - -# {{{ sub Prepare -sub Prepare { - my $self = shift; - return (0, $self->loc("Prepare Stubbed")); -} -# }}} - - -#If this rule applies to this transaction, return true. - -# {{{ sub IsApplicable -sub IsApplicable { - my $self = shift; - return(undef); -} -# }}} - -# {{{ sub DESTROY -sub DESTROY { - my $self = shift; - - # We need to clean up all the references that might maybe get - # oddly circular - $self->{'ScripActionObj'} = undef; - $self->{'ScripObj'} = undef; - $self->{'TemplateObj'} =undef - $self->{'TicketObj'} = undef; - $self->{'TransactionObj'} = undef; -} - -# }}} +use warnings; +package RT::Action::Generic; +use base 'RT::Action'; eval "require RT::Action::Generic_Vendor"; die $@ if ($@ && $@ !~ qr{^Can't locate RT/Action/Generic_Vendor.pm}); +warn "RT::Action::Generic has become RT::Action. Please adjust your deprecated RT::Action::Generic_Vendor file at " . $INC{"RT/Action/Generic_Vendor.pm"} if !$@; + eval "require RT::Action::Generic_Local"; die $@ if ($@ && $@ !~ qr{^Can't locate RT/Action/Generic_Local.pm}); +warn "RT::Action::Generic has become RT::Action. Please adjust your deprecated RT::Action::Generic_Local file at " . $INC{"RT/Action/Generic_Local.pm"} if !$@; 1; + diff --git a/rt/lib/RT/Action/LinearEscalate.pm b/rt/lib/RT/Action/LinearEscalate.pm new file mode 100755 index 000000000..9130f40ca --- /dev/null +++ b/rt/lib/RT/Action/LinearEscalate.pm @@ -0,0 +1,279 @@ +# BEGIN BPS TAGGED BLOCK {{{ +# +# COPYRIGHT: +# +# This software is Copyright (c) 1996-2009 Best Practical Solutions, LLC +# <jesse@bestpractical.com> +# +# (Except where explicitly superseded by other copyright notices) +# +# +# LICENSE: +# +# 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. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301 or visit their web page on the internet at +# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html. +# +# +# CONTRIBUTION SUBMISSION POLICY: +# +# (The following paragraph is not intended to limit the rights granted +# to you to modify and distribute this software under the terms of +# the GNU General Public License and is only of importance to you if +# you choose to contribute your changes and enhancements to the +# community by submitting them to Best Practical Solutions, LLC.) +# +# By intentionally submitting any modifications, corrections or +# derivatives to this work, or any other work intended for use with +# Request Tracker, to Best Practical Solutions, LLC, you confirm that +# you are the copyright holder for those contributions and you grant +# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable, +# royalty-free, perpetual, license to use, copy, create derivative +# works based on those contributions, and sublicense and distribute +# those contributions and any derivatives thereof. +# +# END BPS TAGGED BLOCK }}} + +=head1 NAME + +RT::Action::LinearEscalate - will move a ticket's priority toward its final priority. + +=head1 This vs. RT::Action::EscalatePriority + +This action doesn't change priority if due date is not set. + +This action honor the Starts date. + +This action can apply changes silently. + +This action can replace EscalatePriority completly. If you want to tickets +that have been created without Due date then you can add scrip that sets +default due date. For example a week then priorities of your tickets will +escalate linearly during the week from intial value towards final. + +=head1 This vs. LinearEscalate from the CPAN + +This action is an integration of the module from the CPAN into RT's core +that's happened in RT 3.8. If you're upgrading from 3.6 and have been using +module from the CPAN with old version of RT then you should uninstall it +and use this one. + +However, this action doesn't support control over config. Read </CONFIGURATION> +to find out ways to deal with it. + +=head1 DESCRIPTION + +LinearEscalate is a ScripAction that will move a ticket's priority +from its initial priority to its final priority linearly as +the ticket approaches its due date. + +It's intended to be called by an RT escalation tool. One such tool is called +rt-crontool and is located in $RTHOME/bin (see C<rt-crontool -h> for more details). + +=head1 USAGE + +Once the ScripAction is installed, the following script in "cron" +will get tickets to where they need to be: + + rt-crontool --search RT::Search::FromSQL --search-arg \ + "(Status='new' OR Status='open' OR Status = 'stalled')" \ + --action RT::Action::LinearEscalate + +The Starts date is associated with intial ticket's priority or +the Created field if the former is not set. End of interval is +the Due date. Tickets without due date B<are not updated>. + +=head1 CONFIGURATION + +Initial and Final priorities are controlled by queue's options +and can be defined using the web UI via Configuration tab. This +action should handle correctly situations when initial priority +is greater than final. + +LinearEscalate's behavior can be controlled by two options: + +=over 4 + +=item RecordTransaction - defaults to false and if option is true then +causes the tool to create a transaction on the ticket when it is escalated. + +=item UpdateLastUpdated - which defaults to true and updates the LastUpdated +field when the ticket is escalated, otherwise don't touch anything. + +=back + +You cannot set "UpdateLastUpdated" to false unless "RecordTransaction" +is also false. Well, you can, but we'll just ignore you. + +You can set this options using either in F<RT_SiteConfig.pm>, as action +argument in call to the rt-crontool or in DB if you want to use the action +in scrips. + +From a shell you can use the following command: + + rt-crontool --search RT::Search::FromSQL --search-arg \ + "(Status='new' OR Status='open' OR Status = 'stalled')" \ + --action RT::Action::LinearEscalate \ + --action-arg "RecordTransaction: 1" + +This ScripAction uses RT's internal _Set or __Set calls to set ticket +priority without running scrips or recording a transaction on each +update, if it's been said to. + +=cut + +package RT::Action::LinearEscalate; + +use strict; +use warnings; +use base qw(RT::Action); + +our $VERSION = '0.06'; + +#Do what we need to do and send it out. + +#What does this type of Action does + +sub Describe { + my $self = shift; + my $class = ref($self) || $self; + return "$class will move a ticket's priority toward its final priority."; +} + +sub Prepare { + my $self = shift; + + my $ticket = $self->TicketObj; + + my $due = $ticket->DueObj->Unix; + unless ( $due > 0 ) { + $RT::Logger->debug('Due is not set. Not escalating.'); + return 1; + } + + my $priority_range = ($ticket->FinalPriority ||0) - ($ticket->InitialPriority ||0); + unless ( $priority_range ) { + $RT::Logger->debug('Final and Initial priorities are equal. Not escalating.'); + return 1; + } + + if ( $ticket->Priority >= $ticket->FinalPriority && $priority_range > 0 ) { + $RT::Logger->debug('Current priority is greater than final. Not escalating.'); + return 1; + } + elsif ( $ticket->Priority <= $ticket->FinalPriority && $priority_range < 0 ) { + $RT::Logger->debug('Current priority is lower than final. Not escalating.'); + return 1; + } + + # TODO: compute the number of business days until the ticket is due + + # now we know we have a due date. for every day that passes, + # increment priority according to the formula + + my $starts = $ticket->StartsObj->Unix; + $starts = $ticket->CreatedObj->Unix unless $starts > 0; + my $now = time; + + # do nothing if we didn't reach starts or created date + if ( $starts > $now ) { + $RT::Logger->debug('Starts(Created) is in future. Not escalating.'); + return 1; + } + + $due = $starts + 1 if $due <= $starts; # +1 to avoid div by zero + + my $percent_complete = ($now-$starts)/($due - $starts); + + my $new_priority = int($percent_complete * $priority_range) + ($ticket->InitialPriority || 0); + $new_priority = $ticket->FinalPriority if $new_priority > $ticket->FinalPriority; + $self->{'new_priority'} = $new_priority; + + return 1; +} + +sub Commit { + my $self = shift; + + my $new_value = $self->{'new_priority'}; + return 1 unless defined $new_value; + + my $ticket = $self->TicketObj; + # if the priority hasn't changed do nothing + return 1 if $ticket->Priority == $new_value; + + # override defaults from argument + my ($record, $update) = (0, 1); + { + my $arg = $self->Argument || ''; + if ( $arg =~ /RecordTransaction:\s*(\d+)/i ) { + $record = $1; + $RT::Logger->debug("Overrode RecordTransaction: $record"); + } + if ( $arg =~ /UpdateLastUpdated:\s*(\d+)/i ) { + $update = $1; + $RT::Logger->debug("Overrode UpdateLastUpdated: $update"); + } + $update = 1 if $record; + } + + $RT::Logger->debug( + 'Linearly escalating priority of ticket #'. $ticket->Id + .' from '. $ticket->Priority .' to '. $new_value + .' and'. ($record? '': ' do not') .' record a transaction' + .' and'. ($update? '': ' do not') .' touch last updated field' + ); + + my ( $val, $msg ); + unless ( $record ) { + unless ( $update ) { + ( $val, $msg ) = $ticket->__Set( + Field => 'Priority', + Value => $new_value, + ); + } + else { + ( $val, $msg ) = $ticket->_Set( + Field => 'Priority', + Value => $new_value, + RecordTransaction => 0, + ); + } + } + else { + ( $val, $msg ) = $ticket->SetPriority( $new_value ); + } + + unless ($val) { + $RT::Logger->error( "Couldn't set new priority value: $msg" ); + return (0, $msg); + } + return 1; +} + +eval "require RT::Action::LinearEscalate_Vendor"; +die $@ if ( $@ && $@ !~ qr{^Can't locate RT/Action/LinearEscalate_Vendor.pm} ); +eval "require RT::Action::LinearEscalate_Local"; +die $@ if ( $@ && $@ !~ qr{^Can't locate RT/Action/LinearEscalate_Local.pm} ); + +1; + +=head1 AUTHORS + +Kevin Riggle E<lt>kevinr@bestpractical.comE<gt> + +Ruslan Zakirov E<lt>ruz@bestpractical.comE<gt> + +=cut diff --git a/rt/lib/RT/Action/Notify.pm b/rt/lib/RT/Action/Notify.pm index 82cad1e58..30238fd61 100755 --- a/rt/lib/RT/Action/Notify.pm +++ b/rt/lib/RT/Action/Notify.pm @@ -1,8 +1,8 @@ # BEGIN BPS TAGGED BLOCK {{{ # # COPYRIGHT: -# -# This software is Copyright (c) 1996-2009 Best Practical Solutions, LLC +# +# This software is Copyright (c) 1996-2009 Best Practical Solutions, LLC # <jesse@bestpractical.com> # # (Except where explicitly superseded by other copyright notices) @@ -45,14 +45,16 @@ # those contributions and any derivatives thereof. # # END BPS TAGGED BLOCK }}} + # package RT::Action::Notify; -require RT::Action::SendEmail; -use Mail::Address; + use strict; -use vars qw/@ISA/; -@ISA = qw(RT::Action::SendEmail); +use warnings; +use base qw(RT::Action::SendEmail); + +use Email::Address; =head2 Prepare @@ -67,8 +69,6 @@ sub Prepare { $self->SUPER::Prepare(); } -# {{{ sub SetRecipients - =head2 SetRecipients Sets the recipients of this meesage to Owner, Requestor, AdminCc, Cc or All. @@ -79,73 +79,63 @@ Explicitly B<does not> notify the creator of the transaction by default sub SetRecipients { my $self = shift; - my $arg = $self->Argument; + my $ticket = $self->TicketObj; + my $arg = $self->Argument; $arg =~ s/\bAll\b/Owner,Requestor,AdminCc,Cc/; my ( @To, @PseudoTo, @Cc, @Bcc ); if ( $arg =~ /\bOtherRecipients\b/ ) { - if ( $self->TransactionObj->Attachments->First ) { - my @cc_addresses = Mail::Address->parse($self->TransactionObj->Attachments->First->GetHeader('RT-Send-Cc')); - foreach my $addr (@cc_addresses) { - push @Cc, $addr->address; - } - my @bcc_addresses = Mail::Address->parse($self->TransactionObj->Attachments->First->GetHeader('RT-Send-Bcc')); - - foreach my $addr (@bcc_addresses) { - push @Bcc, $addr->address; - } - + if ( my $attachment = $self->TransactionObj->Attachments->First ) { + push @Cc, map { $_->address } Email::Address->parse( + $attachment->GetHeader('RT-Send-Cc') + ); + push @Bcc, map { $_->address } Email::Address->parse( + $attachment->GetHeader('RT-Send-Bcc') + ); } } if ( $arg =~ /\bRequestor\b/ ) { - push ( @To, $self->TicketObj->Requestors->MemberEmailAddresses ); + push @To, $ticket->Requestors->MemberEmailAddresses; } - - if ( $arg =~ /\bCc\b/ ) { #If we have a To, make the Ccs, Ccs, otherwise, promote them to To if (@To) { - push ( @Cc, $self->TicketObj->Cc->MemberEmailAddresses ); - push ( @Cc, $self->TicketObj->QueueObj->Cc->MemberEmailAddresses ); + push ( @Cc, $ticket->Cc->MemberEmailAddresses ); + push ( @Cc, $ticket->QueueObj->Cc->MemberEmailAddresses ); } else { - push ( @Cc, $self->TicketObj->Cc->MemberEmailAddresses ); - push ( @To, $self->TicketObj->QueueObj->Cc->MemberEmailAddresses ); + push ( @Cc, $ticket->Cc->MemberEmailAddresses ); + push ( @To, $ticket->QueueObj->Cc->MemberEmailAddresses ); } } - if ( ( $arg =~ /\bOwner\b/ ) - && ( $self->TicketObj->OwnerObj->id != $RT::Nobody->id ) ) - { - - # If we're not sending to Ccs or requestors, + if ( $arg =~ /\bOwner\b/ && $ticket->OwnerObj->id != $RT::Nobody->id ) { + # If we're not sending to Ccs or requestors, # then the Owner can be the To. if (@To) { - push ( @Bcc, $self->TicketObj->OwnerObj->EmailAddress ); + push ( @Bcc, $ticket->OwnerObj->EmailAddress ); } else { - push ( @To, $self->TicketObj->OwnerObj->EmailAddress ); + push ( @To, $ticket->OwnerObj->EmailAddress ); } } if ( $arg =~ /\bAdminCc\b/ ) { - push ( @Bcc, $self->TicketObj->AdminCc->MemberEmailAddresses ); - push ( @Bcc, $self->TicketObj->QueueObj->AdminCc->MemberEmailAddresses ); + push ( @Bcc, $ticket->AdminCc->MemberEmailAddresses ); + push ( @Bcc, $ticket->QueueObj->AdminCc->MemberEmailAddresses ); } - if ($RT::UseFriendlyToLine) { + if ( RT->Config->Get('UseFriendlyToLine') ) { unless (@To) { - push ( - @PseudoTo, - sprintf($RT::FriendlyToLineFormat, $arg, $self->TicketObj->id), - ); + push @PseudoTo, + sprintf RT->Config->Get('FriendlyToLineFormat'), $arg, $ticket->id; } } @@ -154,7 +144,7 @@ sub SetRecipients { #Strip the sender out of the To, Cc and AdminCc and set the # recipients fields used to build the message by the superclass. # unless a flag is set - if ($RT::NotifyActor) { + if (RT->Config->Get('NotifyActor')) { @{ $self->{'To'} } = @To; @{ $self->{'Cc'} } = @Cc; @{ $self->{'Bcc'} } = @Bcc; @@ -169,8 +159,6 @@ sub SetRecipients { } -# }}} - eval "require RT::Action::Notify_Vendor"; die $@ if ($@ && $@ !~ qr{^Can't locate RT/Action/Notify_Vendor.pm}); eval "require RT::Action::Notify_Local"; diff --git a/rt/lib/RT/Action/NotifyAsComment.pm b/rt/lib/RT/Action/NotifyAsComment.pm index 215f453d3..b2eb5acd8 100755 --- a/rt/lib/RT/Action/NotifyAsComment.pm +++ b/rt/lib/RT/Action/NotifyAsComment.pm @@ -1,8 +1,8 @@ # BEGIN BPS TAGGED BLOCK {{{ # # COPYRIGHT: -# -# This software is Copyright (c) 1996-2009 Best Practical Solutions, LLC +# +# This software is Copyright (c) 1996-2009 Best Practical Solutions, LLC # <jesse@bestpractical.com> # # (Except where explicitly superseded by other copyright notices) @@ -45,13 +45,13 @@ # those contributions and any derivatives thereof. # # END BPS TAGGED BLOCK }}} + package RT::Action::NotifyAsComment; require RT::Action::Notify; use strict; -use vars qw/@ISA/; -@ISA = qw(RT::Action::Notify); - +use warnings; +use base qw(RT::Action::Notify); =head2 SetReturnAddress diff --git a/rt/lib/RT/Action/NotifyGroup.pm b/rt/lib/RT/Action/NotifyGroup.pm new file mode 100644 index 000000000..6b830cb86 --- /dev/null +++ b/rt/lib/RT/Action/NotifyGroup.pm @@ -0,0 +1,209 @@ +# BEGIN BPS TAGGED BLOCK {{{ +# +# COPYRIGHT: +# +# This software is Copyright (c) 1996-2009 Best Practical Solutions, LLC +# <jesse@bestpractical.com> +# +# (Except where explicitly superseded by other copyright notices) +# +# +# LICENSE: +# +# 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. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301 or visit their web page on the internet at +# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html. +# +# +# CONTRIBUTION SUBMISSION POLICY: +# +# (The following paragraph is not intended to limit the rights granted +# to you to modify and distribute this software under the terms of +# the GNU General Public License and is only of importance to you if +# you choose to contribute your changes and enhancements to the +# community by submitting them to Best Practical Solutions, LLC.) +# +# By intentionally submitting any modifications, corrections or +# derivatives to this work, or any other work intended for use with +# Request Tracker, to Best Practical Solutions, LLC, you confirm that +# you are the copyright holder for those contributions and you grant +# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable, +# royalty-free, perpetual, license to use, copy, create derivative +# works based on those contributions, and sublicense and distribute +# those contributions and any derivatives thereof. +# +# END BPS TAGGED BLOCK }}} + +=head1 NAME + +RT::Action::NotifyGroup - RT Action that sends notifications to groups and/or users + +=head1 DESCRIPTION + +RT action module that allow you to notify particular groups and/or users. +Distribution is shipped with C<rt-email-group-admin> script that +is command line tool for managing NotifyGroup scrip actions. For more +more info see its documentation. + +=cut + +package RT::Action::NotifyGroup; + +use strict; +use warnings; +use base qw(RT::Action::Notify); + +require RT::User; +require RT::Group; + +=head1 METHODS + +=head2 SetRecipients + +Sets the recipients of this message to Groups and/or Users. + +=cut + +sub SetRecipients { + my $self = shift; + + my $arg = $self->Argument; + foreach( $self->__SplitArg( $arg ) ) { + $self->_HandleArgument( $_ ); + } + + my $creator = $self->TransactionObj->CreatorObj->EmailAddress(); + unless( $RT::NotifyActor ) { + @{ $self->{'To'} } = grep ( !/^\Q$creator\E$/, @{ $self->{'To'} } ); + } + + $self->{'seen_ueas'} = {}; + + return 1; +} + +sub _HandleArgument { + my $self = shift; + my $instance = shift; + + if ( $instance !~ /\D/ ) { + my $obj = RT::Principal->new( $self->CurrentUser ); + $obj->Load( $instance ); + return $self->_HandlePrincipal( $obj ); + } + + my $group = RT::Group->new( $self->CurrentUser ); + $group->LoadUserDefinedGroup( $instance ); + # to check disabled and so on + return $self->_HandlePrincipal( $group->PrincipalObj ) + if $group->id; + + require Email::Address; + + my $user = RT::User->new( $self->CurrentUser ); + if ( $instance =~ /^$Email::Address::addr_spec$/ ) { + $user->LoadByEmail( $instance ); + return $self->__PushUserAddress( $instance ) + unless $user->id; + } else { + $user->Load( $instance ); + } + return $self->_HandlePrincipal( $user->PrincipalObj ) + if $user->id; + + $RT::Logger->error( + "'$instance' is not principal id, group name, user name," + ." user email address or any email address" + ); + + return; +} + +sub _HandlePrincipal { + my $self = shift; + my $obj = shift; + unless( $obj->id ) { + $RT::Logger->error( "Couldn't load principal #$obj" ); + return; + } + if( $obj->Disabled ) { + $RT::Logger->info( "Principal #$obj is disabled => skip" ); + return; + } + if( !$obj->PrincipalType ) { + $RT::Logger->crit( "Principal #$obj has empty type" ); + } elsif( lc $obj->PrincipalType eq 'user' ) { + $self->__HandleUserArgument( $obj->Object ); + } elsif( lc $obj->PrincipalType eq 'group' ) { + $self->__HandleGroupArgument( $obj->Object ); + } else { + $RT::Logger->info( "Principal #$obj has unsupported type" ); + } + return; +} + +sub __HandleUserArgument { + my $self = shift; + my $obj = shift; + + my $uea = $obj->EmailAddress; + unless( $uea ) { + $RT::Logger->warning( "User #". $obj->id ." has no email address" ); + return; + } + $self->__PushUserAddress( $uea ); +} + +sub __HandleGroupArgument { + my $self = shift; + my $obj = shift; + + my $members = $obj->UserMembersObj; + while( my $m = $members->Next ) { + $self->__HandleUserArgument( $m ); + } +} + +sub __SplitArg { + return grep length, map {s/^\s+//; s/\s+$//; $_} split /,/, $_[1]; +} + +sub __PushUserAddress { + my $self = shift; + my $uea = shift; + push @{ $self->{'To'} }, $uea unless $self->{'seen_ueas'}{ $uea }++; + return; +} + + +=head1 AUTHOR + +Ruslan U. Zakirov E<lt>ruz@bestpractical.comE<gt> + +L<RT::Action::NotifyGroupAsComment>, F<rt-email-group-admin> + +=cut + +eval "require RT::Action::NotifyGroup_Vendor"; +if ($@ && $@ !~ qr{^Can't locate RT/Action/NotifyGroup_Vendor.pm}) { + die $@; +}; + +eval "require RT::Action::NotifyGroup_Local"; +if ($@ && $@ !~ qr{^Can't locate RT/Action/NotifyGroup_Local.pm}) { + die $@; +}; + +1; diff --git a/rt/lib/RT/Action/NotifyGroupAsComment.pm b/rt/lib/RT/Action/NotifyGroupAsComment.pm new file mode 100644 index 000000000..bee0a01a1 --- /dev/null +++ b/rt/lib/RT/Action/NotifyGroupAsComment.pm @@ -0,0 +1,91 @@ +# BEGIN BPS TAGGED BLOCK {{{ +# +# COPYRIGHT: +# +# This software is Copyright (c) 1996-2009 Best Practical Solutions, LLC +# <jesse@bestpractical.com> +# +# (Except where explicitly superseded by other copyright notices) +# +# +# LICENSE: +# +# 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. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301 or visit their web page on the internet at +# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html. +# +# +# CONTRIBUTION SUBMISSION POLICY: +# +# (The following paragraph is not intended to limit the rights granted +# to you to modify and distribute this software under the terms of +# the GNU General Public License and is only of importance to you if +# you choose to contribute your changes and enhancements to the +# community by submitting them to Best Practical Solutions, LLC.) +# +# By intentionally submitting any modifications, corrections or +# derivatives to this work, or any other work intended for use with +# Request Tracker, to Best Practical Solutions, LLC, you confirm that +# you are the copyright holder for those contributions and you grant +# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable, +# royalty-free, perpetual, license to use, copy, create derivative +# works based on those contributions, and sublicense and distribute +# those contributions and any derivatives thereof. +# +# END BPS TAGGED BLOCK }}} + +=head1 NAME + +RT::Action::NotifyGroupAsComment - RT Action that sends notifications to groups and/or users as comment + +=head1 DESCRIPTION + +This is subclass of L<RT::Action::NotifyGroup> that send comments instead of replies. +See C<rt-email-group-admin> and L<RT::Action::NotifyGroup> docs for more info. + +=cut + +package RT::Action::NotifyGroupAsComment; + +use strict; +use warnings; + +use RT::Action::NotifyGroup; + +use base qw(RT::Action::NotifyGroup); + +sub SetReturnAddress { + my $self = shift; + $self->{'comment'} = 1; + return $self->SUPER::SetReturnAddress( @_, is_comment => 1 ); +} + +=head1 AUTHOR + +Ruslan U. Zakirov E<lt>ruz@bestpractical.comE<gt> + +=cut + +eval "require RT::Action::NotifyGroupAsComment_Vendor"; +if ($@ && $@ !~ qr{^Can't locate RT/Action/NotifyGroupAsComment_Vendor.pm}) { + die $@; +}; + +eval "require RT::Action::NotifyGroupAsComment_Local"; +if ($@ && $@ !~ qr{^Can't locate RT/Action/NotifyGroupAsComment_Local.pm}) { + die $@; +}; + +1; diff --git a/rt/lib/RT/Action/RecordComment.pm b/rt/lib/RT/Action/RecordComment.pm index c0256d6d7..bac17e96e 100644 --- a/rt/lib/RT/Action/RecordComment.pm +++ b/rt/lib/RT/Action/RecordComment.pm @@ -1,8 +1,8 @@ # BEGIN BPS TAGGED BLOCK {{{ # # COPYRIGHT: -# -# This software is Copyright (c) 1996-2009 Best Practical Solutions, LLC +# +# This software is Copyright (c) 1996-2009 Best Practical Solutions, LLC # <jesse@bestpractical.com> # # (Except where explicitly superseded by other copyright notices) @@ -45,11 +45,10 @@ # those contributions and any derivatives thereof. # # END BPS TAGGED BLOCK }}} + package RT::Action::RecordComment; -require RT::Action::Generic; +use base 'RT::Action'; use strict; -use vars qw/@ISA/; -@ISA = qw(RT::Action::Generic); =head1 NAME diff --git a/rt/lib/RT/Action/RecordCorrespondence.pm b/rt/lib/RT/Action/RecordCorrespondence.pm index 10a890e4e..044893b97 100644 --- a/rt/lib/RT/Action/RecordCorrespondence.pm +++ b/rt/lib/RT/Action/RecordCorrespondence.pm @@ -1,8 +1,8 @@ # BEGIN BPS TAGGED BLOCK {{{ # # COPYRIGHT: -# -# This software is Copyright (c) 1996-2009 Best Practical Solutions, LLC +# +# This software is Copyright (c) 1996-2009 Best Practical Solutions, LLC # <jesse@bestpractical.com> # # (Except where explicitly superseded by other copyright notices) @@ -45,11 +45,10 @@ # those contributions and any derivatives thereof. # # END BPS TAGGED BLOCK }}} + package RT::Action::RecordCorrespondence; -require RT::Action::Generic; +use base 'RT::Action'; use strict; -use vars qw/@ISA/; -@ISA = qw(RT::Action::Generic); =head1 NAME diff --git a/rt/lib/RT/Action/ResolveMembers.pm b/rt/lib/RT/Action/ResolveMembers.pm index fab049b0a..ff826ccc1 100644 --- a/rt/lib/RT/Action/ResolveMembers.pm +++ b/rt/lib/RT/Action/ResolveMembers.pm @@ -1,8 +1,8 @@ # BEGIN BPS TAGGED BLOCK {{{ # # COPYRIGHT: -# -# This software is Copyright (c) 1996-2009 Best Practical Solutions, LLC +# +# This software is Copyright (c) 1996-2009 Best Practical Solutions, LLC # <jesse@bestpractical.com> # # (Except where explicitly superseded by other copyright notices) @@ -45,15 +45,14 @@ # those contributions and any derivatives thereof. # # END BPS TAGGED BLOCK }}} + # This Action will resolve all members of a resolved group ticket package RT::Action::ResolveMembers; -require RT::Action::Generic; +use base 'RT::Action'; require RT::Links; use strict; -use vars qw/@ISA/; -@ISA=qw(RT::Action::Generic); #Do what we need to do and send it out. diff --git a/rt/lib/RT/Action/SendEmail.pm b/rt/lib/RT/Action/SendEmail.pm index ed5ec4fd6..a09bd3e56 100755 --- a/rt/lib/RT/Action/SendEmail.pm +++ b/rt/lib/RT/Action/SendEmail.pm @@ -1,8 +1,8 @@ # BEGIN BPS TAGGED BLOCK {{{ # # COPYRIGHT: -# -# This software is Copyright (c) 1996-2009 Best Practical Solutions, LLC +# +# This software is Copyright (c) 1996-2009 Best Practical Solutions, LLC # <jesse@bestpractical.com> # # (Except where explicitly superseded by other copyright notices) @@ -45,20 +45,21 @@ # those contributions and any derivatives thereof. # # END BPS TAGGED BLOCK }}} + # Portions Copyright 2000 Tobias Brox <tobix@cpan.org> package RT::Action::SendEmail; -require RT::Action::Generic; use strict; -use vars qw/@ISA/; -@ISA = qw(RT::Action::Generic); +use warnings; -use MIME::Words qw(encode_mimeword); +use base qw(RT::Action); use RT::EmailParser; -use Mail::Address; -use Date::Format qw(strftime); +use RT::Interface::Email; +use Email::Address; +our @EMAIL_RECIPIENT_HEADERS = qw(To Cc Bcc); + =head1 NAME @@ -68,53 +69,85 @@ RT::Action::AutoReply is a good example subclass. =head1 SYNOPSIS - require RT::Action::SendEmail; - @ISA = qw(RT::Action::SendEmail); - + use base 'RT::Action::SendEmail'; =head1 DESCRIPTION Basically, you create another module RT::Action::YourAction which ISA RT::Action::SendEmail. -=begin testing +=head1 METHODS -ok (require RT::Action::SendEmail); +=head2 CleanSlate -=end testing +Cleans class-wide options, like L</SquelchMailTo> or L</AttachTickets>. +=cut -=head1 AUTHOR - -Jesse Vincent <jesse@bestpractical.com> and Tobias Brox <tobix@cpan.org> +sub CleanSlate { + my $self = shift; + $self->SquelchMailTo(undef); + $self->AttachTickets(undef); +} -=head1 SEE ALSO +=head2 Commit -perl(1). +Sends the prepared message and writes outgoing record into DB if the feature is +activated in the config. =cut -# {{{ Scrip methods (_Init, Commit, Prepare, IsApplicable) +sub Commit { + my $self = shift; + $self->DeferDigestRecipients() if RT->Config->Get('RecordOutgoingEmail'); + my $message = $self->TemplateObj->MIMEObj; -# {{{ sub Commit + my $orig_message; + if ( RT->Config->Get('RecordOutgoingEmail') + && RT->Config->Get('GnuPG')->{'Enable'} ) + { -sub Commit { - # DO NOT SHIFT @_ in this subroutine. It breaks Hook::LexWrap's - # ability to pass @_ to a 'post' routine. - my $self = $_[0]; + # it's hacky, but we should know if we're going to crypt things + my $attachment = $self->TransactionObj->Attachments->First; + + my %crypt; + foreach my $argument (qw(Sign Encrypt)) { + if ( $attachment + && defined $attachment->GetHeader("X-RT-$argument") ) + { + $crypt{$argument} = $attachment->GetHeader("X-RT-$argument"); + } else { + $crypt{$argument} = $self->TicketObj->QueueObj->$argument(); + } + } + if ( $crypt{'Sign'} || $crypt{'Encrypt'} ) { + $orig_message = $message->dup; + } + } - my ($ret) = $self->SendMessage( $self->TemplateObj->MIMEObj ); - if ( $ret > 0 ) { - $self->RecordOutgoingMailTransaction( $self->TemplateObj->MIMEObj ) - if ($RT::RecordOutgoingEmail); + my ($ret) = $self->SendMessage($message); + if ( $ret > 0 && RT->Config->Get('RecordOutgoingEmail') ) { + if ($orig_message) { + $message->attach( + Type => 'application/x-rt-original-message', + Disposition => 'inline', + Data => $orig_message->as_string, + ); + } + $self->RecordOutgoingMailTransaction($message); + $self->RecordDeferredRecipients(); } - return (abs $ret); + + + return ( abs $ret ); } -# }}} +=head2 Prepare + +Builds an outgoing email we're going to send using scrip's template. -# {{{ sub Prepare +=cut sub Prepare { my $self = shift; @@ -136,116 +169,116 @@ sub Prepare { $self->RemoveInappropriateRecipients(); my %seen; - foreach my $type qw(To Cc Bcc) { - @{ $self->{ $type } } = - grep defined && length && !$seen{ lc $_ }++, - @{ $self->{ $type } }; + foreach my $type (@EMAIL_RECIPIENT_HEADERS) { + @{ $self->{$type} } + = grep defined && length && !$seen{ lc $_ }++, + @{ $self->{$type} }; } # Go add all the Tos, Ccs and Bccs that we need to to the message to # make it happy, but only if we actually have values in those arrays. - # TODO: We should be pulling the recipients out of the template and shove them into To, Cc and Bcc +# TODO: We should be pulling the recipients out of the template and shove them into To, Cc and Bcc - $self->SetHeader( 'To', join ( ', ', @{ $self->{'To'} } ) ) - if ( ! $MIMEObj->head->get('To') && $self->{'To'} && @{ $self->{'To'} } ); - $self->SetHeader( 'Cc', join ( ', ', @{ $self->{'Cc'} } ) ) - if ( !$MIMEObj->head->get('Cc') && $self->{'Cc'} && @{ $self->{'Cc'} } ); - $self->SetHeader( 'Bcc', join ( ', ', @{ $self->{'Bcc'} } ) ) - if ( !$MIMEObj->head->get('Bcc') && $self->{'Bcc'} && @{ $self->{'Bcc'} } ); + for my $header (@EMAIL_RECIPIENT_HEADERS) { - # PseudoTo (fake to headers) shouldn't get matched for message recipients. + $self->SetHeader( $header, join( ', ', @{ $self->{$header} } ) ) + if ( !$MIMEObj->head->get($header) + && $self->{$header} + && @{ $self->{$header} } ); +} + # PseudoTo (fake to headers) shouldn't get matched for message recipients. # If we don't have any 'To' header (but do have other recipients), drop in # the pseudo-to header. - $self->SetHeader( 'To', join ( ', ', @{ $self->{'PseudoTo'} } ) ) - if ( $self->{'PseudoTo'} && ( @{ $self->{'PseudoTo'} } ) - and ( !$MIMEObj->head->get('To') ) ) and ( $MIMEObj->head->get('Cc') or $MIMEObj->head->get('Bcc')); + $self->SetHeader( 'To', join( ', ', @{ $self->{'PseudoTo'} } ) ) + if $self->{'PseudoTo'} + && @{ $self->{'PseudoTo'} } + && !$MIMEObj->head->get('To') + && ( $MIMEObj->head->get('Cc') or $MIMEObj->head->get('Bcc') ); # We should never have to set the MIME-Version header $self->SetHeader( 'MIME-Version', '1.0' ); # fsck.com #5959: Since RT sends 8bit mail, we should say so. - $self->SetHeader( 'Content-Transfer-Encoding','8bit'); + $self->SetHeader( 'Content-Transfer-Encoding', '8bit' ); # For security reasons, we only send out textual mails. - my @parts = $MIMEObj; - while (my $part = shift @parts) { - if ($part->is_multipart) { - push @parts, $part->parts; - } - else { - if ( RT::I18N::IsTextualContentType( $part->mime_type ) ) { - $part->head->mime_attr( "Content-Type" => $part->mime_type ) - } else { - $part->head->mime_attr( "Content-Type" => 'text/plain' ); - } - $part->head->mime_attr( "Content-Type.charset" => 'utf-8' ); - } + foreach my $part ( grep !$_->is_multipart, $MIMEObj->parts_DFS ) { + my $type = $part->mime_type || 'text/plain'; + $type = 'text/plain' unless RT::I18N::IsTextualContentType($type); + $part->head->mime_attr( "Content-Type" => $type ); + $part->head->mime_attr( "Content-Type.charset" => 'utf-8' ); } - - RT::I18N::SetMIMEEntityToEncoding( $MIMEObj, $RT::EmailOutputEncoding, 'mime_words_ok' ); + RT::I18N::SetMIMEEntityToEncoding( $MIMEObj, + RT->Config->Get('EmailOutputEncoding'), + 'mime_words_ok', ); # Build up a MIME::Entity that looks like the original message. - $self->AddAttachments() if ( $MIMEObj->head->get('RT-Attach-Message') ); + $self->AddAttachments if ( $MIMEObj->head->get('RT-Attach-Message') + && ( $MIMEObj->head->get('RT-Attach-Message') !~ /^(n|no|0|off|false)$/i ) ); + + $self->AddTickets; + + my $attachment = $self->TransactionObj->Attachments->First; + if ($attachment + && !( + $attachment->GetHeader('X-RT-Encrypt') + || $self->TicketObj->QueueObj->Encrypt + ) + ) + { + $attachment->SetHeader( 'X-RT-Encrypt' => 1 ) + if ( $attachment->GetHeader("X-RT-Incoming-Encryption") || '' ) eq + 'Success'; + } return $result; - } -# }}} - -# }}} - - - =head2 To -Returns an array of Mail::Address objects containing all the To: recipients for this notification +Returns an array of L<Email::Address> objects containing all the To: recipients for this notification =cut sub To { my $self = shift; - return ($self->_AddressesFromHeader('To')); + return ( $self->AddressesFromHeader('To') ); } =head2 Cc -Returns an array of Mail::Address objects containing all the Cc: recipients for this notification +Returns an array of L<Email::Address> objects containing all the Cc: recipients for this notification =cut -sub Cc { +sub Cc { my $self = shift; - return ($self->_AddressesFromHeader('Cc')); + return ( $self->AddressesFromHeader('Cc') ); } =head2 Bcc -Returns an array of Mail::Address objects containing all the Bcc: recipients for this notification +Returns an array of L<Email::Address> objects containing all the Bcc: recipients for this notification =cut - sub Bcc { my $self = shift; - return ($self->_AddressesFromHeader('Bcc')); + return ( $self->AddressesFromHeader('Bcc') ); } -sub _AddressesFromHeader { - my $self = shift; - my $field = shift; - my $header = $self->TemplateObj->MIMEObj->head->get($field); - my @addresses = Mail::Address->parse($header); +sub AddressesFromHeader { + my $self = shift; + my $field = shift; + my $header = $self->TemplateObj->MIMEObj->head->get($field); + my @addresses = Email::Address->parse($header); return (@addresses); } - -# {{{ SendMessage - =head2 SendMessage MIMEObj sends the message using RT's preferred API. @@ -254,6 +287,7 @@ TODO: Break this out to a separate module =cut sub SendMessage { + # DO NOT SHIFT @_ in this subroutine. It breaks Hook::LexWrap's # ability to pass @_ to a 'post' routine. my ( $self, $MIMEObj ) = @_; @@ -262,36 +296,36 @@ sub SendMessage { chomp $msgid; $self->ScripActionObj->{_Message_ID}++; - - $RT::Logger->info( $msgid . " #" - . $self->TicketObj->id . "/" - . $self->TransactionObj->id - . " - Scrip " - . $self->ScripObj->id . " " - . $self->ScripObj->Description ); - - #If we don't have any recipients to send to, don't send a message; - unless ( $MIMEObj->head->get('To') - || $MIMEObj->head->get('Cc') - || $MIMEObj->head->get('Bcc') ) - { - $RT::Logger->info( $msgid . " No recipients found. Not sending.\n" ); - return (-1); - } - unless ($MIMEObj->head->get('Date')) { - # We coerce localtime into an array since strftime has a flawed prototype that only accepts - # a list - $MIMEObj->head->replace(Date => strftime('%a, %d %b %Y %H:%M:%S %z', @{[localtime()]})); - } + $RT::Logger->info( $msgid . " #" + . $self->TicketObj->id . "/" + . $self->TransactionObj->id + . " - Scrip " + . ($self->ScripObj->id || '#rule'). " " + . ( $self->ScripObj->Description || '' ) ); + + my $status = RT::Interface::Email::SendEmail( + Entity => $MIMEObj, + Ticket => $self->TicketObj, + Transaction => $self->TransactionObj, + ); - return (0) unless ($self->OutputMIMEObject($MIMEObj)); + + return $status unless ($status > 0 || exists $self->{'Deferred'}); my $success = $msgid . " sent "; - foreach( qw(To Cc Bcc) ) { + foreach (@EMAIL_RECIPIENT_HEADERS) { my $recipients = $MIMEObj->head->get($_); - $success .= " $_: ". $recipients if $recipients; + $success .= " $_: " . $recipients if $recipients; } + + if( exists $self->{'Deferred'} ) { + for (qw(daily weekly susp)) { + $success .= "\nBatched email $_ for: ". join(", ", keys %{ $self->{'Deferred'}{ $_ } } ) + if exists $self->{'Deferred'}{ $_ }; + } + } + $success =~ s/\n//g; $RT::Logger->info($success); @@ -299,140 +333,166 @@ sub SendMessage { return (1); } +=head2 AddAttachments -=head2 OutputMIMEObject MIME::Entity - -Sends C<MIME::Entity> as an email message according to RT's mailer configuration. - -=cut - +Takes any attachments to this transaction and attaches them to the message +we're building. +=cut -sub OutputMIMEObject { +sub AddAttachments { my $self = shift; - my $MIMEObj = shift; - - my $msgid = $MIMEObj->head->get('Message-ID'); - chomp $msgid; - - my $SendmailArguments = $RT::SendmailArguments; - if (defined $RT::VERPPrefix && defined $RT::VERPDomain) { - my $EnvelopeFrom = $self->TransactionObj->CreatorObj->EmailAddress; - $EnvelopeFrom =~ s/@/=/g; - $EnvelopeFrom =~ s/\s//g; - $SendmailArguments .= " -f ${RT::VERPPrefix}${EnvelopeFrom}\@${RT::VERPDomain}"; - } - - if ( $RT::MailCommand eq 'sendmailpipe' ) { - eval { - # don't ignore CHLD signal to get proper exit code - local $SIG{'CHLD'} = 'DEFAULT'; + my $MIMEObj = $self->TemplateObj->MIMEObj; - my $mail; - unless( open $mail, "|$RT::SendmailPath $SendmailArguments" ) { - die "Couldn't run $RT::SendmailPath: $!"; - } + $MIMEObj->head->delete('RT-Attach-Message'); - # if something wrong with $mail->print we will get PIPE signal, handle it - local $SIG{'PIPE'} = sub { die "$RT::SendmailPath closed pipe" }; - $MIMEObj->print($mail); + my $attachments = RT::Attachments->new($RT::SystemUser); + $attachments->Limit( + FIELD => 'TransactionId', + VALUE => $self->TransactionObj->Id + ); - unless ( close $mail ) { - die "Close failed: $!" if $!; # system error - # sendmail exit statuses mostly errors with data not software - # TODO: status parsing: core dump, exit on signal or EX_* - $RT::Logger->warning( "$RT::SendmailPath exitted with status $?" ); - } - }; - if ($@) { - $RT::Logger->crit( $msgid . "Could not send mail: " . $@ ); - return 0; - } - } - else { - my @mailer_args = ($RT::MailCommand); - my $method = 'send'; + # Don't attach anything blank + $attachments->LimitNotEmpty; + $attachments->OrderBy( FIELD => 'id' ); - local $ENV{MAILADDRESS}; + # We want to make sure that we don't include the attachment that's + # being used as the "Content" of this message" unless that attachment's + # content type is not like text/... + my $transaction_content_obj = $self->TransactionObj->ContentObj; - if ( $RT::MailCommand eq 'sendmail' ) { - push @mailer_args, split(/\s+/, $SendmailArguments); - } - elsif ( $RT::MailCommand eq 'smtp' ) { - $ENV{MAILADDRESS} = $RT::SMTPFrom || $MIMEObj->head->get('From'); - push @mailer_args, ( Host => $RT::SMTPServer ); - push @mailer_args, ( Debug => $RT::SMTPDebug ); - $method = 'smtpsend'; - } - else { - push @mailer_args, $RT::MailParams; + if ( $transaction_content_obj + && $transaction_content_obj->ContentType =~ m{text/}i ) + { + # If this was part of a multipart/alternative, skip all of the kids + my $parent = $transaction_content_obj->ParentObj; + if ($parent and $parent->Id and $parent->ContentType eq "multipart/alternative") { + $attachments->Limit( + ENTRYAGGREGATOR => 'AND', + FIELD => 'parent', + OPERATOR => '!=', + VALUE => $parent->Id, + ); + } else { + $attachments->Limit( + ENTRYAGGREGATOR => 'AND', + FIELD => 'id', + OPERATOR => '!=', + VALUE => $transaction_content_obj->Id, + ); } + } - unless ( $MIMEObj->$method(@mailer_args) ) { - $RT::Logger->crit( $msgid . "Could not send mail." ); - return (0); + # attach any of this transaction's attachments + my $seen_attachment = 0; + while ( my $attach = $attachments->Next ) { + if ( !$seen_attachment ) { + $MIMEObj->make_multipart( 'mixed', Force => 1 ); + $seen_attachment = 1; } + $self->AddAttachment($attach); } - return 1; } -# }}} - -# {{{ AddAttachments +=head2 AddAttachment $attachment -=head2 AddAttachments - -Takes any attachments to this transaction and attaches them to the message +Takes one attachment object of L<RT::Attachmment> class and attaches it to the message we're building. =cut +sub AddAttachment { + my $self = shift; + my $attach = shift; + my $MIMEObj = shift || $self->TemplateObj->MIMEObj; + + $MIMEObj->attach( + Type => $attach->ContentType, + Charset => $attach->OriginalEncoding, + Data => $attach->OriginalContent, + Filename => $self->MIMEEncodeString( $attach->Filename ), + 'RT-Attachment:' => $self->TicketObj->Id . "/" + . $self->TransactionObj->Id . "/" + . $attach->id, + Encoding => '-SUGGEST', + ); +} -sub AddAttachments { - my $self = shift; +=head2 AttachTickets [@IDs] - my $MIMEObj = $self->TemplateObj->MIMEObj; +Returns or set list of ticket's IDs that should be attached to an outgoing message. - $MIMEObj->head->delete('RT-Attach-Message'); +B<Note> this method works as a class method and setup things global, so you have to +clean list by passing undef as argument. - my $attachments = RT::Attachments->new($RT::SystemUser); - $attachments->Limit( - FIELD => 'TransactionId', - VALUE => $self->TransactionObj->Id - ); - $attachments->OrderBy( FIELD => 'id'); - - my $transaction_content_obj = $self->TransactionObj->ContentObj; +=cut - # attach any of this transaction's attachments - while ( my $attach = $attachments->Next ) { +{ + my $list = []; - # Don't attach anything blank - next unless ( $attach->ContentLength ); - -# We want to make sure that we don't include the attachment that's being used as the "Content" of this message. - next - if ( $transaction_content_obj - && $transaction_content_obj->Id == $attach->Id - && $transaction_content_obj->ContentType =~ qr{text/plain}i ); - $MIMEObj->make_multipart('mixed'); - $MIMEObj->attach( - Type => $attach->ContentType, - Charset => $attach->OriginalEncoding, - Data => $attach->OriginalContent, - Filename => $self->MIMEEncodeString( $attach->Filename, - $RT::EmailOutputEncoding ), - 'RT-Attachment:' => $self->TicketObj->Id."/".$self->TransactionObj->Id."/".$attach->id, - Encoding => '-SUGGEST' - ); + sub AttachTickets { + my $self = shift; + $list = [ grep defined, @_ ] if @_; + return @$list; } +} + +=head2 AddTickets + +Attaches tickets to the current message, list of tickets' ids get from +L</AttachTickets> method. +=cut + +sub AddTickets { + my $self = shift; + $self->AddTicket($_) foreach $self->AttachTickets; + return; } -# }}} +=head2 AddTicket $ID -# {{{ RecordOutgoingMailTransaction +Attaches a ticket with ID to the message. + +Each ticket is attached as multipart entity and all its messages and attachments +are attached as sub entities in order of creation, but only if transaction type +is Create or Correspond. + +=cut + +sub AddTicket { + my $self = shift; + my $tid = shift; + + # XXX: we need a current user here, but who is current user? + my $attachs = RT::Attachments->new($RT::SystemUser); + my $txn_alias = $attachs->TransactionAlias; + $attachs->Limit( ALIAS => $txn_alias, FIELD => 'Type', VALUE => 'Create' ); + $attachs->Limit( + ALIAS => $txn_alias, + FIELD => 'Type', + VALUE => 'Correspond' + ); + $attachs->LimitByTicket($tid); + $attachs->LimitNotEmpty; + $attachs->OrderBy( FIELD => 'Created' ); + + my $ticket_mime = MIME::Entity->build( + Type => 'multipart/mixed', + Top => 0, + Description => "ticket #$tid", + ); + while ( my $attachment = $attachs->Next ) { + $self->AddAttachment( $attachment, $ticket_mime ); + } + if ( $ticket_mime->parts ) { + my $email_mime = $self->TemplateObj->MIMEObj; + $email_mime->make_multipart; + $email_mime->add_part($ticket_mime); + } + return; +} =head2 RecordOutgoingMailTransaction MIMEObj @@ -440,12 +500,9 @@ Record a transaction in RT with this outgoing message for future record-keeping =cut - - sub RecordOutgoingMailTransaction { - my $self = shift; + my $self = shift; my $MIMEObj = shift; - my @parts = $MIMEObj->parts; my @attachments; @@ -453,26 +510,28 @@ sub RecordOutgoingMailTransaction { foreach my $part (@parts) { my $attach = $part->head->get('RT-Attachment'); if ($attach) { - $RT::Logger->debug("We found an attachment. we want to not record it."); + $RT::Logger->debug( + "We found an attachment. we want to not record it."); push @attachments, $attach; } else { $RT::Logger->debug("We found a part. we want to record it."); push @keep, $part; } } - $MIMEObj->parts(\@keep); + $MIMEObj->parts( \@keep ); foreach my $attachment (@attachments) { - $MIMEObj->head->add('RT-Attachment', $attachment); + $MIMEObj->head->add( 'RT-Attachment', $attachment ); } RT::I18N::SetMIMEEntityToEncoding( $MIMEObj, 'utf-8', 'mime_words_ok' ); - my $transaction = RT::Transaction->new($self->TransactionObj->CurrentUser); + my $transaction + = RT::Transaction->new( $self->TransactionObj->CurrentUser ); - # XXX: TODO -> Record attachments as references to things in the attachments table, maybe. +# XXX: TODO -> Record attachments as references to things in the attachments table, maybe. my $type; - if ($self->TransactionObj->Type eq 'Comment') { + if ( $self->TransactionObj->Type eq 'Comment' ) { $type = 'CommentEmailRecord'; } else { $type = 'EmailRecord'; @@ -489,19 +548,15 @@ sub RecordOutgoingMailTransaction { ActivateScrips => 0 ); - if( $id ) { - $self->{'OutgoingMailTransaction'} = $id; + if ($id) { + $self->{'OutgoingMailTransaction'} = $id; } else { - $RT::Logger->warning( "Could not record outgoing message transaction: $msg" ); + $RT::Logger->warning( + "Could not record outgoing message transaction: $msg"); } return $id; } -# }}} -# - -# {{{ sub SetRTSpecialHeaders - =head2 SetRTSpecialHeaders This routine adds all the random headers that RT wants in a mail message @@ -514,161 +569,266 @@ sub SetRTSpecialHeaders { $self->SetSubject(); $self->SetSubjectToken(); - $self->SetHeaderAsEncoding( 'Subject', $RT::EmailOutputEncoding ) - if ($RT::EmailOutputEncoding); + $self->SetHeaderAsEncoding( 'Subject', + RT->Config->Get('EmailOutputEncoding') ) + if ( RT->Config->Get('EmailOutputEncoding') ); $self->SetReturnAddress(); $self->SetReferencesHeaders(); - unless ($self->TemplateObj->MIMEObj->head->get('Message-ID')) { - # Get Message-ID for this txn - my $msgid = ""; - $msgid = $self->TransactionObj->Message->First->GetHeader("RT-Message-ID") - || $self->TransactionObj->Message->First->GetHeader("Message-ID") - if $self->TransactionObj->Message && $self->TransactionObj->Message->First; + unless ( $self->TemplateObj->MIMEObj->head->get('Message-ID') ) { - # If there is one, and we can parse it, then base our Message-ID on it - if ($msgid - and $msgid =~ s/<(rt-.*?-\d+-\d+)\.(\d+)-\d+-\d+\@\Q$RT::Organization\E>$/ + # Get Message-ID for this txn + my $msgid = ""; + if ( my $msg = $self->TransactionObj->Message->First ) { + $msgid = $msg->GetHeader("RT-Message-ID") + || $msg->GetHeader("Message-ID"); + } + + # If there is one, and we can parse it, then base our Message-ID on it + if ( $msgid + and $msgid + =~ s/<(rt-.*?-\d+-\d+)\.(\d+)-\d+-\d+\@\QRT->Config->Get('Organization')\E>$/ "<$1." . $self->TicketObj->id . "-" . $self->ScripObj->id . "-" . $self->ScripActionObj->{_Message_ID} - . "@" . $RT::Organization . ">"/eg - and $2 == $self->TicketObj->id) { - $self->SetHeader( "Message-ID" => $msgid ); - } else { - $self->SetHeader( 'Message-ID', - "<rt-" - . $RT::VERSION . "-" - . $$ . "-" - . CORE::time() . "-" - . int(rand(2000)) . '.' - . $self->TicketObj->id . "-" - . $self->ScripObj->id . "-" # Scrip - . $self->ScripActionObj->{_Message_ID} . "@" # Email sent - . $RT::Organization - . ">" ); - } + . "@" . RT->Config->Get('Organization') . ">"/eg + and $2 == $self->TicketObj->id + ) + { + $self->SetHeader( "Message-ID" => $msgid ); + } else { + $self->SetHeader( + 'Message-ID' => RT::Interface::Email::GenMessageId( + Ticket => $self->TicketObj, + Scrip => $self->ScripObj, + ScripAction => $self->ScripActionObj + ), + ); + } } $self->SetHeader( 'Precedence', "bulk" ) - unless ( $self->TemplateObj->MIMEObj->head->get("Precedence") ); + unless ( $self->TemplateObj->MIMEObj->head->get("Precedence") ); - $self->SetHeader( 'X-RT-Loop-Prevention', $RT::rtname ); + $self->SetHeader( 'X-RT-Loop-Prevention', RT->Config->Get('rtname') ); $self->SetHeader( 'RT-Ticket', - $RT::rtname . " #" . $self->TicketObj->id() ); + RT->Config->Get('rtname') . " #" . $self->TicketObj->id() ); $self->SetHeader( 'Managed-by', "RT $RT::VERSION (http://www.bestpractical.com/rt/)" ); - $self->SetHeader( 'RT-Originator', - $self->TransactionObj->CreatorObj->EmailAddress ); +# XXX, TODO: use /ShowUser/ShowUserEntry(or something like that) when it would be +# refactored into user's method. + if ( my $email = $self->TransactionObj->CreatorObj->EmailAddress ) { + $self->SetHeader( 'RT-Originator', $email ); + } } -# }}} +sub DeferDigestRecipients { + my $self = shift; + $RT::Logger->debug( "Calling SetRecipientDigests for transaction " . $self->TransactionObj . ", id " . $self->TransactionObj->id ); + + # The digest attribute will be an array of notifications that need to + # be sent for this transaction. The array will have the following + # format for its objects. + # $digest_hash -> {daily|weekly|susp} -> address -> {To|Cc|Bcc} + # -> sent -> {true|false} + # The "sent" flag will be used by the cron job to indicate that it has + # run on this transaction. + # In a perfect world we might move this hash construction to the + # extension module itself. + my $digest_hash = {}; + + foreach my $mailfield (@EMAIL_RECIPIENT_HEADERS) { + # If we have a "PseudoTo", the "To" contains it, so we don't need to access it + next if ( ( $self->{'PseudoTo'} && @{ $self->{'PseudoTo'} } ) && ( $mailfield eq 'To' ) ); + $RT::Logger->debug( "Working on mailfield $mailfield; recipients are " . join( ',', @{ $self->{$mailfield} } ) ); + + # Store the 'daily digest' folk in an array. + my ( @send_now, @daily_digest, @weekly_digest, @suspended ); + + # Have to get the list of addresses directly from the MIME header + # at this point. + $RT::Logger->debug( $self->TemplateObj->MIMEObj->head->as_string ); + foreach my $rcpt ( map { $_->address } $self->AddressesFromHeader($mailfield) ) { + next unless $rcpt; + my $user_obj = RT::User->new($RT::SystemUser); + $user_obj->LoadByEmail($rcpt); + if ( ! $user_obj->id ) { + # If there's an email address in here without an associated + # RT user, pass it on through. + $RT::Logger->debug( "User $rcpt is not associated with an RT user object. Send mail."); + push( @send_now, $rcpt ); + next; + } -# }}} + my $mailpref = RT->Config->Get( 'EmailFrequency', $user_obj ) || ''; + $RT::Logger->debug( "Got user mail preference '$mailpref' for user $rcpt"); -# {{{ RemoveInappropriateRecipients + if ( $mailpref =~ /daily/i ) { push( @daily_digest, $rcpt ) } + elsif ( $mailpref =~ /weekly/i ) { push( @weekly_digest, $rcpt ) } + elsif ( $mailpref =~ /suspend/i ) { push( @suspended, $rcpt ) } + else { push( @send_now, $rcpt ) } + } -=head2 RemoveInappropriateRecipients + # Reset the relevant mail field. + $RT::Logger->debug( "Removing deferred recipients from $mailfield: line"); + if (@send_now) { + $self->SetHeader( $mailfield, join( ', ', @send_now ) ); + } else { # No recipients! Remove the header. + $self->TemplateObj->MIMEObj->head->delete($mailfield); + } -Remove addresses that are RT addresses or that are on this transaction's blacklist + # Push the deferred addresses into the appropriate field in + # our attribute hash, with the appropriate mail header. + $RT::Logger->debug( + "Setting deferred recipients for attribute creation"); + $digest_hash->{'daily'}->{$_} = {'header' => $mailfield , _sent => 0} for (@daily_digest); + $digest_hash->{'weekly'}->{$_} ={'header' => $mailfield, _sent => 0} for (@weekly_digest); + $digest_hash->{'susp'}->{$_} = {'header' => $mailfield, _sent =>0 } for (@suspended); + } -=cut + if ( scalar keys %$digest_hash ) { -sub RemoveInappropriateRecipients { + # Save the hash so that we can add it as an attribute to the + # outgoing email transaction. + $self->{'Deferred'} = $digest_hash; + } else { + $RT::Logger->debug( "No recipients found for deferred delivery on " + . "transaction #" + . $self->TransactionObj->id ); + } +} + + + +sub RecordDeferredRecipients { my $self = shift; + return unless exists $self->{'Deferred'}; + + my $txn_id = $self->{'OutgoingMailTransaction'}; + return unless $txn_id; - my $msgid = $self->TemplateObj->MIMEObj->head->get ('Message-Id'); + my $txn_obj = RT::Transaction->new( $self->CurrentUser ); + $txn_obj->Load( $txn_id ); + my( $ret, $msg ) = $txn_obj->AddAttribute( + Name => 'DeferredRecipients', + Content => $self->{'Deferred'} + ); + $RT::Logger->warning( "Unable to add deferred recipients to outgoing transaction: $msg" ) + unless $ret; + return ($ret,$msg); +} +=head2 SquelchMailTo [@ADDRESSES] - my @blacklist; +Mark ADDRESSES to be removed from list of the recipients. Returns list of the addresses. +To empty list pass undefined argument. + +B<Note> that this method can be called as class method and works globaly. Don't forget to +clean this list when blocking is not required anymore, pass undef to do this. + +=cut - my @types = qw/To Cc Bcc/; +{ + my $squelch = []; - # Weed out any RT addresses. We really don't want to talk to ourselves! - foreach my $type (@types) { - @{ $self->{$type} } = - RT::EmailParser::CullRTAddresses( "", @{ $self->{$type} } ); + sub SquelchMailTo { + my $self = shift; + if (@_) { + $squelch = [ grep defined, @_ ]; + } + return @$squelch; } +} + +=head2 RemoveInappropriateRecipients + +Remove addresses that are RT addresses or that are on this transaction's blacklist + +=cut + +sub RemoveInappropriateRecipients { + my $self = shift; + + my @blacklist = (); # If there are no recipients, don't try to send the message. # If the transaction has content and has the header RT-Squelch-Replies-To - if ( $self->TransactionObj->Attachments->First() ) { - if ( - $self->TransactionObj->Attachments->First->GetHeader( - 'RT-DetectedAutoGenerated') - ) - { + my $msgid = $self->TemplateObj->MIMEObj->head->get('Message-Id'); + if ( my $attachment = $self->TransactionObj->Attachments->First ) { + + if ( $attachment->GetHeader('RT-DetectedAutoGenerated') ) { # What do we want to do with this? It's probably (?) a bounce # caused by one of the watcher addresses being broken. # Default ("true") is to redistribute, for historical reasons. - if ( !$RT::RedistributeAutoGeneratedMessages ) { + if ( !RT->Config->Get('RedistributeAutoGeneratedMessages') ) { # Don't send to any watchers. - @{ $self->{'To'} } = (); - @{ $self->{'Cc'} } = (); - @{ $self->{'Bcc'} } = (); - - $RT::Logger->info( $msgid . " The incoming message was autogenerated. Not redistributing this message based on site configuration.\n"); - } - elsif ( $RT::RedistributeAutoGeneratedMessages eq 'privileged' ) { + @{ $self->{$_} } = () for (@EMAIL_RECIPIENT_HEADERS); + $RT::Logger->info( $msgid + . " The incoming message was autogenerated. " + . "Not redistributing this message based on site configuration." + ); + } elsif ( RT->Config->Get('RedistributeAutoGeneratedMessages') eq + 'privileged' ) + { # Only send to "privileged" watchers. - # - - foreach my $type (@types) { - + foreach my $type (@EMAIL_RECIPIENT_HEADERS) { foreach my $addr ( @{ $self->{$type} } ) { my $user = RT::User->new($RT::SystemUser); $user->LoadByEmail($addr); - @{ $self->{$type} } = - grep ( !/^\Q$addr\E$/, @{ $self->{$type} } ) - if ( !$user->Privileged ); - + push @blacklist, $addr if ( !$user->Privileged ); } } - $RT::Logger->info( $msgid . " The incoming message was autogenerated. Not redistributing this message to unprivileged users based on site configuration.\n"); - + $RT::Logger->info( $msgid + . " The incoming message was autogenerated. " + . "Not redistributing this message to unprivileged users based on site configuration." + ); } - } - my $squelch = - $self->TransactionObj->Attachments->First->GetHeader( - 'RT-Squelch-Replies-To'); - - if ($squelch) { - @blacklist = split( /,/, $squelch ); + if ( my $squelch = $attachment->GetHeader('RT-Squelch-Replies-To') ) { + push @blacklist, split( /,/, $squelch ); } } - # Let's grab the SquelchMailTo attribue and push those entries into the @blacklist - my @non_recipients = $self->TicketObj->SquelchMailTo; - foreach my $attribute (@non_recipients) { - push @blacklist, $attribute->Content; - } +# Let's grab the SquelchMailTo attribue and push those entries into the @blacklist + push @blacklist, map $_->Content, $self->TicketObj->SquelchMailTo; + push @blacklist, $self->SquelchMailTo; # Cycle through the people we're sending to and pull out anyone on the # system blacklist - foreach my $person_to_yank (@blacklist) { - $person_to_yank =~ s/\s//g; - foreach my $type (@types) { - @{ $self->{$type} } = - grep ( !/^\Q$person_to_yank\E$/, @{ $self->{$type} } ); + # Trim leading and trailing spaces. + @blacklist = map { RT::User->CanonicalizeEmailAddress( $_->address ) } Email::Address->parse(join(', ', grep {defined} @blacklist)); + + foreach my $type (@EMAIL_RECIPIENT_HEADERS) { + my @addrs; + foreach my $addr ( @{ $self->{$type} } ) { + + # Weed out any RT addresses. We really don't want to talk to ourselves! + # If we get a reply back, that means it's not an RT address + if ( !RT::EmailParser->CullRTAddresses($addr) ) { + $RT::Logger->info( $msgid . "$addr appears to point to this RT instance. Skipping" ); + next; + } + if ( grep /^\Q$addr\E$/, @blacklist ) { + $RT::Logger->info( $msgid . "$addr was blacklisted for outbound mail on this transaction. Skipping"); + next; + } + push @addrs, $addr; } + @{ $self->{$type} } = @addrs; } } -# }}} -# {{{ sub SetReturnAddress - =head2 SetReturnAddress is_comment => BOOLEAN Calculate and set From and Reply-To headers based on the is_comment flag. @@ -680,6 +840,7 @@ sub SetReturnAddress { my $self = shift; my %args = ( is_comment => 0, + friendly_name => undef, @_ ); @@ -689,33 +850,35 @@ sub SetReturnAddress { if ( $args{'is_comment'} ) { $replyto = $self->TicketObj->QueueObj->CommentAddress - || $RT::CommentAddress; - } - else { + || RT->Config->Get('CommentAddress'); + } else { $replyto = $self->TicketObj->QueueObj->CorrespondAddress - || $RT::CorrespondAddress; + || RT->Config->Get('CorrespondAddress'); } unless ( $self->TemplateObj->MIMEObj->head->get('From') ) { - if ($RT::UseFriendlyFromLine) { - my $friendly_name = $self->TransactionObj->CreatorObj->RealName - || $self->TransactionObj->CreatorObj->Name; - if ( $friendly_name =~ /^"(.*)"$/ ) { # a quoted string - $friendly_name = $1; + if ( RT->Config->Get('UseFriendlyFromLine') ) { + my $friendly_name = $args{friendly_name}; + + unless ( $friendly_name ) { + $friendly_name = $self->TransactionObj->CreatorObj->FriendlyName; + if ( $friendly_name =~ /^"(.*)"$/ ) { # a quoted string + $friendly_name = $1; + } } $friendly_name =~ s/"/\\"/g; $self->SetHeader( 'From', sprintf( - $RT::FriendlyFromLineFormat, - $self->MIMEEncodeString( $friendly_name, - $RT::EmailOutputEncoding ), + RT->Config->Get('FriendlyFromLineFormat'), + $self->MIMEEncodeString( + $friendly_name, RT->Config->Get('EmailOutputEncoding') + ), $replyto ), ); - } - else { + } else { $self->SetHeader( 'From', $replyto ); } } @@ -726,10 +889,6 @@ sub SetReturnAddress { } -# }}} - -# {{{ sub SetHeader - =head2 SetHeader FIELD, VALUE Set the FIELD of the current MIME object into VALUE. @@ -743,20 +902,16 @@ sub SetHeader { chomp $val; chomp $field; - $self->TemplateObj->MIMEObj->head->fold_length( $field, 10000 ); - $self->TemplateObj->MIMEObj->head->replace( $field, $val ); - return $self->TemplateObj->MIMEObj->head->get($field); + my $head = $self->TemplateObj->MIMEObj->head; + $head->fold_length( $field, 10000 ); + $head->replace( $field, $val ); + return $head->get($field); } -# }}} - - -# {{{ sub SetSubject - =head2 SetSubject -This routine sets the subject. it does not add the rt tag. that gets done elsewhere -If $self->{'Subject'} is already defined, it uses that. otherwise, it tries to get +This routine sets the subject. it does not add the rt tag. That gets done elsewhere +If subject is already defined via template, it uses that. otherwise, it tries to get the transaction's subject. =cut @@ -765,39 +920,29 @@ sub SetSubject { my $self = shift; my $subject; - my $message = $self->TransactionObj->Attachments; if ( $self->TemplateObj->MIMEObj->head->get('Subject') ) { return (); } + + my $message = $self->TransactionObj->Attachments; + $message->RowsPerPage(1); if ( $self->{'Subject'} ) { $subject = $self->{'Subject'}; + } elsif ( my $first = $message->First ) { + my $tmp = $first->GetHeader('Subject'); + $subject = defined $tmp ? $tmp : $self->TicketObj->Subject; + } else { + $subject = $self->TicketObj->Subject; } - elsif ( ( $message->First() ) && ( $message->First->Headers ) ) { - my $header = $message->First->Headers(); - $header =~ s/\n\s+/ /g; - if ( $header =~ /^Subject: (.*?)$/m ) { - $subject = $1; - } - else { - $subject = $self->TicketObj->Subject(); - } - - } - else { - $subject = $self->TicketObj->Subject(); - } + $subject = '' unless defined $subject; + chomp $subject; - $subject =~ s/(\r\n|\n|\s)/ /gi; + $subject =~ s/(\r\n|\n|\s)/ /g; - chomp $subject; $self->SetHeader( 'Subject', $subject ); } -# }}} - -# {{{ sub SetSubjectToken - =head2 SetSubjectToken This routine fixes the RT tag in the subject. It's unlikely that you want to overwrite this. @@ -806,22 +951,16 @@ This routine fixes the RT tag in the subject. It's unlikely that you want to ove sub SetSubjectToken { my $self = shift; - my $sub = $self->TemplateObj->MIMEObj->head->get('Subject'); - my $id = $self->TicketObj->id; - - my $token_re = $RT::EmailSubjectTagRegex; - $token_re = qr/\Q$RT::rtname\E/o unless $token_re; - return if $sub =~ /\[$token_re\s+#$id\]/; - $sub =~ s/(\r\n|\n|\s)/ /gi; - chomp $sub; - $self->TemplateObj->MIMEObj->head->replace( - Subject => "[$RT::rtname #$id] $sub", + my $head = $self->TemplateObj->MIMEObj->head; + $head->replace( + Subject => RT::Interface::Email::AddSubjectTag( + Encode::decode_utf8( $head->get('Subject') ), + $self->TicketObj, + ), ); } -# }}} - =head2 SetReferencesHeaders Set References and In-Reply-To headers for this message. @@ -829,18 +968,14 @@ Set References and In-Reply-To headers for this message. =cut sub SetReferencesHeaders { - my $self = shift; my ( @in_reply_to, @references, @msgid ); - my $attachments = $self->TransactionObj->Message; - - if ( my $top = $attachments->First() ) { - @in_reply_to = split(/\s+/m, $top->GetHeader('In-Reply-To') || ''); - @references = split(/\s+/m, $top->GetHeader('References') || '' ); - @msgid = split(/\s+/m, $top->GetHeader('Message-ID') || ''); - } - else { + if ( my $top = $self->TransactionObj->Message->First ) { + @in_reply_to = split( /\s+/m, $top->GetHeader('In-Reply-To') || '' ); + @references = split( /\s+/m, $top->GetHeader('References') || '' ); + @msgid = split( /\s+/m, $top->GetHeader('Message-ID') || '' ); + } else { return (undef); } @@ -848,53 +983,54 @@ sub SetReferencesHeaders { # the RT Web UI, and hence we want to *not* append its Message-ID # to the References and In-Reply-To. OR it came from an outside # source, and we should treat it as per the RFC - if ( "@msgid" =~ /<(rt-.*?-\d+-\d+)\.(\d+-0-0)\@$RT::Organization>/) { + my $org = RT->Config->Get('Organization'); + if ( "@msgid" =~ /<(rt-.*?-\d+-\d+)\.(\d+)-0-0\@\Q$org\E>/ ) { + + # Make all references which are internal be to version which we + # have sent out - # Make all references which are internal be to version which we - # have sent out - for (@references, @in_reply_to) { - s/<(rt-.*?-\d+-\d+)\.(\d+-0-0)\@$RT::Organization>$/ + for ( @references, @in_reply_to ) { + s/<(rt-.*?-\d+-\d+)\.(\d+-0-0)\@\Q$org\E>$/ "<$1." . $self->TicketObj->id . "-" . $self->ScripObj->id . "-" . $self->ScripActionObj->{_Message_ID} . - "@" . $RT::Organization . ">"/eg - } + "@" . $org . ">"/eg + } - # In reply to whatever the internal message was in reply to - $self->SetHeader( 'In-Reply-To', join( " ", ( @in_reply_to ))); + # In reply to whatever the internal message was in reply to + $self->SetHeader( 'In-Reply-To', join( " ", (@in_reply_to) ) ); - # Default the references to whatever we're in reply to - @references = @in_reply_to unless @references; + # Default the references to whatever we're in reply to + @references = @in_reply_to unless @references; - # References are unchanged from internal + # References are unchanged from internal } else { - # In reply to that message - $self->SetHeader( 'In-Reply-To', join( " ", ( @msgid ))); - # Default the references to whatever we're in reply to - @references = @in_reply_to unless @references; + # In reply to that message + $self->SetHeader( 'In-Reply-To', join( " ", (@msgid) ) ); - # Push that message onto the end of the references - push @references, @msgid; + # Default the references to whatever we're in reply to + @references = @in_reply_to unless @references; + + # Push that message onto the end of the references + push @references, @msgid; } # Push pseudo-ref to the front my $pseudo_ref = $self->PseudoReference; - @references = ($pseudo_ref, grep { $_ ne $pseudo_ref } @references); + @references = ( $pseudo_ref, grep { $_ ne $pseudo_ref } @references ); # If there are more than 10 references headers, remove all but the # first four and the last six (Gotta keep this from growing # forever) - splice(@references, 4, -6) if ($#references >= 10); + splice( @references, 4, -6 ) if ( $#references >= 10 ); # Add on the references - $self->SetHeader( 'References', join( " ", @references) ); + $self->SetHeader( 'References', join( " ", @references ) ); $self->TemplateObj->MIMEObj->head->fold_length( 'References', 80 ); } -# }}} - =head2 PseudoReference Returns a fake Message-ID: header for the ticket to allow a base level of threading @@ -904,13 +1040,13 @@ Returns a fake Message-ID: header for the ticket to allow a base level of thread sub PseudoReference { my $self = shift; - my $pseudo_ref = '<RT-Ticket-'.$self->TicketObj->id .'@'.$RT::Organization .'>'; + my $pseudo_ref + = '<RT-Ticket-' + . $self->TicketObj->id . '@' + . RT->Config->Get('Organization') . '>'; return $pseudo_ref; } - -# {{{ SetHeadingAsEncoding - =head2 SetHeaderAsEncoding($field_name, $charset_encoding) This routine converts the field into specified charset encoding. @@ -921,86 +1057,37 @@ sub SetHeaderAsEncoding { my $self = shift; my ( $field, $enc ) = ( shift, shift ); - if ($field eq 'From' and $RT::SMTPFrom) { - $self->TemplateObj->MIMEObj->head->replace( $field, $RT::SMTPFrom ); - return; - } - - my $value = $self->TemplateObj->MIMEObj->head->get($field); - - $value = $self->MIMEEncodeString($value, $enc); + my $head = $self->TemplateObj->MIMEObj->head; - $self->TemplateObj->MIMEObj->head->replace( $field, $value ); + if ( lc($field) eq 'from' and RT->Config->Get('SMTPFrom') ) { + $head->replace( $field, RT->Config->Get('SMTPFrom') ); + return; + } + my $value = $head->get( $field ); + $value = $self->MIMEEncodeString( $value, $enc ); + $head->replace( $field, $value ); -} -# }}} +} -# {{{ MIMEEncodeString +=head2 MIMEEncodeString -=head2 MIMEEncodeString STRING ENCODING +Takes a perl string and optional encoding pass it over +L<RT::Interface::Email/EncodeToMIME>. -Takes a string and a possible encoding and returns the string wrapped in MIME goo. +Basicly encode a string using B encoding according to RFC2047. =cut sub MIMEEncodeString { - my $self = shift; - my $value = shift; - # using RFC2047 notation, sec 2. - # encoded-word = "=?" charset "?" encoding "?" encoded-text "?=" - my $charset = shift; - my $encoding = 'B'; - # An 'encoded-word' may not be more than 75 characters long - # - # MIME encoding increases 4/3*(number of bytes), and always in multiples - # of 4. Thus we have to find the best available value of bytes available - # for each chunk. - # - # First we get the integer max which max*4/3 would fit on space. - # Then we find the greater multiple of 3 lower or equal than $max. - my $max = int(((75-length('=?'.$charset.'?'.$encoding.'?'.'?='))*3)/4); - $max = int($max/3)*3; - - chomp $value; - - if ( $max <= 0 ) { - # gives an error... - $RT::Logger->crit("Can't encode! Charset or encoding too big.\n"); - return ($value); - } - - return ($value) unless $value =~ /[^\x20-\x7e]/; - - $value =~ s/\s*$//; - - # we need perl string to split thing char by char - Encode::_utf8_on($value) unless Encode::is_utf8( $value ); - - my ($tmp, @chunks) = ('', ()); - while ( length $value ) { - my $char = substr($value, 0, 1, ''); - my $octets = Encode::encode( $charset, $char ); - if ( length($tmp) + length($octets) > $max ) { - push @chunks, $tmp; - $tmp = ''; - } - $tmp .= $octets; - } - push @chunks, $tmp if length $tmp; - - # encode an join chuncks - $value = join "\n ", - map encode_mimeword( $_, $encoding, $charset ), @chunks ; - return($value); + my $self = shift; + return RT::Interface::Email::EncodeToMIME( String => $_[0], Charset => $_[1] ); } -# }}} - eval "require RT::Action::SendEmail_Vendor"; -die $@ if ($@ && $@ !~ qr{^Can't locate RT/Action/SendEmail_Vendor.pm}); +die $@ if ( $@ && $@ !~ qr{^Can't locate RT/Action/SendEmail_Vendor.pm} ); eval "require RT::Action::SendEmail_Local"; -die $@ if ($@ && $@ !~ qr{^Can't locate RT/Action/SendEmail_Local.pm}); +die $@ if ( $@ && $@ !~ qr{^Can't locate RT/Action/SendEmail_Local.pm} ); 1; diff --git a/rt/lib/RT/Action/SetPriority.pm b/rt/lib/RT/Action/SetPriority.pm index b4c8ee199..9b0838926 100644 --- a/rt/lib/RT/Action/SetPriority.pm +++ b/rt/lib/RT/Action/SetPriority.pm @@ -1,8 +1,8 @@ # BEGIN BPS TAGGED BLOCK {{{ # # COPYRIGHT: -# -# This software is Copyright (c) 1996-2009 Best Practical Solutions, LLC +# +# This software is Copyright (c) 1996-2009 Best Practical Solutions, LLC # <jesse@bestpractical.com> # # (Except where explicitly superseded by other copyright notices) @@ -45,12 +45,11 @@ # those contributions and any derivatives thereof. # # END BPS TAGGED BLOCK }}} + package RT::Action::SetPriority; -require RT::Action::Generic; +use base 'RT::Action'; use strict; -use vars qw/@ISA/; -@ISA=qw(RT::Action::Generic); #Do what we need to do and send it out. diff --git a/rt/lib/RT/Action/UserDefined.pm b/rt/lib/RT/Action/UserDefined.pm index 7bf6eee51..80ef49224 100644 --- a/rt/lib/RT/Action/UserDefined.pm +++ b/rt/lib/RT/Action/UserDefined.pm @@ -1,8 +1,8 @@ # BEGIN BPS TAGGED BLOCK {{{ # # COPYRIGHT: -# -# This software is Copyright (c) 1996-2009 Best Practical Solutions, LLC +# +# This software is Copyright (c) 1996-2009 Best Practical Solutions, LLC # <jesse@bestpractical.com> # # (Except where explicitly superseded by other copyright notices) @@ -45,14 +45,11 @@ # those contributions and any derivatives thereof. # # END BPS TAGGED BLOCK }}} - package RT::Action::UserDefined; -use RT::Action::Generic; +use base 'RT::Action'; use strict; -use vars qw/@ISA/; -@ISA = qw(RT::Action::Generic); =head2 Prepare |