2 # BEGIN BPS TAGGED BLOCK {{{
6 # This software is Copyright (c) 1996-2015 Best Practical Solutions, LLC
7 # <sales@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 }}}
54 my @libs = ("@RT_LIB_PATH@", "@LOCAL_LIB_PATH@");
58 unless ( File::Spec->file_name_is_absolute($lib) ) {
60 if ( File::Spec->file_name_is_absolute(__FILE__) ) {
61 $bin_path = ( File::Spec->splitpath(__FILE__) )[1];
66 $bin_path = $FindBin::Bin;
69 $lib = File::Spec->catfile( $bin_path, File::Spec->updir, $lib );
76 use Date::Format qw( strftime );
79 use RT::Interface::CLI qw( CleanEnv loc );
80 use RT::Interface::Email;
88 print loc("Usage:") . " $0 -m (daily|weekly) [--print] [--help]\n";
90 "[_1] is a utility, meant to be run from cron, that dispatches all deferred RT notifications as a per-user digest.",
93 print "\n\t-m, --mode\t"
94 . loc("Specify whether this is a daily or weekly run.") . "\n";
95 print "\t-p, --print\t"
96 . loc("Print the resulting digest messages to STDOUT; don't mail them. Do not mark them as sent")
98 print "\t-v, --verbose\t" . loc("Give output even on messages successfully sent") . "\n";
99 print "\t-h, --help\t" . loc("Print this message") . "\n";
101 if ( $error eq 'help' ) {
104 print loc("Error") . ": " . loc($error) . "\n";
109 my ( $frequency, $print, $verbose, $help ) = ( '', '', '', '' );
111 'mode=s' => \$frequency,
113 'verbose' => \$verbose,
117 usage('help') if $help;
118 usage("Mode argument must be 'daily' or 'weekly'")
119 unless $frequency =~ /^(daily|weekly)$/;
121 run( $frequency, $print );
124 my $frequency = shift;
127 ## Find all the tickets that have been modified within the time frame
128 ## described by $frequency.
130 my ( $all_digest, $sent_transactions ) = find_transactions($frequency);
132 ## Iterate through our huge hash constructing the digest message
133 ## for each user and sending it.
135 foreach my $user ( keys %$all_digest ) {
136 my ( $contents_list, $contents_body ) = build_digest_for_user( $user, $all_digest->{$user} );
137 # Now we have a content head and a content body. We can send a message.
138 if ( send_digest( $user, $contents_list, $contents_body ) ) {
139 print "Sent message to $user\n" if $verbose;
140 mark_transactions_sent( $frequency, $user, values %{$sent_transactions->{$user}} ) unless ($print);
142 print "Failed to send message to $user\n";
151 my ( $to, $index, $messages ) = @_;
153 # Combine the index and the messages.
155 my $body = "============== Tickets with activity in the last "
156 . ( $frequency eq 'daily' ? "day" : "seven days" ) . "\n\n";
159 $body .= "\n\n============== Messages recorded in the last "
160 . ( $frequency eq 'daily' ? "day" : "seven days" ) . "\n\n";
163 # Load our template. If we cannot load the template, abort
164 # immediately rather than failing through many loops.
165 my $digest_template = RT::Template->new( RT->SystemUser );
166 my ( $ret, $msg ) = $digest_template->Load('Email Digest');
168 print loc("Failed to load template")
169 . " 'Email Digest': "
171 . ". Cannot continue.\n";
174 ( $ret, $msg ) = $digest_template->Parse( Argument => $body );
176 print loc("Failed to parse template")
177 . " 'Email Digest'. Cannot continue.\n";
181 # Set our sender and recipient.
182 $digest_template->MIMEObj->head->replace(
183 'From', Encode::encode( "UTF-8", RT::Config->Get('CorrespondAddress') ) );
184 $digest_template->MIMEObj->head->replace(
185 'To', Encode::encode( "UTF-8", $to ) );
188 $digest_template->MIMEObj->print;
191 return RT::Interface::Email::SendEmail( Entity => $digest_template->MIMEObj)
195 # =item mark_transactions_sent( $frequency, $user, @txn_list );
197 # Takes a frequency string (either 'daily' or 'weekly'), a user and one or more
198 # transaction objects as its arguments. Marks the given deferred
199 # notifications as sent.
203 sub mark_transactions_sent {
204 my ( $freq, $user, @txns ) = @_;
205 return unless $freq =~ /(daily|weekly)/;
207 foreach my $txn (@txns) {
209 # Grab the attribute, mark the "sent" as true, and store the new
211 if ( my $attr = $txn->FirstAttribute('DeferredRecipients') ) {
212 my $deferred = $attr->Content;
213 $deferred->{$freq}->{$user}->{'_sent'} = 1;
215 Name => 'DeferredRecipients',
216 Description => 'Deferred recipients for this message',
217 Content => $deferred,
224 my $frequency = shift;
226 # Specify a short time for digest overlap, in case we aren't starting
227 # this process exactly on time.
228 my $OVERLAP_HEDGE = -30;
230 my $since_date = RT::Date->new( RT->SystemUser );
231 $since_date->Set( Format => 'unix', Value => time() );
232 if ( $frequency eq 'daily' ) {
233 $since_date->AddDays(-1);
235 $since_date->AddDays(-7);
238 $since_date->AddSeconds($OVERLAP_HEDGE);
243 sub find_transactions {
244 my $frequency = shift;
245 my $since_date = since_date($frequency);
247 my $txns = RT::Transactions->new( RT->SystemUser );
249 # First limit to recent transactions.
253 VALUE => $since_date->ISO
256 # Next limit to ticket transactions.
258 FIELD => 'ObjectType',
260 VALUE => 'RT::Ticket',
261 ENTRYAGGREGATOR => 'AND'
264 my $sent_transactions = {};
266 while ( my $txn = $txns->Next ) {
267 my $ticket = $txn->Ticket;
268 my $queue = $txn->TicketObj->QueueObj->Name;
269 # Xxx todo - may clobber if two queues have the same name
270 foreach my $user ( $txn->DeferredRecipients($frequency) ) {
271 $all_digest->{$user}->{$queue}->{$ticket}->{ $txn->id } = $txn->ContentObj;
272 $sent_transactions->{$user}->{ $txn->id } = $txn;
276 return ( $all_digest, $sent_transactions );
279 sub build_digest_for_user {
281 my $user_digest = shift;
283 my $contents_list = ''; # Holds the digest index.
284 my $contents_body = ''; # Holds the digest body.
286 # Has the user been disabled since a message was deferred on his/her
288 my $user_obj = RT::User->new( RT->SystemUser );
289 $user_obj->LoadByEmail($user);
290 if ( $user_obj->PrincipalObj->Disabled ) {
291 print STDERR loc("Skipping disabled user") . " $user\n";
295 print loc("Message for user") . " $user:\n\n" if $print;
296 foreach my $queue ( keys %$user_digest ) {
297 $contents_list .= "Queue $queue:\n";
298 $contents_body .= "Queue $queue:\n";
299 foreach my $ticket ( sort keys %{ $user_digest->{$queue} } ) {
300 my $tkt_txns = $user_digest->{$queue}->{$ticket};
301 my $ticket_obj = RT::Ticket->new( RT->SystemUser );
302 $ticket_obj->Load($ticket);
304 # Spit out the index entry for this ticket.
305 my $ticket_title = sprintf(
307 $ticket, $ticket_obj->Status, $ticket_obj->OwnerObj->Name,
310 $contents_list .= $ticket_title;
312 # Spit out the messages for the transactions on this ticket.
313 $contents_body .= "\n== $ticket_title\n";
314 foreach my $txn ( sort keys %$tkt_txns ) {
315 my $msg = $tkt_txns->{$txn};
317 # $msg contains an RT::Attachment with our outgoing
318 # message. Print a few headers for clarity's sake.
319 $contents_body .= "From: " . $msg->GetHeader('From') . "\n";
320 my $date = $msg->GetHeader('Date ');
322 my $txn_obj = RT::Transaction->new( RT->SystemUser );
323 $txn_obj->Load($txn);
324 my $date_obj = RT::Date->new( RT->SystemUser );
327 Value => $txn_obj->Created
329 $date = strftime( '%a, %d %b %Y %H:%M:%S %z',
330 @{ [ localtime( $date_obj->Unix ) ] } );
332 $contents_body .= "Date: $date\n\n";
333 $contents_body .= $msg->Content . "\n";
334 $contents_body .= "-------\n";
335 } # foreach transaction
339 return ( $contents_list, $contents_body );
347 rt-email-digest - dispatch deferred notifications as a per-user digest
351 rt-email-digest -m (daily|weekly) [--print] [--help]
355 This script is a tool to dispatch all deferred RT notifications as a per-user
364 Specify whether this is a daily or weekly run.
366 --mode is equal to -m
370 Print the resulting digest messages to STDOUT; don't mail them. Do not mark them as sent
372 --print is equal to -p
378 --help is equal to -h