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