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); }
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)]);
private async Task <DirectEmailHistory> InternalSendDirectAsync(Site site, DirectEmailHistory history, IDictionary <string, string> tags) { if (history == null) { throw new ArgumentNullException(nameof(history)); } var emailBase = await GetEmailBase(history.EmailBaseId, history.LanguageId); if (emailBase.EmailBaseText == null) { emailBase = await GetEmailBase(history.EmailBaseId, await _languageService.GetDefaultLanguageIdAsync()); } var stubble = new StubbleBuilder().Build(); using var message = new MimeMessage { Subject = history.Subject, Body = new BodyBuilder { TextBody = await stubble .RenderAsync(emailBase.EmailBaseText.TemplateText, tags), HtmlBody = await stubble .RenderAsync(emailBase.EmailBaseText.TemplateHtml, tags, new Stubble.Core.Settings.RenderSettings { SkipHtmlEncoding = true }) }.ToMessageBody() }; message.From.Add(new MailboxAddress(history.FromName, history.FromEmailAddress)); try { if (!string.IsNullOrEmpty(history.OverrideToEmailAddress)) { message.To.Add(MailboxAddress.Parse(history.OverrideToEmailAddress)); } else { message.To.Add(new MailboxAddress(history.ToName, history.ToEmailAddress)); } } catch (ParseException ex) { _logger.LogError("Unable to parse email address: {EmailAddress}", history.ToEmailAddress); throw new GraException($"Unable to parse email address: {history.ToEmailAddress}", ex); } using var client = new SmtpClient { // accept any STARTTLS certificate ServerCertificateValidationCallback = (_, __, ___, ____) => true }; client.MessageSent += (_, e) => { history.SentResponse = e.Response?.Length > 255 ? e.Response[..255]