Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

OSOE-693: Display and send warnings also when 80 and 90% of the e-mail quota is reached #89

Merged
merged 28 commits into from
Oct 2, 2023
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
3956a32
Adding new logic for 80% and 90% reminders
wAsnk Sep 28, 2023
2f185a4
Adding percentage warnings
wAsnk Sep 28, 2023
eef620a
Fixing calculations
wAsnk Sep 28, 2023
6d1e19f
Adding new functionality UI test
wAsnk Sep 28, 2023
7f2d54f
Shouldn't do the rest if
wAsnk Sep 28, 2023
2d5fba9
Fixing code analyzer validations
wAsnk Sep 28, 2023
2d0b9b9
Breaking long lines
wAsnk Sep 29, 2023
a554740
Renaming class
wAsnk Sep 29, 2023
afcdef8
Moving function
wAsnk Sep 29, 2023
42c4343
Using generic text
wAsnk Sep 29, 2023
9b67d0a
Renaming files to more descriptive names
wAsnk Sep 29, 2023
6a67c4c
Adding email quota subject service
wAsnk Sep 29, 2023
81e7998
Renaming and code cleanups
wAsnk Sep 29, 2023
e2c195e
Cleanup and additional docs
wAsnk Sep 29, 2023
911c1a0
Merge remote-tracking branch 'origin/dev' into issue/OSOE-693
wAsnk Sep 29, 2023
dd16538
Should only check if necessary
wAsnk Sep 29, 2023
3e0b148
Code cleanup
wAsnk Sep 29, 2023
8d7c65d
Using same rounding as module
wAsnk Sep 29, 2023
e08b721
Update Lombiq.Hosting.Tenants.EmailQuotaManagement/Views/DashboardQuo…
wAsnk Oct 2, 2023
930ea80
Update Lombiq.Hosting.Tenants.EmailQuotaManagement/Views/DashboardQuo…
wAsnk Oct 2, 2023
4f55268
Update Lombiq.Hosting.Tenants.EmailQuotaManagement/Views/EmailSetting…
wAsnk Oct 2, 2023
1b0e8da
Update Lombiq.Hosting.Tenants.EmailQuotaManagement/Views/EmailTemplat…
wAsnk Oct 2, 2023
65b7a37
Update Lombiq.Hosting.Tenants.EmailQuotaManagement/Views/EmailTemplat…
wAsnk Oct 2, 2023
301666c
Update Lombiq.Hosting.Tenants.EmailQuotaManagement/Services/IEmailQuo…
wAsnk Oct 2, 2023
602d93b
Update Lombiq.Hosting.Tenants.EmailQuotaManagement/Services/IEmailQuo…
wAsnk Oct 2, 2023
00046f5
Update Lombiq.Hosting.Tenants.EmailQuotaManagement/Services/IEmailQuo…
wAsnk Oct 2, 2023
aeffcb7
Renaming function
wAsnk Oct 2, 2023
c5e3c0b
Adding remarks
wAsnk Oct 2, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
using Lombiq.Tests.UI.Helpers;
using Lombiq.Tests.UI.Services;
using OpenQA.Selenium;
using Shouldly;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace Lombiq.Hosting.Tenants.EmailQuotaManagement.Tests.UI.Extensions;
Expand All @@ -11,7 +13,8 @@ public static class TestCaseUITestContextExtensions
{
private const string SuccessfulSubject = "Successful test message";
private const string UnSuccessfulSubject = "Unsuccessful test message";
private const string DashboardWarning =
private const string WarningSubject = "[Warning] Your DotNest site has used";
Piedone marked this conversation as resolved.
Show resolved Hide resolved
private const string DashboardExceededMessage =
"//p[contains(@class,'alert-danger')][contains(.,'It seems that your site sent out more e-mails')]";

public static async Task TestEmailQuotaManagementBehaviorAsync(
Expand All @@ -21,26 +24,56 @@ public static async Task TestEmailQuotaManagementBehaviorAsync(
{
await context.SignInDirectlyAndGoToDashboardAsync();

context.Missing(By.XPath(DashboardWarning));
context.Missing(By.XPath(DashboardExceededMessage));

await context.GoToAdminRelativeUrlAsync("/Settings/email");

CheckEmailsSentWarningMessage(context, exists: moduleShouldInterfere, maximumEmailQuota, 0);

await SendTestEmailAsync(context, SuccessfulSubject);
context.SuccessMessageExists();
var warningEmails = new List<int>();
for (int i = 0; i < maximumEmailQuota; i++)
{
await SendTestEmailAsync(context, SuccessfulSubject);
context.SuccessMessageExists();
CheckEmailsSentWarningMessage(context, exists: moduleShouldInterfere, maximumEmailQuota, i + 1);
var warningLevel = Convert.ToInt32((double)(i + 1) / maximumEmailQuota * 100) / 10 * 10;
if (!moduleShouldInterfere) continue;

CheckEmailsSentWarningMessage(context, exists: moduleShouldInterfere, maximumEmailQuota, 1);

await context.GoToDashboardAsync();
context.CheckExistence(By.XPath(DashboardWarning), exists: moduleShouldInterfere);
if (warningLevel >= 100)
{
await context.GoToDashboardAsync();
context.CheckExistence(By.XPath(DashboardExceededMessage), exists: moduleShouldInterfere);
}
else if (warningLevel >= 80)
{
await context.GoToDashboardAsync();
context.CheckExistence(
By.XPath($"//p[contains(@class,'alert-warning')]" +
$"[contains(.,'It seems that your site sent out {warningLevel.ToTechnicalString()}% of e-mail')]"),
exists: moduleShouldInterfere);
if (!warningEmails.Contains(warningLevel))
{
warningEmails.Add(warningLevel);
}
}
Piedone marked this conversation as resolved.
Show resolved Hide resolved
}

await SendTestEmailAsync(context, UnSuccessfulSubject);
await context.GoToSmtpWebUIAsync();
context.CheckExistence(ByHelper.SmtpInboxRow(SuccessfulSubject), exists: true);
context.CheckExistence(
ByHelper.SmtpInboxRow("[Action Required] Your DotNest site has run over its e-mail quota"),
exists: moduleShouldInterfere);
var warningMessageExists = context.CheckExistence(
ByHelper.SmtpInboxRow(WarningSubject),
exists: moduleShouldInterfere);
if (warningMessageExists)
{
(context.GetAll(
ByHelper.SmtpInboxRow(WarningSubject)).Count == warningEmails.Count)
.ShouldBeTrue();
Piedone marked this conversation as resolved.
Show resolved Hide resolved
}

context.CheckExistence(ByHelper.SmtpInboxRow(UnSuccessfulSubject), exists: !moduleShouldInterfere);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,24 @@ public async Task OnResultExecutionAsync(ResultExecutingContext context, ResultE
actionRouteArea == $"{nameof(OrchardCore)}.{nameof(OrchardCore.Admin)}" &&
actionRouteValue is nameof(AdminController.Index) &&
context.Result is ViewResult &&
_quotaService.ShouldLimitEmails() &&
(await _quotaService.IsQuotaOverTheLimitAsync()).IsOverQuota)
_quotaService.ShouldLimitEmails())
{
var layout = await _layoutAccessor.GetLayoutAsync();
var contentZone = layout.Zones["Content"];
var currentEmailQuota = await _quotaService.IsQuotaOverTheLimitAsync();

await contentZone.AddAsync(await _shapeFactory.CreateAsync("EmailQuotaError"), "0");
var roundedCurrentPercentage = _quotaService.CurrentUsagePercentage(currentEmailQuota.EmailQuota) / 10 * 10;
Piedone marked this conversation as resolved.
Show resolved Hide resolved

if (roundedCurrentPercentage >= 80)
{
await contentZone.AddAsync(
await _shapeFactory.CreateAsync("EmailQuotaError", new
{
currentEmailQuota.IsOverQuota,
UsagePercentage = roundedCurrentPercentage,
}),
"0");
}
}

await next();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ public class EmailQuotaIndex : MapIndex
{
public int CurrentEmailQuotaCount { get; set; }
public DateTime LastReminder { get; set; }
public int LastReminderPercentage { get; set; }
}

public class EmailQuotaIndexProvider : IndexProvider<EmailQuota>
Expand All @@ -18,5 +19,6 @@ public override void Describe(DescribeContext<EmailQuota> context) =>
{
CurrentEmailQuotaCount = emailQuota.CurrentEmailQuotaCount,
LastReminder = emailQuota.LastReminder,
LastReminderPercentage = emailQuota.LastReminderPercentage,
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,18 @@ public int Create()
{
SchemaBuilder.CreateMapIndexTable<EmailQuotaIndex>(
table => table.Column<int>(nameof(EmailQuotaIndex.CurrentEmailQuotaCount))
.Column<DateTime>(nameof(EmailQuotaIndex.LastReminder)));
.Column<DateTime>(nameof(EmailQuotaIndex.LastReminder))
.Column<int>(nameof(EmailQuotaIndex.LastReminderPercentage)));

return 1;
return 2;
}

public int UpdateFrom1()
{
SchemaBuilder.AlterTable(nameof(EmailQuotaIndex), table => table
.AddColumn<int>(nameof(EmailQuotaIndex.LastReminderPercentage))
);

return 2;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ public class EmailQuota
{
public int CurrentEmailQuotaCount { get; set; }
public DateTime LastReminder { get; set; }
Piedone marked this conversation as resolved.
Show resolved Hide resolved
public int LastReminderPercentage { get; set; }
}
Piedone marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
using OrchardCore.Email;
using OrchardCore.Environment.Shell;
using OrchardCore.Environment.Shell.Scope;
using OrchardCore.Modules;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace Lombiq.Hosting.Tenants.EmailQuotaManagement.Services;
Expand All @@ -16,7 +16,6 @@ public class EmailSenderQuotaService : ISmtpService
private readonly ISmtpService _smtpService;
private readonly IQuotaService _quotaService;
private readonly IEmailQuotaEmailService _emailQuotaEmailService;
private readonly IClock _clock;
private readonly ShellSettings _shellSettings;
private readonly IEmailTemplateService _emailTemplateService;

Expand All @@ -25,15 +24,13 @@ public EmailSenderQuotaService(
IStringLocalizer<EmailSenderQuotaService> stringLocalizer,
IQuotaService quotaService,
IEmailQuotaEmailService emailQuotaEmailService,
IClock clock,
ShellSettings shellSettings,
IEmailTemplateService emailTemplateService)
{
_smtpService = smtpService;
T = stringLocalizer;
_quotaService = quotaService;
_emailQuotaEmailService = emailQuotaEmailService;
_clock = clock;
_shellSettings = shellSettings;
_emailTemplateService = emailTemplateService;
}
Expand All @@ -46,10 +43,11 @@ public async Task<SmtpResult> SendAsync(MailMessage message)
}

var isQuotaOverResult = await _quotaService.IsQuotaOverTheLimitAsync();
await SendAlertEmailIfNecessaryAsync(isQuotaOverResult.EmailQuota);

// Should send the email if the quota is not over the limit.
if (isQuotaOverResult.IsOverQuota)
{
await SendAlertEmailIfNecessaryAsync(isQuotaOverResult.EmailQuota);

return SmtpResult.Failed(T["The email quota for the site has been exceeded."]);
}

Expand All @@ -64,32 +62,58 @@ public async Task<SmtpResult> SendAsync(MailMessage message)

private async Task SendAlertEmailIfNecessaryAsync(EmailQuota emailQuota)
{
if (IsSameMonth(_clock.UtcNow, emailQuota.LastReminder)) return;
var currentUsagePercentage = _quotaService.CurrentUsagePercentage(emailQuota);
if (!_quotaService.ShouldSendReminderEmail(emailQuota, currentUsagePercentage)) return;

emailQuota.LastReminder = _clock.UtcNow;
_quotaService.SaveQuota(emailQuota);
var siteOwnerEmails = (await _emailQuotaEmailService.CollectUserEmailsForExceedingQuotaAsync()).ToList();
if (currentUsagePercentage >= 100)
{
var emailMessage = new MailMessage
{
Subject = T["[Action Required] Your DotNest site has run over its e-mail quota"],
IsHtmlBody = true,
};
SendQuotaEmail(siteOwnerEmails, emailMessage, "EmailQuota", currentUsagePercentage);
_quotaService.SaveQuotaReminder(emailQuota);
return;
}

var siteOwnerEmails = await _emailQuotaEmailService.CollectUserEmailsForExceedingQuotaAsync();
SendQuotaEmailWithPercentage(siteOwnerEmails, currentUsagePercentage / 10 * 10);
_quotaService.SaveQuotaReminder(emailQuota);
}

private void SendQuotaEmailWithPercentage(
IEnumerable<string> siteOwnerEmails,
int percentage)
{
var emailMessage = new MailMessage
{
Subject = T["[Action Required] Your DotNest site has run over its e-mail quota"],
Subject = T["[Warning] Your DotNest site has used {0}% of its e-mail quota", percentage],
IsHtmlBody = true,
};
SendQuotaEmail(siteOwnerEmails, emailMessage, $"EmailQuotaWarning", percentage);
}

private void SendQuotaEmail(
IEnumerable<string> siteOwnerEmails,
MailMessage emailMessage,
string emailTemplateName,
int percentage)
{
foreach (var siteOwnerEmail in siteOwnerEmails)
{
ShellScope.AddDeferredTask(async _ =>
{
emailMessage.To = siteOwnerEmail;
emailMessage.Body = await _emailTemplateService.RenderEmailTemplateAsync("EmailQuota", new
emailMessage.Body = await _emailTemplateService.RenderEmailTemplateAsync(emailTemplateName, new
{
HostName = _shellSettings.Name,
Percentage = percentage,
});
// ISmtpService must be used within this class otherwise it won't call the original ISmtpService
// implementation, but loop back to here.
await _smtpService.SendAsync(emailMessage);
});
}
}

private static bool IsSameMonth(DateTime date1, DateTime date2) =>
date1.Month == date2.Month && date1.Year == date2.Year;
}
Piedone marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,17 @@ public interface IQuotaService
void ResetQuota(EmailQuota emailQuota);

/// <summary>
/// Saves the given quota.
/// Saves the given quota on reminder sent.
/// </summary>
void SaveQuota(EmailQuota emailQuota);
void SaveQuotaReminder(EmailQuota emailQuota);

/// <summary>
/// Returns <see langword="true"/> if the reminder email should be sent.
Piedone marked this conversation as resolved.
Show resolved Hide resolved
/// </summary>
bool ShouldSendReminderEmail(EmailQuota emailQuota, int? currentPercentage = null);

/// <summary>
/// Returns the current quota usage percentage.
/// </summary>
int CurrentUsagePercentage(EmailQuota emailQuota);
Piedone marked this conversation as resolved.
Show resolved Hide resolved
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using OrchardCore.Email;
using OrchardCore.Environment.Shell.Configuration;
using OrchardCore.Modules;
using System;
using System.Threading.Tasks;
using YesSql;

Expand Down Expand Up @@ -67,15 +68,51 @@ public async Task<EmailQuota> GetCurrentQuotaAsync()
public void IncreaseQuota(EmailQuota emailQuota)
{
emailQuota.CurrentEmailQuotaCount++;
SaveQuota(emailQuota);
_session.Save(emailQuota);
}

public void SaveQuota(EmailQuota emailQuota) =>
public void SaveQuotaReminder(EmailQuota emailQuota)
{
emailQuota.LastReminder = _clock.UtcNow;
emailQuota.LastReminderPercentage = CurrentUsagePercentage(emailQuota);
_session.Save(emailQuota);
}

public bool ShouldSendReminderEmail(EmailQuota emailQuota, int? currentPercentage = null)
{
currentPercentage ??= CurrentUsagePercentage(emailQuota);
if (currentPercentage < 80)
{
return false;
}

var isSameMonth = IsSameMonth(_clock.UtcNow, emailQuota.LastReminder);

if (!isSameMonth)
Piedone marked this conversation as resolved.
Show resolved Hide resolved
{
return true;
}

switch (emailQuota.LastReminderPercentage)
{
case >= 80 when currentPercentage < 90:
case >= 90 when currentPercentage < 100:
case >= 100:
return false;
default:
return true;
}
Piedone marked this conversation as resolved.
Show resolved Hide resolved
}

public void ResetQuota(EmailQuota emailQuota)
{
emailQuota.CurrentEmailQuotaCount = 0;
_session.Save(emailQuota);
}

public int CurrentUsagePercentage(EmailQuota emailQuota) =>
Convert.ToInt32(Math.Round((double)emailQuota.CurrentEmailQuotaCount / _emailQuotaOptions.EmailQuotaPerMonth * 100, 0));
Piedone marked this conversation as resolved.
Show resolved Hide resolved

private static bool IsSameMonth(DateTime date1, DateTime date2) =>
date1.Month == date2.Month && date1.Year == date2.Year;
}
Original file line number Diff line number Diff line change
@@ -1 +1,8 @@
<p class="alert alert-danger">@T["It seems that your site sent out more e-mails than the available quota for this month. E-mail sending has been stopped until next month (any feature sending out e-mails will fail)."]</p>
@if (Model.IsOverQuota)
{
<p class="alert alert-danger">@T["It seems that your site sent out more e-mails than the available quota for this month. E-mail sending has been stopped until next month (any feature sending out e-mails will fail)."]</p>
}
else
{
<p class="alert alert-warning">@T["It seems that your site sent out {0}% of e-mails from the available quota for this month. E-mail sending will be stopped until next month (any feature sending out e-mails will fail), when the quota is exceeded.", Model.UsagePercentage]</p>
Piedone marked this conversation as resolved.
Show resolved Hide resolved
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
@{
ViewLayout = "Layout__EmailTemplate";
}
@T["<p>Hi</p><p> It seems that your {0} site sent out more e-mails than the available quota for this month. E-mail sending has been stopped until next month (any feature sending out e-mails will fail).</p><p> Thank you,<br/> Your Admin Crew</p> ", Model.HostName]
@T["<p>Hi,</p><p> It seems that your {0} site sent out more e-mails than the available quota for this month. E-mail sending has been stopped until next month (any feature sending out e-mails will fail).</p><p> Thank you,<br/> Your Admin Crew</p> ", Model.HostName]
Piedone marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
@{
ViewLayout = "Layout__EmailTemplate";
}
@T["<p>Hi,</p><p> It seems that your {0} site sent out {1}% of the available e-mail quota for this month. E-mail sending will be stopped until next month (any feature sending out e-mails will fail), when the quota is exceeded.</p><p> Thank you,<br/> Your Admin Crew</p> ", Model.HostName, Model.Percentage]