예제 #1
0
        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 }));
            }
        }
예제 #2
0
        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)]);
예제 #3
0
        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);
        }
예제 #4
0
        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);
        }
예제 #5
0
        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."
            });
        }
예제 #6
0
        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);
        }