import torrus 1.0.9
[freeside.git] / FS / FS / msg_template.pm
index 958acef..f0cda41 100644 (file)
@@ -8,10 +8,12 @@ use FS::Conf;
 use FS::Record qw( qsearch qsearchs );
 
 use Date::Format qw( time2str );
-use HTML::Entities qw( encode_entities) ;
+use HTML::Entities qw( decode_entities encode_entities ) ;
+use HTML::FormatText;
+use HTML::TreeBuilder;
 use vars '$DEBUG';
 
-$DEBUG=1;
+$DEBUG=0;
 
 =head1 NAME
 
@@ -143,10 +145,6 @@ sub check {
   ;
   return $error if $error;
 
-  my $body = $self->body;
-  $body =~ s/ / /g; # just in case these somehow get in
-  $self->body($body);
-
   $self->mime_type('text/html') unless $self->mime_type;
 
   $self->SUPER::check;
@@ -167,8 +165,20 @@ Customer object (required).
 
 =item object
 
-Additional context object (currently, can be a cust_main object, cust_pkg
-object, or cust_bill object).
+Additional context object (currently, can be a cust_main, cust_pkg, 
+cust_bill, cust_pay, cust_pay_pending, or svc_(acct, phone, broadband, 
+domain) ).  If the object is a svc_*, its cust_pkg will be fetched and 
+used for substitution.
+
+As a special case, this may be an arrayref of two objects.  Both 
+objects will be available for substitution, with their field names 
+prefixed with 'new_' and 'old_' respectively.  This is used in the 
+rt_ticket export when exporting "replace" events.
+
+=item to
+
+Destination address.  The default is to use the customer's 
+invoicing_list addresses.
 
 =back
 
@@ -188,15 +198,37 @@ sub prepare {
   # create substitution table
   ###  
   my %hash;
-  foreach my $obj ($cust_main, $object || ()) {
+  my @objects = ($cust_main);
+  my @prefixes = ('');
+  my $svc;
+  if( ref $object ) {
+    if( ref($object) eq 'ARRAY' ) {
+      # [new, old], for provisioning tickets
+      push @objects, $object->[0], $object->[1];
+      push @prefixes, 'new_', 'old_';
+      $svc = $object->[0] if $object->[0]->isa('FS::svc_Common');
+    }
+    else {
+      push @objects, $object;
+      push @prefixes, '';
+      $svc = $object if $object->isa('FS::svc_Common');
+    }
+  }
+  if( $svc ) {
+    push @objects, $svc->cust_svc->cust_pkg;
+    push @prefixes, '';
+  }
+
+  foreach my $obj (@objects) {
+    my $prefix = shift @prefixes;
     foreach my $name (@{ $subs->{$obj->table} }) {
       if(!ref($name)) {
         # simple case
-        $hash{$name} = $obj->$name();
+        $hash{$prefix.$name} = $obj->$name();
       }
       elsif( ref($name) eq 'ARRAY' ) {
         # [ foo => sub { ... } ]
-        $hash{$name->[0]} = $name->[1]->($obj);
+        $hash{$prefix.($name->[0])} = $name->[1]->($obj);
       }
       else {
         warn "bad msg_template substitution: '$name'\n";
@@ -204,41 +236,68 @@ sub prepare {
       } 
     } 
   } 
-  $_ = encode_entities($_) foreach values(%hash); # HTML escape
+  $_ = encode_entities($_) foreach values(%hash);
+
 
   ###
-  # fill-in
+  # clean up template
   ###
-
   my $subject_tmpl = new Text::Template (
     TYPE   => 'STRING',
     SOURCE => $self->subject,
   );
   my $subject = $subject_tmpl->fill_in( HASH => \%hash );
 
+  my $body = $self->body;
+  my ($skin, $guts) = eviscerate($body);
+  @$guts = map { 
+    $_ = decode_entities($_); # turn all punctuation back into itself
+    s/\r//gs;           # remove \r's
+    s/<br[^>]*>/\n/gsi; # and <br /> tags
+    s/<p>/\n/gsi;       # and <p>
+    s/<\/p>//gsi;       # and </p>
+    s/\240/ /gs;        # and &nbsp;
+    $_
+  } @$guts;
+  
+  $body = '{ use Date::Format qw(time2str); "" }';
+  while(@$skin || @$guts) {
+    $body .= shift(@$skin) || '';
+    $body .= shift(@$guts) || '';
+  }
+
+  ###
+  # fill-in
+  ###
+
   my $body_tmpl = new Text::Template (
-    TYPE   => 'STRING',
-    SOURCE => $self->body,
+    TYPE          => 'STRING',
+    SOURCE        => $body,
   );
-  my $body = $body_tmpl->fill_in( HASH => \%hash );
+
+  $body = $body_tmpl->fill_in( HASH => \%hash );
 
   ###
   # and email
   ###
 
-  my @to = $cust_main->invoicing_list_emailonly;
-  #unless (@to) { #XXX do something }
+  my @to = ($opt{'to'}) || $cust_main->invoicing_list_emailonly;
+  #warn "prepared msg_template with no email destination (custnum ".
+  #  $cust_main->custnum.")\n"
+  #  if !@to;
+  #  warning is not appropriate now that we use these for tickets
 
   my $conf = new FS::Conf;
 
   (
-    'from' => $self->from || 
+    'from' => $self->from_addr || 
               scalar( $conf->config('invoice_from', $cust_main->agentnum) ),
     'to'   => \@to,
+    'bcc'  => $self->bcc_addr || undef,
     'subject'   => $subject,
     'html_body' => $body,
-    #XXX auto-make a text copy w/HTML::FormatText?
-    #  alas, us luddite mutt/pine users just aren't that big a deal
+    'text_body' => HTML::FormatText->new(leftmargin => 0, rightmargin => 70
+                    )->format( HTML::TreeBuilder->new_from_content($body) ),
   );
 
 }
@@ -250,6 +309,8 @@ Fills in the template and sends it to the customer.  Options are as for
 
 =cut
 
+# broken out from prepare() in case we want to queue the sending,
+# preview it, etc.
 sub send {
   my $self = shift;
   send_email(generate_email($self->prepare(@_)));
@@ -258,6 +319,9 @@ sub send {
 # helper sub for package dates
 my $ymd = sub { $_[0] ? time2str('%Y-%m-%d', $_[0]) : '' };
 
+# needed for some things
+my $conf = new FS::Conf;
+
 #return contexts and fill-in values
 # If you add anything, be sure to add a description in 
 # httemplate/edit/msg_template.html.
@@ -278,20 +342,31 @@ sub substitutions {
       ship_country
       ship_daytime ship_night ship_fax
 
-      payby paymask payname paytype payip
+      paymask payname paytype payip
       num_cancelled_pkgs num_ncancelled_pkgs num_pkgs
       classname categoryname
       balance
+      credit_limit
       invoicing_list_emailonly
       cust_status ucfirst_cust_status cust_statuscolor
 
       signupdate dundate
+      expdate
+      packages recurdates
       ),
+      # expdate is a special case
       [ signupdate_ymd    => sub { time2str('%Y-%m-%d', shift->signupdate) } ],
       [ dundate_ymd       => sub { time2str('%Y-%m-%d', shift->dundate) } ],
       [ paydate_my        => sub { sprintf('%02d/%04d', shift->paydate_monthyear) } ],
       [ otaker_first      => sub { shift->access_user->first } ],
       [ otaker_last       => sub { shift->access_user->last } ],
+      [ payby             => sub { FS::payby->shortname(shift->payby) } ],
+      [ company_name      => sub { 
+          $conf->config('company_name', shift->agentnum) 
+        } ],
+      [ company_address   => sub {
+          $conf->config('company_address', shift->agentnum)
+        } ],
     ],
     # next_bill_date
     'cust_pkg'  => [qw( 
@@ -303,6 +378,7 @@ sub substitutions {
       adjourn susp expire 
       labels_short
       ),
+      [ pkg               => sub { shift->part_pkg->pkg } ],
       [ cancel            => sub { shift->getfield('cancel') } ], # grrr...
       [ start_ymd         => sub { $ymd->(shift->getfield('start_date')) } ],
       [ setup_ymd         => sub { $ymd->(shift->getfield('setup')) } ],
@@ -315,14 +391,80 @@ sub substitutions {
     ],
     'cust_bill' => [qw(
       invnum
+      _date
     )],
     #XXX not really thinking about cust_bill substitutions quite yet
     
+    # for welcome and limit warning messages
     'svc_acct' => [qw(
+      svcnum
       username
+      domain
       ),
       [ password          => sub { shift->getfield('_password') } ],
-    ], # for welcome messages
+    ],
+    'svc_domain' => [qw(
+      svcnum
+      domain
+      ),
+      [ registrar         => sub {
+          my $registrar = qsearchs('registrar', 
+            { registrarnum => shift->registrarnum} );
+          $registrar ? $registrar->registrarname : ''
+        }
+      ],
+      [ catchall          => sub { 
+          my $svc_acct = qsearchs('svc_acct', { svcnum => shift->catchall });
+          $svc_acct ? $svc_acct->email : ''
+        }
+      ],
+    ],
+    'svc_phone' => [qw(
+      svcnum
+      phonenum
+      countrycode
+      domain
+      )
+    ],
+    'svc_broadband' => [qw(
+      svcnum
+      speed_up
+      speed_down
+      ip_addr
+      mac_addr
+      )
+    ],
+    # for payment receipts
+    'cust_pay' => [qw(
+      paynum
+      _date
+      ),
+      [ paid              => sub { sprintf("%.2f", shift->paid) } ],
+      # overrides the one in cust_main in cases where a cust_pay is passed
+      [ payby             => sub { FS::payby->shortname(shift->payby) } ],
+      [ date              => sub { time2str("%a %B %o, %Y", shift->_date) } ],
+      [ payinfo           => sub { 
+          my $cust_pay = shift;
+          ($cust_pay->payby eq 'CARD' || $cust_pay->payby eq 'CHEK') ?
+            $cust_pay->paymask : $cust_pay->decrypt($cust_pay->payinfo)
+        } ],
+    ],
+    # for payment decline messages
+    # try to support all cust_pay fields
+    # 'error' is a special case, it contains the raw error from the gateway
+    'cust_pay_pending' => [qw(
+      _date
+      error
+      ),
+      [ paid              => sub { sprintf("%.2f", shift->paid) } ],
+      [ payby             => sub { FS::payby->shortname(shift->payby) } ],
+      [ date              => sub { time2str("%a %B %o, %Y", shift->_date) } ],
+      [ payinfo           => sub {
+          my $pending = shift;
+          ($pending->payby eq 'CARD' || $pending->payby eq 'CHEK') ?
+            $pending->paymask : $pending->decrypt($pending->payinfo)
+        } ],
+    ],
   };
 }
 
@@ -330,25 +472,27 @@ sub _upgrade_data {
   my ($self, %opts) = @_;
 
   my @fixes = (
-    [ 'alerter_msgnum',  'alerter_template',   '',               '' ],
-    [ 'cancel_msgnum',   'cancelmessage',      'cancelsubject',  '' ],
-    [ 'decline_msgnum',  'declinetemplate',    '',               '' ],
-    [ 'impending_recur_msgnum', 'impending_recur_template', '',  '' ],
-    [ 'welcome_msgnum',  'welcome_email',      'welcome_email-subject', 'welcome_email-from' ],
-    [ 'warning_msgnum',  'warning_email',      'warning_email-subject', 'warning_email-from' ],
+    [ 'alerter_msgnum',  'alerter_template',   '',               '', '' ],
+    [ 'cancel_msgnum',   'cancelmessage',      'cancelsubject',  '', '' ],
+    [ 'decline_msgnum',  'declinetemplate',    '',               '', '' ],
+    [ 'impending_recur_msgnum', 'impending_recur_template', '',  '', 'impending_recur_bcc' ],
+    [ 'payment_receipt_msgnum', 'payment_receipt_email', '',     '', '' ],
+    [ 'welcome_msgnum',  'welcome_email',      'welcome_email-subject', 'welcome_email-from', '' ],
+    [ 'warning_msgnum',  'warning_email',      'warning_email-subject', 'warning_email-from', '' ],
   );
  
   my $conf = new FS::Conf;
   my @agentnums = ('', map {$_->agentnum} qsearch('agent', {}));
   foreach my $agentnum (@agentnums) {
     foreach (@fixes) {
-      my ($newname, $oldname, $subject, $from) = @$_;
+      my ($newname, $oldname, $subject, $from, $bcc) = @$_;
       if ($conf->exists($oldname, $agentnum)) {
         my $new = new FS::msg_template({
            'msgname'   => $oldname,
            'agentnum'  => $agentnum,
            'from_addr' => ($from && $conf->config($from, $agentnum)) || 
                           $conf->config('invoice_from', $agentnum),
+           'bcc_addr'  => ($bcc && $conf->config($from, $agentnum)) || '',
            'subject'   => ($subject && $conf->config($subject, $agentnum)) || '',
            'mime_type' => 'text/html',
            'body'      => join('<BR>',$conf->config($oldname, $agentnum)),
@@ -364,6 +508,56 @@ sub _upgrade_data {
   }
 }
 
+sub eviscerate {
+  # Every bit as pleasant as it sounds.
+  #
+  # We do this because Text::Template::Preprocess doesn't
+  # actually work.  It runs the entire template through 
+  # the preprocessor, instead of the code segments.  Which 
+  # is a shame, because Text::Template already contains
+  # the code to do this operation.
+  my $body = shift;
+  my (@outside, @inside);
+  my $depth = 0;
+  my $chunk = '';
+  while($body || $chunk) {
+    my ($first, $delim, $rest);
+    # put all leading non-delimiters into $first
+    ($first, $rest) =
+        ($body =~ /^((?:\\[{}]|[^{}])*)(.*)$/s);
+    $chunk .= $first;
+    # put a leading delimiter into $delim if there is one
+    ($delim, $rest) =
+      ($rest =~ /^([{}]?)(.*)$/s);
+
+    if( $delim eq '{' ) {
+      $chunk .= '{';
+      if( $depth == 0 ) {
+        push @outside, $chunk;
+        $chunk = '';
+      }
+      $depth++;
+    }
+    elsif( $delim eq '}' ) {
+      $depth--;
+      if( $depth == 0 ) {
+        push @inside, $chunk;
+        $chunk = '';
+      }
+      $chunk .= '}';
+    }
+    else {
+      # no more delimiters
+      if( $depth == 0 ) {
+        push @outside, $chunk . $rest;
+      } # else ? something wrong
+      last;
+    }
+    $body = $rest;
+  }
+  (\@outside, \@inside);
+}
+
 =back
 
 =head1 BUGS