private void ProcessAppNavCardAlerts() { var navigationCardsManager = new NavigationCardsManager(_issueManager); List <NavigationCard> cards = navigationCardsManager.GetPendingAlertCards(); LogDebugMessage("Email templates loaded: " + _templates.Count); LogDebugMessage("Pending card alerts: " + cards.Count); // ? We need to store user id and issue id for every email we send out -- avoids dupes? List <string> issuesEmailedToUsers = new List <string>(50); List <string> individualIssuesEmailedToUsers = new List <string>(50); AlertsTemplateHelper alerts = new AlertsTemplateHelper(_templates, GetUrl(_issueManager), _languages); UserManager userManager = new UserManager(_issueManager); bool refreshCache = false; var allOptOuts = navigationCardsManager.GetOptOuts(); foreach (NavigationCard card in cards) { List <IssueDto> individualIssues = new List <IssueDto>(); // Safety checks if (!card.UserId.HasValue) { continue; } if (card.CardData.Alerts.Count == 0) { continue; } UserDto recepient = userManager.Get(card.UserId.Value); // Safety check if (!recepient.Entity.EmailMe || recepient.Entity.Email.IsEmpty()) { continue; } DateTime lastChecked = card.CardData.AlertsLastSent.HasValue ? card.CardData.AlertsLastSent.Value : card.Created; DateTime lastCheckedLocal = lastChecked.ToLocal(_issueManager.UserContext.User.TimeZone); AlertTypeAppNavAlertsTemplateModel model = new AlertTypeAppNavAlertsTemplateModel(); model.TheRecipient = recepient; model.Version = GeminiVersion.Version; model.GeminiUrl = alerts.GeminiUrl; List <int> issuesToAlert = new List <int>(card.CardData.Alerts); foreach (int issueId in issuesToAlert) { IssueDto issue = null; try { issue = _issueManager.Get(issueId); } catch (Exception ex) { LogException(ex); } // Safety check if (issue == null || issue.Entity.IsNew) { continue; } // Dupe check string dupeKey = string.Format("{0}-{1}-{2}", recepient.Entity.Id, issueId, card.Id); if (issuesEmailedToUsers.Contains(dupeKey)) { continue; } var permissionManager = new PermissionsManager(recepient, _types, _permissionSets, _organizations, _issueManager.UserContext.Config.HelpDeskModeGroup, false); if (!permissionManager.CanSeeItem(issue.Project, issue)) { continue; } foreach (var comment in issue.Comments) { if (!permissionManager.CanSeeComment(issue, comment)) { issue.Comments.RemoveAll(c => !permissionManager.CanSeeComment(issue, c)); break; } } // Remove the reported by first entry! if (issue.History.Count > 0 && issue.History[issue.History.Count - 1].Entity.AttributeChanged == ItemAttributeVisibility.ReportedBy) { issue.History.RemoveAt(issue.History.Count - 1); } issue.ChangeLog = _issueManager.GetChangeLog(issue, _issueManager.UserContext.User, recepient, lastCheckedLocal); // Populate model for email template if (card.CardData.Subscription.IndividualAlert) { individualIssues.Add(issue); } if (card.CardData.Subscription.Created && issue.Created.ToUtc(_issueManager.UserContext.User.TimeZone) >= lastChecked) { model.TheItemsCreated.Add(issue); } else { List <IssueAuditDto> allChanges = issue.History.FindAll(h => h.Entity.Created.ToUtc(_issueManager.UserContext.User.TimeZone) >= lastChecked); List <IssueAuditDto> commentChanges = allChanges.FindAll(a => !a.Entity.IsCustom && a.Entity.AttributeChanged == ItemAttributeVisibility.AssociatedComments); List <IssueAuditDto> nonCommentChanges = allChanges.FindAll(a => a.Entity.IsCustom || a.Entity.AttributeChanged != ItemAttributeVisibility.AssociatedComments); // Add comments and updates if (card.CardData.Subscription.Updated && nonCommentChanges.Count > 0 || card.CardData.Subscription.Commented && commentChanges.Count > 0 && issue.Comments.Count > 0) { model.TheItemsUpdated.Add(issue); } if (card.CardData.Subscription.Commented && commentChanges.Count > 0 && issue.Comments.Count > 0) { model.TheItemsCommented.Add(issue); } } // Record the fact that we have processed this issue for this recepient (to prevent dupes) issuesEmailedToUsers.Add(dupeKey); } model.CardTitle = string.Format("{0} {1}", card.Key, card.Title); model.CardKey = card.Key; model.CardDescription = card.Title; model.CardComment = card.CardData.Comment; model.CardUrl = string.Concat(model.GeminiUrl, "workspace/", card.Id, '/', card.Url); // Safety check! if (model.ChangeCount > 0) { List <UserDto> subscribers = GetCardSubscribers(card, navigationCardsManager, userManager, recepient); //if (!subscribers.Contains(recepient) && subscribers.Find(u => u.Entity.Id == recepient.Entity.Id) == null) subscribers.Insert(0, recepient); if (card.CardData.Subscription.IndividualAlert) { foreach (var user in subscribers) { if (allOptOuts.Any(s => s.UserId == user.Entity.Id && s.CardId == card.Id && s.OptOutType == OptOutEmails.OptOutTypes.Alert)) { continue; } foreach (var issue in individualIssues) { string individualDupeKey = string.Format("{0}-{1}", user.Entity.Id, issue.Entity.Id); if (individualIssuesEmailedToUsers.Contains(individualDupeKey)) { continue; } if (user != recepient) { var permissionManager = new PermissionsManager(user, _types, _permissionSets, _organizations, _issueManager.UserContext.Config.HelpDeskModeGroup, false); if (!permissionManager.CanSeeItem(issue.Project, issue)) { continue; } issue.ChangeLog = _issueManager.GetChangeLog(issue, _issueManager.UserContext.User, user, lastCheckedLocal); } var indModel = new AlertTypeIndividualTemplateModel(); indModel.GeminiUrl = model.GeminiUrl; indModel.LinkViewItem = NavigationHelper.GetIssueUrl(_issueManager.UserContext, issue.Entity.ProjectId, issue.EscapedProjectCode, issue.Entity.Id); indModel.TheItem = issue; indModel.TheRecipient = user; indModel.Version = GeminiVersion.Version; indModel.IsNewItem = model.TheItemsCreated.Contains(issue); indModel.CardKey = model.CardKey; indModel.CardDescription = model.CardDescription; indModel.CardComment = model.CardComment; indModel.CardUrl = model.CardUrl; if (!indModel.IsNewItem && issue.ChangeLog.Count == 0) { continue; } var template = alerts.FindTemplateForProject( indModel.IsNewItem ? AlertTemplateType.Created : AlertTemplateType.Updated, issue.Entity.ProjectId, GetLanguageId(user)); string html = alerts.GenerateHtml(template, indModel); if (GeminiApp.GeminiLicense.IsFree) { html = alerts.AddSignature(html); } // Send email string log; string subject = template.Options.Subject.HasValue() ? alerts.GenerateHtml(template, indModel, true) : string.Format("[{0}] {1} {2} ({3})", issue.IssueKey, issue.Type, model.TheItemsCreated.Contains(issue) ? "Created" : "Updated", issue.Title, issue.IsClosed ? "Closed" : string.Empty); EmailHelper.Send(_issueManager.UserContext.Config, subject, html, user.Entity.Email, user.Fullname, true, out log); individualIssuesEmailedToUsers.Add(individualDupeKey); } } } else { var cloneCreated = new List <IssueDto>(model.TheItemsCreated); var cloneUpdated = new List <IssueDto>(model.TheItemsUpdated); var cloneCommented = new List <IssueDto>(model.TheItemsCommented); // Find email template to use (for this project or fall back to default template) foreach (var user in subscribers) { AlertTemplate template = alerts.FindTemplateForProject(AlertTemplateType.AppNavAlerts, 0, GetLanguageId(user)); if (allOptOuts.Any(s => s.UserId == user.Entity.Id && s.CardId == card.Id && s.OptOutType == OptOutEmails.OptOutTypes.Alert)) { continue; } model.TheItemsCreated = new List <IssueDto>(cloneCreated); model.TheItemsUpdated = new List <IssueDto>(cloneUpdated); model.TheItemsCommented = new List <IssueDto>(cloneCommented); if (user != recepient) { var permissionManager = new PermissionsManager(user, _types, _permissionSets, _organizations, _issueManager.UserContext.Config.HelpDeskModeGroup, false); model.TheItemsCreated.RemoveAll(i => !permissionManager.CanSeeItem(i.Project, i)); model.TheItemsUpdated.RemoveAll(i => !permissionManager.CanSeeItem(i.Project, i)); model.TheItemsCommented.RemoveAll(i => !permissionManager.CanSeeItem(i.Project, i)); foreach (var issue in model.TheItemsCreated.Concat(model.TheItemsUpdated).Concat(model.TheItemsCommented)) { issue.ChangeLog = _issueManager.GetChangeLog(issue, _issueManager.UserContext.User, user, lastCheckedLocal); } } //model.TheItemsCreated.RemoveAll(i => i.ChangeLog.Count == 0); model.TheItemsUpdated.RemoveAll(i => i.ChangeLog.Count == 0); model.TheItemsCommented.RemoveAll(i => i.ChangeLog.Count == 0); if (model.ChangeCount == 0) { continue; } // Generate email template string html = alerts.GenerateHtml(template, model); if (GeminiApp.GeminiLicense.IsFree) { html = alerts.AddSignature(html); } string subject = template.Options.Subject.HasValue() ? alerts.GenerateHtml(template, model, true) : string.Format("{0} {1}", card.Key, card.Title); // Send email string log; EmailHelper.Send(_issueManager.UserContext.Config, subject, html, user.Entity.Email, user.Fullname, true, out log); } } } // Remove the alert notifications and update the database lock (card.CardData.Alerts) { card.CardData.Alerts.RemoveAll(a => issuesToAlert.Contains(a)); } card.CardData.AlertsLastSent = DateTime.UtcNow; refreshCache = true; navigationCardsManager.Update(card, false, false); } if (refreshCache) { navigationCardsManager.Cache.NavigationCards.Invalidate(); var webNodes = GeminiApp.Container.Resolve <IWebNodes>(); webNodes.AddDataOnAllNodesButMe(new WebNodeData() { NodeGuid = GeminiApp.GUID, Key = "cache", Value = navigationCardsManager.Cache.NavigationCards.CacheKey }); } }