# BEGIN BPS TAGGED BLOCK {{{ # # COPYRIGHT: # # This software is Copyright (c) 1996-2018 Best Practical Solutions, LLC # # # (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::Dashboard::Mailer; use strict; use warnings; use HTML::Mason; use HTML::RewriteAttributes::Links; use HTML::RewriteAttributes::Resources; use MIME::Types; use POSIX 'tzset'; use RT::Dashboard; use RT::Interface::Web::Handler; use RT::Interface::Web; use File::Temp 'tempdir'; use HTML::Scrubber; use URI::QueryParam; sub MailDashboards { my $self = shift; my %args = ( All => 0, DryRun => 0, Time => time, @_, ); $RT::Logger->debug("Using time $args{Time} for dashboard generation"); my $from = $self->GetFrom(); $RT::Logger->debug("Sending email from $from"); # look through each user for her subscriptions my $Users = RT::Users->new(RT->SystemUser); $Users->LimitToPrivileged; while (defined(my $user = $Users->Next)) { if ($user->PrincipalObj->Disabled) { $RT::Logger->debug("Skipping over " . $user->Name . " due to having a disabled account."); next; } my ($hour, $dow, $dom) = HourDowDomIn($args{Time}, $user->Timezone || RT->Config->Get('Timezone')); $hour .= ':00'; $RT::Logger->debug("Checking ".$user->Name."'s subscriptions: hour $hour, dow $dow, dom $dom"); my $currentuser = RT::CurrentUser->new; $currentuser->LoadByName($user->Name); # look through this user's subscriptions, are any supposed to be generated # right now? for my $subscription ($user->Attributes->Named('Subscription')) { next unless $self->IsSubscriptionReady( %args, Subscription => $subscription, User => $user, LocalTime => [$hour, $dow, $dom], ); my $email = $subscription->SubValue('Recipient') || $user->EmailAddress; eval { $self->SendDashboard( %args, CurrentUser => $currentuser, Email => $email, Subscription => $subscription, From => $from, ) }; if ( $@ ) { $RT::Logger->error("Caught exception: $@"); } else { my $counter = $subscription->SubValue('Counter') || 0; $subscription->SetSubValues(Counter => $counter + 1) unless $args{DryRun}; } } } } sub IsSubscriptionReady { my $self = shift; my %args = ( All => 0, Subscription => undef, User => undef, LocalTime => [0, 0, 0], @_, ); return 1 if $args{All}; my $subscription = $args{Subscription}; my $counter = $subscription->SubValue('Counter') || 0; my $sub_frequency = $subscription->SubValue('Frequency'); my $sub_hour = $subscription->SubValue('Hour'); my $sub_dow = $subscription->SubValue('Dow'); my $sub_dom = $subscription->SubValue('Dom'); my $sub_fow = $subscription->SubValue('Fow') || 1; my ($hour, $dow, $dom) = @{ $args{LocalTime} }; $RT::Logger->debug("Checking against subscription " . $subscription->Id . " for " . $args{User}->Name . " with frequency $sub_frequency, hour $sub_hour, dow $sub_dow, dom $sub_dom, fow $sub_fow, counter $counter"); return 0 if $sub_frequency eq 'never'; # correct hour? return 0 if $sub_hour ne $hour; # all we need is the correct hour for daily dashboards return 1 if $sub_frequency eq 'daily'; if ($sub_frequency eq 'weekly') { # correct day of week? return 0 if $sub_dow ne $dow; # does it match the "every N weeks" clause? return 1 if $counter % $sub_fow == 0; $subscription->SetSubValues(Counter => $counter + 1) unless $args{DryRun}; return 0; } # if monthly, correct day of month? if ($sub_frequency eq 'monthly') { return $sub_dom == $dom; } # monday through friday if ($sub_frequency eq 'm-f') { return 0 if $dow eq 'Sunday' || $dow eq 'Saturday'; return 1; } $RT::Logger->debug("Invalid subscription frequency $sub_frequency for " . $args{User}->Name); # unknown frequency type, bail out return 0; } sub GetFrom { RT->Config->Get('DashboardAddress') || RT->Config->Get('OwnerEmail') } sub SendDashboard { my $self = shift; my %args = ( CurrentUser => undef, Email => undef, Subscription => undef, DryRun => 0, @_, ); my $currentuser = $args{CurrentUser}; my $subscription = $args{Subscription}; my $rows = $subscription->SubValue('Rows'); my $DashboardId = $subscription->SubValue('DashboardId'); my $dashboard = RT::Dashboard->new($currentuser); my ($ok, $msg) = $dashboard->LoadById($DashboardId); # failed to load dashboard. perhaps it was deleted or it changed privacy if (!$ok) { $RT::Logger->warning("Unable to load dashboard $DashboardId of subscription ".$subscription->Id." for user ".$currentuser->Name.": $msg"); return $self->ObsoleteSubscription( %args, Subscription => $subscription, ); } $RT::Logger->debug('Generating dashboard "'.$dashboard->Name.'" for user "'.$currentuser->Name.'":'); if ($args{DryRun}) { print << "SUMMARY"; Dashboard: @{[ $dashboard->Name ]} User: @{[ $currentuser->Name ]} <$args{Email}> SUMMARY return; } local $HTML::Mason::Commands::session{CurrentUser} = $currentuser; local $HTML::Mason::Commands::r = RT::Dashboard::FakeRequest->new; my $content = RunComponent( '/Dashboards/Render.html', id => $dashboard->Id, Preview => 0, ); if ( RT->Config->Get('EmailDashboardRemove') ) { for ( RT->Config->Get('EmailDashboardRemove') ) { $content =~ s/$_//g; } } $content = ScrubContent($content); $RT::Logger->debug("Got ".length($content)." characters of output."); $content = HTML::RewriteAttributes::Links->rewrite( $content, RT->Config->Get('WebURL') . 'Dashboards/Render.html', ); $self->EmailDashboard( %args, Dashboard => $dashboard, Content => $content, ); } sub ObsoleteSubscription { my $self = shift; my %args = ( From => undef, To => undef, Subscription => undef, CurrentUser => undef, @_, ); my $subscription = $args{Subscription}; my $ok = RT::Interface::Email::SendEmailUsingTemplate( From => $args{From}, To => $args{Email}, Template => 'Error: Missing dashboard', Arguments => { SubscriptionObj => $subscription, }, ExtraHeaders => { 'X-RT-Dashboard-Subscription-Id' => $subscription->Id, 'X-RT-Dashboard-Id' => $subscription->SubValue('DashboardId'), }, ); # only delete the subscription if the email looks like it went through if ($ok) { my ($deleted, $msg) = $subscription->Delete(); if ($deleted) { $RT::Logger->debug("Deleted an obsolete subscription: $msg"); } else { $RT::Logger->warning("Unable to delete an obsolete subscription: $msg"); } } else { $RT::Logger->warning("Unable to notify ".$args{CurrentUser}->Name." of an obsolete subscription"); } } sub EmailDashboard { my $self = shift; my %args = ( CurrentUser => undef, Email => undef, Dashboard => undef, Subscription => undef, Content => undef, @_, ); my $subscription = $args{Subscription}; my $dashboard = $args{Dashboard}; my $currentuser = $args{CurrentUser}; my $email = $args{Email}; my $frequency = $subscription->SubValue('Frequency'); my %frequency_lookup = ( 'm-f' => 'Weekday', # loc 'daily' => 'Daily', # loc 'weekly' => 'Weekly', # loc 'monthly' => 'Monthly', # loc 'never' => 'Never', # loc ); my $frequency_display = $frequency_lookup{$frequency} || $frequency; my $subject = sprintf '[%s] ' . RT->Config->Get('DashboardSubject'), RT->Config->Get('rtname'), $currentuser->loc($frequency_display), $dashboard->Name; my $entity = $self->BuildEmail( %args, To => $email, Subject => $subject, ); $entity->head->replace('X-RT-Dashboard-Id', $dashboard->Id); $entity->head->replace('X-RT-Dashboard-Subscription-Id', $subscription->Id); $RT::Logger->debug('Mailing dashboard "'.$dashboard->Name.'" to user '.$currentuser->Name." <$email>"); my $ok = RT::Interface::Email::SendEmail( %{ RT->Config->Get('Crypt')->{'Dashboards'} || {} }, Entity => $entity, ); if (!$ok) { $RT::Logger->error("Failed to email dashboard to user ".$currentuser->Name." <$email>"); return; } $RT::Logger->debug("Done sending dashboard to ".$currentuser->Name." <$email>"); } sub BuildEmail { my $self = shift; my %args = ( Content => undef, From => undef, To => undef, Subject => undef, @_, ); my @parts; my %cid_of; my $content = HTML::RewriteAttributes::Resources->rewrite($args{Content}, sub { my $uri = shift; # already attached this object return "cid:$cid_of{$uri}" if $cid_of{$uri}; my ($data, $filename, $mimetype, $encoding) = GetResource($uri); return $uri unless defined $data; $cid_of{$uri} = time() . $$ . int(rand(1e6)); # Encode textual data in UTF-8, and downgrade (treat # codepoints as codepoints, and ensure the UTF-8 flag is # off) everything else. my @extra; if ( $mimetype =~ m{text/} ) { $data = Encode::encode( "UTF-8", $data ); @extra = ( Charset => "UTF-8" ); } else { utf8::downgrade( $data, 1 ) or $RT::Logger->warning("downgrade $data failed"); } push @parts, MIME::Entity->build( Top => 0, Data => $data, Type => $mimetype, Encoding => $encoding, Disposition => 'inline', Name => RT::Interface::Email::EncodeToMIME( String => $filename ), 'Content-Id' => $cid_of{$uri}, @extra, ); return "cid:$cid_of{$uri}"; }, inline_css => sub { my $uri = shift; my ($content) = GetResource($uri); return defined $content ? $content : ""; }, inline_imports => 1, ); my $entity = MIME::Entity->build( From => Encode::encode("UTF-8", $args{From}), To => Encode::encode("UTF-8", $args{To}), Subject => RT::Interface::Email::EncodeToMIME( String => $args{Subject} ), Type => "multipart/mixed", ); $entity->attach( Type => 'text/html', Charset => 'UTF-8', Data => Encode::encode("UTF-8", $content), Disposition => 'inline', Encoding => "base64", ); for my $part (@parts) { $entity->add_part($part); } $entity->make_singlepart; return $entity; } { my $mason; my $outbuf = ''; my $data_dir = ''; sub _mason { unless ($mason) { $RT::Logger->debug("Creating Mason object."); # user may not have permissions on the data directory, so create a # new one $data_dir = tempdir(CLEANUP => 1); $mason = HTML::Mason::Interp->new( RT::Interface::Web::Handler->DefaultHandlerArgs, out_method => \$outbuf, autohandler_name => '', # disable forced login and more data_dir => $data_dir, ); $mason->set_escape( h => \&RT::Interface::Web::EscapeHTML ); $mason->set_escape( u => \&RT::Interface::Web::EscapeURI ); $mason->set_escape( j => \&RT::Interface::Web::EscapeJS ); } return $mason; } sub RunComponent { _mason->exec(@_); my $ret = $outbuf; $outbuf = ''; return $ret; } } { my $scrubber; sub _scrubber { unless ($scrubber) { $scrubber = HTML::Scrubber->new; # Allow everything by default, except JS attributes ... $scrubber->default( 1 => { '*' => 1, map { ("on$_" => 0) } qw(blur change click dblclick error focus keydown keypress keyup load mousedown mousemove mouseout mouseover mouseup reset select submit unload) } ); # ... and