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