This is an automated email from the ASF dual-hosted git repository.

joao pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/cloudstack.git


The following commit(s) were added to refs/heads/main by this push:
     new f76c6f3ea97 Quota email configuration (#8307)
f76c6f3ea97 is described below

commit f76c6f3ea9710b70770c26193eaccd1caa6207e0
Author: João Jandre <[email protected]>
AuthorDate: Mon Mar 18 08:26:41 2024 -0300

    Quota email configuration (#8307)
    
    * Quota email configuration feature
---
 .../resources/META-INF/db/schema-41900to42000.sql  |  10 ++
 .../apache/cloudstack/quota/QuotaAlertManager.java |   3 +
 .../cloudstack/quota/QuotaAlertManagerImpl.java    | 130 +++++++++++-----
 .../cloudstack/quota/QuotaStatementImpl.java       |  43 ++++--
 .../cloudstack/quota/constant/QuotaConfig.java     |   3 +
 .../quota/dao/QuotaEmailConfigurationDao.java      |  36 +++++
 .../quota/dao/QuotaEmailConfigurationDaoImpl.java  | 105 +++++++++++++
 .../quota/dao/QuotaEmailTemplatesDao.java          |   2 +
 .../quota/dao/QuotaEmailTemplatesDaoImpl.java      |   5 +
 .../quota/vo/QuotaEmailConfigurationVO.java        |  68 +++++++++
 .../quota/spring-framework-quota-context.xml       |   1 +
 .../quota/QuotaAlertManagerImplTest.java           | 163 +++++++++++++++++----
 .../cloudstack/quota/QuotaStatementTest.java       |  67 ++++++++-
 .../api/command/QuotaConfigureEmailCmd.java        |  79 ++++++++++
 .../command/QuotaListEmailConfigurationCmd.java    |  54 +++++++
 .../api/response/QuotaConfigureEmailResponse.java  |  78 ++++++++++
 .../api/response/QuotaResponseBuilder.java         |   8 +
 .../api/response/QuotaResponseBuilderImpl.java     | 106 +++++++++++++-
 .../apache/cloudstack/quota/QuotaServiceImpl.java  |   6 +-
 .../api/response/QuotaResponseBuilderImplTest.java | 112 ++++++++++++++
 20 files changed, 988 insertions(+), 91 deletions(-)

diff --git 
a/engine/schema/src/main/resources/META-INF/db/schema-41900to42000.sql 
b/engine/schema/src/main/resources/META-INF/db/schema-41900to42000.sql
index 3ebab6b15f2..1bb1905443a 100644
--- a/engine/schema/src/main/resources/META-INF/db/schema-41900to42000.sql
+++ b/engine/schema/src/main/resources/META-INF/db/schema-41900to42000.sql
@@ -69,3 +69,13 @@ CALL 
`cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.network_offerings','for_nsx', 'int(1
 CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.network_offerings','nsx_mode', 
'varchar(32) COMMENT "mode in which the network would route traffic"');
 CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.vpc_offerings','for_nsx', 'int(1) 
unsigned DEFAULT "0" COMMENT "is nsx enabled for the resource"');
 CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.vpc_offerings','nsx_mode', 
'varchar(32) COMMENT "mode in which the network would route traffic"');
+
+
+-- Create table to persist quota email template configurations
+CREATE TABLE IF NOT EXISTS `cloud_usage`.`quota_email_configuration`(
+    `account_id` int(11) NOT NULL,
+    `email_template_id` bigint(20) NOT NULL,
+    `enabled` int(1) UNSIGNED NOT NULL,
+    PRIMARY KEY (`account_id`, `email_template_id`),
+    CONSTRAINT `FK_quota_email_configuration_account_id` FOREIGN KEY 
(`account_id`) REFERENCES `cloud_usage`.`quota_account`(`account_id`),
+    CONSTRAINT `FK_quota_email_configuration_email_template_id` FOREIGN KEY 
(`email_template_id`) REFERENCES `cloud_usage`.`quota_email_templates`(`id`));
diff --git 
a/framework/quota/src/main/java/org/apache/cloudstack/quota/QuotaAlertManager.java
 
b/framework/quota/src/main/java/org/apache/cloudstack/quota/QuotaAlertManager.java
index 44204e8d116..f4ee2362c7e 100644
--- 
a/framework/quota/src/main/java/org/apache/cloudstack/quota/QuotaAlertManager.java
+++ 
b/framework/quota/src/main/java/org/apache/cloudstack/quota/QuotaAlertManager.java
@@ -16,11 +16,14 @@
 //under the License.
 package org.apache.cloudstack.quota;
 
+import com.cloud.user.AccountVO;
 import com.cloud.utils.component.Manager;
 
 import org.apache.cloudstack.quota.QuotaAlertManagerImpl.DeferredQuotaEmail;
+import org.apache.cloudstack.quota.constant.QuotaConfig;
 
 public interface QuotaAlertManager extends Manager {
+    boolean isQuotaEmailTypeEnabledForAccount(AccountVO account, 
QuotaConfig.QuotaEmailTemplateTypes quotaEmailTemplateType);
     void checkAndSendQuotaAlertEmails();
     void sendQuotaAlert(DeferredQuotaEmail emailToBeSent);
 }
diff --git 
a/framework/quota/src/main/java/org/apache/cloudstack/quota/QuotaAlertManagerImpl.java
 
b/framework/quota/src/main/java/org/apache/cloudstack/quota/QuotaAlertManagerImpl.java
index ff41a8141d7..b26b3171f5b 100644
--- 
a/framework/quota/src/main/java/org/apache/cloudstack/quota/QuotaAlertManagerImpl.java
+++ 
b/framework/quota/src/main/java/org/apache/cloudstack/quota/QuotaAlertManagerImpl.java
@@ -34,8 +34,10 @@ import 
org.apache.cloudstack.framework.config.dao.ConfigurationDao;
 import org.apache.cloudstack.quota.constant.QuotaConfig;
 import 
org.apache.cloudstack.quota.constant.QuotaConfig.QuotaEmailTemplateTypes;
 import org.apache.cloudstack.quota.dao.QuotaAccountDao;
+import org.apache.cloudstack.quota.dao.QuotaEmailConfigurationDao;
 import org.apache.cloudstack.quota.dao.QuotaEmailTemplatesDao;
 import org.apache.cloudstack.quota.vo.QuotaAccountVO;
+import org.apache.cloudstack.quota.vo.QuotaEmailConfigurationVO;
 import org.apache.cloudstack.quota.vo.QuotaEmailTemplatesVO;
 import org.apache.commons.lang.StringEscapeUtils;
 import org.apache.commons.lang.text.StrSubstitutor;
@@ -80,7 +82,10 @@ public class QuotaAlertManagerImpl extends ManagerBase 
implements QuotaAlertMana
     @Inject
     private QuotaManager _quotaManager;
 
-    private boolean _lockAccountEnforcement = false;
+    @Inject
+    private QuotaEmailConfigurationDao quotaEmailConfigurationDao;
+
+    protected boolean _lockAccountEnforcement = false;
     private String senderAddress;
     protected SMTPMailSender mailSender;
 
@@ -139,55 +144,100 @@ public class QuotaAlertManagerImpl extends ManagerBase 
implements QuotaAlertMana
         return true;
     }
 
+    /**
+     * Returns whether a Quota email type is enabled or not for the provided 
account.
+     */
+    @Override
+    public boolean isQuotaEmailTypeEnabledForAccount(AccountVO account, 
QuotaEmailTemplateTypes quotaEmailTemplateType) {
+        boolean quotaEmailsEnabled = 
QuotaConfig.QuotaEnableEmails.valueIn(account.getAccountId());
+        if (!quotaEmailsEnabled) {
+            logger.debug("Configuration [{}] is disabled for account [{}]. 
Therefore, the account will not receive Quota email of type [{}].", 
QuotaConfig.QuotaEnableEmails.key(), account, quotaEmailTemplateType);
+            return false;
+        }
+
+        QuotaEmailConfigurationVO quotaEmail = 
quotaEmailConfigurationDao.findByAccountIdAndEmailTemplateType(account.getAccountId(),
 quotaEmailTemplateType);
+
+        boolean emailEnabled = quotaEmail == null || quotaEmail.isEnabled();
+        if (emailEnabled) {
+            logger.debug("Quota email [{}] is enabled for account [{}].", 
quotaEmailTemplateType, account);
+        } else {
+            logger.debug("Quota email [{}] has been manually disabled for 
account [{}] through the API quotaConfigureEmail.", quotaEmailTemplateType, 
account);
+        }
+        return emailEnabled;
+    }
+
+
     @Override
     public void checkAndSendQuotaAlertEmails() {
         List<DeferredQuotaEmail> deferredQuotaEmailList = new 
ArrayList<DeferredQuotaEmail>();
-        final BigDecimal zeroBalance = new BigDecimal(0);
+
+        logger.info("Checking and sending quota alert emails.");
         for (final QuotaAccountVO quotaAccount : 
_quotaAcc.listAllQuotaAccount()) {
-            if (logger.isDebugEnabled()) {
-                logger.debug("checkAndSendQuotaAlertEmails accId=" + 
quotaAccount.getId());
-            }
-            BigDecimal accountBalance = quotaAccount.getQuotaBalance();
-            Date balanceDate = quotaAccount.getQuotaBalanceDate();
-            Date alertDate = quotaAccount.getQuotaAlertDate();
-            int lockable = quotaAccount.getQuotaEnforce();
-            BigDecimal thresholdBalance = quotaAccount.getQuotaMinBalance();
-            if (accountBalance != null) {
-                AccountVO account = _accountDao.findById(quotaAccount.getId());
-                if (account == null) {
-                    continue; // the account is removed
-                }
-                logger.debug("checkAndSendQuotaAlertEmails: Check id={} 
bal={}, alertDate={}, lockable={}", account.getId(),
-                        accountBalance, 
DateUtil.displayDateInTimezone(QuotaManagerImpl.getUsageAggregationTimeZone(), 
alertDate),
-                        lockable);
-                if (accountBalance.compareTo(zeroBalance) < 0) {
-                    if (_lockAccountEnforcement && (lockable == 1)) {
-                        if (_quotaManager.isLockable(account)) {
-                            logger.info("Locking account " + 
account.getAccountName() + " due to quota < 0.");
-                            lockAccount(account.getId());
-                        }
-                    }
-                    if (alertDate == null || (balanceDate.after(alertDate) && 
getDifferenceDays(alertDate, new Date()) > 1)) {
-                        logger.info("Sending alert " + 
account.getAccountName() + " due to quota < 0.");
-                        deferredQuotaEmailList.add(new 
DeferredQuotaEmail(account, quotaAccount, 
QuotaConfig.QuotaEmailTemplateTypes.QUOTA_EMPTY));
-                    }
-                } else if (accountBalance.compareTo(thresholdBalance) < 0) {
-                    if (alertDate == null || (balanceDate.after(alertDate) && 
getDifferenceDays(alertDate, new Date()) > 1)) {
-                        logger.info("Sending alert " + 
account.getAccountName() + " due to quota below threshold.");
-                        deferredQuotaEmailList.add(new 
DeferredQuotaEmail(account, quotaAccount, 
QuotaConfig.QuotaEmailTemplateTypes.QUOTA_LOW));
-                    }
-                }
-            }
+            checkQuotaAlertEmailForAccount(deferredQuotaEmailList, 
quotaAccount);
         }
 
         for (DeferredQuotaEmail emailToBeSent : deferredQuotaEmailList) {
-            if (logger.isDebugEnabled()) {
-                logger.debug("checkAndSendQuotaAlertEmails: Attempting to send 
quota alert email to users of account: " + 
emailToBeSent.getAccount().getAccountName());
-            }
+            logger.debug("Attempting to send a quota alert email to users of 
account [{}].", emailToBeSent.getAccount().getAccountName());
             sendQuotaAlert(emailToBeSent);
         }
     }
 
+    /**
+     * Checks a given quota account to see if they should receive any emails. 
First by checking if it has any balance at all, if its account can be found, 
then checks
+     * if they should receive either QUOTA_EMPTY or QUOTA_LOW emails, taking 
into account if these email templates are disabled or not for that account.
+     * */
+    protected void checkQuotaAlertEmailForAccount(List<DeferredQuotaEmail> 
deferredQuotaEmailList, QuotaAccountVO quotaAccount) {
+        logger.debug("Checking {} for email alerts.", quotaAccount);
+        BigDecimal accountBalance = quotaAccount.getQuotaBalance();
+
+        if (accountBalance == null) {
+            logger.debug("{} has a null balance, therefore it will not receive 
quota alert emails.", quotaAccount);
+            return;
+        }
+
+        AccountVO account = _accountDao.findById(quotaAccount.getId());
+        if (account == null) {
+            logger.debug("Account of {} is removed, thus it will not receive 
quota alert emails.", quotaAccount);
+            return;
+        }
+
+        checkBalanceAndAddToEmailList(deferredQuotaEmailList, quotaAccount, 
account, accountBalance);
+    }
+
+    private void checkBalanceAndAddToEmailList(List<DeferredQuotaEmail> 
deferredQuotaEmailList, QuotaAccountVO quotaAccount, AccountVO account, 
BigDecimal accountBalance) {
+        Date balanceDate = quotaAccount.getQuotaBalanceDate();
+        Date alertDate = quotaAccount.getQuotaAlertDate();
+        int lockable = quotaAccount.getQuotaEnforce();
+        BigDecimal thresholdBalance = quotaAccount.getQuotaMinBalance();
+
+        logger.debug("Checking {} with accountBalance [{}], alertDate [{}] and 
lockable [{}] to see if a quota alert email should be sent.", account,
+                accountBalance, 
DateUtil.displayDateInTimezone(QuotaManagerImpl.getUsageAggregationTimeZone(), 
alertDate), lockable);
+
+        boolean shouldSendEmail = alertDate == null || 
(balanceDate.after(alertDate) && getDifferenceDays(alertDate, new Date()) > 1);
+
+        if (accountBalance.compareTo(BigDecimal.ZERO) < 0) {
+            if (_lockAccountEnforcement && lockable == 1 && 
_quotaManager.isLockable(account)) {
+                logger.info("Locking {}, as quota balance is lower than 0.", 
account);
+                lockAccount(account.getId());
+            }
+
+            boolean quotaEmptyEmailEnabled = 
isQuotaEmailTypeEnabledForAccount(account, QuotaEmailTemplateTypes.QUOTA_EMPTY);
+            if (quotaEmptyEmailEnabled && shouldSendEmail) {
+                logger.debug("Adding {} to the deferred emails list, as quota 
balance is lower than 0.", account);
+                deferredQuotaEmailList.add(new DeferredQuotaEmail(account, 
quotaAccount, QuotaEmailTemplateTypes.QUOTA_EMPTY));
+                return;
+            }
+        } else if (accountBalance.compareTo(thresholdBalance) < 0) {
+            boolean quotaLowEmailEnabled = 
isQuotaEmailTypeEnabledForAccount(account, QuotaEmailTemplateTypes.QUOTA_LOW);
+            if (quotaLowEmailEnabled && shouldSendEmail) {
+                logger.debug("Adding {} to the deferred emails list, as quota 
balance [{}] is below the threshold [{}].", account, accountBalance, 
thresholdBalance);
+                deferredQuotaEmailList.add(new DeferredQuotaEmail(account, 
quotaAccount, QuotaEmailTemplateTypes.QUOTA_LOW));
+                return;
+            }
+        }
+        logger.debug("{} will not receive any quota alert emails in this 
round.", account);
+    }
+
     @Override
     public void sendQuotaAlert(DeferredQuotaEmail emailToBeSent) {
         final AccountVO account = emailToBeSent.getAccount();
@@ -285,7 +335,7 @@ public class QuotaAlertManagerImpl extends ManagerBase 
implements QuotaAlertMana
         return optionMap;
     }
 
-    public static long getDifferenceDays(Date d1, Date d2) {
+    public long getDifferenceDays(Date d1, Date d2) {
         long diff = d2.getTime() - d1.getTime();
         return TimeUnit.DAYS.convert(diff, TimeUnit.MILLISECONDS);
     }
diff --git 
a/framework/quota/src/main/java/org/apache/cloudstack/quota/QuotaStatementImpl.java
 
b/framework/quota/src/main/java/org/apache/cloudstack/quota/QuotaStatementImpl.java
index a4ee0e2584e..5ee327fb9a5 100644
--- 
a/framework/quota/src/main/java/org/apache/cloudstack/quota/QuotaStatementImpl.java
+++ 
b/framework/quota/src/main/java/org/apache/cloudstack/quota/QuotaStatementImpl.java
@@ -31,6 +31,8 @@ import 
org.apache.cloudstack.framework.config.dao.ConfigurationDao;
 import org.apache.cloudstack.quota.QuotaAlertManagerImpl.DeferredQuotaEmail;
 import org.apache.cloudstack.quota.constant.QuotaConfig;
 import org.apache.cloudstack.quota.dao.QuotaAccountDao;
+import org.apache.cloudstack.quota.dao.QuotaEmailConfigurationDao;
+import org.apache.cloudstack.quota.dao.QuotaEmailTemplatesDao;
 import org.apache.cloudstack.quota.dao.QuotaUsageDao;
 import org.apache.cloudstack.quota.vo.QuotaAccountVO;
 import org.springframework.stereotype.Component;
@@ -53,6 +55,12 @@ public class QuotaStatementImpl extends ManagerBase 
implements QuotaStatement {
     @Inject
     private ConfigurationDao _configDao;
 
+    @Inject
+    private QuotaEmailConfigurationDao quotaEmailConfigurationDao;
+
+    @Inject
+    private QuotaEmailTemplatesDao quotaEmailTemplatesDao;
+
     final public static int s_LAST_STATEMENT_SENT_DAYS = 6; //ideally should 
be less than 7 days
 
     public enum QuotaStatementPeriods {
@@ -111,29 +119,34 @@ public class QuotaStatementImpl extends ManagerBase 
implements QuotaStatement {
             if (quotaAccount.getQuotaBalance() == null) {
                 continue; // no quota usage for this account ever, ignore
             }
+            AccountVO account = _accountDao.findById(quotaAccount.getId());
+            if (account == null) {
+                logger.debug("Could not find an account corresponding to [{}]. 
Therefore, the statement email will not be sent.", quotaAccount);
+                continue;
+            }
+
+            boolean quotaStatementEmailEnabled = 
_quotaAlert.isQuotaEmailTypeEnabledForAccount(account, 
QuotaConfig.QuotaEmailTemplateTypes.QUOTA_STATEMENT);
+            if (!quotaStatementEmailEnabled) {
+                logger.debug("{} has [{}] email disabled. Therefore the email 
will not be sent.", quotaAccount, 
QuotaConfig.QuotaEmailTemplateTypes.QUOTA_STATEMENT);
+                continue;
+            }
 
             //check if it is statement time
             Calendar interval[] = statementTime(Calendar.getInstance(), 
_period);
 
             Date lastStatementDate = quotaAccount.getLastStatementDate();
             if (interval != null) {
-                AccountVO account = _accountDao.findById(quotaAccount.getId());
-                if (account != null) {
-                    if (lastStatementDate == null || 
getDifferenceDays(lastStatementDate, new Date()) >= s_LAST_STATEMENT_SENT_DAYS 
+ 1) {
-                        BigDecimal quotaUsage = 
_quotaUsage.findTotalQuotaUsage(account.getAccountId(), account.getDomainId(), 
null, interval[0].getTime(), interval[1].getTime());
-                        logger.info("For account=" + quotaAccount.getId() + ", 
quota used = " + quotaUsage);
-                        // send statement
-                        deferredQuotaEmailList.add(new 
DeferredQuotaEmail(account, quotaAccount, quotaUsage, 
QuotaConfig.QuotaEmailTemplateTypes.QUOTA_STATEMENT));
-                    } else {
-                        if (logger.isDebugEnabled()) {
-                            logger.debug("For " + quotaAccount.getId() + " the 
statement has been sent recently");
-
-                        }
-                    }
+                if (lastStatementDate == null || 
getDifferenceDays(lastStatementDate, new Date()) >= s_LAST_STATEMENT_SENT_DAYS 
+ 1) {
+                    BigDecimal quotaUsage = 
_quotaUsage.findTotalQuotaUsage(account.getAccountId(), account.getDomainId(), 
null, interval[0].getTime(), interval[1].getTime());
+                    logger.info("Quota statement for account [{}] has an usage 
of [{}].", quotaAccount, quotaUsage);
+
+                    // send statement
+                    deferredQuotaEmailList.add(new DeferredQuotaEmail(account, 
quotaAccount, quotaUsage, QuotaConfig.QuotaEmailTemplateTypes.QUOTA_STATEMENT));
+                } else {
+                    logger.debug("Quota statement has already been sent 
recently to account [{}].", quotaAccount);
                 }
             } else if (lastStatementDate != null) {
-                logger.info("For " + quotaAccount.getId() + " it is already 
more than " + getDifferenceDays(lastStatementDate, new Date())
-                        + " days, will send statement in next cycle");
+                logger.info("For account {} it is already more than {} days, 
will send statement in next cycle.", quotaAccount.getId(), 
getDifferenceDays(lastStatementDate, new Date()));
             }
         }
 
diff --git 
a/framework/quota/src/main/java/org/apache/cloudstack/quota/constant/QuotaConfig.java
 
b/framework/quota/src/main/java/org/apache/cloudstack/quota/constant/QuotaConfig.java
index 59aa54424cf..df7ffa5c3cd 100644
--- 
a/framework/quota/src/main/java/org/apache/cloudstack/quota/constant/QuotaConfig.java
+++ 
b/framework/quota/src/main/java/org/apache/cloudstack/quota/constant/QuotaConfig.java
@@ -72,6 +72,9 @@ public interface QuotaConfig {
     ConfigKey<String> QuotaEmailFooter = new ConfigKey<>("Advanced", 
String.class, "quota.email.footer", "",
             "Text to be added as a footer for quota emails. Line breaks are 
not automatically inserted between this section and the body.", true, 
ConfigKey.Scope.Domain);
 
+    ConfigKey<Boolean> QuotaEnableEmails = new ConfigKey<>("Advanced", 
Boolean.class, "quota.enable.emails", "true",
+            "Indicates whether Quota emails should be sent or not to accounts. 
When enabled, the behavior for each account can be overridden through the API 
quotaConfigureEmail.", true, ConfigKey.Scope.Account);
+
     enum QuotaEmailTemplateTypes {
         QUOTA_LOW, QUOTA_EMPTY, QUOTA_UNLOCK_ACCOUNT, QUOTA_STATEMENT
     }
diff --git 
a/framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaEmailConfigurationDao.java
 
b/framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaEmailConfigurationDao.java
new file mode 100644
index 00000000000..4bb3395cc11
--- /dev/null
+++ 
b/framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaEmailConfigurationDao.java
@@ -0,0 +1,36 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+package org.apache.cloudstack.quota.dao;
+
+import com.cloud.utils.db.GenericDao;
+import org.apache.cloudstack.quota.constant.QuotaConfig;
+import org.apache.cloudstack.quota.vo.QuotaEmailConfigurationVO;
+
+import java.util.List;
+
+public interface QuotaEmailConfigurationDao extends 
GenericDao<QuotaEmailConfigurationVO, Long> {
+
+    QuotaEmailConfigurationVO findByAccountIdAndEmailTemplateId(long 
accountId, long emailTemplateId);
+
+    QuotaEmailConfigurationVO 
updateQuotaEmailConfiguration(QuotaEmailConfigurationVO 
quotaEmailConfigurationVO);
+
+    void persistQuotaEmailConfiguration(QuotaEmailConfigurationVO 
quotaEmailConfigurationVO);
+
+    List<QuotaEmailConfigurationVO> listByAccount(long accountId);
+
+    QuotaEmailConfigurationVO findByAccountIdAndEmailTemplateType(long 
accountId, QuotaConfig.QuotaEmailTemplateTypes quotaEmailTemplateType);
+}
diff --git 
a/framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaEmailConfigurationDaoImpl.java
 
b/framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaEmailConfigurationDaoImpl.java
new file mode 100644
index 00000000000..9466340ad05
--- /dev/null
+++ 
b/framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaEmailConfigurationDaoImpl.java
@@ -0,0 +1,105 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+package org.apache.cloudstack.quota.dao;
+
+import com.cloud.utils.db.GenericDaoBase;
+import com.cloud.utils.db.JoinBuilder;
+import com.cloud.utils.db.SearchBuilder;
+import com.cloud.utils.db.SearchCriteria;
+import com.cloud.utils.db.Transaction;
+import com.cloud.utils.db.TransactionCallback;
+import com.cloud.utils.db.TransactionLegacy;
+import org.apache.cloudstack.quota.constant.QuotaConfig;
+import org.apache.cloudstack.quota.vo.QuotaEmailConfigurationVO;
+import org.apache.cloudstack.quota.vo.QuotaEmailTemplatesVO;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.PostConstruct;
+import javax.inject.Inject;
+import java.util.List;
+
+@Component
+public class QuotaEmailConfigurationDaoImpl extends 
GenericDaoBase<QuotaEmailConfigurationVO, Long> implements 
QuotaEmailConfigurationDao {
+
+    @Inject
+    private QuotaEmailTemplatesDao quotaEmailTemplatesDao;
+
+    private SearchBuilder<QuotaEmailConfigurationVO> searchBuilderFindByIds;
+
+    private SearchBuilder<QuotaEmailTemplatesVO> 
searchBuilderFindByTemplateName;
+
+    private SearchBuilder<QuotaEmailConfigurationVO> 
searchBuilderFindByTemplateTypeAndAccountId;
+
+    @PostConstruct
+    public void init() {
+        searchBuilderFindByIds = createSearchBuilder();
+        searchBuilderFindByIds.and("account_id", 
searchBuilderFindByIds.entity().getAccountId(), SearchCriteria.Op.EQ);
+        searchBuilderFindByIds.and("email_template_id", 
searchBuilderFindByIds.entity().getEmailTemplateId(), SearchCriteria.Op.EQ);
+        searchBuilderFindByIds.done();
+
+        searchBuilderFindByTemplateName = 
quotaEmailTemplatesDao.createSearchBuilder();
+        searchBuilderFindByTemplateName.and("template_name", 
searchBuilderFindByTemplateName.entity().getTemplateName(), 
SearchCriteria.Op.EQ);
+
+        searchBuilderFindByTemplateTypeAndAccountId = createSearchBuilder();
+        searchBuilderFindByTemplateTypeAndAccountId.and("account_id", 
searchBuilderFindByTemplateTypeAndAccountId.entity().getAccountId(), 
SearchCriteria.Op.EQ);
+        searchBuilderFindByTemplateTypeAndAccountId.join("email_template_id", 
searchBuilderFindByTemplateName, 
searchBuilderFindByTemplateName.entity().getId(),
+                
searchBuilderFindByTemplateTypeAndAccountId.entity().getEmailTemplateId(), 
JoinBuilder.JoinType.INNER);
+
+        searchBuilderFindByTemplateName.done();
+        searchBuilderFindByTemplateTypeAndAccountId.done();
+    }
+
+    @Override
+    public QuotaEmailConfigurationVO findByAccountIdAndEmailTemplateId(long 
accountId, long emailTemplateId) {
+        SearchCriteria<QuotaEmailConfigurationVO> sc = 
searchBuilderFindByIds.create();
+        sc.setParameters("account_id", accountId);
+        sc.setParameters("email_template_id", emailTemplateId);
+        return Transaction.execute(TransactionLegacy.USAGE_DB, 
(TransactionCallback<QuotaEmailConfigurationVO>) status -> findOneBy(sc));
+    }
+
+    @Override
+    public QuotaEmailConfigurationVO 
updateQuotaEmailConfiguration(QuotaEmailConfigurationVO 
quotaEmailConfigurationVO) {
+        SearchCriteria<QuotaEmailConfigurationVO> sc = 
searchBuilderFindByIds.create();
+        sc.setParameters("account_id", 
quotaEmailConfigurationVO.getAccountId());
+        sc.setParameters("email_template_id", 
quotaEmailConfigurationVO.getEmailTemplateId());
+        Transaction.execute(TransactionLegacy.USAGE_DB, 
(TransactionCallback<Integer>) status -> update(quotaEmailConfigurationVO, sc));
+
+        return quotaEmailConfigurationVO;
+    }
+
+    @Override
+    public void persistQuotaEmailConfiguration(QuotaEmailConfigurationVO 
quotaEmailConfigurationVO) {
+        Transaction.execute(TransactionLegacy.USAGE_DB, 
(TransactionCallback<QuotaEmailConfigurationVO>) status -> 
persist(quotaEmailConfigurationVO));
+    }
+
+    @Override
+    public List<QuotaEmailConfigurationVO> listByAccount(long accountId) {
+        SearchCriteria<QuotaEmailConfigurationVO> sc = 
searchBuilderFindByIds.create();
+        sc.setParameters("account_id", accountId);
+
+        return Transaction.execute(TransactionLegacy.USAGE_DB, 
(TransactionCallback<List<QuotaEmailConfigurationVO>>) status -> listBy(sc));
+    }
+
+    @Override
+    public QuotaEmailConfigurationVO findByAccountIdAndEmailTemplateType(long 
accountId, QuotaConfig.QuotaEmailTemplateTypes quotaEmailTemplateType) {
+        SearchCriteria<QuotaEmailConfigurationVO> sc = 
searchBuilderFindByTemplateTypeAndAccountId.create();
+        sc.setParameters("account_id", accountId);
+        sc.setJoinParameters("email_template_id", "template_name", 
quotaEmailTemplateType.toString());
+
+        return Transaction.execute(TransactionLegacy.USAGE_DB, 
(TransactionCallback<QuotaEmailConfigurationVO>) status -> findOneBy(sc));
+    }
+}
diff --git 
a/framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaEmailTemplatesDao.java
 
b/framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaEmailTemplatesDao.java
index 573a7539744..346bb9a4a6a 100644
--- 
a/framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaEmailTemplatesDao.java
+++ 
b/framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaEmailTemplatesDao.java
@@ -24,4 +24,6 @@ import java.util.List;
 public interface QuotaEmailTemplatesDao extends 
GenericDao<QuotaEmailTemplatesVO, Long> {
     List<QuotaEmailTemplatesVO> listAllQuotaEmailTemplates(String 
templateName);
     boolean updateQuotaEmailTemplate(QuotaEmailTemplatesVO template);
+
+    QuotaEmailTemplatesVO findById(long id);
 }
diff --git 
a/framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaEmailTemplatesDaoImpl.java
 
b/framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaEmailTemplatesDaoImpl.java
index b44ace0a1ff..c27f2df299b 100644
--- 
a/framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaEmailTemplatesDaoImpl.java
+++ 
b/framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaEmailTemplatesDaoImpl.java
@@ -66,4 +66,9 @@ public class QuotaEmailTemplatesDaoImpl extends 
GenericDaoBase<QuotaEmailTemplat
             }
         });
     }
+
+    @Override
+    public QuotaEmailTemplatesVO findById(long id) {
+        return Transaction.execute(TransactionLegacy.USAGE_DB, 
(TransactionCallback<QuotaEmailTemplatesVO>) status -> 
QuotaEmailTemplatesDaoImpl.super.findById(id));
+    }
 }
diff --git 
a/framework/quota/src/main/java/org/apache/cloudstack/quota/vo/QuotaEmailConfigurationVO.java
 
b/framework/quota/src/main/java/org/apache/cloudstack/quota/vo/QuotaEmailConfigurationVO.java
new file mode 100644
index 00000000000..e50c7ce6250
--- /dev/null
+++ 
b/framework/quota/src/main/java/org/apache/cloudstack/quota/vo/QuotaEmailConfigurationVO.java
@@ -0,0 +1,68 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+package org.apache.cloudstack.quota.vo;
+
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.Table;
+
+@Entity
+@Table(name = "quota_email_configuration")
+public class QuotaEmailConfigurationVO {
+
+    @Column(name = "account_id")
+    private long accountId;
+
+    @Column(name = "email_template_id")
+    private long emailTemplateId;
+
+    @Column(name = "enabled")
+    private boolean enabled;
+
+    public QuotaEmailConfigurationVO() {
+    }
+
+    public QuotaEmailConfigurationVO(long accountId, long emailTemplateId, 
boolean enable) {
+        this.accountId = accountId;
+        this.emailTemplateId = emailTemplateId;
+        this.enabled = enable;
+    }
+
+    public long getAccountId() {
+        return accountId;
+    }
+
+    public void setAccountId(long accountId) {
+        this.accountId = accountId;
+    }
+
+    public long getEmailTemplateId() {
+        return emailTemplateId;
+    }
+
+    public void setEmailTemplateId(long emailTemplateId) {
+        this.emailTemplateId = emailTemplateId;
+    }
+
+    public boolean isEnabled() {
+        return enabled;
+    }
+
+    public void setEnabled(boolean enabled) {
+        this.enabled = enabled;
+    }
+}
diff --git 
a/framework/quota/src/main/resources/META-INF/cloudstack/quota/spring-framework-quota-context.xml
 
b/framework/quota/src/main/resources/META-INF/cloudstack/quota/spring-framework-quota-context.xml
index 5f1c274f049..e634321208f 100644
--- 
a/framework/quota/src/main/resources/META-INF/cloudstack/quota/spring-framework-quota-context.xml
+++ 
b/framework/quota/src/main/resources/META-INF/cloudstack/quota/spring-framework-quota-context.xml
@@ -30,5 +30,6 @@
        <bean id="QuotaManager" 
class="org.apache.cloudstack.quota.QuotaManagerImpl" />
     <bean id="QuotaAlertManager" 
class="org.apache.cloudstack.quota.QuotaAlertManagerImpl" />
        <bean id="QuotaStatement" 
class="org.apache.cloudstack.quota.QuotaStatementImpl" />
+       <bean id="QuotaEmailConfigurationDao" 
class="org.apache.cloudstack.quota.dao.QuotaEmailConfigurationDaoImpl"/>
 
 </beans>
diff --git 
a/framework/quota/src/test/java/org/apache/cloudstack/quota/QuotaAlertManagerImplTest.java
 
b/framework/quota/src/test/java/org/apache/cloudstack/quota/QuotaAlertManagerImplTest.java
index a0dd4c22814..54d4f1d5b69 100644
--- 
a/framework/quota/src/test/java/org/apache/cloudstack/quota/QuotaAlertManagerImplTest.java
+++ 
b/framework/quota/src/test/java/org/apache/cloudstack/quota/QuotaAlertManagerImplTest.java
@@ -30,9 +30,12 @@ import javax.naming.ConfigurationException;
 import org.apache.cloudstack.framework.config.dao.ConfigurationDao;
 import org.apache.cloudstack.quota.constant.QuotaConfig;
 import org.apache.cloudstack.quota.dao.QuotaAccountDao;
+import org.apache.cloudstack.quota.dao.QuotaEmailConfigurationDaoImpl;
 import org.apache.cloudstack.quota.dao.QuotaEmailTemplatesDao;
 import org.apache.cloudstack.quota.vo.QuotaAccountVO;
+import org.apache.cloudstack.quota.vo.QuotaEmailConfigurationVO;
 import org.apache.cloudstack.quota.vo.QuotaEmailTemplatesVO;
+import org.junit.Assert;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -71,6 +74,9 @@ public class QuotaAlertManagerImplTest extends TestCase {
     @Mock
     private ConfigurationDao configDao;
 
+    @Mock
+    private QuotaEmailConfigurationDaoImpl quotaEmailConfigurationDaoMock;
+
     @Mock
     private QuotaAccountVO quotaAccountVOMock;
 
@@ -92,43 +98,142 @@ public class QuotaAlertManagerImplTest extends TestCase {
 
     @Before
     public void setup() throws IllegalAccessException, NoSuchFieldException, 
ConfigurationException {
-        TransactionLegacy.open("QuotaAlertManagerImplTest");
-    }
-
-    @Test
-    public void testCheckAndSendQuotaAlertEmails() {
         AccountVO accountVO = new AccountVO();
         accountVO.setId(2L);
         accountVO.setDomainId(1L);
         accountVO.setType(Account.Type.NORMAL);
         
Mockito.when(accountDao.findById(Mockito.anyLong())).thenReturn(accountVO);
 
-        QuotaAccountVO acc = new QuotaAccountVO(2L);
-        acc.setQuotaBalance(new BigDecimal(404));
-        acc.setQuotaMinBalance(new BigDecimal(100));
-        acc.setQuotaBalanceDate(new Date());
-        acc.setQuotaAlertDate(null);
-        acc.setQuotaEnforce(0);
-        List<QuotaAccountVO> accounts = new ArrayList<>();
-        accounts.add(acc);
-        Mockito.when(quotaAcc.listAllQuotaAccount()).thenReturn(accounts);
+        Mockito.doReturn(new 
BigDecimal(404)).when(quotaAccountVOMock).getQuotaBalance();
+        Mockito.doReturn(new 
BigDecimal(100)).when(quotaAccountVOMock).getQuotaMinBalance();
+        
Mockito.doReturn(balanceDateMock).when(quotaAccountVOMock).getQuotaBalanceDate();
+        Mockito.doReturn(null).when(quotaAccountVOMock).getQuotaAlertDate();
+        Mockito.doReturn(0).when(quotaAccountVOMock).getQuotaEnforce();
 
-        // Don't test sendQuotaAlert yet
-        
Mockito.doNothing().when(quotaAlertManager).sendQuotaAlert(Mockito.any(QuotaAlertManagerImpl.DeferredQuotaEmail.class));
-        
Mockito.lenient().doReturn(true).when(quotaAlertManager).lockAccount(Mockito.anyLong());
+        TransactionLegacy.open("QuotaAlertManagerImplTest");
+    }
+
+    @Test
+    public void 
isQuotaEmailTypeEnabledForAccountTestConfigurationIsEnabledAndEmailIsConfiguredReturnConfiguredValue()
 {
+        boolean expectedValue = !QuotaConfig.QuotaEnableEmails.value();
+        QuotaEmailConfigurationVO quotaEmailConfigurationVoMock = 
Mockito.mock(QuotaEmailConfigurationVO.class);
+        
Mockito.when(quotaEmailConfigurationVoMock.isEnabled()).thenReturn(expectedValue);
+        
Mockito.doReturn(quotaEmailConfigurationVoMock).when(quotaEmailConfigurationDaoMock).findByAccountIdAndEmailTemplateType(Mockito.anyLong(),
 Mockito.any(QuotaConfig.QuotaEmailTemplateTypes.class));
+
+        boolean result = 
quotaAlertManager.isQuotaEmailTypeEnabledForAccount(accountMock, 
QuotaConfig.QuotaEmailTemplateTypes.QUOTA_EMPTY);
+
+        Assert.assertEquals(expectedValue, result);
+    }
 
-        // call real method on send monthly statement
-        
Mockito.doCallRealMethod().when(quotaAlertManager).checkAndSendQuotaAlertEmails();
+    @Test
+    public void 
isQuotaEmailTypeEnabledForAccountTestConfigurationIsEnabledAndEmailIsNotConfiguredReturnDefaultValue()
 {
+        boolean defaultValue = QuotaConfig.QuotaEnableEmails.value();
+
+        boolean result = 
quotaAlertManager.isQuotaEmailTypeEnabledForAccount(accountMock, 
QuotaConfig.QuotaEmailTemplateTypes.QUOTA_EMPTY);
+
+        Assert.assertEquals(defaultValue, result);
+    }
+
+    @Test
+    public void checkQuotaAlertEmailForAccountTestNullAccountBalance() {
+        Mockito.doReturn(null).when(quotaAccountVOMock).getQuotaBalance();
+        
quotaAlertManager.checkQuotaAlertEmailForAccount(deferredQuotaEmailListMock, 
quotaAccountVOMock);
+        Mockito.verify(accountDao, Mockito.never()).findById(Mockito.any());
+    }
+
+    @Test
+    public void checkQuotaAlertEmailForAccountTestNullAccount() {
+        Mockito.doReturn(new 
BigDecimal(1)).when(quotaAccountVOMock).getQuotaBalance();
+        Mockito.doReturn(null).when(accountDao).findById(Mockito.any());
+        
quotaAlertManager.checkQuotaAlertEmailForAccount(deferredQuotaEmailListMock, 
quotaAccountVOMock);
+        Mockito.verify(quotaAccountVOMock, 
Mockito.never()).getQuotaBalanceDate();
+    }
+
+    @Test
+    public void checkQuotaAlertEmailForAccountTestEnoughBalance() {
+        
quotaAlertManager.checkQuotaAlertEmailForAccount(deferredQuotaEmailListMock, 
quotaAccountVOMock);
+        Mockito.verify(quotaAlertManager, 
Mockito.never()).lockAccount(Mockito.anyLong());
+        Mockito.verify(deferredQuotaEmailListMock, 
Mockito.never()).add(Mockito.any());
+    }
+
+    @Test
+    public void 
checkQuotaAlertEmailForAccountTestBalanceLowerThanZeroAndLockAccountEnforcementFalse()
 {
+        Mockito.doReturn(new 
BigDecimal(-1)).when(quotaAccountVOMock).getQuotaBalance();
+
+        quotaAlertManager._lockAccountEnforcement = false;
+        Mockito.doReturn(1).when(quotaAccountVOMock).getQuotaEnforce();
+        
quotaAlertManager.checkQuotaAlertEmailForAccount(deferredQuotaEmailListMock, 
quotaAccountVOMock);
+        Mockito.verify(quotaAlertManager, 
Mockito.never()).lockAccount(Mockito.anyLong());
+    }
+
+    @Test
+    public void 
checkQuotaAlertEmailForAccountTestBalanceLowerThanZeroAndLockableFalse() {
+        Mockito.doReturn(new 
BigDecimal(-1)).when(quotaAccountVOMock).getQuotaBalance();
+
+        quotaAlertManager._lockAccountEnforcement = true;
+        Mockito.doReturn(1).when(quotaAccountVOMock).getQuotaEnforce();
+        
Mockito.doReturn(false).when(quotaManagerMock).isLockable(Mockito.any());
+        
quotaAlertManager.checkQuotaAlertEmailForAccount(deferredQuotaEmailListMock, 
quotaAccountVOMock);
+        Mockito.verify(quotaAlertManager, 
Mockito.never()).lockAccount(Mockito.anyLong());
+    }
+
+    @Test
+    public void 
checkQuotaAlertEmailForAccountTestBalanceLowerThanZeroAndIsLockableFalse() {
+        Mockito.doReturn(new 
BigDecimal(-1)).when(quotaAccountVOMock).getQuotaBalance();
+
+        quotaAlertManager._lockAccountEnforcement = true;
+        Mockito.doReturn(1).when(quotaAccountVOMock).getQuotaEnforce();
+        
Mockito.doReturn(false).when(quotaManagerMock).isLockable(Mockito.any());
+        
quotaAlertManager.checkQuotaAlertEmailForAccount(deferredQuotaEmailListMock, 
quotaAccountVOMock);
+        Mockito.verify(quotaAlertManager, 
Mockito.never()).lockAccount(Mockito.anyLong());
+    }
+
+    @Test
+    public void 
checkQuotaAlertEmailForAccountTestBalanceLowerThanZeroAndLockAccount() {
+        Mockito.doReturn(new 
BigDecimal(-1)).when(quotaAccountVOMock).getQuotaBalance();
+
+        quotaAlertManager._lockAccountEnforcement = true;
+        Mockito.doReturn(1).when(quotaAccountVOMock).getQuotaEnforce();
+        
Mockito.doReturn(true).when(quotaManagerMock).isLockable(Mockito.any());
+        
quotaAlertManager.checkQuotaAlertEmailForAccount(deferredQuotaEmailListMock, 
quotaAccountVOMock);
+        Mockito.verify(quotaAlertManager).lockAccount(Mockito.anyLong());
+    }
+
+    @Test
+    public void 
checkQuotaAlertEmailForAccountTestBalanceLowerThanZeroAndAlertDateNotNullAndBalanceDateNotAfter()
 {
+        Mockito.doReturn(new 
Date()).when(quotaAccountVOMock).getQuotaAlertDate();
+        Mockito.doReturn(new 
BigDecimal(-1)).when(quotaAccountVOMock).getQuotaBalance();
+        Mockito.doReturn(false).when(balanceDateMock).after(Mockito.any());
+
+        
quotaAlertManager.checkQuotaAlertEmailForAccount(deferredQuotaEmailListMock, 
quotaAccountVOMock);
+        Mockito.verify(deferredQuotaEmailListMock, 
Mockito.never()).add(Mockito.any());
+    }
+
+    public void 
checkQuotaAlertEmailForAccountTestBalanceLowerThanZeroAndAlertDateNotNullAndGetDifferenceDaysSmallerThanOne()
 {
+        Mockito.doReturn(new 
Date()).when(quotaAccountVOMock).getQuotaAlertDate();
+        Mockito.doReturn(new 
BigDecimal(-1)).when(quotaAccountVOMock).getQuotaBalance();
+        Mockito.doReturn(true).when(balanceDateMock).after(Mockito.any());
+        
Mockito.doReturn(0L).when(quotaAlertManager).getDifferenceDays(Mockito.any(), 
Mockito.any());
+
+        
quotaAlertManager.checkQuotaAlertEmailForAccount(deferredQuotaEmailListMock, 
quotaAccountVOMock);
+        Mockito.verify(deferredQuotaEmailListMock, 
Mockito.never()).add(Mockito.any());
+    }
+
+    public void 
checkQuotaAlertEmailForAccountTestBalanceLowerThanZeroAndAlertDateNotNullAndBalanceAfterAndDifferenceBiggerThanOne()
 {
+        Mockito.doReturn(new 
Date()).when(quotaAccountVOMock).getQuotaAlertDate();
+        Mockito.doReturn(new 
BigDecimal(-1)).when(quotaAccountVOMock).getQuotaBalance();
+        Mockito.doReturn(true).when(balanceDateMock).after(Mockito.any());
+        
Mockito.doReturn(2).when(quotaAlertManager).getDifferenceDays(Mockito.any(), 
Mockito.any());
+
+        
quotaAlertManager.checkQuotaAlertEmailForAccount(deferredQuotaEmailListMock, 
quotaAccountVOMock);
+        Mockito.verify(deferredQuotaEmailListMock).add(Mockito.any());
+    }
 
-        // Case1: valid balance, no email should be sent
-        quotaAlertManager.checkAndSendQuotaAlertEmails();
-        Mockito.verify(quotaAlertManager, 
Mockito.times(0)).sendQuotaAlert(Mockito.any(QuotaAlertManagerImpl.DeferredQuotaEmail.class));
+    public void 
checkQuotaAlertEmailForAccountTestBalanceLowerThanZeroAndAlertDateNull() {
+        Mockito.doReturn(new 
BigDecimal(-1)).when(quotaAccountVOMock).getQuotaBalance();
 
-        // Case2: low balance, email should be sent
-        accounts.get(0).setQuotaBalance(new BigDecimal(99));
-        //Mockito.when(quotaAcc.listAll()).thenReturn(accounts);
-        quotaAlertManager.checkAndSendQuotaAlertEmails();
-        Mockito.verify(quotaAlertManager, 
Mockito.times(1)).sendQuotaAlert(Mockito.any(QuotaAlertManagerImpl.DeferredQuotaEmail.class));
+        
quotaAlertManager.checkQuotaAlertEmailForAccount(deferredQuotaEmailListMock, 
quotaAccountVOMock);
+        Mockito.verify(deferredQuotaEmailListMock).add(Mockito.any());
     }
 
     @Test
@@ -196,12 +301,12 @@ public class QuotaAlertManagerImplTest extends TestCase {
     @Test
     public void testGetDifferenceDays() {
         Date now = new Date();
-        assertTrue(QuotaAlertManagerImpl.getDifferenceDays(now, now) == 0L);
+        assertTrue(quotaAlertManager.getDifferenceDays(now, now) == 0L);
         Calendar c = Calendar.getInstance();
         c.setTimeZone(TimeZone.getTimeZone("UTC"));
         Calendar c2 = (Calendar)c.clone();
         c2.add(Calendar.DATE, 1);
-        assertEquals(1L, QuotaAlertManagerImpl.getDifferenceDays(c.getTime(), 
c2.getTime()));
+        assertEquals(1L, quotaAlertManager.getDifferenceDays(c.getTime(), 
c2.getTime()));
     }
 
     @Test
diff --git 
a/framework/quota/src/test/java/org/apache/cloudstack/quota/QuotaStatementTest.java
 
b/framework/quota/src/test/java/org/apache/cloudstack/quota/QuotaStatementTest.java
index baf749cd0c9..507834fef41 100644
--- 
a/framework/quota/src/test/java/org/apache/cloudstack/quota/QuotaStatementTest.java
+++ 
b/framework/quota/src/test/java/org/apache/cloudstack/quota/QuotaStatementTest.java
@@ -16,7 +16,6 @@
 // under the License.
 package org.apache.cloudstack.quota;
 
-import java.io.UnsupportedEncodingException;
 import java.lang.reflect.Field;
 import java.math.BigDecimal;
 import java.util.ArrayList;
@@ -24,17 +23,21 @@ import java.util.Calendar;
 import java.util.Date;
 import java.util.List;
 
-import javax.mail.MessagingException;
 import javax.naming.ConfigurationException;
 
 import org.apache.cloudstack.framework.config.dao.ConfigurationDao;
 import org.apache.cloudstack.quota.QuotaStatementImpl.QuotaStatementPeriods;
+import org.apache.cloudstack.quota.constant.QuotaConfig;
 import org.apache.cloudstack.quota.dao.QuotaAccountDao;
+import org.apache.cloudstack.quota.dao.QuotaEmailConfigurationDaoImpl;
+import org.apache.cloudstack.quota.dao.QuotaEmailTemplatesDao;
 import org.apache.cloudstack.quota.dao.QuotaUsageDao;
 import org.apache.cloudstack.quota.vo.QuotaAccountVO;
+import org.apache.cloudstack.quota.vo.QuotaEmailTemplatesVO;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.InjectMocks;
 import org.mockito.Mock;
 import org.mockito.Mockito;
 import org.mockito.Spy;
@@ -60,7 +63,20 @@ public class QuotaStatementTest extends TestCase {
     @Mock
     QuotaAlertManager alertManager;
 
+    @Mock
+    QuotaEmailConfigurationDaoImpl quotaEmailConfigurationDaoMock;
+
+    @Mock
+    QuotaEmailTemplatesDao quotaEmailTemplatesDaoMock;
+
+    @Mock
+    QuotaEmailTemplatesVO quotaEmailTemplatesVOMock;
+
+    @Mock
+    List<QuotaEmailTemplatesVO> listMock;
+
     @Spy
+    @InjectMocks
     QuotaStatementImpl quotaStatement = new QuotaStatementImpl();
 
     private void injectMockToField(Object mock, String fieldName) throws 
NoSuchFieldException, IllegalAccessException {
@@ -227,7 +243,10 @@ public class QuotaStatementTest extends TestCase {
 
 
     @Test
-    public void testSendStatement() throws UnsupportedEncodingException, 
MessagingException {
+    public void sendStatementTestUnconfiguredEmail() {
+        boolean defaultConfigurationValue = 
QuotaConfig.QuotaEnableEmails.value();
+        
Mockito.doReturn(defaultConfigurationValue).when(alertManager).isQuotaEmailTypeEnabledForAccount(Mockito.any(AccountVO.class),
 Mockito.any(QuotaConfig.QuotaEmailTemplateTypes.class));
+
         Calendar date = Calendar.getInstance();
         AccountVO accountVO = new AccountVO();
         accountVO.setId(2L);
@@ -252,4 +271,46 @@ public class QuotaStatementTest extends TestCase {
         }
     }
 
+    @Test
+    public void sendStatementTestEnabledEmail() {
+        
Mockito.doReturn(true).when(alertManager).isQuotaEmailTypeEnabledForAccount(Mockito.any(AccountVO.class),
 Mockito.any(QuotaConfig.QuotaEmailTemplateTypes.class));
+
+        Calendar date = Calendar.getInstance();
+        AccountVO accountVO = new AccountVO();
+        accountVO.setId(2L);
+        accountVO.setDomainId(1L);
+        
Mockito.lenient().when(accountDao.findById(Mockito.anyLong())).thenReturn(accountVO);
+
+        QuotaAccountVO acc = new QuotaAccountVO(2L);
+        acc.setQuotaBalance(new BigDecimal(404));
+        acc.setLastStatementDate(null);
+        List<QuotaAccountVO> accounts = new ArrayList<>();
+        accounts.add(acc);
+        
Mockito.lenient().when(quotaAcc.listAllQuotaAccount()).thenReturn(accounts);
+
+        
Mockito.lenient().when(quotaUsage.findTotalQuotaUsage(Mockito.anyLong(), 
Mockito.anyLong(), Mockito.anyInt(), Mockito.any(Date.class), 
Mockito.any(Date.class)))
+                .thenReturn(new BigDecimal(100));
+
+        // call real method on send monthly statement
+        quotaStatement.sendStatement();
+        Calendar period[] = quotaStatement.statementTime(date, 
QuotaStatementPeriods.MONTHLY);
+        if (period != null){
+            Mockito.verify(alertManager, 
Mockito.times(1)).sendQuotaAlert(Mockito.any(QuotaAlertManagerImpl.DeferredQuotaEmail.class));
+        }
+    }
+
+    @Test
+    public void sendStatementTestDisabledEmail() {
+        QuotaAccountVO quotaAccountVoMock = Mockito.mock(QuotaAccountVO.class);
+        
Mockito.when(quotaAccountVoMock.getQuotaBalance()).thenReturn(BigDecimal.ONE);
+        
Mockito.when(quotaAcc.listAllQuotaAccount()).thenReturn(List.of(quotaAccountVoMock));
+        AccountVO accountVoMock = Mockito.mock(AccountVO.class);
+        
Mockito.doReturn(accountVoMock).when(accountDao).findById(Mockito.anyLong());
+        
Mockito.doReturn(false).when(alertManager).isQuotaEmailTypeEnabledForAccount(Mockito.any(AccountVO.class),
 Mockito.any(QuotaConfig.QuotaEmailTemplateTypes.class));
+
+        quotaStatement.sendStatement();
+
+        Mockito.verify(quotaStatement, 
Mockito.never()).statementTime(Mockito.any(), Mockito.any());
+    }
+
 }
diff --git 
a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaConfigureEmailCmd.java
 
b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaConfigureEmailCmd.java
new file mode 100644
index 00000000000..01d9ffc1529
--- /dev/null
+++ 
b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaConfigureEmailCmd.java
@@ -0,0 +1,79 @@
+//Licensed to the Apache Software Foundation (ASF) under one
+//or more contributor license agreements.  See the NOTICE file
+//distributed with this work for additional information
+//regarding copyright ownership.  The ASF licenses this file
+//to you under the Apache License, Version 2.0 (the
+//"License"); you may not use this file except in compliance
+//with the License.  You may obtain a copy of the License at
+//
+//http://www.apache.org/licenses/LICENSE-2.0
+//
+//Unless required by applicable law or agreed to in writing,
+//software distributed under the License is distributed on an
+//"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+//KIND, either express or implied.  See the License for the
+//specific language governing permissions and limitations
+//under the License.
+package org.apache.cloudstack.api.command;
+
+import com.cloud.utils.Pair;
+import org.apache.cloudstack.api.APICommand;
+import org.apache.cloudstack.api.ApiConstants;
+import org.apache.cloudstack.api.BaseCmd;
+import org.apache.cloudstack.api.Parameter;
+import org.apache.cloudstack.api.response.AccountResponse;
+import org.apache.cloudstack.api.response.QuotaConfigureEmailResponse;
+import org.apache.cloudstack.api.response.QuotaResponseBuilder;
+import org.apache.cloudstack.quota.vo.QuotaEmailConfigurationVO;
+
+import javax.inject.Inject;
+
+@APICommand(name = "quotaConfigureEmail", responseObject = 
QuotaConfigureEmailResponse.class, description = "Configure a quota email 
template", since = "4.20.0.0",
+        requestHasSensitiveInfo = false, responseHasSensitiveInfo = false)
+public class QuotaConfigureEmailCmd extends BaseCmd {
+
+    @Parameter(name = ApiConstants.ACCOUNT_ID, type = CommandType.UUID, 
entityType = AccountResponse.class, required = true,
+            description = "Account ID for which to configure quota template 
email or min balance")
+    private long accountId;
+
+    @Parameter(name = ApiConstants.TEMPLATE_NAME, type = CommandType.STRING, 
description = "Quota email template name which should be configured")
+    private String templateName;
+
+    @Parameter(name = ApiConstants.ENABLE, type = CommandType.BOOLEAN, 
description = "If the quota email template should be enabled")
+    private Boolean enable;
+
+    @Parameter(name = "minbalance", type = CommandType.DOUBLE, description = 
"New quota account min balance")
+    private Double minBalance;
+
+    @Inject
+    private QuotaResponseBuilder responseBuilder;
+
+    @Override
+    public void execute() {
+        Pair<QuotaEmailConfigurationVO, Double> result = 
responseBuilder.configureQuotaEmail(this);
+        QuotaConfigureEmailResponse quotaConfigureEmailResponse = 
responseBuilder.createQuotaConfigureEmailResponse(result.first(), 
result.second(), accountId);
+        quotaConfigureEmailResponse.setResponseName(getCommandName());
+        this.setResponseObject(quotaConfigureEmailResponse);
+    }
+
+    @Override
+    public long getEntityOwnerId() {
+        return accountId;
+    }
+
+    public long getAccountId() {
+        return accountId;
+    }
+
+    public String getTemplateName() {
+        return templateName;
+    }
+
+    public Boolean getEnable() {
+        return enable;
+    }
+
+    public Double getMinBalance() {
+        return minBalance;
+    }
+}
diff --git 
a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaListEmailConfigurationCmd.java
 
b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaListEmailConfigurationCmd.java
new file mode 100644
index 00000000000..8915158461f
--- /dev/null
+++ 
b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaListEmailConfigurationCmd.java
@@ -0,0 +1,54 @@
+//Licensed to the Apache Software Foundation (ASF) under one
+//or more contributor license agreements.  See the NOTICE file
+//distributed with this work for additional information
+//regarding copyright ownership.  The ASF licenses this file
+//to you under the Apache License, Version 2.0 (the
+//"License"); you may not use this file except in compliance
+//with the License.  You may obtain a copy of the License at
+//
+//http://www.apache.org/licenses/LICENSE-2.0
+//
+//Unless required by applicable law or agreed to in writing,
+//software distributed under the License is distributed on an
+//"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+//KIND, either express or implied.  See the License for the
+//specific language governing permissions and limitations
+//under the License.
+package org.apache.cloudstack.api.command;
+
+import com.cloud.user.Account;
+import org.apache.cloudstack.api.APICommand;
+import org.apache.cloudstack.api.ApiConstants;
+import org.apache.cloudstack.api.BaseCmd;
+import org.apache.cloudstack.api.Parameter;
+import org.apache.cloudstack.api.response.AccountResponse;
+import org.apache.cloudstack.api.response.ListResponse;
+import org.apache.cloudstack.api.response.QuotaConfigureEmailResponse;
+import org.apache.cloudstack.api.response.QuotaResponseBuilder;
+
+import javax.inject.Inject;
+
+@APICommand(name = "quotaListEmailConfiguration", responseObject = 
QuotaConfigureEmailResponse.class, description = "List quota email template 
configurations", since = "4.20.0.0",
+        requestHasSensitiveInfo = false, responseHasSensitiveInfo = false)
+public class QuotaListEmailConfigurationCmd extends BaseCmd {
+
+    @Parameter(name = ApiConstants.ACCOUNT_ID, type = 
BaseCmd.CommandType.UUID, entityType = AccountResponse.class, required = true,
+            description = "Account ID for which to list quota template email 
configurations")
+    private long accountId;
+
+    @Inject
+    private QuotaResponseBuilder responseBuilder;
+
+    @Override
+    public void execute() {
+        ListResponse<QuotaConfigureEmailResponse> response = new 
ListResponse<>();
+        
response.setResponses(responseBuilder.listEmailConfiguration(accountId));
+        response.setResponseName(getCommandName());
+        setResponseObject(response);
+    }
+
+    @Override
+    public long getEntityOwnerId() {
+        return Account.ACCOUNT_ID_SYSTEM;
+    }
+}
diff --git 
a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaConfigureEmailResponse.java
 
b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaConfigureEmailResponse.java
new file mode 100644
index 00000000000..4f84a2c2828
--- /dev/null
+++ 
b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaConfigureEmailResponse.java
@@ -0,0 +1,78 @@
+//Licensed to the Apache Software Foundation (ASF) under one
+//or more contributor license agreements.  See the NOTICE file
+//distributed with this work for additional information
+//regarding copyright ownership.  The ASF licenses this file
+//to you under the Apache License, Version 2.0 (the
+//"License"); you may not use this file except in compliance
+//with the License.  You may obtain a copy of the License at
+//
+//http://www.apache.org/licenses/LICENSE-2.0
+//
+//Unless required by applicable law or agreed to in writing,
+//software distributed under the License is distributed on an
+//"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+//KIND, either express or implied.  See the License for the
+//specific language governing permissions and limitations
+//under the License.
+package org.apache.cloudstack.api.response;
+
+import com.cloud.serializer.Param;
+import com.google.gson.annotations.SerializedName;
+import org.apache.cloudstack.api.BaseResponse;
+
+
+public class QuotaConfigureEmailResponse extends BaseResponse {
+
+    @SerializedName("account")
+    @Param(description = "The configured account's id.")
+    private String accountId;
+
+    @SerializedName("templatename")
+    @Param(description = "The template's name.")
+    private String templateName;
+
+    @SerializedName("enabled")
+    @Param(description = "Whether the template is enabled.")
+    private Boolean enabled;
+
+    @SerializedName("minbalance")
+    @Param(description = "The configured account's min balance.")
+    private Double minBalance;
+
+    public QuotaConfigureEmailResponse() {
+        super("quotaconfigureemail");
+        setResponseName("");
+    }
+
+    public String getAccountId() {
+        return accountId;
+    }
+
+    public void setAccountId(String accountId) {
+        this.accountId = accountId;
+    }
+
+    public String getTemplateName() {
+        return templateName;
+    }
+
+    public void setTemplateName(String templateName) {
+        this.templateName = templateName;
+    }
+
+    public Boolean getEnabled() {
+        return enabled;
+    }
+
+    public void setEnabled(Boolean enabled) {
+        this.enabled = enabled;
+    }
+
+    public Double getMinBalance() {
+        return minBalance;
+    }
+
+    public void setMinBalance(Double minBalance) {
+        this.minBalance = minBalance;
+    }
+}
diff --git 
a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResponseBuilder.java
 
b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResponseBuilder.java
index 36033043bcf..57aa04e00fa 100644
--- 
a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResponseBuilder.java
+++ 
b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResponseBuilder.java
@@ -17,6 +17,7 @@
 package org.apache.cloudstack.api.response;
 
 import org.apache.cloudstack.api.command.QuotaBalanceCmd;
+import org.apache.cloudstack.api.command.QuotaConfigureEmailCmd;
 import org.apache.cloudstack.api.command.QuotaEmailTemplateListCmd;
 import org.apache.cloudstack.api.command.QuotaEmailTemplateUpdateCmd;
 import org.apache.cloudstack.api.command.QuotaStatementCmd;
@@ -24,6 +25,7 @@ import org.apache.cloudstack.api.command.QuotaTariffCreateCmd;
 import org.apache.cloudstack.api.command.QuotaTariffListCmd;
 import org.apache.cloudstack.api.command.QuotaTariffUpdateCmd;
 import org.apache.cloudstack.quota.vo.QuotaBalanceVO;
+import org.apache.cloudstack.quota.vo.QuotaEmailConfigurationVO;
 import org.apache.cloudstack.quota.vo.QuotaTariffVO;
 import org.apache.cloudstack.quota.vo.QuotaUsageVO;
 
@@ -69,4 +71,10 @@ public interface QuotaResponseBuilder {
     QuotaTariffVO createQuotaTariff(QuotaTariffCreateCmd cmd);
 
     boolean deleteQuotaTariff(String quotaTariffUuid);
+
+    Pair<QuotaEmailConfigurationVO, Double> 
configureQuotaEmail(QuotaConfigureEmailCmd cmd);
+
+    QuotaConfigureEmailResponse 
createQuotaConfigureEmailResponse(QuotaEmailConfigurationVO 
quotaEmailConfigurationVO, Double minBalance, long accountId);
+
+    List<QuotaConfigureEmailResponse> listEmailConfiguration(long accountId);
 }
diff --git 
a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImpl.java
 
b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImpl.java
index d4996887a40..94897b410f4 100644
--- 
a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImpl.java
+++ 
b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImpl.java
@@ -38,6 +38,7 @@ import com.cloud.utils.DateUtil;
 import org.apache.cloudstack.api.ApiErrorCode;
 import org.apache.cloudstack.api.ServerApiException;
 import org.apache.cloudstack.api.command.QuotaBalanceCmd;
+import org.apache.cloudstack.api.command.QuotaConfigureEmailCmd;
 import org.apache.cloudstack.api.command.QuotaEmailTemplateListCmd;
 import org.apache.cloudstack.api.command.QuotaEmailTemplateUpdateCmd;
 import org.apache.cloudstack.api.command.QuotaStatementCmd;
@@ -54,12 +55,14 @@ import org.apache.cloudstack.quota.constant.QuotaTypes;
 import org.apache.cloudstack.quota.dao.QuotaAccountDao;
 import org.apache.cloudstack.quota.dao.QuotaBalanceDao;
 import org.apache.cloudstack.quota.dao.QuotaCreditsDao;
+import org.apache.cloudstack.quota.dao.QuotaEmailConfigurationDao;
 import org.apache.cloudstack.quota.dao.QuotaEmailTemplatesDao;
 import org.apache.cloudstack.quota.dao.QuotaTariffDao;
-import org.apache.cloudstack.quota.dao.QuotaUsageDao;
 import org.apache.cloudstack.quota.vo.QuotaAccountVO;
+import org.apache.cloudstack.quota.dao.QuotaUsageDao;
 import org.apache.cloudstack.quota.vo.QuotaBalanceVO;
 import org.apache.cloudstack.quota.vo.QuotaCreditsVO;
+import org.apache.cloudstack.quota.vo.QuotaEmailConfigurationVO;
 import org.apache.cloudstack.quota.vo.QuotaEmailTemplatesVO;
 import org.apache.cloudstack.quota.vo.QuotaTariffVO;
 import org.apache.cloudstack.quota.vo.QuotaUsageVO;
@@ -104,7 +107,7 @@ public class QuotaResponseBuilderImpl implements 
QuotaResponseBuilder {
     @Inject
     private AccountDao _accountDao;
     @Inject
-    private QuotaAccountDao _quotaAccountDao;
+    private QuotaAccountDao quotaAccountDao;
     @Inject
     private DomainDao _domainDao;
     @Inject
@@ -113,6 +116,8 @@ public class QuotaResponseBuilderImpl implements 
QuotaResponseBuilder {
     private QuotaStatement _statement;
     @Inject
     private QuotaManager _quotaManager;
+    @Inject
+    private QuotaEmailConfigurationDao quotaEmailConfigurationDao;
 
     @Override
     public QuotaTariffResponse createQuotaTariffResponse(QuotaTariffVO tariff) 
{
@@ -165,7 +170,7 @@ public class QuotaResponseBuilderImpl implements 
QuotaResponseBuilder {
                 result.add(qr);
             }
         } else {
-            Pair<List<QuotaAccountVO>, Integer> data = 
_quotaAccountDao.listAllQuotaAccount(startIndex, pageSize);
+            Pair<List<QuotaAccountVO>, Integer> data = 
quotaAccountDao.listAllQuotaAccount(startIndex, pageSize);
             count = data.second();
             for (final QuotaAccountVO quotaAccount : data.first()) {
                 AccountVO account = _accountDao.findById(quotaAccount.getId());
@@ -676,4 +681,99 @@ public class QuotaResponseBuilderImpl implements 
QuotaResponseBuilder {
 
         return _quotaTariffDao.updateQuotaTariff(quotaTariff);
     }
+
+    @Override
+    public Pair<QuotaEmailConfigurationVO, Double> 
configureQuotaEmail(QuotaConfigureEmailCmd cmd) {
+        validateQuotaConfigureEmailCmdParameters(cmd);
+
+        Double minBalance = cmd.getMinBalance();
+
+        if (minBalance != null) {
+            _quotaService.setMinBalance(cmd.getAccountId(), 
cmd.getMinBalance());
+        }
+
+        QuotaEmailConfigurationVO configurationVO = 
getQuotaEmailConfigurationVo(cmd);
+        return new Pair<>(configurationVO, minBalance);
+    }
+
+    protected QuotaEmailConfigurationVO 
getQuotaEmailConfigurationVo(QuotaConfigureEmailCmd cmd) {
+        if (cmd.getTemplateName() == null) {
+            return null;
+        }
+
+        List<QuotaEmailTemplatesVO> templateVO = 
_quotaEmailTemplateDao.listAllQuotaEmailTemplates(cmd.getTemplateName());
+        if (templateVO.isEmpty()) {
+            throw new InvalidParameterValueException(String.format("Could not 
find template with name [%s].", cmd.getTemplateName()));
+        }
+        long templateId = templateVO.get(0).getId();
+        QuotaEmailConfigurationVO configurationVO = 
quotaEmailConfigurationDao.findByAccountIdAndEmailTemplateId(cmd.getAccountId(),
 templateId);
+
+        if (configurationVO == null) {
+            configurationVO = new 
QuotaEmailConfigurationVO(cmd.getAccountId(), templateId, cmd.getEnable());
+            
quotaEmailConfigurationDao.persistQuotaEmailConfiguration(configurationVO);
+            return configurationVO;
+        }
+
+        configurationVO.setEnabled(cmd.getEnable());
+        return 
quotaEmailConfigurationDao.updateQuotaEmailConfiguration(configurationVO);
+    }
+
+    protected void 
validateQuotaConfigureEmailCmdParameters(QuotaConfigureEmailCmd cmd) {
+        if (quotaAccountDao.findByIdQuotaAccount(cmd.getAccountId()) == null) {
+            throw new InvalidParameterValueException("You must have the quota 
enabled for this account to configure quota emails.");
+        }
+
+        if (cmd.getTemplateName() == null && cmd.getMinBalance() == null) {
+            throw new InvalidParameterValueException("You should inform at 
least the 'minbalance' or both the 'templatename' and 'enable' parameters.");
+        }
+
+        if ((cmd.getTemplateName() != null && cmd.getEnable() == null) || 
(cmd.getTemplateName() == null && cmd.getEnable() != null)) {
+            throw new InvalidParameterValueException("Parameter 'enable' must 
be informed along with 'templatename'.");
+        }
+    }
+
+    public QuotaConfigureEmailResponse 
createQuotaConfigureEmailResponse(QuotaEmailConfigurationVO 
quotaEmailConfigurationVO, Double minBalance, long accountId) {
+        QuotaConfigureEmailResponse quotaConfigureEmailResponse = new 
QuotaConfigureEmailResponse();
+
+        Account account = _accountDao.findByIdIncludingRemoved(accountId);
+        if (quotaEmailConfigurationVO != null) {
+            QuotaEmailTemplatesVO templateVO = 
_quotaEmailTemplateDao.findById(quotaEmailConfigurationVO.getEmailTemplateId());
+
+            quotaConfigureEmailResponse.setAccountId(account.getUuid());
+            
quotaConfigureEmailResponse.setTemplateName(templateVO.getTemplateName());
+            
quotaConfigureEmailResponse.setEnabled(quotaEmailConfigurationVO.isEnabled());
+        }
+
+        quotaConfigureEmailResponse.setMinBalance(minBalance);
+
+        return quotaConfigureEmailResponse;
+    }
+
+    @Override
+    public List<QuotaConfigureEmailResponse> listEmailConfiguration(long 
accountId) {
+        List<QuotaEmailConfigurationVO> emailConfigurationVOList = 
quotaEmailConfigurationDao.listByAccount(accountId);
+        Account account = _accountDao.findById(accountId);
+        QuotaAccountVO quotaAccountVO = 
quotaAccountDao.findByIdQuotaAccount(accountId);
+
+        List<QuotaConfigureEmailResponse> quotaConfigureEmailResponseList = 
new ArrayList<>();
+        for (QuotaEmailConfigurationVO quotaEmailConfigurationVO : 
emailConfigurationVOList) {
+            
quotaConfigureEmailResponseList.add(createQuotaConfigureEmailResponse(quotaEmailConfigurationVO,
 account, quotaAccountVO));
+        }
+
+        return quotaConfigureEmailResponseList;
+    }
+
+    protected QuotaConfigureEmailResponse 
createQuotaConfigureEmailResponse(QuotaEmailConfigurationVO 
quotaEmailConfigurationVO, Account account, QuotaAccountVO quotaAccountVO) {
+        QuotaConfigureEmailResponse quotaConfigureEmailResponse = new 
QuotaConfigureEmailResponse();
+
+        QuotaEmailTemplatesVO templateVO = 
_quotaEmailTemplateDao.findById(quotaEmailConfigurationVO.getEmailTemplateId());
+
+        quotaConfigureEmailResponse.setAccountId(account.getUuid());
+        
quotaConfigureEmailResponse.setTemplateName(templateVO.getTemplateName());
+        
quotaConfigureEmailResponse.setEnabled(quotaEmailConfigurationVO.isEnabled());
+
+        
quotaConfigureEmailResponse.setMinBalance(quotaAccountVO.getQuotaMinBalance().doubleValue());
+
+        return quotaConfigureEmailResponse;
+    }
 }
diff --git 
a/plugins/database/quota/src/main/java/org/apache/cloudstack/quota/QuotaServiceImpl.java
 
b/plugins/database/quota/src/main/java/org/apache/cloudstack/quota/QuotaServiceImpl.java
index 88a69c47e05..da3f50b165a 100644
--- 
a/plugins/database/quota/src/main/java/org/apache/cloudstack/quota/QuotaServiceImpl.java
+++ 
b/plugins/database/quota/src/main/java/org/apache/cloudstack/quota/QuotaServiceImpl.java
@@ -28,10 +28,12 @@ import javax.inject.Inject;
 import javax.naming.ConfigurationException;
 
 import org.apache.cloudstack.api.command.QuotaBalanceCmd;
+import org.apache.cloudstack.api.command.QuotaConfigureEmailCmd;
 import org.apache.cloudstack.api.command.QuotaCreditsCmd;
 import org.apache.cloudstack.api.command.QuotaEmailTemplateListCmd;
 import org.apache.cloudstack.api.command.QuotaEmailTemplateUpdateCmd;
 import org.apache.cloudstack.api.command.QuotaEnabledCmd;
+import org.apache.cloudstack.api.command.QuotaListEmailConfigurationCmd;
 import org.apache.cloudstack.api.command.QuotaStatementCmd;
 import org.apache.cloudstack.api.command.QuotaSummaryCmd;
 import org.apache.cloudstack.api.command.QuotaTariffCreateCmd;
@@ -116,6 +118,8 @@ public class QuotaServiceImpl extends ManagerBase 
implements QuotaService, Confi
         cmdList.add(QuotaEmailTemplateUpdateCmd.class);
         cmdList.add(QuotaTariffCreateCmd.class);
         cmdList.add(QuotaTariffDeleteCmd.class);
+        cmdList.add(QuotaConfigureEmailCmd.class);
+        cmdList.add(QuotaListEmailConfigurationCmd.class);
         return cmdList;
     }
 
@@ -128,7 +132,7 @@ public class QuotaServiceImpl extends ManagerBase 
implements QuotaService, Confi
     public ConfigKey<?>[] getConfigKeys() {
         return new ConfigKey<?>[] {QuotaPluginEnabled, QuotaEnableEnforcement, 
QuotaCurrencySymbol, QuotaCurrencyLocale, QuotaStatementPeriod, QuotaSmtpHost, 
QuotaSmtpPort, QuotaSmtpTimeout,
                 QuotaSmtpUser, QuotaSmtpPassword, QuotaSmtpAuthType, 
QuotaSmtpSender, QuotaSmtpEnabledSecurityProtocols, QuotaSmtpUseStartTLS, 
QuotaActivationRuleTimeout, QuotaAccountEnabled,
-                QuotaEmailHeader, QuotaEmailFooter};
+                QuotaEmailHeader, QuotaEmailFooter, QuotaEnableEmails};
     }
 
     @Override
diff --git 
a/plugins/database/quota/src/test/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImplTest.java
 
b/plugins/database/quota/src/test/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImplTest.java
index b960a1be569..899ce649fce 100644
--- 
a/plugins/database/quota/src/test/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImplTest.java
+++ 
b/plugins/database/quota/src/test/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImplTest.java
@@ -30,6 +30,7 @@ import java.util.function.Consumer;
 import com.cloud.domain.DomainVO;
 import com.cloud.domain.dao.DomainDao;
 import org.apache.cloudstack.api.ServerApiException;
+import org.apache.cloudstack.api.command.QuotaConfigureEmailCmd;
 import org.apache.cloudstack.api.command.QuotaEmailTemplateListCmd;
 import org.apache.cloudstack.api.command.QuotaEmailTemplateUpdateCmd;
 import org.apache.cloudstack.framework.config.ConfigKey;
@@ -37,13 +38,17 @@ import org.apache.cloudstack.quota.QuotaService;
 import org.apache.cloudstack.quota.QuotaStatement;
 import org.apache.cloudstack.quota.constant.QuotaConfig;
 import org.apache.cloudstack.quota.constant.QuotaTypes;
+import org.apache.cloudstack.quota.dao.QuotaAccountDao;
 import org.apache.cloudstack.quota.dao.QuotaBalanceDao;
 import org.apache.cloudstack.quota.dao.QuotaCreditsDao;
+import org.apache.cloudstack.quota.dao.QuotaEmailConfigurationDao;
 import org.apache.cloudstack.quota.dao.QuotaEmailTemplatesDao;
 import org.apache.cloudstack.quota.dao.QuotaTariffDao;
 import org.apache.cloudstack.quota.dao.QuotaUsageDao;
+import org.apache.cloudstack.quota.vo.QuotaAccountVO;
 import org.apache.cloudstack.quota.vo.QuotaBalanceVO;
 import org.apache.cloudstack.quota.vo.QuotaCreditsVO;
+import org.apache.cloudstack.quota.vo.QuotaEmailConfigurationVO;
 import org.apache.cloudstack.quota.vo.QuotaEmailTemplatesVO;
 import org.apache.cloudstack.quota.vo.QuotaTariffVO;
 import org.apache.commons.lang3.time.DateUtils;
@@ -103,6 +108,12 @@ public class QuotaResponseBuilderImplTest extends TestCase 
{
     @Mock
     QuotaUsageDao quotaUsageDaoMock;
 
+    @Mock
+    QuotaAccountDao quotaAccountDaoMock;
+
+    @Mock
+    QuotaEmailConfigurationDao quotaEmailConfigurationDaoMock;
+
     @InjectMocks
     QuotaResponseBuilderImpl quotaResponseBuilderSpy = 
Mockito.spy(QuotaResponseBuilderImpl.class);
 
@@ -114,6 +125,15 @@ public class QuotaResponseBuilderImplTest extends TestCase 
{
     @Mock
     DomainVO domainVOMock;
 
+    @Mock
+    QuotaConfigureEmailCmd quotaConfigureEmailCmdMock;
+
+    @Mock
+    QuotaAccountVO quotaAccountVOMock;
+
+    @Mock
+    QuotaEmailTemplatesVO quotaEmailTemplatesVoMock;
+
     private void overrideDefaultQuotaEnabledConfigValue(final Object value) 
throws IllegalAccessException, NoSuchFieldException {
         Field f = ConfigKey.class.getDeclaredField("_defaultValue");
         f.setAccessible(true);
@@ -403,4 +423,96 @@ public class QuotaResponseBuilderImplTest extends TestCase 
{
 
         assertTrue(quotaSummaryResponse.getQuotaEnabled());
     }
+
+
+    @Test (expected = InvalidParameterValueException.class)
+    public void validateQuotaConfigureEmailCmdParametersTestNullQuotaAccount() 
{
+        
Mockito.doReturn(null).when(quotaAccountDaoMock).findByIdQuotaAccount(Mockito.any());
+        
quotaResponseBuilderSpy.validateQuotaConfigureEmailCmdParameters(quotaConfigureEmailCmdMock);
+    }
+
+    @Test (expected = InvalidParameterValueException.class)
+    public void 
validateQuotaConfigureEmailCmdParametersTestNullTemplateNameAndMinBalance() {
+        
Mockito.doReturn(quotaAccountVOMock).when(quotaAccountDaoMock).findByIdQuotaAccount(Mockito.any());
+        
Mockito.doReturn(null).when(quotaConfigureEmailCmdMock).getTemplateName();
+        
Mockito.doReturn(null).when(quotaConfigureEmailCmdMock).getMinBalance();
+        
quotaResponseBuilderSpy.validateQuotaConfigureEmailCmdParameters(quotaConfigureEmailCmdMock);
+    }
+
+    @Test (expected = InvalidParameterValueException.class)
+    public void 
validateQuotaConfigureEmailCmdParametersTestEnableNullAndTemplateNameNotNull() {
+        
Mockito.doReturn(quotaAccountVOMock).when(quotaAccountDaoMock).findByIdQuotaAccount(Mockito.any());
+        
Mockito.doReturn(QuotaConfig.QuotaEmailTemplateTypes.QUOTA_LOW.toString()).when(quotaConfigureEmailCmdMock).getTemplateName();
+        Mockito.doReturn(null).when(quotaConfigureEmailCmdMock).getEnable();
+        
quotaResponseBuilderSpy.validateQuotaConfigureEmailCmdParameters(quotaConfigureEmailCmdMock);
+    }
+
+
+    @Test
+    public void validateQuotaConfigureEmailCmdParametersTestNullTemplateName() 
{
+        
Mockito.doReturn(quotaAccountVOMock).when(quotaAccountDaoMock).findByIdQuotaAccount(Mockito.any());
+        
Mockito.doReturn(null).when(quotaConfigureEmailCmdMock).getTemplateName();
+        Mockito.doReturn(null).when(quotaConfigureEmailCmdMock).getEnable();
+        
Mockito.doReturn(100D).when(quotaConfigureEmailCmdMock).getMinBalance();
+        
quotaResponseBuilderSpy.validateQuotaConfigureEmailCmdParameters(quotaConfigureEmailCmdMock);
+    }
+
+    @Test
+    public void 
validateQuotaConfigureEmailCmdParametersTestWithTemplateNameAndEnable() {
+        
Mockito.doReturn(quotaAccountVOMock).when(quotaAccountDaoMock).findByIdQuotaAccount(Mockito.any());
+        
Mockito.doReturn(QuotaConfig.QuotaEmailTemplateTypes.QUOTA_LOW.toString()).when(quotaConfigureEmailCmdMock).getTemplateName();
+        Mockito.doReturn(true).when(quotaConfigureEmailCmdMock).getEnable();
+        
quotaResponseBuilderSpy.validateQuotaConfigureEmailCmdParameters(quotaConfigureEmailCmdMock);
+    }
+
+    @Test
+    public void getQuotaEmailConfigurationVoTestTemplateNameIsNull() {
+        
Mockito.doReturn(null).when(quotaConfigureEmailCmdMock).getTemplateName();
+
+        QuotaEmailConfigurationVO result = 
quotaResponseBuilderSpy.getQuotaEmailConfigurationVo(quotaConfigureEmailCmdMock);
+
+        Assert.assertNull(result);
+    }
+
+    @Test (expected = InvalidParameterValueException.class)
+    public void getQuotaEmailConfigurationVoTestNoTemplateFound() {
+        
Mockito.doReturn("name").when(quotaConfigureEmailCmdMock).getTemplateName();
+        Mockito.doReturn(new 
ArrayList<QuotaEmailTemplatesVO>()).when(quotaEmailTemplateDaoMock).listAllQuotaEmailTemplates(Mockito.any());
+
+        
quotaResponseBuilderSpy.getQuotaEmailConfigurationVo(quotaConfigureEmailCmdMock);
+    }
+
+    @Test
+    public void getQuotaEmailConfigurationVoTestNewConfiguration() {
+        
Mockito.doReturn("name").when(quotaConfigureEmailCmdMock).getTemplateName();
+        List<QuotaEmailTemplatesVO> templatesVOArrayList = 
List.of(quotaEmailTemplatesVoMock);
+        
Mockito.doReturn(templatesVOArrayList).when(quotaEmailTemplateDaoMock).listAllQuotaEmailTemplates(Mockito.any());
+        
Mockito.doReturn(null).when(quotaEmailConfigurationDaoMock).findByAccountIdAndEmailTemplateId(Mockito.anyLong(),
 Mockito.anyLong());
+
+        QuotaEmailConfigurationVO result = 
quotaResponseBuilderSpy.getQuotaEmailConfigurationVo(quotaConfigureEmailCmdMock);
+
+        
Mockito.verify(quotaEmailConfigurationDaoMock).persistQuotaEmailConfiguration(Mockito.any());
+        assertEquals(0, result.getAccountId());
+        assertEquals(0, result.getEmailTemplateId());
+        assertFalse(result.isEnabled());
+    }
+
+    @Test
+    public void getQuotaEmailConfigurationVoTestExistingConfiguration() {
+        
Mockito.doReturn("name").when(quotaConfigureEmailCmdMock).getTemplateName();
+        List<QuotaEmailTemplatesVO> templatesVOArrayList = 
List.of(quotaEmailTemplatesVoMock);
+        
Mockito.doReturn(templatesVOArrayList).when(quotaEmailTemplateDaoMock).listAllQuotaEmailTemplates(Mockito.any());
+
+        QuotaEmailConfigurationVO quotaEmailConfigurationVO = new 
QuotaEmailConfigurationVO(1, 2, true);
+        
Mockito.doReturn(quotaEmailConfigurationVO).when(quotaEmailConfigurationDaoMock).findByAccountIdAndEmailTemplateId(Mockito.anyLong(),
 Mockito.anyLong());
+        
Mockito.doReturn(quotaEmailConfigurationVO).when(quotaEmailConfigurationDaoMock).updateQuotaEmailConfiguration(Mockito.any());
+
+        QuotaEmailConfigurationVO result = 
quotaResponseBuilderSpy.getQuotaEmailConfigurationVo(quotaConfigureEmailCmdMock);
+
+        
Mockito.verify(quotaEmailConfigurationDaoMock).updateQuotaEmailConfiguration(Mockito.any());
+
+        assertEquals(1, result.getAccountId());
+        assertEquals(2, result.getEmailTemplateId());
+        assertFalse(result.isEnabled());
+    }
 }

Reply via email to