Skip to content

Commit

Permalink
Email is now Built-in! (#2635)
Browse files Browse the repository at this point in the history
  • Loading branch information
majora2007 authored Jan 20, 2024
1 parent 2a539da commit a85644f
Show file tree
Hide file tree
Showing 55 changed files with 5,085 additions and 1,003 deletions.
4 changes: 4 additions & 0 deletions API/API.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="MailKit" Version="4.3.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
Expand Down Expand Up @@ -190,6 +191,9 @@

<ItemGroup>
<Folder Include="config\themes" />
<Content Include="EmailTemplates\**">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<None Include="I18N\**" />
</ItemGroup>

Expand Down
209 changes: 85 additions & 124 deletions API/Controllers/AccountController.cs

Large diffs are not rendered by default.

20 changes: 15 additions & 5 deletions API/Controllers/DeviceController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -92,18 +92,28 @@ public async Task<ActionResult<IEnumerable<DeviceDto>>> GetDevices()
return Ok(await _unitOfWork.DeviceRepository.GetDevicesForUserAsync(User.GetUserId()));
}

/// <summary>
/// Sends a collection of chapters to the user's device
/// </summary>
/// <param name="dto"></param>
/// <returns></returns>
[HttpPost("send-to")]
public async Task<ActionResult> SendToDevice(SendToDeviceDto dto)
{
if (dto.ChapterIds.Any(i => i < 0)) return BadRequest(await _localizationService.Translate(User.GetUserId(), "greater-0", "ChapterIds"));
if (dto.DeviceId < 0) return BadRequest(await _localizationService.Translate(User.GetUserId(), "greater-0", "DeviceId"));

if (await _emailService.IsDefaultEmailService())
var isEmailSetup = (await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).IsEmailSetup();
if (!isEmailSetup)
return BadRequest(await _localizationService.Translate(User.GetUserId(), "send-to-kavita-email"));

// // Validate that the device belongs to the user
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(User.GetUserId(), AppUserIncludes.Devices);
if (user == null || user.Devices.All(d => d.Id != dto.DeviceId)) return BadRequest(await _localizationService.Translate(User.GetUserId(), "send-to-unallowed"));

var userId = User.GetUserId();
await _eventHub.SendMessageToAsync(MessageFactory.NotificationProgress,
MessageFactory.SendingToDeviceEvent(await _localizationService.Translate(User.GetUserId(), "send-to-device-status"),
MessageFactory.SendingToDeviceEvent(await _localizationService.Translate(userId, "send-to-device-status"),
"started"), userId);
try
{
Expand All @@ -112,16 +122,16 @@ await _eventHub.SendMessageToAsync(MessageFactory.NotificationProgress,
}
catch (KavitaException ex)
{
return BadRequest(await _localizationService.Translate(User.GetUserId(), ex.Message));
return BadRequest(await _localizationService.Translate(userId, ex.Message));
}
finally
{
await _eventHub.SendMessageToAsync(MessageFactory.SendingToDevice,
MessageFactory.SendingToDeviceEvent(await _localizationService.Translate(User.GetUserId(), "send-to-device-status"),
MessageFactory.SendingToDeviceEvent(await _localizationService.Translate(userId, "send-to-device-status"),
"ended"), userId);
}

return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-send-to"));
return BadRequest(await _localizationService.Translate(userId, "generic-send-to"));
}


Expand Down
34 changes: 12 additions & 22 deletions API/Controllers/PluginController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,9 @@ namespace API.Controllers;

#nullable enable

public class PluginController : BaseApiController
public class PluginController(IUnitOfWork unitOfWork, ITokenService tokenService, ILogger<PluginController> logger)
: BaseApiController
{
private readonly IUnitOfWork _unitOfWork;
private readonly ITokenService _tokenService;
private readonly ILogger<PluginController> _logger;

public PluginController(IUnitOfWork unitOfWork, ITokenService tokenService, ILogger<PluginController> logger)
{
_unitOfWork = unitOfWork;
_tokenService = tokenService;
_logger = logger;
}

/// <summary>
/// Authenticate with the Server given an apiKey. This will log you in by returning the user object and the JWT token.
/// </summary>
Expand All @@ -42,27 +32,27 @@ public async Task<ActionResult<UserDto>> Authenticate([Required] string apiKey,
// NOTE: In order to log information about plugins, we need some Plugin Description information for each request
// Should log into access table so we can tell the user
var ipAddress = HttpContext.Connection.RemoteIpAddress?.ToString();
var userAgent = HttpContext.Request.Headers["User-Agent"];
var userId = await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey);
var userAgent = HttpContext.Request.Headers.UserAgent;
var userId = await unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey);
if (userId <= 0)
{
_logger.LogInformation("A Plugin ({PluginName}) tried to authenticate with an apiKey that doesn't match. Information {@Information}", pluginName.Replace(Environment.NewLine, string.Empty), new
logger.LogInformation("A Plugin ({PluginName}) tried to authenticate with an apiKey that doesn't match. Information {@Information}", pluginName.Replace(Environment.NewLine, string.Empty), new
{
IpAddress = ipAddress,
UserAgent = userAgent,
ApiKey = apiKey
});
throw new KavitaUnauthenticatedUserException();
}
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId);
_logger.LogInformation("Plugin {PluginName} has authenticated with {UserName} ({UserId})'s API Key", pluginName.Replace(Environment.NewLine, string.Empty), user!.UserName, userId);
var user = await unitOfWork.UserRepository.GetUserByIdAsync(userId);
logger.LogInformation("Plugin {PluginName} has authenticated with {UserName} ({UserId})'s API Key", pluginName.Replace(Environment.NewLine, string.Empty), user!.UserName, userId);
return new UserDto
{
Username = user.UserName!,
Token = await _tokenService.CreateToken(user),
RefreshToken = await _tokenService.CreateRefreshToken(user),
Token = await tokenService.CreateToken(user),
RefreshToken = await tokenService.CreateRefreshToken(user),
ApiKey = user.ApiKey,
KavitaVersion = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.InstallVersion)).Value
KavitaVersion = (await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.InstallVersion)).Value
};
}

Expand All @@ -76,8 +66,8 @@ public async Task<ActionResult<UserDto>> Authenticate([Required] string apiKey,
[HttpGet("version")]
public async Task<ActionResult<string>> GetVersion([Required] string apiKey)
{
var userId = await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey);
var userId = await unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey);
if (userId <= 0) throw new KavitaUnauthenticatedUserException();
return Ok((await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.InstallVersion)).Value);
return Ok((await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.InstallVersion)).Value);
}
}
17 changes: 0 additions & 17 deletions API/Controllers/ServerController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -275,22 +275,6 @@ public async Task<ActionResult> BustReviewAndRecCache()
return Ok();
}

/// <summary>
/// Returns the KavitaEmail version for non-default instances
/// </summary>
/// <returns></returns>
[Authorize("RequireAdminRole")]
[HttpGet("email-version")]
public async Task<ActionResult<string?>> GetEmailVersion()
{
var emailServiceUrl = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.EmailServiceUrl))
.Value;

if (emailServiceUrl.Equals(EmailService.DefaultApiUrl)) return Ok(null);

return Ok(await _emailService.GetVersion(emailServiceUrl));
}

/// <summary>
/// Checks for updates and pushes an event to the UI
/// </summary>
Expand All @@ -301,5 +285,4 @@ public async Task<ActionResult> CheckForAnnouncements()
await _taskScheduler.CheckForUpdate();
return Ok();
}

}
107 changes: 74 additions & 33 deletions API/Controllers/SettingsController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using API.Data;
using API.DTOs.Email;
using API.DTOs.Settings;
using API.Entities;
using API.Entities.Enums;
using API.Extensions;
using API.Helpers.Converters;
Expand Down Expand Up @@ -119,38 +120,28 @@ public async Task<ActionResult<ServerSettingDto>> ResetBaseUrlSettings()
}

/// <summary>
/// Resets the email service url
/// Sends a test email from the Email Service.
/// </summary>
/// <param name="dto"></param>
/// <returns></returns>
[Authorize(Policy = "RequireAdminRole")]
[HttpPost("reset-email-url")]
public async Task<ActionResult<ServerSettingDto>> ResetEmailServiceUrlSettings()
[HttpPost("test-email-url")]
public async Task<ActionResult<EmailTestResultDto>> TestEmailServiceUrl(TestEmailDto dto)
{
_logger.LogInformation("{UserName} is resetting Email Service Url Setting", User.GetUsername());
var emailSetting = await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.EmailServiceUrl);
emailSetting.Value = EmailService.DefaultApiUrl;
_unitOfWork.SettingsRepository.Update(emailSetting);

if (!await _unitOfWork.CommitAsync())
{
await _unitOfWork.RollbackAsync();
}

return Ok(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync());
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(User.GetUserId());
return Ok(await _emailService.SendTestEmail(user!.Email));
}

/// <summary>
/// Sends a test email from the Email Service. Will not send if email service is the Default Provider
/// Is the minimum information setup for Email to work
/// </summary>
/// <param name="dto"></param>
/// <returns></returns>
[Authorize(Policy = "RequireAdminRole")]
[HttpPost("test-email-url")]
public async Task<ActionResult<EmailTestResultDto>> TestEmailServiceUrl(TestEmailDto dto)
[Authorize]
[HttpGet("is-email-setup")]
public async Task<ActionResult<bool>> IsEmailSetup()
{
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(User.GetUserId());
var emailService = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.EmailServiceUrl)).Value;
return Ok(await _emailService.TestConnectivity(dto.Url, user!.Email, !emailService.Equals(EmailService.DefaultApiUrl)));
var settings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync();
return Ok(settings.IsEmailSetup());
}


Expand Down Expand Up @@ -233,6 +224,10 @@ public async Task<ActionResult<ServerSettingDto>> UpdateSettings(ServerSettingDt
_unitOfWork.SettingsRepository.Update(setting);
}

UpdateEmailSettings(setting, updateSettingsDto);



if (setting.Key == ServerSettingKey.IpAddresses && updateSettingsDto.IpAddresses != setting.Value)
{
if (OsInfo.IsDocker) continue;
Expand Down Expand Up @@ -289,17 +284,6 @@ public async Task<ActionResult<ServerSettingDto>> UpdateSettings(ServerSettingDt
_unitOfWork.SettingsRepository.Update(setting);
}

if (setting.Key == ServerSettingKey.EmailServiceUrl && updateSettingsDto.EmailServiceUrl + string.Empty != setting.Value)
{
setting.Value = string.IsNullOrEmpty(updateSettingsDto.EmailServiceUrl) ? EmailService.DefaultApiUrl : updateSettingsDto.EmailServiceUrl;
setting.Value = UrlHelper.RemoveEndingSlash(setting.Value);
FlurlHttp.ConfigureClient(setting.Value, cli =>
cli.Settings.HttpClientFactory = new UntrustedCertClientFactory());

_unitOfWork.SettingsRepository.Update(setting);
}


if (setting.Key == ServerSettingKey.BookmarkDirectory && bookmarkDirectory != setting.Value)
{
// Validate new directory can be used
Expand Down Expand Up @@ -392,6 +376,63 @@ public async Task<ActionResult<ServerSettingDto>> UpdateSettings(ServerSettingDt
return Ok(updateSettingsDto);
}

private void UpdateEmailSettings(ServerSetting setting, ServerSettingDto updateSettingsDto)
{
if (setting.Key == ServerSettingKey.EmailHost && updateSettingsDto.SmtpConfig.Host + string.Empty != setting.Value)
{
setting.Value = updateSettingsDto.SmtpConfig.Host + string.Empty;
_unitOfWork.SettingsRepository.Update(setting);
}

if (setting.Key == ServerSettingKey.EmailPort && updateSettingsDto.SmtpConfig.Port + string.Empty != setting.Value)
{
setting.Value = updateSettingsDto.SmtpConfig.Port + string.Empty;
_unitOfWork.SettingsRepository.Update(setting);
}

if (setting.Key == ServerSettingKey.EmailAuthPassword && updateSettingsDto.SmtpConfig.Password + string.Empty != setting.Value)
{
setting.Value = updateSettingsDto.SmtpConfig.Password + string.Empty;
_unitOfWork.SettingsRepository.Update(setting);
}

if (setting.Key == ServerSettingKey.EmailAuthUserName && updateSettingsDto.SmtpConfig.UserName + string.Empty != setting.Value)
{
setting.Value = updateSettingsDto.SmtpConfig.UserName + string.Empty;
_unitOfWork.SettingsRepository.Update(setting);
}

if (setting.Key == ServerSettingKey.EmailSenderAddress && updateSettingsDto.SmtpConfig.SenderAddress + string.Empty != setting.Value)
{
setting.Value = updateSettingsDto.SmtpConfig.SenderAddress + string.Empty;
_unitOfWork.SettingsRepository.Update(setting);
}

if (setting.Key == ServerSettingKey.EmailSenderDisplayName && updateSettingsDto.SmtpConfig.SenderDisplayName + string.Empty != setting.Value)
{
setting.Value = updateSettingsDto.SmtpConfig.SenderDisplayName + string.Empty;
_unitOfWork.SettingsRepository.Update(setting);
}

if (setting.Key == ServerSettingKey.EmailSizeLimit && updateSettingsDto.SmtpConfig.SizeLimit + string.Empty != setting.Value)
{
setting.Value = updateSettingsDto.SmtpConfig.SizeLimit + string.Empty;
_unitOfWork.SettingsRepository.Update(setting);
}

if (setting.Key == ServerSettingKey.EmailEnableSsl && updateSettingsDto.SmtpConfig.EnableSsl + string.Empty != setting.Value)
{
setting.Value = updateSettingsDto.SmtpConfig.EnableSsl + string.Empty;
_unitOfWork.SettingsRepository.Update(setting);
}

if (setting.Key == ServerSettingKey.EmailCustomizedTemplates && updateSettingsDto.SmtpConfig.CustomizedTemplates + string.Empty != setting.Value)
{
setting.Value = updateSettingsDto.SmtpConfig.CustomizedTemplates + string.Empty;
_unitOfWork.SettingsRepository.Update(setting);
}
}

[Authorize(Policy = "RequireAdminRole")]
[HttpGet("task-frequencies")]
public ActionResult<IEnumerable<string>> GetTaskFrequencies()
Expand Down
1 change: 1 addition & 0 deletions API/DTOs/Email/EmailTestResultDto.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ public class EmailTestResultDto
{
public bool Successful { get; set; }
public string ErrorMessage { get; set; } = default!;
public string EmailAddress { get; set; } = default!;
}
20 changes: 20 additions & 0 deletions API/DTOs/Settings/SMTPConfigDto.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
namespace API.DTOs.Settings;

public class SmtpConfigDto
{
public string SenderAddress { get; set; } = string.Empty;
public string SenderDisplayName { get; set; } = string.Empty;
public string UserName { get; set; } = string.Empty;
public string Password { get; set; } = string.Empty;
public string Host { get; set; } = string.Empty;
public int Port { get; set; } = 0;
public bool EnableSsl { get; set; } = true;
/// <summary>
/// Limit in bytes for allowing files to be added as attachments. Defaults to 25MB
/// </summary>
public int SizeLimit { get; set; } = 26_214_400;
/// <summary>
/// Should Kavita use config/templates for Email templates or the default ones
/// </summary>
public bool CustomizedTemplates { get; set; } = false;
}
21 changes: 16 additions & 5 deletions API/DTOs/Settings/ServerSettingDTO.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,6 @@ public class ServerSettingDto
/// </summary>
/// <remarks>If null or empty string, will default back to default install setting aka <see cref="DirectoryService.BookmarkDirectory"/></remarks>
public string BookmarksDirectory { get; set; } = default!;
/// <summary>
/// Email service to use for the invite user flow, forgot password, etc.
/// </summary>
/// <remarks>If null or empty string, will default back to default install setting aka <see cref="EmailService.DefaultApiUrl"/></remarks>
public string EmailServiceUrl { get; set; } = default!;
public string InstallVersion { get; set; } = default!;
/// <summary>
/// Represents a unique Id to this Kavita installation. Only used in Stats to identify unique installs.
Expand Down Expand Up @@ -88,4 +83,20 @@ public class ServerSettingDto
/// How large the cover images should be
/// </summary>
public CoverImageSize CoverImageSize { get; set; }
/// <summary>
/// SMTP Configuration
/// </summary>
public SmtpConfigDto SmtpConfig { get; set; }

/// <summary>
/// Are at least some basics filled in
/// </summary>
/// <returns></returns>
public bool IsEmailSetup()
{
//return false;
return !string.IsNullOrEmpty(SmtpConfig.Host)
&& !string.IsNullOrEmpty(SmtpConfig.UserName)
&& !string.IsNullOrEmpty(HostName);
}
}
Loading

0 comments on commit a85644f

Please sign in to comment.