1<?php 2# $Id$ 3 4/** 5 * Simple class to represent a user. 6 */ 7class MailboxHandler extends PFAHandler 8{ 9 protected $db_table = 'mailbox'; 10 protected $id_field = 'username'; 11 protected $domain_field = 'domain'; 12 protected $searchfields = array('username'); 13 14 # init $this->struct, $this->db_table and $this->id_field 15 protected function initStruct() 16 { 17 $passwordReset = (int) ( Config::bool('forgotten_user_password_reset') && !Config::read('mailbox_postpassword_script') ); 18 $reset_by_sms = 0; 19 if ($passwordReset && Config::read_string('sms_send_function')) { 20 $reset_by_sms = 1; 21 } 22 $editPw = 1; 23 if (!$this->new && Config::read('mailbox_postpassword_script')) { 24 $editPw = 0; 25 } 26 27 $this->struct = array( 28 29 # field name allow display in... type $PALANG label $PALANG description default / options / ... 30 # editing? form list 31 'username' => pacol($this->new, 1, 1, 'mail', 'pEdit_mailbox_username' , '' , '' ), 32 'local_part' => pacol($this->new, 0, 0, 'text', 'pEdit_mailbox_username' , '' , '' ), 33 'domain' => pacol($this->new, 0, 1, 'enum', '' , '' , '', 34 /*options*/ $this->allowed_domains ), 35 # TODO: maildir: display in list is needed to include maildir in SQL result (for post_edit hook) 36 # TODO: (not a perfect solution, but works for now - maybe we need a separate "include in SELECT query" field?) 37 'maildir' => pacol($this->new, 0, 1, 'text', '' , '' , '' ), 38 'password' => pacol($editPw, $editPw,0, 'pass', 'password' , 'pCreate_mailbox_password_text' , '' ), 39 'password2' => pacol($editPw, $editPw,0, 'pass', 'password_again' , '' , '', 40 /*options*/ array(), 41 /*not_in_db*/ 0, 42 /*dont_write_to_db*/ 1, 43 /*select*/ 'password as password2' 44 ), 45 'name' => pacol(1, 1, 1, 'text', 'name' , 'pCreate_mailbox_name_text' , '' ), 46 'quota' => pacol(1, 1, 1, 'int' , 'pEdit_mailbox_quota' , 'pEdit_mailbox_quota_text' , '' ), # in MB 47 # read_from_db_postprocess() also sets 'quotabytes' for use in init() 48 # TODO: read used quota from quota/quota2 table 49 'active' => pacol(1, 1, 1, 'bool', 'active' , '' , 1 ), 50 'welcome_mail' => pacol($this->new, $this->new, 0, 'bool', 'pCreate_mailbox_mail' , '' , 1, 51 /*options*/ array(), 52 /*not_in_db*/ 1 ), 53 'phone' => pacol(1, $reset_by_sms, 0, 'text', 'pCreate_mailbox_phone' , 'pCreate_mailbox_phone_desc' , ''), 54 'email_other' => pacol(1, $passwordReset, 0, 'mail', 'pCreate_mailbox_email' , 'pCreate_mailbox_email_desc' , ''), 55 'token' => pacol(1, 0, 0, 'text', '' , '' ), 56 'token_validity' => pacol(1, 0, 0, 'ts', '' , '', date("Y-m-d H:i:s",time())), 57 'created' => pacol(0, 0, 1, 'ts', 'created' , '' ), 58 'modified' => pacol(0, 0, 1, 'ts', 'last_modified' , '' ), 59 'password_expiry' => pacol(0, 0, 1, 'ts', 'password_expiration' , '' ), 60 # TODO: add virtual 'notified' column and allow to display who received a vacation response? 61 ); 62 63 # update allowed quota 64 if (count($this->struct['domain']['options']) > 0) { 65 $this->prefill('domain', $this->struct['domain']['options'][0]); 66 } 67 } 68 69 public function init(string $id) : bool 70 { 71 if (!parent::init($id)) { 72 return false; 73 } 74 75 if ($this->new) { 76 $currentquota = 0; 77 } else { 78 $currentquota = $this->result['quotabytes']; # parent::init called ->view() 79 } 80 81 $this->updateMaxquota($this->domain, $currentquota); 82 83 return true; # still here? good. 84 } 85 86 protected function domain_from_id() 87 { 88 list(/*NULL*/, $domain) = explode('@', $this->id); 89 return $domain; 90 } 91 92 /** 93 * show max allowed quota in quota field description 94 * @param string - domain 95 * @param int - current quota 96 */ 97 protected function updateMaxquota($domain, $currentquota) 98 { 99 if ($domain == '') { 100 return false; 101 } 102 103 $maxquota = $this->allowed_quota($domain, $currentquota); 104 105 if ($maxquota == 0) { 106 # TODO: show 'unlimited' 107 # } elseif ($maxquota < 0) { 108 # TODO: show 'disabled' - at the moment, just shows '-1' 109 } else { 110 $this->struct['quota']['desc'] = Config::lang_f('mb_max', "" . $maxquota); 111 } 112 } 113 114 protected function initMsg() 115 { 116 $this->msg['error_already_exists'] = 'email_address_already_exists'; 117 $this->msg['error_does_not_exist'] = 'pCreate_mailbox_username_text_error1'; 118 $this->msg['confirm_delete'] = 'confirm_delete_mailbox'; 119 120 if ($this->new) { 121 $this->msg['logname'] = 'create_mailbox'; 122 $this->msg['store_error'] = 'pCreate_mailbox_result_error'; 123 $this->msg['successmessage'] = 'pCreate_mailbox_result_success'; 124 } else { 125 $this->msg['logname'] = 'edit_mailbox'; 126 $this->msg['store_error'] = 'mailbox_update_failed'; 127 $this->msg['successmessage'] = 'mailbox_updated'; 128 } 129 } 130 131 public function webformConfig() 132 { 133 if ($this->new) { # the webform will display a local_part field + domain dropdown on $new 134 $this->struct['username']['display_in_form'] = 0; 135 $this->struct['local_part']['display_in_form'] = 1; 136 $this->struct['domain']['display_in_form'] = 1; 137 } 138 139 return array( 140 # $PALANG labels 141 'formtitle_create' => 'pCreate_mailbox_welcome', 142 'formtitle_edit' => 'pEdit_mailbox_welcome', 143 'create_button' => 'add_mailbox', 144 145 # various settings 146 'required_role' => 'admin', 147 'listview' => 'list-virtual.php', 148 'early_init' => 0, 149 'prefill' => array('domain'), 150 ); 151 } 152 153 154 protected function validate_new_id() 155 { 156 if ($this->id == '') { 157 $this->errormsg[$this->id_field] = Config::lang('pCreate_mailbox_username_text_error1'); 158 return false; 159 } 160 161 $email_check = check_email($this->id); 162 if ($email_check != '') { 163 $this->errormsg[$this->id_field] = $email_check; 164 return false; 165 } 166 167 list(/*NULL*/, $domain) = explode('@', $this->id); 168 169 if (!$this->create_allowed($domain)) { 170 $this->errormsg[] = Config::lang('pCreate_mailbox_username_text_error3'); 171 return false; 172 } 173 174 # check if an alias with this name already exists - if yes, don't allow to create the mailbox 175 $handler = new AliasHandler(1); 176 $handler->calledBy('MailboxHandler'); # make sure mailbox creation still works if the alias limit for the domain is hit 177 178 if (!$handler->init($this->id)) { 179 # TODO: keep original error message from AliasHandler 180 $this->errormsg[] = Config::lang('email_address_already_exists'); 181 return false; 182 } 183 184 return true; # still here? good! 185 } 186 187 /** 188 * check number of existing mailboxes for this domain - is one more allowed? 189 */ 190 private function create_allowed($domain) 191 { 192 $limit = get_domain_properties($domain); 193 194 if ($limit['mailboxes'] == 0) { 195 return true; 196 } # unlimited 197 if ($limit['mailboxes'] < 0) { 198 return false; 199 } # disabled 200 if ($limit['mailbox_count'] >= $limit['mailboxes']) { 201 return false; 202 } 203 return true; 204 } 205 206 /** 207 * merge local_part and domain to address 208 * called by edit.php (if id_field is editable and hidden in editform) _before_ ->init 209 */ 210 public function mergeId($values) 211 { 212 if ($this->struct['local_part']['display_in_form'] == 1 && $this->struct['domain']['display_in_form']) { # webform mode - combine to 'address' field 213 return $values['local_part'] . '@' . $values['domain']; 214 } else { 215 return $values[$this->id_field]; 216 } 217 } 218 219 220 protected function read_from_db_postprocess($db_result) 221 { 222 foreach ($db_result as $key => $row) { 223 if (isset($row['quota']) && is_numeric($row['quota']) && $row['quota'] > -1) { # quota could be disabled in $struct 224 $db_result[$key]['quotabytes'] = $row['quota']; 225 $db_result[$key]['quota'] = divide_quota( (int) $row['quota']); # convert quota to MB 226 } else { 227 $db_result[$key]['quotabytes'] = -1; 228 $db_result[$key]['quota'] = -1; 229 } 230 } 231 return $db_result; 232 } 233 234 235 protected function preSave() : bool 236 { 237 if (isset($this->values['quota']) && $this->values['quota'] != -1 && is_numeric($this->values['quota'])) { 238 $multiplier = Config::read_string('quota_multiplier'); 239 if ($multiplier == 0 || !is_numeric($multiplier)) { // or empty string, or null, or false... 240 $multiplier = 1; 241 } 242 $this->values['quota'] = $this->values['quota'] * $multiplier; # convert quota from MB to bytes 243 } 244 245 // Avoid trying to store '' in an integer field 246 if ($this->values['quota'] === '') { 247 $this->values['quota'] = 0; 248 } 249 250 $ah = new AliasHandler($this->new, $this->admin_username); 251 252 $ah->calledBy('MailboxHandler'); 253 254 if (!$ah->init($this->id)) { 255 $arraykeys = array_keys($ah->errormsg); 256 $this->errormsg[] = $ah->errormsg[$arraykeys[0]]; # TODO: implement this as PFAHandler->firstErrormsg() 257 return false; 258 } 259 260 $alias_data = array(); 261 262 if (isset($this->values['active'])) { # might not be set in edit mode 263 $alias_data['active'] = $this->values['active']; 264 } 265 266 if ($this->new) { 267 $alias_data['goto'] = array($this->id); # 'goto_mailbox' = 1; # would be technically correct, but setting 'goto' is easier 268 } 269 270 if (!$ah->set($alias_data)) { 271 $this->errormsg[] = $ah->errormsg[0]; 272 return false; 273 } 274 275 if (!$ah->save()) { 276 $this->errormsg[] = $ah->errormsg[0]; 277 return false; 278 } 279 280 if (!empty($this->values['password'])) { 281 // provide some default value to keep MySQL etc happy. 282 $this->values['password_expiry'] = date('Y-m-d H:i', strtotime("+365 days")); 283 if (Config::bool('password_expiration')) { 284 $domain_dirty = $this->domain_from_id(); 285 $domain = trim($domain_dirty, "`'"); // naive assumption it is ' escaping. 286 $password_expiration_value = (int)get_password_expiration_value($domain); 287 $this->values['password_expiry'] = date('Y-m-d H:i', strtotime("+$password_expiration_value day")); 288 } 289 } 290 291 return true; 292 } 293 294 protected function setmore(array $values) 295 { 296 if (array_key_exists('quota', $this->values)) { 297 $this->values['quota'] = (int)$this->values['quota']; 298 } 299 } 300 301 // Could perhaps also use _validate_local_part($new_value) { .... } 302 public function set(array $values) 303 { 304 // See: https://github.com/postfixadmin/postfixadmin/issues/282 - ensure the 'local_part' does not contain an @ sign. 305 $ok = true; 306 if (isset($values['local_part']) && strpos($values['local_part'], '@')) { 307 $this->errormsg['local_part'] = Config::lang('pCreate_mailbox_local_part_error'); 308 $ok = false; 309 } 310 return $ok && parent::set($values); 311 } 312 313 protected function postSave() : bool 314 { 315 if ($this->new) { 316 if (!$this->mailbox_post_script()) { 317 # return false; # TODO: should this be fatal? 318 } 319 320 if ($this->values['welcome_mail'] == true) { 321 if (!$this->send_welcome_mail()) { 322 # return false; # TODO: should this be fatal? 323 } 324 } 325 326 if (!$this->create_mailbox_subfolders()) { 327 $this->infomsg[] = Config::lang_f('pCreate_mailbox_result_succes_nosubfolders', $this->id); 328 } 329 } else { # edit mode 330 # alias active status is updated in before_store() 331 332 # postedit hook 333 # TODO: implement a poststore() function? - would make handling of old and new values much easier... 334 335 $old_mh = new MailboxHandler(); 336 337 if (!$old_mh->init($this->id)) { 338 $this->errormsg[] = $old_mh->errormsg[0]; 339 } elseif (!$old_mh->view()) { 340 $this->errormsg[] = $old_mh->errormsg[0]; 341 } else { 342 $oldvalues = $old_mh->result(); 343 344 $this->values['maildir'] = $oldvalues['maildir']; 345 346 if (isset($this->values['quota'])) { 347 $quota = $this->values['quota']; 348 } else { 349 $quota = $oldvalues['quota']; 350 } 351 352 if (!$this->mailbox_post_script()) { 353 # TODO: should this be fatal? 354 } 355 } 356 } 357 return true; # even if a hook failed, mark the overall operation as OK 358 } 359 360 public function delete() 361 { 362 if (! $this->view()) { 363 $this->errormsg[] = Config::Lang('pFetchmail_invalid_mailbox'); # TODO: can users hit this message at all? init() should already fail... 364 return false; 365 } 366 367 # the correct way would be to delete the alias and fetchmail entries with *Handler before 368 # deleting the mailbox, but it's easier and a bit faster to do it on the database level. 369 # cleaning up all tables doesn't hurt, even if vacation or displaying the quota is disabled 370 371 db_delete('fetchmail', 'mailbox', $this->id); 372 db_delete('vacation', 'email', $this->id); 373 db_delete('vacation_notification', 'on_vacation', $this->id); # should be caught by cascade, if PgSQL 374 db_delete('quota', 'username', $this->id); 375 db_delete('quota2', 'username', $this->id); 376 db_delete('alias', 'address', $this->id); 377 db_delete($this->db_table, $this->id_field, $this->id); # finally delete the mailbox 378 379 if (!$this->mailbox_postdeletion()) { 380 $this->errormsg[] = Config::Lang('mailbox_postdel_failed'); 381 } 382 383 list(/*NULL*/, $domain) = explode('@', $this->id); 384 db_log($domain, 'delete_mailbox', $this->id); 385 $this->infomsg[] = Config::Lang_f('pDelete_delete_success', $this->id); 386 return true; 387 } 388 389 390 391 protected function _prefill_domain($field, $val) 392 { 393 if (in_array($val, $this->struct[$field]['options'])) { 394 $this->struct[$field]['default'] = $val; 395 $this->updateMaxquota($val, 0); 396 } 397 } 398 399 /** 400 * check if quota is allowed 401 */ 402 protected function _validate_quota($field, $val) 403 { 404 if (!$this->check_quota($val)) { 405 $this->errormsg[$field] = Config::lang('pEdit_mailbox_quota_text_error'); 406 return false; 407 } 408 return true; 409 } 410 411 /** 412 * - compare password / password2 field (error message will be displayed at password2 field) 413 * - autogenerate password if enabled in config and $new 414 * - display password on $new if enabled in config or autogenerated 415 */ 416 protected function _validate_password($field, $val) 417 { 418 if (!$this->_validate_password2($field, $val)) { 419 return false; 420 } 421 422 if ($this->new && Config::read('generate_password') == 'YES' && $val == '') { 423 # auto-generate new password 424 unset($this->errormsg[$field]); # remove "password too short" error message 425 $val = generate_password(); 426 $this->values[$field] = $val; # we are doing this "behind the back" of set() 427 $this->infomsg[] = Config::Lang('password') . ": $val"; 428 return false; # to avoid that set() overwrites $this->values[$field] 429 } elseif ($this->new && Config::read('show_password') == 'YES') { 430 $this->infomsg[] = Config::Lang('password') . ": $val"; 431 } 432 433 return true; # still here? good. 434 } 435 436 /** 437 * compare password / password2 field 438 * error message will be displayed at the password2 field 439 */ 440 protected function _validate_password2($field, $val) 441 { 442 return $this->compare_password_fields('password', 'password2'); 443 } 444 445 /** 446 * on $this->new, set localpart based on address 447 */ 448 protected function _missing_local_part($field) 449 { 450 list($local_part, $domain) = explode('@', $this->id); 451 $this->RAWvalues['local_part'] = $local_part; 452 } 453 454 /** 455 * on $this->new, set domain based on address 456 */ 457 protected function _missing_domain($field) 458 { 459 list($local_part, $domain) = explode('@', $this->id); 460 $this->RAWvalues['domain'] = $domain; 461 } 462 463 # TODO: read used quota from quota/quota2 table, then enable _formatted_quota() 464 # public function _formatted_quota ($item) { return $item['used_quota'] . ' / ' . $item['quota'] ; } 465 466 467 468 /** 469 * calculate maildir path for the mailbox 470 */ 471 protected function _missing_maildir($field) 472 { 473 list($local_part, $domain) = explode('@', $this->id); 474 475 $maildir_name_hook = Config::read('maildir_name_hook'); 476 477 if (is_string($maildir_name_hook) && $maildir_name_hook != 'NO' && function_exists($maildir_name_hook)) { 478 $maildir = $maildir_name_hook($domain, $this->id); 479 } elseif (Config::bool('domain_path')) { 480 if (Config::bool('domain_in_mailbox')) { 481 $maildir = $domain . "/" . $this->id . "/"; 482 } else { 483 $maildir = $domain . "/" . $local_part . "/"; 484 } 485 } else { 486 # If $CONF['domain_path'] is set to NO, $CONF['domain_in_mailbox] is forced to YES. 487 # Otherwise user@example.com and user@foo.bar would be mixed up in the same maildir "user/". 488 $maildir = $this->id . "/"; 489 } 490 $this->RAWvalues['maildir'] = $maildir; 491 } 492 493 private function send_welcome_mail() 494 { 495 $fTo = $this->id; 496 $fFrom = smtp_get_admin_email(); 497 if (empty($fFrom) || $fFrom == 'CLI') { 498 $fFrom = $this->id; 499 } 500 $fSubject = Config::lang('pSendmail_subject_text'); 501 $fBody = Config::read('welcome_text'); 502 503 if (!smtp_mail($fTo, $fFrom, $fSubject, smtp_get_admin_password(), $fBody)) { 504 $this->errormsg[] = Config::lang_f('pSendmail_result_error', $this->id); 505 return false; 506 } 507 508 return true; 509 } 510 511 512 /** 513 * Check if the user is creating a mailbox within the quota limits of the domain 514 * 515 * @param int $quota - quota wanted for the mailbox 516 * @return boolean - true if requested quota is OK, otherwise false 517 * @todo merge with allowed_quota? 518 */ 519 protected function check_quota($quota) 520 { 521 if (!Config::bool('quota')) { 522 return true; # enforcing quotas is disabled - just allow it 523 } 524 525 $quota = (int) $quota; 526 527 list(/*NULL*/, $domain) = explode('@', $this->id); 528 $limit = get_domain_properties($domain); 529 530 if (($limit['maxquota'] < 0) and ($quota < 0)) { 531 return true; # maxquota and $quota are both disabled -> OK, no need for more checks 532 } 533 534 if (($limit['maxquota'] > 0) and ($quota == 0)) { 535 return false; # mailbox with unlimited quota on a domain with maxquota restriction -> not allowed, no more checks needed 536 } 537 538 if ($limit['maxquota'] != 0 && $quota > $limit['maxquota']) { 539 return false; # mailbox bigger than maxquota restriction (and maxquota != unlimited) -> not allowed, no more checks needed 540 } 541 542 # TODO: detailed error message ("domain quota exceeded", "mailbox quota too big" etc.) via flash_error? Or "available quota: xxx MB"? 543 if (!Config::bool('domain_quota')) { 544 return true; # enforcing domain_quota is disabled - just allow it 545 } elseif ($limit['quota'] <= 0) { # TODO: CHECK - 0 (unlimited) is fine, not sure about <= -1 (disabled)... 546 $rval = true; 547 } elseif ($quota == 0) { # trying to create an unlimited mailbox, but domain quota is set 548 return false; 549 } else { 550 $table_mailbox = table_by_key('mailbox'); 551 $query = "SELECT SUM(quota) as sum FROM $table_mailbox WHERE domain = ? AND username != ?"; 552 553 $rows = db_query_all($query, array($domain, $this->id)); 554 555 $cur_quota_total = divide_quota($rows[0]['sum']); # convert to MB 556 if (($quota + $cur_quota_total) > $limit['quota']) { 557 $rval = false; 558 } else { 559 $rval = true; 560 } 561 } 562 563 return $rval; 564 } 565 566 567 /** 568 * Get allowed maximum quota for a mailbox 569 * 570 * @param string $domain 571 * @param int $current_user_quota (in bytes) 572 * @return int allowed maximum quota (in MB) 573 */ 574 protected function allowed_quota($domain, $current_user_quota) 575 { 576 if (!Config::bool('quota')) { 577 return 0; # quota disabled means no limits - no need for more checks 578 } 579 580 $domain_properties = get_domain_properties($domain); 581 582 $tMaxquota = $domain_properties['maxquota']; 583 584 if (Config::bool('domain_quota') && $domain_properties['quota']) { 585 $dquota = $domain_properties['quota'] - $domain_properties['total_quota'] + divide_quota($current_user_quota); 586 if ($dquota < $tMaxquota) { 587 $tMaxquota = $dquota; 588 } 589 590 if ($tMaxquota == 0) { 591 $tMaxquota = $dquota; 592 } 593 } 594 return $tMaxquota; 595 } 596 597 598 /** 599 * Called after a mailbox has been created or edited in the DBMS. 600 * 601 * @return boolean success/failure status 602 */ 603 protected function mailbox_post_script() 604 { 605 if ($this->new) { 606 $cmd = Config::read_string('mailbox_postcreation_script'); 607 $warnmsg = Config::Lang('mailbox_postcreate_failed'); 608 } else { 609 $cmd = Config::read_string('mailbox_postedit_script'); 610 $warnmsg = Config::Lang('mailbox_postedit_failed'); 611 } 612 613 if ($this->new) { 614 $cmd_pw = Config::read('mailbox_postpassword_script'); 615 $warnmsg_pw = Config::Lang('mailbox_postpassword_failed'); 616 } 617 618 if (empty($cmd) && empty($cmd_pw)) { 619 return true; 620 } # nothing to do 621 622 list(/*NULL*/, $domain) = explode('@', $this->id); 623 $quota = $this->values['quota']; 624 625 if (empty($this->id) || empty($domain) || empty($this->values['maildir'])) { 626 trigger_error('In '.__FUNCTION__.': empty username, domain and/or maildir parameter', E_USER_ERROR); 627 return false; 628 } 629 630 $cmdarg1=escapeshellarg($this->id); 631 $cmdarg2=escapeshellarg($domain); 632 $status = true; 633 634 if (!empty($cmd)) { 635 $cmdarg3=escapeshellarg($this->values['maildir']); 636 if ($quota <= 0) { 637 $quota = 0; 638 } # TODO: check if this is correct behaviour 639 $cmdarg4 = escapeshellarg("" . $quota); 640 $command= "$cmd $cmdarg1 $cmdarg2 $cmdarg3 $cmdarg4"; 641 $retval=0; 642 $output=array(); 643 $firstline=''; 644 $firstline=exec($command, $output, $retval); 645 if (0!=$retval) { 646 error_log("Running $command yielded return value=$retval, first line of output=$firstline"); 647 $this->errormsg[] .= $warnmsg; 648 $status = false; 649 } 650 } 651 652 if (!empty($cmd_pw)) { 653 // Use proc_open call to avoid safe_mode problems and to prevent showing plain password in process table 654 $spec = array( 655 0 => array("pipe", "r"), // stdin 656 1 => array("pipe", "w"), // stdout 657 ); 658 659 $command = "$cmd_pw $cmdarg1 $cmdarg2 2>&1"; 660 661 $proc = proc_open($command, $spec, $pipes); 662 663 if (!$proc) { 664 error_log("can't proc_open $cmd_pw"); 665 $this->errormsg[] .= $warnmsg_pw; 666 $status = false; 667 } else { 668 // Write passwords through pipe to command stdin -- provide old password, then new password. 669 fwrite($pipes[0], "\0", 1); 670 fwrite($pipes[0], $this->values['password'] . "\0", 1+strlen($this->values['password'])); 671 $output = stream_get_contents($pipes[1]); 672 fclose($pipes[0]); 673 fclose($pipes[1]); 674 675 $retval = proc_close($proc); 676 677 if (0!=$retval) { 678 error_log("Running $command yielded return value=$retval, output was: " . json_encode($output)); 679 $this->errormsg[] .= $warnmsg_pw; 680 $status = false; 681 } 682 } 683 } 684 685 return $status; 686 } 687 688 /** 689 * Called after a mailbox has been deleted 690 * 691 * @return boolean true on success, false on failure 692 * also adds a detailed error message to $this->errormsg[] 693 */ 694 protected function mailbox_postdeletion() 695 { 696 $cmd = Config::read_string('mailbox_postdeletion_script'); 697 698 if (empty($cmd)) { 699 return true; 700 } 701 702 list(/*NULL*/, $domain) = explode('@', $this->id); 703 704 if (empty($this->id) || empty($domain)) { 705 $this->errormsg[] = 'Empty username and/or domain parameter in mailbox_postdeletion'; 706 return false; 707 } 708 709 $cmdarg1=escapeshellarg($this->id); 710 $cmdarg2=escapeshellarg($domain); 711 $command = "$cmd $cmdarg1 $cmdarg2"; 712 $retval=0; 713 $output=array(); 714 $firstline=''; 715 $firstline=exec($command, $output, $retval); 716 if (0!=$retval) { 717 error_log("Running $command yielded return value=$retval, first line of output=$firstline"); 718 $this->errormsg[] = 'Problems running mailbox postdeletion script!'; 719 return false; 720 } 721 722 return true; 723 } 724 725 726 727 /** 728 * Called by postSave() after a mailbox has been created. 729 * Immediately returns, unless configuration indicates 730 * that one or more sub-folders should be created. 731 * 732 * Triggers E_USER_ERROR if configuration error is detected. 733 * 734 * If IMAP login fails, the problem is logged to the system log 735 * (such as /var/log/httpd/error_log), and the function returns 736 * FALSE. 737 * 738 * Doesn't clean up, if only some of the folders could be 739 * created. 740 * 741 * @return boolean TRUE if everything succeeds, FALSE on all errors 742 */ 743 protected function create_mailbox_subfolders() 744 { 745 $create_mailbox_subdirs = Config::read('create_mailbox_subdirs'); 746 if (empty($create_mailbox_subdirs)) { 747 return true; 748 } 749 750 if (!function_exists('imap_open')) { 751 trigger_error('imap_open function not present; cannot create_mailbox_subdirs'); 752 return false; 753 } 754 755 if (!is_array($create_mailbox_subdirs)) { 756 trigger_error('create_mailbox_subdirs must be an array', E_USER_ERROR); 757 return false; 758 } 759 760 $s_host = Config::read_string('create_mailbox_subdirs_host'); 761 if (empty($s_host)) { 762 trigger_error('An IMAP/POP server host ($CONF["create_mailbox_subdirs_host"]) must be configured, if sub-folders are to be created', E_USER_ERROR); 763 return false; 764 } 765 766 $s_options=''; 767 768 $create_mailbox_subdirs_hostoptions = Config::read('create_mailbox_subdirs_hostoptions'); 769 if (!empty($create_mailbox_subdirs_hostoptions)) { 770 if (!is_array($create_mailbox_subdirs_hostoptions)) { 771 trigger_error('The $CONF["create_mailbox_subdirs_hostoptions"] parameter must be an array', E_USER_ERROR); 772 return false; 773 } 774 foreach ($create_mailbox_subdirs_hostoptions as $o) { 775 $s_options.='/'.$o; 776 } 777 } 778 779 $s_port=''; 780 if (Config::has('create_mailbox_subdirs_hostport')) { 781 $create_mailbox_subdirs_hostport = Config::read('create_mailbox_subdirs_hostport'); 782 if (!empty($create_mailbox_subdirs_hostport)) { 783 $s_port = $create_mailbox_subdirs_hostport; 784 if (intval($s_port)!=$s_port) { 785 trigger_error('The $CONF["create_mailbox_subdirs_hostport"] parameter must be an integer', E_USER_ERROR); 786 return false; 787 } 788 $s_port=':'.$s_port; 789 } 790 } 791 792 $s='{'.$s_host.$s_port.$s_options.'}'; 793 794 sleep(1); # give the mail triggering the mailbox creation a chance to do its job 795 796 $i=@imap_open($s, $this->id, $this->values['password']); 797 if (false==$i) { 798 error_log('Could not log into IMAP/POP server: ' . $this->id . ': ' . imap_last_error()); 799 return false; 800 } 801 802 $s_prefix = Config::read_string('create_mailbox_subdirs_prefix'); 803 foreach ($create_mailbox_subdirs as $f) { 804 $f='{'.$s_host.'}'.$s_prefix.$f; 805 $res=imap_createmailbox($i, $f); 806 if (!$res) { 807 error_log('Could not create IMAP folder $f: ' . $this->id . ': ' . imap_last_error()); 808 @imap_close($i); 809 return false; 810 } 811 @imap_subscribe($i, $f); 812 } 813 814 @imap_close($i); 815 return true; 816 } 817 818 #TODO: more self explaining language strings! 819} 820 821/* vim: set expandtab softtabstop=4 tabstop=4 shiftwidth=4: */ 822