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