X-Git-Url: http://git.freeside.biz/gitweb/?a=blobdiff_plain;f=rt%2Fsbin%2Frt-email-dashboards;fp=rt%2Fsbin%2Frt-email-dashboards;h=d46e0fe204a4809be86d28696ca0397af1555add;hb=b4b0c7e72d7eaee2fbfc7022022c9698323203dd;hp=0000000000000000000000000000000000000000;hpb=2dfda73eeb3eae2d4f894099754794ef07d060dd;p=freeside.git diff --git a/rt/sbin/rt-email-dashboards b/rt/sbin/rt-email-dashboards new file mode 100755 index 000000000..d46e0fe20 --- /dev/null +++ b/rt/sbin/rt-email-dashboards @@ -0,0 +1,568 @@ +#!/usr/bin/perl +# BEGIN BPS TAGGED BLOCK {{{ +# +# COPYRIGHT: +# +# This software is Copyright (c) 1996-2009 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 }}} +use strict; +use warnings; + +# fix lib paths, some may be relative +BEGIN { + require File::Spec; + my @libs = ("lib", "local/lib"); + my $bin_path; + + for my $lib (@libs) { + unless ( File::Spec->file_name_is_absolute($lib) ) { + unless ($bin_path) { + if ( File::Spec->file_name_is_absolute(__FILE__) ) { + $bin_path = ( File::Spec->splitpath(__FILE__) )[1]; + } + else { + require FindBin; + no warnings "once"; + $bin_path = $FindBin::Bin; + } + } + $lib = File::Spec->catfile( $bin_path, File::Spec->updir, $lib ); + } + unshift @INC, $lib; + } + +} + +use RT; +use RT::Interface::Web; +use RT::Interface::Web::Handler; +use RT::Dashboard; +use RT::Interface::CLI qw{ CleanEnv loc }; + +use Getopt::Long; +use HTML::Mason; +use HTML::RewriteAttributes::Resources; +use HTML::RewriteAttributes::Links; +use MIME::Types; +use POSIX 'tzset'; +use File::Temp 'tempdir'; + +# Clean out all the nasties from the environment +CleanEnv(); + +# Load the config file +RT::LoadConfig(); + +# Connect to the database and get RT::SystemUser and RT::Nobody loaded +RT::Init(); + +$HTML::Mason::Commands::r = RT::Dashboard::FakeRequest->new; + +no warnings 'once'; + +# Read in the options +my %opts; +GetOptions( \%opts, + "help", "dryrun", "verbose", "debug", "epoch=i", "all", "skip-acl" +); + +if ($opts{'help'}) { + require Pod::Usage; + import Pod::Usage; + pod2usage(-message => "RT Email Dashboards\n", -verbose => 1); + exit 1; +} + +# helper functions +sub verbose { print loc(@_), "\n" if $opts{debug} || $opts{verbose}; 1 } +sub debug { print loc(@_), "\n" if $opts{debug}; 1 } +sub error { $RT::Logger->error(loc(@_)); verbose(@_); 1 } +sub warning { $RT::Logger->warning(loc(@_)); verbose(@_); 1 } + +my $now = $opts{epoch} || time; +verbose "Using time [_1]", scalar localtime($now); + +my $from = get_from(); +debug "Sending email from [_1]", $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) { + debug "Skipping over " + . $user->Name + . " due to having a disabled account."; + next; + } + + my ($hour, $dow, $dom) = hour_dow_dom_in($user->Timezone || RT->Config->Get('Timezone')); + $hour .= ':00'; + debug "Checking [_1]'s subscriptions: hour [_2], dow [_3], dom [_4]", + $user->Name, $hour, $dow, $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')) { + my $counter = $subscription->SubValue('Counter') || 0; + + if (!$opts{all}) { + debug "Checking against subscription with frequency [_1], hour [_2], dow [_3], dom [_4]", + $subscription->SubValue('Frequency'), $subscription->SubValue('Hour'), + $subscription->SubValue('Dow'), $subscription->SubValue('Dom'); + + next if $subscription->SubValue('Frequency') eq 'never'; + + # correct hour? + next if $subscription->SubValue('Hour') ne $hour; + + # if weekly, correct day of week? + if ( $subscription->SubValue('Frequency') eq 'weekly' ) { + next if $subscription->SubValue('Dow') ne $dow; + my $fow = $subscription->SubValue('Fow') || 1; + if ( $counter % $fow ) { + $subscription->SetSubValues( Counter => $counter + 1 ) + unless $opts{'dryrun'}; + next; + } + } + + # if monthly, correct day of month? + elsif ($subscription->SubValue('Frequency') eq 'monthly') { + next if $subscription->SubValue('Dom') != $dom; + } + + elsif ($subscription->SubValue('Frequency') eq 'm-f') { + next if $dow eq 'Sunday' || $dow eq 'Saturday'; + } + } + + my $email = $subscription->SubValue('Recipient') + || $user->EmailAddress; + + eval { send_dashboard($currentuser, $email, $subscription) }; + if ( $@ ) { + error 'Caught exception: ' . $@; + } + else { + $subscription->SetSubValues( + Counter => $counter + 1 ) + unless $opts{'dryrun'}; + } + } +} + +sub send_dashboard { + my ($currentuser, $email, $subscription) = @_; + + my $rows = $subscription->SubValue('Rows'); + + my $dashboard = RT::Dashboard->new($currentuser); + + my ($ok, $msg) = $dashboard->LoadById($subscription->SubValue('DashboardId')); + + # failed to load dashboard. perhaps it was deleted or it changed privacy + if (!$ok) { + warning "Unable to load dashboard [_1] of subscription [_2] for user [_3]: [_4]", + $subscription->SubValue('DashboardId'), + $subscription->Id, + $currentuser->Name, + $msg; + + my $ok = RT::Interface::Email::SendEmailUsingTemplate( + From => $from, + To => $email, + Template => 'Error: Missing dashboard', + Arguments => { + SubscriptionObj => $subscription, + }, + ); + + # only delete the subscription if the email looks like it went through + if ($ok) { + my ($deleted, $msg) = $subscription->Delete(); + if ($deleted) { + verbose("Deleted an obsolete subscription: [_1]", $msg); + } + else { + warning("Unable to delete an obsolete subscription: [_1]", $msg); + } + } + else { + warning("Unable to notify [_1] of an obsolete subscription", $currentuser->Name); + } + + return; + } + + verbose 'Creating dashboard "[_1]" for user "[_2]":', + $dashboard->Name, + $currentuser->Name; + + if ($opts{'dryrun'}) { + print << "SUMMARY"; + Dashboard: @{[ $dashboard->Name ]} + User: @{[ $currentuser->Name ]} <$email> +SUMMARY + return; + } + + $HTML::Mason::Commands::session{CurrentUser} = $currentuser; + my $contents = run_component( + '/Dashboards/Render.html', + id => $dashboard->Id, + Preview => 0, + ); + + for (@{ RT->Config->Get('EmailDashboardRemove') || [] }) { + $contents =~ s/$_//g; + } + + debug "Got [_1] characters of output.", length $contents; + + $contents = HTML::RewriteAttributes::Links->rewrite( + $contents, + RT->Config->Get('WebURL') . '/Dashboards/Render.html', + ); + + email_dashboard($currentuser, $email, $dashboard, $subscription, $contents); +} + +sub email_dashboard { + my ($currentuser, $email, $dashboard, $subscription, $content) = @_; + + verbose 'Sending dashboard "[_1]" to user [_2] <[_3]>', + $dashboard->Name, + $currentuser->Name, + $email; + + my $subject = sprintf '[%s] ' . RT->Config->Get('DashboardSubject'), + RT->Config->Get('rtname'), + ucfirst($subscription->SubValue('Frequency')), + $dashboard->Name; + + my $entity = build_email($content, $from, $email, $subject); + + my $ok = RT::Interface::Email::SendEmail( + Entity => $entity, + ); + + debug "Done sending dashboard to [_1] <[_2]>", + $currentuser->Name, $email + and return if $ok; + + error 'Failed to email dashboard to user [_1] <[_2]>', + $currentuser->Name, $email; +} + +sub build_email { + my ($content, $from, $to, $subject) = @_; + my @parts; + my %cid_of; + + $content = HTML::RewriteAttributes::Resources->rewrite($content, sub { + my $uri = shift; + + # already attached this object + return "cid:$cid_of{$uri}" if $cid_of{$uri}; + + $cid_of{$uri} = time() . $$ . int(rand(1e6)); + my ($data, $filename, $mimetype, $encoding) = get_resource($uri); + + # downgrade non-text strings, because all strings are utf8 by + # default, which is wrong for non-text strings. + if ( $mimetype !~ m{text/} ) { + utf8::downgrade( $data, 1 ) or warning "downgrade $data failed"; + } + + push @parts, MIME::Entity->build( + Top => 0, + Data => $data, + Type => $mimetype, + Encoding => $encoding, + Disposition => 'inline', + Name => $filename, + 'Content-Id' => $cid_of{$uri}, + ); + + return "cid:$cid_of{$uri}"; + }, + inline_css => sub { + my $uri = shift; + my ($content) = get_resource($uri); + return $content; + }, + inline_imports => 1, + ); + + my $entity = MIME::Entity->build( + From => $from, + To => $to, + Subject => $subject, + Type => "multipart/mixed", + ); + + $entity->attach( + Data => Encode::encode_utf8($content), + Type => 'text/html', + Charset => 'UTF-8', + Disposition => 'inline', + ); + + for my $part (@parts) { + $entity->add_part($part); + } + + return $entity; +} + +sub get_from { + RT->Config->Get('DashboardAddress') || RT->Config->Get('OwnerEmail') +} + +{ + my $mason; + my $outbuf = ''; + my $data_dir = ''; + + sub mason { + unless ($mason) { + 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, + ); + } + return $mason; + } + + sub run_component { + mason->exec(@_); + my $ret = $outbuf; + $outbuf = ''; + return $ret; + } +} + +{ + my %cache; + + sub hour_dow_dom_in { + my $tz = shift; + return @{$cache{$tz}} if exists $cache{$tz}; + + my ($hour, $dow, $dom); + + { + local $ENV{'TZ'} = $tz; + ## Using POSIX::tzset fixes a bug where the TZ environment variable + ## is cached. + tzset(); + (undef, undef, $hour, $dom, undef, undef, $dow) = localtime($now); + } + tzset(); # return back previous value + + $hour = "0$hour" + if length($hour) == 1; + $dow = (qw/Sunday Monday Tuesday Wednesday Thursday Friday Saturday/)[$dow]; + + return @{$cache{$tz}} = ($hour, $dow, $dom); + } +} + +sub get_resource { + my $uri = URI->new(shift); + my ($content, $filename, $mimetype, $encoding); + + verbose "Getting resource [_1]", $uri; + + # strip out the equivalent of WebURL, so we start at the correct / + my $path = $uri->path; + my $webpath = RT->Config->Get('WebPath'); + $path =~ s/^\Q$webpath//; + + # add a leading / if needed + $path = "/$path" + unless $path =~ m{^/}; + + # grab the query arguments + my %args; + for (split /&/, ($uri->query||'')) { + my ($k, $v) = /^(.*?)=(.*)$/ + or die "Unable to parse query parameter '$_'"; + + for ($k, $v) { s/%(..)/chr hex $1/ge } + + # no value yet, simple key=value + if (!exists $args{$k}) { + $args{$k} = $v; + } + # already have key=value, need to upgrade it to key=[value1, value2] + elsif (!ref($args{$k})) { + $args{$k} = [$args{$k}, $v]; + } + # already key=[value1, value2], just add the new value + else { + push @{ $args{$k} }, $v; + } + } + + debug "Running component '[_1]'", $path; + $content = run_component($path, %args); + + # guess at the filename from the component name + $filename = $1 if $path =~ m{^.*/(.*?)$}; + + # the rest of this was taken from Email::MIME::CreateHTML::Resolver::LWP + ($mimetype, $encoding) = MIME::Types::by_suffix($filename); + + my $content_type = $HTML::Mason::Commands::r->content_type; + if ($content_type) { + $mimetype = $content_type; + + # strip down to just a MIME type + $mimetype = $1 if $mimetype =~ /(\S+);\s*charset=(.*)$/; + } + + #If all else fails then some conservative and general-purpose defaults are: + $mimetype ||= 'application/octet-stream'; + $encoding ||= 'base64'; + + debug "Resource [_1]: length=[_2] filename='[_3]' mimetype='[_4]', encoding='[_5]'", + $uri, + length($content), + $filename, + $mimetype, + $encoding; + + return ($content, $filename, $mimetype, $encoding); +} + +package RT::Dashboard::FakeRequest; +sub new { bless {}, shift } +sub header_out { shift } +sub headers_out { shift } +sub content_type { + my $self = shift; + $self->{content_type} = shift if @_; + return $self->{content_type}; +} + +=head1 NAME + +rt-email-dashboards - Send email dashboards + +=head1 SYNOPSIS + + /opt/rt3/local/sbin/rt-email-dashboards [options] + +=head1 DESCRIPTION + +This tool will send users email based on how they have subscribed to +dashboards. A dashboard is a set of saved searches, the subscription controls +how often that dashboard is sent and how it's displayed. + +Each subscription has an hour, and possibly day of week or day of month. These +are taken to be in the user's timezone if available, UTC otherwise. + +=head1 SETUP + +You'll need to have cron run this script every hour. Here's an example crontab +entry to do this. + + 0 * * * * /usr/bin/perl /opt/rt3/local/sbin/rt-email-dashboards + +This will run the script every hour on the hour. This may need some further +tweaking to be run as the correct user. + +=head1 OPTIONS + +This tool supports a few options. Most are for debugging. + +=over 8 + +=item --help + +Display this documentation + +=item --dryrun + +Figure out which dashboards would be sent, but don't actually generate them + +=item --epoch SECONDS + +Instead of using the current time to figure out which dashboards should be +sent, use SECONDS (usually since midnight Jan 1st, 1970, so C<1192216018> would +be Oct 12 19:06:58 GMT 2007). + +=item --verbose + +Print out some tracing information (such as which dashboards are being +generated and sent out) + +=item --debug + +Print out more tracing information (such as each user and subscription that is +being considered) + +=item --all + +Ignore subscription frequency when considering each dashboard (should only be +used with --dryrun) + +=back + +=cut +