diff --git a/include/class-Mailbox.php b/include/class-Mailbox.php index ef0522f..82626fc 100644 --- a/include/class-Mailbox.php +++ b/include/class-Mailbox.php @@ -1,219 +1,230 @@ . // load dependent traits class_exists( 'Domain' ); trait MailboxTrait { use DomainTrait; /** * Get the Mailbox ID * * @return int */ public function getMailboxID() { return $this->get( 'mailbox_ID' ); } /** * Get the mailbox username * * @return string */ public function getMailboxUsername() { return $this->get( 'mailbox_username' ); } /** * Get the mailbox address * * @return string E-mail */ public function getMailboxAddress() { return sprintf( "%s@%s", $this->get( 'mailbox_username' ), $this->get( 'domain_name' ) ); } /** * Get the mailbox description (if any) * * @return string */ public function getMailboxDescription() { return $this->get( 'mailbox_description' ); } /** * Get the mailbox permalink * * @return string */ public function getMailboxPermalink( $absolute = false ) { return Mailbox::permalink( $this->get( 'domain_name' ), $this->get( 'mailbox_username' ) ); } /** * Update this mailbox password * * @param string $password * @return string */ public function updateMailboxPassword( $password = null ) { if( ! $password ) { $password = generate_password(); } $enc_password = Mailbox::encryptPassword( $password ); + query( 'START TRANSACTION' ); + // update ( new MailboxAPI() ) ->whereMailbox( $this ) ->update( [ new DBCol( 'mailbox_password', $enc_password, 's' ), ] ); + // register this event in the registry + APILog::insert( [ + 'family' => 'mailbox', + 'action' => 'newpassword', + 'mailbox' => $this, + ] ); + + query( 'COMMIT' ); + return $password; } /** * Get the last size in bytes * * @return int */ public function getMailboxLastSizeBytes() { return $this->get( 'mailbox_lastsizebytes' ); } /** * Normalize a Mailbox after being fetched from database */ protected function normalizeMailbox() { $this->normalizeDomain(); $this->integers( 'mailbox_ID', 'mailbox_lastsizebytes' ); $this->booleans( 'mailbox_receive' ); } /** * Get the mailbox filesystem pathname in the MTA host * * TODO: actually all the mailbox are on the same host. * Then, we should support multiple hosts. * * @return string */ public function getMailboxPath() { // require a valid filename or throw $mailbox_user = $this->getMailboxUsername(); require_safe_dirname( $mailbox_user ); // mailboxes are stored under a $BASE/domain/username filesystem structure return $this->getDomainMailboxesPath() . __ . $mailbox_user; } } /** * A mailbox */ class Mailbox extends Queried { use MailboxTrait; /** * Database table */ const T = 'mailbox'; /** * Constructor */ public function __construct() { $this->normalizeMailbox(); } /** * Get the mailbox permalink * * @param $domain string * @param $mailbox string * @param $absolute boolean * @return string */ public static function permalink( $domain, $mailbox = null, $absolute = false ) { $part = site_page( 'mailbox.php', $absolute ) . _ . $domain; if( $mailbox ) { $part .= _ . $mailbox; } return $part; } /** * Encrypt a password * * @param string $password Clear text password * @return string One-way encrypted password */ public static function encryptPassword( $password ) { global $HOSTING_CONFIG; // the Mailbox password encryption mechanism can be customized if( isset( $HOSTING_CONFIG->MAILBOX_ENCRYPT_PWD ) ) { return call_user_func( $HOSTING_CONFIG->MAILBOX_ENCRYPT_PWD, $password ); } // or then just a default behaviour /** * The default behaviour is to adopt the crypt() encryption mechanism * with SHA512 and some random salt. It's strong enough nowadays. * * Read your MTA/MDA documentation, whatever you are using. * We don't know how your infrastructure works, so we don't know * how you want your password encrypted in the database and what kind * of password encryption mechanisms your MTA/MDA supports. * * In short if you are using Postfix this default configuration may work * because you may have Postfix configured as follow: * * Anyway you can use whatever MTA/MDA that talks with a MySQL database * and so you should adopt the most stronger encryption mechanism available. * * https://doc.dovecot.org/configuration_manual/authentication/password_schemes/ */ $salt = bin2hex( openssl_random_pseudo_bytes( 3 ) ); return '{SHA512-CRYPT}' . crypt( $password, "$6$$salt" ); } /** * Force to get a Mailbox ID, whatever is passed * * @param mixed $mailbox Mailbox object or Mailbox ID * @return int */ public static function getID( $mailbox ) { return is_object( $mailbox ) ? $mailbox->getMailboxID() : (int)$mailbox; } } diff --git a/www/mailbox.php b/www/mailbox.php index 9f12a7b..0130146 100644 --- a/www/mailbox.php +++ b/www/mailbox.php @@ -1,167 +1,189 @@ . /* * This is the mailbox edit page */ // load framework require '../load.php'; // wanted domain and mailbox username list( $domain_name, $mailbox_username ) = url_parts( 2, 1 ); // some useful information $domain = null; $mailbox = null; $plan = null; $mailbox_password = null; // check if the page is about a specific Mailbox if( $mailbox_username ) { // retrieve the mailbox and its domain and its Plan $mailbox = ( new MailboxFullAPI() ) ->joinPlan( 'LEFT' ) ->whereDomainName( $domain_name ) ->whereMailboxUsername( $mailbox_username ) ->whereMailboxIsEditable() ->queryRow(); // 404? $mailbox or PageNotFound::spawn(); // the mailbox object has the domain stuff - recycle it $domain = $mailbox; // the mailbox object has the Plan stuff - recycle it $plan = $mailbox; } else { // retrieve just the domain and its Plan $domain = ( new DomainAPI() ) ->whereDomainName( $domain_name ) ->whereDomainIsEditable() ->joinPlan( 'LEFT' ) ->queryRow(); // 404? $domain or PageNotFound::spawn(); // the domain object has the Plan stuff - recycle it $plan = $domain; } // does the user want to create a Mailbox? if( !$mailbox ) { // count the actual number of Domain Mailbox(es) $mailbox_count = (int) ( new MailboxAPI() ) ->select( 'COUNT(*) count' ) ->whereDomain( $domain ) ->queryValue( 'count' ); // check if I can add another Mailbox if( $mailbox_count >= $plan->getPlanMailboxes() && !has_permission( 'edit-email-all' ) ) { BadRequest::spawn( __( "Your Plan does not allow this action" ), 401 ); } } /* * Change the mailbox password */ if( $mailbox && is_action( 'mailbox-password-reset' ) ) { $mailbox_password = $mailbox->updateMailboxPassword(); } /** * Eventually save the notes */ if( $mailbox && is_action( 'save-mailbox-notes' ) ) { // read the description $description = $_POST['mailbox_description'] ?? null; + query( 'START TRANSACTION' ); + // save the description ( new MailboxAPI() ) ->whereMailbox( $mailbox ) ->update( [ 'mailbox_description' => $description, ] ); + // remember this action in the registry + APILog::insert( [ + 'family' => 'mailbox', + 'action' => 'description.change', + 'mailbox' => $mailbox, + ] ); + + query( 'COMMIT' ); + // POST -> redirect -> GET http_redirect( $mailbox->getMailboxPermalink() ); } /* * Create the mailbox */ if( !$mailbox && is_action( 'mailbox-create' ) && isset( $_POST['mailbox_username'] ) ) { // assure that the username is not too long $_POST['mailbox_username'] = luser_input( $_POST['mailbox_username'], 64 ); // check if the mailbox already exist $mailbox_exists = ( new MailboxFullAPI() ) ->select( 1 ) ->whereDomainName( $domain_name ) ->whereMailboxUsername( $_POST['mailbox_username'] ) ->queryRow(); // check if we can create the mailbox if( !$mailbox_exists ) { // assign a damn temporary password $mailbox_password = generate_password(); $mailbox_password_safe = Mailbox::encryptPassword( $mailbox_password ); + query( 'START TRANSACTION' ); + // really create the mailbox insert_row( 'mailbox', [ new DBCol( 'mailbox_username', $_POST['mailbox_username'], 's' ), new DBCol( 'domain_ID', $domain->getDomainID(), 'd' ), new DBCol( 'mailbox_password', $mailbox_password_safe, 's' ), ] ); + + // register this event in the registry + APILog::insert( [ + 'family' => 'mailbox', + 'action' => 'create', + 'mailbox' => last_inserted_ID(), + ] ); + + query( 'COMMIT' ); } // POST -> redirect -> GET http_redirect( Mailbox::permalink( $domain->getDomainName(), $_POST['mailbox_username'] ) ); } // spawn header Header::spawn( [ 'uid' => false, 'title-prefix' => __( "Mailbox" ), 'title' => $mailbox ? $mailbox->getMailboxAddress() : __( "create" ), 'breadcrumb' => [ new MenuEntry( null, $domain->getDomainPermalink(), $domain->getDomainName() ), ], ] ); // spawn the page content template( 'mailbox', [ 'mailbox' => $mailbox, 'mailbox_password' => $mailbox_password, 'domain' => $domain, 'plan' => $plan, ] ); // spawn the footer Footer::spawn();