backport-ish from 1.9: blowfish apsswords exporting to LDAP should warn and return...
[freeside.git] / FS / FS / svc_acct.pm
index 48116d7..4802903 100644 (file)
@@ -21,11 +21,11 @@ use Fcntl qw(:flock);
 use Date::Format;
 use Crypt::PasswdMD5 1.2;
 use Data::Dumper;
-use Authen::Passphrase;
-use FS::UID qw( datasrc );
+use FS::UID qw( datasrc driver_name );
 use FS::Conf;
 use FS::Record qw( qsearch qsearchs fields dbh dbdef );
 use FS::Msgcat qw(gettext);
+use FS::UI::bytecount;
 use FS::svc_Common;
 use FS::cust_svc;
 use FS::part_svc;
@@ -103,6 +103,7 @@ $FS::UID::callback{'FS::svc_acct'} = sub {
   $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');
 };
 
 @saltset = ( 'a'..'z' , 'A'..'Z' , '0'..'9' , '.' , '/' );
@@ -172,8 +173,6 @@ FS::svc_Common.  The following fields are currently supported:
 
 =item _password - generated if blank
 
-=item _password_encoding - plain, crypt, ldap (or empty for autodetection)
-
 =item sec_phrase - security phrase
 
 =item popnum - Point of presence (see L<FS::svc_acct_pop>)
@@ -222,7 +221,7 @@ sub table_info {
   {
     'name'   => 'Account',
     'longname_plural' => 'Access accounts and mailboxes',
-    'sorts' => [ 'username', 'uid', ],
+    'sorts' => [ 'username', 'uid', 'last_login', ],
     'display_weight' => 10,
     'cancel_weight'  => 50, 
     'fields' => {
@@ -292,6 +291,61 @@ sub table_info {
                          disable_inventory => 1,
                          disable_select => 1,
                        },
+        'upbytes'   => { label => 'Upload',
+                         type  => 'text',
+                         disable_inventory => 1,
+                         disable_select => 1,
+                         'format' => \&FS::UI::bytecount::display_bytecount,
+                         'parse' => \&FS::UI::bytecount::parse_bytecount,
+                       },
+        'downbytes' => { label => 'Download',
+                         type  => 'text',
+                         disable_inventory => 1,
+                         disable_select => 1,
+                         'format' => \&FS::UI::bytecount::display_bytecount,
+                         'parse' => \&FS::UI::bytecount::parse_bytecount,
+                       },
+        'totalbytes'=> { label => 'Total up and download',
+                         type  => 'text',
+                         disable_inventory => 1,
+                         disable_select => 1,
+                         'format' => \&FS::UI::bytecount::display_bytecount,
+                         'parse' => \&FS::UI::bytecount::parse_bytecount,
+                       },
+        'seconds_threshold'   => { label => 'Seconds threshold',
+                                   type  => 'text',
+                                   disable_inventory => 1,
+                                   disable_select => 1,
+                                 },
+        'upbytes_threshold'   => { label => 'Upload threshold',
+                                   type  => 'text',
+                                   disable_inventory => 1,
+                                   disable_select => 1,
+                                   'format' => \&FS::UI::bytecount::display_bytecount,
+                                   'parse' => \&FS::UI::bytecount::parse_bytecount,
+                                 },
+        'downbytes_threshold' => { label => 'Download threshold',
+                                   type  => 'text',
+                                   disable_inventory => 1,
+                                   disable_select => 1,
+                                   'format' => \&FS::UI::bytecount::display_bytecount,
+                                   'parse' => \&FS::UI::bytecount::parse_bytecount,
+                                 },
+        'totalbytes_threshold'=> { label => 'Total up and download threshold',
+                                   type  => 'text',
+                                   disable_inventory => 1,
+                                   disable_select => 1,
+                                   'format' => \&FS::UI::bytecount::display_bytecount,
+                                   'parse' => \&FS::UI::bytecount::parse_bytecount,
+                                 },
+        'last_login'=>           {
+                                   label     => 'Last login',
+                                   type      => 'disabled',
+                                 },
+        'last_logout'=>          {
+                                   label     => 'Last logout',
+                                   type      => 'disabled',
+                                 },
     },
   };
 }
@@ -314,6 +368,42 @@ sub _fieldhandlers {
   };
 }
 
+sub last_login {
+  shift->_lastlog('in', @_);
+}
+
+sub last_logout {
+  shift->_lastlog('out', @_);
+}
+
+sub _lastlog {
+  my( $self, $op, $time ) = @_;
+
+  if ( defined($time) ) {
+    warn "$me last_log$op called on svcnum ". $self->svcnum.
+         ' ('. $self->email. "): $time\n"
+      if $DEBUG;
+
+    my $dbh = dbh;
+
+    my $sql = "UPDATE svc_acct SET last_log$op = ? WHERE svcnum = ?";
+    warn "$me $sql\n"
+      if $DEBUG;
+
+    my $sth = $dbh->prepare( $sql )
+      or die "Error preparing $sql: ". $dbh->errstr;
+    my $rv = $sth->execute($time, $self->svcnum);
+    die "Error executing $sql: ". $sth->errstr
+      unless defined($rv);
+    die "Can't update last_log$op for svcnum". $self->svcnum
+      if $rv == 0;
+
+    $self->{'Hash'}->{"last_log$op"} = $time;
+  }else{
+    $self->getfield("last_log$op");
+  }
+}
+
 =item search_sql STRING
 
 Class method which returns an SQL fragment to search for the given string.
@@ -609,6 +699,12 @@ sub delete {
     }
   }
 
+  my $error = $self->SUPER::delete;
+  if ( $error ) {
+    $dbh->rollback if $oldAutoCommit;
+    return $error;
+  }
+
   foreach my $radius_usergroup (
     qsearch('radius_usergroup', { 'svcnum' => $self->svcnum } )
   ) {
@@ -619,12 +715,6 @@ sub delete {
     }
   }
 
-  my $error = $self->SUPER::delete;
-  if ( $error ) {
-    $dbh->rollback if $oldAutoCommit;
-    return $error;
-  }
-
   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
   '';
 }
@@ -886,9 +976,6 @@ sub check {
               || $self->ut_snumbern('upbytes')
               || $self->ut_snumbern('downbytes')
               || $self->ut_snumbern('totalbytes')
-              || $self->ut_enum( '_password_encoding',
-                                 [ '', qw( plain crypt ldap ) ]
-                               )
   ;
   return $error if $error;
 
@@ -920,6 +1007,12 @@ sub check {
   unless ( $username_ampersand ) {
     $recref->{username} =~ /\&/ and return gettext('illegal_username');
   }
+  if ( $password_noampersand ) {
+    $recref->{_password} =~ /\&/ and return gettext('illegal_password');
+  }
+  if ( $password_noexclamation ) {
+    $recref->{_password} =~ /\!/ and return gettext('illegal_password');
+  }
   unless ( $username_percent ) {
     $recref->{username} =~ /\%/ and return gettext('illegal_username');
   }
@@ -1027,92 +1120,36 @@ sub check {
     $self->ut_textn($_);
   }
 
-  if ( $recref->{_password_encoding} eq 'ldap' ) {
-
-    if ( $recref->{_password} =~ /^(\{[\w\-]+\})(!?.{0,64})$/ ) {
-      $recref->{_password} = uc($1).$2;
-    } else {
-      return 'Illegal (ldap-encoded) password: '. $recref->{_password};
-    }
-
-  } elsif ( $recref->{_password_encoding} eq 'crypt' ) {
-
-    if ( $recref->{_password} =~
-           #/^(\$\w+\$.*|[\w\+\/]{13}|_[\w\+\/]{19}|\*)$/
-           /^(!!?)?(\$\w+\$.*|[\w\+\/]{13}|_[\w\+\/]{19}|\*)$/
-       ) {
-
-      $recref->{_password} = $1.$2;
-
-    } else {
-      return 'Illegal (crypt-encoded) password';
-    }
-
-  } elsif ( $recref->{_password_encoding} eq 'plain' ) { 
-
-    #generate a password if it is blank
-    $recref->{_password} = join('',map($pw_set[ int(rand $#pw_set) ], (0..7) ) )
-      unless length( $recref->{_password} );
-
-    if ( $recref->{_password} =~ /^([^\t\n]{$passwordmin,$passwordmax})$/ ) {
-      $recref->{_password} = $1;
-    } else {
-      return gettext('illegal_password'). " $passwordmin-$passwordmax ".
-             FS::Msgcat::_gettext('illegal_password_characters').
-             ": ". $recref->{_password};
-    }
-
-    if ( $password_noampersand ) {
-      $recref->{_password} =~ /\&/ and return gettext('illegal_password');
-    }
-    if ( $password_noexclamation ) {
-      $recref->{_password} =~ /\!/ and return gettext('illegal_password');
-    }
-
+  #generate a password if it is blank
+  $recref->{_password} = join('',map($pw_set[ int(rand $#pw_set) ], (0..7) ) )
+    unless ( $recref->{_password} );
+
+  #if ( $recref->{_password} =~ /^((\*SUSPENDED\* )?)([^\t\n]{4,16})$/ ) {
+  if ( $recref->{_password} =~ /^((\*SUSPENDED\* |!!?)?)([^\t\n]{$passwordmin,$passwordmax})$/ ) {
+    $recref->{_password} = $1.$3;
+    #uncomment this to encrypt password immediately upon entry, or run
+    #bin/crypt_pw in cron to give new users a window during which their
+    #password is available to techs, for faxing, etc.  (also be aware of 
+    #radius issues!)
+    #$recref->{password} = $1.
+    #  crypt($3,$saltset[int(rand(64))].$saltset[int(rand(64))]
+    #;
+  } elsif ( $recref->{_password} =~ /^((\*SUSPENDED\* |!!?)?)([\w\.\/\$\;\+]{13,64})$/ ) {
+    $recref->{_password} = $1.$3;
+  } elsif ( $recref->{_password} eq '*' ) {
+    $recref->{_password} = '*';
+  } elsif ( $recref->{_password} eq '!' ) {
+    $recref->{_password} = '!';
+  } elsif ( $recref->{_password} eq '!!' ) {
+    $recref->{_password} = '!!';
   } else {
-
-    #carp "warning: _password_encoding unspecified\n";
-
-    #generate a password if it is blank
-    unless ( length( $recref->{_password} ) ) {
-
-      $recref->{_password} =
-        join('',map($pw_set[ int(rand $#pw_set) ], (0..7) ) );
-      $recref->{_password_encoding} = 'plain';
-
-    } else {
-  
-      #if ( $recref->{_password} =~ /^((\*SUSPENDED\* )?)([^\t\n]{4,16})$/ ) {
-      if ( $recref->{_password} =~ /^((\*SUSPENDED\* |!!?)?)([^\t\n]{$passwordmin,$passwordmax})$/ ) {
-        $recref->{_password} = $1.$3;
-        $recref->{_password_encoding} = 'plain';
-      } elsif ( $recref->{_password} =~
-                  /^((\*SUSPENDED\* |!!?)?)([\w\.\/\$\;\+]{13,64})$/
-              ) {
-        $recref->{_password} = $1.$3;
-        $recref->{_password_encoding} = 'crypt';
-      } elsif ( $recref->{_password} eq '*' ) {
-        $recref->{_password} = '*';
-        $recref->{_password_encoding} = 'crypt';
-      } elsif ( $recref->{_password} eq '!' ) {
-        $recref->{_password_encoding} = 'crypt';
-        $recref->{_password} = '!';
-      } elsif ( $recref->{_password} eq '!!' ) {
-        $recref->{_password} = '!!';
-        $recref->{_password_encoding} = 'crypt';
-      } else {
-        #return "Illegal password";
-        return gettext('illegal_password'). " $passwordmin-$passwordmax ".
-               FS::Msgcat::_gettext('illegal_password_characters').
-               ": ". $recref->{_password};
-      }
-
-    }
-
+    #return "Illegal password";
+    return gettext('illegal_password'). " $passwordmin-$passwordmax ".
+           FS::Msgcat::_gettext('illegal_password_characters').
+           ": ". $recref->{_password};
   }
 
   $self->SUPER::check;
-
 }
 
 =item _check_system
@@ -1149,11 +1186,19 @@ sub _check_duplicate {
   my $global_unique = $conf->config('global_unique-username') || 'none';
   return '' if $global_unique eq 'disabled';
 
-  #this is Pg-specific.  what to do for mysql etc?
-  # ( mysql LOCK TABLES certainly isn't equivalent or useful here :/ )
   warn "$me locking svc_acct table for duplicate search" if $DEBUG;
-  dbh->do("LOCK TABLE svc_acct IN SHARE ROW EXCLUSIVE MODE")
-    or die dbh->errstr;
+  if ( driver_name =~ /^Pg/i ) {
+    dbh->do("LOCK TABLE svc_acct IN SHARE ROW EXCLUSIVE MODE")
+      or die dbh->errstr;
+  } elsif ( driver_name =~ /^mysql/i ) {
+    dbh->do("SELECT * FROM duplicate_lock
+               WHERE lockname = 'svc_acct'
+              FOR UPDATE"
+          ) or die dbh->errstr;
+  } else {
+    die "unknown database ". driver_name.
+        "; don't know how to lock for duplicate search";
+  }
   warn "$me acquired svc_acct table lock for duplicate search" if $DEBUG;
 
   my $part_svc = qsearchs('part_svc', { 'svcpart' => $self->svcpart } );
@@ -1588,7 +1633,10 @@ sub _op_usage {
 
   my $action = $op2action{$op};
 
-  if ( &{$op2condition{$op}}($self, $column, $amount) ) {
+  if ( &{$op2condition{$op}}($self, $column, $amount) &&
+        ( $action eq 'suspend'   && !$self->overlimit 
+       || $action eq 'unsuspend' &&  $self->overlimit ) 
+     ) {
     foreach my $part_export ( $self->cust_svc->part_svc->part_export ) {
       if ($part_export->option('overlimit_groups')) {
         my ($new,$old);
@@ -1602,6 +1650,7 @@ sub _op_usage {
           $new = $self; $old = $other;
         }
         my $error = $part_export->export_replace($new, $old);
+        $error ||= $self->overlimit($action);
         if ( $error ) {
           $dbh->rollback if $oldAutoCommit;
           return "Error replacing radius groups in export, ${op}: $error";
@@ -1614,6 +1663,7 @@ sub _op_usage {
        && &{$op2condition{$op}}($self, $column, $amount)    ) {
     #my $error = $self->$action();
     my $error = $self->cust_svc->cust_pkg->$action();
+    # $error ||= $self->overlimit($action);
     if ( $error ) {
       $dbh->rollback if $oldAutoCommit;
       return "Error ${action}ing: $error";
@@ -1672,6 +1722,7 @@ sub set_usage {
   my $dbh = dbh;
 
   my $reset = 0;
+  my %handyhash = ();
   foreach my $field (keys %$valueref){
     $reset = 1 if $valueref->{$field};
     $self->setfield($field, $valueref->{$field});
@@ -1683,12 +1734,48 @@ sub set_usage {
                            )
                        )
                      );
+    $handyhash{$field} = $self->getfield($field);
+    $handyhash{$field.'_threshold'} = $self->getfield($field.'_threshold');
   }
-  my $error = $self->replace;
-  die $error if $error;
+  #my $error = $self->replace;   #NO! we avoid the call to ->check for
+  #die $error if $error;         #services not explicity changed via the UI
+
+  my $sql = "UPDATE svc_acct SET " .
+    join (',', map { "$_ =  ?" } (keys %handyhash) ).
+    " WHERE svcnum = ?";
+
+  warn "$me $sql\n"
+    if $DEBUG;
+
+  if (scalar(keys %handyhash)) {
+    my $sth = $dbh->prepare( $sql )
+      or die "Error preparing $sql: ". $dbh->errstr;
+    my $rv = $sth->execute((values %handyhash), $self->svcnum);
+    die "Error executing $sql: ". $sth->errstr
+      unless defined($rv);
+    die "Can't update usage for svcnum ". $self->svcnum
+      if $rv == 0;
+  }
+
+  if ( $reset ) {
+    my $error;
+
+    if ($self->overlimit) {
+      $error = $self->overlimit('unsuspend');
+      foreach my $part_export ( $self->cust_svc->part_svc->part_export ) {
+        if ($part_export->option('overlimit_groups')) {
+          my $old = new FS::svc_acct $self->hashref;
+          my $groups = &{ $self->_fieldhandlers->{'usergroup'} }
+                         ($self, $part_export->option('overlimit_groups'));
+          $old->usergroup( $groups );
+          $error ||= $part_export->export_replace($self, $old);
+        }
+      }
+    }
 
-  if ( $conf->exists("svc_acct-usage_unsuspend") && $reset ) {
-    my $error = $self->cust_svc->cust_pkg->unsuspend;
+    if ( $conf->exists("svc_acct-usage_unsuspend")) {
+      $error ||= $self->cust_svc->cust_pkg->unsuspend;
+    }
     if ( $error ) {
       $dbh->rollback if $oldAutoCommit;
       return "Error unsuspending: $error";
@@ -1819,6 +1906,17 @@ sub get_session_history {
   $self->cust_svc->get_session_history(@_);
 }
 
+=item last_login_text 
+
+Returns text describing the time of last login.
+
+=cut
+
+sub last_login_text {
+  my $self = shift;
+  $self->last_login ? ctime($self->last_login) : 'unknown';
+}
+
 =item get_cdrs TIMESTAMP_START TIMESTAMP_END [ 'OPTION' => 'VALUE ... ]
 
 =cut
@@ -1944,42 +2042,23 @@ sub check_password {
   #self-service and pay up
   ( my $password = $self->_password ) =~ s/^\*SUSPENDED\* //;
 
-  if ( $self->_password_encoding eq 'ldap' ) {
-
-    my $auth = from_rfc2307 Authen::Passphrase $self->_password;
-    return $auth->match($check_password);
-
-  } elsif ( $self->_password_encoding eq 'crypt' ) {
-
-    my $auth = from_crypt Authen::Passphrase $self->_password;
-    return $auth->match($check_password);
-
-  } elsif ( $self->_password_encoding eq 'plain' ) {
-
-    return $check_password eq $password;
-
+  #eventually should check a "password-encoding" field
+  if ( $password =~ /^(\*|!!?)$/ ) { #no self-service login
+    return 0;
+  } elsif ( length($password) < 13 ) { #plaintext
+    $check_password eq $password;
+  } elsif ( length($password) == 13 ) { #traditional DES crypt
+    crypt($check_password, $password) eq $password;
+  } elsif ( $password =~ /^\$1\$/ ) { #MD5 crypt
+    unix_md5_crypt($check_password, $password) eq $password;
+  } elsif ( $password =~ /^\$2a?\$/ ) { #Blowfish
+    warn "Can't check password: Blowfish encryption not yet supported, svcnum".
+         $self->svcnum. "\n";
+    0;
   } else {
-
-    #XXX this could be replaced with Authen::Passphrase stuff
-
-    if ( $password =~ /^(\*|!!?)$/ ) { #no self-service login
-      return 0;
-    } elsif ( length($password) < 13 ) { #plaintext
-      $check_password eq $password;
-    } elsif ( length($password) == 13 ) { #traditional DES crypt
-      crypt($check_password, $password) eq $password;
-    } elsif ( $password =~ /^\$1\$/ ) { #MD5 crypt
-      unix_md5_crypt($check_password, $password) eq $password;
-    } elsif ( $password =~ /^\$2a?\$/ ) { #Blowfish
-      warn "Can't check password: Blowfish encryption not yet supported, ".
-           "svcnum ".  $self->svcnum. "\n";
-      0;
-    } else {
-      warn "Can't check password: Unrecognized encryption for svcnum ".
-           $self->svcnum. "\n";
-      0;
-    }
-
+    warn "Can't check password: Unrecognized encryption for svcnum ".
+         $self->svcnum. "\n";
+    0;
   }
 
 }
@@ -2000,40 +2079,14 @@ database.
 
 sub crypt_password {
   my $self = shift;
-
-  if ( $self->_password_encoding eq 'ldap' ) {
-
-    if ( $self->_password =~ /^\{(PLAIN|CLEARTEXT)\}(.+)$/ ) {
-      my $plain = $2;
-
-      #XXX this could be replaced with Authen::Passphrase stuff
-
-      my $encryption = ( scalar(@_) && $_[0] ) ? shift : 'crypt';
-      if ( $encryption eq 'crypt' ) {
-        crypt(
-          $self->_password,
-          $saltset[int(rand(64))].$saltset[int(rand(64))]
-        );
-      } elsif ( $encryption eq 'md5' ) {
-        unix_md5_crypt( $self->_password );
-      } elsif ( $encryption eq 'blowfish' ) {
-        croak "unknown encryption method $encryption";
-      } else {
-        croak "unknown encryption method $encryption";
-      }
-
-    } elsif ( $self->_password =~ /^\{CRYPT\}(.+)$/ ) {
-      $1;
-    }
-
-  } elsif ( $self->_password_encoding eq 'crypt' ) {
-
-    return $self->_password;
-
-  } elsif ( $self->_password_encoding eq 'plain' ) {
-
-    #XXX this could be replaced with Authen::Passphrase stuff
-
+  #eventually should check a "password-encoding" field
+  if ( length($self->_password) == 13
+       || $self->_password =~ /^\$(1|2a?)\$/
+       || $self->_password =~ /^(\*|NP|\*LK\*|!!?)$/
+     )
+  {
+    $self->_password;
+  } else {
     my $encryption = ( scalar(@_) && $_[0] ) ? shift : 'crypt';
     if ( $encryption eq 'crypt' ) {
       crypt(
@@ -2047,44 +2100,14 @@ sub crypt_password {
     } else {
       croak "unknown encryption method $encryption";
     }
-
-  } else {
-
-    if ( length($self->_password) == 13
-         || $self->_password =~ /^\$(1|2a?)\$/
-         || $self->_password =~ /^(\*|NP|\*LK\*|!!?)$/
-       )
-    {
-      $self->_password;
-    } else {
-    
-      #XXX this could be replaced with Authen::Passphrase stuff
-
-      my $encryption = ( scalar(@_) && $_[0] ) ? shift : 'crypt';
-      if ( $encryption eq 'crypt' ) {
-        crypt(
-          $self->_password,
-          $saltset[int(rand(64))].$saltset[int(rand(64))]
-        );
-      } elsif ( $encryption eq 'md5' ) {
-        unix_md5_crypt( $self->_password );
-      } elsif ( $encryption eq 'blowfish' ) {
-        croak "unknown encryption method $encryption";
-      } else {
-        croak "unknown encryption method $encryption";
-      }
-
-    }
-
   }
-
 }
 
 =item ldap_password [ DEFAULT_ENCRYPTION_TYPE ]
 
 Returns an encrypted password in "LDAP" format, with a curly-bracked prefix
-describing the format, for example, "{PLAIN}himom", "{CRYPT}94pAVyK/4oIBk" or
-"{MD5}5426824942db4253f87a1009fd5d2d4".
+describing the format, for example, "{CRYPT}94pAVyK/4oIBk" or
+"{PLAIN-MD5}5426824942db4253f87a1009fd5d2d4f".
 
 The optional DEFAULT_ENCRYPTION_TYPE is not yet used, but the idea is for it
 to work the same as the B</crypt_password> method.
@@ -2094,71 +2117,34 @@ to work the same as the B</crypt_password> method.
 sub ldap_password {
   my $self = shift;
   #eventually should check a "password-encoding" field
-
-  if ( $self->_password_encoding eq 'ldap' ) {
-
-    return $self->_password;
-
-  } elsif ( $self->_password_encoding eq 'crypt' ) {
-
-    if ( length($self->_password) == 13 ) { #crypt
-      return '{CRYPT}'. $self->_password;
-    } elsif ( $self->_password =~ /^\$1\$(.*)$/ && length($1) == 31 ) { #passwdMD5
-      return '{MD5}'. $1;
-    #} elsif ( $self->_password =~ /^\$2a?\$(.*)$/ ) { #Blowfish
-    #  die "Blowfish encryption not supported in this context, svcnum ".
-    #      $self->svcnum. "\n";
-    } else {
-      warn "encryption method not (yet?) supported in LDAP context";
-      return '{CRYPT}*'; #unsupported, should not auth
-    }
-
-  } elsif ( $self->_password_encoding eq 'plain' ) {
-
+  if ( length($self->_password) == 13 ) { #crypt
+    return '{CRYPT}'. $self->_password;
+  } elsif ( $self->_password =~ /^\$1\$(.*)$/ && length($1) == 31 ) { #passwdMD5
+    return '{MD5}'. $1;
+  } elsif ( $self->_password =~ /^\$2a?\$(.*)$/ ) { #Blowfish
+    warn "Blowfish encryption not supported in this context, svcnum ".
+         $self->svcnum. "\n";
+    return '{CRYPT}*'; #unsupported, should not auth
+  } elsif ( $self->_password =~ /^(\w{48})$/ ) { #LDAP SSHA
+    return '{SSHA}'. $1;
+  } elsif ( $self->_password =~ /^(\w{64})$/ ) { #LDAP NS-MTA-MD5
+    return '{NS-MTA-MD5}'. $1;
+  } else { #plaintext
     return '{PLAIN}'. $self->_password;
-
-    #return '{CLEARTEXT}'. $self->_password; #?
-
-  } else {
-
-    if ( length($self->_password) == 13 ) { #crypt
-      return '{CRYPT}'. $self->_password;
-    } elsif ( $self->_password =~ /^\$1\$(.*)$/ && length($1) == 31 ) { #passwdMD5
-      return '{MD5}'. $1;
-    } elsif ( $self->_password =~ /^\$2a?\$(.*)$/ ) { #Blowfish
-      warn "Blowfish encryption not supported in this context, svcnum ".
-          $self->svcnum. "\n";
-      return '{CRYPT}*';
-
-    #are these two necessary anymore?
-    } elsif ( $self->_password =~ /^(\w{48})$/ ) { #LDAP SSHA
-      return '{SSHA}'. $1;
-    } elsif ( $self->_password =~ /^(\w{64})$/ ) { #LDAP NS-MTA-MD5
-      return '{NS-MTA-MD5}'. $1;
-
-    } else { #plaintext
-      return '{PLAIN}'. $self->_password;
-
-      #return '{CLEARTEXT}'. $self->_password; #?
-      
-      #XXX this could be replaced with Authen::Passphrase stuff if it gets used
-      #my $encryption = ( scalar(@_) && $_[0] ) ? shift : 'crypt';
-      #if ( $encryption eq 'crypt' ) {
-      #  return '{CRYPT}'. crypt(
-      #    $self->_password,
-      #    $saltset[int(rand(64))].$saltset[int(rand(64))]
-      #  );
-      #} elsif ( $encryption eq 'md5' ) {
-      #  unix_md5_crypt( $self->_password );
-      #} elsif ( $encryption eq 'blowfish' ) {
-      #  croak "unknown encryption method $encryption";
-      #} else {
-      #  croak "unknown encryption method $encryption";
-      #}
-    }
-
+    #my $encryption = ( scalar(@_) && $_[0] ) ? shift : 'crypt';
+    #if ( $encryption eq 'crypt' ) {
+    #  return '{CRYPT}'. crypt(
+    #    $self->_password,
+    #    $saltset[int(rand(64))].$saltset[int(rand(64))]
+    #  );
+    #} elsif ( $encryption eq 'md5' ) {
+    #  unix_md5_crypt( $self->_password );
+    #} elsif ( $encryption eq 'blowfish' ) {
+    #  croak "unknown encryption method $encryption";
+    #} else {
+    #  croak "unknown encryption method $encryption";
+    #}
   }
-
 }
 
 =item domain_slash_username
@@ -2387,7 +2373,6 @@ sub reached_threshold {
 
       my $to = join(', ', grep { $_ !~ /^(POST|FAX)$/ } 
                                $cust_main->invoicing_list,
-                               $svc_acct->email,
                                ($opt{'to'} ? $opt{'to'} : ())
                    );
 
@@ -2402,8 +2387,12 @@ sub reached_threshold {
                         'last'      => $cust_main->getfield('last'),
                         'pkg'       => $cust_pkg->part_pkg->pkg,
                         'column'    => $opt{'column'},
-                        'amount'    => $svc_acct->getfield($opt{'column'}),
-                        'threshold' => $threshold,
+                        '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,
                       } );
 
 
@@ -2447,5 +2436,61 @@ schema.html from the base documentation.
 
 =cut
 
+=item domain_select_hash %OPTIONS
+
+Returns a hash SVCNUM => DOMAIN ...  representing the domains this customer
+may at present purchase.
+
+Currently available options are: I<pkgnum> I<svcpart>
+
+=cut
+
+sub domain_select_hash {
+  my ($self, %options) = @_;
+  my %domains = ();
+  my $part_svc;
+  my $cust_pkg;
+
+  if (ref($self)) {
+    $part_svc = $self->part_svc;
+    $cust_pkg = $self->cust_svc->cust_pkg
+      if $self->cust_svc;
+  }
+
+  $part_svc = qsearchs('part_svc', { 'svcpart' => $options{svcpart} })
+    if $options{'svcpart'};
+
+  $cust_pkg = qsearchs('cust_pkg', { 'pkgnum' => $options{pkgnum} })
+    if $options{'pkgnum'};
+
+  if ($part_svc && ( $part_svc->part_svc_column('domsvc')->columnflag eq 'S'
+                  || $part_svc->part_svc_column('domsvc')->columnflag eq 'F')) {
+    %domains = map { $_->svcnum => $_->domain }
+               map { qsearchs('svc_domain', { 'svcnum' => $_ }) }
+               split(',', $part_svc->part_svc_column('domsvc')->columnvalue);
+  }elsif ($cust_pkg && !$conf->exists('svc_acct-alldomains') ) {
+    %domains = map { $_->svcnum => $_->domain }
+               map { qsearchs('svc_domain', { 'svcnum' => $_->svcnum }) }
+               map { qsearch('cust_svc', { 'pkgnum' => $_->pkgnum } ) }
+               qsearch('cust_pkg', { 'custnum' => $cust_pkg->custnum });
+  }else{
+    %domains = map { $_->svcnum => $_->domain } qsearch('svc_domain', {} );
+  }
+
+  if ($part_svc && $part_svc->part_svc_column('domsvc')->columnflag eq 'D') {
+    my $svc_domain = qsearchs('svc_domain',
+      { 'svcnum' => $part_svc->part_svc_column('domsvc')->columnvalue } );
+    if ( $svc_domain ) {
+      $domains{$svc_domain->svcnum}  = $svc_domain->domain;
+    }else{
+      warn "unknown svc_domain.svcnum for part_svc_column domsvc: ".
+           $part_svc->part_svc_column('domsvc')->columnvalue;
+
+    }
+  }
+
+  (%domains);
+}
+
 1;