Email is now Built-in! (#2635)

This commit is contained in:
Joe Milazzo 2024-01-20 11:16:54 -06:00 committed by GitHub
parent 2a539da24c
commit a85644fb6b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
55 changed files with 5129 additions and 1047 deletions

View File

@ -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>
@ -190,6 +191,9 @@
<ItemGroup>
<Folder Include="config\themes" />
<Content Include="EmailTemplates\**">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<None Include="I18N\**" />
</ItemGroup>

View File

@ -334,7 +334,9 @@ public class AccountController : BaseApiController
/// <summary>
/// Initiates the flow to update a user's email address. The email address is not changed in this API. A confirmation link is sent/dumped which will
/// Initiates the flow to update a user's email address.
///
/// If email is not setup, then the email address is not changed in this API. A confirmation link is sent/dumped which will
/// validate the email. It must be confirmed for the email to update.
/// </summary>
/// <param name="dto"></param>
@ -374,10 +376,22 @@ public class AccountController : BaseApiController
return BadRequest(await _localizationService.Translate(User.GetUserId(), "generate-token"));
}
user.EmailConfirmed = false;
var serverSettings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync();
var shouldEmailUser = serverSettings.IsEmailSetup() || !_emailService.IsValidEmail(user.Email);
user.EmailConfirmed = !shouldEmailUser;
user.ConfirmationToken = token;
await _userManager.UpdateAsync(user);
if (!shouldEmailUser)
{
return Ok(new InviteUserResponse
{
EmailLink = string.Empty,
EmailSent = false
});
}
// Send a confirmation email
try
{
@ -396,30 +410,27 @@ public class AccountController : BaseApiController
}
var accessible = await _accountService.CheckIfAccessible(Request);
if (accessible)
try
{
try
var invitingUser = (await _unitOfWork.UserRepository.GetAdminUsersAsync()).First().UserName!;
// Email the old address of the update change
BackgroundJob.Enqueue(() => _emailService.SendEmailChangeEmail(new ConfirmationEmailDto()
{
// Email the old address of the update change
await _emailService.SendEmailChangeEmail(new ConfirmationEmailDto()
{
EmailAddress = string.IsNullOrEmpty(user.Email) ? dto.Email : user.Email,
InstallId = BuildInfo.Version.ToString(),
InvitingUser = (await _unitOfWork.UserRepository.GetAdminUsersAsync()).First().UserName!,
ServerConfirmationLink = emailLink
});
}
catch (Exception)
{
/* Swallow exception */
}
EmailAddress = string.IsNullOrEmpty(user.Email) ? dto.Email : user.Email,
InstallId = BuildInfo.Version.ToString(),
InvitingUser = invitingUser,
ServerConfirmationLink = emailLink
}));
}
catch (Exception)
{
/* Swallow exception */
}
return Ok(new InviteUserResponse
{
EmailLink = string.Empty,
EmailSent = accessible
EmailSent = true
});
}
catch (Exception ex)
@ -579,8 +590,7 @@ public class AccountController : BaseApiController
/// <summary>
/// Invites a user to the server. Will generate a setup link for continuing setup. If the server is not accessible, no
/// email will be sent.
/// Invites a user to the server. Will generate a setup link for continuing setup. If email is not setup, a link will be presented to user to continue setup.
/// </summary>
/// <param name="dto"></param>
/// <returns></returns>
@ -679,15 +689,15 @@ public class AccountController : BaseApiController
return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-invite-user"));
}
try
{
var emailLink = await _accountService.GenerateEmailLink(Request, user.ConfirmationToken, "confirm-email", dto.Email);
_logger.LogCritical("[Invite User]: Email Link for {UserName}: {Link}", user.UserName, emailLink);
if (!_emailService.IsValidEmail(dto.Email))
var settings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync();
if (!_emailService.IsValidEmail(dto.Email) || !settings.IsEmailSetup())
{
_logger.LogInformation("[Invite User] {Email} doesn't appear to be an email, so will not send an email to address", dto.Email.Replace(Environment.NewLine, string.Empty));
_logger.LogInformation("[Invite User] {Email} doesn't appear to be an email or email is not setup", dto.Email.Replace(Environment.NewLine, string.Empty));
return Ok(new InviteUserResponse
{
EmailLink = emailLink,
@ -696,22 +706,17 @@ public class AccountController : BaseApiController
});
}
var accessible = await _accountService.CheckIfAccessible(Request);
if (accessible)
BackgroundJob.Enqueue(() => _emailService.SendInviteEmail(new ConfirmationEmailDto()
{
// Do the email send on a background thread to ensure UI can move forward without having to wait for a timeout when users use fake emails
BackgroundJob.Enqueue(() => _emailService.SendConfirmationEmail(new ConfirmationEmailDto()
{
EmailAddress = dto.Email,
InvitingUser = adminUser.UserName,
ServerConfirmationLink = emailLink
}));
}
EmailAddress = dto.Email,
InvitingUser = adminUser.UserName,
ServerConfirmationLink = emailLink
}));
return Ok(new InviteUserResponse
{
EmailLink = emailLink,
EmailSent = accessible
EmailSent = true
});
}
catch (Exception ex)
@ -837,7 +842,6 @@ public class AccountController : BaseApiController
await _eventHub.SendMessageToAsync(MessageFactory.UserUpdate,
MessageFactory.UserUpdateEvent(user.Id, user.UserName!), user.Id);
// Perform Login code
return Ok();
}
@ -882,6 +886,10 @@ public class AccountController : BaseApiController
[EnableRateLimiting("Authentication")]
public async Task<ActionResult<string>> ForgotPassword([FromQuery] string email)
{
var settings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync();
if (!settings.IsEmailSetup()) return Ok(await _localizationService.Get("en", "email-not-enabled"));
var user = await _unitOfWork.UserRepository.GetUserByEmailAsync(email);
if (user == null)
{
@ -896,26 +904,28 @@ public class AccountController : BaseApiController
if (string.IsNullOrEmpty(user.Email) || !user.EmailConfirmed)
return BadRequest(await _localizationService.Translate(user.Id, "confirm-email"));
var token = await _userManager.GeneratePasswordResetTokenAsync(user);
var emailLink = await _accountService.GenerateEmailLink(Request, token, "confirm-reset-password", user.Email);
_logger.LogCritical("[Forgot Password]: Email Link for {UserName}: {Link}", user.UserName, emailLink);
if (!_emailService.IsValidEmail(user.Email))
{
_logger.LogCritical("[Forgot Password]: User is trying to do a forgot password flow, but their email ({Email}) isn't valid. No email will be send", user.Email);
_logger.LogCritical("[Forgot Password]: User is trying to do a forgot password flow, but their email ({Email}) isn't valid. No email will be send. Admin must change it in UI", user.Email);
return Ok(await _localizationService.Translate(user.Id, "invalid-email"));
}
if (await _accountService.CheckIfAccessible(Request))
{
await _emailService.SendPasswordResetEmail(new PasswordResetEmailDto()
{
EmailAddress = user.Email,
ServerConfirmationLink = emailLink,
InstallId = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.InstallId)).Value
});
return Ok(await _localizationService.Translate(user.Id, "email-sent"));
}
return Ok(await _localizationService.Translate(user.Id, "not-accessible-password"));
var token = await _userManager.GeneratePasswordResetTokenAsync(user);
var emailLink = await _accountService.GenerateEmailLink(Request, token, "confirm-reset-password", user.Email);
user.ConfirmationToken = token;
_unitOfWork.UserRepository.Update(user);
await _unitOfWork.CommitAsync();
_logger.LogCritical("[Forgot Password]: Email Link for {UserName}: {Link}", user.UserName, emailLink);
var installId = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.InstallId)).Value;
BackgroundJob.Enqueue(() => _emailService.SendForgotPasswordEmail(new PasswordResetEmailDto()
{
EmailAddress = user.Email,
ServerConfirmationLink = emailLink,
InstallId = installId
}));
return Ok(await _localizationService.Translate(user.Id, "email-sent"));
}
[HttpGet("email-confirmed")]
@ -965,7 +975,7 @@ public class AccountController : BaseApiController
/// <returns></returns>
[HttpPost("resend-confirmation-email")]
[EnableRateLimiting("Authentication")]
public async Task<ActionResult<string>> ResendConfirmationSendEmail([FromQuery] int userId)
public async Task<ActionResult<InviteUserResponse>> ResendConfirmationSendEmail([FromQuery] int userId)
{
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId);
if (user == null) return BadRequest(await _localizationService.Get("en", "no-user"));
@ -976,96 +986,47 @@ public class AccountController : BaseApiController
if (user.EmailConfirmed) return BadRequest(await _localizationService.Translate(user.Id, "user-already-confirmed"));
var token = await _userManager.GenerateEmailConfirmationTokenAsync(user);
var emailLink = await _accountService.GenerateEmailLink(Request, token, "confirm-email", user.Email);
user.ConfirmationToken = token;
_unitOfWork.UserRepository.Update(user);
await _unitOfWork.CommitAsync();
var emailLink = await _accountService.GenerateEmailLink(Request, token, "confirm-email-update", user.Email);
_logger.LogCritical("[Email Migration]: Email Link for {UserName}: {Link}", user.UserName, emailLink);
if (!_emailService.IsValidEmail(user.Email))
{
_logger.LogCritical("[Email Migration]: User {UserName} is trying to resend an invite flow, but their email ({Email}) isn't valid. No email will be send", user.UserName, user.Email);
return BadRequest(await _localizationService.Translate(user.Id, "invalid-email"));
}
if (await _accountService.CheckIfAccessible(Request))
var serverSettings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync();
var shouldEmailUser = serverSettings.IsEmailSetup() || !_emailService.IsValidEmail(user.Email);
if (!shouldEmailUser)
{
try
return Ok(new InviteUserResponse()
{
await _emailService.SendMigrationEmail(new EmailMigrationDto()
{
EmailAddress = user.Email!,
Username = user.UserName!,
ServerConfirmationLink = emailLink,
InstallId = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.InstallId)).Value
});
}
catch (Exception ex)
{
_logger.LogError(ex, "There was an issue resending invite email");
return BadRequest(await _localizationService.Translate(user.Id, "generic-invite-email"));
}
return Ok(emailLink);
EmailLink = emailLink,
EmailSent = false,
InvalidEmail = !_emailService.IsValidEmail(user.Email)
});
}
return BadRequest(await _localizationService.Translate(user.Id, "not-accessible"));
BackgroundJob.Enqueue(() => _emailService.SendInviteEmail(new ConfirmationEmailDto()
{
EmailAddress = user.Email!,
InvitingUser = User.GetUsername(),
ServerConfirmationLink = emailLink,
InstallId = serverSettings.InstallId
}));
return Ok(new InviteUserResponse()
{
EmailLink = emailLink,
EmailSent = true,
InvalidEmail = !_emailService.IsValidEmail(user.Email)
});
}
/// <summary>
/// This is similar to invite. Essentially we authenticate the user's password then go through invite email flow
/// </summary>
/// <param name="dto"></param>
/// <returns></returns>
[AllowAnonymous]
[HttpPost("migrate-email")]
public async Task<ActionResult<string>> MigrateEmail(MigrateUserEmailDto dto)
{
// If there is an admin account already, return
var users = await _unitOfWork.UserRepository.GetAdminUsersAsync();
if (users.Any()) return BadRequest(await _localizationService.Get("en", "admin-already-exists"));
// Check if there is an existing invite
var emailValidationErrors = await _accountService.ValidateEmail(dto.Email);
if (emailValidationErrors.Any())
{
var invitedUser = await _unitOfWork.UserRepository.GetUserByEmailAsync(dto.Email);
if (await _userManager.IsEmailConfirmedAsync(invitedUser!))
return BadRequest(await _localizationService.Get("en", "user-already-registered", invitedUser!.UserName));
_logger.LogInformation("A user is attempting to login, but hasn't accepted email invite");
return BadRequest(await _localizationService.Get("en", "user-already-invited"));
}
var user = await _userManager.Users
.Include(u => u.UserPreferences)
.SingleOrDefaultAsync(x => x.NormalizedUserName == dto.Username.ToUpper());
if (user == null) return BadRequest(await _localizationService.Get("en", "invalid-username"));
var validPassword = await _signInManager.UserManager.CheckPasswordAsync(user, dto.Password);
if (!validPassword) return BadRequest(await _localizationService.Get("en", "bad-credentials"));
try
{
var token = await _userManager.GenerateEmailConfirmationTokenAsync(user);
user.Email = dto.Email;
if (!await ConfirmEmailToken(token, user)) return BadRequest(await _localizationService.Get("en", "critical-email-migration"));
_unitOfWork.UserRepository.Update(user);
await _unitOfWork.CommitAsync();
return Ok();
}
catch (Exception ex)
{
_logger.LogError(ex, "There was an issue during email migration. Contact support");
_unitOfWork.UserRepository.Delete(user);
await _unitOfWork.CommitAsync();
}
return BadRequest(await _localizationService.Get("en", "critical-email-migration"));
}
private async Task<bool> ConfirmEmailToken(string token, AppUser user)
{
var result = await _userManager.ConfirmEmailAsync(user, token);

View File

@ -92,18 +92,28 @@ public class DeviceController : BaseApiController
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
{
@ -112,16 +122,16 @@ public class DeviceController : BaseApiController
}
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"));
}

View File

@ -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>
@ -42,11 +32,11 @@ public class PluginController : BaseApiController
// 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,
@ -54,15 +44,15 @@ public class PluginController : BaseApiController
});
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
};
}
@ -76,8 +66,8 @@ public class PluginController : BaseApiController
[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);
}
}

View File

@ -275,22 +275,6 @@ public class ServerController : BaseApiController
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>
@ -301,5 +285,4 @@ public class ServerController : BaseApiController
await _taskScheduler.CheckForUpdate();
return Ok();
}
}

View File

@ -6,6 +6,7 @@ using System.Threading.Tasks;
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;
@ -119,28 +120,7 @@ public class SettingsController : BaseApiController
}
/// <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());
}
/// <summary>
/// Sends a test email from the Email Service. Will not send if email service is the Default Provider
/// Sends a test email from the Email Service.
/// </summary>
/// <param name="dto"></param>
/// <returns></returns>
@ -149,8 +129,19 @@ public class SettingsController : BaseApiController
public async Task<ActionResult<EmailTestResultDto>> TestEmailServiceUrl(TestEmailDto dto)
{
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)));
return Ok(await _emailService.SendTestEmail(user!.Email));
}
/// <summary>
/// Is the minimum information setup for Email to work
/// </summary>
/// <returns></returns>
[Authorize]
[HttpGet("is-email-setup")]
public async Task<ActionResult<bool>> IsEmailSetup()
{
var settings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync();
return Ok(settings.IsEmailSetup());
}
@ -233,6 +224,10 @@ public class SettingsController : BaseApiController
_unitOfWork.SettingsRepository.Update(setting);
}
UpdateEmailSettings(setting, updateSettingsDto);
if (setting.Key == ServerSettingKey.IpAddresses && updateSettingsDto.IpAddresses != setting.Value)
{
if (OsInfo.IsDocker) continue;
@ -289,17 +284,6 @@ public class SettingsController : BaseApiController
_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
@ -392,6 +376,63 @@ public class SettingsController : BaseApiController
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()

View File

@ -7,4 +7,5 @@ public class EmailTestResultDto
{
public bool Successful { get; set; }
public string ErrorMessage { get; set; } = default!;
public string EmailAddress { get; set; } = default!;
}

View File

@ -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;
}

View File

@ -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.
@ -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);
}
}

View File

@ -0,0 +1,59 @@
using System;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using API.Services;
using Flurl.Http;
using Microsoft.Extensions.Logging;
namespace API.Data.ManualMigrations;
public static class MigrateEmailTemplates
{
private const string EmailChange = "https://raw.githubusercontent.com/Kareadita/KavitaEmail/main/KavitaEmail/config/templates/EmailChange.html";
private const string EmailConfirm = "https://raw.githubusercontent.com/Kareadita/KavitaEmail/main/KavitaEmail/config/templates/EmailConfirm.html";
private const string EmailPasswordReset = "https://raw.githubusercontent.com/Kareadita/KavitaEmail/main/KavitaEmail/config/templates/EmailPasswordReset.html";
private const string SendToDevice = "https://raw.githubusercontent.com/Kareadita/KavitaEmail/main/KavitaEmail/config/templates/SendToDevice.html";
private const string EmailTest = "https://raw.githubusercontent.com/Kareadita/KavitaEmail/main/KavitaEmail/config/templates/EmailTest.html";
public static async Task Migrate(IDirectoryService directoryService, ILogger<Program> logger)
{
var files = directoryService.GetFiles(directoryService.CustomizedTemplateDirectory);
if (files.Any())
{
logger.LogCritical("Running MigrateEmailTemplates migration - Completed. This is not an error");
return;
}
// Write files to directory
await DownloadAndWriteToFile(EmailChange, Path.Join(directoryService.CustomizedTemplateDirectory, "EmailChange.html"), logger);
await DownloadAndWriteToFile(EmailConfirm, Path.Join(directoryService.CustomizedTemplateDirectory, "EmailConfirm.html"), logger);
await DownloadAndWriteToFile(EmailPasswordReset, Path.Join(directoryService.CustomizedTemplateDirectory, "EmailPasswordReset.html"), logger);
await DownloadAndWriteToFile(SendToDevice, Path.Join(directoryService.CustomizedTemplateDirectory, "SendToDevice.html"), logger);
await DownloadAndWriteToFile(EmailTest, Path.Join(directoryService.CustomizedTemplateDirectory, "EmailTest.html"), logger);
logger.LogCritical("Running MigrateEmailTemplates migration - Please be patient, this may take some time. This is not an error");
}
private static async Task DownloadAndWriteToFile(string url, string filePath, ILogger<Program> logger)
{
try
{
// Download the raw text using Flurl
var content = await url.GetStringAsync();
// Write the content to a file
await File.WriteAllTextAsync(filePath, content);
logger.LogInformation("{File} downloaded and written successfully", filePath);
}
catch (FlurlHttpException ex)
{
logger.LogError(ex, "Unable to download {Url} to {FilePath}. Please perform yourself!", url, filePath);
}
}
}

View File

@ -223,12 +223,10 @@ public static class Seed
}, // Not used from DB, but DB is sync with appSettings.json
new() {Key = ServerSettingKey.AllowStatCollection, Value = "true"},
new() {Key = ServerSettingKey.EnableOpds, Value = "true"},
new() {Key = ServerSettingKey.EnableAuthentication, Value = "true"},
new() {Key = ServerSettingKey.BaseUrl, Value = "/"},
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},
new() {Key = ServerSettingKey.TotalBackups, Value = "30"},
new() {Key = ServerSettingKey.TotalLogs, Value = "30"},
new() {Key = ServerSettingKey.EnableFolderWatching, Value = "false"},
@ -241,6 +239,16 @@ public static class Seed
new() {
Key = ServerSettingKey.CacheSize, Value = Configuration.DefaultCacheMemory + string.Empty
}, // Not used from DB, but DB is sync with appSettings.json
new() {Key = ServerSettingKey.EmailHost, Value = string.Empty},
new() {Key = ServerSettingKey.EmailPort, Value = string.Empty},
new() {Key = ServerSettingKey.EmailAuthPassword, Value = string.Empty},
new() {Key = ServerSettingKey.EmailAuthUserName, Value = string.Empty},
new() {Key = ServerSettingKey.EmailSenderAddress, Value = string.Empty},
new() {Key = ServerSettingKey.EmailSenderDisplayName, Value = string.Empty},
new() {Key = ServerSettingKey.EmailEnableSsl, Value = "true"},
new() {Key = ServerSettingKey.EmailSizeLimit, Value = 26_214_400 + string.Empty},
new() {Key = ServerSettingKey.EmailCustomizedTemplates, Value = "false"},
}.ToArray());
foreach (var defaultSetting in DefaultSettings)

View File

@ -0,0 +1,348 @@
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
<head>
<meta charset="utf-8">
<!-- utf-8 works for most cases -->
<meta name="viewport" content="width=device-width">
<!-- Forcing initial-scale shouldn't be necessary -->
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<!-- Use the latest (edge) version of IE rendering engine -->
<meta name="x-apple-disable-message-reformatting">
<!-- Disable auto-scale in iOS 10 Mail entirely -->
<title>Kavita - [Plain HTML]</title>
<!-- The title tag shows in email notifications, like Android 4.4. -->
<!-- Web Font / @font-face : BEGIN -->
<!-- NOTE: If web fonts are not required, lines 10 - 27 can be safely removed. -->
<!-- Desktop Outlook chokes on web font references and defaults to Times New Roman, so we force a safe fallback font. -->
<!--[if mso]>
<style>
* {
font-family: Arial, sans-serif !important;
}
</style>
<![endif]-->
<!-- All other clients get the webfont reference; some will render the font and others will silently fail to the fallbacks. More on that here: http://stylecampaign.com/blog/2015/02/webfont-support-in-email/ -->
<!--[if !mso]><!-->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Spartan:wght@500;700&display=swap" rel="stylesheet">
<link rel="stylesheet" type="text/css" href="https://use.fontawesome.com/releases/v5.6.3/css/all.css" integrity="sha384-UHRtZLI+pbxtHCWp1t77Bi1L4ZtiqrqD80Kn4Z8NTSRyMA2Fd33n5dQ8lWUE00s/" crossorigin="anonymous">
<!--<![endif]-->
<!-- Web Font / @font-face : END -->
<!-- CSS Reset -->
<style>
@import url('https://fonts.googleapis.com/css2?family=Spartan:wght@500;700&display=swap');
/* What it does: Remove spaces around the email design added by some email clients. */
/* Beware: It can remove the padding / margin and add a background color to the compose a reply window. */
html,
body {
margin: 0 auto !important;
padding: 0 !important;
height: 100% !important;
width: 100% !important;
}
/* What it does: Stops email clients resizing small text. */
* {
-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%;
}
/* What it does: Centers email on Android 4.4 */
div[style*="margin: 16px 0"] {
margin: 0 !important;
}
/* What it does: Stops Outlook from adding extra spacing to tables. */
table,
td {
mso-table-lspace: 0pt !important;
mso-table-rspace: 0pt !important;
}
/* What it does: Fixes webkit padding issue. Fix for Yahoo mail table alignment bug. Applies table-layout to the first 2 tables then removes for anything nested deeper. */
table {
border-spacing: 0 !important;
border-collapse: collapse !important;
table-layout: fixed !important;
margin: 0 auto !important;
}
table table table {
table-layout: auto;
}
i {
color: #fff;
font-size: 26px;
}
/* What it does: Uses a better rendering method when resizing images in IE. */
img {
-ms-interpolation-mode: bicubic;
}
/* What it does: A work-around for email clients meddling in triggered links. */
*[x-apple-data-detectors],
/* iOS */
.x-gmail-data-detectors,
/* Gmail */
.x-gmail-data-detectors *,
.aBn {
border-bottom: 0 !important;
cursor: default !important;
color: inherit !important;
text-decoration: none !important;
font-size: inherit !important;
font-family: inherit !important;
font-weight: inherit !important;
line-height: inherit !important;
}
/* What it does: Prevents Gmail from displaying an download button on large, non-linked images. */
.a6S {
display: none !important;
opacity: 0.01 !important;
}
/* If the above doesn't work, add a .g-img class to any image in question. */
img.g-img + div {
display: none !important;
}
/* What it does: Prevents underlining the button text in Windows 10 */
.button-link {
text-decoration: none !important;
}
/* What it does: Removes right gutter in Gmail iOS app: https://github.com/TedGoas/Cerberus/issues/89 */
/* Create one of these media queries for each additional viewport size you'd like to fix */
/* Thanks to Eric Lepetit @ericlepetitsf) for help troubleshooting */
@media only screen and (min-device-width: 375px) and (max-device-width: 413px) {
/* iPhone 6 and 6+ */
.email-container {
min-width: 375px !important;
}
}
</style>
<!-- Progressive Enhancements -->
<style>
/* What it does: Hover styles for buttons */
.button-td,
.button-a {
transition: all 100ms ease-in;
}
.button-td:hover,
.button-a:hover {
background: #000000 !important;
border-color: #000000 !important;
color: white !important;
}
/* Media Queries */
@media screen and (max-width: 480px) {
/* What it does: Forces elements to resize to the full width of their container. Useful for resizing images beyond their max-width. */
.fluid {
width: 100% !important;
max-width: 100% !important;
height: auto !important;
margin-left: auto !important;
margin-right: auto !important;
}
/* What it does: Forces table cells into full-width rows. */
.stack-column,
.stack-column-center {
display: block !important;
width: 100% !important;
max-width: 100% !important;
direction: ltr !important;
}
/* And center justify these ones. */
.stack-column-center {
text-align: center !important;
}
/* What it does: Generic utility class for centering. Useful for images, buttons, and nested tables. */
.center-on-narrow {
text-align: center !important;
display: block !important;
margin-left: auto !important;
margin-right: auto !important;
float: none !important;
}
table.center-on-narrow {
display: inline-block !important;
}
/* What it does: Adjust typography on small screens to improve readability */
.email-container p {
font-size: 17px !important;
line-height: 22px !important;
}
}
</style>
<!-- What it does: Makes background images in 72ppi Outlook render at correct size. -->
<!--[if gte mso 9]>
<xml>
<o:OfficeDocumentSettings>
<o:AllowPNG/>
<o:PixelsPerInch>96</o:PixelsPerInch>
</o:OfficeDocumentSettings>
</xml>
<![endif]-->
</head>
<body width="100%" bgcolor="#F1F1F1" style="margin: 0; mso-line-height-rule: exactly;">
<center style="width: 100%; background: #F1F1F1; text-align: left;">
<!-- Visually Hidden Preheader Text : BEGIN -->
<div style="display:none;font-size:1px;line-height:1px;max-height:0px;max-width:0px;opacity:0;overflow:hidden;mso-hide:all;font-family: sans-serif;"> Your account's email has been updated on {{InvitingUser}}'s Kavita instance. Click the button to validate your email.</div>
<!-- Visually Hidden Preheader Text : END -->
<!--
Set the email width. Defined in two places:
1. max-width for all clients except Desktop Windows Outlook, allowing the email to squish on narrow but never go wider than 680px.
2. MSO tags for Desktop Windows Outlook enforce a 680px width.
Note: The Fluid and Responsive templates have a different width (600px). The hybrid grid is more "fragile", and I've found that 680px is a good width. Change with caution.
-->
<div style="max-width: 680px; margin: auto;" class="email-container">
<!--[if mso]>
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="680" align="center">
<tr>
<td>
<![endif]-->
<!-- Email Body : BEGIN -->
<table role="presentation" cellspacing="0" cellpadding="0" border="0" align="center" width="100%" style="max-width: 680px;" class="email-container">
<!-- HEADER : BEGIN -->
<tr>
<td bgcolor="#4AC694">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr>
<td style="margin-left: -10px;padding: 10px 40px 10px 40px; text-align: center;"> <img src="https://www.kavitareader.com/img/email/logo-256.png" alt="kavita_logo" width="75" style="height:auto;display:inline-block;vertical-align:middle;" />
<div style="min-height:75px;line-height:75px;display:inline-block;vertical-align:middle;color:#fff;font-family: 'Spartan', sans-serif;font-size:2rem;font-weight:700;">Kavita</div>
</td>
</tr>
</table>
</td>
</tr>
<!-- HEADER : END -->
<!-- HERO : BEGIN -->
<tr>
<td bgcolor="#fff" align="center" valign="top" style="text-align: center; background-position: center center !important; background-size: cover !important;">
<!--[if gte mso 9]>
<v:rect xmlns:v="urn:schemas-microsoft-com:vml" fill="true" stroke="false" style="width:680px; height:380px; background-position: center center !important;">
<v:fill type="tile" src="background.png" color="#222222" />
<v:textbox inset="0,0,0,0">
<![endif]-->
<div>
<!--[if mso]>
<table role="presentation" border="0" cellspacing="0" cellpadding="0" align="center" width="500">
<tr>
<td align="center" valign="middle" width="500">
<![endif]-->
<table role="presentation" border="0" cellpadding="0" cellspacing="0" align="center" width="100%" style="max-width:500px; margin: auto;">
<tr>
<td height="20" style="font-size:20px; line-height:20px;">&nbsp;</td>
</tr>
<tr>
<td align="center" valign="middle">
<table>
<tr>
<td valign="top" style="text-align: center; padding: 20px 0 10px 20px;">
<h1 style="margin: 0; font-family: 'Montserrat', sans-serif; font-size: 30px; line-height: 36px; font-weight: bold;">Email Change Update</h1> </td>
</tr>
<tr>
<td valign="top" style="text-align: center; padding: 10px 20px 15px 20px; font-family: sans-serif; font-size: 18px; line-height: 20px;">
<p style="margin: 0;">Your account's email has been updated on {{InvitingUser}}'s Kavita instance.</p>
</td>
</tr>
<tr>
<td valign="top" style="text-align: center; padding: 10px 20px 15px 20px; font-family: sans-serif; font-size: 18px; line-height: 20px;">
<p style="margin: 0;">Please click the following link to validate your email change. The email is not changed until you complete validation.</p>
</td>
</tr>
<tr>
<td valign="top" align="center" style="text-align: center; padding: 15px 0px 20px 0px;">
<!-- Button : BEGIN -->
<center>
<table role="presentation" align="center" cellspacing="0" cellpadding="0" border="0" class="center-on-narrow" style="text-align: center;">
<tr>
<td style="border-radius: 50px; background: #153643; text-align: center;" class="button-td">
<a href="{{Link}}" style="background: #153643; border: 15px solid #153643; font-family: 'Montserrat', sans-serif; font-size: 14px; line-height: 1.1; text-align: center; text-decoration: none; display: block; border-radius: 50px; font-weight: bold;" class="button-a"> <span style="color:#ffffff;" class="button-link">&nbsp;&nbsp;&nbsp;&nbsp;CONFIRM EMAIL&nbsp;&nbsp;&nbsp;&nbsp;</span> </a>
</td>
</tr>
</table>
</center>
<!-- Button : END -->
<tr>
<td valign="top" style="text-align: center; padding: 10px 20px 15px 20px; font-family: sans-serif; font-size: 12px; line-height: 20px;">
<p style="margin: 0;">If the button above does not work, please find the link here: <a style="color:inherit;margin: 0;width: 100%;word-break: break-all;" href="{{Link}}">{{Link}}</a></p>
</td>
</tr>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td height="20" style="font-size:20px; line-height:20px;">&nbsp;</td>
</tr>
</table>
<!--[if mso]>
</td>
</tr>
</table>
<![endif]-->
</div>
<!--[if gte mso 9]>
</v:textbox>
</v:rect>
<![endif]-->
</td>
</tr>
<!-- HERO : END -->
<!-- SOCIAL : BEGIN -->
<tr>
<td bgcolor="#292828">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr>
<td style="padding: 15px 30px; text-align: center;">
<table align="center" style="text-align: center;">
<tr>
<td>
<a href="https://discord.gg/b52wT37kt7"><img style="width:25px" src="https://www.kavitareader.com/img/email/discord-white.png" width="" height="" style="margin:0; padding:0; border:none; display:block;" border="0" alt="Discord"></a>
</td>
<td width="20">&nbsp;</td>
<td>
<a href="https://www.reddit.com/r/KavitaManga/"><img style="width:25px" src="https://www.kavitareader.com/img/email/reddit-white.png" width="" height="" style="margin:0; padding:0; border:none; display:block;" border="0" alt="Reddit"></a>
</td>
<td width="20">&nbsp;</td>
<td>
<a href="https://github.com/Kareadita/Kavita/"><img style="width:25px" src="https://www.kavitareader.com/img/email/github-white.png" width="" height="" style="margin:0; padding:0; border:none; display:block;" border="0" alt="Github"></a>
</td>
<td width="20">&nbsp;</td>
<td>
<a href="https://opencollective.com/kavita"><img style="width:25px" src="https://www.kavitareader.com/img/email/open-collective-white.png" width="" height="" style="margin:0; padding:0; border:none; display:block;" border="0" alt="Open Collective"></a>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
<!-- SOCIAL : END -->
</table>
<!-- Email Body : END -->
<!--[if mso]>
</td>
</tr>
</table>
<![endif]-->
</div>
</center>
</body>
</html>

View File

@ -0,0 +1,348 @@
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
<head>
<meta charset="utf-8">
<!-- utf-8 works for most cases -->
<meta name="viewport" content="width=device-width">
<!-- Forcing initial-scale shouldn't be necessary -->
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<!-- Use the latest (edge) version of IE rendering engine -->
<meta name="x-apple-disable-message-reformatting">
<!-- Disable auto-scale in iOS 10 Mail entirely -->
<title>Kavita - [Plain HTML]</title>
<!-- The title tag shows in email notifications, like Android 4.4. -->
<!-- Web Font / @font-face : BEGIN -->
<!-- NOTE: If web fonts are not required, lines 10 - 27 can be safely removed. -->
<!-- Desktop Outlook chokes on web font references and defaults to Times New Roman, so we force a safe fallback font. -->
<!--[if mso]>
<style>
* {
font-family: Arial, sans-serif !important;
}
</style>
<![endif]-->
<!-- All other clients get the webfont reference; some will render the font and others will silently fail to the fallbacks. More on that here: http://stylecampaign.com/blog/2015/02/webfont-support-in-email/ -->
<!--[if !mso]><!-->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Spartan:wght@500;700&display=swap" rel="stylesheet">
<link rel="stylesheet" type="text/css" href="https://use.fontawesome.com/releases/v5.6.3/css/all.css" integrity="sha384-UHRtZLI+pbxtHCWp1t77Bi1L4ZtiqrqD80Kn4Z8NTSRyMA2Fd33n5dQ8lWUE00s/" crossorigin="anonymous">
<!--<![endif]-->
<!-- Web Font / @font-face : END -->
<!-- CSS Reset -->
<style>
@import url('https://fonts.googleapis.com/css2?family=Spartan:wght@500;700&display=swap');
/* What it does: Remove spaces around the email design added by some email clients. */
/* Beware: It can remove the padding / margin and add a background color to the compose a reply window. */
html,
body {
margin: 0 auto !important;
padding: 0 !important;
height: 100% !important;
width: 100% !important;
}
/* What it does: Stops email clients resizing small text. */
* {
-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%;
}
/* What it does: Centers email on Android 4.4 */
div[style*="margin: 16px 0"] {
margin: 0 !important;
}
/* What it does: Stops Outlook from adding extra spacing to tables. */
table,
td {
mso-table-lspace: 0pt !important;
mso-table-rspace: 0pt !important;
}
/* What it does: Fixes webkit padding issue. Fix for Yahoo mail table alignment bug. Applies table-layout to the first 2 tables then removes for anything nested deeper. */
table {
border-spacing: 0 !important;
border-collapse: collapse !important;
table-layout: fixed !important;
margin: 0 auto !important;
}
table table table {
table-layout: auto;
}
i {
color: #fff;
font-size: 26px;
}
/* What it does: Uses a better rendering method when resizing images in IE. */
img {
-ms-interpolation-mode: bicubic;
}
/* What it does: A work-around for email clients meddling in triggered links. */
*[x-apple-data-detectors],
/* iOS */
.x-gmail-data-detectors,
/* Gmail */
.x-gmail-data-detectors *,
.aBn {
border-bottom: 0 !important;
cursor: default !important;
color: inherit !important;
text-decoration: none !important;
font-size: inherit !important;
font-family: inherit !important;
font-weight: inherit !important;
line-height: inherit !important;
}
/* What it does: Prevents Gmail from displaying an download button on large, non-linked images. */
.a6S {
display: none !important;
opacity: 0.01 !important;
}
/* If the above doesn't work, add a .g-img class to any image in question. */
img.g-img + div {
display: none !important;
}
/* What it does: Prevents underlining the button text in Windows 10 */
.button-link {
text-decoration: none !important;
}
/* What it does: Removes right gutter in Gmail iOS app: https://github.com/TedGoas/Cerberus/issues/89 */
/* Create one of these media queries for each additional viewport size you'd like to fix */
/* Thanks to Eric Lepetit @ericlepetitsf) for help troubleshooting */
@media only screen and (min-device-width: 375px) and (max-device-width: 413px) {
/* iPhone 6 and 6+ */
.email-container {
min-width: 375px !important;
}
}
</style>
<!-- Progressive Enhancements -->
<style>
/* What it does: Hover styles for buttons */
.button-td,
.button-a {
transition: all 100ms ease-in;
}
.button-td:hover,
.button-a:hover {
background: #000000 !important;
border-color: #000000 !important;
color: white !important;
}
/* Media Queries */
@media screen and (max-width: 480px) {
/* What it does: Forces elements to resize to the full width of their container. Useful for resizing images beyond their max-width. */
.fluid {
width: 100% !important;
max-width: 100% !important;
height: auto !important;
margin-left: auto !important;
margin-right: auto !important;
}
/* What it does: Forces table cells into full-width rows. */
.stack-column,
.stack-column-center {
display: block !important;
width: 100% !important;
max-width: 100% !important;
direction: ltr !important;
}
/* And center justify these ones. */
.stack-column-center {
text-align: center !important;
}
/* What it does: Generic utility class for centering. Useful for images, buttons, and nested tables. */
.center-on-narrow {
text-align: center !important;
display: block !important;
margin-left: auto !important;
margin-right: auto !important;
float: none !important;
}
table.center-on-narrow {
display: inline-block !important;
}
/* What it does: Adjust typography on small screens to improve readability */
.email-container p {
font-size: 17px !important;
line-height: 22px !important;
}
}
</style>
<!-- What it does: Makes background images in 72ppi Outlook render at correct size. -->
<!--[if gte mso 9]>
<xml>
<o:OfficeDocumentSettings>
<o:AllowPNG/>
<o:PixelsPerInch>96</o:PixelsPerInch>
</o:OfficeDocumentSettings>
</xml>
<![endif]-->
</head>
<body width="100%" bgcolor="#F1F1F1" style="margin: 0; mso-line-height-rule: exactly;">
<center style="width: 100%; background: #F1F1F1; text-align: left;">
<!-- Visually Hidden Preheader Text : BEGIN -->
<div style="display:none;font-size:1px;line-height:1px;max-height:0px;max-width:0px;opacity:0;overflow:hidden;mso-hide:all;font-family: sans-serif;"> You have been invited to {{InvitingUser}}'s Kavita instance. Click the button to accept the invite. </div>
<!-- Visually Hidden Preheader Text : END -->
<!--
Set the email width. Defined in two places:
1. max-width for all clients except Desktop Windows Outlook, allowing the email to squish on narrow but never go wider than 680px.
2. MSO tags for Desktop Windows Outlook enforce a 680px width.
Note: The Fluid and Responsive templates have a different width (600px). The hybrid grid is more "fragile", and I've found that 680px is a good width. Change with caution.
-->
<div style="max-width: 680px; margin: auto;" class="email-container">
<!--[if mso]>
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="680" align="center">
<tr>
<td>
<![endif]-->
<!-- Email Body : BEGIN -->
<table role="presentation" cellspacing="0" cellpadding="0" border="0" align="center" width="100%" style="max-width: 680px;" class="email-container">
<!-- HEADER : BEGIN -->
<tr>
<td bgcolor="#4AC694">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr>
<td style="margin-left: -10px;padding: 10px 40px 10px 40px; text-align: center;"> <img src="https://www.kavitareader.com/img/email/logo-256.png" alt="kavita_logo" width="75" style="height:auto;display:inline-block;vertical-align:middle;" />
<div style="min-height:75px;line-height:75px;display:inline-block;vertical-align:middle;color:#fff;font-family: 'Spartan', sans-serif;font-size:2rem;font-weight:700;">Kavita</div>
</td>
</tr>
</table>
</td>
</tr>
<!-- HEADER : END -->
<!-- HERO : BEGIN -->
<tr>
<td bgcolor="#fff" align="center" valign="top" style="text-align: center; background-position: center center !important; background-size: cover !important;">
<!--[if gte mso 9]>
<v:rect xmlns:v="urn:schemas-microsoft-com:vml" fill="true" stroke="false" style="width:680px; height:380px; background-position: center center !important;">
<v:fill type="tile" src="background.png" color="#222222" />
<v:textbox inset="0,0,0,0">
<![endif]-->
<div>
<!--[if mso]>
<table role="presentation" border="0" cellspacing="0" cellpadding="0" align="center" width="500">
<tr>
<td align="center" valign="middle" width="500">
<![endif]-->
<table role="presentation" border="0" cellpadding="0" cellspacing="0" align="center" width="100%" style="max-width:500px; margin: auto;">
<tr>
<td height="20" style="font-size:20px; line-height:20px;">&nbsp;</td>
</tr>
<tr>
<td align="center" valign="middle">
<table>
<tr>
<td valign="top" style="text-align: center; padding: 20px 0 10px 20px;">
<h1 style="margin: 0; font-family: 'Montserrat', sans-serif; font-size: 30px; line-height: 36px; font-weight: bold;">You've Been Invited</h1> </td>
</tr>
<tr>
<td valign="top" style="text-align: center; padding: 10px 20px 15px 20px; font-family: sans-serif; font-size: 18px; line-height: 20px;">
<p style="margin: 0;">You have been invited to {{InvitingUser}}'s Kavita instance.</p>
</td>
</tr>
<tr>
<td valign="top" style="text-align: center; padding: 10px 20px 15px 20px; font-family: sans-serif; font-size: 18px; line-height: 20px;">
<p style="margin: 0;">Please click the following link to setup an account for yourself and start reading.</p>
</td>
</tr>
<tr>
<td valign="top" align="center" style="text-align: center; padding: 15px 0px 20px 0px;">
<!-- Button : BEGIN -->
<center>
<table role="presentation" align="center" cellspacing="0" cellpadding="0" border="0" class="center-on-narrow" style="text-align: center;">
<tr>
<td style="border-radius: 50px; background: #153643; text-align: center;" class="button-td">
<a href="{{Link}}" style="background: #153643; border: 15px solid #153643; font-family: 'Montserrat', sans-serif; font-size: 14px; line-height: 1.1; text-align: center; text-decoration: none; display: block; border-radius: 50px; font-weight: bold;" class="button-a"> <span style="color:#ffffff;" class="button-link">&nbsp;&nbsp;&nbsp;&nbsp;ACCEPT INVITE&nbsp;&nbsp;&nbsp;&nbsp;</span> </a>
</td>
</tr>
</table>
</center>
<!-- Button : END -->
<tr>
<td valign="top" style="text-align: center; padding: 10px 20px 15px 20px; font-family: sans-serif; font-size: 12px; line-height: 20px;">
<p style="margin: 0;">If the button above does not work, please find the link here: <a style="color:inherit;margin: 0;width: 100%;word-break: break-all;" href="{{Link}}">{{Link}}</a></p>
</td>
</tr>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td height="20" style="font-size:20px; line-height:20px;">&nbsp;</td>
</tr>
</table>
<!--[if mso]>
</td>
</tr>
</table>
<![endif]-->
</div>
<!--[if gte mso 9]>
</v:textbox>
</v:rect>
<![endif]-->
</td>
</tr>
<!-- HERO : END -->
<!-- SOCIAL : BEGIN -->
<tr>
<td bgcolor="#292828">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr>
<td style="padding: 15px 30px; text-align: center;">
<table align="center" style="text-align: center;">
<tr>
<td>
<a href="https://discord.gg/b52wT37kt7"><img style="width:25px" src="https://www.kavitareader.com/img/email/discord-white.png" width="" height="" style="margin:0; padding:0; border:none; display:block;" border="0" alt="Discord"></a>
</td>
<td width="20">&nbsp;</td>
<td>
<a href="https://www.reddit.com/r/KavitaManga/"><img style="width:25px" src="https://www.kavitareader.com/img/email/reddit-white.png" width="" height="" style="margin:0; padding:0; border:none; display:block;" border="0" alt="Reddit"></a>
</td>
<td width="20">&nbsp;</td>
<td>
<a href="https://github.com/Kareadita/Kavita/"><img style="width:25px" src="https://www.kavitareader.com/img/email/github-white.png" width="" height="" style="margin:0; padding:0; border:none; display:block;" border="0" alt="Github"></a>
</td>
<td width="20">&nbsp;</td>
<td>
<a href="https://opencollective.com/kavita"><img style="width:25px" src="https://www.kavitareader.com/img/email/open-collective-white.png" width="" height="" style="margin:0; padding:0; border:none; display:block;" border="0" alt="Open Collective"></a>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
<!-- SOCIAL : END -->
</table>
<!-- Email Body : END -->
<!--[if mso]>
</td>
</tr>
</table>
<![endif]-->
</div>
</center>
</body>
</html>

View File

@ -0,0 +1,348 @@
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
<head>
<meta charset="utf-8">
<!-- utf-8 works for most cases -->
<meta name="viewport" content="width=device-width">
<!-- Forcing initial-scale shouldn't be necessary -->
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<!-- Use the latest (edge) version of IE rendering engine -->
<meta name="x-apple-disable-message-reformatting">
<!-- Disable auto-scale in iOS 10 Mail entirely -->
<title>Kavita - [Plain HTML]</title>
<!-- The title tag shows in email notifications, like Android 4.4. -->
<!-- Web Font / @font-face : BEGIN -->
<!-- NOTE: If web fonts are not required, lines 10 - 27 can be safely removed. -->
<!-- Desktop Outlook chokes on web font references and defaults to Times New Roman, so we force a safe fallback font. -->
<!--[if mso]>
<style>
* {
font-family: Arial, sans-serif !important;
}
</style>
<![endif]-->
<!-- All other clients get the webfont reference; some will render the font and others will silently fail to the fallbacks. More on that here: http://stylecampaign.com/blog/2015/02/webfont-support-in-email/ -->
<!--[if !mso]><!-->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Spartan:wght@500;700&display=swap" rel="stylesheet">
<link rel="stylesheet" type="text/css" href="https://use.fontawesome.com/releases/v5.6.3/css/all.css" integrity="sha384-UHRtZLI+pbxtHCWp1t77Bi1L4ZtiqrqD80Kn4Z8NTSRyMA2Fd33n5dQ8lWUE00s/" crossorigin="anonymous">
<!--<![endif]-->
<!-- Web Font / @font-face : END -->
<!-- CSS Reset -->
<style>
@import url(https://fonts.googleapis.com/css2?family=Spartan:wght@500;700&display=swap);
/* What it does: Remove spaces around the email design added by some email clients. */
/* Beware: It can remove the padding / margin and add a background color to the compose a reply window. */
html,
body {
margin: 0 auto !important;
padding: 0 !important;
height: 100% !important;
width: 100% !important;
}
/* What it does: Stops email clients resizing small text. */
* {
-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%;
}
/* What it does: Centers email on Android 4.4 */
div[style*="margin: 16px 0"] {
margin: 0 !important;
}
/* What it does: Stops Outlook from adding extra spacing to tables. */
table,
td {
mso-table-lspace: 0pt !important;
mso-table-rspace: 0pt !important;
}
/* What it does: Fixes webkit padding issue. Fix for Yahoo mail table alignment bug. Applies table-layout to the first 2 tables then removes for anything nested deeper. */
table {
border-spacing: 0 !important;
border-collapse: collapse !important;
table-layout: fixed !important;
margin: 0 auto !important;
}
table table table {
table-layout: auto;
}
i {
color: #fff;
font-size: 26px;
}
/* What it does: Uses a better rendering method when resizing images in IE. */
img {
-ms-interpolation-mode: bicubic;
}
/* What it does: A work-around for email clients meddling in triggered links. */
*[x-apple-data-detectors],
/* iOS */
.x-gmail-data-detectors,
/* Gmail */
.x-gmail-data-detectors *,
.aBn {
border-bottom: 0 !important;
cursor: default !important;
color: inherit !important;
text-decoration: none !important;
font-size: inherit !important;
font-family: inherit !important;
font-weight: inherit !important;
line-height: inherit !important;
}
/* What it does: Prevents Gmail from displaying an download button on large, non-linked images. */
.a6S {
display: none !important;
opacity: 0.01 !important;
}
/* If the above doesn't work, add a .g-img class to any image in question. */
img.g-img + div {
display: none !important;
}
/* What it does: Prevents underlining the button text in Windows 10 */
.button-link {
text-decoration: none !important;
}
/* What it does: Removes right gutter in Gmail iOS app: https://github.com/TedGoas/Cerberus/issues/89 */
/* Create one of these media queries for each additional viewport size you'd like to fix */
/* Thanks to Eric Lepetit @ericlepetitsf) for help troubleshooting */
@media only screen and (min-device-width: 375px) and (max-device-width: 413px) {
/* iPhone 6 and 6+ */
.email-container {
min-width: 375px !important;
}
}
</style>
<!-- Progressive Enhancements -->
<style>
/* What it does: Hover styles for buttons */
.button-td,
.button-a {
transition: all 100ms ease-in;
}
.button-td:hover,
.button-a:hover {
background: #000000 !important;
border-color: #000000 !important;
color: white !important;
}
/* Media Queries */
@media screen and (max-width: 480px) {
/* What it does: Forces elements to resize to the full width of their container. Useful for resizing images beyond their max-width. */
.fluid {
width: 100% !important;
max-width: 100% !important;
height: auto !important;
margin-left: auto !important;
margin-right: auto !important;
}
/* What it does: Forces table cells into full-width rows. */
.stack-column,
.stack-column-center {
display: block !important;
width: 100% !important;
max-width: 100% !important;
direction: ltr !important;
}
/* And center justify these ones. */
.stack-column-center {
text-align: center !important;
}
/* What it does: Generic utility class for centering. Useful for images, buttons, and nested tables. */
.center-on-narrow {
text-align: center !important;
display: block !important;
margin-left: auto !important;
margin-right: auto !important;
float: none !important;
}
table.center-on-narrow {
display: inline-block !important;
}
/* What it does: Adjust typography on small screens to improve readability */
.email-container p {
font-size: 17px !important;
line-height: 22px !important;
}
}
</style>
<!-- What it does: Makes background images in 72ppi Outlook render at correct size. -->
<!--[if gte mso 9]>
<xml>
<o:OfficeDocumentSettings>
<o:AllowPNG/>
<o:PixelsPerInch>96</o:PixelsPerInch>
</o:OfficeDocumentSettings>
</xml>
<![endif]-->
</head>
<body width="100%" bgcolor="#F1F1F1" style="margin: 0; mso-line-height-rule: exactly;">
<center style="width: 100%; background: #F1F1F1; text-align: left;">
<!-- Visually Hidden Preheader Text : BEGIN -->
<div style="display:none;font-size:1px;line-height:1px;max-height:0px;max-width:0px;opacity:0;overflow:hidden;mso-hide:all;font-family: sans-serif;"> Email confirmation is required for continued access. Click the button to confirm your email. </div>
<!-- Visually Hidden Preheader Text : END -->
<!--
Set the email width. Defined in two places:
1. max-width for all clients except Desktop Windows Outlook, allowing the email to squish on narrow but never go wider than 680px.
2. MSO tags for Desktop Windows Outlook enforce a 680px width.
Note: The Fluid and Responsive templates have a different width (600px). The hybrid grid is more "fragile", and I've found that 680px is a good width. Change with caution.
-->
<div style="max-width: 680px; margin: auto;" class="email-container">
<!--[if mso]>
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="680" align="center">
<tr>
<td>
<![endif]-->
<!-- Email Body : BEGIN -->
<table role="presentation" cellspacing="0" cellpadding="0" border="0" align="center" width="100%" style="max-width: 680px;" class="email-container">
<!-- HEADER : BEGIN -->
<tr>
<td bgcolor="#4AC694">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr>
<td style="margin-left: -10px;padding: 10px 40px 10px 40px; text-align: center;"> <img src="https://www.kavitareader.com/img/email/logo-256.png" alt="kavita_logo" width="75" style="height:auto;display:inline-block;vertical-align:middle;" />
<div style="min-height:75px;line-height:75px;display:inline-block;vertical-align:middle;color:#fff;font-family: 'Spartan', sans-serif;font-size:2rem;font-weight:700;">Kavita</div>
</td>
</tr>
</table>
</td>
</tr>
<!-- HEADER : END -->
<!-- HERO : BEGIN -->
<tr>
<td bgcolor="#fff" align="center" valign="top" style="text-align: center; background-position: center center !important; background-size: cover !important;">
<!--[if gte mso 9]>
<v:rect xmlns:v="urn:schemas-microsoft-com:vml" fill="true" stroke="false" style="width:680px; height:380px; background-position: center center !important;">
<v:fill type="tile" src="background.png" color="#222222" />
<v:textbox inset="0,0,0,0">
<![endif]-->
<div>
<!--[if mso]>
<table role="presentation" border="0" cellspacing="0" cellpadding="0" align="center" width="500">
<tr>
<td align="center" valign="middle" width="500">
<![endif]-->
<table role="presentation" border="0" cellpadding="0" cellspacing="0" align="center" width="100%" style="max-width:500px; margin: auto;">
<tr>
<td height="20" style="font-size:20px; line-height:20px;">&nbsp;</td>
</tr>
<tr>
<td align="center" valign="middle">
<table>
<tr>
<td valign="top" style="text-align: center; padding: 20px 0 10px 20px;">
<h1 style="margin: 0; font-family: 'Montserrat', sans-serif; font-size: 30px; line-height: 36px; font-weight: bold;">Forgot your password?</h1> </td>
</tr>
<tr>
<td valign="top" style="text-align: center; padding: 10px 20px 15px 20px; font-family: sans-serif; font-size: 18px; line-height: 20px;">
<p style="margin: 0;">That's okay, it happens! Click on the button below to reset your password.</p>
</td>
</tr>
<tr>
<td valign="top" align="center" style="text-align: center; padding: 15px 0px 20px 0px;">
<!-- Button : BEGIN -->
<center>
<table role="presentation" align="center" cellspacing="0" cellpadding="0" border="0" class="center-on-narrow" style="text-align: center;">
<tr>
<td style="border-radius: 50px; background: #153643; text-align: center;" class="button-td">
<a href="{{Link}}" style="background: #153643; border: 15px solid #153643; font-family: 'Montserrat', sans-serif; font-size: 14px; line-height: 1.1; text-align: center; text-decoration: none; display: block; border-radius: 50px; font-weight: bold;" class="button-a"> <span style="color:#ffffff;" class="button-link">&nbsp;&nbsp;&nbsp;&nbsp;RESET YOUR PASSWORD&nbsp;&nbsp;&nbsp;&nbsp;</span> </a>
</td>
</tr>
</table>
</center>
<!-- Button : END -->
<tr>
<td valign="top" style="text-align: center; padding: 0px 20px 0px 20px; font-family: sans-serif; font-size: 12px; line-height: 20px;">
<p style="margin: 0;">If you did not perform this action, ignore this email.</p>
</td>
</tr>
<tr>
<td valign="top" style="text-align: center; padding: 10px 20px 15px 20px; font-family: sans-serif; font-size: 12px; line-height: 20px;">
<p style="margin: 0;">If the button above does not work, please find the link here: <a style="color:inherit;margin: 0;width: 100%;word-break: break-all;" href="{{Link}}">{{Link}}</a></p>
</td>
</tr>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td height="20" style="font-size:20px; line-height:20px;">&nbsp;</td>
</tr>
</table>
<!--[if mso]>
</td>
</tr>
</table>
<![endif]-->
</div>
<!--[if gte mso 9]>
</v:textbox>
</v:rect>
<![endif]-->
</td>
</tr>
<!-- HERO : END -->
<!-- SOCIAL : BEGIN -->
<tr>
<td bgcolor="#292828">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr>
<td style="padding: 15px 30px; text-align: center;">
<table align="center" style="text-align: center;">
<tr>
<td>
<a href="https://discord.gg/b52wT37kt7"><img style="width:25px" src="https://www.kavitareader.com/img/email/discord-white.png" width="" height="" style="margin:0; padding:0; border:none; display:block;" border="0" alt="Discord"></a>
</td>
<td width="20">&nbsp;</td>
<td>
<a href="https://www.reddit.com/r/KavitaManga/"><img style="width:25px" src="https://www.kavitareader.com/img/email/reddit-white.png" width="" height="" style="margin:0; padding:0; border:none; display:block;" border="0" alt="Reddit"></a>
</td>
<td width="20">&nbsp;</td>
<td>
<a href="https://github.com/Kareadita/Kavita/"><img style="width:25px" src="https://www.kavitareader.com/img/email/github-white.png" width="" height="" style="margin:0; padding:0; border:none; display:block;" border="0" alt="Github"></a>
</td>
<td width="20">&nbsp;</td>
<td>
<a href="https://opencollective.com/kavita"><img style="width:25px" src="https://www.kavitareader.com/img/email/open-collective-white.png" width="" height="" style="margin:0; padding:0; border:none; display:block;" border="0" alt="Open Collective"></a>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
<!-- SOCIAL : END -->
</table>
<!-- Email Body : END -->
<!--[if mso]>
</td>
</tr>
</table>
<![endif]-->
</div>
</center>
</body>
</html>

View File

@ -0,0 +1,325 @@
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
<head>
<meta charset="utf-8">
<!-- utf-8 works for most cases -->
<meta name="viewport" content="width=device-width">
<!-- Forcing initial-scale shouldn't be necessary -->
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<!-- Use the latest (edge) version of IE rendering engine -->
<meta name="x-apple-disable-message-reformatting">
<!-- Disable auto-scale in iOS 10 Mail entirely -->
<title>Event - [Plain HTML]</title>
<!-- The title tag shows in email notifications, like Android 4.4. -->
<!-- Web Font / @font-face : BEGIN -->
<!-- NOTE: If web fonts are not required, lines 10 - 27 can be safely removed. -->
<!-- Desktop Outlook chokes on web font references and defaults to Times New Roman, so we force a safe fallback font. -->
<!--[if mso]>
<style>
* {
font-family: Arial, sans-serif !important;
}
</style>
<![endif]-->
<!-- All other clients get the webfont reference; some will render the font and others will silently fail to the fallbacks. More on that here: http://stylecampaign.com/blog/2015/02/webfont-support-in-email/ -->
<!--[if !mso]><!-->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Spartan:wght@500;700&display=swap" rel="stylesheet">
<link rel="stylesheet" type="text/css" href="https://use.fontawesome.com/releases/v5.6.3/css/all.css" integrity="sha384-UHRtZLI+pbxtHCWp1t77Bi1L4ZtiqrqD80Kn4Z8NTSRyMA2Fd33n5dQ8lWUE00s/" crossorigin="anonymous">
<!--<![endif]-->
<!-- Web Font / @font-face : END -->
<!-- CSS Reset -->
<style>
@import url('https://fonts.googleapis.com/css2?family=Spartan:wght@500;700&display=swap');
/* What it does: Remove spaces around the email design added by some email clients. */
/* Beware: It can remove the padding / margin and add a background color to the compose a reply window. */
html,
body {
margin: 0 auto !important;
padding: 0 !important;
height: 100% !important;
width: 100% !important;
}
/* What it does: Stops email clients resizing small text. */
* {
-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%;
}
/* What it does: Centers email on Android 4.4 */
div[style*="margin: 16px 0"] {
margin: 0 !important;
}
/* What it does: Stops Outlook from adding extra spacing to tables. */
table,
td {
mso-table-lspace: 0pt !important;
mso-table-rspace: 0pt !important;
}
/* What it does: Fixes webkit padding issue. Fix for Yahoo mail table alignment bug. Applies table-layout to the first 2 tables then removes for anything nested deeper. */
table {
border-spacing: 0 !important;
border-collapse: collapse !important;
table-layout: fixed !important;
margin: 0 auto !important;
}
table table table {
table-layout: auto;
}
i {
color: #fff;
font-size: 26px;
}
/* What it does: Uses a better rendering method when resizing images in IE. */
img {
-ms-interpolation-mode: bicubic;
}
/* What it does: A work-around for email clients meddling in triggered links. */
*[x-apple-data-detectors],
/* iOS */
.x-gmail-data-detectors,
/* Gmail */
.x-gmail-data-detectors *,
.aBn {
border-bottom: 0 !important;
cursor: default !important;
color: inherit !important;
text-decoration: none !important;
font-size: inherit !important;
font-family: inherit !important;
font-weight: inherit !important;
line-height: inherit !important;
}
/* What it does: Prevents Gmail from displaying an download button on large, non-linked images. */
.a6S {
display: none !important;
opacity: 0.01 !important;
}
/* If the above doesn't work, add a .g-img class to any image in question. */
img.g-img + div {
display: none !important;
}
/* What it does: Prevents underlining the button text in Windows 10 */
.button-link {
text-decoration: none !important;
}
/* What it does: Removes right gutter in Gmail iOS app: https://github.com/TedGoas/Cerberus/issues/89 */
/* Create one of these media queries for each additional viewport size you'd like to fix */
/* Thanks to Eric Lepetit @ericlepetitsf) for help troubleshooting */
@media only screen and (min-device-width: 375px) and (max-device-width: 413px) {
/* iPhone 6 and 6+ */
.email-container {
min-width: 375px !important;
}
}
</style>
<!-- Progressive Enhancements -->
<style>
/* What it does: Hover styles for buttons */
.button-td,
.button-a {
transition: all 100ms ease-in;
}
.button-td:hover,
.button-a:hover {
background: #000000 !important;
border-color: #000000 !important;
color: white !important;
}
/* Media Queries */
@media screen and (max-width: 480px) {
/* What it does: Forces elements to resize to the full width of their container. Useful for resizing images beyond their max-width. */
.fluid {
width: 100% !important;
max-width: 100% !important;
height: auto !important;
margin-left: auto !important;
margin-right: auto !important;
}
/* What it does: Forces table cells into full-width rows. */
.stack-column,
.stack-column-center {
display: block !important;
width: 100% !important;
max-width: 100% !important;
direction: ltr !important;
}
/* And center justify these ones. */
.stack-column-center {
text-align: center !important;
}
/* What it does: Generic utility class for centering. Useful for images, buttons, and nested tables. */
.center-on-narrow {
text-align: center !important;
display: block !important;
margin-left: auto !important;
margin-right: auto !important;
float: none !important;
}
table.center-on-narrow {
display: inline-block !important;
}
/* What it does: Adjust typography on small screens to improve readability */
.email-container p {
font-size: 17px !important;
line-height: 22px !important;
}
}
</style>
<!-- What it does: Makes background images in 72ppi Outlook render at correct size. -->
<!--[if gte mso 9]>
<xml>
<o:OfficeDocumentSettings>
<o:AllowPNG/>
<o:PixelsPerInch>96</o:PixelsPerInch>
</o:OfficeDocumentSettings>
</xml>
<![endif]-->
</head>
<body width="100%" bgcolor="#F1F1F1" style="margin: 0; mso-line-height-rule: exactly;">
<center style="width: 100%; background: #F1F1F1; text-align: left;">
<!-- Visually Hidden Preheader Text : BEGIN -->
<div style="display:none;font-size:1px;line-height:1px;max-height:0px;max-width:0px;opacity:0;overflow:hidden;mso-hide:all;font-family: sans-serif;">
This is a Test Email
</div>
<!-- Visually Hidden Preheader Text : END -->
<!--
Set the email width. Defined in two places:
1. max-width for all clients except Desktop Windows Outlook, allowing the email to squish on narrow but never go wider than 680px.
2. MSO tags for Desktop Windows Outlook enforce a 680px width.
Note: The Fluid and Responsive templates have a different width (600px). The hybrid grid is more "fragile", and I've found that 680px is a good width. Change with caution.
-->
<div style="max-width: 680px; margin: auto;" class="email-container">
<!--[if mso]>
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="680" align="center">
<tr>
<td>
<![endif]-->
<!-- Email Body : BEGIN -->
<table role="presentation" cellspacing="0" cellpadding="0" border="0" align="center" width="100%" style="max-width: 680px;" class="email-container">
<!-- HEADER : BEGIN -->
<tr>
<td bgcolor="#4AC694">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr>
<td style="margin-left: -10px;padding: 10px 40px 10px 40px; text-align: center;"> <img src="https://www.kavitareader.com/img/email/logo-256.png" alt="kavita_logo" width="75" style="height:auto;display:inline-block;vertical-align:middle;" />
<div style="min-height:75px;line-height:75px;display:inline-block;vertical-align:middle;color:#fff;font-family: 'Spartan', sans-serif;font-size:2rem;font-weight:700;">Kavita</div>
</td>
</tr>
</table>
</td>
</tr>
<!-- HEADER : END -->
<!-- HERO : BEGIN -->
<tr>
<td bgcolor="#fff" align="center" valign="top" style="text-align: center; background-position: center center !important; background-size: cover !important;">
<!--[if gte mso 9]>
<v:rect xmlns:v="urn:schemas-microsoft-com:vml" fill="true" stroke="false" style="width:680px; height:380px; background-position: center center !important;">
<v:fill type="tile" src="background.png" color="#222222" />
<v:textbox inset="0,0,0,0">
<![endif]-->
<div>
<!--[if mso]>
<table role="presentation" border="0" cellspacing="0" cellpadding="0" align="center" width="500">
<tr>
<td align="center" valign="middle" width="500">
<![endif]-->
<table role="presentation" border="0" cellpadding="0" cellspacing="0" align="center" width="100%" style="max-width:500px; margin: auto;">
<tr>
<td height="20" style="font-size:20px; line-height:20px;">&nbsp;</td>
</tr>
<tr>
<td align="center" valign="middle">
<table>
<tr>
<td valign="top" style="text-align: center; padding: 20px 0 10px 20px;">
<h1 style="margin: 0; font-family: 'Montserrat', sans-serif; font-size: 30px; line-height: 36px; font-weight: bold;">This is a Test Email</h1> </td>
</tr>
<tr>
<td valign="top" style="text-align: center; padding: 10px 20px 15px 20px; font-family: sans-serif; font-size: 18px; line-height: 20px;">
<p style="margin: 0;">If you've received this email, your KavitaEmail is setup</p>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td height="20" style="font-size:20px; line-height:20px;">&nbsp;</td>
</tr>
</table>
<!--[if mso]>
</td>
</tr>
</table>
<![endif]-->
</div>
<!--[if gte mso 9]>
</v:textbox>
</v:rect>
<![endif]-->
</td>
</tr>
<!-- HERO : END -->
<!-- SOCIAL : BEGIN -->
<tr>
<td bgcolor="#292828">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr>
<td style="padding: 15px 30px; text-align: center;">
<table align="center" style="text-align: center;">
<tr>
<td>
<a href="https://discord.gg/b52wT37kt7"><img style="width:25px" src="https://www.kavitareader.com/img/email/discord-white.png" width="" height="" style="margin:0; padding:0; border:none; display:block;" border="0" alt="Discord"></a>
</td>
<td width="20">&nbsp;</td>
<td>
<a href="https://www.reddit.com/r/KavitaManga/"><img style="width:25px" src="https://www.kavitareader.com/img/email/reddit-white.png" width="" height="" style="margin:0; padding:0; border:none; display:block;" border="0" alt="Reddit"></a>
</td>
<td width="20">&nbsp;</td>
<td>
<a href="https://github.com/Kareadita/Kavita/"><img style="width:25px" src="https://www.kavitareader.com/img/email/github-white.png" width="" height="" style="margin:0; padding:0; border:none; display:block;" border="0" alt="Github"></a>
</td>
<td width="20">&nbsp;</td>
<td>
<a href="https://opencollective.com/kavita"><img style="width:25px" src="https://www.kavitareader.com/img/email/open-collective-white.png" width="" height="" style="margin:0; padding:0; border:none; display:block;" border="0" alt="Open Collective"></a>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
<!-- SOCIAL : END -->
</table>
<!-- Email Body : END -->
<!--[if mso]>
</td>
</tr>
</table>
<![endif]-->
</div>
</center>
</body>
</html>

View File

@ -0,0 +1,323 @@
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
<head>
<meta charset="utf-8">
<!-- utf-8 works for most cases -->
<meta name="viewport" content="width=device-width">
<!-- Forcing initial-scale shouldn't be necessary -->
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<!-- Use the latest (edge) version of IE rendering engine -->
<meta name="x-apple-disable-message-reformatting">
<!-- Disable auto-scale in iOS 10 Mail entirely -->
<title>Event - [Plain HTML]</title>
<!-- The title tag shows in email notifications, like Android 4.4. -->
<!-- Web Font / @font-face : BEGIN -->
<!-- NOTE: If web fonts are not required, lines 10 - 27 can be safely removed. -->
<!-- Desktop Outlook chokes on web font references and defaults to Times New Roman, so we force a safe fallback font. -->
<!--[if mso]>
<style>
* {
font-family: Arial, sans-serif !important;
}
</style>
<![endif]-->
<!-- All other clients get the webfont reference; some will render the font and others will silently fail to the fallbacks. More on that here: http://stylecampaign.com/blog/2015/02/webfont-support-in-email/ -->
<!--[if !mso]><!-->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Spartan:wght@500;700&display=swap" rel="stylesheet">
<link rel="stylesheet" type="text/css" href="https://use.fontawesome.com/releases/v5.6.3/css/all.css" integrity="sha384-UHRtZLI+pbxtHCWp1t77Bi1L4ZtiqrqD80Kn4Z8NTSRyMA2Fd33n5dQ8lWUE00s/" crossorigin="anonymous">
<!--<![endif]-->
<!-- Web Font / @font-face : END -->
<!-- CSS Reset -->
<style>
@import url('https://fonts.googleapis.com/css2?family=Spartan:wght@500;700&display=swap');
/* What it does: Remove spaces around the email design added by some email clients. */
/* Beware: It can remove the padding / margin and add a background color to the compose a reply window. */
html,
body {
margin: 0 auto !important;
padding: 0 !important;
height: 100% !important;
width: 100% !important;
}
/* What it does: Stops email clients resizing small text. */
* {
-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%;
}
/* What it does: Centers email on Android 4.4 */
div[style*="margin: 16px 0"] {
margin: 0 !important;
}
/* What it does: Stops Outlook from adding extra spacing to tables. */
table,
td {
mso-table-lspace: 0pt !important;
mso-table-rspace: 0pt !important;
}
/* What it does: Fixes webkit padding issue. Fix for Yahoo mail table alignment bug. Applies table-layout to the first 2 tables then removes for anything nested deeper. */
table {
border-spacing: 0 !important;
border-collapse: collapse !important;
table-layout: fixed !important;
margin: 0 auto !important;
}
table table table {
table-layout: auto;
}
i {
color: #fff;
font-size: 26px;
}
/* What it does: Uses a better rendering method when resizing images in IE. */
img {
-ms-interpolation-mode: bicubic;
}
/* What it does: A work-around for email clients meddling in triggered links. */
*[x-apple-data-detectors],
/* iOS */
.x-gmail-data-detectors,
/* Gmail */
.x-gmail-data-detectors *,
.aBn {
border-bottom: 0 !important;
cursor: default !important;
color: inherit !important;
text-decoration: none !important;
font-size: inherit !important;
font-family: inherit !important;
font-weight: inherit !important;
line-height: inherit !important;
}
/* What it does: Prevents Gmail from displaying an download button on large, non-linked images. */
.a6S {
display: none !important;
opacity: 0.01 !important;
}
/* If the above doesn't work, add a .g-img class to any image in question. */
img.g-img + div {
display: none !important;
}
/* What it does: Prevents underlining the button text in Windows 10 */
.button-link {
text-decoration: none !important;
}
/* What it does: Removes right gutter in Gmail iOS app: https://github.com/TedGoas/Cerberus/issues/89 */
/* Create one of these media queries for each additional viewport size you'd like to fix */
/* Thanks to Eric Lepetit @ericlepetitsf) for help troubleshooting */
@media only screen and (min-device-width: 375px) and (max-device-width: 413px) {
/* iPhone 6 and 6+ */
.email-container {
min-width: 375px !important;
}
}
</style>
<!-- Progressive Enhancements -->
<style>
/* What it does: Hover styles for buttons */
.button-td,
.button-a {
transition: all 100ms ease-in;
}
.button-td:hover,
.button-a:hover {
background: #000000 !important;
border-color: #000000 !important;
color: white !important;
}
/* Media Queries */
@media screen and (max-width: 480px) {
/* What it does: Forces elements to resize to the full width of their container. Useful for resizing images beyond their max-width. */
.fluid {
width: 100% !important;
max-width: 100% !important;
height: auto !important;
margin-left: auto !important;
margin-right: auto !important;
}
/* What it does: Forces table cells into full-width rows. */
.stack-column,
.stack-column-center {
display: block !important;
width: 100% !important;
max-width: 100% !important;
direction: ltr !important;
}
/* And center justify these ones. */
.stack-column-center {
text-align: center !important;
}
/* What it does: Generic utility class for centering. Useful for images, buttons, and nested tables. */
.center-on-narrow {
text-align: center !important;
display: block !important;
margin-left: auto !important;
margin-right: auto !important;
float: none !important;
}
table.center-on-narrow {
display: inline-block !important;
}
/* What it does: Adjust typography on small screens to improve readability */
.email-container p {
font-size: 17px !important;
line-height: 22px !important;
}
}
</style>
<!-- What it does: Makes background images in 72ppi Outlook render at correct size. -->
<!--[if gte mso 9]>
<xml>
<o:OfficeDocumentSettings>
<o:AllowPNG/>
<o:PixelsPerInch>96</o:PixelsPerInch>
</o:OfficeDocumentSettings>
</xml>
<![endif]-->
</head>
<body width="100%" bgcolor="#F1F1F1" style="margin: 0; mso-line-height-rule: exactly;">
<center style="width: 100%; background: #F1F1F1; text-align: left;">
<!-- Visually Hidden Preheader Text : BEGIN -->
<div style="display:none;font-size:1px;line-height:1px;max-height:0px;max-width:0px;opacity:0;overflow:hidden;mso-hide:all;font-family: sans-serif;"> You've been sent a file from Kavita! </div>
<!-- Visually Hidden Preheader Text : END -->
<!--
Set the email width. Defined in two places:
1. max-width for all clients except Desktop Windows Outlook, allowing the email to squish on narrow but never go wider than 680px.
2. MSO tags for Desktop Windows Outlook enforce a 680px width.
Note: The Fluid and Responsive templates have a different width (600px). The hybrid grid is more "fragile", and I've found that 680px is a good width. Change with caution.
-->
<div style="max-width: 680px; margin: auto;" class="email-container">
<!--[if mso]>
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="680" align="center">
<tr>
<td>
<![endif]-->
<!-- Email Body : BEGIN -->
<table role="presentation" cellspacing="0" cellpadding="0" border="0" align="center" width="100%" style="max-width: 680px;" class="email-container">
<!-- HEADER : BEGIN -->
<tr>
<td bgcolor="#4AC694">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr>
<td style="margin-left: -10px;padding: 10px 40px 10px 40px; text-align: center;"> <img src="https://www.kavitareader.com/img/email/logo-256.png" alt="kavita_logo" width="75" style="height:auto;display:inline-block;vertical-align:middle;" />
<div style="min-height:75px;line-height:75px;display:inline-block;vertical-align:middle;color:#fff;font-family: 'Spartan', sans-serif;font-size:2rem;font-weight:700;">Kavita</div>
</td>
</tr>
</table>
</td>
</tr>
<!-- HEADER : END -->
<!-- HERO : BEGIN -->
<tr>
<td bgcolor="#fff" align="center" valign="top" style="text-align: center; background-position: center center !important; background-size: cover !important;">
<!--[if gte mso 9]>
<v:rect xmlns:v="urn:schemas-microsoft-com:vml" fill="true" stroke="false" style="width:680px; height:380px; background-position: center center !important;">
<v:fill type="tile" src="background.png" color="#222222" />
<v:textbox inset="0,0,0,0">
<![endif]-->
<div>
<!--[if mso]>
<table role="presentation" border="0" cellspacing="0" cellpadding="0" align="center" width="500">
<tr>
<td align="center" valign="middle" width="500">
<![endif]-->
<table role="presentation" border="0" cellpadding="0" cellspacing="0" align="center" width="100%" style="max-width:500px; margin: auto;">
<tr>
<td height="20" style="font-size:20px; line-height:20px;">&nbsp;</td>
</tr>
<tr>
<td align="center" valign="middle">
<table>
<tr>
<td valign="top" style="text-align: center; padding: 20px 0 10px 20px;">
<h1 style="margin: 0; font-family: 'Montserrat', sans-serif; font-size: 30px; line-height: 36px; font-weight: bold;">You sent a file from Kavita</h1> </td>
</tr>
<tr>
<td valign="top" style="text-align: center; padding: 10px 20px 15px 20px; font-family: sans-serif; font-size: 18px; line-height: 20px;">
<p style="margin: 0;">Please find attached the file(s) you've sent.</p>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td height="20" style="font-size:20px; line-height:20px;">&nbsp;</td>
</tr>
</table>
<!--[if mso]>
</td>
</tr>
</table>
<![endif]-->
</div>
<!--[if gte mso 9]>
</v:textbox>
</v:rect>
<![endif]-->
</td>
</tr>
<!-- HERO : END -->
<!-- SOCIAL : BEGIN -->
<tr>
<td bgcolor="#292828">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr>
<td style="padding: 15px 30px; text-align: center;">
<table align="center" style="text-align: center;">
<tr>
<td>
<a href="https://discord.gg/b52wT37kt7"><img style="width:25px" src="https://www.kavitareader.com/img/email/discord-white.png" width="" height="" style="margin:0; padding:0; border:none; display:block;" border="0" alt="Discord"></a>
</td>
<td width="20">&nbsp;</td>
<td>
<a href="https://www.reddit.com/r/KavitaManga/"><img style="width:25px" src="https://www.kavitareader.com/img/email/reddit-white.png" width="" height="" style="margin:0; padding:0; border:none; display:block;" border="0" alt="Reddit"></a>
</td>
<td width="20">&nbsp;</td>
<td>
<a href="https://github.com/Kareadita/Kavita/"><img style="width:25px" src="https://www.kavitareader.com/img/email/github-white.png" width="" height="" style="margin:0; padding:0; border:none; display:block;" border="0" alt="Github"></a>
</td>
<td width="20">&nbsp;</td>
<td>
<a href="https://opencollective.com/kavita"><img style="width:25px" src="https://www.kavitareader.com/img/email/open-collective-white.png" width="" height="" style="margin:0; padding:0; border:none; display:block;" border="0" alt="Open Collective"></a>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
<!-- SOCIAL : END -->
</table>
<!-- Email Body : END -->
<!--[if mso]>
</td>
</tr>
</table>
<![endif]-->
</div>
</center>
</body>
</html>

View File

@ -52,6 +52,7 @@ public enum ServerSettingKey
/// Is Authentication needed for non-admin accounts
/// </summary>
/// <remarks>Deprecated. This is no longer used v0.5.1+. Assume Authentication is always in effect</remarks>
[Obsolete("Not supported as of v0.5.1")]
[Description("EnableAuthentication")]
EnableAuthentication = 8,
/// <summary>
@ -79,6 +80,7 @@ public enum ServerSettingKey
/// If SMTP is enabled on the server
/// </summary>
[Description("CustomEmailService")]
[Obsolete("Use Email settings instead")]
EmailServiceUrl = 13,
/// <summary>
/// If Kavita should save bookmarks as WebP images
@ -147,6 +149,38 @@ public enum ServerSettingKey
/// The size of the cover image thumbnail. Defaults to <see cref="CoverImageSize"/>.Default
/// </summary>
[Description("CoverImageSize")]
CoverImageSize = 27
CoverImageSize = 27,
#region EmailSettings
/// <summary>
/// The address of the emailer host
/// </summary>
[Description("EmailSenderAddress")]
EmailSenderAddress = 28,
/// <summary>
/// What the email name should be
/// </summary>
[Description("EmailSenderDisplayName")]
EmailSenderDisplayName = 29,
[Description("EmailAuthUserName")]
EmailAuthUserName = 30,
[Description("EmailAuthPassword")]
EmailAuthPassword = 31,
[Description("EmailHost")]
EmailHost = 32,
[Description("EmailPort")]
EmailPort = 33,
[Description("EmailEnableSsl")]
EmailEnableSsl = 34,
/// <summary>
/// Number of bytes that the sender allows to be sent through
/// </summary>
[Description("EmailSizeLimit")]
EmailSizeLimit = 35,
/// <summary>
/// Should Kavita use config/templates for Email templates or the default ones
/// </summary>
[Description("EmailCustomizedTemplates")]
EmailCustomizedTemplates = 36,
#endregion
}

View File

@ -47,9 +47,6 @@ public class ServerSettingConverter : ITypeConverter<IEnumerable<ServerSetting>,
case ServerSettingKey.BookmarkDirectory:
destination.BookmarksDirectory = row.Value;
break;
case ServerSettingKey.EmailServiceUrl:
destination.EmailServiceUrl = row.Value;
break;
case ServerSettingKey.InstallVersion:
destination.InstallVersion = row.Value;
break;
@ -83,6 +80,45 @@ public class ServerSettingConverter : ITypeConverter<IEnumerable<ServerSetting>,
case ServerSettingKey.CoverImageSize:
destination.CoverImageSize = Enum.Parse<CoverImageSize>(row.Value);
break;
case ServerSettingKey.BackupDirectory:
destination.BookmarksDirectory = row.Value;
break;
case ServerSettingKey.EmailHost:
destination.SmtpConfig ??= new SmtpConfigDto();
destination.SmtpConfig.Host = row.Value;
break;
case ServerSettingKey.EmailPort:
destination.SmtpConfig ??= new SmtpConfigDto();
destination.SmtpConfig.Port = string.IsNullOrEmpty(row.Value) ? 0 : int.Parse(row.Value);
break;
case ServerSettingKey.EmailAuthPassword:
destination.SmtpConfig ??= new SmtpConfigDto();
destination.SmtpConfig.Password = row.Value;
break;
case ServerSettingKey.EmailAuthUserName:
destination.SmtpConfig ??= new SmtpConfigDto();
destination.SmtpConfig.UserName = row.Value;
break;
case ServerSettingKey.EmailSenderAddress:
destination.SmtpConfig ??= new SmtpConfigDto();
destination.SmtpConfig.SenderAddress = row.Value;
break;
case ServerSettingKey.EmailSenderDisplayName:
destination.SmtpConfig ??= new SmtpConfigDto();
destination.SmtpConfig.SenderDisplayName = row.Value;
break;
case ServerSettingKey.EmailEnableSsl:
destination.SmtpConfig ??= new SmtpConfigDto();
destination.SmtpConfig.EnableSsl = bool.Parse(row.Value);
break;
case ServerSettingKey.EmailSizeLimit:
destination.SmtpConfig ??= new SmtpConfigDto();
destination.SmtpConfig.SizeLimit = int.Parse(row.Value);
break;
case ServerSettingKey.EmailCustomizedTemplates:
destination.SmtpConfig ??= new SmtpConfigDto();
destination.SmtpConfig.CustomizedTemplates = bool.Parse(row.Value);
break;
}
}

View File

@ -39,6 +39,7 @@
"admin-already-exists": "Admin already exists",
"invalid-username": "Invalid username",
"critical-email-migration": "There was an issue during email migration. Contact support",
"email-not-enabled": "Email is not enabled on this server. You cannot perform this action.",
"chapter-doesnt-exist": "Chapter does not exist",
"file-missing": "File was not found in book",
@ -53,7 +54,9 @@
"generic-device-update": "There was an error when updating the device",
"generic-device-delete": "There was an error when deleting the device",
"greater-0": "{0} must be greater than 0",
"send-to-kavita-email": "Send to device cannot be used with Kavita's email service. Please configure your own.",
"send-to-kavita-email": "Send to device cannot be used without Email setup",
"send-to-unallowed":"You cannot send to a device that isn't yours",
"send-to-size-limit": "The file(s) you are trying to send are too large for your emailer",
"send-to-device-status": "Transferring files to your device",
"generic-send-to": "There was an error sending the file(s) to the device",
"series-doesnt-exist": "Series does not exist",

View File

@ -107,6 +107,10 @@ public class DeviceService : IDeviceService
public async Task<bool> SendTo(IReadOnlyList<int> chapterIds, int deviceId)
{
var settings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync();
if (!settings.IsEmailSetup())
throw new KavitaException("send-to-kavita-email");
var device = await _unitOfWork.DeviceRepository.GetDeviceById(deviceId);
if (device == null) throw new KavitaException("device-doesnt-exist");
@ -114,6 +118,10 @@ public class DeviceService : IDeviceService
if (files.Any(f => f.Format is not (MangaFormat.Epub or MangaFormat.Pdf)) && device.Platform == DevicePlatform.Kindle)
throw new KavitaException("send-to-permission");
// If the size of the files is too big
if (files.Sum(f => f.Bytes) >= settings.SmtpConfig.SizeLimit)
throw new KavitaException("send-to-size-limit");
device.UpdateLastUsed();
_unitOfWork.DeviceRepository.Update(device);

View File

@ -26,6 +26,8 @@ public interface IDirectoryService
string SiteThemeDirectory { get; }
string FaviconDirectory { get; }
string LocalizationDirectory { get; }
string CustomizedTemplateDirectory { get; }
string TemplateDirectory { get; }
/// <summary>
/// Original BookmarkDirectory. Only used for resetting directory. Use <see cref="ServerSettingKey.BackupDirectory"/> for actual path.
/// </summary>
@ -81,6 +83,8 @@ public class DirectoryService : IDirectoryService
public string SiteThemeDirectory { get; }
public string FaviconDirectory { get; }
public string LocalizationDirectory { get; }
public string CustomizedTemplateDirectory { get; }
public string TemplateDirectory { get; }
private readonly ILogger<DirectoryService> _logger;
private const RegexOptions MatchOptions = RegexOptions.Compiled | RegexOptions.IgnoreCase;
@ -114,6 +118,10 @@ public class DirectoryService : IDirectoryService
FaviconDirectory = FileSystem.Path.Join(FileSystem.Directory.GetCurrentDirectory(), "config", "favicons");
ExistOrCreate(FaviconDirectory);
LocalizationDirectory = FileSystem.Path.Join(FileSystem.Directory.GetCurrentDirectory(), "I18N");
CustomizedTemplateDirectory = FileSystem.Path.Join(FileSystem.Directory.GetCurrentDirectory(), "config", "templates");
ExistOrCreate(CustomizedTemplateDirectory);
TemplateDirectory = FileSystem.Path.Join(FileSystem.Directory.GetCurrentDirectory(), "EmailTemplates");
ExistOrCreate(TemplateDirectory);
}
/// <summary>

View File

@ -1,34 +1,45 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.IO;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using API.Data;
using API.DTOs.Email;
using API.Entities.Enums;
using Flurl;
using Flurl.Http;
using Kavita.Common;
using Kavita.Common.EnvironmentInfo;
using Kavita.Common.Helpers;
using MailKit.Security;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using MimeKit;
namespace API.Services;
#nullable enable
internal class EmailOptionsDto
{
public IList<string> ToEmails { get; set; }
public string Subject { get; set; }
public string Body { get; set; }
public IList<KeyValuePair<string, string>> PlaceHolders { get; set; }
/// <summary>
/// Filenames to attach
/// </summary>
public IList<string>? Attachments { get; set; }
}
public interface IEmailService
{
Task SendConfirmationEmail(ConfirmationEmailDto data);
Task SendInviteEmail(ConfirmationEmailDto data);
Task<bool> CheckIfAccessible(string host);
Task<bool> SendMigrationEmail(EmailMigrationDto data);
Task<bool> SendPasswordResetEmail(PasswordResetEmailDto data);
Task<bool> SendForgotPasswordEmail(PasswordResetEmailDto dto);
Task<bool> SendFilesToEmail(SendToDto data);
Task<EmailTestResultDto> TestConnectivity(string emailUrl, string adminEmail, bool sendEmail);
Task<EmailTestResultDto> SendTestEmail(string adminEmail);
Task<bool> IsDefaultEmailService();
Task SendEmailChangeEmail(ConfirmationEmailDto data);
Task<string?> GetVersion(string emailUrl);
bool IsValidEmail(string email);
}
@ -37,41 +48,62 @@ public class EmailService : IEmailService
private readonly ILogger<EmailService> _logger;
private readonly IUnitOfWork _unitOfWork;
private readonly IDownloadService _downloadService;
private readonly IDirectoryService _directoryService;
private const string TemplatePath = @"{0}.html";
/// <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, IDownloadService downloadService)
public EmailService(ILogger<EmailService> logger, IUnitOfWork unitOfWork, IDownloadService downloadService, IDirectoryService directoryService)
{
_logger = logger;
_unitOfWork = unitOfWork;
_downloadService = downloadService;
FlurlHttp.ConfigureClient(DefaultApiUrl, cli =>
cli.Settings.HttpClientFactory = new UntrustedCertClientFactory());
_directoryService = directoryService;
}
/// <summary>
/// Test if this instance is accessible outside the network
/// Test if the email settings are working. Rejects if user email isn't valid or not all data is setup in server settings.
/// </summary>
/// <remarks>This will do some basic filtering to auto return false if the emailUrl is a LAN ip</remarks>
/// <param name="emailUrl"></param>
/// <param name="sendEmail">Should an email be sent if connectivity is successful</param>
/// <returns></returns>
public async Task<EmailTestResultDto> TestConnectivity(string emailUrl, string adminEmail, bool sendEmail)
public async Task<EmailTestResultDto> SendTestEmail(string adminEmail)
{
var result = new EmailTestResultDto();
var result = new EmailTestResultDto
{
EmailAddress = adminEmail
};
var settings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync();
if (!IsValidEmail(adminEmail) || !settings.IsEmailSetup())
{
result.ErrorMessage = "You need to fill in more information in settings and ensure your account has a valid email to send a test email";
result.Successful = false;
return result;
}
// TODO: Come back and update the template. We can't do it with the v0.8.0 release
var placeholders = new List<KeyValuePair<string, string>>
{
new ("{{Host}}", settings.HostName),
};
try
{
if (IsLocalIpAddress(emailUrl))
var emailOptions = new EmailOptionsDto()
{
result.Successful = false;
result.ErrorMessage = "This is a local IP address";
}
result.Successful = await SendEmailWithGet($"{emailUrl}/api/test?adminEmail={Url.Encode(adminEmail)}&sendEmail={sendEmail}");
Subject = "KavitaEmail Test",
Body = UpdatePlaceHolders(await GetEmailBody("EmailTest"), placeholders),
ToEmails = new List<string>()
{
adminEmail
}
};
await SendEmail(emailOptions);
result.Successful = true;
}
catch (KavitaException ex)
{
@ -82,229 +114,210 @@ public class EmailService : IEmailService
return result;
}
[Obsolete]
public async Task<bool> IsDefaultEmailService()
{
return (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.EmailServiceUrl))!.Value!
.Equals(DefaultApiUrl);
}
/// <summary>
/// Sends an email that has a link that will finalize an Email Change
/// </summary>
/// <param name="data"></param>
public async Task SendEmailChangeEmail(ConfirmationEmailDto data)
{
var emailLink = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.EmailServiceUrl))!.Value;
var success = await SendEmailWithPost(emailLink + "/api/account/email-change", data);
if (!success)
var placeholders = new List<KeyValuePair<string, string>>
{
_logger.LogError("There was a critical error sending Confirmation email");
}
}
new ("{{InvitingUser}}", data.InvitingUser),
new ("{{Link}}", data.ServerConfirmationLink)
};
public async Task<string> GetVersion(string emailUrl)
{
try
var emailOptions = new EmailOptionsDto()
{
var settings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync();
var response = await $"{emailUrl}/api/about/version"
.WithHeader("Accept", "application/json")
.WithHeader("User-Agent", "Kavita")
.WithHeader("x-api-key", "MsnvA2DfQqxSK5jh")
.WithHeader("x-kavita-version", BuildInfo.Version)
.WithHeader("x-kavita-installId", settings.InstallId)
.WithHeader("Content-Type", "application/json")
.WithTimeout(TimeSpan.FromSeconds(10))
.GetStringAsync();
if (!string.IsNullOrEmpty(response))
Subject = UpdatePlaceHolders("Your email has been changed on {{InvitingUser}}'s Server", placeholders),
Body = UpdatePlaceHolders(await GetEmailBody("EmailChange"), placeholders),
ToEmails = new List<string>()
{
return response.Replace("\"", string.Empty);
data.EmailAddress
}
}
catch (Exception)
{
return null;
}
};
return null;
await SendEmail(emailOptions);
}
/// <summary>
/// Validates the email address. Does not test it actually receives mail
/// </summary>
/// <param name="email"></param>
/// <returns></returns>
public bool IsValidEmail(string email)
{
return new EmailAddressAttribute().IsValid(email);
}
public async Task SendConfirmationEmail(ConfirmationEmailDto data)
/// <summary>
/// Sends an invite email to a user to setup their account
/// </summary>
/// <param name="data"></param>
public async Task SendInviteEmail(ConfirmationEmailDto data)
{
var emailLink = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.EmailServiceUrl)).Value;
var success = await SendEmailWithPost(emailLink + "/api/invite/confirm", data);
if (!success)
var placeholders = new List<KeyValuePair<string, string>>
{
_logger.LogError("There was a critical error sending Confirmation email");
}
}
new ("{{InvitingUser}}", data.InvitingUser),
new ("{{Link}}", data.ServerConfirmationLink)
};
public async Task<bool> CheckIfAccessible(string 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
try
var emailOptions = new EmailOptionsDto()
{
if (IsLocalIpAddress(host))
Subject = UpdatePlaceHolders("You've been invited to join {{InvitingUser}}'s Server", placeholders),
Body = UpdatePlaceHolders(await GetEmailBody("EmailConfirm"), placeholders),
ToEmails = new List<string>()
{
_logger.LogDebug("[EmailService] Server is not accessible, using local ip");
return false;
data.EmailAddress
}
};
var url = DefaultApiUrl + "/api/reachable?host=" + host;
_logger.LogDebug("[EmailService] Checking if this server is accessible for sending an email to: {Url}", url);
return await SendEmailWithGet(url);
}
catch (Exception)
await SendEmail(emailOptions);
}
public Task<bool> CheckIfAccessible(string host)
{
return Task.FromResult(true);
}
public async Task<bool> SendForgotPasswordEmail(PasswordResetEmailDto dto)
{
var placeholders = new List<KeyValuePair<string, string>>
{
return false;
}
}
new ("{{Link}}", dto.ServerConfirmationLink),
};
public async Task<bool> SendMigrationEmail(EmailMigrationDto data)
{
var emailLink = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.EmailServiceUrl)).Value;
return await SendEmailWithPost(emailLink + "/api/invite/email-migration", data);
}
var emailOptions = new EmailOptionsDto()
{
Subject = UpdatePlaceHolders("A password reset has been requested", placeholders),
Body = UpdatePlaceHolders(await GetEmailBody("EmailPasswordReset"), placeholders),
ToEmails = new List<string>()
{
dto.EmailAddress
}
};
public async Task<bool> SendPasswordResetEmail(PasswordResetEmailDto data)
{
var emailLink = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.EmailServiceUrl)).Value;
return await SendEmailWithPost(emailLink + "/api/invite/email-password-reset", data);
await SendEmail(emailOptions);
return true;
}
public async Task<bool> SendFilesToEmail(SendToDto data)
{
if (await IsDefaultEmailService()) return false;
var emailLink = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.EmailServiceUrl)).Value;
return await SendEmailWithFiles(emailLink + "/api/sendto", data.FilePaths, data.DestinationEmail);
var serverSetting = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync();
if (!serverSetting.IsEmailSetup()) return false;
var emailOptions = new EmailOptionsDto()
{
Subject = "Send file from Kavita",
ToEmails = new List<string>()
{
data.DestinationEmail
},
Body = await GetEmailBody("SendToDevice"),
Attachments = data.FilePaths.ToList()
};
await SendEmail(emailOptions);
return true;
}
private async Task<bool> SendEmailWithGet(string url, int timeoutSecs = 30)
private async Task SendEmail(EmailOptionsDto userEmailOptions)
{
var smtpConfig = (await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).SmtpConfig;
var email = new MimeMessage()
{
Subject = userEmailOptions.Subject,
};
email.From.Add(new MailboxAddress(smtpConfig.SenderDisplayName, smtpConfig.SenderAddress));
var body = new BodyBuilder
{
HtmlBody = userEmailOptions.Body
};
if (userEmailOptions.Attachments != null)
{
foreach (var attachment in userEmailOptions.Attachments)
{
await body.Attachments.AddAsync(attachment);
}
}
email.Body = body.ToMessageBody();
foreach (var toEmail in userEmailOptions.ToEmails)
{
email.To.Add(new MailboxAddress(toEmail, toEmail));
}
using var smtpClient = new MailKit.Net.Smtp.SmtpClient();
smtpClient.Timeout = 20000;
var ssl = smtpConfig.EnableSsl ? SecureSocketOptions.Auto : SecureSocketOptions.None;
await smtpClient.ConnectAsync(smtpConfig.Host, smtpConfig.Port, ssl);
if (!string.IsNullOrEmpty(smtpConfig.UserName) && !string.IsNullOrEmpty(smtpConfig.Password))
{
await smtpClient.AuthenticateAsync(smtpConfig.UserName, smtpConfig.Password);
}
ServicePointManager.SecurityProtocol = SecurityProtocolType.SystemDefault;
try
{
var settings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync();
var response = await (url)
.WithHeader("Accept", "application/json")
.WithHeader("User-Agent", "Kavita")
.WithHeader("x-api-key", "MsnvA2DfQqxSK5jh")
.WithHeader("x-kavita-version", BuildInfo.Version)
.WithHeader("x-kavita-installId", settings.InstallId)
.WithHeader("Content-Type", "application/json")
.WithTimeout(TimeSpan.FromSeconds(timeoutSecs))
.GetStringAsync();
if (!string.IsNullOrEmpty(response) && bool.Parse(response))
{
return true;
}
await smtpClient.SendAsync(email);
}
catch (Exception ex)
{
throw new KavitaException(ex.Message);
_logger.LogError(ex, "There was an issue sending the email");
throw;
}
finally
{
await smtpClient.DisconnectAsync(true);
}
return false;
}
private async Task<bool> SendEmailWithPost(string url, object data, int timeoutSecs = 30)
private async Task<string> GetTemplatePath(string templateName)
{
try
if ((await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).SmtpConfig.CustomizedTemplates)
{
var settings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync();
var response = await (url)
.WithHeader("Accept", "application/json")
.WithHeader("User-Agent", "Kavita")
.WithHeader("x-api-key", "MsnvA2DfQqxSK5jh")
.WithHeader("x-kavita-version", BuildInfo.Version)
.WithHeader("x-kavita-installId", settings.InstallId)
.WithHeader("Content-Type", "application/json")
.WithTimeout(TimeSpan.FromSeconds(timeoutSecs))
.PostJsonAsync(data);
var templateDirectory = Path.Join(_directoryService.CustomizedTemplateDirectory, TemplatePath);
var fullName = string.Format(templateDirectory, templateName);
if (_directoryService.FileSystem.File.Exists(fullName)) return fullName;
_logger.LogError("Customized Templates is on, but template {TemplatePath} is missing", fullName);
}
if (response.StatusCode != StatusCodes.Status200OK)
return string.Format(Path.Join(_directoryService.TemplateDirectory, TemplatePath), templateName);
}
private async Task<string> GetEmailBody(string templateName)
{
var templatePath = await GetTemplatePath(templateName);
var body = await File.ReadAllTextAsync(templatePath);
return body;
}
private static string UpdatePlaceHolders(string text, IList<KeyValuePair<string, string>> keyValuePairs)
{
if (string.IsNullOrEmpty(text) || keyValuePairs == null) return text;
foreach (var (key, value) in keyValuePairs)
{
if (text.Contains(key))
{
var errorMessage = await response.GetStringAsync();
throw new KavitaException(errorMessage);
text = text.Replace(key, value);
}
}
catch (FlurlHttpException ex)
{
_logger.LogError(ex, "There was an exception when interacting with Email Service");
return false;
}
return true;
return text;
}
private async Task<bool> SendEmailWithFiles(string url, IEnumerable<string> filePaths, string destEmail, int timeoutSecs = 300)
{
try
{
var settings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync();
var response = await (url)
.WithHeader("User-Agent", "Kavita")
.WithHeader("x-api-key", "MsnvA2DfQqxSK5jh")
.WithHeader("x-kavita-version", BuildInfo.Version)
.WithHeader("x-kavita-installId", settings.InstallId)
.WithTimeout(timeoutSecs)
.AllowHttpStatus("4xx")
.PostMultipartAsync(mp =>
{
mp.AddString("email", destEmail);
var index = 1;
foreach (var filepath in filePaths)
{
mp.AddFile("file" + index, filepath, _downloadService.GetContentTypeFromFile(filepath));
index++;
}
}
);
if (response.StatusCode != StatusCodes.Status200OK)
{
var errorMessage = await response.GetStringAsync();
throw new KavitaException(errorMessage);
}
}
catch (FlurlHttpException ex)
{
_logger.LogError(ex, "There was an exception when sending Email for SendTo");
return false;
}
return true;
}
private static bool IsLocalIpAddress(string url)
{
var host = url.Split(':')[0];
try
{
// get host IP addresses
var hostIPs = Dns.GetHostAddresses(host);
// get local IP addresses
var localIPs = Dns.GetHostAddresses(Dns.GetHostName());
// test if any host IP equals to any local IP or to localhost
foreach (var hostIp in hostIPs)
{
// is localhost
if (IPAddress.IsLoopback(hostIp)) return true;
// is local address
if (localIPs.Contains(hostIp))
{
return true;
}
}
}
catch
{
// ignored
}
return false;
}
}

View File

@ -244,6 +244,9 @@ public class Startup
await MigrateSmartFilterEncoding.Migrate(unitOfWork, dataContext, logger);
await MigrateLibrariesToHaveAllFileTypes.Migrate(unitOfWork, dataContext, logger);
// v0.7.14
await MigrateEmailTemplates.Migrate(directoryService, logger);
// Update the version in the DB after all migrations are run
var installVersion = await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.InstallVersion);
installVersion.Value = BuildInfo.Version.ToString();

View File

@ -0,0 +1,348 @@
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
<head>
<meta charset="utf-8">
<!-- utf-8 works for most cases -->
<meta name="viewport" content="width=device-width">
<!-- Forcing initial-scale shouldn't be necessary -->
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<!-- Use the latest (edge) version of IE rendering engine -->
<meta name="x-apple-disable-message-reformatting">
<!-- Disable auto-scale in iOS 10 Mail entirely -->
<title>Event - [Plain HTML]</title>
<!-- The title tag shows in email notifications, like Android 4.4. -->
<!-- Web Font / @font-face : BEGIN -->
<!-- NOTE: If web fonts are not required, lines 10 - 27 can be safely removed. -->
<!-- Desktop Outlook chokes on web font references and defaults to Times New Roman, so we force a safe fallback font. -->
<!--[if mso]>
<style>
* {
font-family: Arial, sans-serif !important;
}
</style>
<![endif]-->
<!-- All other clients get the webfont reference; some will render the font and others will silently fail to the fallbacks. More on that here: http://stylecampaign.com/blog/2015/02/webfont-support-in-email/ -->
<!--[if !mso]><!-->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Spartan:wght@500;700&display=swap" rel="stylesheet">
<link rel="stylesheet" type="text/css" href="https://use.fontawesome.com/releases/v5.6.3/css/all.css" integrity="sha384-UHRtZLI+pbxtHCWp1t77Bi1L4ZtiqrqD80Kn4Z8NTSRyMA2Fd33n5dQ8lWUE00s/" crossorigin="anonymous">
<!--<![endif]-->
<!-- Web Font / @font-face : END -->
<!-- CSS Reset -->
<style>
@import url('https://fonts.googleapis.com/css2?family=Spartan:wght@500;700&display=swap');
/* What it does: Remove spaces around the email design added by some email clients. */
/* Beware: It can remove the padding / margin and add a background color to the compose a reply window. */
html,
body {
margin: 0 auto !important;
padding: 0 !important;
height: 100% !important;
width: 100% !important;
}
/* What it does: Stops email clients resizing small text. */
* {
-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%;
}
/* What it does: Centers email on Android 4.4 */
div[style*="margin: 16px 0"] {
margin: 0 !important;
}
/* What it does: Stops Outlook from adding extra spacing to tables. */
table,
td {
mso-table-lspace: 0pt !important;
mso-table-rspace: 0pt !important;
}
/* What it does: Fixes webkit padding issue. Fix for Yahoo mail table alignment bug. Applies table-layout to the first 2 tables then removes for anything nested deeper. */
table {
border-spacing: 0 !important;
border-collapse: collapse !important;
table-layout: fixed !important;
margin: 0 auto !important;
}
table table table {
table-layout: auto;
}
i {
color: #fff;
font-size: 26px;
}
/* What it does: Uses a better rendering method when resizing images in IE. */
img {
-ms-interpolation-mode: bicubic;
}
/* What it does: A work-around for email clients meddling in triggered links. */
*[x-apple-data-detectors],
/* iOS */
.x-gmail-data-detectors,
/* Gmail */
.x-gmail-data-detectors *,
.aBn {
border-bottom: 0 !important;
cursor: default !important;
color: inherit !important;
text-decoration: none !important;
font-size: inherit !important;
font-family: inherit !important;
font-weight: inherit !important;
line-height: inherit !important;
}
/* What it does: Prevents Gmail from displaying an download button on large, non-linked images. */
.a6S {
display: none !important;
opacity: 0.01 !important;
}
/* If the above doesn't work, add a .g-img class to any image in question. */
img.g-img + div {
display: none !important;
}
/* What it does: Prevents underlining the button text in Windows 10 */
.button-link {
text-decoration: none !important;
}
/* What it does: Removes right gutter in Gmail iOS app: https://github.com/TedGoas/Cerberus/issues/89 */
/* Create one of these media queries for each additional viewport size you'd like to fix */
/* Thanks to Eric Lepetit @ericlepetitsf) for help troubleshooting */
@media only screen and (min-device-width: 375px) and (max-device-width: 413px) {
/* iPhone 6 and 6+ */
.email-container {
min-width: 375px !important;
}
}
</style>
<!-- Progressive Enhancements -->
<style>
/* What it does: Hover styles for buttons */
.button-td,
.button-a {
transition: all 100ms ease-in;
}
.button-td:hover,
.button-a:hover {
background: #000000 !important;
border-color: #000000 !important;
color: white !important;
}
/* Media Queries */
@media screen and (max-width: 480px) {
/* What it does: Forces elements to resize to the full width of their container. Useful for resizing images beyond their max-width. */
.fluid {
width: 100% !important;
max-width: 100% !important;
height: auto !important;
margin-left: auto !important;
margin-right: auto !important;
}
/* What it does: Forces table cells into full-width rows. */
.stack-column,
.stack-column-center {
display: block !important;
width: 100% !important;
max-width: 100% !important;
direction: ltr !important;
}
/* And center justify these ones. */
.stack-column-center {
text-align: center !important;
}
/* What it does: Generic utility class for centering. Useful for images, buttons, and nested tables. */
.center-on-narrow {
text-align: center !important;
display: block !important;
margin-left: auto !important;
margin-right: auto !important;
float: none !important;
}
table.center-on-narrow {
display: inline-block !important;
}
/* What it does: Adjust typography on small screens to improve readability */
.email-container p {
font-size: 17px !important;
line-height: 22px !important;
}
}
</style>
<!-- What it does: Makes background images in 72ppi Outlook render at correct size. -->
<!--[if gte mso 9]>
<xml>
<o:OfficeDocumentSettings>
<o:AllowPNG/>
<o:PixelsPerInch>96</o:PixelsPerInch>
</o:OfficeDocumentSettings>
</xml>
<![endif]-->
</head>
<body width="100%" bgcolor="#F1F1F1" style="margin: 0; mso-line-height-rule: exactly;">
<center style="width: 100%; background: #F1F1F1; text-align: left;">
<!-- Visually Hidden Preheader Text : BEGIN -->
<div style="display:none;font-size:1px;line-height:1px;max-height:0px;max-width:0px;opacity:0;overflow:hidden;mso-hide:all;font-family: sans-serif;"> Your account's email has been updated on {{InvitingUser}}'s Kavita instance. Click the button to validate your email. </div>
<!-- Visually Hidden Preheader Text : END -->
<!--
Set the email width. Defined in two places:
1. max-width for all clients except Desktop Windows Outlook, allowing the email to squish on narrow but never go wider than 680px.
2. MSO tags for Desktop Windows Outlook enforce a 680px width.
Note: The Fluid and Responsive templates have a different width (600px). The hybrid grid is more "fragile", and I've found that 680px is a good width. Change with caution.
-->
<div style="max-width: 680px; margin: auto;" class="email-container">
<!--[if mso]>
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="680" align="center">
<tr>
<td>
<![endif]-->
<!-- Email Body : BEGIN -->
<table role="presentation" cellspacing="0" cellpadding="0" border="0" align="center" width="100%" style="max-width: 680px;" class="email-container">
<!-- HEADER : BEGIN -->
<tr>
<td bgcolor="#4AC694">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr>
<td style="margin-left: -10px;padding: 10px 40px 10px 40px; text-align: center;"> <img src="https://www.kavitareader.com/img/email/logo-256.png" alt="kavita_logo" width="75" style="height:auto;display:inline-block;vertical-align:middle;" />
<div style="min-height:75px;line-height:75px;display:inline-block;vertical-align:middle;color:#fff;font-family: 'Spartan', sans-serif;font-size:2rem;font-weight:700;">Kavita</div>
</td>
</tr>
</table>
</td>
</tr>
<!-- HEADER : END -->
<!-- HERO : BEGIN -->
<tr>
<td bgcolor="#fff" align="center" valign="top" style="text-align: center; background-position: center center !important; background-size: cover !important;">
<!--[if gte mso 9]>
<v:rect xmlns:v="urn:schemas-microsoft-com:vml" fill="true" stroke="false" style="width:680px; height:380px; background-position: center center !important;">
<v:fill type="tile" src="background.png" color="#222222" />
<v:textbox inset="0,0,0,0">
<![endif]-->
<div>
<!--[if mso]>
<table role="presentation" border="0" cellspacing="0" cellpadding="0" align="center" width="500">
<tr>
<td align="center" valign="middle" width="500">
<![endif]-->
<table role="presentation" border="0" cellpadding="0" cellspacing="0" align="center" width="100%" style="max-width:500px; margin: auto;">
<tr>
<td height="20" style="font-size:20px; line-height:20px;">&nbsp;</td>
</tr>
<tr>
<td align="center" valign="middle">
<table>
<tr>
<td valign="top" style="text-align: center; padding: 20px 0 10px 20px;">
<h1 style="margin: 0; font-family: 'Montserrat', sans-serif; font-size: 30px; line-height: 36px; font-weight: bold;">Email Change Update</h1> </td>
</tr>
<tr>
<td valign="top" style="text-align: center; padding: 10px 20px 15px 20px; font-family: sans-serif; font-size: 18px; line-height: 20px;">
<p style="margin: 0;">Your account's email has been updated on {{InvitingUser}}'s Kavita instance.</p>
</td>
</tr>
<tr>
<td valign="top" style="text-align: center; padding: 10px 20px 15px 20px; font-family: sans-serif; font-size: 18px; line-height: 20px;">
<p style="margin: 0;">Please click the following link to validate your email change.</p>
</td>
</tr>
<tr>
<td valign="top" align="center" style="text-align: center; padding: 15px 0px 20px 0px;">
<!-- Button : BEGIN -->
<center>
<table role="presentation" align="center" cellspacing="0" cellpadding="0" border="0" class="center-on-narrow" style="text-align: center;">
<tr>
<td style="border-radius: 50px; background: #153643; text-align: center;" class="button-td">
<a href="{{Link}}" style="background: #153643; border: 15px solid #153643; font-family: 'Montserrat', sans-serif; font-size: 14px; line-height: 1.1; text-align: center; text-decoration: none; display: block; border-radius: 50px; font-weight: bold;" class="button-a"> <span style="color:#ffffff;" class="button-link">&nbsp;&nbsp;&nbsp;&nbsp;CONFIRM EMAIL&nbsp;&nbsp;&nbsp;&nbsp;</span> </a>
</td>
</tr>
</table>
</center>
<!-- Button : END -->
<tr>
<td valign="top" style="text-align: center; padding: 10px 20px 15px 20px; font-family: sans-serif; font-size: 12px; line-height: 20px;">
<p style="margin: 0;">If the button above does not work, please find the link here: <a style="color:inherit;margin: 0;width: 100%;word-break: break-all;" href="{{Link}}">{{Link}}</a></p>
</td>
</tr>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td height="20" style="font-size:20px; line-height:20px;">&nbsp;</td>
</tr>
</table>
<!--[if mso]>
</td>
</tr>
</table>
<![endif]-->
</div>
<!--[if gte mso 9]>
</v:textbox>
</v:rect>
<![endif]-->
</td>
</tr>
<!-- HERO : END -->
<!-- SOCIAL : BEGIN -->
<tr>
<td bgcolor="#292828">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr>
<td style="padding: 15px 30px; text-align: center;">
<table align="center" style="text-align: center;">
<tr>
<td>
<a href="https://discord.gg/b52wT37kt7"><img style="width:25px" src="https://www.kavitareader.com/img/email/discord-white.png" width="" height="" style="margin:0; padding:0; border:none; display:block;" border="0" alt="Discord"></a>
</td>
<td width="20">&nbsp;</td>
<td>
<a href="https://www.reddit.com/r/KavitaManga/"><img style="width:25px" src="https://www.kavitareader.com/img/email/reddit-white.png" width="" height="" style="margin:0; padding:0; border:none; display:block;" border="0" alt="Reddit"></a>
</td>
<td width="20">&nbsp;</td>
<td>
<a href="https://github.com/Kareadita/Kavita/"><img style="width:25px" src="https://www.kavitareader.com/img/email/github-white.png" width="" height="" style="margin:0; padding:0; border:none; display:block;" border="0" alt="Github"></a>
</td>
<td width="20">&nbsp;</td>
<td>
<a href="https://opencollective.com/kavita"><img style="width:25px" src="https://www.kavitareader.com/img/email/open-collective-white.png" width="" height="" style="margin:0; padding:0; border:none; display:block;" border="0" alt="Open Collective"></a>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
<!-- SOCIAL : END -->
</table>
<!-- Email Body : END -->
<!--[if mso]>
</td>
</tr>
</table>
<![endif]-->
</div>
</center>
</body>
</html>

View File

@ -0,0 +1,348 @@
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
<head>
<meta charset="utf-8">
<!-- utf-8 works for most cases -->
<meta name="viewport" content="width=device-width">
<!-- Forcing initial-scale shouldn't be necessary -->
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<!-- Use the latest (edge) version of IE rendering engine -->
<meta name="x-apple-disable-message-reformatting">
<!-- Disable auto-scale in iOS 10 Mail entirely -->
<title>Event - [Plain HTML]</title>
<!-- The title tag shows in email notifications, like Android 4.4. -->
<!-- Web Font / @font-face : BEGIN -->
<!-- NOTE: If web fonts are not required, lines 10 - 27 can be safely removed. -->
<!-- Desktop Outlook chokes on web font references and defaults to Times New Roman, so we force a safe fallback font. -->
<!--[if mso]>
<style>
* {
font-family: Arial, sans-serif !important;
}
</style>
<![endif]-->
<!-- All other clients get the webfont reference; some will render the font and others will silently fail to the fallbacks. More on that here: http://stylecampaign.com/blog/2015/02/webfont-support-in-email/ -->
<!--[if !mso]><!-->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Spartan:wght@500;700&display=swap" rel="stylesheet">
<link rel="stylesheet" type="text/css" href="https://use.fontawesome.com/releases/v5.6.3/css/all.css" integrity="sha384-UHRtZLI+pbxtHCWp1t77Bi1L4ZtiqrqD80Kn4Z8NTSRyMA2Fd33n5dQ8lWUE00s/" crossorigin="anonymous">
<!--<![endif]-->
<!-- Web Font / @font-face : END -->
<!-- CSS Reset -->
<style>
@import url('https://fonts.googleapis.com/css2?family=Spartan:wght@500;700&display=swap');
/* What it does: Remove spaces around the email design added by some email clients. */
/* Beware: It can remove the padding / margin and add a background color to the compose a reply window. */
html,
body {
margin: 0 auto !important;
padding: 0 !important;
height: 100% !important;
width: 100% !important;
}
/* What it does: Stops email clients resizing small text. */
* {
-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%;
}
/* What it does: Centers email on Android 4.4 */
div[style*="margin: 16px 0"] {
margin: 0 !important;
}
/* What it does: Stops Outlook from adding extra spacing to tables. */
table,
td {
mso-table-lspace: 0pt !important;
mso-table-rspace: 0pt !important;
}
/* What it does: Fixes webkit padding issue. Fix for Yahoo mail table alignment bug. Applies table-layout to the first 2 tables then removes for anything nested deeper. */
table {
border-spacing: 0 !important;
border-collapse: collapse !important;
table-layout: fixed !important;
margin: 0 auto !important;
}
table table table {
table-layout: auto;
}
i {
color: #fff;
font-size: 26px;
}
/* What it does: Uses a better rendering method when resizing images in IE. */
img {
-ms-interpolation-mode: bicubic;
}
/* What it does: A work-around for email clients meddling in triggered links. */
*[x-apple-data-detectors],
/* iOS */
.x-gmail-data-detectors,
/* Gmail */
.x-gmail-data-detectors *,
.aBn {
border-bottom: 0 !important;
cursor: default !important;
color: inherit !important;
text-decoration: none !important;
font-size: inherit !important;
font-family: inherit !important;
font-weight: inherit !important;
line-height: inherit !important;
}
/* What it does: Prevents Gmail from displaying an download button on large, non-linked images. */
.a6S {
display: none !important;
opacity: 0.01 !important;
}
/* If the above doesn't work, add a .g-img class to any image in question. */
img.g-img + div {
display: none !important;
}
/* What it does: Prevents underlining the button text in Windows 10 */
.button-link {
text-decoration: none !important;
}
/* What it does: Removes right gutter in Gmail iOS app: https://github.com/TedGoas/Cerberus/issues/89 */
/* Create one of these media queries for each additional viewport size you'd like to fix */
/* Thanks to Eric Lepetit @ericlepetitsf) for help troubleshooting */
@media only screen and (min-device-width: 375px) and (max-device-width: 413px) {
/* iPhone 6 and 6+ */
.email-container {
min-width: 375px !important;
}
}
</style>
<!-- Progressive Enhancements -->
<style>
/* What it does: Hover styles for buttons */
.button-td,
.button-a {
transition: all 100ms ease-in;
}
.button-td:hover,
.button-a:hover {
background: #000000 !important;
border-color: #000000 !important;
color: white !important;
}
/* Media Queries */
@media screen and (max-width: 480px) {
/* What it does: Forces elements to resize to the full width of their container. Useful for resizing images beyond their max-width. */
.fluid {
width: 100% !important;
max-width: 100% !important;
height: auto !important;
margin-left: auto !important;
margin-right: auto !important;
}
/* What it does: Forces table cells into full-width rows. */
.stack-column,
.stack-column-center {
display: block !important;
width: 100% !important;
max-width: 100% !important;
direction: ltr !important;
}
/* And center justify these ones. */
.stack-column-center {
text-align: center !important;
}
/* What it does: Generic utility class for centering. Useful for images, buttons, and nested tables. */
.center-on-narrow {
text-align: center !important;
display: block !important;
margin-left: auto !important;
margin-right: auto !important;
float: none !important;
}
table.center-on-narrow {
display: inline-block !important;
}
/* What it does: Adjust typography on small screens to improve readability */
.email-container p {
font-size: 17px !important;
line-height: 22px !important;
}
}
</style>
<!-- What it does: Makes background images in 72ppi Outlook render at correct size. -->
<!--[if gte mso 9]>
<xml>
<o:OfficeDocumentSettings>
<o:AllowPNG/>
<o:PixelsPerInch>96</o:PixelsPerInch>
</o:OfficeDocumentSettings>
</xml>
<![endif]-->
</head>
<body width="100%" bgcolor="#F1F1F1" style="margin: 0; mso-line-height-rule: exactly;">
<center style="width: 100%; background: #F1F1F1; text-align: left;">
<!-- Visually Hidden Preheader Text : BEGIN -->
<div style="display:none;font-size:1px;line-height:1px;max-height:0px;max-width:0px;opacity:0;overflow:hidden;mso-hide:all;font-family: sans-serif;"> You have been invited to {{InvitingUser}}'s Kavita instance. Click the button to accept the invite. </div>
<!-- Visually Hidden Preheader Text : END -->
<!--
Set the email width. Defined in two places:
1. max-width for all clients except Desktop Windows Outlook, allowing the email to squish on narrow but never go wider than 680px.
2. MSO tags for Desktop Windows Outlook enforce a 680px width.
Note: The Fluid and Responsive templates have a different width (600px). The hybrid grid is more "fragile", and I've found that 680px is a good width. Change with caution.
-->
<div style="max-width: 680px; margin: auto;" class="email-container">
<!--[if mso]>
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="680" align="center">
<tr>
<td>
<![endif]-->
<!-- Email Body : BEGIN -->
<table role="presentation" cellspacing="0" cellpadding="0" border="0" align="center" width="100%" style="max-width: 680px;" class="email-container">
<!-- HEADER : BEGIN -->
<tr>
<td bgcolor="#4AC694">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr>
<td style="margin-left: -10px;padding: 10px 40px 10px 40px; text-align: center;"> <img src="https://www.kavitareader.com/img/email/logo-256.png" alt="kavita_logo" width="75" style="height:auto;display:inline-block;vertical-align:middle;" />
<div style="min-height:75px;line-height:75px;display:inline-block;vertical-align:middle;color:#fff;font-family: 'Spartan', sans-serif;font-size:2rem;font-weight:700;">Kavita</div>
</td>
</tr>
</table>
</td>
</tr>
<!-- HEADER : END -->
<!-- HERO : BEGIN -->
<tr>
<td bgcolor="#fff" align="center" valign="top" style="text-align: center; background-position: center center !important; background-size: cover !important;">
<!--[if gte mso 9]>
<v:rect xmlns:v="urn:schemas-microsoft-com:vml" fill="true" stroke="false" style="width:680px; height:380px; background-position: center center !important;">
<v:fill type="tile" src="background.png" color="#222222" />
<v:textbox inset="0,0,0,0">
<![endif]-->
<div>
<!--[if mso]>
<table role="presentation" border="0" cellspacing="0" cellpadding="0" align="center" width="500">
<tr>
<td align="center" valign="middle" width="500">
<![endif]-->
<table role="presentation" border="0" cellpadding="0" cellspacing="0" align="center" width="100%" style="max-width:500px; margin: auto;">
<tr>
<td height="20" style="font-size:20px; line-height:20px;">&nbsp;</td>
</tr>
<tr>
<td align="center" valign="middle">
<table>
<tr>
<td valign="top" style="text-align: center; padding: 20px 0 10px 20px;">
<h1 style="margin: 0; font-family: 'Montserrat', sans-serif; font-size: 30px; line-height: 36px; font-weight: bold;">You've Been Invited</h1> </td>
</tr>
<tr>
<td valign="top" style="text-align: center; padding: 10px 20px 15px 20px; font-family: sans-serif; font-size: 18px; line-height: 20px;">
<p style="margin: 0;">You have been invited to {{InvitingUser}}'s Kavita instance.</p>
</td>
</tr>
<tr>
<td valign="top" style="text-align: center; padding: 10px 20px 15px 20px; font-family: sans-serif; font-size: 18px; line-height: 20px;">
<p style="margin: 0;">Please click the following link to setup an account for yourself and start reading.</p>
</td>
</tr>
<tr>
<td valign="top" align="center" style="text-align: center; padding: 15px 0px 20px 0px;">
<!-- Button : BEGIN -->
<center>
<table role="presentation" align="center" cellspacing="0" cellpadding="0" border="0" class="center-on-narrow" style="text-align: center;">
<tr>
<td style="border-radius: 50px; background: #153643; text-align: center;" class="button-td">
<a href="{{Link}}" style="background: #153643; border: 15px solid #153643; font-family: 'Montserrat', sans-serif; font-size: 14px; line-height: 1.1; text-align: center; text-decoration: none; display: block; border-radius: 50px; font-weight: bold;" class="button-a"> <span style="color:#ffffff;" class="button-link">&nbsp;&nbsp;&nbsp;&nbsp;ACCEPT INVITE&nbsp;&nbsp;&nbsp;&nbsp;</span> </a>
</td>
</tr>
</table>
</center>
<!-- Button : END -->
<tr>
<td valign="top" style="text-align: center; padding: 10px 20px 15px 20px; font-family: sans-serif; font-size: 12px; line-height: 20px;">
<p style="margin: 0;">If the button above does not work, please find the link here: <a style="color:inherit;margin: 0;width: 100%;word-break: break-all;" href="{{Link}}">{{Link}}</a></p>
</td>
</tr>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td height="20" style="font-size:20px; line-height:20px;">&nbsp;</td>
</tr>
</table>
<!--[if mso]>
</td>
</tr>
</table>
<![endif]-->
</div>
<!--[if gte mso 9]>
</v:textbox>
</v:rect>
<![endif]-->
</td>
</tr>
<!-- HERO : END -->
<!-- SOCIAL : BEGIN -->
<tr>
<td bgcolor="#292828">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr>
<td style="padding: 15px 30px; text-align: center;">
<table align="center" style="text-align: center;">
<tr>
<td>
<a href="https://discord.gg/b52wT37kt7"><img style="width:25px" src="https://www.kavitareader.com/img/email/discord-white.png" width="" height="" style="margin:0; padding:0; border:none; display:block;" border="0" alt="Discord"></a>
</td>
<td width="20">&nbsp;</td>
<td>
<a href="https://www.reddit.com/r/KavitaManga/"><img style="width:25px" src="https://www.kavitareader.com/img/email/reddit-white.png" width="" height="" style="margin:0; padding:0; border:none; display:block;" border="0" alt="Reddit"></a>
</td>
<td width="20">&nbsp;</td>
<td>
<a href="https://github.com/Kareadita/Kavita/"><img style="width:25px" src="https://www.kavitareader.com/img/email/github-white.png" width="" height="" style="margin:0; padding:0; border:none; display:block;" border="0" alt="Github"></a>
</td>
<td width="20">&nbsp;</td>
<td>
<a href="https://opencollective.com/kavita"><img style="width:25px" src="https://www.kavitareader.com/img/email/open-collective-white.png" width="" height="" style="margin:0; padding:0; border:none; display:block;" border="0" alt="Open Collective"></a>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
<!-- SOCIAL : END -->
</table>
<!-- Email Body : END -->
<!--[if mso]>
</td>
</tr>
</table>
<![endif]-->
</div>
</center>
</body>
</html>

View File

@ -0,0 +1,343 @@
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
<head>
<meta charset="utf-8">
<!-- utf-8 works for most cases -->
<meta name="viewport" content="width=device-width">
<!-- Forcing initial-scale shouldn't be necessary -->
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<!-- Use the latest (edge) version of IE rendering engine -->
<meta name="x-apple-disable-message-reformatting">
<!-- Disable auto-scale in iOS 10 Mail entirely -->
<title>Kavita - [Plain HTML]</title>
<!-- The title tag shows in email notifications, like Android 4.4. -->
<!-- Web Font / @font-face : BEGIN -->
<!-- NOTE: If web fonts are not required, lines 10 - 27 can be safely removed. -->
<!-- Desktop Outlook chokes on web font references and defaults to Times New Roman, so we force a safe fallback font. -->
<!--[if mso]>
<style>
* {
font-family: Arial, sans-serif !important;
}
</style>
<![endif]-->
<!-- All other clients get the webfont reference; some will render the font and others will silently fail to the fallbacks. More on that here: http://stylecampaign.com/blog/2015/02/webfont-support-in-email/ -->
<!--[if !mso]><!-->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Spartan:wght@500;700&display=swap" rel="stylesheet">
<link rel="stylesheet" type="text/css" href="https://use.fontawesome.com/releases/v5.6.3/css/all.css" integrity="sha384-UHRtZLI+pbxtHCWp1t77Bi1L4ZtiqrqD80Kn4Z8NTSRyMA2Fd33n5dQ8lWUE00s/" crossorigin="anonymous">
<!--<![endif]-->
<!-- Web Font / @font-face : END -->
<!-- CSS Reset -->
<style>
@import url(https://fonts.googleapis.com/css2?family=Spartan:wght@500;700&display=swap);
/* What it does: Remove spaces around the email design added by some email clients. */
/* Beware: It can remove the padding / margin and add a background color to the compose a reply window. */
html,
body {
margin: 0 auto !important;
padding: 0 !important;
height: 100% !important;
width: 100% !important;
}
/* What it does: Stops email clients resizing small text. */
* {
-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%;
}
/* What it does: Centers email on Android 4.4 */
div[style*="margin: 16px 0"] {
margin: 0 !important;
}
/* What it does: Stops Outlook from adding extra spacing to tables. */
table,
td {
mso-table-lspace: 0pt !important;
mso-table-rspace: 0pt !important;
}
/* What it does: Fixes webkit padding issue. Fix for Yahoo mail table alignment bug. Applies table-layout to the first 2 tables then removes for anything nested deeper. */
table {
border-spacing: 0 !important;
border-collapse: collapse !important;
table-layout: fixed !important;
margin: 0 auto !important;
}
table table table {
table-layout: auto;
}
i {
color: #fff;
font-size: 26px;
}
/* What it does: Uses a better rendering method when resizing images in IE. */
img {
-ms-interpolation-mode: bicubic;
}
/* What it does: A work-around for email clients meddling in triggered links. */
*[x-apple-data-detectors],
/* iOS */
.x-gmail-data-detectors,
/* Gmail */
.x-gmail-data-detectors *,
.aBn {
border-bottom: 0 !important;
cursor: default !important;
color: inherit !important;
text-decoration: none !important;
font-size: inherit !important;
font-family: inherit !important;
font-weight: inherit !important;
line-height: inherit !important;
}
/* What it does: Prevents Gmail from displaying an download button on large, non-linked images. */
.a6S {
display: none !important;
opacity: 0.01 !important;
}
/* If the above doesn't work, add a .g-img class to any image in question. */
img.g-img + div {
display: none !important;
}
/* What it does: Prevents underlining the button text in Windows 10 */
.button-link {
text-decoration: none !important;
}
/* What it does: Removes right gutter in Gmail iOS app: https://github.com/TedGoas/Cerberus/issues/89 */
/* Create one of these media queries for each additional viewport size you'd like to fix */
/* Thanks to Eric Lepetit @ericlepetitsf) for help troubleshooting */
@media only screen and (min-device-width: 375px) and (max-device-width: 413px) {
/* iPhone 6 and 6+ */
.email-container {
min-width: 375px !important;
}
}
</style>
<!-- Progressive Enhancements -->
<style>
/* What it does: Hover styles for buttons */
.button-td,
.button-a {
transition: all 100ms ease-in;
}
.button-td:hover,
.button-a:hover {
background: #000000 !important;
border-color: #000000 !important;
color: white !important;
}
/* Media Queries */
@media screen and (max-width: 480px) {
/* What it does: Forces elements to resize to the full width of their container. Useful for resizing images beyond their max-width. */
.fluid {
width: 100% !important;
max-width: 100% !important;
height: auto !important;
margin-left: auto !important;
margin-right: auto !important;
}
/* What it does: Forces table cells into full-width rows. */
.stack-column,
.stack-column-center {
display: block !important;
width: 100% !important;
max-width: 100% !important;
direction: ltr !important;
}
/* And center justify these ones. */
.stack-column-center {
text-align: center !important;
}
/* What it does: Generic utility class for centering. Useful for images, buttons, and nested tables. */
.center-on-narrow {
text-align: center !important;
display: block !important;
margin-left: auto !important;
margin-right: auto !important;
float: none !important;
}
table.center-on-narrow {
display: inline-block !important;
}
/* What it does: Adjust typography on small screens to improve readability */
.email-container p {
font-size: 17px !important;
line-height: 22px !important;
}
}
</style>
<!-- What it does: Makes background images in 72ppi Outlook render at correct size. -->
<!--[if gte mso 9]>
<xml>
<o:OfficeDocumentSettings>
<o:AllowPNG/>
<o:PixelsPerInch>96</o:PixelsPerInch>
</o:OfficeDocumentSettings>
</xml>
<![endif]-->
</head>
<body width="100%" bgcolor="#F1F1F1" style="margin: 0; mso-line-height-rule: exactly;">
<center style="width: 100%; background: #F1F1F1; text-align: left;">
<!-- Visually Hidden Preheader Text : BEGIN -->
<div style="display:none;font-size:1px;line-height:1px;max-height:0px;max-width:0px;opacity:0;overflow:hidden;mso-hide:all;font-family: sans-serif;"> Email confirmation is required for continued access. Click the button to confirm your email. </div>
<!-- Visually Hidden Preheader Text : END -->
<!--
Set the email width. Defined in two places:
1. max-width for all clients except Desktop Windows Outlook, allowing the email to squish on narrow but never go wider than 680px.
2. MSO tags for Desktop Windows Outlook enforce a 680px width.
Note: The Fluid and Responsive templates have a different width (600px). The hybrid grid is more "fragile", and I've found that 680px is a good width. Change with caution.
-->
<div style="max-width: 680px; margin: auto;" class="email-container">
<!--[if mso]>
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="680" align="center">
<tr>
<td>
<![endif]-->
<!-- Email Body : BEGIN -->
<table role="presentation" cellspacing="0" cellpadding="0" border="0" align="center" width="100%" style="max-width: 680px;" class="email-container">
<!-- HEADER : BEGIN -->
<tr>
<td bgcolor="#4AC694">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr>
<td style="margin-left: -10px;padding: 10px 40px 10px 40px; text-align: center;"> <img src="https://www.kavitareader.com/img/email/logo-256.png" alt="kavita_logo" width="75" style="height:auto;display:inline-block;vertical-align:middle;" />
<div style="min-height:75px;line-height:75px;display:inline-block;vertical-align:middle;color:#fff;font-family: 'Spartan', sans-serif;font-size:2rem;font-weight:700;">Kavita</div>
</td>
</tr>
</table>
</td>
</tr>
<!-- HEADER : END -->
<!-- HERO : BEGIN -->
<tr>
<td bgcolor="#fff" align="center" valign="top" style="text-align: center; background-position: center center !important; background-size: cover !important;">
<!--[if gte mso 9]>
<v:rect xmlns:v="urn:schemas-microsoft-com:vml" fill="true" stroke="false" style="width:680px; height:380px; background-position: center center !important;">
<v:fill type="tile" src="background.png" color="#222222" />
<v:textbox inset="0,0,0,0">
<![endif]-->
<div>
<!--[if mso]>
<table role="presentation" border="0" cellspacing="0" cellpadding="0" align="center" width="500">
<tr>
<td align="center" valign="middle" width="500">
<![endif]-->
<table role="presentation" border="0" cellpadding="0" cellspacing="0" align="center" width="100%" style="max-width:500px; margin: auto;">
<tr>
<td height="20" style="font-size:20px; line-height:20px;">&nbsp;</td>
</tr>
<tr>
<td align="center" valign="middle">
<table>
<tr>
<td valign="top" style="text-align: center; padding: 20px 0 10px 20px;">
<h1 style="margin: 0; font-family: 'Montserrat', sans-serif; font-size: 30px; line-height: 36px; font-weight: bold;">Confirmation Required</h1> </td>
</tr>
<tr>
<td valign="top" style="text-align: center; padding: 10px 20px 15px 20px; font-family: sans-serif; font-size: 18px; line-height: 20px;">
<p style="margin: 0;">Please click the following link to confirm your email for {{User}}</p>
</td>
</tr>
<tr>
<td valign="top" align="center" style="text-align: center; padding: 15px 0px 20px 0px;">
<!-- Button : BEGIN -->
<center>
<table role="presentation" align="center" cellspacing="0" cellpadding="0" border="0" class="center-on-narrow" style="text-align: center;">
<tr>
<td style="border-radius: 50px; background: #153643; text-align: center;" class="button-td">
<a href="{{Link}}" style="background: #153643; border: 15px solid #153643; font-family: 'Montserrat', sans-serif; font-size: 14px; line-height: 1.1; text-align: center; text-decoration: none; display: block; border-radius: 50px; font-weight: bold;" class="button-a"> <span style="color:#ffffff;" class="button-link">&nbsp;&nbsp;&nbsp;&nbsp;CONFIRM&nbsp;&nbsp;&nbsp;&nbsp;</span> </a>
</td>
</tr>
</table>
</center>
<!-- Button : END -->
<tr>
<td valign="top" style="text-align: center; padding: 10px 20px 15px 20px; font-family: sans-serif; font-size: 12px; line-height: 20px;">
<p style="margin: 0;">If the button above does not work, please find the link here: <a style="color:inherit;margin: 0;width: 100%;word-break: break-all;" href="{{Link}}">{{Link}}</a></p>
</td>
</tr>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td height="20" style="font-size:20px; line-height:20px;">&nbsp;</td>
</tr>
</table>
<!--[if mso]>
</td>
</tr>
</table>
<![endif]-->
</div>
<!--[if gte mso 9]>
</v:textbox>
</v:rect>
<![endif]-->
</td>
</tr>
<!-- HERO : END -->
<!-- SOCIAL : BEGIN -->
<tr>
<td bgcolor="#292828">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr>
<td style="padding: 15px 30px; text-align: center;">
<table align="center" style="text-align: center;">
<tr>
<td>
<a href="https://discord.gg/b52wT37kt7"><img style="width:25px" src="https://www.kavitareader.com/img/email/discord-white.png" width="" height="" style="margin:0; padding:0; border:none; display:block;" border="0" alt="Discord"></a>
</td>
<td width="20">&nbsp;</td>
<td>
<a href="https://www.reddit.com/r/KavitaManga/"><img style="width:25px" src="https://www.kavitareader.com/img/email/reddit-white.png" width="" height="" style="margin:0; padding:0; border:none; display:block;" border="0" alt="Reddit"></a>
</td>
<td width="20">&nbsp;</td>
<td>
<a href="https://github.com/Kareadita/Kavita/"><img style="width:25px" src="https://www.kavitareader.com/img/email/github-white.png" width="" height="" style="margin:0; padding:0; border:none; display:block;" border="0" alt="Github"></a>
</td>
<td width="20">&nbsp;</td>
<td>
<a href="https://opencollective.com/kavita"><img style="width:25px" src="https://www.kavitareader.com/img/email/open-collective-white.png" width="" height="" style="margin:0; padding:0; border:none; display:block;" border="0" alt="Open Collective"></a>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
<!-- SOCIAL : END -->
</table>
<!-- Email Body : END -->
<!--[if mso]>
</td>
</tr>
</table>
<![endif]-->
</div>
</center>
</body>
</html>

View File

@ -0,0 +1,348 @@
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
<head>
<meta charset="utf-8">
<!-- utf-8 works for most cases -->
<meta name="viewport" content="width=device-width">
<!-- Forcing initial-scale shouldn't be necessary -->
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<!-- Use the latest (edge) version of IE rendering engine -->
<meta name="x-apple-disable-message-reformatting">
<!-- Disable auto-scale in iOS 10 Mail entirely -->
<title>Kavita - [Plain HTML]</title>
<!-- The title tag shows in email notifications, like Android 4.4. -->
<!-- Web Font / @font-face : BEGIN -->
<!-- NOTE: If web fonts are not required, lines 10 - 27 can be safely removed. -->
<!-- Desktop Outlook chokes on web font references and defaults to Times New Roman, so we force a safe fallback font. -->
<!--[if mso]>
<style>
* {
font-family: Arial, sans-serif !important;
}
</style>
<![endif]-->
<!-- All other clients get the webfont reference; some will render the font and others will silently fail to the fallbacks. More on that here: http://stylecampaign.com/blog/2015/02/webfont-support-in-email/ -->
<!--[if !mso]><!-->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Spartan:wght@500;700&display=swap" rel="stylesheet">
<link rel="stylesheet" type="text/css" href="https://use.fontawesome.com/releases/v5.6.3/css/all.css" integrity="sha384-UHRtZLI+pbxtHCWp1t77Bi1L4ZtiqrqD80Kn4Z8NTSRyMA2Fd33n5dQ8lWUE00s/" crossorigin="anonymous">
<!--<![endif]-->
<!-- Web Font / @font-face : END -->
<!-- CSS Reset -->
<style>
@import url(https://fonts.googleapis.com/css2?family=Spartan:wght@500;700&display=swap);
/* What it does: Remove spaces around the email design added by some email clients. */
/* Beware: It can remove the padding / margin and add a background color to the compose a reply window. */
html,
body {
margin: 0 auto !important;
padding: 0 !important;
height: 100% !important;
width: 100% !important;
}
/* What it does: Stops email clients resizing small text. */
* {
-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%;
}
/* What it does: Centers email on Android 4.4 */
div[style*="margin: 16px 0"] {
margin: 0 !important;
}
/* What it does: Stops Outlook from adding extra spacing to tables. */
table,
td {
mso-table-lspace: 0pt !important;
mso-table-rspace: 0pt !important;
}
/* What it does: Fixes webkit padding issue. Fix for Yahoo mail table alignment bug. Applies table-layout to the first 2 tables then removes for anything nested deeper. */
table {
border-spacing: 0 !important;
border-collapse: collapse !important;
table-layout: fixed !important;
margin: 0 auto !important;
}
table table table {
table-layout: auto;
}
i {
color: #fff;
font-size: 26px;
}
/* What it does: Uses a better rendering method when resizing images in IE. */
img {
-ms-interpolation-mode: bicubic;
}
/* What it does: A work-around for email clients meddling in triggered links. */
*[x-apple-data-detectors],
/* iOS */
.x-gmail-data-detectors,
/* Gmail */
.x-gmail-data-detectors *,
.aBn {
border-bottom: 0 !important;
cursor: default !important;
color: inherit !important;
text-decoration: none !important;
font-size: inherit !important;
font-family: inherit !important;
font-weight: inherit !important;
line-height: inherit !important;
}
/* What it does: Prevents Gmail from displaying an download button on large, non-linked images. */
.a6S {
display: none !important;
opacity: 0.01 !important;
}
/* If the above doesn't work, add a .g-img class to any image in question. */
img.g-img + div {
display: none !important;
}
/* What it does: Prevents underlining the button text in Windows 10 */
.button-link {
text-decoration: none !important;
}
/* What it does: Removes right gutter in Gmail iOS app: https://github.com/TedGoas/Cerberus/issues/89 */
/* Create one of these media queries for each additional viewport size you'd like to fix */
/* Thanks to Eric Lepetit @ericlepetitsf) for help troubleshooting */
@media only screen and (min-device-width: 375px) and (max-device-width: 413px) {
/* iPhone 6 and 6+ */
.email-container {
min-width: 375px !important;
}
}
</style>
<!-- Progressive Enhancements -->
<style>
/* What it does: Hover styles for buttons */
.button-td,
.button-a {
transition: all 100ms ease-in;
}
.button-td:hover,
.button-a:hover {
background: #000000 !important;
border-color: #000000 !important;
color: white !important;
}
/* Media Queries */
@media screen and (max-width: 480px) {
/* What it does: Forces elements to resize to the full width of their container. Useful for resizing images beyond their max-width. */
.fluid {
width: 100% !important;
max-width: 100% !important;
height: auto !important;
margin-left: auto !important;
margin-right: auto !important;
}
/* What it does: Forces table cells into full-width rows. */
.stack-column,
.stack-column-center {
display: block !important;
width: 100% !important;
max-width: 100% !important;
direction: ltr !important;
}
/* And center justify these ones. */
.stack-column-center {
text-align: center !important;
}
/* What it does: Generic utility class for centering. Useful for images, buttons, and nested tables. */
.center-on-narrow {
text-align: center !important;
display: block !important;
margin-left: auto !important;
margin-right: auto !important;
float: none !important;
}
table.center-on-narrow {
display: inline-block !important;
}
/* What it does: Adjust typography on small screens to improve readability */
.email-container p {
font-size: 17px !important;
line-height: 22px !important;
}
}
</style>
<!-- What it does: Makes background images in 72ppi Outlook render at correct size. -->
<!--[if gte mso 9]>
<xml>
<o:OfficeDocumentSettings>
<o:AllowPNG/>
<o:PixelsPerInch>96</o:PixelsPerInch>
</o:OfficeDocumentSettings>
</xml>
<![endif]-->
</head>
<body width="100%" bgcolor="#F1F1F1" style="margin: 0; mso-line-height-rule: exactly;">
<center style="width: 100%; background: #F1F1F1; text-align: left;">
<!-- Visually Hidden Preheader Text : BEGIN -->
<div style="display:none;font-size:1px;line-height:1px;max-height:0px;max-width:0px;opacity:0;overflow:hidden;mso-hide:all;font-family: sans-serif;"> Email confirmation is required for continued access. Click the button to confirm your email. </div>
<!-- Visually Hidden Preheader Text : END -->
<!--
Set the email width. Defined in two places:
1. max-width for all clients except Desktop Windows Outlook, allowing the email to squish on narrow but never go wider than 680px.
2. MSO tags for Desktop Windows Outlook enforce a 680px width.
Note: The Fluid and Responsive templates have a different width (600px). The hybrid grid is more "fragile", and I've found that 680px is a good width. Change with caution.
-->
<div style="max-width: 680px; margin: auto;" class="email-container">
<!--[if mso]>
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="680" align="center">
<tr>
<td>
<![endif]-->
<!-- Email Body : BEGIN -->
<table role="presentation" cellspacing="0" cellpadding="0" border="0" align="center" width="100%" style="max-width: 680px;" class="email-container">
<!-- HEADER : BEGIN -->
<tr>
<td bgcolor="#4AC694">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr>
<td style="margin-left: -10px;padding: 10px 40px 10px 40px; text-align: center;"> <img src="https://www.kavitareader.com/img/email/logo-256.png" alt="kavita_logo" width="75" style="height:auto;display:inline-block;vertical-align:middle;" />
<div style="min-height:75px;line-height:75px;display:inline-block;vertical-align:middle;color:#fff;font-family: 'Spartan', sans-serif;font-size:2rem;font-weight:700;">Kavita</div>
</td>
</tr>
</table>
</td>
</tr>
<!-- HEADER : END -->
<!-- HERO : BEGIN -->
<tr>
<td bgcolor="#fff" align="center" valign="top" style="text-align: center; background-position: center center !important; background-size: cover !important;">
<!--[if gte mso 9]>
<v:rect xmlns:v="urn:schemas-microsoft-com:vml" fill="true" stroke="false" style="width:680px; height:380px; background-position: center center !important;">
<v:fill type="tile" src="background.png" color="#222222" />
<v:textbox inset="0,0,0,0">
<![endif]-->
<div>
<!--[if mso]>
<table role="presentation" border="0" cellspacing="0" cellpadding="0" align="center" width="500">
<tr>
<td align="center" valign="middle" width="500">
<![endif]-->
<table role="presentation" border="0" cellpadding="0" cellspacing="0" align="center" width="100%" style="max-width:500px; margin: auto;">
<tr>
<td height="20" style="font-size:20px; line-height:20px;">&nbsp;</td>
</tr>
<tr>
<td align="center" valign="middle">
<table>
<tr>
<td valign="top" style="text-align: center; padding: 20px 0 10px 20px;">
<h1 style="margin: 0; font-family: 'Montserrat', sans-serif; font-size: 30px; line-height: 36px; font-weight: bold;">Forgot your password?</h1> </td>
</tr>
<tr>
<td valign="top" style="text-align: center; padding: 10px 20px 15px 20px; font-family: sans-serif; font-size: 18px; line-height: 20px;">
<p style="margin: 0;">That's okay, it happens! Click on the button below to reset your password.</p>
</td>
</tr>
<tr>
<td valign="top" align="center" style="text-align: center; padding: 15px 0px 20px 0px;">
<!-- Button : BEGIN -->
<center>
<table role="presentation" align="center" cellspacing="0" cellpadding="0" border="0" class="center-on-narrow" style="text-align: center;">
<tr>
<td style="border-radius: 50px; background: #153643; text-align: center;" class="button-td">
<a href="{{Link}}" style="background: #153643; border: 15px solid #153643; font-family: 'Montserrat', sans-serif; font-size: 14px; line-height: 1.1; text-align: center; text-decoration: none; display: block; border-radius: 50px; font-weight: bold;" class="button-a"> <span style="color:#ffffff;" class="button-link">&nbsp;&nbsp;&nbsp;&nbsp;RESET YOUR PASSWORD&nbsp;&nbsp;&nbsp;&nbsp;</span> </a>
</td>
</tr>
</table>
</center>
<!-- Button : END -->
<tr>
<td valign="top" style="text-align: center; padding: 0px 20px 0px 20px; font-family: sans-serif; font-size: 12px; line-height: 20px;">
<p style="margin: 0;">If you did not perform this action, ignore this email.</p>
</td>
</tr>
<tr>
<td valign="top" style="text-align: center; padding: 10px 20px 15px 20px; font-family: sans-serif; font-size: 12px; line-height: 20px;">
<p style="margin: 0;">If the button above does not work, please find the link here: <a style="color:inherit;margin: 0;width: 100%;word-break: break-all;" href="{{Link}}">{{Link}}</a></p>
</td>
</tr>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td height="20" style="font-size:20px; line-height:20px;">&nbsp;</td>
</tr>
</table>
<!--[if mso]>
</td>
</tr>
</table>
<![endif]-->
</div>
<!--[if gte mso 9]>
</v:textbox>
</v:rect>
<![endif]-->
</td>
</tr>
<!-- HERO : END -->
<!-- SOCIAL : BEGIN -->
<tr>
<td bgcolor="#292828">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr>
<td style="padding: 15px 30px; text-align: center;">
<table align="center" style="text-align: center;">
<tr>
<td>
<a href="https://discord.gg/b52wT37kt7"><img style="width:25px" src="https://www.kavitareader.com/img/email/discord-white.png" width="" height="" style="margin:0; padding:0; border:none; display:block;" border="0" alt="Discord"></a>
</td>
<td width="20">&nbsp;</td>
<td>
<a href="https://www.reddit.com/r/KavitaManga/"><img style="width:25px" src="https://www.kavitareader.com/img/email/reddit-white.png" width="" height="" style="margin:0; padding:0; border:none; display:block;" border="0" alt="Reddit"></a>
</td>
<td width="20">&nbsp;</td>
<td>
<a href="https://github.com/Kareadita/Kavita/"><img style="width:25px" src="https://www.kavitareader.com/img/email/github-white.png" width="" height="" style="margin:0; padding:0; border:none; display:block;" border="0" alt="Github"></a>
</td>
<td width="20">&nbsp;</td>
<td>
<a href="https://opencollective.com/kavita"><img style="width:25px" src="https://www.kavitareader.com/img/email/open-collective-white.png" width="" height="" style="margin:0; padding:0; border:none; display:block;" border="0" alt="Open Collective"></a>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
<!-- SOCIAL : END -->
</table>
<!-- Email Body : END -->
<!--[if mso]>
</td>
</tr>
</table>
<![endif]-->
</div>
</center>
</body>
</html>

View File

@ -0,0 +1,325 @@
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
<head>
<meta charset="utf-8">
<!-- utf-8 works for most cases -->
<meta name="viewport" content="width=device-width">
<!-- Forcing initial-scale shouldn't be necessary -->
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<!-- Use the latest (edge) version of IE rendering engine -->
<meta name="x-apple-disable-message-reformatting">
<!-- Disable auto-scale in iOS 10 Mail entirely -->
<title>Event - [Plain HTML]</title>
<!-- The title tag shows in email notifications, like Android 4.4. -->
<!-- Web Font / @font-face : BEGIN -->
<!-- NOTE: If web fonts are not required, lines 10 - 27 can be safely removed. -->
<!-- Desktop Outlook chokes on web font references and defaults to Times New Roman, so we force a safe fallback font. -->
<!--[if mso]>
<style>
* {
font-family: Arial, sans-serif !important;
}
</style>
<![endif]-->
<!-- All other clients get the webfont reference; some will render the font and others will silently fail to the fallbacks. More on that here: http://stylecampaign.com/blog/2015/02/webfont-support-in-email/ -->
<!--[if !mso]><!-->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Spartan:wght@500;700&display=swap" rel="stylesheet">
<link rel="stylesheet" type="text/css" href="https://use.fontawesome.com/releases/v5.6.3/css/all.css" integrity="sha384-UHRtZLI+pbxtHCWp1t77Bi1L4ZtiqrqD80Kn4Z8NTSRyMA2Fd33n5dQ8lWUE00s/" crossorigin="anonymous">
<!--<![endif]-->
<!-- Web Font / @font-face : END -->
<!-- CSS Reset -->
<style>
@import url('https://fonts.googleapis.com/css2?family=Spartan:wght@500;700&display=swap');
/* What it does: Remove spaces around the email design added by some email clients. */
/* Beware: It can remove the padding / margin and add a background color to the compose a reply window. */
html,
body {
margin: 0 auto !important;
padding: 0 !important;
height: 100% !important;
width: 100% !important;
}
/* What it does: Stops email clients resizing small text. */
* {
-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%;
}
/* What it does: Centers email on Android 4.4 */
div[style*="margin: 16px 0"] {
margin: 0 !important;
}
/* What it does: Stops Outlook from adding extra spacing to tables. */
table,
td {
mso-table-lspace: 0pt !important;
mso-table-rspace: 0pt !important;
}
/* What it does: Fixes webkit padding issue. Fix for Yahoo mail table alignment bug. Applies table-layout to the first 2 tables then removes for anything nested deeper. */
table {
border-spacing: 0 !important;
border-collapse: collapse !important;
table-layout: fixed !important;
margin: 0 auto !important;
}
table table table {
table-layout: auto;
}
i {
color: #fff;
font-size: 26px;
}
/* What it does: Uses a better rendering method when resizing images in IE. */
img {
-ms-interpolation-mode: bicubic;
}
/* What it does: A work-around for email clients meddling in triggered links. */
*[x-apple-data-detectors],
/* iOS */
.x-gmail-data-detectors,
/* Gmail */
.x-gmail-data-detectors *,
.aBn {
border-bottom: 0 !important;
cursor: default !important;
color: inherit !important;
text-decoration: none !important;
font-size: inherit !important;
font-family: inherit !important;
font-weight: inherit !important;
line-height: inherit !important;
}
/* What it does: Prevents Gmail from displaying an download button on large, non-linked images. */
.a6S {
display: none !important;
opacity: 0.01 !important;
}
/* If the above doesn't work, add a .g-img class to any image in question. */
img.g-img + div {
display: none !important;
}
/* What it does: Prevents underlining the button text in Windows 10 */
.button-link {
text-decoration: none !important;
}
/* What it does: Removes right gutter in Gmail iOS app: https://github.com/TedGoas/Cerberus/issues/89 */
/* Create one of these media queries for each additional viewport size you'd like to fix */
/* Thanks to Eric Lepetit @ericlepetitsf) for help troubleshooting */
@media only screen and (min-device-width: 375px) and (max-device-width: 413px) {
/* iPhone 6 and 6+ */
.email-container {
min-width: 375px !important;
}
}
</style>
<!-- Progressive Enhancements -->
<style>
/* What it does: Hover styles for buttons */
.button-td,
.button-a {
transition: all 100ms ease-in;
}
.button-td:hover,
.button-a:hover {
background: #000000 !important;
border-color: #000000 !important;
color: white !important;
}
/* Media Queries */
@media screen and (max-width: 480px) {
/* What it does: Forces elements to resize to the full width of their container. Useful for resizing images beyond their max-width. */
.fluid {
width: 100% !important;
max-width: 100% !important;
height: auto !important;
margin-left: auto !important;
margin-right: auto !important;
}
/* What it does: Forces table cells into full-width rows. */
.stack-column,
.stack-column-center {
display: block !important;
width: 100% !important;
max-width: 100% !important;
direction: ltr !important;
}
/* And center justify these ones. */
.stack-column-center {
text-align: center !important;
}
/* What it does: Generic utility class for centering. Useful for images, buttons, and nested tables. */
.center-on-narrow {
text-align: center !important;
display: block !important;
margin-left: auto !important;
margin-right: auto !important;
float: none !important;
}
table.center-on-narrow {
display: inline-block !important;
}
/* What it does: Adjust typography on small screens to improve readability */
.email-container p {
font-size: 17px !important;
line-height: 22px !important;
}
}
</style>
<!-- What it does: Makes background images in 72ppi Outlook render at correct size. -->
<!--[if gte mso 9]>
<xml>
<o:OfficeDocumentSettings>
<o:AllowPNG/>
<o:PixelsPerInch>96</o:PixelsPerInch>
</o:OfficeDocumentSettings>
</xml>
<![endif]-->
</head>
<body width="100%" bgcolor="#F1F1F1" style="margin: 0; mso-line-height-rule: exactly;">
<center style="width: 100%; background: #F1F1F1; text-align: left;">
<!-- Visually Hidden Preheader Text : BEGIN -->
<div style="display:none;font-size:1px;line-height:1px;max-height:0px;max-width:0px;opacity:0;overflow:hidden;mso-hide:all;font-family: sans-serif;">
This is a Test Email
</div>
<!-- Visually Hidden Preheader Text : END -->
<!--
Set the email width. Defined in two places:
1. max-width for all clients except Desktop Windows Outlook, allowing the email to squish on narrow but never go wider than 680px.
2. MSO tags for Desktop Windows Outlook enforce a 680px width.
Note: The Fluid and Responsive templates have a different width (600px). The hybrid grid is more "fragile", and I've found that 680px is a good width. Change with caution.
-->
<div style="max-width: 680px; margin: auto;" class="email-container">
<!--[if mso]>
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="680" align="center">
<tr>
<td>
<![endif]-->
<!-- Email Body : BEGIN -->
<table role="presentation" cellspacing="0" cellpadding="0" border="0" align="center" width="100%" style="max-width: 680px;" class="email-container">
<!-- HEADER : BEGIN -->
<tr>
<td bgcolor="#4AC694">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr>
<td style="margin-left: -10px;padding: 10px 40px 10px 40px; text-align: center;"> <img src="https://www.kavitareader.com/img/email/logo-256.png" alt="kavita_logo" width="75" style="height:auto;display:inline-block;vertical-align:middle;" />
<div style="min-height:75px;line-height:75px;display:inline-block;vertical-align:middle;color:#fff;font-family: 'Spartan', sans-serif;font-size:2rem;font-weight:700;">Kavita</div>
</td>
</tr>
</table>
</td>
</tr>
<!-- HEADER : END -->
<!-- HERO : BEGIN -->
<tr>
<td bgcolor="#fff" align="center" valign="top" style="text-align: center; background-position: center center !important; background-size: cover !important;">
<!--[if gte mso 9]>
<v:rect xmlns:v="urn:schemas-microsoft-com:vml" fill="true" stroke="false" style="width:680px; height:380px; background-position: center center !important;">
<v:fill type="tile" src="background.png" color="#222222" />
<v:textbox inset="0,0,0,0">
<![endif]-->
<div>
<!--[if mso]>
<table role="presentation" border="0" cellspacing="0" cellpadding="0" align="center" width="500">
<tr>
<td align="center" valign="middle" width="500">
<![endif]-->
<table role="presentation" border="0" cellpadding="0" cellspacing="0" align="center" width="100%" style="max-width:500px; margin: auto;">
<tr>
<td height="20" style="font-size:20px; line-height:20px;">&nbsp;</td>
</tr>
<tr>
<td align="center" valign="middle">
<table>
<tr>
<td valign="top" style="text-align: center; padding: 20px 0 10px 20px;">
<h1 style="margin: 0; font-family: 'Montserrat', sans-serif; font-size: 30px; line-height: 36px; font-weight: bold;">This is a Test Email</h1> </td>
</tr>
<tr>
<td valign="top" style="text-align: center; padding: 10px 20px 15px 20px; font-family: sans-serif; font-size: 18px; line-height: 20px;">
<p style="margin: 0;">If you've received this email, your KavitaEmail is setup</p>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td height="20" style="font-size:20px; line-height:20px;">&nbsp;</td>
</tr>
</table>
<!--[if mso]>
</td>
</tr>
</table>
<![endif]-->
</div>
<!--[if gte mso 9]>
</v:textbox>
</v:rect>
<![endif]-->
</td>
</tr>
<!-- HERO : END -->
<!-- SOCIAL : BEGIN -->
<tr>
<td bgcolor="#292828">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr>
<td style="padding: 15px 30px; text-align: center;">
<table align="center" style="text-align: center;">
<tr>
<td>
<a href="https://discord.gg/b52wT37kt7"><img style="width:25px" src="https://www.kavitareader.com/img/email/discord-white.png" width="" height="" style="margin:0; padding:0; border:none; display:block;" border="0" alt="Discord"></a>
</td>
<td width="20">&nbsp;</td>
<td>
<a href="https://www.reddit.com/r/KavitaManga/"><img style="width:25px" src="https://www.kavitareader.com/img/email/reddit-white.png" width="" height="" style="margin:0; padding:0; border:none; display:block;" border="0" alt="Reddit"></a>
</td>
<td width="20">&nbsp;</td>
<td>
<a href="https://github.com/Kareadita/Kavita/"><img style="width:25px" src="https://www.kavitareader.com/img/email/github-white.png" width="" height="" style="margin:0; padding:0; border:none; display:block;" border="0" alt="Github"></a>
</td>
<td width="20">&nbsp;</td>
<td>
<a href="https://opencollective.com/kavita"><img style="width:25px" src="https://www.kavitareader.com/img/email/open-collective-white.png" width="" height="" style="margin:0; padding:0; border:none; display:block;" border="0" alt="Open Collective"></a>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
<!-- SOCIAL : END -->
</table>
<!-- Email Body : END -->
<!--[if mso]>
</td>
</tr>
</table>
<![endif]-->
</div>
</center>
</body>
</html>

View File

@ -0,0 +1,323 @@
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
<head>
<meta charset="utf-8">
<!-- utf-8 works for most cases -->
<meta name="viewport" content="width=device-width">
<!-- Forcing initial-scale shouldn't be necessary -->
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<!-- Use the latest (edge) version of IE rendering engine -->
<meta name="x-apple-disable-message-reformatting">
<!-- Disable auto-scale in iOS 10 Mail entirely -->
<title>Event - [Plain HTML]</title>
<!-- The title tag shows in email notifications, like Android 4.4. -->
<!-- Web Font / @font-face : BEGIN -->
<!-- NOTE: If web fonts are not required, lines 10 - 27 can be safely removed. -->
<!-- Desktop Outlook chokes on web font references and defaults to Times New Roman, so we force a safe fallback font. -->
<!--[if mso]>
<style>
* {
font-family: Arial, sans-serif !important;
}
</style>
<![endif]-->
<!-- All other clients get the webfont reference; some will render the font and others will silently fail to the fallbacks. More on that here: http://stylecampaign.com/blog/2015/02/webfont-support-in-email/ -->
<!--[if !mso]><!-->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Spartan:wght@500;700&display=swap" rel="stylesheet">
<link rel="stylesheet" type="text/css" href="https://use.fontawesome.com/releases/v5.6.3/css/all.css" integrity="sha384-UHRtZLI+pbxtHCWp1t77Bi1L4ZtiqrqD80Kn4Z8NTSRyMA2Fd33n5dQ8lWUE00s/" crossorigin="anonymous">
<!--<![endif]-->
<!-- Web Font / @font-face : END -->
<!-- CSS Reset -->
<style>
@import url('https://fonts.googleapis.com/css2?family=Spartan:wght@500;700&display=swap');
/* What it does: Remove spaces around the email design added by some email clients. */
/* Beware: It can remove the padding / margin and add a background color to the compose a reply window. */
html,
body {
margin: 0 auto !important;
padding: 0 !important;
height: 100% !important;
width: 100% !important;
}
/* What it does: Stops email clients resizing small text. */
* {
-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%;
}
/* What it does: Centers email on Android 4.4 */
div[style*="margin: 16px 0"] {
margin: 0 !important;
}
/* What it does: Stops Outlook from adding extra spacing to tables. */
table,
td {
mso-table-lspace: 0pt !important;
mso-table-rspace: 0pt !important;
}
/* What it does: Fixes webkit padding issue. Fix for Yahoo mail table alignment bug. Applies table-layout to the first 2 tables then removes for anything nested deeper. */
table {
border-spacing: 0 !important;
border-collapse: collapse !important;
table-layout: fixed !important;
margin: 0 auto !important;
}
table table table {
table-layout: auto;
}
i {
color: #fff;
font-size: 26px;
}
/* What it does: Uses a better rendering method when resizing images in IE. */
img {
-ms-interpolation-mode: bicubic;
}
/* What it does: A work-around for email clients meddling in triggered links. */
*[x-apple-data-detectors],
/* iOS */
.x-gmail-data-detectors,
/* Gmail */
.x-gmail-data-detectors *,
.aBn {
border-bottom: 0 !important;
cursor: default !important;
color: inherit !important;
text-decoration: none !important;
font-size: inherit !important;
font-family: inherit !important;
font-weight: inherit !important;
line-height: inherit !important;
}
/* What it does: Prevents Gmail from displaying an download button on large, non-linked images. */
.a6S {
display: none !important;
opacity: 0.01 !important;
}
/* If the above doesn't work, add a .g-img class to any image in question. */
img.g-img + div {
display: none !important;
}
/* What it does: Prevents underlining the button text in Windows 10 */
.button-link {
text-decoration: none !important;
}
/* What it does: Removes right gutter in Gmail iOS app: https://github.com/TedGoas/Cerberus/issues/89 */
/* Create one of these media queries for each additional viewport size you'd like to fix */
/* Thanks to Eric Lepetit @ericlepetitsf) for help troubleshooting */
@media only screen and (min-device-width: 375px) and (max-device-width: 413px) {
/* iPhone 6 and 6+ */
.email-container {
min-width: 375px !important;
}
}
</style>
<!-- Progressive Enhancements -->
<style>
/* What it does: Hover styles for buttons */
.button-td,
.button-a {
transition: all 100ms ease-in;
}
.button-td:hover,
.button-a:hover {
background: #000000 !important;
border-color: #000000 !important;
color: white !important;
}
/* Media Queries */
@media screen and (max-width: 480px) {
/* What it does: Forces elements to resize to the full width of their container. Useful for resizing images beyond their max-width. */
.fluid {
width: 100% !important;
max-width: 100% !important;
height: auto !important;
margin-left: auto !important;
margin-right: auto !important;
}
/* What it does: Forces table cells into full-width rows. */
.stack-column,
.stack-column-center {
display: block !important;
width: 100% !important;
max-width: 100% !important;
direction: ltr !important;
}
/* And center justify these ones. */
.stack-column-center {
text-align: center !important;
}
/* What it does: Generic utility class for centering. Useful for images, buttons, and nested tables. */
.center-on-narrow {
text-align: center !important;
display: block !important;
margin-left: auto !important;
margin-right: auto !important;
float: none !important;
}
table.center-on-narrow {
display: inline-block !important;
}
/* What it does: Adjust typography on small screens to improve readability */
.email-container p {
font-size: 17px !important;
line-height: 22px !important;
}
}
</style>
<!-- What it does: Makes background images in 72ppi Outlook render at correct size. -->
<!--[if gte mso 9]>
<xml>
<o:OfficeDocumentSettings>
<o:AllowPNG/>
<o:PixelsPerInch>96</o:PixelsPerInch>
</o:OfficeDocumentSettings>
</xml>
<![endif]-->
</head>
<body width="100%" bgcolor="#F1F1F1" style="margin: 0; mso-line-height-rule: exactly;">
<center style="width: 100%; background: #F1F1F1; text-align: left;">
<!-- Visually Hidden Preheader Text : BEGIN -->
<div style="display:none;font-size:1px;line-height:1px;max-height:0px;max-width:0px;opacity:0;overflow:hidden;mso-hide:all;font-family: sans-serif;"> You've been sent a file from Kavita! </div>
<!-- Visually Hidden Preheader Text : END -->
<!--
Set the email width. Defined in two places:
1. max-width for all clients except Desktop Windows Outlook, allowing the email to squish on narrow but never go wider than 680px.
2. MSO tags for Desktop Windows Outlook enforce a 680px width.
Note: The Fluid and Responsive templates have a different width (600px). The hybrid grid is more "fragile", and I've found that 680px is a good width. Change with caution.
-->
<div style="max-width: 680px; margin: auto;" class="email-container">
<!--[if mso]>
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="680" align="center">
<tr>
<td>
<![endif]-->
<!-- Email Body : BEGIN -->
<table role="presentation" cellspacing="0" cellpadding="0" border="0" align="center" width="100%" style="max-width: 680px;" class="email-container">
<!-- HEADER : BEGIN -->
<tr>
<td bgcolor="#4AC694">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr>
<td style="margin-left: -10px;padding: 10px 40px 10px 40px; text-align: center;"> <img src="https://www.kavitareader.com/img/email/logo-256.png" alt="kavita_logo" width="75" style="height:auto;display:inline-block;vertical-align:middle;" />
<div style="min-height:75px;line-height:75px;display:inline-block;vertical-align:middle;color:#fff;font-family: 'Spartan', sans-serif;font-size:2rem;font-weight:700;">Kavita</div>
</td>
</tr>
</table>
</td>
</tr>
<!-- HEADER : END -->
<!-- HERO : BEGIN -->
<tr>
<td bgcolor="#fff" align="center" valign="top" style="text-align: center; background-position: center center !important; background-size: cover !important;">
<!--[if gte mso 9]>
<v:rect xmlns:v="urn:schemas-microsoft-com:vml" fill="true" stroke="false" style="width:680px; height:380px; background-position: center center !important;">
<v:fill type="tile" src="background.png" color="#222222" />
<v:textbox inset="0,0,0,0">
<![endif]-->
<div>
<!--[if mso]>
<table role="presentation" border="0" cellspacing="0" cellpadding="0" align="center" width="500">
<tr>
<td align="center" valign="middle" width="500">
<![endif]-->
<table role="presentation" border="0" cellpadding="0" cellspacing="0" align="center" width="100%" style="max-width:500px; margin: auto;">
<tr>
<td height="20" style="font-size:20px; line-height:20px;">&nbsp;</td>
</tr>
<tr>
<td align="center" valign="middle">
<table>
<tr>
<td valign="top" style="text-align: center; padding: 20px 0 10px 20px;">
<h1 style="margin: 0; font-family: 'Montserrat', sans-serif; font-size: 30px; line-height: 36px; font-weight: bold;">You sent a file from Kavita</h1> </td>
</tr>
<tr>
<td valign="top" style="text-align: center; padding: 10px 20px 15px 20px; font-family: sans-serif; font-size: 18px; line-height: 20px;">
<p style="margin: 0;">Please find attached the file(s) you've sent.</p>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td height="20" style="font-size:20px; line-height:20px;">&nbsp;</td>
</tr>
</table>
<!--[if mso]>
</td>
</tr>
</table>
<![endif]-->
</div>
<!--[if gte mso 9]>
</v:textbox>
</v:rect>
<![endif]-->
</td>
</tr>
<!-- HERO : END -->
<!-- SOCIAL : BEGIN -->
<tr>
<td bgcolor="#292828">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr>
<td style="padding: 15px 30px; text-align: center;">
<table align="center" style="text-align: center;">
<tr>
<td>
<a href="https://discord.gg/b52wT37kt7"><img style="width:25px" src="https://www.kavitareader.com/img/email/discord-white.png" width="" height="" style="margin:0; padding:0; border:none; display:block;" border="0" alt="Discord"></a>
</td>
<td width="20">&nbsp;</td>
<td>
<a href="https://www.reddit.com/r/KavitaManga/"><img style="width:25px" src="https://www.kavitareader.com/img/email/reddit-white.png" width="" height="" style="margin:0; padding:0; border:none; display:block;" border="0" alt="Reddit"></a>
</td>
<td width="20">&nbsp;</td>
<td>
<a href="https://github.com/Kareadita/Kavita/"><img style="width:25px" src="https://www.kavitareader.com/img/email/github-white.png" width="" height="" style="margin:0; padding:0; border:none; display:block;" border="0" alt="Github"></a>
</td>
<td width="20">&nbsp;</td>
<td>
<a href="https://opencollective.com/kavita"><img style="width:25px" src="https://www.kavitareader.com/img/email/open-collective-white.png" width="" height="" style="margin:0; padding:0; border:none; display:block;" border="0" alt="Open Collective"></a>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
<!-- SOCIAL : END -->
</table>
<!-- Email Body : END -->
<!--[if mso]>
</td>
</tr>
</table>
<![endif]-->
</div>
</center>
</body>
</html>

View File

@ -0,0 +1,10 @@
namespace Skeleton.DTOs;
public record ConfirmationEmailDto
{
public string InvitingUser { get; init; }
public string EmailAddress { get; init; }
public string ServerConfirmationLink { get; init; }
public string InstallId { get; init; }
}

View File

@ -0,0 +1,9 @@
namespace Skeleton.DTOs;
public class EmailMigrationDto
{
public string EmailAddress { get; init; }
public string ServerConfirmationLink { get; init; }
public string Username { get; init; }
public string InstallId { get; init; }
}

View File

@ -0,0 +1,15 @@
using System.Collections.Generic;
namespace Skeleton.DTOs;
public class EmailOptionsDto
{
public IList<string> ToEmails { get; set; }
public string Subject { get; set; }
public string Body { get; set; }
public IList<KeyValuePair<string, string>> PlaceHolders { get; set; }
/// <summary>
/// Filenames to attach
/// </summary>
public IList<string> Attachments { get; set; }
}

View File

@ -0,0 +1,8 @@
namespace Skeleton.DTOs;
public class PasswordResetDto
{
public string EmailAddress { get; init; }
public string ServerConfirmationLink { get; init; }
public string InstallId { get; init; }
}

View File

@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>

View File

@ -11,6 +11,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kavita.Common", "Kavita.Com
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "API.Benchmark", "API.Benchmark\API.Benchmark.csproj", "{3D781D18-2452-421F-A81A-59254FEE1FEC}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kavita.Email", "Kavita.Email\Kavita.Email.csproj", "{1CC0DACD-A6E0-4A9F-8286-69F1EE247F7F}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -72,5 +74,17 @@ Global
{3D781D18-2452-421F-A81A-59254FEE1FEC}.Release|x64.Build.0 = Release|Any CPU
{3D781D18-2452-421F-A81A-59254FEE1FEC}.Release|x86.ActiveCfg = Release|Any CPU
{3D781D18-2452-421F-A81A-59254FEE1FEC}.Release|x86.Build.0 = Release|Any CPU
{1CC0DACD-A6E0-4A9F-8286-69F1EE247F7F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1CC0DACD-A6E0-4A9F-8286-69F1EE247F7F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1CC0DACD-A6E0-4A9F-8286-69F1EE247F7F}.Debug|x64.ActiveCfg = Debug|Any CPU
{1CC0DACD-A6E0-4A9F-8286-69F1EE247F7F}.Debug|x64.Build.0 = Debug|Any CPU
{1CC0DACD-A6E0-4A9F-8286-69F1EE247F7F}.Debug|x86.ActiveCfg = Debug|Any CPU
{1CC0DACD-A6E0-4A9F-8286-69F1EE247F7F}.Debug|x86.Build.0 = Debug|Any CPU
{1CC0DACD-A6E0-4A9F-8286-69F1EE247F7F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1CC0DACD-A6E0-4A9F-8286-69F1EE247F7F}.Release|Any CPU.Build.0 = Release|Any CPU
{1CC0DACD-A6E0-4A9F-8286-69F1EE247F7F}.Release|x64.ActiveCfg = Release|Any CPU
{1CC0DACD-A6E0-4A9F-8286-69F1EE247F7F}.Release|x64.Build.0 = Release|Any CPU
{1CC0DACD-A6E0-4A9F-8286-69F1EE247F7F}.Release|x86.ActiveCfg = Release|Any CPU
{1CC0DACD-A6E0-4A9F-8286-69F1EE247F7F}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal

1
UI/Web/.gitignore vendored
View File

@ -1,3 +1,4 @@
node_modules/
test-results/
playwright-report/
i18n-cache-busting.json

View File

@ -1 +1 @@
{"zh_Hant":"05191aaae25a26a8597559e8318f97db","zh_Hans":"ca663a190b259b41ac365b6b5537558e","uk":"ccf59f571821ab842882378395ccf48c","tr":"5d6427179210cc370400b816c9d1116d","th":"1e27a1e1cadb2b9f92d85952bffaab95","sk":"24de417448b577b4899e917b70a43263","ru":"c547f0995c167817dd2408e4e9279de2","pt_BR":"44c7cd3da6baad38887fb03ac4ec5581","pt":"af4162a48f01c5260d6436e7e000c5ef","pl":"c6488fdb9a1ecfe5cde6bd1c264902aa","nl":"3ff322f7b24442bd6bceb5c692146d4f","nb_NO":"99914b932bd37a50b983c5e7c90ae93b","ms":"9fdfcc11a2e8a58a4baa691b93d93ff7","ko":"447e24f9f60e1b9f36bc0b087d059dbd","ja":"2f46a5ad1364a71255dd76c0094a9264","it":"4ef0a0ef56bab4650eda37e0dd841982","id":"0beab79883a28035c393768fb8c8ecbd","hi":"d850bb49ec6b5a5ccf9986823f095ab8","fr":"b41b7065960b431a9833140e9014189e","es":"151b6c17ef7382da9f0f22b87a346b7d","en":"d07463979db4cc7ab6e0089889cfc730","de":"f8e3ec31790044be07e222703ed0575a","cs":"0f0c433b9fd641977e89a42e92e4b884"}
{"zh_Hant":"05191aaae25a26a8597559e8318f97db","zh_Hans":"7edb04f6c2439da2cde73996aed08029","uk":"ccf59f571821ab842882378395ccf48c","tr":"5d6427179210cc370400b816c9d1116d","th":"1e27a1e1cadb2b9f92d85952bffaab95","sk":"24de417448b577b4899e917b70a43263","ru":"c547f0995c167817dd2408e4e9279de2","pt_BR":"5acd3a08c1d9aabfae5a74a438cff79b","pt":"af4162a48f01c5260d6436e7e000c5ef","pl":"c6488fdb9a1ecfe5cde6bd1c264902aa","nl":"3ff322f7b24442bd6bceb5c692146d4f","nb_NO":"99914b932bd37a50b983c5e7c90ae93b","ms":"9fdfcc11a2e8a58a4baa691b93d93ff7","ko":"447e24f9f60e1b9f36bc0b087d059dbd","ja":"27bec4796972f0338404ebdb5829af14","it":"4ef0a0ef56bab4650eda37e0dd841982","id":"cfaff69f0a68d9b6196b6c11986508f8","hi":"d850bb49ec6b5a5ccf9986823f095ab8","fr":"c648c43f9ea0bb20ddb00c0566bbd85a","es":"5816bb68d1d64c40de890c0be0222c71","en":"9ba658d4565ee2d245791896559b2271","de":"c3a4fd22b51fd5a675363a6a35d1611e","cs":"bd76bfbd0e5538378dfe99d034b2adfe"}

View File

@ -199,7 +199,7 @@ export class AccountService {
}
resendConfirmationEmail(userId: number) {
return this.httpClient.post<string>(this.baseUrl + 'account/resend-confirmation-email?userId=' + userId, {}, TextResonse);
return this.httpClient.post<InviteUserResponse>(this.baseUrl + 'account/resend-confirmation-email?userId=' + userId, {});
}
inviteUser(model: {email: string, roles: Array<string>, libraries: Array<number>, ageRestriction: AgeRestriction}) {
@ -310,7 +310,7 @@ export class AccountService {
}
private refreshAccount() {
refreshAccount() {
if (this.currentUser === null || this.currentUser === undefined) return of();
return this.httpClient.get<User>(this.baseUrl + 'account/refresh-account').pipe(map((user: User) => {
if (user) {

View File

@ -6,6 +6,7 @@ import { UpdateVersionEvent } from '../_models/events/update-version-event';
import { Job } from '../_models/job/job';
import { KavitaMediaError } from '../admin/_models/media-error';
import {TextResonse} from "../_types/text-response";
import {map} from "rxjs/operators";
@Injectable({
providedIn: 'root'
@ -14,66 +15,62 @@ export class ServerService {
baseUrl = environment.apiUrl;
constructor(private httpClient: HttpClient) { }
constructor(private http: HttpClient) { }
getServerInfo() {
return this.httpClient.get<ServerInfoSlim>(this.baseUrl + 'server/server-info-slim');
return this.http.get<ServerInfoSlim>(this.baseUrl + 'server/server-info-slim');
}
clearCache() {
return this.httpClient.post(this.baseUrl + 'server/clear-cache', {});
return this.http.post(this.baseUrl + 'server/clear-cache', {});
}
cleanupWantToRead() {
return this.httpClient.post(this.baseUrl + 'server/cleanup-want-to-read', {});
return this.http.post(this.baseUrl + 'server/cleanup-want-to-read', {});
}
backupDatabase() {
return this.httpClient.post(this.baseUrl + 'server/backup-db', {});
return this.http.post(this.baseUrl + 'server/backup-db', {});
}
analyzeFiles() {
return this.httpClient.post(this.baseUrl + 'server/analyze-files', {});
return this.http.post(this.baseUrl + 'server/analyze-files', {});
}
checkForUpdate() {
return this.httpClient.get<UpdateVersionEvent>(this.baseUrl + 'server/check-update', {});
return this.http.get<UpdateVersionEvent>(this.baseUrl + 'server/check-update', {});
}
checkForUpdates() {
return this.httpClient.get(this.baseUrl + 'server/check-for-updates', {});
return this.http.get(this.baseUrl + 'server/check-for-updates', {});
}
getChangelog() {
return this.httpClient.get<UpdateVersionEvent[]>(this.baseUrl + 'server/changelog', {});
return this.http.get<UpdateVersionEvent[]>(this.baseUrl + 'server/changelog', {});
}
isServerAccessible() {
return this.httpClient.get<boolean>(this.baseUrl + 'server/accessible');
return this.http.get<boolean>(this.baseUrl + 'server/accessible');
}
getRecurringJobs() {
return this.httpClient.get<Job[]>(this.baseUrl + 'server/jobs');
return this.http.get<Job[]>(this.baseUrl + 'server/jobs');
}
convertMedia() {
return this.httpClient.post(this.baseUrl + 'server/convert-media', {});
return this.http.post(this.baseUrl + 'server/convert-media', {});
}
bustCache() {
return this.httpClient.post(this.baseUrl + 'server/bust-review-and-rec-cache', {});
return this.http.post(this.baseUrl + 'server/bust-review-and-rec-cache', {});
}
getMediaErrors() {
return this.httpClient.get<Array<KavitaMediaError>>(this.baseUrl + 'server/media-errors', {});
return this.http.get<Array<KavitaMediaError>>(this.baseUrl + 'server/media-errors', {});
}
clearMediaAlerts() {
return this.httpClient.post(this.baseUrl + 'server/clear-media-alerts', {});
}
getEmailVersion() {
return this.httpClient.get<string>(this.baseUrl + 'server/email-version', TextResonse);
return this.http.post(this.baseUrl + 'server/clear-media-alerts', {});
}
}

View File

@ -1,11 +1,12 @@
import { Component, Input } from '@angular/core';
import {Component, inject, Input} from '@angular/core';
import { FormGroup, FormControl, Validators, ReactiveFormsModule } from '@angular/forms';
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
import { Member } from 'src/app/_models/auth/member';
import { AccountService } from 'src/app/_services/account.service';
import { SentenceCasePipe } from '../../../_pipes/sentence-case.pipe';
import { NgIf } from '@angular/common';
import {TranslocoDirective} from "@ngneat/transloco";
import {translate, TranslocoDirective} from "@ngneat/transloco";
import {ToastrService} from "ngx-toastr";
@Component({
selector: 'app-reset-password-modal',
@ -16,16 +17,21 @@ import {TranslocoDirective} from "@ngneat/transloco";
})
export class ResetPasswordModalComponent {
private readonly toastr = inject(ToastrService);
private readonly accountService = inject(AccountService);
public readonly modal = inject(NgbActiveModal);
@Input({required: true}) member!: Member;
errorMessage = '';
resetPasswordForm: FormGroup = new FormGroup({
password: new FormControl('', [Validators.required]),
});
constructor(public modal: NgbActiveModal, private accountService: AccountService) { }
save() {
this.accountService.resetPassword(this.member.username, this.resetPasswordForm.value.password,'').subscribe(() => {
this.toastr.success(translate('toasts.password-updated'))
this.modal.close();
});
}

View File

@ -1,5 +1,6 @@
import { EncodeFormat } from "./encode-format";
import {CoverImageSize} from "./cover-image-size";
import {SmtpConfig} from "./smtp-config";
export interface ServerSettings {
cacheDirectory: string;
@ -22,4 +23,5 @@ export interface ServerSettings {
onDeckProgressDays: number;
onDeckUpdateDays: number;
coverImageSize: CoverImageSize;
smtpConfig: SmtpConfig;
}

View File

@ -0,0 +1,11 @@
export interface SmtpConfig {
senderAddress: string;
senderDisplayName: string;
userName: string;
password: string;
host: string;
port: number;
enableSsl: boolean;
sizeLimit: number;
customizedTemplates: boolean;
}

View File

@ -2,25 +2,10 @@
<div class="container-fluid">
<form [formGroup]="settingsForm" *ngIf="serverSettings !== undefined">
<h4>{{t('title')}}</h4>
<p [innerHTML]="t('description', {link: link}) | safeHtml">
<span class="text-warning">{{t('send-to-warning')}}</span>
</p>
<div class="mb-3">
<label for="settings-emailservice" class="form-label">{{t('email-url-label')}}</label><i class="ms-1 fa fa-info-circle" placement="right" [ngbTooltip]="emailServiceTooltip" role="button" tabindex="0"></i>
<ng-template #emailServiceTooltip>{{t('email-url-tooltip')}}</ng-template>
<span class="visually-hidden" id="settings-emailservice-help"><ng-container [ngTemplateOutlet]="emailServiceTooltip"></ng-container></span>
<div class="input-group">
<input id="settings-emailservice" aria-describedby="settings-emailservice-help" class="form-control" formControlName="emailServiceUrl" type="url" autocapitalize="off" inputmode="url">
<button class="btn btn-outline-secondary" (click)="resetEmailServiceUrl()">
{{t('reset')}}
</button>
<button class="btn btn-outline-secondary" (click)="testEmailServiceUrl()">
{{t('test')}}
</button>
</div>
</div>
<div class="mb-3">
<p>You must fill out both Host Name and SMTP settings to use email-based functionality within Kavita.</p>
<div class="mb-3 pe-2 ps-2 ">
<label for="settings-hostname" class="form-label">{{t('host-name-label')}}</label><i class="fa fa-info-circle ms-1" placement="right" [ngbTooltip]="hostNameTooltip" role="button" tabindex="0"></i>
<ng-template #hostNameTooltip>{{t('host-name-tooltip')}}</ng-template>
<span class="visually-hidden" id="settings-hostname-help">
@ -35,7 +20,93 @@
</div>
</div>
<div class="mt-3">
<div class="row g-0">
<div class="col-md-6 col-sm-12 pe-2 ps-2 mb-2">
<label for="settings-sender-address" class="form-label">{{t('sender-address-label')}}</label>
<i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right" [ngbTooltip]="senderAddressTooltip" role="button" tabindex="0"></i>
<ng-template #senderAddressTooltip>{{t('sender-address-tooltip')}}</ng-template>
<span class="visually-hidden" id="settings-sender-address-help"><ng-container [ngTemplateOutlet]="senderAddressTooltip"></ng-container></span>
<input type="text" class="form-control" aria-describedby="manga-header" formControlName="senderAddress" id="settings-sender-address" />
</div>
<div class="col-md-6 col-sm-12 pe-2 ps-2 mb-2">
<label for="settings-sender-displayname" class="form-label">{{t('sender-displayname-label')}}</label>
<i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right" [ngbTooltip]="senderDisplayNameTooltip" role="button" tabindex="0"></i>
<ng-template #senderDisplayNameTooltip>{{t('sender-displayname-tooltip')}}</ng-template>
<span class="visually-hidden" id="settings-sender-displayname-help"><ng-container [ngTemplateOutlet]="senderDisplayNameTooltip"></ng-container></span>
<input type="text" class="form-control" aria-describedby="manga-header" formControlName="senderDisplayName" id="settings-sender-displayname" />
</div>
</div>
<div class="row g-0">
<div class="col-md-4 col-sm-12 pe-2 ps-2 mb-2">
<label for="settings-sender-address" class="form-label">{{t('host-label')}}</label>
<i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right" [ngbTooltip]="hostTooltip" role="button" tabindex="0"></i>
<ng-template #hostTooltip>{{t('host-tooltip')}}</ng-template>
<span class="visually-hidden" id="settings-host-help"><ng-container [ngTemplateOutlet]="hostTooltip"></ng-container></span>
<input type="text" class="form-control" aria-describedby="manga-header" formControlName="host" id="settings-host" />
</div>
<div class="col-md-4 col-sm-12 pe-2 ps-2 mb-2">
<label for="settings-port" class="form-label">{{t('port-label')}}</label>
<input type="number" min="1" class="form-control" aria-describedby="manga-header" formControlName="port" id="settings-port" />
</div>
<div class="col-md-4 col-sm-12 pe-2 ps-2 mb-2">
<div class="form-check form-switch" style="margin-top: 36px">
<input type="checkbox" id="settings-enable-ssl" role="switch" formControlName="enableSsl" class="form-check-input"
aria-labelledby="auto-close-label">
<label class="form-check-label" for="settings-enable-ssl">{{t('enable-ssl-label')}}</label>
</div>
</div>
</div>
<div class="row g-0">
<div class="col-md-6 col-sm-12 pe-2 ps-2 mb-2">
<label for="settings-username" class="form-label">{{t('username-label')}}</label>
<i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right" [ngbTooltip]="usernameTooltip" role="button" tabindex="0"></i>
<ng-template #usernameTooltip>{{t('username-tooltip')}}</ng-template>
<span class="visually-hidden" id="settings-username-help"><ng-container [ngTemplateOutlet]="usernameTooltip"></ng-container></span>
<input type="text" class="form-control" aria-describedby="manga-header" formControlName="userName" id="settings-username" />
</div>
<div class="col-md-6 col-sm-12 pe-2 ps-2 mb-2">
<label for="settings-password" class="form-label">{{t('password-label')}}</label>
<i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right" [ngbTooltip]="passwordTooltip" role="button" tabindex="0"></i>
<ng-template #passwordTooltip>{{t('password-tooltip')}}</ng-template>
<span class="visually-hidden" id="settings-password-help"><ng-container [ngTemplateOutlet]="usernameTooltip"></ng-container></span>
<input type="password" class="form-control" aria-describedby="manga-header" formControlName="password" id="settings-password" />
</div>
</div>
<div class="row g-0">
<div class="col-md-6 col-sm-12 pe-2 ps-2 mb-2">
<label for="settings-size-limit" class="form-label">{{t('size-limit-label')}}</label>
<i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right" [ngbTooltip]="sizeLimitTooltip" role="button" tabindex="0"></i>
<ng-template #sizeLimitTooltip>{{t('size-limit-tooltip')}}</ng-template>
<span class="visually-hidden" id="settings-size-limit-help"><ng-container [ngTemplateOutlet]="sizeLimitTooltip"></ng-container></span>
<input type="text" class="form-control" aria-describedby="manga-header" formControlName="sizeLimit" id="settings-size-limit" />
</div>
<div class="col-md-6 col-sm-12 pe-2 ps-2 mb-2">
<div class="form-check form-switch" style="margin-top: 36px">
<input type="checkbox" id="settings-customized-templates" role="switch" formControlName="customizedTemplates" class="form-check-input"
aria-labelledby="auto-close-label">
<label class="form-check-label" for="settings-customized-templates">{{t('customized-templates-label')}}</label>
<i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right" [ngbTooltip]="customizedTemplatesTooltip" role="button" tabindex="0"></i>
<ng-template #customizedTemplatesTooltip>{{t('customized-templates-tooltip')}}</ng-template>
<span class="visually-hidden" id="settings-customized-templates-help"><ng-container [ngTemplateOutlet]="customizedTemplatesTooltip"></ng-container></span>
</div>
</div>
</div>
</div>
<div class="col-auto d-flex d-md-block justify-content-sm-center text-md-end">
<button type="button" class="flex-fill btn btn-secondary me-2" (click)="test()">{{t('test')}}</button>
<button type="button" class="flex-fill btn btn-secondary me-2" (click)="resetToDefaults()">{{t('reset-to-default')}}</button>
<button type="button" class="flex-fill btn btn-secondary me-2" (click)="resetForm()">{{t('reset')}}</button>
<button type="submit" class="flex-fill btn btn-primary" (click)="saveSettings()" [disabled]="!settingsForm.dirty">{{t('save')}}</button>

View File

@ -1,14 +1,20 @@
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, inject, OnInit} from '@angular/core';
import {FormControl, FormGroup, ReactiveFormsModule, Validators} from '@angular/forms';
import {ToastrService} from 'ngx-toastr';
import {forkJoin, take} from 'rxjs';
import {EmailTestResult, SettingsService} from '../settings.service';
import {take} from 'rxjs';
import {SettingsService} from '../settings.service';
import {ServerSettings} from '../_models/server-settings';
import {NgbTooltip} from '@ng-bootstrap/ng-bootstrap';
import {NgIf, NgTemplateOutlet} from '@angular/common';
import {translate, TranslocoModule, TranslocoService} from "@ngneat/transloco";
import {
NgbAccordionBody,
NgbAccordionButton,
NgbAccordionCollapse,
NgbAccordionDirective, NgbAccordionHeader, NgbAccordionItem,
NgbTooltip
} from '@ng-bootstrap/ng-bootstrap';
import {NgForOf, NgIf, NgTemplateOutlet, TitleCasePipe} from '@angular/common';
import {translate, TranslocoModule} from "@ngneat/transloco";
import {SafeHtmlPipe} from "../../_pipes/safe-html.pipe";
import {ServerService} from "../../_services/server.service";
import {ManageAlertsComponent} from "../manage-alerts/manage-alerts.component";
@Component({
selector: 'app-manage-email-settings',
@ -16,38 +22,47 @@ import {ServerService} from "../../_services/server.service";
styleUrls: ['./manage-email-settings.component.scss'],
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [NgIf, ReactiveFormsModule, NgbTooltip, NgTemplateOutlet, TranslocoModule, SafeHtmlPipe]
imports: [NgIf, ReactiveFormsModule, NgbTooltip, NgTemplateOutlet, TranslocoModule, SafeHtmlPipe, ManageAlertsComponent, NgbAccordionBody, NgbAccordionButton, NgbAccordionCollapse, NgbAccordionDirective, NgbAccordionHeader, NgbAccordionItem, NgForOf, TitleCasePipe]
})
export class ManageEmailSettingsComponent implements OnInit {
serverSettings!: ServerSettings;
settingsForm: FormGroup = new FormGroup({});
link = '<a href="https://github.com/Kareadita/KavitaEmail" target="_blank" rel="noopener noreferrer">Kavita Email</a>';
emailVersion: string | null = null;
private readonly cdRef = inject(ChangeDetectorRef);
private readonly serverService = inject(ServerService);
private readonly settingsService = inject(SettingsService);
private readonly toastr = inject(ToastrService);
constructor() { }
serverSettings!: ServerSettings;
settingsForm: FormGroup = new FormGroup({});
ngOnInit(): void {
this.settingsService.getServerSettings().pipe(take(1)).subscribe((settings: ServerSettings) => {
this.serverSettings = settings;
this.settingsForm.addControl('emailServiceUrl', new FormControl(this.serverSettings.emailServiceUrl, [Validators.required]));
this.settingsForm.addControl('hostName', new FormControl(this.serverSettings.hostName, []));
this.cdRef.markForCheck();
});
this.serverService.getEmailVersion().subscribe(version => {
this.emailVersion = version;
this.settingsForm.addControl('host', new FormControl(this.serverSettings.smtpConfig.host, []));
this.settingsForm.addControl('port', new FormControl(this.serverSettings.smtpConfig.port, []));
this.settingsForm.addControl('userName', new FormControl(this.serverSettings.smtpConfig.userName, []));
this.settingsForm.addControl('enableSsl', new FormControl(this.serverSettings.smtpConfig.enableSsl, []));
this.settingsForm.addControl('password', new FormControl(this.serverSettings.smtpConfig.password, []));
this.settingsForm.addControl('senderAddress', new FormControl(this.serverSettings.smtpConfig.senderAddress, []));
this.settingsForm.addControl('senderDisplayName', new FormControl(this.serverSettings.smtpConfig.senderDisplayName, []));
this.settingsForm.addControl('sizeLimit', new FormControl(this.serverSettings.smtpConfig.sizeLimit, [Validators.min(1)]));
this.settingsForm.addControl('customizedTemplates', new FormControl(this.serverSettings.smtpConfig.customizedTemplates, [Validators.min(1)]));
this.cdRef.markForCheck();
});
}
resetForm() {
this.settingsForm.get('emailServiceUrl')?.setValue(this.serverSettings.emailServiceUrl);
this.settingsForm.get('hostName')?.setValue(this.serverSettings.hostName);
this.settingsForm.addControl('host', new FormControl(this.serverSettings.smtpConfig.host, []));
this.settingsForm.addControl('port', new FormControl(this.serverSettings.smtpConfig.port, []));
this.settingsForm.addControl('userName', new FormControl(this.serverSettings.smtpConfig.userName, []));
this.settingsForm.addControl('enableSsl', new FormControl(this.serverSettings.smtpConfig.enableSsl, []));
this.settingsForm.addControl('password', new FormControl(this.serverSettings.smtpConfig.password, []));
this.settingsForm.addControl('senderAddress', new FormControl(this.serverSettings.smtpConfig.senderAddress, []));
this.settingsForm.addControl('senderDisplayName', new FormControl(this.serverSettings.smtpConfig.senderDisplayName, []));
this.settingsForm.addControl('sizeLimit', new FormControl(this.serverSettings.smtpConfig.sizeLimit, [Validators.min(1)]));
this.settingsForm.addControl('customizedTemplates', new FormControl(this.serverSettings.smtpConfig.customizedTemplates, [Validators.min(1)]));
this.settingsForm.markAsPristine();
this.cdRef.markForCheck();
}
@ -57,6 +72,15 @@ export class ManageEmailSettingsComponent implements OnInit {
modelSettings.emailServiceUrl = this.settingsForm.get('emailServiceUrl')?.value;
modelSettings.hostName = this.settingsForm.get('hostName')?.value;
modelSettings.smtpConfig.host = this.settingsForm.get('host')?.value;
modelSettings.smtpConfig.port = this.settingsForm.get('port')?.value;
modelSettings.smtpConfig.userName = this.settingsForm.get('userName')?.value;
modelSettings.smtpConfig.enableSsl = this.settingsForm.get('enableSsl')?.value;
modelSettings.smtpConfig.password = this.settingsForm.get('password')?.value;
modelSettings.smtpConfig.senderAddress = this.settingsForm.get('senderAddress')?.value;
modelSettings.smtpConfig.senderDisplayName = this.settingsForm.get('senderDisplayName')?.value;
modelSettings.smtpConfig.sizeLimit = this.settingsForm.get('sizeLimit')?.value;
modelSettings.smtpConfig.customizedTemplates = this.settingsForm.get('customizedTemplates')?.value;
this.settingsService.updateServerSettings(modelSettings).pipe(take(1)).subscribe((settings: ServerSettings) => {
this.serverSettings = settings;
@ -77,32 +101,13 @@ export class ManageEmailSettingsComponent implements OnInit {
});
}
resetEmailServiceUrl() {
this.settingsService.resetEmailServerSettings().pipe(take(1)).subscribe((settings: ServerSettings) => {
this.serverSettings.emailServiceUrl = settings.emailServiceUrl;
this.resetForm();
this.toastr.success(translate('toasts.email-service-reset'));
}, (err: any) => {
console.error('error: ', err);
});
}
testEmailServiceUrl() {
if (this.settingsForm.get('emailServiceUrl')?.value === '') return;
forkJoin([this.settingsService.testEmailServerSettings(this.settingsForm.get('emailServiceUrl')?.value), this.serverService.getEmailVersion()])
.pipe(take(1)).subscribe(async (results) => {
const result = results[0] as EmailTestResult;
if (result.successful) {
const version = ('. Kavita Email: ' + results[1] ? 'v' + results[1] : '');
this.toastr.success(translate('toasts.email-service-reachable') + ' - ' + version);
test() {
this.settingsService.testEmailServerSettings().subscribe(res => {
if (res.successful) {
this.toastr.success(translate('toasts.email-sent', {email: res.emailAddress}));
} else {
this.toastr.error(translate('toasts.email-service-unresponsive') + result.errorMessage.split('(')[0]);
this.toastr.error(translate('toasts.email-not-sent-test'))
}
}, (err: any) => {
console.error('error: ', err);
});
}
}

View File

@ -99,7 +99,7 @@ export class ManageUsersComponent implements OnInit {
setTimeout(() => {
this.loadMembers();
this.toastr.success(this.translocoService.translate('toasts.user-deleted', {user: member.username}));
}, 30); // SetTimeout because I've noticed this can run superfast and not give enough time for data to flush
}, 30); // SetTimeout because I've noticed this can run super fast and not give enough time for data to flush
});
}
}
@ -112,15 +112,13 @@ export class ManageUsersComponent implements OnInit {
}
resendEmail(member: Member) {
this.serverService.isServerAccessible().subscribe(canAccess => {
this.accountService.resendConfirmationEmail(member.id).subscribe(async (email) => {
if (canAccess) {
this.toastr.info(this.translocoService.translate('toasts.email-sent', {user: member.username}));
return;
}
await this.confirmService.alert(
this.translocoService.translate('toasts.click-email-link') + '<br/> <a href="' + email + '" target="_blank" rel="noopener noreferrer">' + email + '</a>');
});
this.accountService.resendConfirmationEmail(member.id).subscribe(async (response) => {
if (response.emailSent) {
this.toastr.info(this.translocoService.translate('toasts.email-sent', {email: member.username}));
return;
}
await this.confirmService.alert(
this.translocoService.translate('toasts.click-email-link') + '<br/> <a href="' + response.emailLink + '" target="_blank" rel="noopener noreferrer">' + response.emailLink + '</a>');
});
}

View File

@ -11,6 +11,7 @@ import { ServerSettings } from './_models/server-settings';
export interface EmailTestResult {
successful: boolean;
errorMessage: string;
emailAddress: string;
}
@Injectable({
@ -46,12 +47,12 @@ export class SettingsService {
return this.http.post<ServerSettings>(this.baseUrl + 'settings/reset-base-url', {});
}
resetEmailServerSettings() {
return this.http.post<ServerSettings>(this.baseUrl + 'settings/reset-email-url', {});
testEmailServerSettings() {
return this.http.post<EmailTestResult>(this.baseUrl + 'settings/test-email-url', {});
}
testEmailServerSettings(emailUrl: string) {
return this.http.post<EmailTestResult>(this.baseUrl + 'settings/test-email-url', {url: emailUrl});
isEmailSetup() {
return this.http.get<string>(this.baseUrl + 'server/is-email-setup', TextResonse).pipe(map(d => d == "true"));
}
getTaskFrequencies() {

View File

@ -49,6 +49,9 @@ export class ConfirmEmailChangeComponent implements OnInit {
this.accountService.confirmEmailUpdate({email: this.email, token: this.token}).subscribe((errors) => {
this.confirmed = true;
this.cdRef.markForCheck();
// Once we are confirmed, we need to refresh our user information (in case the user is already authenticated)
this.accountService.refreshAccount().subscribe();
setTimeout(() => this.router.navigateByUrl('login'), 2000);
});
}

View File

@ -4,7 +4,7 @@
<div class="card-title">
<div class="container-fluid row mb-2">
<div class="col-10 col-sm-11">
<h4 id="email-card">{{t('email-label')}}
<h4 id="email-card">{{t('email-title')}}
@if(emailConfirmed) {
<i class="fa-solid fa-circle-check ms-1 confirm-icon" aria-hidden="true" [ngbTooltip]="t('email-confirmed')"></i>
<span class="visually-hidden">{{t('email-confirmed')}}</span>

View File

@ -74,7 +74,13 @@ export class ChangeEmailComponent implements OnInit {
if (updateEmailResponse.emailSent) {
this.toastr.success(translate('toasts.email-sent-to'));
} else {
this.toastr.success(translate('toasts.change-email-private'));
this.toastr.success(translate('toasts.change-email-no-email'));
this.accountService.refreshAccount().subscribe(user => {
this.user = user;
this.form.get('email')?.setValue(this.user?.email);
this.cdRef.markForCheck();
});
}
this.isViewMode = true;

View File

@ -14,6 +14,10 @@
{{t('description')}}
</p>
@if(hasEmailSetup) {
<div class="alert alert-warning" role="alert">{{t('email-setup-alert')}}</div>
}
<div #collapse="ngbCollapse" [(ngbCollapse)]="addDeviceIsCollapsed">
<app-edit-device [device]="device" (deviceAdded)="loadDevices()" (deviceUpdated)="loadDevices()"></app-edit-device>
</div>

View File

@ -1,6 +1,10 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
import { ToastrService } from 'ngx-toastr';
import { Subject } from 'rxjs';
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
inject,
OnInit
} from '@angular/core';
import { Device } from 'src/app/_models/device/device';
import { DeviceService } from 'src/app/_services/device.service';
import { DevicePlatformPipe } from '../../_pipes/device-platform.pipe';
@ -9,6 +13,7 @@ import { NgIf, NgFor } from '@angular/common';
import { EditDeviceComponent } from '../edit-device/edit-device.component';
import { NgbCollapse } from '@ng-bootstrap/ng-bootstrap';
import {TranslocoDirective} from "@ngneat/transloco";
import {SettingsService} from "../../admin/settings.service";
@Component({
selector: 'app-manage-devices',
@ -18,26 +23,25 @@ import {TranslocoDirective} from "@ngneat/transloco";
standalone: true,
imports: [NgbCollapse, EditDeviceComponent, NgIf, NgFor, SentenceCasePipe, DevicePlatformPipe, TranslocoDirective]
})
export class ManageDevicesComponent implements OnInit, OnDestroy {
export class ManageDevicesComponent implements OnInit {
private readonly cdRef = inject(ChangeDetectorRef);
private readonly deviceService = inject(DeviceService);
private readonly settingsService = inject(SettingsService);
devices: Array<Device> = [];
addDeviceIsCollapsed: boolean = true;
device: Device | undefined;
private readonly onDestroy = new Subject<void>();
constructor(public deviceService: DeviceService, private toastr: ToastrService,
private readonly cdRef: ChangeDetectorRef) { }
hasEmailSetup = false;
ngOnInit(): void {
this.settingsService.isEmailSetup().subscribe(res => {
this.hasEmailSetup = res;
this.cdRef.markForCheck();
});
this.loadDevices();
}
ngOnDestroy(): void {
this.onDestroy.next();
this.onDestroy.complete();
}
loadDevices() {
this.addDeviceIsCollapsed = true;

View File

@ -17,406 +17,404 @@
}
@defer (when tab.fragment === FragmentID.Preferences; prefetch on idle) {
<ng-container *ngIf="tab.fragment === FragmentID.Preferences">
<p>
{{t('pref-description')}}
</p>
<ng-container *ngIf="tab.fragment === FragmentID.Preferences">
<p>
{{t('pref-description')}}
</p>
<form [formGroup]="settingsForm" *ngIf="user !== undefined">
<div ngbAccordion [closeOthers]="true" #acc="ngbAccordion">
<div ngbAccordionItem [id]="AccordionPanelID.GlobalSettings" [collapsed]="false">
<h2 class="accordion-header" ngbAccordionHeader>
<button class="accordion-button" ngbAccordionButton type="button" [attr.aria-expanded]="acc.isExpanded(AccordionPanelID.GlobalSettings)" aria-controls="collapseOne">
{{t('global-settings-title')}}
</button>
</h2>
<div ngbAccordionCollapse>
<div ngbAccordionBody>
<ng-template>
<div class="row g-0">
<div class="col-md-6 col-sm-12 pe-2 mb-2">
<label for="settings-global-layoutmode" class="form-label">{{t('page-layout-mode-label')}}</label>
<i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right" [ngbTooltip]="layoutModeTooltip" role="button" tabindex="0"></i>
<ng-template #layoutModeTooltip>{{t('page-layout-mode-tooltip')}}</ng-template>
<span class="visually-hidden" id="settings-global-layoutmode-help">
<ng-container [ngTemplateOutlet]="layoutModeTooltip"></ng-container>
</span>
<select class="form-select" aria-describedby="manga-header" formControlName="globalPageLayoutMode" id="settings-global-layoutmode">
<option *ngFor="let opt of pageLayoutModesTranslated" [value]="opt.value">{{opt.text | titlecase}}</option>
</select>
</div>
<div class="col-md-6 col-sm-12 pe-2 mb-2">
<label for="settings-global-locale" class="form-label">{{t('locale-label')}}</label>
<i class="fa fa-info-circle ms-1"
aria-hidden="true" placement="right" [ngbTooltip]="localeTooltip" role="button" tabindex="0"></i>
<ng-template #localeTooltip>{{t('locale-tooltip')}}</ng-template>
<span class="visually-hidden" id="settings-global-locale-help">
<ng-container [ngTemplateOutlet]="localeTooltip"></ng-container>
</span>
<select class="form-select" aria-describedby="manga-header" formControlName="locale" id="settings-global-locale">
<option *ngFor="let opt of locales" [value]="opt.isoCode">{{opt.title | titlecase}}</option>
</select>
</div>
</div>
<div class="row g-0">
<div class="col-md-6 col-sm-12 pe-2 mb-2">
<div class="form-check form-switch">
<input type="checkbox" id="blur-unread-summaries" role="switch" formControlName="blurUnreadSummaries" class="form-check-input" aria-describedby="settings-global-blurUnreadSummaries-help" [value]="true" aria-labelledby="auto-close-label">
<label class="form-check-label" for="blur-unread-summaries">{{t('blur-unread-summaries-label')}}</label><i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right" [ngbTooltip]="blurUnreadSummariesTooltip" role="button" tabindex="0"></i>
</div>
<ng-template #blurUnreadSummariesTooltip>{{t('blur-unread-summaries-tooltip')}}</ng-template>
<span class="visually-hidden" id="settings-global-blurUnreadSummaries-help">
<ng-container [ngTemplateOutlet]="blurUnreadSummariesTooltip"></ng-container>
</span>
</div>
</div>
<div class="row g-0">
<div class="col-md-6 col-sm-12 pe-2 mb-2">
<div class="form-check form-switch">
<input type="checkbox" id="prompt-download" role="switch" formControlName="promptForDownloadSize" class="form-check-input" aria-describedby="settings-global-promptForDownloadSize-help" [value]="true" aria-labelledby="auto-close-label">
<label class="form-check-label" for="prompt-download">{{t('prompt-on-download-label')}}</label><i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right" [ngbTooltip]="promptForDownloadSizeTooltip" role="button" tabindex="0"></i>
</div>
<ng-template #promptForDownloadSizeTooltip>{{t('prompt-on-download-tooltip', {size: '100'})}}</ng-template>
<span class="visually-hidden" id="settings-global-promptForDownloadSize-help">
<ng-container [ngTemplateOutlet]="promptForDownloadSizeTooltip"></ng-container>
</span>
</div>
</div>
<div class="row g-0">
<div class="col-md-6 col-sm-12 pe-2 mb-2">
<div class="form-check form-switch">
<input type="checkbox" id="no-transitions" role="switch" formControlName="noTransitions" class="form-check-input"
aria-describedby="settings-global-noTransitions-help" [value]="true" aria-labelledby="auto-close-label">
<label class="form-check-label" for="no-transitions">{{t('disable-animations-label')}}</label><i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right" [ngbTooltip]="noTransitionsTooltip" role="button" tabindex="0"></i>
</div>
<ng-template #noTransitionsTooltip>{{t('disable-animations-tooltip')}}</ng-template>
<span class="visually-hidden" id="settings-global-noTransitions-help">
<ng-container [ngTemplateOutlet]="noTransitionsTooltip"></ng-container>
</span>
</div>
</div>
<div class="row g-0">
<div class="col-md-12 col-sm-12 pe-2 mb-2">
<div class="form-check form-switch">
<input type="checkbox" id="collapse-relationships" role="switch" formControlName="collapseSeriesRelationships"
aria-describedby="settings-collapse-relationships-help" class="form-check-input" aria-labelledby="auto-close-label">
<label class="form-check-label" for="collapse-relationships">{{t('collapse-series-relationships-label')}}</label>
<i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right" [ngbTooltip]="collapseSeriesRelationshipsTooltip" role="button" tabindex="0"></i>
</div>
<ng-template #collapseSeriesRelationshipsTooltip>{{t('collapse-series-relationships-tooltip')}}</ng-template>
<span class="visually-hidden" id="settings-collapse-relationships-help">
<ng-container [ngTemplateOutlet]="collapseSeriesRelationshipsTooltip"></ng-container>
</span>
</div>
</div>
<div class="row g-0">
<div class="col-md-12 col-sm-12 pe-2 mb-2">
<div class="form-check form-switch">
<input type="checkbox" id="share-reviews" role="switch" formControlName="shareReviews"
aria-describedby="settings-share-reviews-help" class="form-check-input" aria-labelledby="auto-close-label">
<label class="form-check-label" for="share-reviews">{{t('share-series-reviews-label')}}</label>
<i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right" [ngbTooltip]="shareReviewsTooltip" role="button" tabindex="0"></i>
</div>
<ng-template #shareReviewsTooltip>{{t('share-series-reviews-tooltip')}}</ng-template>
<span class="visually-hidden" id="settings-share-reviews-help">
<ng-container [ngTemplateOutlet]="shareReviewsTooltip"></ng-container>
</span>
</div>
</div>
<div class="col-auto d-flex d-md-block justify-content-sm-center text-md-end mb-3">
<button type="button" class="flex-fill btn btn-secondary me-2" (click)="resetForm()" aria-describedby="reading-panel">{{t('reset')}}</button>
<button type="submit" class="flex-fill btn btn-primary" (click)="save()" aria-describedby="reading-panel" [disabled]="!settingsForm.dirty">{{t('save')}}</button>
</div>
</ng-template>
</div>
</div>
</div>
<div ngbAccordionItem [id]="AccordionPanelID.ImageReader">
<form [formGroup]="settingsForm" *ngIf="user !== undefined">
<div ngbAccordion [closeOthers]="true" #acc="ngbAccordion">
<div ngbAccordionItem [id]="AccordionPanelID.GlobalSettings" [collapsed]="false">
<h2 class="accordion-header" ngbAccordionHeader>
<button class="accordion-button" ngbAccordionButton type="button" [attr.aria-expanded]="acc.isExpanded(AccordionPanelID.ImageReader)" aria-controls="collapseOne">
{{t('image-reader-settings-title')}}
</button>
<button class="accordion-button" ngbAccordionButton type="button" [attr.aria-expanded]="acc.isExpanded(AccordionPanelID.GlobalSettings)" aria-controls="collapseOne">
{{t('global-settings-title')}}
</button>
</h2>
<div ngbAccordionCollapse>
<div ngbAccordionBody>
<ng-template>
<div class="row g-0">
<div class="col-md-6 col-sm-12 pe-2 mb-2">
<label for="settings-reading-direction" class="form-label">{{t('reading-direction-label')}}</label><i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right" [ngbTooltip]="readingDirectionTooltip" role="button" tabindex="0"></i>
<ng-template #readingDirectionTooltip>{{t('reading-direction-tooltip')}}</ng-template>
<span class="visually-hidden" id="settings-reading-direction-help">
<ng-container [ngTemplateOutlet]="readingDirectionTooltip"></ng-container>
</span>
<select class="form-select" aria-describedby="manga-header" formControlName="readingDirection" id="settings-reading-direction">
<option *ngFor="let opt of readingDirectionsTranslated" [value]="opt.value">{{opt.text | titlecase}}</option>
</select>
<label for="settings-global-layoutmode" class="form-label">{{t('page-layout-mode-label')}}</label>
<i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right" [ngbTooltip]="layoutModeTooltip" role="button" tabindex="0"></i>
<ng-template #layoutModeTooltip>{{t('page-layout-mode-tooltip')}}</ng-template>
<span class="visually-hidden" id="settings-global-layoutmode-help">
<ng-container [ngTemplateOutlet]="layoutModeTooltip"></ng-container>
</span>
<select class="form-select" aria-describedby="manga-header" formControlName="globalPageLayoutMode" id="settings-global-layoutmode">
<option *ngFor="let opt of pageLayoutModesTranslated" [value]="opt.value">{{opt.text | titlecase}}</option>
</select>
</div>
<div class="col-md-6 col-sm-12 pe-2 mb-2">
<label for="settings-global-locale" class="form-label">{{t('locale-label')}}</label>
<i class="fa fa-info-circle ms-1"
aria-hidden="true" placement="right" [ngbTooltip]="localeTooltip" role="button" tabindex="0"></i>
<ng-template #localeTooltip>{{t('locale-tooltip')}}</ng-template>
<span class="visually-hidden" id="settings-global-locale-help">
<ng-container [ngTemplateOutlet]="localeTooltip"></ng-container>
</span>
<select class="form-select" aria-describedby="manga-header" formControlName="locale" id="settings-global-locale">
<option *ngFor="let opt of locales" [value]="opt.isoCode">{{opt.title | titlecase}}</option>
</select>
</div>
</div>
<div class="col-md-6 col-sm-12 pe-2 mb-2">
<label for="settings-scaling-option" class="form-label">{{t('scaling-option-label')}}</label><i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right" [ngbTooltip]="scalingOptionTooltip" role="button" tabindex="0"></i>
<ng-template #scalingOptionTooltip>{{t('scaling-option-tooltip')}}</ng-template>
<span class="visually-hidden" id="settings-scaling-option-help">
<ng-container [ngTemplateOutlet]="scalingOptionTooltip"></ng-container>
</span>
<select class="form-select" aria-describedby="manga-header" formControlName="scalingOption" id="settings-scaling-option">
<option *ngFor="let opt of scalingOptionsTranslated" [value]="opt.value">{{opt.text | titlecase}}</option>
</select>
</div>
</div>
<div class="row g-0">
<div class="col-md-6 col-sm-12 pe-2 mb-2">
<label for="settings-pagesplit-option" class="form-label">{{t('page-splitting-label')}}</label><i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right" [ngbTooltip]="pageSplitOptionTooltip" role="button" tabindex="0"></i>
<ng-template #pageSplitOptionTooltip>{{t('page-splitting-tooltip')}}</ng-template>
<span class="visually-hidden" id="settings-pagesplit-option-help">
<ng-container [ngTemplateOutlet]="pageSplitOptionTooltip"></ng-container>
</span>
<select class="form-select" aria-describedby="manga-header" formControlName="pageSplitOption" id="settings-pagesplit-option">
<option *ngFor="let opt of pageSplitOptionsTranslated" [value]="opt.value">{{opt.text | titlecase}}</option>
</select>
</div>
<div class="col-md-6 col-sm-12 pe-2 mb-2">
<label for="settings-readingmode-option" class="form-label">{{t('reading-mode-label')}}</label>
<select class="form-select" aria-describedby="manga-header" formControlName="readerMode" id="settings-readingmode-option">
<option *ngFor="let opt of readingModesTranslated" [value]="opt.value">{{opt.text | titlecase}}</option>
</select>
</div>
</div>
<div class="row g-0">
<div class="col-md-6 col-sm-12 pe-2 mb-2" *ngIf="true">
<label for="settings-layoutmode-option" class="form-label">{{t('layout-mode-label')}}</label><i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right" [ngbTooltip]="layoutModeTooltip" role="button" tabindex="0"></i>
<ng-template #layoutModeTooltip>{{t('layout-mode-tooltip')}}</ng-template>
<span class="visually-hidden" id="settings-layoutmode-option-help">
<ng-container [ngTemplateOutlet]="layoutModeTooltip"></ng-container>
</span>
<select class="form-select" aria-describedby="manga-header" formControlName="layoutMode" id="settings-layoutmode-option">
<option *ngFor="let opt of layoutModesTranslated" [value]="opt.value">{{opt.text | titlecase}}</option>
</select>
</div>
<div class="col-md-6 col-sm-12 pe-2 mb-2">
<label for="settings-background-color-option" class="form-label">{{t('background-color-label')}}</label>
<input [value]="user!.preferences!.backgroundColor"
class="form-control"
id="settings-background-color-option"
(colorPickerChange)="handleBackgroundColorChange()"
[style.background]="user!.preferences!.backgroundColor"
[cpAlphaChannel]="'disabled'"
[(colorPicker)]="user!.preferences!.backgroundColor"/>
</div>
</div>
<div class="row g-0">
<div class="col-md-6 col-sm-12 pe-2 mb-2">
<div class="mb-3 mt-1">
<div class="row g-0">
<div class="col-md-6 col-sm-12 pe-2 mb-2">
<div class="form-check form-switch">
<input type="checkbox" id="auto-close" role="switch" formControlName="autoCloseMenu" class="form-check-input" [value]="true" aria-labelledby="auto-close-label">
<label class="form-check-label" for="auto-close">{{t('auto-close-menu-label')}}</label>
<input type="checkbox" id="blur-unread-summaries" role="switch" formControlName="blurUnreadSummaries" class="form-check-input" aria-describedby="settings-global-blurUnreadSummaries-help" [value]="true" aria-labelledby="auto-close-label">
<label class="form-check-label" for="blur-unread-summaries">{{t('blur-unread-summaries-label')}}</label><i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right" [ngbTooltip]="blurUnreadSummariesTooltip" role="button" tabindex="0"></i>
</div>
<ng-template #blurUnreadSummariesTooltip>{{t('blur-unread-summaries-tooltip')}}</ng-template>
<span class="visually-hidden" id="settings-global-blurUnreadSummaries-help">
<ng-container [ngTemplateOutlet]="blurUnreadSummariesTooltip"></ng-container>
</span>
</div>
</div>
<div class="col-md-6 col-sm-12 pe-2 mb-2">
<div class="mb-3 mt-1">
<div class="form-check form-switch">
<input type="checkbox" id="show-screen-hints" role="switch" formControlName="showScreenHints" class="form-check-input" [value]="true" aria-labelledby="auto-close-label">
<label class="form-check-label" for="show-screen-hints">{{t('show-screen-hints-label')}}</label>
</div>
</div>
</div>
</div>
<div class="row g-0">
<div class="col-md-6 col-sm-12 pe-2 mb-2">
<div class="mb-3 mt-1">
<div class="row g-0">
<div class="col-md-6 col-sm-12 pe-2 mb-2">
<div class="form-check form-switch">
<input type="checkbox" id="emulate-book" role="switch" formControlName="emulateBook" class="form-check-input" [value]="true">
<label class="form-check-label me-1" for="emulate-book">{{t('emulate-comic-book-label')}}</label><i class="fa fa-info-circle" aria-hidden="true" placement="top" ngbTooltip="Applies a shadow effect to emulate reading from a book" role="button" tabindex="0"></i>
<input type="checkbox" id="prompt-download" role="switch" formControlName="promptForDownloadSize" class="form-check-input" aria-describedby="settings-global-promptForDownloadSize-help" [value]="true" aria-labelledby="auto-close-label">
<label class="form-check-label" for="prompt-download">{{t('prompt-on-download-label')}}</label><i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right" [ngbTooltip]="promptForDownloadSizeTooltip" role="button" tabindex="0"></i>
</div>
</div>
</div>
<div class="col-md-6 col-sm-12 pe-2 mb-2">
<div class="mb-3 mt-1">
<div class="form-check form-switch">
<input type="checkbox" id="swipe-to-paginate" role="switch" formControlName="swipeToPaginate" class="form-check-input" [value]="true">
<label class="form-check-label me-1" for="swipe-to-paginate">{{t('swipe-to-paginate-label')}}</label><i class="fa fa-info-circle" aria-hidden="true" placement="top" ngbTooltip="Should swiping on the screen cause the next or previous page to be triggered" role="button" tabindex="0"></i>
</div>
</div>
</div>
</div>
<div class="col-auto d-flex d-md-block justify-content-sm-center text-md-end mb-3">
<button type="button" class="flex-fill btn btn-secondary me-2" (click)="resetForm()" aria-describedby="reading-panel">{{t('reset')}}</button>
<button type="submit" class="flex-fill btn btn-primary" (click)="save()" aria-describedby="reading-panel" [disabled]="!settingsForm.dirty">{{t('save')}}</button>
</div>
</ng-template>
<ng-template #promptForDownloadSizeTooltip>{{t('prompt-on-download-tooltip', {size: '100'})}}</ng-template>
<span class="visually-hidden" id="settings-global-promptForDownloadSize-help">
<ng-container [ngTemplateOutlet]="promptForDownloadSizeTooltip"></ng-container>
</span>
</div>
</div>
<div class="row g-0">
<div class="col-md-6 col-sm-12 pe-2 mb-2">
<div class="form-check form-switch">
<input type="checkbox" id="no-transitions" role="switch" formControlName="noTransitions" class="form-check-input"
aria-describedby="settings-global-noTransitions-help" [value]="true" aria-labelledby="auto-close-label">
<label class="form-check-label" for="no-transitions">{{t('disable-animations-label')}}</label><i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right" [ngbTooltip]="noTransitionsTooltip" role="button" tabindex="0"></i>
</div>
<ng-template #noTransitionsTooltip>{{t('disable-animations-tooltip')}}</ng-template>
<span class="visually-hidden" id="settings-global-noTransitions-help">
<ng-container [ngTemplateOutlet]="noTransitionsTooltip"></ng-container>
</span>
</div>
</div>
<div class="row g-0">
<div class="col-md-12 col-sm-12 pe-2 mb-2">
<div class="form-check form-switch">
<input type="checkbox" id="collapse-relationships" role="switch" formControlName="collapseSeriesRelationships"
aria-describedby="settings-collapse-relationships-help" class="form-check-input" aria-labelledby="auto-close-label">
<label class="form-check-label" for="collapse-relationships">{{t('collapse-series-relationships-label')}}</label>
<i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right" [ngbTooltip]="collapseSeriesRelationshipsTooltip" role="button" tabindex="0"></i>
</div>
<ng-template #collapseSeriesRelationshipsTooltip>{{t('collapse-series-relationships-tooltip')}}</ng-template>
<span class="visually-hidden" id="settings-collapse-relationships-help">
<ng-container [ngTemplateOutlet]="collapseSeriesRelationshipsTooltip"></ng-container>
</span>
</div>
</div>
<div class="row g-0">
<div class="col-md-12 col-sm-12 pe-2 mb-2">
<div class="form-check form-switch">
<input type="checkbox" id="share-reviews" role="switch" formControlName="shareReviews"
aria-describedby="settings-share-reviews-help" class="form-check-input" aria-labelledby="auto-close-label">
<label class="form-check-label" for="share-reviews">{{t('share-series-reviews-label')}}</label>
<i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right" [ngbTooltip]="shareReviewsTooltip" role="button" tabindex="0"></i>
</div>
<ng-template #shareReviewsTooltip>{{t('share-series-reviews-tooltip')}}</ng-template>
<span class="visually-hidden" id="settings-share-reviews-help">
<ng-container [ngTemplateOutlet]="shareReviewsTooltip"></ng-container>
</span>
</div>
</div>
<div class="col-auto d-flex d-md-block justify-content-sm-center text-md-end mb-3">
<button type="button" class="flex-fill btn btn-secondary me-2" (click)="resetForm()" aria-describedby="reading-panel">{{t('reset')}}</button>
<button type="submit" class="flex-fill btn btn-primary" (click)="save()" aria-describedby="reading-panel" [disabled]="!settingsForm.dirty">{{t('save')}}</button>
</div>
</ng-template>
</div>
</div>
</div>
<div ngbAccordionItem [id]="AccordionPanelID.ImageReader">
<h2 class="accordion-header" ngbAccordionHeader>
<button class="accordion-button" ngbAccordionButton type="button" [attr.aria-expanded]="acc.isExpanded(AccordionPanelID.ImageReader)" aria-controls="collapseOne">
{{t('image-reader-settings-title')}}
</button>
</h2>
<div ngbAccordionCollapse>
<div ngbAccordionBody>
<ng-template>
<div class="row g-0">
<div class="col-md-6 col-sm-12 pe-2 mb-2">
<label for="settings-reading-direction" class="form-label">{{t('reading-direction-label')}}</label><i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right" [ngbTooltip]="readingDirectionTooltip" role="button" tabindex="0"></i>
<ng-template #readingDirectionTooltip>{{t('reading-direction-tooltip')}}</ng-template>
<span class="visually-hidden" id="settings-reading-direction-help">
<ng-container [ngTemplateOutlet]="readingDirectionTooltip"></ng-container>
</span>
<select class="form-select" aria-describedby="manga-header" formControlName="readingDirection" id="settings-reading-direction">
<option *ngFor="let opt of readingDirectionsTranslated" [value]="opt.value">{{opt.text | titlecase}}</option>
</select>
</div>
<div class="col-md-6 col-sm-12 pe-2 mb-2">
<label for="settings-scaling-option" class="form-label">{{t('scaling-option-label')}}</label><i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right" [ngbTooltip]="scalingOptionTooltip" role="button" tabindex="0"></i>
<ng-template #scalingOptionTooltip>{{t('scaling-option-tooltip')}}</ng-template>
<span class="visually-hidden" id="settings-scaling-option-help">
<ng-container [ngTemplateOutlet]="scalingOptionTooltip"></ng-container>
</span>
<select class="form-select" aria-describedby="manga-header" formControlName="scalingOption" id="settings-scaling-option">
<option *ngFor="let opt of scalingOptionsTranslated" [value]="opt.value">{{opt.text | titlecase}}</option>
</select>
</div>
</div>
<div class="row g-0">
<div class="col-md-6 col-sm-12 pe-2 mb-2">
<label for="settings-pagesplit-option" class="form-label">{{t('page-splitting-label')}}</label><i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right" [ngbTooltip]="pageSplitOptionTooltip" role="button" tabindex="0"></i>
<ng-template #pageSplitOptionTooltip>{{t('page-splitting-tooltip')}}</ng-template>
<span class="visually-hidden" id="settings-pagesplit-option-help">
<ng-container [ngTemplateOutlet]="pageSplitOptionTooltip"></ng-container>
</span>
<select class="form-select" aria-describedby="manga-header" formControlName="pageSplitOption" id="settings-pagesplit-option">
<option *ngFor="let opt of pageSplitOptionsTranslated" [value]="opt.value">{{opt.text | titlecase}}</option>
</select>
</div>
<div class="col-md-6 col-sm-12 pe-2 mb-2">
<label for="settings-readingmode-option" class="form-label">{{t('reading-mode-label')}}</label>
<select class="form-select" aria-describedby="manga-header" formControlName="readerMode" id="settings-readingmode-option">
<option *ngFor="let opt of readingModesTranslated" [value]="opt.value">{{opt.text | titlecase}}</option>
</select>
</div>
</div>
<div class="row g-0">
<div class="col-md-6 col-sm-12 pe-2 mb-2" *ngIf="true">
<label for="settings-layoutmode-option" class="form-label">{{t('layout-mode-label')}}</label><i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right" [ngbTooltip]="layoutModeTooltip" role="button" tabindex="0"></i>
<ng-template #layoutModeTooltip>{{t('layout-mode-tooltip')}}</ng-template>
<span class="visually-hidden" id="settings-layoutmode-option-help">
<ng-container [ngTemplateOutlet]="layoutModeTooltip"></ng-container>
</span>
<select class="form-select" aria-describedby="manga-header" formControlName="layoutMode" id="settings-layoutmode-option">
<option *ngFor="let opt of layoutModesTranslated" [value]="opt.value">{{opt.text | titlecase}}</option>
</select>
</div>
<div class="col-md-6 col-sm-12 pe-2 mb-2">
<label for="settings-background-color-option" class="form-label">{{t('background-color-label')}}</label>
<input [value]="user!.preferences!.backgroundColor"
class="form-control"
id="settings-background-color-option"
(colorPickerChange)="handleBackgroundColorChange()"
[style.background]="user!.preferences!.backgroundColor"
[cpAlphaChannel]="'disabled'"
[(colorPicker)]="user!.preferences!.backgroundColor"/>
</div>
</div>
<div class="row g-0">
<div class="col-md-6 col-sm-12 pe-2 mb-2">
<div class="mb-3 mt-1">
<div class="form-check form-switch">
<input type="checkbox" id="auto-close" role="switch" formControlName="autoCloseMenu" class="form-check-input" [value]="true" aria-labelledby="auto-close-label">
<label class="form-check-label" for="auto-close">{{t('auto-close-menu-label')}}</label>
</div>
</div>
</div>
<div ngbAccordionItem [id]="AccordionPanelID.BookReader">
<h2 class="accordion-header" ngbAccordionHeader>
<button class="accordion-button" ngbAccordionButton type="button" [attr.aria-expanded]="acc.isExpanded(AccordionPanelID.BookReader)" aria-controls="collapseOne">
{{t('book-reader-settings-title')}}
</button>
</h2>
<div ngbAccordionCollapse>
<div ngbAccordionBody>
<ng-template>
<div class="row g-0">
<div class="col-md-4 col-sm-12 pe-2 mb-3">
<label id="taptopaginate-label" class="form-label"></label>
<div class="mb-3">
<div class="form-check form-switch">
<input type="checkbox" role="switch" id="taptopaginate" formControlName="bookReaderTapToPaginate" class="form-check-input" [value]="true" aria-labelledby="taptopaginate-label">
<label for="taptopaginate" class="form-check-label">{{t('tap-to-paginate-label')}}</label><i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right" [ngbTooltip]="tapToPaginateOptionTooltip" role="button" tabindex="0"></i>
<ng-template #tapToPaginateOptionTooltip>{{t('tap-to-paginate-tooltip')}}</ng-template>
<span class="visually-hidden" id="settings-taptopaginate-option-help">
<ng-container [ngTemplateOutlet]="tapToPaginateOptionTooltip"></ng-container>
</span>
</div>
</div>
</div>
<div class="col-md-4 col-sm-12 pe-2 mb-3">
<label id="immersivemode-label" class="form-label"></label>
<div class="mb-3">
<div class="form-check form-switch">
<input type="checkbox" role="switch" id="immersivemode" formControlName="bookReaderImmersiveMode" class="form-check-input" [value]="true" aria-labelledby="immersivemode-label">
<label for="immersivemode" class="form-check-label">{{t('immersive-mode-label')}}</label><i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right" [ngbTooltip]="immersivemodeOptionTooltip" role="button" tabindex="0"></i>
<ng-template #immersivemodeOptionTooltip>{{t('immersive-mode-label')}}</ng-template>
<span class="visually-hidden" id="settings-immersivemode-option-help">
<ng-container [ngTemplateOutlet]="immersivemodeOptionTooltip"></ng-container>
</span>
</div>
</div>
</div>
</div>
<div class="row g-0">
<div class="col-md-6 col-sm-12 pe-2 mb-3">
<label for="settings-book-reading-direction" class="form-label">{{t('reading-direction-label')}}</label><i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right" [ngbTooltip]="bookReadingDirectionTooltip" role="button" tabindex="0"></i>
<ng-template #bookReadingDirectionTooltip>{{t('reading-direction-book-tooltip')}}</ng-template>
<span class="visually-hidden" id="settings-book-reading-direction-book-help">
<ng-container [ngTemplateOutlet]="bookReadingDirectionTooltip"></ng-container>
</span>
<select id="settings-book-reading-direction" class="form-select" aria-describedby="settings-book-reading-direction-help" formControlName="bookReaderReadingDirection">
<option *ngFor="let opt of readingDirectionsTranslated" [value]="opt.value">{{opt.text | titlecase}}</option>
</select>
</div>
<div class="col-md-6 col-sm-12 pe-2 mb-3">
<label for="settings-fontfamily-option" class="form-label">{{t('font-family-label')}}</label><i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right" [ngbTooltip]="fontFamilyOptionTooltip" role="button" tabindex="0"></i>
<ng-template #fontFamilyOptionTooltip>{{t('font-family-tooltip')}}</ng-template>
<span class="visually-hidden" id="settings-fontfamily-option-help">
<ng-container [ngTemplateOutlet]="fontFamilyOptionTooltip"></ng-container>
</span>
<select id="settings-fontfamily-option" class="form-select" aria-describedby="settings-fontfamily-option-help" formControlName="bookReaderFontFamily">
<option *ngFor="let opt of fontFamilies" [value]="opt">{{opt | titlecase}}</option>
</select>
</div>
</div>
<div class="row g-0">
<div class="col-md-6 col-sm-12 pe-2 mb-3">
<label for="settings-book-writing-style" class="form-label me-1">{{t('writing-style-label')}}</label><i class="fa fa-info-circle ms-1" aria-hidden="true" aria-describedby="settings-book-writing-style-help" placement="right" [ngbTooltip]="bookWritingStyleToolTip" role="button" tabindex="0"></i>
<ng-template #bookWritingStyleToolTip>{{t('writing-style-tooltip')}}</ng-template>
<span class="visually-hidden" id="settings-book-writing-style-help">
<ng-container [ngTemplateOutlet]="bookWritingStyleToolTip"></ng-container>
</span>
<select class="form-select" aria-describedby="settings-book-writing-style-help" formControlName="bookReaderWritingStyle" id="settings-book-writing-style" >
<option *ngFor="let opt of bookWritingStylesTranslated" [value]="opt.value">{{opt.text | titlecase}}</option>
</select>
</div>
<div class="col-md-6 col-sm-12 pe-2 mb-3">
<label for="settings-book-layout-mode" class="form-label">{{t('layout-mode-book-label')}}</label><i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right" [ngbTooltip]="bookLayoutModeTooltip" role="button" tabindex="0"></i>
<ng-template #bookLayoutModeTooltip>{{t('layout-mode-book-tooltip')}}</ng-template>
<span class="visually-hidden" id="settings-book-layout-mode-help">
<ng-container [ngTemplateOutlet]="bookLayoutModeTooltip"></ng-container>
</span>
<select class="form-select" aria-describedby="settings-book-layout-mode-help" formControlName="bookReaderLayoutMode" id="settings-book-layout-mode">
<option *ngFor="let opt of bookLayoutModesTranslated" [value]="opt.value">{{opt.text | titlecase}}</option>
</select>
</div>
</div>
<div class="row g-0">
<div class="col-md-6 col-sm-12 pe-2 mb-3">
<label for="settings-color-theme-option" class="form-label">{{t('color-theme-book-label')}}</label><i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right" [ngbTooltip]="bookColorThemeTooltip" role="button" tabindex="0"></i>
<ng-template #bookColorThemeTooltip>{{t('color-theme-book-tooltip')}}</ng-template>
<span class="visually-hidden" id="settings-color-theme-option-help">
<ng-container [ngTemplateOutlet]="bookColorThemeTooltip"></ng-container>
</span>
<select class="form-select" aria-describedby="settings-color-theme-option-help" formControlName="bookReaderThemeName" id="settings-color-theme-option">
<option *ngFor="let opt of bookColorThemesTranslated" [value]="opt.name">{{opt.name | titlecase}}</option>
</select>
</div>
</div>
<div class="row g-0">
<div class="col-md-4 col-sm-12 pe-2 mb-3">
<label for="fontsize" class="form-label range-label">{{t('font-size-book-label')}}</label>
<input type="range" class="form-range" id="fontsize"
min="50" max="300" step="10" formControlName="bookReaderFontSize">
<span class="range-text">{{settingsForm.get('bookReaderFontSize')?.value + '%'}}</span>
</div>
<div class="col-md-4 col-sm-12 pe-2 mb-3">
<div class="range-label">
<label class="form-label" for="linespacing">{{t('line-height-book-label')}}</label><i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right" [ngbTooltip]="bookLineHeightOptionTooltip" role="button" tabindex="0"></i>
<ng-template #bookLineHeightOptionTooltip>{{t('line-height-book-tooltip')}}</ng-template>
<span class="visually-hidden" id="settings-booklineheight-option-help">
<ng-container [ngTemplateOutlet]="bookLineHeightOptionTooltip"></ng-container>
</span>
</div>
<input type="range" class="form-range" id="linespacing" min="100" max="200" step="10"
formControlName="bookReaderLineSpacing" aria-describedby="settings-booklineheight-option-help">
<span class="range-text">{{settingsForm.get('bookReaderLineSpacing')?.value + '%'}}</span>
</div>
<div class="col-md-4 col-sm-12 pe-2 mb-3">
<div class="range-label">
<label class="form-label">{{t('margin-book-label')}}</label><i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right" [ngbTooltip]="bookReaderMarginOptionTooltip" role="button" tabindex="0"></i>
<ng-template #bookReaderMarginOptionTooltip>{{t('margin-book-tooltip')}}</ng-template>
<span class="visually-hidden" id="settings-bookmargin-option-help">
<ng-container [ngTemplateOutlet]="bookReaderMarginOptionTooltip"></ng-container>
</span>
</div>
<input type="range" class="form-range" id="margin" min="0" max="30" step="5" formControlName="bookReaderMargin" aria-describedby="bookmargin">
<span class="range-text">{{settingsForm.get('bookReaderMargin')?.value + '%'}}</span>
</div>
</div>
<div class="col-auto d-flex d-md-block justify-content-sm-center text-md-end mb-3">
<button type="button" class="flex-fill btn btn-secondary me-2" (click)="resetForm()" aria-describedby="reading-panel">{{t('reset')}}</button>
<button type="submit" class="flex-fill btn btn-primary" (click)="save()" aria-describedby="reading-panel" [disabled]="!settingsForm.dirty">{{t('save')}}</button>
</div>
</ng-template>
<div class="col-md-6 col-sm-12 pe-2 mb-2">
<div class="mb-3 mt-1">
<div class="form-check form-switch">
<input type="checkbox" id="show-screen-hints" role="switch" formControlName="showScreenHints" class="form-check-input" [value]="true" aria-labelledby="auto-close-label">
<label class="form-check-label" for="show-screen-hints">{{t('show-screen-hints-label')}}</label>
</div>
</div>
</div>
</div>
</form>
</ng-container>
<div class="row g-0">
<div class="col-md-6 col-sm-12 pe-2 mb-2">
<div class="mb-3 mt-1">
<div class="form-check form-switch">
<input type="checkbox" id="emulate-book" role="switch" formControlName="emulateBook" class="form-check-input" [value]="true">
<label class="form-check-label me-1" for="emulate-book">{{t('emulate-comic-book-label')}}</label><i class="fa fa-info-circle" aria-hidden="true" placement="top" ngbTooltip="Applies a shadow effect to emulate reading from a book" role="button" tabindex="0"></i>
</div>
</div>
</div>
<div class="col-md-6 col-sm-12 pe-2 mb-2">
<div class="mb-3 mt-1">
<div class="form-check form-switch">
<input type="checkbox" id="swipe-to-paginate" role="switch" formControlName="swipeToPaginate" class="form-check-input" [value]="true">
<label class="form-check-label me-1" for="swipe-to-paginate">{{t('swipe-to-paginate-label')}}</label><i class="fa fa-info-circle" aria-hidden="true" placement="top" ngbTooltip="Should swiping on the screen cause the next or previous page to be triggered" role="button" tabindex="0"></i>
</div>
</div>
</div>
</div>
<div class="col-auto d-flex d-md-block justify-content-sm-center text-md-end mb-3">
<button type="button" class="flex-fill btn btn-secondary me-2" (click)="resetForm()" aria-describedby="reading-panel">{{t('reset')}}</button>
<button type="submit" class="flex-fill btn btn-primary" (click)="save()" aria-describedby="reading-panel" [disabled]="!settingsForm.dirty">{{t('save')}}</button>
</div>
</ng-template>
</div>
</div>
</div>
<div ngbAccordionItem [id]="AccordionPanelID.BookReader">
<h2 class="accordion-header" ngbAccordionHeader>
<button class="accordion-button" ngbAccordionButton type="button" [attr.aria-expanded]="acc.isExpanded(AccordionPanelID.BookReader)" aria-controls="collapseOne">
{{t('book-reader-settings-title')}}
</button>
</h2>
<div ngbAccordionCollapse>
<div ngbAccordionBody>
<ng-template>
<div class="row g-0">
<div class="col-md-4 col-sm-12 pe-2 mb-3">
<label id="taptopaginate-label" class="form-label"></label>
<div class="mb-3">
<div class="form-check form-switch">
<input type="checkbox" role="switch" id="taptopaginate" formControlName="bookReaderTapToPaginate" class="form-check-input" [value]="true" aria-labelledby="taptopaginate-label">
<label for="taptopaginate" class="form-check-label">{{t('tap-to-paginate-label')}}</label><i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right" [ngbTooltip]="tapToPaginateOptionTooltip" role="button" tabindex="0"></i>
<ng-template #tapToPaginateOptionTooltip>{{t('tap-to-paginate-tooltip')}}</ng-template>
<span class="visually-hidden" id="settings-taptopaginate-option-help">
<ng-container [ngTemplateOutlet]="tapToPaginateOptionTooltip"></ng-container>
</span>
</div>
</div>
</div>
<div class="col-md-4 col-sm-12 pe-2 mb-3">
<label id="immersivemode-label" class="form-label"></label>
<div class="mb-3">
<div class="form-check form-switch">
<input type="checkbox" role="switch" id="immersivemode" formControlName="bookReaderImmersiveMode" class="form-check-input" [value]="true" aria-labelledby="immersivemode-label">
<label for="immersivemode" class="form-check-label">{{t('immersive-mode-label')}}</label><i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right" [ngbTooltip]="immersivemodeOptionTooltip" role="button" tabindex="0"></i>
<ng-template #immersivemodeOptionTooltip>{{t('immersive-mode-label')}}</ng-template>
<span class="visually-hidden" id="settings-immersivemode-option-help">
<ng-container [ngTemplateOutlet]="immersivemodeOptionTooltip"></ng-container>
</span>
</div>
</div>
</div>
</div>
<div class="row g-0">
<div class="col-md-6 col-sm-12 pe-2 mb-3">
<label for="settings-book-reading-direction" class="form-label">{{t('reading-direction-label')}}</label><i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right" [ngbTooltip]="bookReadingDirectionTooltip" role="button" tabindex="0"></i>
<ng-template #bookReadingDirectionTooltip>{{t('reading-direction-book-tooltip')}}</ng-template>
<span class="visually-hidden" id="settings-book-reading-direction-book-help">
<ng-container [ngTemplateOutlet]="bookReadingDirectionTooltip"></ng-container>
</span>
<select id="settings-book-reading-direction" class="form-select" aria-describedby="settings-book-reading-direction-help" formControlName="bookReaderReadingDirection">
<option *ngFor="let opt of readingDirectionsTranslated" [value]="opt.value">{{opt.text | titlecase}}</option>
</select>
</div>
<div class="col-md-6 col-sm-12 pe-2 mb-3">
<label for="settings-fontfamily-option" class="form-label">{{t('font-family-label')}}</label><i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right" [ngbTooltip]="fontFamilyOptionTooltip" role="button" tabindex="0"></i>
<ng-template #fontFamilyOptionTooltip>{{t('font-family-tooltip')}}</ng-template>
<span class="visually-hidden" id="settings-fontfamily-option-help">
<ng-container [ngTemplateOutlet]="fontFamilyOptionTooltip"></ng-container>
</span>
<select id="settings-fontfamily-option" class="form-select" aria-describedby="settings-fontfamily-option-help" formControlName="bookReaderFontFamily">
<option *ngFor="let opt of fontFamilies" [value]="opt">{{opt | titlecase}}</option>
</select>
</div>
</div>
<div class="row g-0">
<div class="col-md-6 col-sm-12 pe-2 mb-3">
<label for="settings-book-writing-style" class="form-label me-1">{{t('writing-style-label')}}</label><i class="fa fa-info-circle ms-1" aria-hidden="true" aria-describedby="settings-book-writing-style-help" placement="right" [ngbTooltip]="bookWritingStyleToolTip" role="button" tabindex="0"></i>
<ng-template #bookWritingStyleToolTip>{{t('writing-style-tooltip')}}</ng-template>
<span class="visually-hidden" id="settings-book-writing-style-help">
<ng-container [ngTemplateOutlet]="bookWritingStyleToolTip"></ng-container>
</span>
<select class="form-select" aria-describedby="settings-book-writing-style-help" formControlName="bookReaderWritingStyle" id="settings-book-writing-style" >
<option *ngFor="let opt of bookWritingStylesTranslated" [value]="opt.value">{{opt.text | titlecase}}</option>
</select>
</div>
<div class="col-md-6 col-sm-12 pe-2 mb-3">
<label for="settings-book-layout-mode" class="form-label">{{t('layout-mode-book-label')}}</label><i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right" [ngbTooltip]="bookLayoutModeTooltip" role="button" tabindex="0"></i>
<ng-template #bookLayoutModeTooltip>{{t('layout-mode-book-tooltip')}}</ng-template>
<span class="visually-hidden" id="settings-book-layout-mode-help">
<ng-container [ngTemplateOutlet]="bookLayoutModeTooltip"></ng-container>
</span>
<select class="form-select" aria-describedby="settings-book-layout-mode-help" formControlName="bookReaderLayoutMode" id="settings-book-layout-mode">
<option *ngFor="let opt of bookLayoutModesTranslated" [value]="opt.value">{{opt.text | titlecase}}</option>
</select>
</div>
</div>
<div class="row g-0">
<div class="col-md-6 col-sm-12 pe-2 mb-3">
<label for="settings-color-theme-option" class="form-label">{{t('color-theme-book-label')}}</label><i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right" [ngbTooltip]="bookColorThemeTooltip" role="button" tabindex="0"></i>
<ng-template #bookColorThemeTooltip>{{t('color-theme-book-tooltip')}}</ng-template>
<span class="visually-hidden" id="settings-color-theme-option-help">
<ng-container [ngTemplateOutlet]="bookColorThemeTooltip"></ng-container>
</span>
<select class="form-select" aria-describedby="settings-color-theme-option-help" formControlName="bookReaderThemeName" id="settings-color-theme-option">
<option *ngFor="let opt of bookColorThemesTranslated" [value]="opt.name">{{opt.name | titlecase}}</option>
</select>
</div>
</div>
<div class="row g-0">
<div class="col-md-4 col-sm-12 pe-2 mb-3">
<label for="fontsize" class="form-label range-label">{{t('font-size-book-label')}}</label>
<input type="range" class="form-range" id="fontsize"
min="50" max="300" step="10" formControlName="bookReaderFontSize">
<span class="range-text">{{settingsForm.get('bookReaderFontSize')?.value + '%'}}</span>
</div>
<div class="col-md-4 col-sm-12 pe-2 mb-3">
<div class="range-label">
<label class="form-label" for="linespacing">{{t('line-height-book-label')}}</label><i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right" [ngbTooltip]="bookLineHeightOptionTooltip" role="button" tabindex="0"></i>
<ng-template #bookLineHeightOptionTooltip>{{t('line-height-book-tooltip')}}</ng-template>
<span class="visually-hidden" id="settings-booklineheight-option-help">
<ng-container [ngTemplateOutlet]="bookLineHeightOptionTooltip"></ng-container>
</span>
</div>
<input type="range" class="form-range" id="linespacing" min="100" max="200" step="10"
formControlName="bookReaderLineSpacing" aria-describedby="settings-booklineheight-option-help">
<span class="range-text">{{settingsForm.get('bookReaderLineSpacing')?.value + '%'}}</span>
</div>
<div class="col-md-4 col-sm-12 pe-2 mb-3">
<div class="range-label">
<label class="form-label">{{t('margin-book-label')}}</label><i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right" [ngbTooltip]="bookReaderMarginOptionTooltip" role="button" tabindex="0"></i>
<ng-template #bookReaderMarginOptionTooltip>{{t('margin-book-tooltip')}}</ng-template>
<span class="visually-hidden" id="settings-bookmargin-option-help">
<ng-container [ngTemplateOutlet]="bookReaderMarginOptionTooltip"></ng-container>
</span>
</div>
<input type="range" class="form-range" id="margin" min="0" max="30" step="5" formControlName="bookReaderMargin" aria-describedby="bookmargin">
<span class="range-text">{{settingsForm.get('bookReaderMargin')?.value + '%'}}</span>
</div>
</div>
<div class="col-auto d-flex d-md-block justify-content-sm-center text-md-end mb-3">
<button type="button" class="flex-fill btn btn-secondary me-2" (click)="resetForm()" aria-describedby="reading-panel">{{t('reset')}}</button>
<button type="submit" class="flex-fill btn btn-primary" (click)="save()" aria-describedby="reading-panel" [disabled]="!settingsForm.dirty">{{t('save')}}</button>
</div>
</ng-template>
</div>
</div>
</div>
</div>
</form>
</ng-container>
}
@defer (when tab.fragment === FragmentID.Clients; prefetch on idle) {
<div class="alert alert-warning" role="alert" *ngIf="!opdsEnabled">{{t('clients-opds-alert')}}</div>
<p>{{t('clients-opds-description')}}</p>

View File

@ -212,6 +212,7 @@
"no-devices": "There are no devices setup yet",
"platform-label": "Platform: ",
"email-label": "Email: ",
"email-setup-alert": "Want to send files to your devices? Have the your admin setup Email settings first!",
"add": "{{common.add}}",
"delete": "{{common.delete}}",
"edit": "{{common.edit}}"
@ -242,7 +243,8 @@
},
"change-email": {
"email-label": "{{common.email}}",
"email-title": "Email",
"email-label": "New Email",
"current-password-label": "Current Password",
"email-not-confirmed": "This email is not confirmed",
"email-confirmed": "This email is confirmed",
@ -1066,15 +1068,33 @@
"manage-email-settings": {
"title": "Email Services (SMTP)",
"description": "Kavita comes out of the box with an email service to power tasks like inviting users, password reset requests, etc. Emails sent via our service are deleted immediately. You can use your own email service by setting up the {{link}} service. Set the URL of the email service and use the Test button to ensure it works. You can reset these settings to default at any time. There is no way to disable emails for authentication, although you are not required to use a valid email address for users. Confirmation links will always be saved to logs and presented in the UI. Registration/confirmation emails will not be sent if you are not accessing Kavita via a publicly reachable URL or unless the Host Name feature is configured.",
"send-to-warning": "If you want Send to Device to work you must host your own email service.",
"description": "In order to use some functions of Kavita like Forgot Password and Send To Device, an email provider must be setup. Other features like Password change are less secure without Email setup.",
"send-to-warning": "If you want Send to Device to work you must setup your email settings",
"email-url-label": "Email Service URL",
"email-url-tooltip": "Use fully qualified URL of the email service. Do not include ending slash.",
"email-settings-title": "Email Settings",
"reset": "{{common.reset}}",
"test": "Test",
"host-name-label": "Host Name",
"host-name-tooltip": "Domain Name (of Reverse Proxy). If set, email generation will always use this.",
"host-name-validation": "Host name must start with http(s) and not end in /",
"sender-address-label": "Sender Address",
"sender-address-tooltip": "",
"sender-displayname-label": "Display Name",
"sender-displayname-tooltip": "",
"host-label": "Host",
"host-tooltip": "",
"port-label": "Port",
"port-tooltip": "",
"username-label": "Username",
"password-label": "Password",
"enable-ssl-label": "Use SSL on Email Server",
"size-limit-label": "Size Limit",
"size-limit-tooltip": "How many bytes can the Email Server handle for attachments",
"customized-templates-label": "Customized Templates",
"customized-templates-tooltip": "Should Kavita use config/templates directory for templates rather than default? You are responsible to keep up to date with template changes.",
"reset-to-default": "{{common.reset-to-default}}",
"save": "{{common.save}}"
},
@ -1964,6 +1984,7 @@
"file-send-to": "File(s) emailed to {{name}}",
"theme-missing": "The active theme no longer exists. Please refresh the page.",
"email-sent": "Email sent to {{email}}",
"email-not-sent-test": "There was an exception when sending the email. Check logs for details. This indicates improper settings.",
"email-not-sent": "Email on file is not a valid email and can not be sent. A link has been dumped in logs. The admin can provide this link to complete flow.",
"k+-license-saved": "License Key saved, but it is not valid. Click check to revalidate the subscription. First time registration may take a min to propagate.",
"k+-unlocked": "Kavita+ unlocked!",
@ -1988,7 +2009,7 @@
"age-restriction-updated": "Age Restriction has been updated",
"email-sent-to-no-existing": "Existing email is not valid. A link has been dumped to logs. Ask admin for link to complete email change.",
"email-sent-to": "An email has been sent to your old email address for confirmation.",
"change-email-private": "The server is not publicly accessible. Ask the admin to fetch your confirmation link from the logs",
"change-email-no-email": "Email has been updated",
"device-updated": "Device updated",
"device-created": "Device created",
"confirm-regen-covers": "Refresh covers will force all cover images to be recalculated. This is a heavy operation. Are you sure you don't want to perform a Scan instead?",

View File

@ -133,6 +133,8 @@ then
cd "$dir"
Package "osx-x64"
cd "$dir"
Package "osx-arm64"
cd "$dir"
else
Package "$RID"
cd "$dir"

View File

@ -315,7 +315,7 @@
"tags": [
"Account"
],
"summary": "Initiates the flow to update a user's email address. The email address is not changed in this API. A confirmation link is sent/dumped which will\r\nvalidate the email. It must be confirmed for the email to update.",
"summary": "Initiates the flow to update a user's email address.\r\n \r\nIf email is not setup, then the email address is not changed in this API. A confirmation link is sent/dumped which will\r\nvalidate the email. It must be confirmed for the email to update.",
"requestBody": {
"description": "",
"content": {
@ -461,7 +461,7 @@
"tags": [
"Account"
],
"summary": "Invites a user to the server. Will generate a setup link for continuing setup. If the server is not accessible, no\r\nemail will be sent.",
"summary": "Invites a user to the server. Will generate a setup link for continuing setup. If email is not setup, a link will be presented to user to continue setup.",
"requestBody": {
"description": "",
"content": {
@ -778,67 +778,17 @@
"content": {
"text/plain": {
"schema": {
"type": "string"
"$ref": "#/components/schemas/InviteUserResponse"
}
},
"application/json": {
"schema": {
"type": "string"
"$ref": "#/components/schemas/InviteUserResponse"
}
},
"text/json": {
"schema": {
"type": "string"
}
}
}
}
}
}
},
"/api/Account/migrate-email": {
"post": {
"tags": [
"Account"
],
"summary": "This is similar to invite. Essentially we authenticate the user's password then go through invite email flow",
"requestBody": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/MigrateUserEmailDto"
}
},
"text/json": {
"schema": {
"$ref": "#/components/schemas/MigrateUserEmailDto"
}
},
"application/*+json": {
"schema": {
"$ref": "#/components/schemas/MigrateUserEmailDto"
}
}
}
},
"responses": {
"200": {
"description": "Success",
"content": {
"text/plain": {
"schema": {
"type": "string"
}
},
"application/json": {
"schema": {
"type": "string"
}
},
"text/json": {
"schema": {
"type": "string"
"$ref": "#/components/schemas/InviteUserResponse"
}
}
}
@ -1670,7 +1620,9 @@
"tags": [
"Device"
],
"summary": "Sends a collection of chapters to the user's device",
"requestBody": {
"description": "",
"content": {
"application/json": {
"schema": {
@ -9848,36 +9800,6 @@
}
}
},
"/api/Server/email-version": {
"get": {
"tags": [
"Server"
],
"summary": "Returns the KavitaEmail version for non-default instances",
"responses": {
"200": {
"description": "Success",
"content": {
"text/plain": {
"schema": {
"type": "string"
}
},
"application/json": {
"schema": {
"type": "string"
}
},
"text/json": {
"schema": {
"type": "string"
}
}
}
}
}
}
},
"/api/Server/check-for-updates": {
"get": {
"tags": [
@ -10084,42 +10006,12 @@
}
}
},
"/api/Settings/reset-email-url": {
"post": {
"tags": [
"Settings"
],
"summary": "Resets the email service url",
"responses": {
"200": {
"description": "Success",
"content": {
"text/plain": {
"schema": {
"$ref": "#/components/schemas/ServerSettingDto"
}
},
"application/json": {
"schema": {
"$ref": "#/components/schemas/ServerSettingDto"
}
},
"text/json": {
"schema": {
"$ref": "#/components/schemas/ServerSettingDto"
}
}
}
}
}
}
},
"/api/Settings/test-email-url": {
"post": {
"tags": [
"Settings"
],
"summary": "Sends a test email from the Email Service. Will not send if email service is the Default Provider",
"summary": "Sends a test email from the Email Service.",
"requestBody": {
"description": "",
"content": {
@ -10164,6 +10056,36 @@
}
}
},
"/api/Settings/is-email-setup": {
"get": {
"tags": [
"Settings"
],
"summary": "Is the minimum information setup for Email to work",
"responses": {
"200": {
"description": "Success",
"content": {
"text/plain": {
"schema": {
"type": "boolean"
}
},
"application/json": {
"schema": {
"type": "boolean"
}
},
"text/json": {
"schema": {
"type": "boolean"
}
}
}
}
}
}
},
"/api/Settings/task-frequencies": {
"get": {
"tags": [
@ -14965,6 +14887,10 @@
"errorMessage": {
"type": "string",
"nullable": true
},
"emailAddress": {
"type": "string",
"nullable": true
}
},
"additionalProperties": false,
@ -15615,6 +15541,25 @@
},
"additionalProperties": false
},
"InviteUserResponse": {
"type": "object",
"properties": {
"emailLink": {
"type": "string",
"description": "Email link used to setup the user account",
"nullable": true
},
"emailSent": {
"type": "boolean",
"description": "Was an email sent (ie is this server accessible)"
},
"invalidEmail": {
"type": "boolean",
"description": "When a user has an invalid email and is attempting to perform a flow."
}
},
"additionalProperties": false
},
"JobDto": {
"type": "object",
"properties": {
@ -16258,24 +16203,6 @@
"additionalProperties": false,
"description": "Represents a member of a Kavita server."
},
"MigrateUserEmailDto": {
"type": "object",
"properties": {
"email": {
"type": "string",
"nullable": true
},
"username": {
"type": "string",
"nullable": true
},
"password": {
"type": "string",
"nullable": true
}
},
"additionalProperties": false
},
"NextExpectedChapterDto": {
"type": "object",
"properties": {
@ -18572,11 +18499,6 @@
"description": "Where Bookmarks are stored.",
"nullable": true
},
"emailServiceUrl": {
"type": "string",
"description": "Email service to use for the invite user flow, forgot password, etc.",
"nullable": true
},
"installVersion": {
"type": "string",
"nullable": true
@ -18640,6 +18562,9 @@
"type": "integer",
"description": "How large the cover images should be",
"format": "int32"
},
"smtpConfig": {
"$ref": "#/components/schemas/SmtpConfigDto"
}
},
"additionalProperties": false
@ -18922,6 +18847,48 @@
},
"additionalProperties": false
},
"SmtpConfigDto": {
"type": "object",
"properties": {
"senderAddress": {
"type": "string",
"nullable": true
},
"senderDisplayName": {
"type": "string",
"nullable": true
},
"userName": {
"type": "string",
"nullable": true
},
"password": {
"type": "string",
"nullable": true
},
"host": {
"type": "string",
"nullable": true
},
"port": {
"type": "integer",
"format": "int32"
},
"enableSsl": {
"type": "boolean"
},
"sizeLimit": {
"type": "integer",
"description": "Limit in bytes for allowing files to be added as attachments. Defaults to 25MB",
"format": "int32"
},
"customizedTemplates": {
"type": "boolean",
"description": "Should Kavita use config/templates for Email templates or the default ones"
}
},
"additionalProperties": false
},
"SortOptions": {
"type": "object",
"properties": {