RT#29354: Password Security in Email [customer fields, images, js files]
[freeside.git] / FS / FS / svc_acct.pm
index a76d93d..53b12f1 100644 (file)
@@ -4,6 +4,7 @@ use base qw( FS::svc_Domain_Mixin FS::svc_PBX_Mixin
              FS::svc_Radius_Mixin
              FS::svc_Tower_Mixin
              FS::svc_IP_Mixin
+             FS::Password_Mixin
              FS::svc_Common
            );
 
@@ -17,8 +18,7 @@ use vars qw( $DEBUG $me $conf $skip_fuzzyfiles
              $username_slash $username_equals $username_pound
              $username_exclamation
              $password_noampersand $password_noexclamation
-             $warning_template $warning_from $warning_subject $warning_mimetype
-             $warning_cc
+             $warning_msgnum
              $smtpmachine
              $radius_password $radius_ip
              $dirhash
@@ -44,7 +44,6 @@ use FS::PagedSearch qw( psearch ); # XXX in v4, replace with FS::Cursor
 use FS::part_pkg;
 use FS::part_svc;
 use FS::svc_acct_pop;
-use FS::cust_main_invoice;
 use FS::svc_domain;
 use FS::svc_pbx;
 use FS::raddb;
@@ -57,7 +56,6 @@ use FS::svc_forward;
 use FS::svc_www;
 use FS::cdr;
 use FS::tower_sector;
-use FS::Misc;
 
 $DEBUG = 0;
 $me = '[FS::svc_acct]';
@@ -91,22 +89,7 @@ FS::UID->install_callback( sub {
   $password_noampersand = $conf->exists('password-noexclamation');
   $password_noexclamation = $conf->exists('password-noexclamation');
   $dirhash = $conf->config('dirhash') || 0;
-  if ( $conf->exists('warning_email') ) {
-    $warning_template = new Text::Template (
-      TYPE   => 'ARRAY',
-      SOURCE => [ map "$_\n", $conf->config('warning_email') ]
-    ) or warn "can't create warning email template: $Text::Template::ERROR";
-    $warning_from = $conf->config('warning_email-from'); # || 'your-isp-is-dum'
-    $warning_subject = $conf->config('warning_email-subject') || 'Warning';
-    $warning_mimetype = $conf->config('warning_email-mimetype') || 'text/plain';
-    $warning_cc = $conf->config('warning_email-cc');
-  } else {
-    $warning_template = '';
-    $warning_from = '';
-    $warning_subject = '';
-    $warning_mimetype = '';
-    $warning_cc = '';
-  }
+  $warning_msgnum = $conf->config('threshold_warning_msgnum');
   $smtpmachine = $conf->config('smtpmachine');
   $radius_password = $conf->config('radius-password') || 'Password';
   $radius_ip = $conf->config('radius-ip') || 'Framed-IP-Address';
@@ -701,6 +684,9 @@ sub insert {
     'child_objects' => $self->child_objects,
     %options,
   );
+
+  $error ||= $self->insert_password_history;
+
   if ( $error ) {
     $dbh->rollback if $oldAutoCommit;
     return $error;
@@ -724,114 +710,51 @@ sub insert {
         || $conf->exists('emailinvoiceauto')
         && ! $cust_main->invoicing_list_emailonly
        ) {
-      my @invoicing_list = $cust_main->invoicing_list;
-      push @invoicing_list, $self->email;
-      $cust_main->invoicing_list(\@invoicing_list);
+
+      # slight false laziness w/ edit/process/cust_main.cgi...
+      # and also slightly arbitrary behavior.
+      # if the "real name" of this account matches the first + last name
+      # of a contact, attach the email address to that person.
+      my @contacts = map { $_->contact } $cust_main->cust_contact;
+      my $myname = $self->get('finger');
+      my ($contact) =
+        grep { $_->get('first') . ' ' . $_->get('last') eq $myname } @contacts;
+      # otherwise just pick the first one
+      $contact ||= $contacts[0];
+      # if there is one
+      $contact ||= FS::contact->new({
+          'custnum'       => $cust_main->get('custnum'),
+          'locationnum'   => $cust_main->get('bill_locationnum'),
+          'last'          => $cust_main->get('last'),
+          'first'         => $cust_main->get('first'),
+      });
+      $contact->set('emailaddress', $self->email);
+      $contact->set('invoice_dest', 'Y');
+
+      if ( $contact->get('contactnum') ) {
+        $error = $contact->replace;
+      } else {
+        $error = $contact->insert;
+      }
+
+      if ( $error ) {
+        $dbh->rollback if $oldAutoCommit;
+        return "creating invoice destination contact: $error";
+      }
     }
 
-    #welcome email/letter
+    #welcome email
     my @welcome_exclude_svcparts = $conf->config('svc_acct_welcome_exclude');
     unless ( grep { $_ eq $self->svcpart } @welcome_exclude_svcparts ) {
-      #indent skips a level for some reason
-        #welcome email
         my $error = '';
         my $msgnum = $conf->config('welcome_msgnum', $agentnum);
         if ( $msgnum ) {
           my $msg_template = qsearchs('msg_template', { msgnum => $msgnum });
           $error = $msg_template->send('cust_main' => $cust_main,
                                        'object'    => $self);
+          #should this do something on error?
         }
-        else { #!$msgnum
-          my ($to,$welcome_template,$welcome_from,$welcome_subject,$welcome_subject_template,$welcome_mimetype)
-            = ('','','','','','');
-
-          if ( $conf->exists('welcome_email', $agentnum) ) {
-            $welcome_template = new Text::Template (
-              TYPE   => 'ARRAY',
-              SOURCE => [ map "$_\n", $conf->config('welcome_email', $agentnum) ]
-            ) or warn "can't create welcome email template: $Text::Template::ERROR";
-            $welcome_from = $conf->config('welcome_email-from', $agentnum);
-              # || 'your-isp-is-dum'
-            $welcome_subject = $conf->config('welcome_email-subject', $agentnum)
-              || 'Welcome';
-            $welcome_subject_template = new Text::Template (
-              TYPE   => 'STRING',
-              SOURCE => $welcome_subject,
-            ) or warn "can't create welcome email subject template: $Text::Template::ERROR";
-            $welcome_mimetype = $conf->config('welcome_email-mimetype', $agentnum)
-              || 'text/plain';
-          }
-          if ( $welcome_template ) {
-            my $to = join(', ', grep { $_ !~ /^(POST|FAX)$/ } $cust_main->invoicing_list );
-            if ( $to ) {
-
-              my %hash = (
-                           'custnum'  => $self->custnum,
-                           'username' => $self->username,
-                           'password' => $self->_password,
-                           'first'    => $cust_main->first,
-                           'last'     => $cust_main->getfield('last'),
-                           'pkg'      => $cust_pkg->part_pkg->pkg,
-                         );
-              my $wqueue = new FS::queue {
-                'svcnum' => $self->svcnum,
-                'job'    => 'FS::svc_acct::send_email'
-              };
-              my $error = $wqueue->insert(
-                'to'       => $to,
-                'from'     => $welcome_from,
-                'subject'  => $welcome_subject_template->fill_in( HASH => \%hash, ),
-                'mimetype' => $welcome_mimetype,
-                'body'     => $welcome_template->fill_in( HASH => \%hash, ),
-              );
-              if ( $error ) {
-                $dbh->rollback if $oldAutoCommit;
-                return "error queuing welcome email: $error";
-              }
-
-              if ( $options{'depend_jobnum'} ) {
-                warn "$me depend_jobnum found; adding to welcome email dependancies"
-                  if $DEBUG;
-                if ( ref($options{'depend_jobnum'}) ) {
-                  warn "$me adding jobs ". join(', ', @{$options{'depend_jobnum'}} ).
-                       "to welcome email dependancies"
-                    if $DEBUG;
-                  push @jobnums, @{ $options{'depend_jobnum'} };
-                } else {
-                  warn "$me adding job $options{'depend_jobnum'} ".
-                       "to welcome email dependancies"
-                    if $DEBUG;
-                  push @jobnums, $options{'depend_jobnum'};
-                }
-              }
-
-              foreach my $jobnum ( @jobnums ) {
-                my $error = $wqueue->depend_insert($jobnum);
-                if ( $error ) {
-                  $dbh->rollback if $oldAutoCommit;
-                  return "error queuing welcome email job dependancy: $error";
-                }
-              }
-
-            }
-
-          } # if $welcome_template
-        } # if !$msgnum
-        # print welcome letter
-        if ($conf->exists('svc_acct_welcome_letter')) {
-          my $queue = new FS::queue {
-            'job'     => 'FS::svc_acct::process_print_welcome_letter',
-          };
-          $error = $queue->insert(
-            'svcnum'  => $self->svcnum,
-            'template' => 'svc_acct_welcome_letter',
-          );
-          if ($error) {
-            warn "can't send welcome letter: $error";
-          }
-        }
-      #indent skipped a level for some reason
-    } # unless in @welcome_exclude_svcparts
+    }
   } # if $cust_pkg
 
   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
@@ -904,23 +827,6 @@ sub delete {
   local $FS::UID::AutoCommit = 0;
   my $dbh = dbh;
 
-  foreach my $cust_main_invoice (
-    qsearch( 'cust_main_invoice', { 'dest' => $self->svcnum } )
-  ) {
-    unless ( defined($cust_main_invoice) ) {
-      warn "WARNING: something's wrong with qsearch";
-      next;
-    }
-    my %hash = $cust_main_invoice->hash;
-    $hash{'dest'} = $self->email;
-    my $new = new FS::cust_main_invoice \%hash;
-    my $error = $new->replace($cust_main_invoice);
-    if ( $error ) {
-      $dbh->rollback if $oldAutoCommit;
-      return $error;
-    }
-  }
-
   foreach my $svc_domain (
     qsearch( 'svc_domain', { 'catchall' => $self->svcnum } )
   ) {
@@ -1001,6 +907,12 @@ sub replace {
   my $dbh = dbh;
 
   $error = $new->SUPER::replace($old, @_); # usergroup here
+
+  # don't need to record this unless the password was changed
+  if ( $old->_password ne $new->_password ) {
+    $error ||= $new->insert_password_history;
+  }
+
   if ( $error ) {
     $dbh->rollback if $oldAutoCommit;
     return $error if $error;
@@ -2136,23 +2048,17 @@ sub _op_usage {
     }
   }
 
-  if ($warning_template && &{$op2warncondition{$op}}($self, $column, $amount)) {
+  if ($warning_msgnum && &{$op2warncondition{$op}}($self, $column, $amount)) {
     my $wqueue = new FS::queue {
       'svcnum' => $self->svcnum,
       'job'    => 'FS::svc_acct::reached_threshold',
     };
 
-    my $to = '';
-    if ($op eq '-'){
-      $to = $warning_cc if &{$op2condition{$op}}($self, $column, $amount);
-    }
-
     # x_threshold race
     my $error = $wqueue->insert(
       'svcnum' => $self->svcnum,
       'op'     => $op,
-      'column' => $column,
-      'to'     => $to,
+      'column' => $column
     );
     if ( $error ) {
       $dbh->rollback if $oldAutoCommit;
@@ -2770,6 +2676,25 @@ sub virtual_maildir {
   $self->domain. '/maildirs/'. $self->username. '/';
 }
 
+=item password_svc_check
+
+Override, for L<FS::Password_Mixin>.  Not really intended for other use.
+
+=cut
+
+sub password_svc_check {
+  my ($self, $password) = @_;
+  foreach my $field ( qw(username finger) ) {
+    foreach my $word (split(/\W+/,$self->get($field))) {
+      next unless length($word) > 2;
+      if ($password =~ /$word/i) {
+        return qq(Password contains account information '$word');
+      }
+    }
+  }
+  return '';
+}
+
 =back
 
 =head1 CLASS METHODS
@@ -2851,32 +2776,6 @@ sub _search_svc {
 
 =over 4
 
-=item send_email
-
-This is the FS::svc_acct job-queue-able version.  It still uses
-FS::Misc::send_email under-the-hood.
-
-=cut
-
-sub send_email {
-  my %opt = @_;
-
-  eval "use FS::Misc qw(send_email)";
-  die $@ if $@;
-
-  $opt{mimetype} ||= 'text/plain';
-  $opt{mimetype} .= '; charset="iso-8859-1"' unless $opt{mimetype} =~ /charset/;
-
-  my $error = send_email(
-    'from'         => $opt{from},
-    'to'           => $opt{to},
-    'subject'      => $opt{subject},
-    'content-type' => $opt{mimetype},
-    'body'         => [ map "$_\n", split("\n", $opt{body}) ],
-  );
-  die $error if $error;
-}
-
 =item check_and_rebuild_fuzzyfiles
 
 =cut
@@ -2990,72 +2889,39 @@ sub reached_threshold {
     my $error = $svc_acct->replace;
     die $error if $error; # email next time, i guess
 
-    if ( $warning_template ) {
-      eval "use FS::Misc qw(send_email)";
-      die $@ if $@;
+    if ( $warning_msgnum ) {
 
-      my $cust_pkg  = $svc_acct->cust_svc->cust_pkg;
-      my $cust_main = $cust_pkg->cust_main;
+      my $msg_template = qsearchs('msg_template',{ msgnum => $warning_msgnum });
+      die "Could not load template for threshold_warning_msgnum ($warning_msgnum)" unless $msg_template;
+
+      my $cust_main = $svc_acct->cust_svc->cust_pkg->cust_main;
 
-      my $to = join(', ', grep { $_ !~ /^(POST|FAX)$/ } 
-                               $cust_main->invoicing_list,
-                               ($opt{'to'} ? $opt{'to'} : ())
-                   );
-
-      my $mimetype = $warning_mimetype;
-      $mimetype .= '; charset="iso-8859-1"' unless $opt{mimetype} =~ /charset/;
-
-      my $body       =  $warning_template->fill_in( HASH => {
-                        'custnum'   => $cust_main->custnum,
-                        'username'  => $svc_acct->username,
-                        'password'  => $svc_acct->_password,
-                        'first'     => $cust_main->first,
-                        'last'      => $cust_main->getfield('last'),
-                        'pkg'       => $cust_pkg->part_pkg->pkg,
-                        'column'    => $opt{'column'},
-                        'amount'    => $opt{'column'} =~/bytes/
-                                       ? FS::UI::bytecount::display_bytecount($svc_acct->getfield($opt{'column'}))
-                                       : $svc_acct->getfield($opt{'column'}),
-                        'threshold' => $opt{'column'} =~/bytes/
-                                       ? FS::UI::bytecount::display_bytecount($threshold)
-                                       : $threshold,
-                      } );
-
-
-      my $error = send_email(
-        'from'         => $warning_from,
-        'to'           => $to,
-        'subject'      => $warning_subject,
-        'content-type' => $mimetype,
-        'body'         => [ map "$_\n", split("\n", $body) ],
+      my $to = join(', ', $cust_main->invoicing_list_emailonly );
+
+      my $error = $msg_template->send(
+        cust_main     => $cust_main,
+        object        => $svc_acct,
+        to            => $to,
+        substitutions => {
+          # have to override these, because we changed threshold above
+          'column'    => $opt{'column'},
+          'amount'    => $opt{'column'} =~/bytes/
+                         ? FS::UI::bytecount::display_bytecount($svc_acct->getfield($opt{'column'}))
+                         : $svc_acct->getfield($opt{'column'}),
+          'threshold' => $opt{'column'} =~/bytes/
+                         ? FS::UI::bytecount::display_bytecount($threshold)
+                         : $threshold,
+        },
       );
-      die $error if $error;
+
+      die "Error sending threshold warning email: $error" if $error;
+
     }
   }else{
     die "unknown op: " . $opt{'op'};
   }
 }
 
-sub process_print_welcome_letter {
-  my %opt = @_;
-
-  my $self = qsearchs('svc_acct', { 'svcnum' => $opt{'svcnum'} } )
-    or die "invalid svc_acct: " . $opt{'svcnum'};
-  my $cust_main = $self->cust_svc->cust_pkg->cust_main;
-
-  my $ps = $cust_main->print_ps('svc_acct_welcome_letter',
-    'extra_fields' => {
-      map { $_ => $self->$_ } $self->fields, # or maybe just username & password?
-    },
-  );
-  my $error = FS::Misc::do_print(
-    [ $ps ],
-    'agentnum' => $cust_main->agentnum,
-  );
-  die $error if $error;
-
-}
-
 =back
 
 =head1 BUGS