diff --git a/API/Controllers/AccountController.cs b/API/Controllers/AccountController.cs index 0c51391bd..c48546837 100644 --- a/API/Controllers/AccountController.cs +++ b/API/Controllers/AccountController.cs @@ -395,7 +395,7 @@ public class AccountController : BaseApiController // Send a confirmation email try { - var emailLink = await _accountService.GenerateEmailLink(Request, user.ConfirmationToken, "confirm-email-update", dto.Email); + var emailLink = await _emailService.GenerateEmailLink(Request, user.ConfirmationToken, "confirm-email-update", dto.Email); _logger.LogCritical("[Update Email]: Email Link for {UserName}: {Link}", user.UserName, emailLink); if (!_emailService.IsValidEmail(user.Email)) @@ -585,7 +585,7 @@ public class AccountController : BaseApiController if (string.IsNullOrEmpty(user.ConfirmationToken)) return BadRequest(await _localizationService.Translate(User.GetUserId(), "manual-setup-fail")); - return await _accountService.GenerateEmailLink(Request, user.ConfirmationToken, "confirm-email", user.Email!, withBaseUrl); + return await _emailService.GenerateEmailLink(Request, user.ConfirmationToken, "confirm-email", user.Email!, withBaseUrl); } @@ -691,7 +691,7 @@ public class AccountController : BaseApiController try { - var emailLink = await _accountService.GenerateEmailLink(Request, user.ConfirmationToken, "confirm-email", dto.Email); + var emailLink = await _emailService.GenerateEmailLink(Request, user.ConfirmationToken, "confirm-email", dto.Email); _logger.LogCritical("[Invite User]: Email Link for {UserName}: {Link}", user.UserName, emailLink); var settings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(); @@ -911,7 +911,7 @@ public class AccountController : BaseApiController } var token = await _userManager.GeneratePasswordResetTokenAsync(user); - var emailLink = await _accountService.GenerateEmailLink(Request, token, "confirm-reset-password", user.Email); + var emailLink = await _emailService.GenerateEmailLink(Request, token, "confirm-reset-password", user.Email); user.ConfirmationToken = token; _unitOfWork.UserRepository.Update(user); await _unitOfWork.CommitAsync(); @@ -989,7 +989,7 @@ public class AccountController : BaseApiController user.ConfirmationToken = token; _unitOfWork.UserRepository.Update(user); await _unitOfWork.CommitAsync(); - var emailLink = await _accountService.GenerateEmailLink(Request, token, "confirm-email-update", user.Email); + var emailLink = await _emailService.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)) diff --git a/API/Controllers/DeviceController.cs b/API/Controllers/DeviceController.cs index 05d9af478..175ebf3bd 100644 --- a/API/Controllers/DeviceController.cs +++ b/API/Controllers/DeviceController.cs @@ -126,7 +126,7 @@ public class DeviceController : BaseApiController } finally { - await _eventHub.SendMessageToAsync(MessageFactory.SendingToDevice, + await _eventHub.SendMessageToAsync(MessageFactory.NotificationProgress, MessageFactory.SendingToDeviceEvent(await _localizationService.Translate(userId, "send-to-device-status"), "ended"), userId); } @@ -167,7 +167,7 @@ public class DeviceController : BaseApiController } finally { - await _eventHub.SendMessageToAsync(MessageFactory.SendingToDevice, + await _eventHub.SendMessageToAsync(MessageFactory.NotificationProgress, MessageFactory.SendingToDeviceEvent(await _localizationService.Translate(User.GetUserId(), "send-to-device-status"), "ended"), userId); } @@ -175,8 +175,6 @@ public class DeviceController : BaseApiController return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-send-to")); } - - } diff --git a/API/Controllers/LibraryController.cs b/API/Controllers/LibraryController.cs index d043188d8..b4b86dccf 100644 --- a/API/Controllers/LibraryController.cs +++ b/API/Controllers/LibraryController.cs @@ -112,6 +112,13 @@ public class LibraryController : BaseApiController if (!await _unitOfWork.CommitAsync()) return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-library")); _logger.LogInformation("Created a new library: {LibraryName}", library.Name); + // Restart Folder watching if on + var settings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(); + if (settings.EnableFolderWatching) + { + await _libraryWatcher.RestartWatching(); + } + // Assign all the necessary users with this library side nav var userIds = admins.Select(u => u.Id).Append(User.GetUserId()).ToList(); var userNeedingNewLibrary = (await _unitOfWork.UserRepository.GetAllUsersAsync(AppUserIncludes.SideNavStreams)) diff --git a/API/Controllers/ServerController.cs b/API/Controllers/ServerController.cs index 5c08a2cc5..b0de79c4c 100644 --- a/API/Controllers/ServerController.cs +++ b/API/Controllers/ServerController.cs @@ -209,18 +209,6 @@ public class ServerController : BaseApiController return Ok(await _versionUpdaterService.GetAllReleases()); } - /// - /// Is this server accessible to the outside net - /// - /// If the instance has the HostName set, this will return true whether or not it is accessible externally - /// - [HttpGet("accessible")] - [AllowAnonymous] - public async Task> IsServerAccessible() - { - return Ok(await _accountService.CheckIfAccessible(Request)); - } - /// /// Returns a list of reoccurring jobs. Scheduled ad-hoc jobs will not be returned. /// diff --git a/API/Controllers/SettingsController.cs b/API/Controllers/SettingsController.cs index 0a80e60fb..1c384e7fe 100644 --- a/API/Controllers/SettingsController.cs +++ b/API/Controllers/SettingsController.cs @@ -500,4 +500,16 @@ public class SettingsController : BaseApiController // NOTE: This must match Hangfire's underlying cron system. Hangfire is unique return Ok(CronHelper.IsValidCron(cronExpression)); } + + /// + /// Sends a test email to see if email settings are hooked up correctly + /// + /// + [Authorize(Policy = "RequireAdminRole")] + [HttpPost("test-email-url")] + public async Task> TestEmailServiceUrl() + { + var user = await _unitOfWork.UserRepository.GetUserByIdAsync(User.GetUserId()); + return Ok(await _emailService.SendTestEmail(user!.Email)); + } } diff --git a/API/Services/AccountService.cs b/API/Services/AccountService.cs index aebc24ed7..45c88dd3f 100644 --- a/API/Services/AccountService.cs +++ b/API/Services/AccountService.cs @@ -27,9 +27,6 @@ public interface IAccountService Task HasBookmarkPermission(AppUser? user); Task HasDownloadPermission(AppUser? user); Task HasChangeRestrictionRole(AppUser? user); - Task CheckIfAccessible(HttpRequest request); - Task GenerateEmailLink(HttpRequest request, string token, string routePart, string email, bool withHost = true); - } public class AccountService : IAccountService @@ -37,50 +34,13 @@ public class AccountService : IAccountService private readonly UserManager _userManager; private readonly ILogger _logger; private readonly IUnitOfWork _unitOfWork; - private readonly IHostEnvironment _environment; - private readonly IEmailService _emailService; public const string DefaultPassword = "[k.2@RZ!mxCQkJzE"; - private const string LocalHost = "localhost:4200"; - public AccountService(UserManager userManager, ILogger logger, IUnitOfWork unitOfWork, - IHostEnvironment environment, IEmailService emailService) + public AccountService(UserManager userManager, ILogger logger, IUnitOfWork unitOfWork) { _userManager = userManager; _logger = logger; _unitOfWork = unitOfWork; - _environment = environment; - _emailService = emailService; - } - - /// - /// Checks if the instance is accessible. If the host name is filled out, then it will assume it is accessible as email generation will use host name. - /// - /// - /// - public async Task CheckIfAccessible(HttpRequest request) - { - var host = _environment.IsDevelopment() ? LocalHost : request.Host.ToString(); - return !string.IsNullOrEmpty((await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).HostName) || await _emailService.CheckIfAccessible(host); - } - - public async Task GenerateEmailLink(HttpRequest request, string token, string routePart, string email, bool withHost = true) - { - var serverSettings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(); - var host = _environment.IsDevelopment() ? LocalHost : request.Host.ToString(); - var basePart = $"{request.Scheme}://{host}{request.PathBase}/"; - if (!string.IsNullOrEmpty(serverSettings.HostName)) - { - basePart = serverSettings.HostName; - if (!serverSettings.BaseUrl.Equals(Configuration.DefaultBaseUrl)) - { - var removeCount = serverSettings.BaseUrl.EndsWith('/') ? 1 : 0; - basePart += serverSettings.BaseUrl.Substring(0, serverSettings.BaseUrl.Length - removeCount); - } - } - - if (withHost) return $"{basePart}/registration/{routePart}?token={HttpUtility.UrlEncode(token)}&email={HttpUtility.UrlEncode(email)}"; - return $"registration/{routePart}?token={HttpUtility.UrlEncode(token)}&email={HttpUtility.UrlEncode(email)}" - .Replace("//", "/"); } public async Task> ChangeUserPassword(AppUser user, string newPassword) diff --git a/API/Services/EmailService.cs b/API/Services/EmailService.cs index afe582145..848b41e23 100644 --- a/API/Services/EmailService.cs +++ b/API/Services/EmailService.cs @@ -5,10 +5,13 @@ using System.IO; using System.Linq; using System.Net; using System.Threading.Tasks; +using System.Web; using API.Data; using API.DTOs.Email; using Kavita.Common; using MailKit.Security; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using MimeKit; @@ -36,6 +39,9 @@ public interface IEmailService Task SendTestEmail(string adminEmail); Task SendEmailChangeEmail(ConfirmationEmailDto data); bool IsValidEmail(string email); + + Task GenerateEmailLink(HttpRequest request, string token, string routePart, string email, + bool withHost = true); } public class EmailService : IEmailService @@ -43,19 +49,17 @@ public class EmailService : IEmailService private readonly ILogger _logger; private readonly IUnitOfWork _unitOfWork; private readonly IDirectoryService _directoryService; + private readonly IHostEnvironment _environment; private const string TemplatePath = @"{0}.html"; - /// - /// This is used to initially set or reset the ServerSettingKey. Do not access from the code, access via UnitOfWork - /// - public const string DefaultApiUrl = "https://email.kavitareader.com"; + private const string LocalHost = "localhost:4200"; - - public EmailService(ILogger logger, IUnitOfWork unitOfWork, IDirectoryService directoryService) + public EmailService(ILogger logger, IUnitOfWork unitOfWork, IDirectoryService directoryService, IHostEnvironment environment) { _logger = logger; _unitOfWork = unitOfWork; _directoryService = directoryService; + _environment = environment; } /// @@ -142,6 +146,26 @@ public class EmailService : IEmailService return new EmailAddressAttribute().IsValid(email); } + public async Task GenerateEmailLink(HttpRequest request, string token, string routePart, string email, bool withHost = true) + { + var serverSettings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(); + var host = _environment.IsDevelopment() ? LocalHost : request.Host.ToString(); + var basePart = $"{request.Scheme}://{host}{request.PathBase}"; + if (!string.IsNullOrEmpty(serverSettings.HostName)) + { + basePart = serverSettings.HostName; + if (!serverSettings.BaseUrl.Equals(Configuration.DefaultBaseUrl)) + { + var removeCount = serverSettings.BaseUrl.EndsWith('/') ? 1 : 0; + basePart += serverSettings.BaseUrl[..^removeCount]; + } + } + + if (withHost) return $"{basePart}/registration/{routePart}?token={HttpUtility.UrlEncode(token)}&email={HttpUtility.UrlEncode(email)}"; + return $"registration/{routePart}?token={HttpUtility.UrlEncode(token)}&email={HttpUtility.UrlEncode(email)}" + .Replace("//", "/"); + } + /// /// Sends an invite email to a user to setup their account /// diff --git a/Kavita.Common/EnvironmentInfo/IOsInfo.cs b/Kavita.Common/EnvironmentInfo/IOsInfo.cs index 1f851be00..d8cc6a070 100644 --- a/Kavita.Common/EnvironmentInfo/IOsInfo.cs +++ b/Kavita.Common/EnvironmentInfo/IOsInfo.cs @@ -10,7 +10,9 @@ public static class OsInfo public static bool IsLinux => Os is Os.Linux or Os.LinuxMusl or Os.Bsd; public static bool IsOsx => Os == Os.Osx; public static bool IsWindows => Os == Os.Windows; - public static bool IsDocker => Environment.GetEnvironmentVariable("DOTNET_RUNNING_IN_CONTAINER") == "true"; + public static bool IsDocker => + Environment.GetEnvironmentVariable("DOTNET_RUNNING_IN_CONTAINER") == "true" || + Environment.GetEnvironmentVariable("LSIO_FIRST_PARTY") == "true"; static OsInfo() { diff --git a/UI/Web/src/app/_services/library.service.ts b/UI/Web/src/app/_services/library.service.ts index 176ce9042..75abf3a03 100644 --- a/UI/Web/src/app/_services/library.service.ts +++ b/UI/Web/src/app/_services/library.service.ts @@ -1,11 +1,13 @@ import { HttpClient } from '@angular/common/http'; -import { Injectable } from '@angular/core'; +import {DestroyRef, Injectable} from '@angular/core'; import { of } from 'rxjs'; -import { map } from 'rxjs/operators'; +import {filter, map, tap} from 'rxjs/operators'; import { environment } from 'src/environments/environment'; import { JumpKey } from '../_models/jumpbar/jump-key'; import { Library, LibraryType } from '../_models/library/library'; import { DirectoryDto } from '../_models/system/directory-dto'; +import {EVENTS, MessageHubService} from "./message-hub.service"; +import {takeUntilDestroyed} from "@angular/core/rxjs-interop"; @Injectable({ @@ -18,7 +20,12 @@ export class LibraryService { private libraryNames: {[key:number]: string} | undefined = undefined; private libraryTypes: {[key: number]: LibraryType} | undefined = undefined; - constructor(private httpClient: HttpClient) {} + constructor(private httpClient: HttpClient, private readonly messageHub: MessageHubService, private readonly destroyRef: DestroyRef) { + this.messageHub.messages$.pipe(takeUntilDestroyed(this.destroyRef), filter(e => e.event === EVENTS.LibraryModified), + tap((e) => { + this.libraryNames = undefined; + })).subscribe(); + } getLibraryNames() { if (this.libraryNames != undefined) { diff --git a/UI/Web/src/app/_services/server.service.ts b/UI/Web/src/app/_services/server.service.ts index 3ccdad395..4fb37c283 100644 --- a/UI/Web/src/app/_services/server.service.ts +++ b/UI/Web/src/app/_services/server.service.ts @@ -50,10 +50,6 @@ export class ServerService { return this.http.get(this.baseUrl + 'server/changelog', {}); } - isServerAccessible() { - return this.http.get(this.baseUrl + 'server/accessible'); - } - getRecurringJobs() { return this.http.get(this.baseUrl + 'server/jobs'); } diff --git a/UI/Web/src/app/admin/manage-email-settings/manage-email-settings.component.html b/UI/Web/src/app/admin/manage-email-settings/manage-email-settings.component.html index 8055973e0..5a98d7651 100644 --- a/UI/Web/src/app/admin/manage-email-settings/manage-email-settings.component.html +++ b/UI/Web/src/app/admin/manage-email-settings/manage-email-settings.component.html @@ -1,7 +1,7 @@
-

{{t('title')}}

+

{{t('title')}}

You must fill out both Host Name and SMTP settings to use email-based functionality within Kavita.

@@ -11,8 +11,13 @@ - +
+ + + +
+
{{t('host-name-validation')}} @@ -27,15 +32,17 @@ {{t('sender-address-tooltip')}} - +
+ +
{{t('sender-displayname-tooltip')}} - +
@@ -45,12 +52,12 @@ {{t('host-tooltip')}} - +
- +
@@ -68,12 +75,12 @@ {{t('username-tooltip')}} - +
- +
@@ -83,7 +90,7 @@ {{t('size-limit-tooltip')}} - +
diff --git a/UI/Web/src/app/admin/manage-email-settings/manage-email-settings.component.ts b/UI/Web/src/app/admin/manage-email-settings/manage-email-settings.component.ts index 1ace95653..d19598d3c 100644 --- a/UI/Web/src/app/admin/manage-email-settings/manage-email-settings.component.ts +++ b/UI/Web/src/app/admin/manage-email-settings/manage-email-settings.component.ts @@ -1,4 +1,4 @@ -import {ChangeDetectionStrategy, ChangeDetectorRef, Component, inject, OnInit} from '@angular/core'; +import {ChangeDetectionStrategy, ChangeDetectorRef, Component, DestroyRef, inject, OnInit} from '@angular/core'; import {FormControl, FormGroup, ReactiveFormsModule, Validators} from '@angular/forms'; import {ToastrService} from 'ngx-toastr'; import {take} from 'rxjs'; @@ -15,6 +15,8 @@ import {NgForOf, NgIf, NgTemplateOutlet, TitleCasePipe} from '@angular/common'; import {translate, TranslocoModule} from "@ngneat/transloco"; import {SafeHtmlPipe} from "../../_pipes/safe-html.pipe"; import {ManageAlertsComponent} from "../manage-alerts/manage-alerts.component"; +import {takeUntilDestroyed} from "@angular/core/rxjs-interop"; +import {filter} from "rxjs/operators"; @Component({ selector: 'app-manage-email-settings', @@ -22,7 +24,9 @@ import {ManageAlertsComponent} from "../manage-alerts/manage-alerts.component"; styleUrls: ['./manage-email-settings.component.scss'], standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, - imports: [NgIf, ReactiveFormsModule, NgbTooltip, NgTemplateOutlet, TranslocoModule, SafeHtmlPipe, ManageAlertsComponent, NgbAccordionBody, NgbAccordionButton, NgbAccordionCollapse, NgbAccordionDirective, NgbAccordionHeader, NgbAccordionItem, NgForOf, TitleCasePipe] + imports: [NgIf, ReactiveFormsModule, NgbTooltip, NgTemplateOutlet, TranslocoModule, SafeHtmlPipe, + ManageAlertsComponent, NgbAccordionBody, NgbAccordionButton, NgbAccordionCollapse, NgbAccordionDirective, + NgbAccordionHeader, NgbAccordionItem, NgForOf, TitleCasePipe] }) export class ManageEmailSettingsComponent implements OnInit { @@ -47,6 +51,7 @@ export class ManageEmailSettingsComponent implements OnInit { 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(); }); } @@ -67,6 +72,22 @@ export class ManageEmailSettingsComponent implements OnInit { this.cdRef.markForCheck(); } + autofillGmail() { + this.settingsForm.get('host')?.setValue('smtp.gmail.com'); + this.settingsForm.get('port')?.setValue(587); + this.settingsForm.get('sizeLimit')?.setValue(26214400); + this.settingsForm.get('enableSsl')?.setValue(true); + this.cdRef.markForCheck(); + } + + autofillOutlook() { + this.settingsForm.get('host')?.setValue('smtp-mail.outlook.com'); + this.settingsForm.get('port')?.setValue(587 ); + this.settingsForm.get('sizeLimit')?.setValue(1048576); + this.settingsForm.get('enableSsl')?.setValue(true); + this.cdRef.markForCheck(); + } + async saveSettings() { const modelSettings = Object.assign({}, this.serverSettings); modelSettings.emailServiceUrl = this.settingsForm.get('emailServiceUrl')?.value; diff --git a/UI/Web/src/app/manga-reader/_components/infinite-scroller/infinite-scroller.component.ts b/UI/Web/src/app/manga-reader/_components/infinite-scroller/infinite-scroller.component.ts index 4ac1c465c..5329fee8b 100644 --- a/UI/Web/src/app/manga-reader/_components/infinite-scroller/infinite-scroller.component.ts +++ b/UI/Web/src/app/manga-reader/_components/infinite-scroller/infinite-scroller.component.ts @@ -206,9 +206,9 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy { .pipe(debounceTime(20), takeUntilDestroyed(this.destroyRef)) .subscribe((event) => this.handleScrollEvent(event)); - // fromEvent(this.isFullscreenMode ? this.readerElemRef.nativeElement : this.document.body, 'scrollend') - // .pipe(debounceTime(20), takeUntilDestroyed(this.destroyRef)) - // .subscribe((event) => this.handleScrollEndEvent(event)); + fromEvent(this.isFullscreenMode ? this.readerElemRef.nativeElement : this.document.body, 'scrollend') + .pipe(debounceTime(20), takeUntilDestroyed(this.destroyRef)) + .subscribe((event) => this.handleScrollEndEvent(event)); } ngOnInit(): void { diff --git a/UI/Web/src/assets/langs/en.json b/UI/Web/src/assets/langs/en.json index 1a89ac738..59d4daec1 100644 --- a/UI/Web/src/assets/langs/en.json +++ b/UI/Web/src/assets/langs/en.json @@ -1094,6 +1094,8 @@ "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.", + "gmail-label": "Gmail", + "outlook-label": "Outlook", "reset-to-default": "{{common.reset-to-default}}", "save": "{{common.save}}" diff --git a/openapi.json b/openapi.json index aa0986bd9..f75e079db 100644 --- a/openapi.json +++ b/openapi.json @@ -7,7 +7,7 @@ "name": "GPL-3.0", "url": "https://github.com/Kareadita/Kavita/blob/develop/LICENSE" }, - "version": "0.7.13.2" + "version": "0.7.13.5" }, "servers": [ {