Kavita+ Overhaul & New Changelog (#3507)

This commit is contained in:
Joe Milazzo 2025-01-20 08:14:57 -06:00 committed by GitHub
parent d880c1690c
commit a5707617f2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
249 changed files with 14775 additions and 2300 deletions

View file

@ -8,7 +8,10 @@ using System.Threading.Tasks;
using System.Web;
using API.Data;
using API.DTOs.Email;
using API.Entities;
using API.Services.Plus;
using Kavita.Common;
using Kavita.Common.Extensions;
using MailKit.Security;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Hosting;
@ -29,6 +32,8 @@ internal class EmailOptionsDto
/// Filenames to attach
/// </summary>
public IList<string>? Attachments { get; set; }
public int? ToUserId { get; set; }
public required string Template { get; set; }
}
public interface IEmailService
@ -43,6 +48,9 @@ public interface IEmailService
Task<string> GenerateEmailLink(HttpRequest request, string token, string routePart, string email,
bool withHost = true);
Task<bool> SendTokenExpiredEmail(int userId, ScrobbleProvider provider);
Task<bool> SendTokenExpiringSoonEmail(int userId, ScrobbleProvider provider);
}
public class EmailService : IEmailService
@ -56,6 +64,14 @@ public class EmailService : IEmailService
private const string TemplatePath = @"{0}.html";
private const string LocalHost = "localhost:4200";
public const string SendToDeviceTemplate = "SendToDevice";
public const string EmailTestTemplate = "EmailTest";
public const string EmailChangeTemplate = "EmailChange";
public const string TokenExpirationTemplate = "TokenExpiration";
public const string TokenExpiringSoonTemplate = "TokenExpiringSoon";
public const string EmailConfirmTemplate = "EmailConfirm";
public const string EmailPasswordResetTemplate = "EmailPasswordReset";
public EmailService(ILogger<EmailService> logger, IUnitOfWork unitOfWork, IDirectoryService directoryService,
IHostEnvironment environment, ILocalizationService localizationService)
{
@ -104,12 +120,13 @@ public class EmailService : IEmailService
var emailOptions = new EmailOptionsDto()
{
Subject = "Kavita - Email Test",
Body = UpdatePlaceHolders(await GetEmailBody("EmailTest"), placeholders),
Template = EmailTestTemplate,
Body = UpdatePlaceHolders(await GetEmailBody(EmailTestTemplate), placeholders),
Preheader = "Kavita - Email Test",
ToEmails = new List<string>()
{
adminEmail
}
},
};
await SendEmail(emailOptions);
@ -139,7 +156,8 @@ public class EmailService : IEmailService
var emailOptions = new EmailOptionsDto()
{
Subject = UpdatePlaceHolders("Your email has been changed on {{InvitingUser}}'s Server", placeholders),
Body = UpdatePlaceHolders(await GetEmailBody("EmailChange"), placeholders),
Template = EmailChangeTemplate,
Body = UpdatePlaceHolders(await GetEmailBody(EmailChangeTemplate), placeholders),
Preheader = UpdatePlaceHolders("Your email has been changed on {{InvitingUser}}'s Server", placeholders),
ToEmails = new List<string>()
{
@ -155,9 +173,9 @@ public class EmailService : IEmailService
/// </summary>
/// <param name="email"></param>
/// <returns></returns>
public bool IsValidEmail(string email)
public bool IsValidEmail(string? email)
{
return new EmailAddressAttribute().IsValid(email);
return !string.IsNullOrEmpty(email) && new EmailAddressAttribute().IsValid(email);
}
public async Task<string> GenerateEmailLink(HttpRequest request, string token, string routePart, string email, bool withHost = true)
@ -180,6 +198,66 @@ public class EmailService : IEmailService
.Replace("//", "/");
}
public async Task<bool> SendTokenExpiredEmail(int userId, ScrobbleProvider provider)
{
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId);
var settings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync();
if (user == null || !IsValidEmail(user.Email) || !settings.IsEmailSetup()) return false;
var placeholders = new List<KeyValuePair<string, string>>
{
new ("{{UserName}}", user.UserName!),
new ("{{Provider}}", provider.ToDescription()),
new ("{{Link}}", $"{settings.HostName}/settings#account" ),
};
var emailOptions = new EmailOptionsDto()
{
Subject = UpdatePlaceHolders("Kavita - Your {{Provider}} token has expired and scrobbling events have stopped", placeholders),
Template = TokenExpirationTemplate,
Body = UpdatePlaceHolders(await GetEmailBody(TokenExpirationTemplate), placeholders),
Preheader = UpdatePlaceHolders("Kavita - Your {{Provider}} token has expired and scrobbling events have stopped", placeholders),
ToEmails = new List<string>()
{
user.Email
}
};
await SendEmail(emailOptions);
return true;
}
public async Task<bool> SendTokenExpiringSoonEmail(int userId, ScrobbleProvider provider)
{
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId);
var settings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync();
if (user == null || !IsValidEmail(user.Email) || !settings.IsEmailSetup()) return false;
var placeholders = new List<KeyValuePair<string, string>>
{
new ("{{UserName}}", user.UserName!),
new ("{{Provider}}", provider.ToDescription()),
new ("{{Link}}", $"{settings.HostName}/settings#account" ),
};
var emailOptions = new EmailOptionsDto()
{
Subject = UpdatePlaceHolders("Kavita - Your {{Provider}} token will expire soon!", placeholders),
Template = TokenExpiringSoonTemplate,
Body = UpdatePlaceHolders(await GetEmailBody(TokenExpiringSoonTemplate), placeholders),
Preheader = UpdatePlaceHolders("Kavita - Your {{Provider}} token will expire soon!", placeholders),
ToEmails = new List<string>()
{
user.Email
}
};
await SendEmail(emailOptions);
return true;
}
/// <summary>
/// Sends an invite email to a user to setup their account
/// </summary>
@ -195,7 +273,8 @@ public class EmailService : IEmailService
var emailOptions = new EmailOptionsDto()
{
Subject = UpdatePlaceHolders("You've been invited to join {{InvitingUser}}'s Server", placeholders),
Body = UpdatePlaceHolders(await GetEmailBody("EmailConfirm"), placeholders),
Template = EmailConfirmTemplate,
Body = UpdatePlaceHolders(await GetEmailBody(EmailConfirmTemplate), placeholders),
Preheader = UpdatePlaceHolders("You've been invited to join {{InvitingUser}}'s Server", placeholders),
ToEmails = new List<string>()
{
@ -221,8 +300,9 @@ public class EmailService : IEmailService
var emailOptions = new EmailOptionsDto()
{
Subject = UpdatePlaceHolders("A password reset has been requested", placeholders),
Body = UpdatePlaceHolders(await GetEmailBody("EmailPasswordReset"), placeholders),
Preheader = "A password reset has been requested",
Template = EmailPasswordResetTemplate,
Body = UpdatePlaceHolders(await GetEmailBody(EmailPasswordResetTemplate), placeholders),
Preheader = "Email confirmation is required for continued access. Click the button to confirm your email.",
ToEmails = new List<string>()
{
dto.EmailAddress
@ -242,11 +322,9 @@ public class EmailService : IEmailService
{
Subject = "Send file from Kavita",
Preheader = "File(s) sent from Kavita",
ToEmails = new List<string>()
{
data.DestinationEmail
},
Body = await GetEmailBody("SendToDevice"),
ToEmails = [data.DestinationEmail],
Template = SendToDeviceTemplate,
Body = await GetEmailBody(SendToDeviceTemplate),
Attachments = data.FilePaths.ToList()
};
@ -302,21 +380,66 @@ public class EmailService : IEmailService
ServicePointManager.SecurityProtocol = SecurityProtocolType.SystemDefault;
var emailAddress = userEmailOptions.ToEmails[0];
AppUser? user;
if (userEmailOptions.Template == SendToDeviceTemplate)
{
user = await _unitOfWork.UserRepository.GetUserByDeviceEmail(emailAddress);
}
else
{
user = await _unitOfWork.UserRepository.GetUserByEmailAsync(emailAddress);
}
try
{
await smtpClient.SendAsync(email);
if (user != null)
{
await LogEmailHistory(user.Id, userEmailOptions.Template, userEmailOptions.Subject, userEmailOptions.Body, "Sent");
}
}
catch (Exception ex)
{
_logger.LogError(ex, "There was an issue sending the email");
if (user != null)
{
await LogEmailHistory(user.Id, userEmailOptions.Template, userEmailOptions.Subject, userEmailOptions.Body, "Failed", ex.Message);
}
_logger.LogError("Could not find user on file for email, {Template} email was not sent and not recorded into history table", userEmailOptions.Template);
throw;
}
finally
{
await smtpClient.DisconnectAsync(true);
}
}
/// <summary>
/// Logs email history for the specified user.
/// </summary>
private async Task LogEmailHistory(int appUserId, string emailTemplate, string subject, string body, string deliveryStatus, string? errorMessage = null)
{
var emailHistory = new EmailHistory
{
AppUserId = appUserId,
EmailTemplate = emailTemplate,
Sent = deliveryStatus == "Sent",
Body = body,
Subject = subject,
SendDate = DateTime.UtcNow,
DeliveryStatus = deliveryStatus,
ErrorMessage = errorMessage
};
_unitOfWork.DataContext.EmailHistory.Add(emailHistory);
await _unitOfWork.CommitAsync();
}
private async Task<string> GetTemplatePath(string templateName)
{
if ((await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).SmtpConfig.CustomizedTemplates)