Private Email Service Support (#1028)

* Added ServerSettingKey's for SMTP and moved email service code to Kavita. Nothing integrated in the UI yet.

* Undo all the custom SMTP stuff and prepare for custom email service url.

* Foundation for email service to use a custom url is setup.

* Implemented the ability to hook up custom email url
This commit is contained in:
Joseph Milazzo 2022-02-04 09:54:54 -08:00 committed by GitHub
parent 2517ee75b2
commit 2ae9f8c203
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 193 additions and 54 deletions

View file

@ -101,6 +101,9 @@
<Compile Remove="logs\**" />
<Compile Remove="temp\**" />
<Compile Remove="covers\**" />
<Compile Remove="DTOs\Email\SmtpConfig.cs" />
<Compile Remove="DTOs\Email\EmailOptionsDto.cs" />
<Compile Remove="Helpers\Converters\SmtpConverter.cs" />
</ItemGroup>
<ItemGroup>

View file

@ -1,8 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Reflection;
using System.Threading.Tasks;
using System.Web;
@ -17,8 +15,6 @@ using API.Errors;
using API.Extensions;
using API.Services;
using AutoMapper;
using AutoMapper.QueryableExtensions;
using Flurl.Util;
using Kavita.Common;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
@ -113,14 +109,6 @@ namespace API.Controllers
ApiKey = HashUtil.ApiKey()
};
// I am removing Authentication disabled code
// var settings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync();
// if (!settings.EnableAuthentication && !registerDto.IsAdmin)
// {
// _logger.LogInformation("User {UserName} is being registered as non-admin with no server authentication. Using default password", registerDto.Username);
// registerDto.Password = AccountService.DefaultPassword;
// }
var result = await _userManager.CreateAsync(user, registerDto.Password);
if (!result.Succeeded) return BadRequest(result.Errors);
@ -132,22 +120,6 @@ namespace API.Controllers
var roleResult = await _userManager.AddToRoleAsync(user, PolicyConstants.AdminRole);
if (!roleResult.Succeeded) return BadRequest(result.Errors);
// // When we register an admin, we need to grant them access to all Libraries.
// if (registerDto.IsAdmin)
// {
// _logger.LogInformation("{UserName} is being registered as admin. Granting access to all libraries",
// user.UserName);
// var libraries = (await _unitOfWork.LibraryRepository.GetLibrariesAsync()).ToList();
// foreach (var lib in libraries)
// {
// lib.AppUsers ??= new List<AppUser>();
// lib.AppUsers.Add(user);
// }
//
// if (libraries.Any() && !await _unitOfWork.CommitAsync())
// _logger.LogError("There was an issue granting library access. Please do this manually");
// }
return new UserDto
{
Username = user.UserName,

View file

@ -4,14 +4,17 @@ using System.IO;
using System.Linq;
using System.Threading.Tasks;
using API.Data;
using API.DTOs.Email;
using API.DTOs.Settings;
using API.Entities.Enums;
using API.Extensions;
using API.Helpers.Converters;
using API.Services;
using AutoMapper;
using Flurl.Http;
using Kavita.Common;
using Kavita.Common.Extensions;
using Kavita.Common.Helpers;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
@ -25,15 +28,17 @@ namespace API.Controllers
private readonly ITaskScheduler _taskScheduler;
private readonly IDirectoryService _directoryService;
private readonly IMapper _mapper;
private readonly IEmailService _emailService;
public SettingsController(ILogger<SettingsController> logger, IUnitOfWork unitOfWork, ITaskScheduler taskScheduler,
IDirectoryService directoryService, IMapper mapper)
IDirectoryService directoryService, IMapper mapper, IEmailService emailService)
{
_logger = logger;
_unitOfWork = unitOfWork;
_taskScheduler = taskScheduler;
_directoryService = directoryService;
_mapper = mapper;
_emailService = emailService;
}
[AllowAnonymous]
@ -64,6 +69,36 @@ namespace API.Controllers
return await UpdateSettings(_mapper.Map<ServerSettingDto>(Seed.DefaultSettings));
}
/// <summary>
/// Resets the email service url
/// </summary>
/// <returns></returns>
[Authorize(Policy = "RequireAdminRole")]
[HttpPost("reset-email-url")]
public async Task<ActionResult<ServerSettingDto>> ResetEmailServiceUrlSettings()
{
_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());
}
[Authorize(Policy = "RequireAdminRole")]
[HttpPost("test-email-url")]
public async Task<ActionResult<bool>> TestEmailServiceUrl(TestEmailDto dto)
{
return Ok(await _emailService.TestConnectivity(dto.Url));
}
[Authorize(Policy = "RequireAdminRole")]
[HttpPost]
public async Task<ActionResult<ServerSettingDto>> UpdateSettings(ServerSettingDto updateSettingsDto)
@ -173,6 +208,15 @@ namespace API.Controllers
await _taskScheduler.ScheduleStatsTasks();
}
}
if (setting.Key == ServerSettingKey.EmailServiceUrl && updateSettingsDto.EmailServiceUrl + string.Empty != setting.Value)
{
setting.Value = string.IsNullOrEmpty(updateSettingsDto.EmailServiceUrl) ? EmailService.DefaultApiUrl : updateSettingsDto.EmailServiceUrl;
FlurlHttp.ConfigureClient(setting.Value, cli =>
cli.Settings.HttpClientFactory = new UntrustedCertClientFactory());
_unitOfWork.SettingsRepository.Update(setting);
}
}
if (!_unitOfWork.HasChanges()) return Ok(updateSettingsDto);

View file

@ -0,0 +1,6 @@
namespace API.DTOs.Email;
public class TestEmailDto
{
public string Url { get; set; }
}

View file

@ -32,5 +32,10 @@ namespace API.DTOs.Settings
/// </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; }
/// <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; }
}
}

View file

@ -60,6 +60,7 @@ namespace API.Data
new () {Key = ServerSettingKey.InstallId, Value = HashUtil.AnonymousToken()},
new () {Key = ServerSettingKey.InstallVersion, Value = BuildInfo.Version.ToString()},
new () {Key = ServerSettingKey.BookmarkDirectory, Value = directoryService.BookmarkDirectory},
new () {Key = ServerSettingKey.EmailServiceUrl, Value = EmailService.DefaultApiUrl},
};
foreach (var defaultSetting in DefaultSettings)

View file

@ -71,6 +71,10 @@ namespace API.Entities.Enums
/// </summary>
[Description("BookmarkDirectory")]
BookmarkDirectory = 12,
/// <summary>
/// If SMTP is enabled on the server
/// </summary>
[Description("CustomEmailService")]
EmailServiceUrl = 13,
}
}

View file

@ -39,7 +39,6 @@ namespace API.Extensions
services.AddScoped<IAccountService, AccountService>();
services.AddScoped<IEmailService, EmailService>();
services.AddScoped<IFileSystem, FileSystem>();
services.AddScoped<IFileService, FileService>();
services.AddScoped<ICacheHelper, CacheHelper>();

View file

@ -2,6 +2,7 @@
using System.Linq;
using API.DTOs;
using API.DTOs.CollectionTags;
using API.DTOs.Email;
using API.DTOs.Metadata;
using API.DTOs.Reader;
using API.DTOs.ReadingLists;
@ -148,7 +149,6 @@ namespace API.Helpers
CreateMap<IEnumerable<ServerSetting>, ServerSettingDto>()
.ConvertUsing<ServerSettingConverter>();
}
}
}

View file

@ -42,6 +42,9 @@ namespace API.Helpers.Converters
case ServerSettingKey.BookmarkDirectory:
destination.BookmarksDirectory = row.Value;
break;
case ServerSettingKey.EmailServiceUrl:
destination.EmailServiceUrl = row.Value;
break;
}
}

View file

@ -1,10 +1,11 @@
using System;
using System.Net.Http;
using System.Threading.Tasks;
using API.Data;
using API.DTOs.Email;
using API.Services.Tasks;
using API.Entities.Enums;
using Flurl.Http;
using Kavita.Common.EnvironmentInfo;
using Kavita.Common.Helpers;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
@ -16,25 +17,39 @@ public interface IEmailService
Task<bool> CheckIfAccessible(string host);
Task SendMigrationEmail(EmailMigrationDto data);
Task SendPasswordResetEmail(PasswordResetEmailDto data);
Task<bool> TestConnectivity(string emailUrl);
}
public class EmailService : IEmailService
{
private readonly ILogger<EmailService> _logger;
private const string ApiUrl = "https://email.kavitareader.com";
private readonly IUnitOfWork _unitOfWork;
public EmailService(ILogger<EmailService> logger)
/// <summary>
/// This is used to initially set or reset the ServerSettingKey. Do not access from the code, access via UnitOfWork
/// </summary>
public const string DefaultApiUrl = "https://email.kavitareader.com";
public EmailService(ILogger<EmailService> logger, IUnitOfWork unitOfWork)
{
_logger = logger;
_unitOfWork = unitOfWork;
FlurlHttp.ConfigureClient(ApiUrl, cli =>
FlurlHttp.ConfigureClient(DefaultApiUrl, cli =>
cli.Settings.HttpClientFactory = new UntrustedCertClientFactory());
}
public async Task<bool> TestConnectivity(string emailUrl)
{
FlurlHttp.ConfigureClient(emailUrl, cli =>
cli.Settings.HttpClientFactory = new UntrustedCertClientFactory());
return await SendEmailWithGet(emailUrl + "/api/email/test");
}
public async Task SendConfirmationEmail(ConfirmationEmailDto data)
{
var success = await SendEmailWithPost(ApiUrl + "/api/email/confirm", data);
var success = await SendEmailWithPost(DefaultApiUrl + "/api/email/confirm", data);
if (!success)
{
_logger.LogError("There was a critical error sending Confirmation email");
@ -43,17 +58,20 @@ public class EmailService : IEmailService
public async Task<bool> CheckIfAccessible(string host)
{
return await SendEmailWithGet(ApiUrl + "/api/email/reachable?host=" + host);
// This is the only exception for using the default because we need an external service to check if the server is accessible for emails
return await SendEmailWithGet(DefaultApiUrl + "/api/email/reachable?host=" + host);
}
public async Task SendMigrationEmail(EmailMigrationDto data)
{
await SendEmailWithPost(ApiUrl + "/api/email/email-migration", data);
var emailLink = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.EmailServiceUrl)).Value;
await SendEmailWithPost(emailLink + "/api/email/email-migration", data);
}
public async Task SendPasswordResetEmail(PasswordResetEmailDto data)
{
await SendEmailWithPost(ApiUrl + "/api/email/email-password-reset", data);
var emailLink = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.EmailServiceUrl)).Value;
await SendEmailWithPost(emailLink + "/api/email/email-password-reset", data);
}
private static async Task<bool> SendEmailWithGet(string url)
@ -106,4 +124,5 @@ public class EmailService : IEmailService
}
return true;
}
}

View file

@ -7,6 +7,7 @@ using API.DTOs.Stats;
using API.Entities.Enums;
using Flurl.Http;
using Kavita.Common.EnvironmentInfo;
using Kavita.Common.Helpers;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;

View file

@ -1,14 +1,13 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using API.DTOs.Update;
using API.SignalR;
using API.SignalR.Presence;
using Flurl.Http;
using Flurl.Http.Configuration;
using Kavita.Common.EnvironmentInfo;
using Kavita.Common.Helpers;
using MarkdownDeep;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.Hosting;
@ -44,15 +43,6 @@ internal class GithubReleaseMetadata
public string Published_At { get; init; }
}
public class UntrustedCertClientFactory : DefaultHttpClientFactory
{
public override HttpMessageHandler CreateMessageHandler() {
return new HttpClientHandler {
ServerCertificateCustomValidationCallback = (_, _, _, _) => true
};
}
}
public interface IVersionUpdaterService
{
Task<UpdateNotificationDto> CheckForUpdate();