communigate: domain aliases, enabled services, RT#7083
[freeside.git] / FS / FS / svc_acct.pm
index 649a783..18b9af5 100644 (file)
@@ -38,6 +38,7 @@ 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;
 use FS::queue;
 use FS::radius_usergroup;
@@ -161,45 +162,71 @@ FS::svc_Common.  The following fields are currently supported:
 
 =over 4
 
-=item svcnum - primary key (assigned automatcially for new accounts)
+=item svcnum
+
+Primary key (assigned automatcially for new accounts)
 
 =item username
 
-=item _password - generated if blank
+=item _password
+
+generated if blank
+
+=item _password_encoding
+
+plain, crypt, ldap (or empty for autodetection)
+
+=item sec_phrase
 
-=item _password_encoding - plain, crypt, ldap (or empty for autodetection)
+security phrase
 
-=item sec_phrase - security phrase
+=item popnum
 
-=item popnum - Point of presence (see L<FS::svc_acct_pop>)
+Point of presence (see L<FS::svc_acct_pop>)
 
 =item uid
 
 =item gid
 
-=item finger - GECOS
+=item finger
 
-=item dir - set automatically if blank (and uid is not)
+GECOS
+
+=item dir
+
+set automatically if blank (and uid is not)
 
 =item shell
 
-=item quota - (unimplementd)
+=item quota
+
+=item slipip
 
-=item slipip - IP address
+IP address
 
-=item seconds - 
+=item seconds
 
-=item upbytes - 
+=item upbytes
 
-=item downbytes - 
+=item downbyte
 
-=item totalbytes - 
+=item totalbytes
+
+=item domsvc
 
-=item domsvc - svcnum from svc_domain
+svcnum from svc_domain
 
-=item radius_I<Radius_Attribute> - I<Radius-Attribute> (reply)
+=item pbxsvc
 
-=item rc_I<Radius_Attribute> - I<Radius-Attribute> (check)
+Optional svcnum from svc_pbx
+
+=item radius_I<Radius_Attribute>
+
+I<Radius-Attribute> (reply)
+
+=item rc_I<Radius_Attribute>
+
+I<Radius-Attribute> (check)
 
 =back
 
@@ -244,8 +271,58 @@ sub table_info {
                          disable_fixed => 1,
                          disable_select => 1,
                        },
+        'cgp_type'=> { 
+                       label => 'Communigate account type',
+                       type => 'select',
+                       select_list => [qw( MultiMailbox TextMailbox MailDirMailbox AGrade BGrade CGrade )],
+                       disable_inventory => 1,
+                       disable_select    => 1,
+                     },
+        'cgp_accessmodes' => { 
+                               label => 'Communigate enabled services',
+                               type  => 'communigate_pro-accessmodes',
+                               disable_inventory => 1,
+                               disable_select    => 1,
+                             },
+        'cgp_aliases' => { 
+                           label => 'Communigate aliases',
+                           type  => 'text',
+                           disable_inventory => 1,
+                           disable_select    => 1,
+                         },
+        'cgp_deletemode' => { 
+                              label => 'Communigate message delete method',
+                              type  => 'select',
+                              select_list => [ 'Move To Trash', 'Immediately', 'Mark' ],
+                              disable_inventory => 1,
+                              disable_select    => 1,
+                            },
+        'cgp_emptytrash' => { 
+                              label => 'Communigate on logout remove trash',
+                              type  => 'text',
+                              disable_inventory => 1,
+                              disable_select    => 1,
+                            },
         'quota'     => { 
-                         label => 'Quota',
+                         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,
@@ -275,6 +352,20 @@ sub table_info {
                          disable_inventory => 1,
 
                        },
+        'domsvc'    => {
+                         label     => 'Domain',
+                         type      => 'select',
+                         select_table => 'svc_domain',
+                         select_key   => 'svcnum',
+                         select_label => 'domain',
+                         disable_inventory => 1,
+
+                       },
+        'pbxsvc'    => { label => 'PBX',
+                         type  => 'select-svc_pbx.html',
+                         disable_inventory => 1,
+                         disable_select => 1, #UI wonky, pry works otherwise
+                       },
         'usergroup' => {
                          label => 'RADIUS groups',
                          type  => 'radius_usergroup_selector',
@@ -436,13 +527,7 @@ sub search_sql {
       $class->search_sql_field('username', $string ).
     ' ) ';
   } else {
-    ' ( '.
-      $class->search_sql_field('username', $string).
-      ( $string =~ /^\d+$/
-          ? 'OR '. $class->search_sql_field('svcnum', $string)
-          : ''
-      ).
-    ' ) ';
+    $class->search_sql_field('username', $string);
   }
 }
 
@@ -662,13 +747,16 @@ sub insert {
 }
 
 # set usage fields and thresholds if unset but set in a package def
+# AND the package already has a last bill date (otherwise they get double added)
 sub preinsert_hook_first {
   my $self = shift;
 
   return '' unless $self->pkgnum;
 
   my $cust_pkg = qsearchs( 'cust_pkg', { 'pkgnum' => $self->pkgnum } );
-  my $part_pkg = $cust_pkg->part_pkg if $cust_pkg;
+  return '' unless $cust_pkg && $cust_pkg->last_bill;
+
+  my $part_pkg = $cust_pkg->part_pkg;
   return '' unless $part_pkg && $part_pkg->can('usage_valuehash');
 
   my %values = $part_pkg->usage_valuehash;
@@ -1017,15 +1105,21 @@ sub check {
 
   my $error = $self->ut_numbern('svcnum')
               #|| $self->ut_number('domsvc')
-              || $self->ut_foreign_key('domsvc', 'svc_domain', 'svcnum' )
+              || $self->ut_foreign_key( 'domsvc', 'svc_domain', 'svcnum' )
+              || $self->ut_foreign_keyn('pbxsvc', 'svc_pbx',    'svcnum' )
               || $self->ut_textn('sec_phrase')
               || $self->ut_snumbern('seconds')
               || $self->ut_snumbern('upbytes')
               || $self->ut_snumbern('downbytes')
               || $self->ut_snumbern('totalbytes')
-              || $self->ut_enum( '_password_encoding',
-                                 [ '', qw( plain crypt ldap ) ]
-                               )
+              || $self->ut_enum('_password_encoding', ['',qw(plain crypt ldap)])
+              || $self->ut_enum('password_selfchange', [ '', 'Y' ])
+              || $self->ut_enum('password_recover',    [ '', 'Y' ])
+              || $self->ut_textn('cgp_accessmodes')
+              || $self->ut_alphan('cgp_type')
+              || $self->ut_textn('cgp_aliases' ) #well
+              || $self->ut_alphasn('cgp_deletemode')
+              || $self->ut_alphan('cgp_emptytrash')
   ;
   return $error if $error;
 
@@ -1161,8 +1255,12 @@ sub check {
       or return "Illegal finger: ". $self->getfield('finger');
   $self->setfield('finger', $1);
 
-  $recref->{quota} =~ /^(\w*)$/ or return "Illegal quota";
-  $recref->{quota} = $1;
+  for (qw( quota file_quota file_maxsize )) {
+    $recref->{$_} =~ /^(\w*)$/ or return "Illegal $_";
+    $recref->{$_} = $1;
+  }
+  $recref->{file_maxnum} =~ /^\s*(\d*)\s*$/ or return "Illegal file_maxnum";
+  $recref->{file_maxnum} = $1;
 
   unless ( $part_svc->part_svc_column('slipip')->columnflag eq 'F' ) {
     if ( $recref->{slipip} eq '' ) {
@@ -1184,13 +1282,14 @@ sub check {
 
   # First, if _password is blank, generate one and set default encoding.
   if ( ! $recref->{_password} ) {
-    $self->set_password('');
+    $error = $self->set_password('');
   }
   # But if there's a _password but no encoding, assume it's plaintext and 
   # set it to default encoding.
   elsif ( ! $recref->{_password_encoding} ) {
-    $self->set_password($recref->{_password});
+    $error = $self->set_password($recref->{_password});
   }
+  return $error if $error;
 
   # Next, check _password to ensure compliance with the encoding.
   if ( $recref->{_password_encoding} eq 'ldap' ) {
@@ -1232,11 +1331,8 @@ sub check {
       $recref->{_password} =~ /\!/ and return gettext('illegal_password');
     }
   }
-  elsif ( $recref->{_password_encoding} eq 'legacy' ) {
-    # this happens when set_password fails
-    return gettext('illegal_password'). " $passwordmin-$passwordmax ".
-           FS::Msgcat::_gettext('illegal_password_characters').
-           ": ". $recref->{_password};
+  else {
+    return "invalid password encoding ('".$recref->{_password_encoding}."'";
   }
   $self->SUPER::check;
 
@@ -1297,73 +1393,81 @@ is >0), one will be generated randomly.
 =cut
 
 sub set_password {
-  my $self = shift;
-  my $pass = shift;
-  my ($encoding, $encryption);
+  my( $self, $pass ) = ( shift, shift );
+
+  warn "[$me] set_password (to $pass) called on $self: ". Dumper($self)
+     if $DEBUG;
+
+  my $failure = gettext('illegal_password'). " $passwordmin-$passwordmax ".
+                FS::Msgcat::_gettext('illegal_password_characters').
+                ": ". $pass;
 
+  my( $encoding, $encryption ) = ('', '');
 
-  if($self->_password_encoding) {
+  if ( $self->_password_encoding ) {
     $encoding = $self->_password_encoding;
     # identify existing encryption method, try to use it.
     $encryption = $self->_password_encryption;
-    if(!$encryption) {
+    if (!$encryption) {
       # use the system default
       undef $encoding;
     }
   }
 
-  if(!$encoding) {
+  if ( !$encoding ) {
     # set encoding to system default
-    ($encoding, $encryption) = split(/-/, lc($conf->config('default-password-encoding')));
+    ($encoding, $encryption) =
+      split(/-/, lc($conf->config('default-password-encoding')));
     $encoding ||= 'legacy';
     $self->_password_encoding($encoding);
   }
 
-  if($encoding eq 'legacy') {
+  if ( $encoding eq 'legacy' ) {
+
     # The legacy behavior from check():
     # If the password is blank, randomize it and set encoding to 'plain'.
     if(!defined($pass) or (length($pass) == 0 and $passwordmin)) {
       $pass = join('',map($pw_set[ int(rand $#pw_set) ], (0..7) ) );
       $self->_password_encoding('plain');
-    }
-    else {
+    } else {
       # Prefix + valid-length password
       if ( $pass =~ /^((\*SUSPENDED\* |!!?)?)([^\t\n]{$passwordmin,$passwordmax})$/ ) {
         $pass = $1.$3;
         $self->_password_encoding('plain');
-      }
       # Prefix + crypt string
-      elsif ( $pass =~ /^((\*SUSPENDED\* |!!?)?)([\w\.\/\$\;\+]{13,64})$/ ) {
+      elsif ( $pass =~ /^((\*SUSPENDED\* |!!?)?)([\w\.\/\$\;\+]{13,64})$/ ) {
         $pass = $1.$3;
         $self->_password_encoding('crypt');
-      }
       # Various disabled crypt passwords
-      elsif ( $pass eq '*' or
-              $pass eq '!' or
-              $pass eq '!!' ) {
+      } elsif ( $pass eq '*' || $pass eq '!' || $pass eq '!!' ) {
         $self->_password_encoding('crypt');
+      } else {
+        return $failure;
       }
-      else {
-        # do nothing; check() will recognize this as an error
-      }
-   }
+    }
+
+    $self->_password($pass);
+    return;
+
   }
-  elsif($encoding eq 'crypt') {
-    if($encryption eq 'md5') {
+
+  return $failure
+    if $passwordmin && length($pass) < $passwordmin
+    or $passwordmax && length($pass) > $passwordmax;
+
+  if ( $encoding eq 'crypt' ) {
+    if ($encryption eq 'md5') {
       $pass = unix_md5_crypt($pass);
-    }
-    elsif($encryption eq 'des') {
+    } elsif ($encryption eq 'des') {
       $pass = crypt($pass, $saltset[int(rand(64))].$saltset[int(rand(64))]);
     }
-  }
-  elsif($encoding eq 'ldap') {
-    if($encryption eq 'md5') {
+
+  } elsif ( $encoding eq 'ldap' ) {
+    if ($encryption eq 'md5') {
       $pass = md5_base64($pass);
-    }
-    elsif($encryption eq 'sha1') {
+    } elsif ($encryption eq 'sha1') {
       $pass = sha1_base64($pass);
-    }
-    elsif($encryption eq 'crypt') {
+    } elsif ($encryption eq 'crypt') {
       $pass = crypt($pass, $saltset[int(rand(64))].$saltset[int(rand(64))]);
     }
     # else $encryption eq 'plain', do nothing
@@ -1631,30 +1735,20 @@ for the password.
 sub radius_password {
   my $self = shift;
 
-  my($pw_attrib, $password);
+  my $pw_attrib;
   if ( $self->_password_encoding eq 'ldap' ) {
-
     $pw_attrib = 'Password-With-Header';
-    $password = $self->_password;
-
   } elsif ( $self->_password_encoding eq 'crypt' ) {
-
     $pw_attrib = 'Crypt-Password';
-    $password = $self->_password;
-
   } elsif ( $self->_password_encoding eq 'plain' ) {
-
-    $pw_attrib = $radius_password; #Cleartext-Password?  man rlm_pap
-    $password = $self->_password;
-
+    $pw_attrib = $radius_password;
   } else {
-
-    $pw_attrib = length($password) <= 12 ? $radius_password : 'Crypt-Password';
-    $password = $self->_password;
-
+    $pw_attrib = length($self->_password) <= 12
+                   ? $radius_password
+                   : 'Crypt-Password';
   }
 
-  ($pw_attrib, $password);
+  ($pw_attrib, $self->_password);
 
 }
 
@@ -2117,8 +2211,8 @@ sub set_usage {
   #$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);
+  local($FS::Record::nowarn_identical) = 1;
+  my $error = $new->replace($self); #call exports
   if ( $error ) {
     $dbh->rollback if $oldAutoCommit;
     return "Error replacing: $error";