using Microsoft.Extensions.Localization; using MongoDB.Entities; using PrivaPub.Infrastructure.Jobs; using PrivaPub.Models.Jobs; using PrivaPub.Models.User; using PrivaPub.Resources; using System.Security.Cryptography; using System.Text.Json; namespace PrivaPub.Services { public sealed record EmailVerificationPayload(string VerificationId); // Sends the code that proves a login owns the address it asked for. The code exists only in the email; the row keeps its // hash. An address another login has verified meanwhile gets nothing, so the answer to the request never told whether // the address was taken, and no stranger's inbox receives a code it didn't ask for twice. public class EmailVerificationJob : IJobHandler { public const string Host = "email"; readonly IMailer _mailer; readonly IStringLocalizer _localizer; readonly ILogger _logger; public EmailVerificationJob(IMailer mailer, IStringLocalizer localizer, ILogger logger) { _mailer = mailer; _localizer = localizer; _logger = logger; } public JobKind Kind => JobKind.SendEmailVerification; public int Concurrency => 1; public int MaxAttempts => 3; public int PerHostLimit => 1; public static string NewCode() => RandomNumberGenerator.GetInt32(0, 1_000_000).ToString("D6"); public async Task Handle(Job job, CancellationToken token) { var payload = JsonSerializer.Deserialize(job.Payload); var now = DateTime.UtcNow; var pending = string.IsNullOrEmpty(payload?.VerificationId) ? default : await DB.Default.Find().Match(v => v.ID == payload.VerificationId && v.ExpiresAt > now).ExecuteFirstAsync(token); if (pending == default) return JobOutcome.Done; var user = await DB.Default.Find().MatchID(pending.RootUserId).ExecuteFirstAsync(token); if (user is not { DeletedAt: null, IsBanned: false }) return JobOutcome.Done; if (await DB.Default.Find().Match(u => u.Email == pending.Email && u.IsEmailValidated && u.ID != user.ID && u.DeletedAt == null).ExecuteAnyAsync(token)) return JobOutcome.Done; var code = NewCode(); await DB.Default.Update().MatchID(pending.ID).Modify(v => v.CodeHash, RecoveryJob.Hash(code)).ExecuteAsync(token); try { await _mailer.Send(pending.Email, _localizer["PrivaPub - Confirm your email address"], string.Format(_localizer[ "Hello,\n\nThe PrivaPub login {0} asked to use this address. If it was you, enter this code within an hour:\n\n{1}\n\nIf it was not you, ignore this email: the address is not used."], user.UserName, code), token); return JobOutcome.Done; } catch (Exception ex) when (ex is not OperationCanceledException || !token.IsCancellationRequested) { _logger.LogWarning(ex, "The verification email could not be sent"); return JobOutcome.Retry("smtp"); } } } }