public async Task <JsonResult> SendTestEmail(string emailAddress) { try { var site = await GetCurrentSiteAsync(); var link = await _siteLookupService.GetSiteLinkAsync(site.Id); var details = new DirectEmailDetails(site.Name) { DirectEmailSystemId = "TestMessage", IsTest = true, SendingUserId = GetActiveUserId(), ToAddress = emailAddress, }; details.Tags.Add("Sitelink", link.ToString()); var result = await _emailSerivce.SendDirectAsync(details); return(Json(new { success = result.Successful, message = result.SentResponse })); } catch (Exception ex) { _logger.LogError(ex, "Error sending test email: {Message}", ex.Message); return(Json(new { success = false, message = ex.Message })); } }
private async Task <JobStatus> SendBulkListAsync(int userId, int jobId, IProgress <JobStatus> progress, JobDetailsSendBulkEmails jobDetails, CancellationToken token) { var sw = System.Diagnostics.Stopwatch.StartNew(); int emailsSent = 0; int emailsSkipped = 0; int userCounter = 0; int addSentCounter = 0; var problemEmails = new HashSet <string>(); var filter = new EmailReminderFilter { MailingList = jobDetails.MailingList, Take = 30 }; var subscribers = await _emailReminderService.GetSubscribersWithCountAsync(filter); var subscribedCount = subscribers.Count; var subscribed = subscribers.Data; _logger.LogInformation("Email job {JobId}: found {Count} subscribed users, processing first batch of {BatchCount}", jobId, subscribedCount, subscribed.Count); token.Register(() => { _logger.LogWarning("Email job {JobId} for user {UserId} was cancelled after {EmailsSent} sent, {EmailsSkipped} skipped of {SubscribedUsersCount} total in {TimeElapsed}.", jobId, userId, emailsSent, emailsSkipped, subscribedCount, sw.Elapsed.ToString(SpanFormat, CultureInfo.InvariantCulture)); }); if (subscribed.Count > 0) { var site = await _siteLookupService.GetByIdAsync(GetCurrentSiteId()); var emailDetails = new DirectEmailDetails(site.Name) { IsBulk = true, SendingUserId = userId, DirectEmailTemplateId = jobDetails.EmailTemplateId }; var elapsedStatus = sw.Elapsed; var elapsedUpdateDbStatus = sw.Elapsed; var elapsedLogInfoStatus = sw.Elapsed; var elapsedLogInfoPercent = 0; progress.Report(new JobStatus { PercentComplete = 0, Title = $"Sending email...", Status = $"Preparing to email {subscribed.Count} participants...", Error = false }); while (subscribed.Count > 0) { foreach (var emailReminder in subscribed) { if (problemEmails.Contains(emailReminder.Email)) { emailsSkipped++; continue; } bool clearToSend = true; var isParticipant = await _userService .IsEmailSubscribedAsync(emailReminder.Email); if (isParticipant) { clearToSend = false; } if (emailReminder.SentAt != null || !clearToSend) { // send email _logger.LogTrace("Email job {JobId}: skipping email {Count}/{Total} to {Email}: {Message}", jobId, userCounter + 1, subscribedCount, emailReminder.Email, emailReminder.SentAt != null ? " already sent at " + emailReminder.SentAt : " is a subscribed participant"); emailsSkipped++; } else { // send email _logger.LogTrace("Email job {JobId}: sending email {Count}/{Total} to {Email} with template {EmailTemplate}", jobId, userCounter + 1, subscribedCount, emailReminder.Email, jobDetails.EmailTemplateId); // send email to user try { emailDetails.ToAddress = emailReminder.Email; emailDetails.LanguageId = emailReminder.LanguageId; emailDetails.ClearTags(); emailDetails.SetTag("Email", emailReminder.Email); DirectEmailHistory result = null; try { result = await _emailService.SendDirectAsync(emailDetails); await _emailReminderService.UpdateSentDateAsync(emailReminder.Id); } catch (Exception ex) { _logger.LogWarning("Unable to email {ToAddress}: {ErrorMessage}", emailDetails.ToAddress, ex.Message); } if (result?.Successful == true) { addSentCounter++; emailsSent++; } else { problemEmails.Add(emailReminder.Email); } } catch (Exception ex) { _logger.LogError(ex, "Email job {JobId}: Send failed to {UserId} at {Email}: {ErrorMessage}", jobId, emailReminder.Id, emailReminder.Email, ex.Message); problemEmails.Add(emailReminder.Email); } } userCounter++; if (token.IsCancellationRequested) { break; } if (sw.Elapsed.TotalSeconds - elapsedStatus.TotalSeconds > 5 || userCounter == 1) { elapsedStatus = sw.Elapsed; var remaining = TimeSpan .FromMilliseconds(elapsedStatus.TotalMilliseconds / userCounter * (subscribedCount - userCounter)) .ToString(SpanFormat, CultureInfo.InvariantCulture); var status = new JobStatus { PercentComplete = userCounter * 100 / subscribedCount, Status = $"Sent {emailsSent}, skipped {emailsSkipped} of {subscribedCount}; {elapsedStatus.ToString(SpanFormat, CultureInfo.InvariantCulture)}, remaining: {remaining}, problems: {problemEmails.Count}", Error = false }; progress.Report(status); if (sw.Elapsed.TotalSeconds - elapsedUpdateDbStatus.TotalSeconds > 60 || userCounter == 1) { elapsedUpdateDbStatus = sw.Elapsed; if (addSentCounter > 0) { await _emailService.IncrementSentCountAsync( jobDetails.EmailTemplateId, addSentCounter); addSentCounter = 0; } var dbStatusText = string.Format(CultureInfo.InvariantCulture, "{0}%: {1}", status.PercentComplete, status.Status); await _jobRepository.UpdateStatusAsync(jobId, dbStatusText[..Math.Min(dbStatusText.Length, 255)]);
public async Task <Models.ServiceResult> EmailAllUsernames(string email) { var site = await _siteLookupService.GetByIdAsync(GetCurrentSiteId()); var lookupEmail = email.Trim(); var usernames = await _userRepository.GetUserIdAndUsernames(lookupEmail); if (usernames?.Data.Any() != true) { return(new Models.ServiceResult { Status = Models.ServiceResultStatus.Error, Message = "There are no usernames associated with the email address: {0}.", Arguments = new[] { lookupEmail } }); } var sb = new StringBuilder(); foreach (string username in usernames.Data) { sb.Append("- ").AppendLine(username); } var directEmailDetails = new DirectEmailDetails(site.Name) { DirectEmailSystemId = "UsernameRecovery", LanguageId = await _languageService .GetLanguageIdAsync(CultureInfo.CurrentUICulture.Name), SendingUserId = await _userRepository.GetSystemUserId(), ToUserId = usernames.Id }; directEmailDetails.Tags.Add("Email", lookupEmail); directEmailDetails.Tags.Add("Content", sb.ToString()); var siteLink = await _siteLookupService.GetSiteLinkAsync(site.Id); directEmailDetails.Tags.Add("Sitelink", siteLink.AbsoluteUri); var result = new Models.ServiceResult(); try { var history = await _emailService.SendDirectAsync(directEmailDetails); result.Status = history?.Successful == true ? Models.ServiceResultStatus.Success : Models.ServiceResultStatus.Error; } catch (GraException ex) { if (ex?.InnerException is MimeKit.ParseException) { result.Status = Models.ServiceResultStatus.Error; result.Message = Annotations.Validate.EmailAddressInvalid; result.Arguments = new[] { email }; } } return(result); }
public async Task <Models.ServiceResult> GenerateTokenAndEmail(string username, string recoveryUrl) { string trimmedUsername = username.Trim(); var user = await _userRepository.GetByUsernameAsync(trimmedUsername); if (user == null) { _logger.LogInformation("Username {Username} doesn't exist so can't create a recovery token.", trimmedUsername); return(new Models.ServiceResult { Status = Models.ServiceResultStatus.Error, Message = Annotations.Validate.Username, Arguments = new[] { trimmedUsername } }); } if (string.IsNullOrEmpty(user.Email)) { _logger.LogInformation("User {Username} ({UserId}) doesn't have an email address configured so cannot send a recovery token.", user?.Username, user?.Id); return(new Models.ServiceResult { Status = Models.ServiceResultStatus.Error, Message = Annotations.Validate.EmailConfigured, Arguments = new[] { trimmedUsername } }); } // clear any existing tokens var existingRequests = await _recoveryTokenRepository.GetByUserIdAsync(user.Id); foreach (var request in existingRequests) { await _recoveryTokenRepository.RemoveSaveAsync(-1, request.Id); } string tokenString = _tokenGenerator.Generate().ToUpperInvariant().Trim(); // insert new token await _recoveryTokenRepository.AddSaveAsync(-1, new RecoveryToken { Token = tokenString.ToLowerInvariant(), UserId = user.Id }); _logger.LogInformation("Cleared {Existing} existing recovery tokens and inserted a new one for {Username} ({UserId})", existingRequests.Count(), user?.Username, user.Id); var site = await _siteLookupService.GetByIdAsync(GetCurrentSiteId()); var directEmailDetails = new DirectEmailDetails(site.Name) { DirectEmailSystemId = "PasswordRecovery", LanguageId = await _languageService .GetLanguageIdAsync(CultureInfo.CurrentUICulture.Name), SendingUserId = await _userRepository.GetSystemUserId(), ToUserId = user.Id }; directEmailDetails.Tags.Add("RecoveryLink", $"{recoveryUrl}?username={trimmedUsername}&token={tokenString}"); directEmailDetails.Tags.Add("RecoveryBaseLink", recoveryUrl); directEmailDetails.Tags.Add("Username", trimmedUsername); directEmailDetails.Tags.Add("Token", tokenString); var siteLink = await _siteLookupService.GetSiteLinkAsync(site.Id); directEmailDetails.Tags.Add("Sitelink", siteLink.AbsoluteUri); var result = new Models.ServiceResult(); try { var history = await _emailService.SendDirectAsync(directEmailDetails); result.Status = history?.Successful == true ? Models.ServiceResultStatus.Success : Models.ServiceResultStatus.Error; } catch (GraException ex) { if (ex?.InnerException is MimeKit.ParseException) { result.Status = Models.ServiceResultStatus.Error; result.Message = Annotations.Validate.AssociatedEmailAddressInvalid; result.Arguments = new[] { username }; } } return(result); }
public async Task <JobStatus> RunSendNewsEmailsJob(int jobId, System.Threading.CancellationToken token, IProgress <JobStatus> progress) { var sw = System.Diagnostics.Stopwatch.StartNew(); var job = await _jobRepository.GetByIdAsync(jobId); var jobDetails = JsonConvert.DeserializeObject <JobSendNewsEmails>(job.SerializedParameters); _logger.LogInformation("Job {JobId}: {JobType} to send emails for post {NewsPostId}", job.Id, job.JobType, jobDetails.NewsPostId); token.Register(() => { _logger.LogWarning("Job {JobId}: {JobType} to send emails for post {NewsPostId} cancelled after {Elapsed} ms", job.Id, job.JobType, sw?.Elapsed.TotalMilliseconds); }); var post = await _newsPostRepository.GetByIdAsync(jobDetails.NewsPostId); if (string.IsNullOrEmpty(post.CategoryName)) { var category = await _newsCategoryRepository.GetByIdAsync(post.CategoryId); post.CategoryName = category?.Name; } if (post == null) { await _jobRepository.UpdateStatusAsync(jobId, $"Could not locate news post id {jobDetails.NewsPostId} to send emails."); return(new JobStatus { Complete = true, Error = true, Status = $"Could not locate news post id {jobDetails.NewsPostId} to send emails." }); } var subscribedUserIds = (await _userRepository .GetNewsSubscribedUserIdsAsync(job.SiteId)).ToList(); if (subscribedUserIds.Count == 0) { await _jobRepository.UpdateStatusAsync(jobId, "No subscribed users to send emails to."); return(new JobStatus { Complete = true, Error = false, Status = "No subscribed users to send emails to." }); } int sentEmails = 0; var lastUpdate = sw.Elapsed.TotalSeconds; await _jobRepository.UpdateStatusAsync(jobId, $"Preparing to email {subscribedUserIds.Count} users..."); progress?.Report(new JobStatus { PercentComplete = 0, Status = $"Preparing to email {subscribedUserIds.Count} users..." }); var directEmailDetails = new DirectEmailDetails(jobDetails.SiteName) { DirectEmailSystemId = "NewsPost", IsBulk = true, SendingUserId = await _userRepository.GetSystemUserId() }; directEmailDetails.Tags.Add("Category", post.CategoryName); directEmailDetails.Tags.Add("PostLink", jobDetails.PostLink); directEmailDetails.Tags.Add("PostTitle", post.Title); directEmailDetails.Tags.Add("Summary", post.EmailSummary); directEmailDetails.Tags.Add("UnsubscribeLink", jobDetails.SiteMcLink); foreach (var userId in subscribedUserIds) { if (token.IsCancellationRequested) { await _jobRepository.UpdateStatusAsync(jobId, $"Cancelling after {sentEmails}/{subscribedUserIds.Count} emails in {sw?.Elapsed.TotalMilliseconds} ms."); return(new JobStatus { PercentComplete = sentEmails * 100 / subscribedUserIds.Count, Complete = true, Status = $"Cancelling after {sentEmails}/{subscribedUserIds.Count} emails in {sw?.Elapsed.TotalMilliseconds} ms." }); } directEmailDetails.ToUserId = userId; try { var history = await _emailService.SendDirectAsync(directEmailDetails); if (history.Successful) { sentEmails++; } else { _logger.LogWarning("Unable to send newsletter notification email to user {UserId}", userId); } } catch (Exception ex) { _logger.LogWarning("Unable to send newsletter notification email to user {UserId}: {ErrorMessage}", userId, ex.Message); } if (sw.Elapsed.TotalSeconds > lastUpdate + 5) { await _jobRepository.UpdateStatusAsync(jobId, $"Sent {sentEmails}/{subscribedUserIds.Count} emails..."); progress?.Report(new JobStatus { PercentComplete = sentEmails * 100 / subscribedUserIds.Count, Status = $"Sent {sentEmails}/{subscribedUserIds.Count} emails..." }); lastUpdate = sw.Elapsed.TotalSeconds; } } await _jobRepository.UpdateStatusAsync(jobId, $"Sent emails to {sentEmails}/{subscribedUserIds.Count} users in {sw?.Elapsed.TotalMilliseconds} ms."); return(new JobStatus { PercentComplete = sentEmails * 100 / subscribedUserIds.Count, Complete = true, Status = $"Sent emails to {sentEmails}/{subscribedUserIds.Count} users in {sw?.Elapsed.TotalMilliseconds} ms." }); }
public async Task <DirectEmailHistory> SendDirectAsync(DirectEmailDetails directEmailDetails) { if (directEmailDetails == null) { throw new ArgumentNullException(nameof(directEmailDetails)); } string toAddress; string toName; int languageId; Site site; if (directEmailDetails.ToUserId.HasValue) { var user = await _userRepository.GetByIdAsync(directEmailDetails.ToUserId ?? directEmailDetails.SendingUserId); if (string.IsNullOrEmpty(user.Email)) { _logger.LogError("Unable to send email to user id {UserId}: no email address configured.", directEmailDetails.ToUserId); throw new GraException($"User id {directEmailDetails.ToUserId} does not have an email address configured."); } site = await _siteLookupService.GetByIdAsync(user.SiteId); toAddress = user.Email; toName = user.FullName; languageId = directEmailDetails.LanguageId ?? (string.IsNullOrEmpty(user.Culture) ? await _languageService.GetDefaultLanguageIdAsync() : await _languageService.GetLanguageIdAsync(user.Culture)); } else { var user = await _userRepository.GetByIdAsync(directEmailDetails.SendingUserId); site = await _siteLookupService.GetByIdAsync(user.SiteId); toAddress = directEmailDetails.ToAddress; toName = directEmailDetails.ToName; languageId = directEmailDetails.LanguageId ?? await _languageService.GetLanguageIdAsync(CultureInfo.CurrentCulture.Name); } if (!SiteCanSendMail(site)) { throw new GraException("Unable to send mail, please ensure from name, from email, and outgoing mail server are configured in Site Management -> Configuration."); } var history = new DirectEmailHistory { CreatedBy = directEmailDetails.SendingUserId, FromEmailAddress = site.FromEmailAddress, FromName = site.FromEmailName, IsBulk = directEmailDetails.IsBulk, LanguageId = languageId, OverrideToEmailAddress = string .IsNullOrWhiteSpace(_config[ConfigurationKey.EmailOverride]) ? null : _config[ConfigurationKey.EmailOverride], ToEmailAddress = toAddress, ToName = toName }; if (directEmailDetails.ToUserId.HasValue) { history.UserId = directEmailDetails.ToUserId.Value; } DirectEmailTemplate directEmailTemplate = await GetDirectEmailTemplateAsync(directEmailDetails.DirectEmailSystemId, directEmailDetails.DirectEmailTemplateId, history.LanguageId); if (directEmailTemplate == null || directEmailTemplate.DirectEmailTemplateText == null) { // not available in the requested language, use the default language history.LanguageId = await _languageService.GetDefaultLanguageIdAsync(); directEmailTemplate = await GetDirectEmailTemplateAsync(directEmailDetails.DirectEmailSystemId, directEmailDetails.DirectEmailTemplateId, history.LanguageId); } history.DirectEmailTemplateId = directEmailTemplate.Id; history.EmailBaseId = directEmailTemplate.EmailBaseId; var stubble = new StubbleBuilder().Build(); history.Subject = await stubble .RenderAsync(directEmailTemplate.DirectEmailTemplateText.Subject, directEmailDetails.Tags); history.BodyText = await stubble .RenderAsync(directEmailTemplate.DirectEmailTemplateText.BodyCommonMark, directEmailDetails.Tags); history.BodyHtml = CommonMark.CommonMarkConverter.Convert(history.BodyText); string preview = await stubble .RenderAsync(directEmailTemplate.DirectEmailTemplateText.Preview, directEmailDetails.Tags); string title = await stubble .RenderAsync(directEmailTemplate.DirectEmailTemplateText.Title, directEmailDetails.Tags); string footer = CommonMark.CommonMarkConverter.Convert(await stubble .RenderAsync(directEmailTemplate.DirectEmailTemplateText.Footer, directEmailDetails.Tags)); history = await InternalSendDirectAsync(site, history, new Dictionary <string, string> { { "Footer", footer }, { "Preview", preview }, { "Title", title }, { "BodyHtml", history.BodyHtml }, { "BodyText", history.BodyText } }); if (directEmailDetails.IsBulk && !directEmailDetails.IsTest) { if (directEmailDetails.ToUserId.HasValue) { history.BodyHtml = null; history.BodyText = null; await _directEmailHistoryRepository.AddSaveNoAuditAsync(history); } if (!directEmailTemplate.SentBulk) { await _directEmailTemplateRepository .UpdateSentBulkAsync(directEmailTemplate.Id); } } else { if (!directEmailDetails.IsTest) { await _directEmailHistoryRepository.AddSaveNoAuditAsync(history); } await IncrementSentCountAsync(directEmailTemplate.Id); } return(history); }