2 # BEGIN BPS TAGGED BLOCK {{{
6 # This software is Copyright (c) 1996-2009 Best Practical Solutions, LLC
7 # <jesse@bestpractical.com>
9 # (Except where explicitly superseded by other copyright notices)
14 # This work is made available to you under the terms of Version 2 of
15 # the GNU General Public License. A copy of that license should have
16 # been provided with this software, but in any event can be snarfed
19 # This work is distributed in the hope that it will be useful, but
20 # WITHOUT ANY WARRANTY; without even the implied warranty of
21 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
22 # General Public License for more details.
24 # You should have received a copy of the GNU General Public License
25 # along with this program; if not, write to the Free Software
26 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
27 # 02110-1301 or visit their web page on the internet at
28 # http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
31 # CONTRIBUTION SUBMISSION POLICY:
33 # (The following paragraph is not intended to limit the rights granted
34 # to you to modify and distribute this software under the terms of
35 # the GNU General Public License and is only of importance to you if
36 # you choose to contribute your changes and enhancements to the
37 # community by submitting them to Best Practical Solutions, LLC.)
39 # By intentionally submitting any modifications, corrections or
40 # derivatives to this work, or any other work intended for use with
41 # Request Tracker, to Best Practical Solutions, LLC, you confirm that
42 # you are the copyright holder for those contributions and you grant
43 # Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
44 # royalty-free, perpetual, license to use, copy, create derivative
45 # works based on those contributions, and sublicense and distribute
46 # those contributions and any derivatives thereof.
48 # END BPS TAGGED BLOCK }}}
52 # fix lib paths, some may be relative
55 my @libs = ("@RT_LIB_PATH@", "@LOCAL_LIB_PATH@");
59 unless ( File::Spec->file_name_is_absolute($lib) ) {
61 if ( File::Spec->file_name_is_absolute(__FILE__) ) {
62 $bin_path = ( File::Spec->splitpath(__FILE__) )[1];
67 $bin_path = $FindBin::Bin;
70 $lib = File::Spec->catfile( $bin_path, File::Spec->updir, $lib );
78 use RT::Interface::Web;
79 use RT::Interface::Web::Handler;
81 use RT::Interface::CLI qw{ CleanEnv loc };
85 use HTML::RewriteAttributes::Resources;
86 use HTML::RewriteAttributes::Links;
89 use File::Temp 'tempdir';
91 # Clean out all the nasties from the environment
94 # Load the config file
97 # Connect to the database and get RT::SystemUser and RT::Nobody loaded
100 $HTML::Mason::Commands::r = RT::Dashboard::FakeRequest->new;
104 # Read in the options
107 "help", "dryrun", "verbose", "debug", "epoch=i", "all", "skip-acl"
113 pod2usage(-message => "RT Email Dashboards\n", -verbose => 1);
118 sub verbose { print loc(@_), "\n" if $opts{debug} || $opts{verbose}; 1 }
119 sub debug { print loc(@_), "\n" if $opts{debug}; 1 }
120 sub error { $RT::Logger->error(loc(@_)); verbose(@_); 1 }
121 sub warning { $RT::Logger->warning(loc(@_)); verbose(@_); 1 }
123 my $now = $opts{epoch} || time;
124 verbose "Using time [_1]", scalar localtime($now);
126 my $from = get_from();
127 debug "Sending email from [_1]", $from;
129 # look through each user for her subscriptions
130 my $Users = RT::Users->new($RT::SystemUser);
131 $Users->LimitToPrivileged;
133 while (defined(my $user = $Users->Next)) {
134 if ($user->PrincipalObj->Disabled) {
135 debug "Skipping over "
137 . " due to having a disabled account.";
141 my ($hour, $dow, $dom) = hour_dow_dom_in($user->Timezone || RT->Config->Get('Timezone'));
143 debug "Checking [_1]'s subscriptions: hour [_2], dow [_3], dom [_4]",
144 $user->Name, $hour, $dow, $dom;
146 my $currentuser = RT::CurrentUser->new;
147 $currentuser->LoadByName($user->Name);
149 # look through this user's subscriptions, are any supposed to be generated
151 for my $subscription ($user->Attributes->Named('Subscription')) {
152 my $counter = $subscription->SubValue('Counter') || 0;
155 debug "Checking against subscription with frequency [_1], hour [_2], dow [_3], dom [_4]",
156 $subscription->SubValue('Frequency'), $subscription->SubValue('Hour'),
157 $subscription->SubValue('Dow'), $subscription->SubValue('Dom');
159 next if $subscription->SubValue('Frequency') eq 'never';
162 next if $subscription->SubValue('Hour') ne $hour;
164 # if weekly, correct day of week?
165 if ( $subscription->SubValue('Frequency') eq 'weekly' ) {
166 next if $subscription->SubValue('Dow') ne $dow;
167 my $fow = $subscription->SubValue('Fow') || 1;
168 if ( $counter % $fow ) {
169 $subscription->SetSubValues( Counter => $counter + 1 )
170 unless $opts{'dryrun'};
175 # if monthly, correct day of month?
176 elsif ($subscription->SubValue('Frequency') eq 'monthly') {
177 next if $subscription->SubValue('Dom') != $dom;
180 elsif ($subscription->SubValue('Frequency') eq 'm-f') {
181 next if $dow eq 'Sunday' || $dow eq 'Saturday';
185 my $email = $subscription->SubValue('Recipient')
186 || $user->EmailAddress;
188 eval { send_dashboard($currentuser, $email, $subscription) };
190 error 'Caught exception: ' . $@;
193 $subscription->SetSubValues(
194 Counter => $counter + 1 )
195 unless $opts{'dryrun'};
201 my ($currentuser, $email, $subscription) = @_;
203 my $rows = $subscription->SubValue('Rows');
205 my $dashboard = RT::Dashboard->new($currentuser);
207 my ($ok, $msg) = $dashboard->LoadById($subscription->SubValue('DashboardId'));
209 # failed to load dashboard. perhaps it was deleted or it changed privacy
211 warning "Unable to load dashboard [_1] of subscription [_2] for user [_3]: [_4]",
212 $subscription->SubValue('DashboardId'),
217 my $ok = RT::Interface::Email::SendEmailUsingTemplate(
220 Template => 'Error: Missing dashboard',
222 SubscriptionObj => $subscription,
226 # only delete the subscription if the email looks like it went through
228 my ($deleted, $msg) = $subscription->Delete();
230 verbose("Deleted an obsolete subscription: [_1]", $msg);
233 warning("Unable to delete an obsolete subscription: [_1]", $msg);
237 warning("Unable to notify [_1] of an obsolete subscription", $currentuser->Name);
243 verbose 'Creating dashboard "[_1]" for user "[_2]":',
247 if ($opts{'dryrun'}) {
249 Dashboard: @{[ $dashboard->Name ]}
250 User: @{[ $currentuser->Name ]} <$email>
255 $HTML::Mason::Commands::session{CurrentUser} = $currentuser;
256 my $contents = run_component(
257 '/Dashboards/Render.html',
258 id => $dashboard->Id,
262 for (@{ RT->Config->Get('EmailDashboardRemove') || [] }) {
263 $contents =~ s/$_//g;
266 debug "Got [_1] characters of output.", length $contents;
268 $contents = HTML::RewriteAttributes::Links->rewrite(
270 RT->Config->Get('WebURL') . '/Dashboards/Render.html',
273 email_dashboard($currentuser, $email, $dashboard, $subscription, $contents);
276 sub email_dashboard {
277 my ($currentuser, $email, $dashboard, $subscription, $content) = @_;
279 verbose 'Sending dashboard "[_1]" to user [_2] <[_3]>',
284 my $subject = sprintf '[%s] ' . RT->Config->Get('DashboardSubject'),
285 RT->Config->Get('rtname'),
286 ucfirst($subscription->SubValue('Frequency')),
289 my $entity = build_email($content, $from, $email, $subject);
291 my $ok = RT::Interface::Email::SendEmail(
295 debug "Done sending dashboard to [_1] <[_2]>",
296 $currentuser->Name, $email
299 error 'Failed to email dashboard to user [_1] <[_2]>',
300 $currentuser->Name, $email;
304 my ($content, $from, $to, $subject) = @_;
308 $content = HTML::RewriteAttributes::Resources->rewrite($content, sub {
311 # already attached this object
312 return "cid:$cid_of{$uri}" if $cid_of{$uri};
314 $cid_of{$uri} = time() . $$ . int(rand(1e6));
315 my ($data, $filename, $mimetype, $encoding) = get_resource($uri);
317 # downgrade non-text strings, because all strings are utf8 by
318 # default, which is wrong for non-text strings.
319 if ( $mimetype !~ m{text/} ) {
320 utf8::downgrade( $data, 1 ) or warning "downgrade $data failed";
323 push @parts, MIME::Entity->build(
327 Encoding => $encoding,
328 Disposition => 'inline',
330 'Content-Id' => $cid_of{$uri},
333 return "cid:$cid_of{$uri}";
337 my ($content) = get_resource($uri);
343 my $entity = MIME::Entity->build(
347 Type => "multipart/mixed",
351 Data => Encode::encode_utf8($content),
354 Disposition => 'inline',
357 for my $part (@parts) {
358 $entity->add_part($part);
365 RT->Config->Get('DashboardAddress') || RT->Config->Get('OwnerEmail')
375 debug "Creating Mason object.";
377 # user may not have permissions on the data directory, so create a
379 $data_dir = tempdir(CLEANUP => 1);
381 $mason = HTML::Mason::Interp->new(
382 RT::Interface::Web::Handler->DefaultHandlerArgs,
383 out_method => \$outbuf,
384 autohandler_name => '', # disable forced login and more
385 data_dir => $data_dir,
402 sub hour_dow_dom_in {
404 return @{$cache{$tz}} if exists $cache{$tz};
406 my ($hour, $dow, $dom);
409 local $ENV{'TZ'} = $tz;
410 ## Using POSIX::tzset fixes a bug where the TZ environment variable
413 (undef, undef, $hour, $dom, undef, undef, $dow) = localtime($now);
415 tzset(); # return back previous value
418 if length($hour) == 1;
419 $dow = (qw/Sunday Monday Tuesday Wednesday Thursday Friday Saturday/)[$dow];
421 return @{$cache{$tz}} = ($hour, $dow, $dom);
426 my $uri = URI->new(shift);
427 my ($content, $filename, $mimetype, $encoding);
429 verbose "Getting resource [_1]", $uri;
431 # strip out the equivalent of WebURL, so we start at the correct /
432 my $path = $uri->path;
433 my $webpath = RT->Config->Get('WebPath');
434 $path =~ s/^\Q$webpath//;
436 # add a leading / if needed
438 unless $path =~ m{^/};
440 # grab the query arguments
442 for (split /&/, ($uri->query||'')) {
443 my ($k, $v) = /^(.*?)=(.*)$/
444 or die "Unable to parse query parameter '$_'";
446 for ($k, $v) { s/%(..)/chr hex $1/ge }
448 # no value yet, simple key=value
449 if (!exists $args{$k}) {
452 # already have key=value, need to upgrade it to key=[value1, value2]
453 elsif (!ref($args{$k})) {
454 $args{$k} = [$args{$k}, $v];
456 # already key=[value1, value2], just add the new value
458 push @{ $args{$k} }, $v;
462 debug "Running component '[_1]'", $path;
463 $content = run_component($path, %args);
465 # guess at the filename from the component name
466 $filename = $1 if $path =~ m{^.*/(.*?)$};
468 # the rest of this was taken from Email::MIME::CreateHTML::Resolver::LWP
469 ($mimetype, $encoding) = MIME::Types::by_suffix($filename);
471 my $content_type = $HTML::Mason::Commands::r->content_type;
473 $mimetype = $content_type;
475 # strip down to just a MIME type
476 $mimetype = $1 if $mimetype =~ /(\S+);\s*charset=(.*)$/;
479 #If all else fails then some conservative and general-purpose defaults are:
480 $mimetype ||= 'application/octet-stream';
481 $encoding ||= 'base64';
483 debug "Resource [_1]: length=[_2] filename='[_3]' mimetype='[_4]', encoding='[_5]'",
490 return ($content, $filename, $mimetype, $encoding);
493 package RT::Dashboard::FakeRequest;
494 sub new { bless {}, shift }
495 sub header_out { shift }
496 sub headers_out { shift }
499 $self->{content_type} = shift if @_;
500 return $self->{content_type};
505 rt-email-dashboards - Send email dashboards
509 /opt/rt3/local/sbin/rt-email-dashboards [options]
513 This tool will send users email based on how they have subscribed to
514 dashboards. A dashboard is a set of saved searches, the subscription controls
515 how often that dashboard is sent and how it's displayed.
517 Each subscription has an hour, and possibly day of week or day of month. These
518 are taken to be in the user's timezone if available, UTC otherwise.
522 You'll need to have cron run this script every hour. Here's an example crontab
525 0 * * * * @PERL@ /opt/rt3/local/sbin/rt-email-dashboards
527 This will run the script every hour on the hour. This may need some further
528 tweaking to be run as the correct user.
532 This tool supports a few options. Most are for debugging.
538 Display this documentation
542 Figure out which dashboards would be sent, but don't actually generate them
544 =item --epoch SECONDS
546 Instead of using the current time to figure out which dashboards should be
547 sent, use SECONDS (usually since midnight Jan 1st, 1970, so C<1192216018> would
548 be Oct 12 19:06:58 GMT 2007).
552 Print out some tracing information (such as which dashboards are being
553 generated and sent out)
557 Print out more tracing information (such as each user and subscription that is
562 Ignore subscription frequency when considering each dashboard (should only be