diff --git a/include/class-FTP.php b/include/class-FTP.php
index 52402a7..7fb31f5 100644
--- a/include/class-FTP.php
+++ b/include/class-FTP.php
@@ -1,106 +1,148 @@
 <?php
 # Copyright (C) 2019 Valerio Bozzolan
 # Boz Libre Hosting Panel
 #
 # This program is free software: you can redistribute it and/or modify
 # it under the terms of the GNU Affero General Public License as
 # published by the Free Software Foundation, either version 3 of the
 # License, or (at your option) any later version.
 #
 # This program is distributed in the hope that it will be useful,
 # but WITHOUT ANY WARRANTY; without even the implied warranty of
 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 # GNU Affero General Public License for more details.
 #
 # You should have received a copy of the GNU General Public License
 # along with this program. If not, see <http://www.gnu.org/licenses/>.
 
 // load dependent traits
 class_exists( 'Domain' );
 
 /**
  * Methods for an FTP class
  */
 trait FTPTrait {
 
 	use DomainTrait;
 
 	/**
 	 * Get the FTP login username
 	 *
 	 * @return string E-mail
 	 */
 	public function getFTPLogin() {
 		return $this->get( 'ftp_login' );
 	}
 
 	/**
 	 * Check if this FTP account is active
 	 *
 	 * @return bool
 	 */
 	public function isFTPActive() {
 		return $this->get( 'ftp_active' );
 	}
 
 	/**
 	 * Get the mailbox permalink
 	 *
 	 * @return string
 	 */
 	public function getFTPPermalink( $absolute = false ) {
 		return FTP::permalink(
 			$this->getDomainName(),
 			$this->getFTPLogin()
 		);
 	}
 
 	protected function normalizeFTP() {
 		$this->normalizeDomain();
 		$this->booleans( 'ftp_active' );
 		$this->integers( 'ftp_ulbandwidth',
 		                 'ftp_dlbandwidth',
 		                 'ftp_quotasize',
 		                 'ftp_quotafiles'
 		);
 	}
 
 }
 
 /**
  * An FTP user
  */
 class FTP extends Queried {
 
 	use FTPTrait;
 
 	/**
 	 * Table name
 	 */
 	const T = 'ftp';
 
 	/**
 	 * Constructor
 	 *
 	 * Normalize the object obtained from the database
 	 */
 	public function __construct() {
 		$this->normalizeFTP();
 	}
 
 	/**
 	 * Get the FTP permalink from domain name and FTP login
 	 *
 	 * @param string $domain Domain name
 	 * @param string $login FTP user login
 	 * @return string
 	 */
 	public static function permalink( $domain, $login = null ) {
 		$url = sprintf( '%s/%s', ROOT . '/ftp.php', $domain );
 		if( $login ) {
 			$url .= "/$login";
 		}
 		return $url;
 	}
 
+	/**
+	 * Encrypt an FTP user password
+	 *
+	 * @param $password string Clear text password
+	 * @return          string One-way encrypted password
+	 */
+	public static function encryptPassword( $password ) {
+		global $HOSTING_CONFIG;
+
+		// the FTP password encryption mechanism can be customized
+		if( isset( $HOSTING_CONFIG->FTP_ENCRYPT_PWD ) ) {
+			return call_user_func( $HOSTING_CONFIG->FTP_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 FTP server 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 FTP server supports.
+		 *
+		 * In short if you are using PureFTPd this default configuration may work
+		 * because you may have PureFTPd configured as follow:
+		 *  ...
+		 *  MYSQLCrypt crypt
+		 *  ...
+		 *
+		 * You can read more here:
+		 *   https://download.pureftpd.org/pub/pure-ftpd/doc/README.MySQL
+		 *
+		 * Anyway you can use whatever FTP server that talks with a MySQL database
+		 * and so you should adopt the most stronger encryption mechanism available.
+		 */
+
+		$salt = bin2hex( openssl_random_pseudo_bytes( 3 ) );
+		return '{SHA512-CRYPT}' . crypt( $password, "$6$$salt" );
+	}
+
 }
diff --git a/include/class-Mailbox.php b/include/class-Mailbox.php
index dae1da5..228b919 100644
--- a/include/class-Mailbox.php
+++ b/include/class-Mailbox.php
@@ -1,136 +1,164 @@
 <?php
 # Copyright (C) 2018, 2019 Valerio Bozzolan
 # Boz Libre Hosting Panel
 #
 # This program is free software: you can redistribute it and/or modify
 # it under the terms of the GNU Affero General Public License as
 # published by the Free Software Foundation, either version 3 of the
 # License, or (at your option) any later version.
 #
 # This program is distributed in the hope that it will be useful,
 # but WITHOUT ANY WARRANTY; without even the implied warranty of
 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 # GNU Affero General Public License for more details.
 #
 # You should have received a copy of the GNU General Public License
 # along with this program. If not, see <http://www.gnu.org/licenses/>.
 
 // load dependent traits
 class_exists( 'Domain' );
 
 trait MailboxTrait {
 
 	use DomainTrait;
 
 	/**
 	 * 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 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 );
 
 		// update
 		( new MailboxAPI() )
 			->whereMailbox( $this )
 			->update( [
 				new DBCol( 'mailbox_password', $enc_password, 's' ),
 			] );
 
 		return $password;
 	}
 
 	/**
 	 * Normalize a Mailbox after being fetched from database
 	 */
 	protected function normalizeMailbox() {
 		$this->normalizeDomain();
 		$this->booleans( 'mailbox_receive' );
 	}
 
 }
 
 /**
  * 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
 	 *
-	 * TODO: do not hardcode to my Dovecot configuration
+	 * @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" );
 	}
 
 }
diff --git a/load-example.php b/load-example.php
index c828a0d..7fac38f 100644
--- a/load-example.php
+++ b/load-example.php
@@ -1,52 +1,67 @@
 <?php
-# Copyright (C) 2018 Valerio Bozzolan
+# Copyright (C) 2018, 2019 Valerio Bozzolan
 # Boz Libre Hosting Panel
 #
 # This program is free software: you can redistribute it and/or modify
 # it under the terms of the GNU Affero General Public License as
 # published by the Free Software Foundation, either version 3 of the
 # License, or (at your option) any later version.
 #
 # This program is distributed in the hope that it will be useful,
 # but WITHOUT ANY WARRANTY; without even the implied warranty of
 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 # GNU Affero General Public License for more details.
 #
 # You should have received a copy of the GNU General Public License
 # along with this program. If not, see <http://www.gnu.org/licenses/>.
 
 /*
  * This is an example configuration file
  *
  * Please fill this file and save as 'load.php'!
  */
 
 // database credentials
 $username = 'insert here database username';
 $password = 'insert here database password';
 $database = 'insert here database name';
 $location = 'localhost';
 
 // database prefix (if any)
 $prefix = '';
 
 // your contact e-mail
 define( 'CONTACT_EMAIL', 'services@example.org' );
 
 // your SMTP credentials
 define( 'MAIL_FROM',     'noreply@example.org' );
 define( 'SMTP_USERNAME', 'noreply@example.org' );
 define( 'SMTP_PASSWORD', 'insert here smtp password' );
 define( 'SMTP_AUTH',     'PLAIN' );
 define( 'SMTP_TLS',      true );
 define( 'SMTP_SERVER',   'mail.example.org' );
 define( 'SMTP_PORT',     465 );
 
 // absolute path to the project directory without trailing slash
 define( 'ABSPATH', __DIR__ );
 
 // absolute web directory without trailing slash
 define( 'ROOT', '' );
 
+// other specific configuration about your hosting environments
+$HOSTING_CONFIG = new stdClass();
+
+// Mailbox password encryption custom mechanism (you can leave this commented for the default)
+# $HOSTING_CONFIG->MAILBOX_ENCRYPT_PWD = function ( $password ) {
+#	$salt = bin2hex( openssl_random_pseudo_bytes( 3 ) );
+#	return '{SHA512-CRYPT}' . crypt( $password, "$6$$salt" );
+# };
+
+// FTP password encryption custom mechanism (you can leave this commented for the default)
+# $HOSTING_CONFIG->FTP_ENCRYPT_PWD = function ( $password ) {
+#	$salt = bin2hex( openssl_random_pseudo_bytes( 3 ) );
+#	return '{SHA512-CRYPT}' . crypt( $password, "$6$$salt" );
+# };
+
 // path to the boz-php framework
 require '/usr/share/php/suckless-php/load.php';
diff --git a/www/ftp.php b/www/ftp.php
index b57aeee..371c57c 100644
--- a/www/ftp.php
+++ b/www/ftp.php
@@ -1,152 +1,164 @@
 <?php
 # Copyright (C) 2019 Valerio Bozzolan
 # Boz Libre Hosting Panel
 #
 # This program is free software: you can redistribute it and/or modify
 # it under the terms of the GNU Affero General Public License as
 # published by the Free Software Foundation, either version 3 of the
 # License, or (at your option) any later version.
 #
 # This program is distributed in the hope that it will be useful,
 # but WITHOUT ANY WARRANTY; without even the implied warranty of
 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 # GNU Affero General Public License for more details.
 #
 # You should have received a copy of the GNU General Public License
 # along with this program. If not, see <http://www.gnu.org/licenses/>.
 
 /*
  * This is the single FTP account creation/edit page
  */
 
 // load framework
 require '../load.php';
 
 // wanted informations
-$domain = null;
-$ftp    = null;
+$domain       = null;
+$ftp          = null;
+$ftp_password = null;
 
 // URL paramenters (maximum both domain and FTP login, minimum just domain)
 list( $domain_name, $ftp_login ) = url_parts( 2, 1 );
 
 // eventually retrieve mailforward from database
 if( $ftp_login ) {
 	$ftp = ( new FTPAPI() )
 		->select( [
 			'domain.domain_ID',
 			'domain_name',
 			'ftp_login',
 		] )
 		->joinFTPDomain()
 		->whereDomainName( $domain_name )
 		->whereFTPLogin( $ftp_login )
 		->whereDomainIsEditable()
 		->queryRow();
 
 	// 404
 	$ftp or PageNotFound::spawn();
 
 	// recycle the mailforward object that has domain informations
 	$domain = $ftp;
 }
 
 // eventually retrieve domain from database
 if( ! $domain ) {
 	$domain = ( new DomainAPI() )
 		->select( [
 			'domain.domain_ID',
 			'domain.domain_name',
 		] )
 		->whereDomainName( $domain_name )
 		->whereDomainIsEditable()
 		->queryRow();
 
 	// 404
 	$domain or PageNotFound::spawn();
 }
 
 if( ! $ftp ) {
 	// to create an FTP user, must edit all FTP users
 	require_permission( 'edit-ftp-all' );
 }
 
 // save destination action
 if( is_action( 'ftp-save' ) ) {
 
 	// save source only during creation
 	if( ! $ftp ) {
 
-		// sanitize
-		if( ! isset( $_POST[ 'ftp_login' ] ) ) {
+		// sanitize data
+		if( !isset( $_POST['ftp_login'] ) || !is_string( $_POST['ftp_login'] ) ) {
 			BadRequest::spawn( __( "missing parameter" ) );
 		}
-		$username = luser_input( $_POST[ 'ftp_login' ], 128 );
-		if( ! validate_mailbox_username( $username ) ) {
+
+		// generate the username (must start with domain name)
+		$username = generate_slug( $domain->getDomainName() ) . '_' . $_POST[ 'ftp_login' ];
+		$username = luser_input( $username, 128 );
+
+		// validate username
+		if( !validate_mailbox_username( $username ) ) {
 			BadRequest::spawn( __( "invalid mailbox name" ) );
 		}
 
 		// check existence
-		$ftp_exists = ( new FTPAPI )
+		$ftp_exists = ( new FTPAPI() )
 			->select( 1 )
 			->whereDomain( $domain )
 			->whereFTPLogin( $username )
 			->queryRow();
 
 		// die if exists
 		if( $ftp_exists ) {
 			BadRequest::spawn( __( "FTP account already existing" ) );
 		}
 
+		// generate a password and die
+		$ftp_password      = generate_password();
+		$ftp_password_safe = FTP::encryptPassword( $ftp_password );
+
 		// insert as new row
 		insert_row( 'ftp', [
-			new DBCol( 'domain_ID', $domain->getDomainID(), 'd' ),
-			new DBCol( 'ftp_login', $username,              's' ),
+			new DBCol( 'domain_ID',    $domain->getDomainID(), 'd' ),
+			new DBCol( 'ftp_login',    $username,              's' ),
+			new DBCol( 'ftp_password', $ftp_password_safe,         's' ),
 		] );
 
 		// POST/redirect/GET
 		http_redirect( FTP::permalink(
 			$domain->getDomainName(),
 			$username,
 			true
 		), 303 );
 	}
 }
 
 // delete action
 if( $ftp ) {
 
 	// action fired when deleting a whole mailforward
 	if( is_action( 'ftp-delete' ) ) {
 
 		// delete the account
 		( new FTPAPI() )
 			->whereFTP( $ftp )
 			->delete();
 
 		// POST/redirect/GET
 		http_redirect( $domain->getDomainPermalink( true ), 303 );
 
 	}
 
 }
 
 // spawn header
 Header::spawn( [
 	'uid' => false,
 	'title-prefix' => __( "FTP user" ),
 	'title' => $ftp
 		? $ftp->getFTPLogin()
 		: __( "create" ),
 	'breadcrumb' => [
 		new MenuEntry( null, $domain->getDomainPermalink(), $domain->getDomainName() ),
 	],
 ] );
 
 // spawn the page content
 template( 'ftp', [
-	'domain' => $domain,
-	'ftp'    => $ftp,
+	'domain'   => $domain,
+	'ftp'      => $ftp,
+	'password' => $ftp_password,
 ] );
 
 // spawn the footer
 Footer::spawn();
diff --git a/www/user.php b/www/user.php
index a0c39cd..7156f9f 100644
--- a/www/user.php
+++ b/www/user.php
@@ -1,172 +1,204 @@
 <?php
 # Copyright (C) 2019 Valerio Bozzolan
 # Boz Libre Hosting Panel
 #
 # This program is free software: you can redistribute it and/or modify
 # it under the terms of the GNU Affero General Public License as
 # published by the Free Software Foundation, either version 3 of the
 # License, or (at your option) any later version.
 #
 # This program is distributed in the hope that it will be useful,
 # but WITHOUT ANY WARRANTY; without even the implied warranty of
 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 # GNU Affero General Public License for more details.
 #
 # You should have received a copy of the GNU General Public License
 # along with this program. If not, see <http://www.gnu.org/licenses/>.
 
 /*
  * This is the single User creation/edit page
  */
 
 // load framework
 require '../load.php';
 
 // require the permission to see the backend
 require_permission( 'backend' );
 
 // wanted informations
 $user = null;
 
 // URL paramenters (user_uid)
 list( $user_uid ) = url_parts( 1, 0 );
 
 // eventually retrieve mailforward from database
 if( $user_uid ) {
 	$user = ( new UserAPI() )
 		->whereUserUID( $user_uid )
 		->whereUserIsEditable()
 		->queryRow();
 
 	// 404
 	if( !$user || !$user->isUserEditable() ) {
 		PageNotFound::spawn();
 	}
 } else {
 	// to create an FTP user, must edit all FTP users
 	require_permission( 'edit-user-all' );
 }
 
 // save destination action
-if( is_action( 'user-save' ) ) {
-
+if( is_action( 'save-user' ) ) {
+
+	$email   = $_POST['email']   ?? null;
+	$uid     = $_POST['uid']     ?? null;
+	$name    = $_POST['name']    ?? null;
+	$surname = $_POST['surname'] ?? null;
+
+	if( $email && $uid && $name && $surname ) {
+		$email = (string) $email;
+
+		// data to be saved
+		$data = [];
+		$data['user_email']   = $email;
+		$data['user_name']    = $name;
+		$data['user_surname'] = $surname;
+
+		if( $user ) {
+			// update existing User
+			( new UserAPI() )
+				->whereUser( $user )
+				->update( $data );
+		} else {
+			// insert new User
+			$data['user_uid']      = $uid;
+			$data['user_active']   = 1;
+			$data['user_password'] = '!';
+			$data['user_role']     = 'user';
+			$data[] = new DBCol( 'user_registration_date', 'NOW()', '-' );
+
+			( new UserAPI() )
+				->insertRow( $data );
+		}
+	}
 }
 
 // add a Domain to the user
 if( is_action( 'add-domain' ) ){
 
 	// check for permissions
 	if( !has_permission( 'edit-user-all' ) ) {
 		error_die( "Not authorized to add a Domain" );
 	}
 
 	// get the Domain by name
 	$domain_name = $_POST['domain_name'] ?? null;
 	if( !$domain_name ) {
 		die( "Please fill that damn Domain name" );
 	}
 
 	// search the Domain name
 	$domain =
 		( new DomainAPI() )
 			->whereDomainName( $domain_name )
 			->queryRow();
 
 	query( 'START TRANSACTION' );
 
 	// domain ID to be assigned to the User
 	$domain_ID = null;
 
 	// does the Domain already exist?
 	if( $domain ) {
 		$domain_ID = $domain->getDomainID();
 	} else {
 		// can I add this Domain?
 		if( has_permission( 'edit-domain-all' ) ) {
 
 			// add this Domain
 			( new DomainAPI() )
 				->insertRow( [
 					'domain_name'   => $domain_name,
 					'domain_active' => 1,
 					new DBCol( 'domain_born', 'NOW()', '-' ),
 				] );
 
 			$domain_ID = last_inserted_ID();
 		}
 	}
 
 	if( $domain_ID ) {
 
 		$is_domain_mine =
 			( new DomainUserAPI() )
 				->whereUserIsMe()
 				->whereDomainID( $domain_ID )
 				->queryRow();
 
 		// is it already mine?
 		if( !$is_domain_mine ) {
 
 			// associate this domain to myself
 			( new DomainUserAPI() )
 				->insertRow( [
 					'domain_ID' => $domain_ID,
 					'user_ID'   => $user->getUserID(),
 					new DBCol( 'domain_user_creation_date', 'NOW()', '-' ),
 				] );
 		}
 
 	} else {
 		die( "this Domain is not registered and can't be added" );
 	}
 
 	query( 'COMMIT' );
 
 	// end add Domain to User
 }
 
 // register action to generate a new password
 $new_password = null;
 if( is_action( 'change-password' ) && $user ) {
 
 	// generate a new password and save
 	$new_password = generate_password();
 	$encrypted = User::encryptPassword( $new_password );
 	( new UserAPI() )
 		->whereUser( $user )
 		->update( [
-			new DBCol( User::PASSWORD, $encrypted, 's' ),
+			User::IS_ACTIVE => 1,
+			User::PASSWORD  => $encrypted,
 		] );
 }
 
 // expose the User domains
 $user_domains = [];
 if( $user ) {
 
 	// get User domains
 	$user_domains =
 		( new DomainUserAPI() )
 			->joinDomain()
 			->whereUser( $user )
 			->orderByDomainName()
 			->queryGenerator();
 }
 
 // spawn header
 Header::spawn( [
 	'uid' => false,
 	'title-prefix' => __( "User" ),
 	'title' => $user
 		? $user->getUserUID()
 		: __( "create" ),
 ] );
 
 // spawn the page content
 template( 'user', [
 	'user'         => $user,
 	'new_password' => $new_password,
 	'user_domains' => $user_domains,
 ] );
 
 // spawn the footer
 Footer::spawn();