Revert "restore our time-tracking hack, RT#83379, RT#81529"
[freeside.git] / FS / FS / svc_acct.pm
index 26d6e5b..053f1e0 100644 (file)
@@ -1,13 +1,14 @@
 package FS::svc_acct;
-
-use strict;
-use base qw( FS::svc_Domain_Mixin
-             FS::svc_CGP_Mixin
-             FS::svc_CGPRule_Mixin
+use base qw( FS::svc_Domain_Mixin FS::svc_PBX_Mixin
+             FS::svc_CGP_Mixin FS::svc_CGPRule_Mixin
              FS::svc_Radius_Mixin
              FS::svc_Tower_Mixin
              FS::svc_IP_Mixin
-             FS::svc_Common );
+             FS::Password_Mixin
+             FS::svc_Common
+           );
+
+use strict;
 use vars qw( $DEBUG $me $conf $skip_fuzzyfiles
              $dir_prefix @shells $usernamemin
              $usernamemax $passwordmin $passwordmax
@@ -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
@@ -40,10 +40,10 @@ use FS::Record qw( qsearch qsearchs fields dbh dbdef );
 use FS::Msgcat qw(gettext);
 use FS::UI::bytecount;
 use FS::UI::Web;
+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;
@@ -55,7 +55,6 @@ use FS::part_export;
 use FS::svc_forward;
 use FS::svc_www;
 use FS::cdr;
-use FS::acct_snarf;
 use FS::tower_sector;
 
 $DEBUG = 0;
@@ -68,12 +67,12 @@ FS::UID->install_callback( sub {
   @shells = $conf->config('shells');
   $usernamemin = $conf->config('usernamemin') || 2;
   $usernamemax = $conf->config('usernamemax');
-  $passwordmin = $conf->config('passwordmin'); # || 6;
-  #blank->6, keep 0
+  $passwordmin = $conf->config('passwordmin');
+  #blank->8, keep 0
   $passwordmin = ( defined($passwordmin) && $passwordmin =~ /\d+/ )
                    ? $passwordmin
-                   : 6;
-  $passwordmax = $conf->config('passwordmax') || 8;
+                   : 8;
+  $passwordmax = $conf->config('passwordmax') || 12;
   $username_letter = $conf->exists('username-letter');
   $username_letterfirst = $conf->exists('username-letterfirst');
   $username_noperiod = $conf->exists('username-noperiod');
@@ -90,31 +89,15 @@ 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';
-  @pw_set = ( 'A'..'Z' ) if $conf->exists('password-generated-allcaps');
+  @pw_set = FS::svc_acct->pw_set;
 }
 );
 
 @saltset = ( 'a'..'z' , 'A'..'Z' , '0'..'9' , '.' , '/' );
-@pw_set = ( 'a'..'z', 'A'..'Z', '0'..'9', '(', ')', '#', '.', ',' );
 
 sub _cache {
   my $self = shift;
@@ -260,6 +243,7 @@ sub table_info {
     'display_weight' => 10,
     'cancel_weight'  => 50, 
     'ip_field' => 'slipip',
+    'manual_require' => 1,
     'fields' => {
         'dir'       => 'Home directory',
         'uid'       => {
@@ -283,6 +267,7 @@ sub table_info {
                          disable_default => 1,
                          disable_fixed => 1,
                          disable_select => 1,
+                         required => 1,
                        },
         'password_selfchange' => { label => 'Password modification',
                                    type  => 'checkbox',
@@ -294,27 +279,25 @@ sub table_info {
                          label => 'Quota', #Mail storage limit
                          type => 'text',
                          disable_inventory => 1,
-                         disable_select => 1,
                        },
         'file_quota'=> { 
                          label => 'File storage limit',
                          type => 'text',
                          disable_inventory => 1,
-                         disable_select => 1,
                        },
         'file_maxnum'=> { 
                          label => 'Number of files limit',
                          type => 'text',
                          disable_inventory => 1,
-                         disable_select => 1,
                        },
         'file_maxsize'=> { 
                          label => 'File size limit',
                          type => 'text',
                          disable_inventory => 1,
-                         disable_select => 1,
                        },
-        '_password' => 'Password',
+        '_password' => { label => 'Password',
+          #required => 1
+                       },
         'gid'       => {
                          label    => 'GID',
                         def_info => 'when blank, defaults to UID',
@@ -333,10 +316,12 @@ sub table_info {
         'domsvc'    => {
                          label     => 'Domain',
                          type      => 'select',
+                         select_svc => 1,
                          select_table => 'svc_domain',
                          select_key   => 'svcnum',
                          select_label => 'domain',
                          disable_inventory => 1,
+                         required => 1,
                        },
         'pbxsvc'    => { label => 'PBX',
                          type  => 'select-svc_pbx.html',
@@ -344,6 +329,15 @@ sub table_info {
                          disable_select => 1, #UI wonky, pry works otherwise
                        },
         'sectornum' => 'Tower sector',
+        'routernum' => 'Router/block',
+        'blocknum'  => {
+                         'label' => 'Address block',
+                         'type'  => 'select',
+                         'select_table' => 'addr_block',
+                          'select_key'   => 'blocknum',
+                         'select_label' => 'cidr',
+                         'disable_inventory' => 1,
+                       },
         'usergroup' => {
                          label => 'RADIUS groups',
                          type  => 'select-radius_group.html',
@@ -356,7 +350,7 @@ sub table_info {
                          type  => 'text',
                          disable_inventory => 1,
                          disable_select => 1,
-                         disable_part_svc_column => 1,
+                         #disable_part_svc_column => 1,
                        },
         'upbytes'   => { label => 'Upload',
                          type  => 'text',
@@ -699,6 +693,9 @@ sub insert {
     'child_objects' => $self->child_objects,
     %options,
   );
+
+  $error ||= $self->insert_password_history;
+
   if ( $error ) {
     $dbh->rollback if $oldAutoCommit;
     return $error;
@@ -722,98 +719,46 @@ 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);
-    }
 
-    #welcome email
-    my @welcome_exclude_svcparts = $conf->config('svc_acct_welcome_exclude');
-    unless ( grep { $_ eq $self->svcpart } @welcome_exclude_svcparts ) {
-        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);
-        }
-        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
+      # slight false laziness w/ edit/process/cust_main.cgi...
+      # and also slightly arbitrary behavior.
+      #
+      # this will never happen but check it anyway
+      my ($contact) = map { $_->contact }
+        qsearch('contact_email', { emailaddress => $self->email });
+
+      if (!$contact) {
+        # 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";
+      }
     }
+
   } # if $cust_pkg
 
   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
@@ -886,37 +831,32 @@ sub delete {
   local $FS::UID::AutoCommit = 0;
   my $dbh = dbh;
 
-  foreach my $cust_main_invoice (
-    qsearch( 'cust_main_invoice', { 'dest' => $self->svcnum } )
+  foreach my $svc_domain (
+    qsearch( 'svc_domain', { 'catchall' => $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);
+    my %hash = new FS::svc_domain->hash;
+    $hash{'catchall'} = '';
+    my $new = new FS::svc_domain \%hash;
+    my $error = $new->replace($svc_domain);
     if ( $error ) {
       $dbh->rollback if $oldAutoCommit;
       return $error;
     }
   }
 
-  foreach my $svc_domain (
-    qsearch( 'svc_domain', { 'catchall' => $self->svcnum } )
+  foreach my $svc_phone (
+    qsearch( 'svc_phone', { 'forward_svcnum' => $self->svcnum })
   ) {
-    my %hash = new FS::svc_domain->hash;
-    $hash{'catchall'} = '';
-    my $new = new FS::svc_domain \%hash;
-    my $error = $new->replace($svc_domain);
+    $svc_phone->set('forward_svcnum', '');
+    my $error = $svc_phone->replace;
     if ( $error ) {
       $dbh->rollback if $oldAutoCommit;
       return $error;
     }
   }
 
-  my $error = $self->SUPER::delete; # usergroup here
+  my $error = $self->delete_password_history
+           || $self->SUPER::delete; # usergroup here
   if ( $error ) {
     $dbh->rollback if $oldAutoCommit;
     return $error;
@@ -983,6 +923,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;
@@ -1386,8 +1332,7 @@ sub check {
       $recref->{_password} = $1;
     } else {
       return gettext('illegal_password'). " $passwordmin-$passwordmax ".
-             FS::Msgcat::_gettext('illegal_password_characters').
-             ": ". $recref->{_password};
+             FS::Msgcat::_gettext('illegal_password_characters');
     }
 
     if ( $password_noampersand ) {
@@ -1903,14 +1848,6 @@ Returns an array of FS::acct_snarf records associated with the account.
 =cut
 
 # unused as originally intended, but now by Communigate Pro "RPOP"
-sub acct_snarf {
-  my $self = shift;
-  qsearch({
-    'table'    => 'acct_snarf',
-    'hashref'  => { 'svcnum' => $self->svcnum },
-    #'order_by' => 'ORDER BY priority ASC',
-  });
-}
 
 =item cgp_rpop_hashref
 
@@ -2043,6 +1980,9 @@ sub _op_usage {
 
   return '' unless $amount;
 
+  return ''
+    if $self->cust_svc->part_svc->part_svc_column($column)->columnflag eq 'F';
+
   local $SIG{HUP} = 'IGNORE';
   local $SIG{INT} = 'IGNORE';
   local $SIG{QUIT} = 'IGNORE';
@@ -2068,14 +2008,16 @@ sub _op_usage {
   die "Can't update $column for svcnum". $self->svcnum
     if $rv == 0;
 
-  #$self->snapshot; #not necessary, we retain the old values
-  #create an object with the updated usage values
-  my $new = qsearchs('svc_acct', { 'svcnum' => $self->svcnum });
-  #call exports
-  my $error = $new->replace($self);
-  if ( $error ) {
-    $dbh->rollback if $oldAutoCommit;
-    return "Error replacing: $error";
+  if ( $conf->exists('radius-chillispot-max') ) {
+    #$self->snapshot; #not necessary, we retain the old values
+    #create an object with the updated usage values
+    my $new = qsearchs('svc_acct', { 'svcnum' => $self->svcnum });
+    #call exports
+    my $error = $new->replace($self);
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return "Error replacing: $error";
+    }
   }
 
   #overlimit_action eq 'cancel' handling
@@ -2126,23 +2068,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;
@@ -2271,15 +2207,17 @@ sub set_usage {
     die "Can't update usage for svcnum ". $self->svcnum
       if $rv == 0;
   }
-
-  #$self->snapshot; #not necessary, we retain the old values
-  #create an object with the updated usage values
-  my $new = qsearchs('svc_acct', { 'svcnum' => $self->svcnum });
-  local($FS::Record::nowarn_identical) = 1;
-  my $error = $new->replace($self); #call exports
-  if ( $error ) {
-    $dbh->rollback if $oldAutoCommit;
-    return "Error replacing: $error";
+  
+  if ( $conf->exists('radius-chillispot-max') ) {
+    #$self->snapshot; #not necessary, we retain the old values
+    #create an object with the updated usage values
+    my $new = qsearchs('svc_acct', { 'svcnum' => $self->svcnum });
+    local($FS::Record::nowarn_identical) = 1;
+    my $error = $new->replace($self); #call exports
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return "Error replacing: $error";
+    }
   }
 
   if ( $reset ) {
@@ -2356,7 +2294,7 @@ sub is_rechargable {
 =item seconds_since TIMESTAMP
 
 Returns the number of seconds this account has been online since TIMESTAMP,
-according to the session monitor (see L<FS::Session>).
+according to the session monitor (see L<FS::session>).
 
 TIMESTAMP is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
 L<Time::Local> and L<Date::Parse> for conversion functions.
@@ -2369,60 +2307,6 @@ sub seconds_since {
   $self->cust_svc->seconds_since(@_);
 }
 
-=item seconds_since_sqlradacct TIMESTAMP_START TIMESTAMP_END
-
-Returns the numbers of seconds this account has been online between
-TIMESTAMP_START (inclusive) and TIMESTAMP_END (exclusive), according to an
-external SQL radacct table, specified via sqlradius export.  Sessions which
-started in the specified range but are still open are counted from session
-start to the end of the range (unless they are over 1 day old, in which case
-they are presumed missing their stop record and not counted).  Also, sessions
-which end in the range but started earlier are counted from the start of the
-range to session end.  Finally, sessions which start before the range but end
-after are counted for the entire range.
-
-TIMESTAMP_START and TIMESTAMP_END are specified as UNIX timestamps; see
-L<perlfunc/"time">.  Also see L<Time::Local> and L<Date::Parse> for conversion
-functions.
-
-=cut
-
-#note: POD here, implementation in FS::cust_svc
-sub seconds_since_sqlradacct {
-  my $self = shift;
-  $self->cust_svc->seconds_since_sqlradacct(@_);
-}
-
-=item attribute_since_sqlradacct TIMESTAMP_START TIMESTAMP_END ATTRIBUTE
-
-Returns the sum of the given attribute for all accounts (see L<FS::svc_acct>)
-in this package for sessions ending between TIMESTAMP_START (inclusive) and
-TIMESTAMP_END (exclusive).
-
-TIMESTAMP_START and TIMESTAMP_END are specified as UNIX timestamps; see
-L<perlfunc/"time">.  Also see L<Time::Local> and L<Date::Parse> for conversion
-functions.
-
-=cut
-
-#note: POD here, implementation in FS::cust_svc
-sub attribute_since_sqlradacct {
-  my $self = shift;
-  $self->cust_svc->attribute_since_sqlradacct(@_);
-}
-
-=item get_session_history TIMESTAMP_START TIMESTAMP_END
-
-Returns an array of hash references of this customers login history for the
-given time range.  (document this better)
-
-=cut
-
-sub get_session_history {
-  my $self = shift;
-  $self->cust_svc->get_session_history(@_);
-}
-
 =item last_login_text 
 
 Returns text describing the time of last login.
@@ -2434,65 +2318,99 @@ sub last_login_text {
   $self->last_login ? ctime($self->last_login) : 'unknown';
 }
 
-=item get_cdrs TIMESTAMP_START TIMESTAMP_END [ 'OPTION' => 'VALUE ... ]
+=item psearch_cdrs OPTIONS
 
-=cut
+Returns a paged search (L<FS::PagedSearch>) for Call Detail Records
+associated with this service. For svc_acct, "associated with" means that
+either the "src" or the "charged_party" field of the CDR matches either
+the "username" field of the service or the username@domain label.
 
-sub get_cdrs {
-  my($self, $start, $end, %opt ) = @_;
-
-  my $did = $self->username; #yup
-
-  my $prefix = $opt{'default_prefix'}; #convergent.au '+61'
-
-  my $for_update = $opt{'for_update'} ? 'FOR UPDATE' : '';
-
-  #SELECT $for_update * FROM cdr
-  #  WHERE calldate >= $start #need a conversion
-  #    AND calldate <  $end   #ditto
-  #    AND (    charged_party = "$did"
-  #          OR charged_party = "$prefix$did" #if length($prefix);
-  #          OR ( ( charged_party IS NULL OR charged_party = '' )
-  #               AND
-  #               ( src = "$did" OR src = "$prefix$did" ) # if length($prefix)
-  #             )
-  #        )
-  #    AND ( freesidestatus IS NULL OR freesidestatus = '' )
-
-  my $charged_or_src;
-  if ( length($prefix) ) {
-    $charged_or_src =
-      " AND (    charged_party = '$did' 
-              OR charged_party = '$prefix$did'
-              OR ( ( charged_party IS NULL OR charged_party = '' )
-                   AND
-                   ( src = '$did' OR src = '$prefix$did' )
-                 )
-            )
-      ";
-  } else {
-    $charged_or_src = 
-      " AND (    charged_party = '$did' 
-              OR ( ( charged_party IS NULL OR charged_party = '' )
-                   AND
-                   src = '$did'
-                 )
-            )
-      ";
+=cut
 
+sub psearch_cdrs {
+  my($self, %options) = @_;
+  my @fields;
+  my %hash;
+  my @where;
+
+  my $did = dbh->quote($self->username);
+  my $diddomain = dbh->quote($self->label);
+
+  my $prefix = $options{'default_prefix'} || ''; #convergent.au '+61'
+  my $prefixdid = dbh->quote($prefix . $self->username);
+
+  my $for_update = $options{'for_update'} ? 'FOR UPDATE' : '';
+
+  if ( $options{inbound} ) {
+    # these will be selected under their DIDs
+    push @where, "FALSE";
+  }
+
+  my @orwhere;
+  if (!$options{'disable_charged_party'}) {
+    push @orwhere,
+      "charged_party = $did",
+      "charged_party = $prefixdid",
+      "charged_party = $diddomain"
+      ;
+  }
+  if (!$options{'disable_src'}) {
+    push @orwhere,
+      "src = $did AND charged_party IS NULL",
+      "src = $prefixdid AND charged_party IS NULL",
+      "src = $diddomain AND charged_party IS NULL"
+      ;
+  }
+  push @where, '(' . join(' OR ', @orwhere) . ')';
+
+  # $options{'status'} = '' is meaningful; for the rest of them it's not
+  if ( exists $options{'status'} ) {
+    $hash{'freesidestatus'} = $options{'status'};
+  }
+  if ( $options{'cdrtypenum'} ) {
+    $hash{'cdrtypenum'} = $options{'cdrtypenum'};
+  }
+  if ( $options{'calltypenum'} ) {
+    $hash{'calltypenum'} = $options{'calltypenum'};
+  }
+  if ( $options{'begin'} ) {
+    push @where, 'startdate >= '. $options{'begin'};
+  } 
+  if ( $options{'end'} ) {
+    push @where, 'startdate < '.  $options{'end'};
+  } 
+  if ( $options{'nonzero'} ) {
+    push @where, 'duration > 0';
+  } 
+
+  my $extra_sql = join(' AND ', @where);
+  if ($extra_sql) {
+    if (keys %hash) {
+      $extra_sql = " AND ".$extra_sql;
+    } else {
+      $extra_sql = " WHERE ".$extra_sql;
+    }
   }
-
-  qsearch(
-    'select'    => "$for_update *",
+  return psearch({
+    'select'    => '*',
     'table'     => 'cdr',
-    'hashref'   => {
-                     #( freesidestatus IS NULL OR freesidestatus = '' )
-                     'freesidestatus' => '',
-                   },
-    'extra_sql' => $charged_or_src,
+    'hashref'   => \%hash,
+    'extra_sql' => $extra_sql,
+    'order_by'  => "ORDER BY startdate $for_update",
+  });
+}
 
-  );
+=item get_cdrs (DEPRECATED)
+
+Like psearch_cdrs, but returns all the L<FS::cdr> objects at once, in a 
+single list. Arguments are the same as for psearch_cdrs.
+
+=cut
 
+sub get_cdrs {
+  my $self = shift;
+  my $psearch = $self->psearch_cdrs(@_);
+  qsearch ( $psearch->{query} )
 }
 
 # sub radius_groups has moved to svc_Radius_Mixin
@@ -2640,6 +2558,11 @@ sub crypt_password {
       );
     } elsif ( $encryption eq 'md5' ) {
       return unix_md5_crypt( $self->_password );
+    } elsif ( $encryption eq 'sha512' ) {
+      return crypt(
+        $self->_password,
+        '$6$rounds=15420$'. join('', map $saltset[int(rand(64))], (1..16) )
+      );
     } elsif ( $encryption eq 'sha1_base64' ) { #for acct_sql
       my $pass = sha1_base64( $self->_password );
       $pass .= '=' x (4 - length($pass) % 4); #properly padded base64
@@ -2785,6 +2708,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
@@ -2866,32 +2808,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
@@ -3005,46 +2921,33 @@ 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'};