Master to 4.2.8
[usit-rt.git] / sbin / rt-email-digest
CommitLineData
84fb5b46
MKG
1#!/usr/bin/perl
2# BEGIN BPS TAGGED BLOCK {{{
3#
4# COPYRIGHT:
5#
320f0092 6# This software is Copyright (c) 1996-2014 Best Practical Solutions, LLC
84fb5b46
MKG
7# <sales@bestpractical.com>
8#
9# (Except where explicitly superseded by other copyright notices)
10#
11#
12# LICENSE:
13#
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
17# from www.gnu.org.
18#
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.
23#
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.
29#
30#
31# CONTRIBUTION SUBMISSION POLICY:
32#
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.)
38#
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.
47#
48# END BPS TAGGED BLOCK }}}
49use warnings;
50use strict;
51
af59614d 52BEGIN { # BEGIN RT CMD BOILERPLATE
84fb5b46 53 require File::Spec;
af59614d 54 require Cwd;
84fb5b46
MKG
55 my @libs = ("lib", "local/lib");
56 my $bin_path;
57
58 for my $lib (@libs) {
59 unless ( File::Spec->file_name_is_absolute($lib) ) {
af59614d 60 $bin_path ||= ( File::Spec->splitpath(Cwd::abs_path(__FILE__)) )[1];
84fb5b46
MKG
61 $lib = File::Spec->catfile( $bin_path, File::Spec->updir, $lib );
62 }
63 unshift @INC, $lib;
64 }
65
66}
67
68use Date::Format qw( strftime );
69use Getopt::Long;
70use RT;
71use RT::Interface::CLI qw( CleanEnv loc );
72use RT::Interface::Email;
73
84fb5b46
MKG
74RT::LoadConfig();
75RT::Init();
af59614d 76CleanEnv();
84fb5b46
MKG
77
78sub usage {
79 my ($error) = @_;
80 print loc("Usage:") . " $0 -m (daily|weekly) [--print] [--help]\n";
81 print loc(
82 "[_1] is a utility, meant to be run from cron, that dispatches all deferred RT notifications as a per-user digest.",
83 $0
84 ) . "\n";
85 print "\n\t-m, --mode\t"
86 . loc("Specify whether this is a daily or weekly run.") . "\n";
87 print "\t-p, --print\t"
88 . loc("Print the resulting digest messages to STDOUT; don't mail them. Do not mark them as sent")
89 . "\n";
01e3b242 90 print "\t-v, --verbose\t" . loc("Give output even on messages successfully sent") . "\n";
84fb5b46
MKG
91 print "\t-h, --help\t" . loc("Print this message") . "\n";
92
93 if ( $error eq 'help' ) {
94 exit 0;
95 } else {
96 print loc("Error") . ": " . loc($error) . "\n";
97 exit 1;
98 }
99}
100
01e3b242 101my ( $frequency, $print, $verbose, $help ) = ( '', '', '', '' );
84fb5b46
MKG
102GetOptions(
103 'mode=s' => \$frequency,
104 'print' => \$print,
01e3b242 105 'verbose' => \$verbose,
84fb5b46
MKG
106 'help' => \$help,
107);
108
109usage('help') if $help;
110usage("Mode argument must be 'daily' or 'weekly'")
111 unless $frequency =~ /^(daily|weekly)$/;
112
113run( $frequency, $print );
114
115sub run {
116 my $frequency = shift;
117 my $print = shift;
118
119## Find all the tickets that have been modified within the time frame
120## described by $frequency.
121
122 my ( $all_digest, $sent_transactions ) = find_transactions($frequency);
123
124## Iterate through our huge hash constructing the digest message
125## for each user and sending it.
126
127 foreach my $user ( keys %$all_digest ) {
128 my ( $contents_list, $contents_body ) = build_digest_for_user( $user, $all_digest->{$user} );
129 # Now we have a content head and a content body. We can send a message.
130 if ( send_digest( $user, $contents_list, $contents_body ) ) {
01e3b242 131 print "Sent message to $user\n" if $verbose;
84fb5b46
MKG
132 mark_transactions_sent( $frequency, $user, values %{$sent_transactions->{$user}} ) unless ($print);
133 } else {
134 print "Failed to send message to $user\n";
135 }
136 }
137}
138exit 0;
139
140# Subroutines.
141
142sub send_digest {
143 my ( $to, $index, $messages ) = @_;
144
145 # Combine the index and the messages.
146
147 my $body = "============== Tickets with activity in the last "
148 . ( $frequency eq 'daily' ? "day" : "seven days" ) . "\n\n";
149
150 $body .= $index;
151 $body .= "\n\n============== Messages recorded in the last "
152 . ( $frequency eq 'daily' ? "day" : "seven days" ) . "\n\n";
153 $body .= $messages;
154
155 # Load our template. If we cannot load the template, abort
156 # immediately rather than failing through many loops.
157 my $digest_template = RT::Template->new( RT->SystemUser );
158 my ( $ret, $msg ) = $digest_template->Load('Email Digest');
159 unless ($ret) {
160 print loc("Failed to load template")
161 . " 'Email Digest': "
162 . $msg
163 . ". Cannot continue.\n";
164 exit 1;
165 }
166 ( $ret, $msg ) = $digest_template->Parse( Argument => $body );
167 unless ($ret) {
168 print loc("Failed to parse template")
169 . " 'Email Digest'. Cannot continue.\n";
170 exit 1;
171 }
172
173 # Set our sender and recipient.
c33a4027
MKG
174 $digest_template->MIMEObj->head->replace(
175 'From', Encode::encode( "UTF-8", RT::Config->Get('CorrespondAddress') ) );
176 $digest_template->MIMEObj->head->replace(
177 'To', Encode::encode( "UTF-8", $to ) );
84fb5b46
MKG
178
179 if ($print) {
180 $digest_template->MIMEObj->print;
181 return 1;
182 } else {
183 return RT::Interface::Email::SendEmail( Entity => $digest_template->MIMEObj)
184 }
185}
186
187# =item mark_transactions_sent( $frequency, $user, @txn_list );
188#
189# Takes a frequency string (either 'daily' or 'weekly'), a user and one or more
190# transaction objects as its arguments. Marks the given deferred
191# notifications as sent.
192#
193# =cut
194
195sub mark_transactions_sent {
196 my ( $freq, $user, @txns ) = @_;
197 return unless $freq =~ /(daily|weekly)/;
198 return unless @txns;
199 foreach my $txn (@txns) {
200
201 # Grab the attribute, mark the "sent" as true, and store the new
202 # value.
203 if ( my $attr = $txn->FirstAttribute('DeferredRecipients') ) {
204 my $deferred = $attr->Content;
205 $deferred->{$freq}->{$user}->{'_sent'} = 1;
206 $txn->SetAttribute(
207 Name => 'DeferredRecipients',
208 Description => 'Deferred recipients for this message',
209 Content => $deferred,
210 );
211 }
212 }
213}
214
215sub since_date {
216 my $frequency = shift;
217
218 # Specify a short time for digest overlap, in case we aren't starting
219 # this process exactly on time.
220 my $OVERLAP_HEDGE = -30;
221
222 my $since_date = RT::Date->new( RT->SystemUser );
223 $since_date->Set( Format => 'unix', Value => time() );
224 if ( $frequency eq 'daily' ) {
225 $since_date->AddDays(-1);
226 } else {
227 $since_date->AddDays(-7);
228 }
229
230 $since_date->AddSeconds($OVERLAP_HEDGE);
231
232 return $since_date;
233}
234
235sub find_transactions {
236 my $frequency = shift;
237 my $since_date = since_date($frequency);
238
239 my $txns = RT::Transactions->new( RT->SystemUser );
240
241 # First limit to recent transactions.
242 $txns->Limit(
243 FIELD => 'Created',
244 OPERATOR => '>',
245 VALUE => $since_date->ISO
246 );
247
248 # Next limit to ticket transactions.
249 $txns->Limit(
250 FIELD => 'ObjectType',
251 OPERATOR => '=',
252 VALUE => 'RT::Ticket',
253 ENTRYAGGREGATOR => 'AND'
254 );
255 my $all_digest = {};
256 my $sent_transactions = {};
257
258 while ( my $txn = $txns->Next ) {
259 my $ticket = $txn->Ticket;
260 my $queue = $txn->TicketObj->QueueObj->Name;
261 # Xxx todo - may clobber if two queues have the same name
262 foreach my $user ( $txn->DeferredRecipients($frequency) ) {
af59614d 263 $all_digest->{$user}->{$queue}->{$ticket}->{ $txn->id } = $txn;
84fb5b46
MKG
264 $sent_transactions->{$user}->{ $txn->id } = $txn;
265 }
266 }
267
268 return ( $all_digest, $sent_transactions );
269}
270
271sub build_digest_for_user {
272 my $user = shift;
273 my $user_digest = shift;
274
275 my $contents_list = ''; # Holds the digest index.
276 my $contents_body = ''; # Holds the digest body.
277
278 # Has the user been disabled since a message was deferred on his/her
279 # behalf?
280 my $user_obj = RT::User->new( RT->SystemUser );
281 $user_obj->LoadByEmail($user);
282 if ( $user_obj->PrincipalObj->Disabled ) {
283 print STDERR loc("Skipping disabled user") . " $user\n";
284 next;
285 }
286
287 print loc("Message for user") . " $user:\n\n" if $print;
288 foreach my $queue ( keys %$user_digest ) {
289 $contents_list .= "Queue $queue:\n";
290 $contents_body .= "Queue $queue:\n";
291 foreach my $ticket ( sort keys %{ $user_digest->{$queue} } ) {
292 my $tkt_txns = $user_digest->{$queue}->{$ticket};
293 my $ticket_obj = RT::Ticket->new( RT->SystemUser );
294 $ticket_obj->Load($ticket);
295
296 # Spit out the index entry for this ticket.
297 my $ticket_title = sprintf(
298 "#%d %s [%s]\t%s\n",
299 $ticket, $ticket_obj->Status, $ticket_obj->OwnerObj->Name,
300 $ticket_obj->Subject
301 );
302 $contents_list .= $ticket_title;
303
304 # Spit out the messages for the transactions on this ticket.
305 $contents_body .= "\n== $ticket_title\n";
306 foreach my $txn ( sort keys %$tkt_txns ) {
af59614d
MKG
307 my $top = $tkt_txns->{$txn}->Attachments->First;
308
309 # $top contains the top-most RT::Attachment with our
310 # outgoing message. It may not be the MIME part with
311 # the content. Print a few headers from it for
312 # clarity's sake.
313 $contents_body .= "From: " . $top->GetHeader('From') . "\n";
314 my $date = $top->GetHeader('Date ');
84fb5b46
MKG
315 unless ($date) {
316 my $txn_obj = RT::Transaction->new( RT->SystemUser );
317 $txn_obj->Load($txn);
318 my $date_obj = RT::Date->new( RT->SystemUser );
319 $date_obj->Set(
320 Format => 'sql',
321 Value => $txn_obj->Created
322 );
323 $date = strftime( '%a, %d %b %Y %H:%M:%S %z',
324 @{ [ localtime( $date_obj->Unix ) ] } );
325 }
326 $contents_body .= "Date: $date\n\n";
af59614d 327 $contents_body .= $tkt_txns->{$txn}->ContentObj->Content . "\n";
84fb5b46
MKG
328 $contents_body .= "-------\n";
329 } # foreach transaction
330 } # foreach ticket
331 } # foreach queue
332
333 return ( $contents_list, $contents_body );
334
335}
336
337__END__
338
339=head1 NAME
340
341rt-email-digest - dispatch deferred notifications as a per-user digest
342
343=head1 SYNOPSIS
344
345 rt-email-digest -m (daily|weekly) [--print] [--help]
346
347=head1 DESCRIPTION
348
349This script is a tool to dispatch all deferred RT notifications as a per-user
350object.
351
352=head1 OPTIONS
353
354=over
355
356=item mode
357
358Specify whether this is a daily or weekly run.
359
360--mode is equal to -m
361
362=item print
363
364Print the resulting digest messages to STDOUT; don't mail them. Do not mark them as sent
365
366--print is equal to -p
367
368=item help
369
370Print this message
371
372--help is equal to -h
373
374=back