diff --git a/cli/update-mailbox-quotas.php b/cli/update-mailbox-quotas.php index bdae8a8..072e723 100755 --- a/cli/update-mailbox-quotas.php +++ b/cli/update-mailbox-quotas.php @@ -1,100 +1,179 @@ #!/usr/bin/php . require __DIR__ . '/../load.php'; +// parse some command line options +$options = getopt( 'h', [ + 'help', + 'print', + 'no-save', +] ); + +// check if we have to show the help message +$SHOW_HELP = isset( $options['h'] ) || isset( $options['help'] ); + +// check if we have to alert the sysadmin (as deafult, no) +$PRINT = isset( $options['print'] ); + +// check if we have to save +$SAVE = !isset( $options['no-save'] ); + +// eventually print an help +if( $SHOW_HELP ) { + echo "Usage: \n"; + echo " {$argv[0]} [OPTIONS]\n\n"; + echo "OPTIONS:\n"; + echo " --print Print the overquota e-mail addresses\n\n"; + echo " --no-save Do not save any mailbox quota.\n"; + echo " Maybe because you just want to alert the sysadmin.\n"; + echo " As default the quotas will be saved in the database.\n\n"; + echo " -h --help Show this help and quit\n"; + exit( 0 ); +} + +// remember the overquota addresses +$overquota = []; + /** * Script to update Mailbox quotas * * See https://gitpull.it/T101 */ -// get every active domain +// query every active Domain plus their Plan (if any) $domains = ( new DomainAPI() ) ->select( [ 'domain.domain_ID', 'domain_name', + 'plan_mailboxquota', ] ) ->whereDomainIsActive() + ->joinPlan( 'LEFT' ) ->queryGenerator(); // for each active domain foreach( $domains as $domain ) { $domain_name = $domain->getDomainName(); // validate domain name if( strpos( $domain_name, __ ) !== false ) { error( "domain '$domain_name' is bad" ); continue; } // get every active mailbox of this domain $mailboxes = ( new MailboxAPI() ) ->select( [ 'mailbox_ID', 'mailbox_username', ] ) ->whereDomain( $domain ) ->whereMailboxIsActive() ->queryGenerator(); - query( 'START TRANSACTION' ); + // eventually start a transaction for the Domain quotas + if( $SAVE ) { + query( 'START TRANSACTION' ); + } // for each mailboxes foreach( $mailboxes as $mailbox ) { $mailbox_username = $mailbox->getMailboxUsername(); // validate mailbox name if( strpos( $mailbox_username, __ ) !== false ) { error( "$domain_name.$mailbox_username is bad" ); continue; } // calculate the quota size $bytes = 0; $expected_path = MAILBOX_BASE_PATH . __ . $domain_name . __ . $mailbox_username; if( file_exists( $expected_path ) ) { $bytes_raw = exec( sprintf( 'du --summarize --bytes -- %s | cut -f1', escapeshellarg( $expected_path ) ) ); $bytes = (int) $bytes_raw; } - // store the value in the history - ( new MailboxSizeAPI() ) - ->insertRow( [ - 'mailbox_ID' => $mailbox->getMailboxID(), - 'mailboxsize_bytes' => $bytes, - new DBCol( 'mailboxsize_date', 'NOW()', '-' ), - ] ); - - // update the denormalized latest Mailbox data - ( new MailboxAPI() ) - ->whereMailbox( $mailbox ) - ->update( [ - 'mailbox_lastsizebytes' => $bytes, - ] ); + // check if the Mailbox is overquota + if( $domain->getPlanMailboxQuota() && $bytes > $domain->getPlanMailboxQuota() ) { + + // eventually create a message for the sysadmin + if( $PRINT ) { + + // allow to customize this message + $msg = template_content( 'single-mailbox-overquota', [ + 'mailbox' => $mailbox, + 'domain' => $domain, + 'plan' => $domain, + 'size' => $bytes, + ] ); + + // store these short and unique messages with their size to allow sort + $overquota[ $msg ] = $bytes; + } + } + + // check if we have to save the current quota somewhere + if( $SAVE ) { + + // store the value in the history + ( new MailboxSizeAPI() ) + ->insertRow( [ + 'mailbox_ID' => $mailbox->getMailboxID(), + 'mailboxsize_bytes' => $bytes, + new DBCol( 'mailboxsize_date', 'NOW()', '-' ), + ] ); + + // update the denormalized latest Mailbox data + ( new MailboxAPI() ) + ->whereMailbox( $mailbox ) + ->update( [ + 'mailbox_lastsizebytes' => $bytes, + ] ); + + } } + // eventually commit the Domain quotas + if( $SAVE ) { + query( 'COMMIT' ); + } +} + +if( $PRINT ) { + + // sort the overquota messages by their size + uasort( $overquota, function( $a, $b ) { + return $b - $a; + } ); + + // now just take the messages + $overquota = array_keys( $overquota ); - query( 'COMMIT' ); + // allow to customize the way the email is sent + template( 'mailbox-overquotas', [ + 'problematic_list' => $overquota, + ] ); } diff --git a/include/class-Plan.php b/include/class-Plan.php index 58b15ea..6e439d7 100644 --- a/include/class-Plan.php +++ b/include/class-Plan.php @@ -1,213 +1,215 @@ . // load Plan trait class_exists( 'Plan' ); /** * Methods for a Plan class */ trait PlanTrait { /** * Get plan ID * * @return int */ public function getPlanID() { return $this->get( 'plan_ID' ); } /* * Get plan name * * @return string */ public function getPlanName() { return $this->get( 'plan_name' ); } /* * Get the plan UID * * @return string */ public function getPlanUID() { return $this->get( 'plan_uid' ); } /** * Get the number of FTP users of this Plan * * @return int|null */ public function getPlanFTPUsers() { return $this->get( 'plan_ftpusers' ); } /** * Get the number of Databases of this Plan * * @return int|null */ public function getPlanDatabases() { return $this->get( 'plan_databases' ); } /** * Get the number of Mailboxes of this Plan * * @return int|null */ public function getPlanMailboxes() { return $this->get( 'plan_mailboxes' ); } /** * Get the number of Mailforwardings of this Plan * * @return int|null */ public function getPlanMailforwardings() { return $this->get( 'plan_mailforwards' ); } /** * Get the Plan yearly price * * @return float */ public function getPlanYearlyPrice() { return $this->get( 'plan_yearlyprice' ); } /** * Get the Plan mailbox quota in bytes * * @return int */ public function getPlanMailboxQuota() { return $this->get( 'plan_mailboxquota' ); } /** * Get the plan edit URl * * @param boolean $absolute True for an absolute URL * @return string */ public function getPlanPermalink( $absolute = false ) { return Plan::permalink( $this->get( 'plan_name' ), $absolute ); } /** * Normalize a Plan object after being retrieved from database */ protected function normalizePlan() { $this->integers( 'plan_ID', 'plan_ftpusers', 'plan_databases', 'plan_mailboxes', 'plan_mailforwards', 'plan_mailboxquota', ); $this->floats( 'plan_yearlyprice' ); } } /** * Describe the 'plan' table */ class Plan extends Queried { use PlanTrait; /** * Table name */ const T = 'plan'; /** * Constructor */ public function __construct() { $this->normalizePlan(); } /** * Get the plan permalink * * @param string $plan_name Plan name * @param boolean $absolute True for an absolute URL */ public static function permalink( $plan_name = null, $absolute = false ) { $url = 'plan.php'; if( $plan_name ) { $url .= "/$plan_name"; } return site_page( $url, $absolute ); } /** * Get the Domain's Plan permalink * * @param string $plan_name Plan name * @param boolean $absolute True for an absolute URL */ public static function domainPermalink( $domain_name = null, $absolute = false ) { $url = 'domain-plan.php'; if( $domain_name ) { $url .= "/$domain_name"; } return site_page( $url, $absolute ); } /** * Get a percentage from two values * * It's always between 0 and 100, or NULL if cannot be calculated. * - * @param $current int Current amount - * @param $max int Maximum amount + * @param $current int Current amount + * @param $max int Maximum amount + * @param $overflow bool Set to TRUE to allow to go over 100% * @return mixed Percentage or NULL */ - public static function percentage( $current, $max ) { + public static function percentage( $current, $max, $overflow = false ) { // no args no party if( !$current || !$max ) { return null; } // cannot be negative if( $current < 0 ) { return 0; } + // calculate the percentage $percentage = $current * 100 / $max; - // cannot be too much - if( $percentage > 100 ) { + // you may don't want to go over 100% + if( $percentage > 100 && !$overflow ) { return 100; } return (int) $percentage; } } diff --git a/template/mailbox-overquotas.php b/template/mailbox-overquotas.php new file mode 100644 index 0000000..37f5b5e --- /dev/null +++ b/template/mailbox-overquotas.php @@ -0,0 +1,33 @@ +. + +/* + * This is the template used to say that some mailboxes were overquota. + * + * Note that the output is considered in plaintext. No escape is done. + * + * Called from: + * cli/update-mailbox-quotas.php + * + * Available variables: + * $problematic_list array List of problematic mailboxes + */ + +echo "Some mailboxes were overquota:\n"; +foreach( $problematic_list as $problematic_entry ) { + echo " $problematic_entry\n"; +} diff --git a/template/single-mailbox-overquota.php b/template/single-mailbox-overquota.php new file mode 100644 index 0000000..a279179 --- /dev/null +++ b/template/single-mailbox-overquota.php @@ -0,0 +1,56 @@ +. + +/* + * This is the template used for the sysadmin email, to say that a mailbox + * is overquota. + * + * It's actually just one row about one single mailbox. + * + * Note that the email is considered in plaintext. + * + * Called from: + * cli/update-mailbox-quotas.php + * + * Available variables: + * $mailbox object + * $domain object + * $plan object + * $size int Actual size of the mailbox in bytes + */ + +$percentage = Plan::percentage( $size, $plan->getPlanMailboxQuota(), true ); + +$human_size = human_filesize( $size ); + +printf( + // it should become something like: + // "foo@example.com: 200MB (22%)" + '%1$s@%2$s: %3$s (%4$s%%)', + + // %1$s + $mailbox->getMailboxUsername(), + + // %2$s + $domain->getDomainName(), + + // %3$s + $human_size, + + // %4$s + $percentage, +);