feat(mass-email): Added the ability to configure the Mass Email, we can now send BCC and we are less likely to be rate limited when not using bcc #4377

pull/4384/head
tidusjar 3 years ago
parent 69e8b5a7e2
commit ca655ae570

@ -38,9 +38,9 @@ namespace Ombi.Core.Tests.Senders
{ {
Body = "Test", Body = "Test",
Subject = "Subject", Subject = "Subject",
Users = new List<Store.Entities.OmbiUser> Users = new List<OmbiUser>
{ {
new Store.Entities.OmbiUser new OmbiUser
{ {
Id = "a" Id = "a"
} }
@ -143,5 +143,86 @@ namespace Ombi.Core.Tests.Senders
_mocker.Verify<IEmailProvider>(x => x.SendAdHoc(It.IsAny<NotificationMessage>(), It.IsAny<EmailNotificationSettings>()), Times.Never); _mocker.Verify<IEmailProvider>(x => x.SendAdHoc(It.IsAny<NotificationMessage>(), It.IsAny<EmailNotificationSettings>()), Times.Never);
} }
[Test]
public async Task SendMassEmail_Bcc()
{
var model = new MassEmailModel
{
Body = "Test",
Subject = "Subject",
Bcc = true,
Users = new List<OmbiUser>
{
new OmbiUser
{
Id = "a"
},
new OmbiUser
{
Id = "b"
}
}
};
_mocker.Setup<OmbiUserManager, IQueryable<OmbiUser>>(x => x.Users).Returns(new List<OmbiUser>
{
new OmbiUser
{
Id = "a",
Email = "Test@test.com"
},
new OmbiUser
{
Id = "b",
Email = "b@test.com"
}
}.AsQueryable().BuildMock().Object);
var result = await _subject.SendMassEmail(model);
_mocker.Verify<IEmailProvider>(x => x.SendAdHoc(It.Is<NotificationMessage>(m => m.Subject == model.Subject
&& m.Message == model.Body
&& m.Other["bcc"] == "Test@test.com,b@test.com"), It.IsAny<EmailNotificationSettings>()), Times.Once);
}
[Test]
public async Task SendMassEmail_Bcc_NoEmails()
{
var model = new MassEmailModel
{
Body = "Test",
Subject = "Subject",
Bcc = true,
Users = new List<OmbiUser>
{
new OmbiUser
{
Id = "a"
},
new OmbiUser
{
Id = "b"
}
}
};
_mocker.Setup<OmbiUserManager, IQueryable<OmbiUser>>(x => x.Users).Returns(new List<OmbiUser>
{
new OmbiUser
{
Id = "a",
},
new OmbiUser
{
Id = "b",
}
}.AsQueryable().BuildMock().Object);
var result = await _subject.SendMassEmail(model);
_mocker.Verify<IEmailProvider>(x => x.SendAdHoc(It.IsAny<NotificationMessage>(), It.IsAny<EmailNotificationSettings>()), Times.Never);
}
} }
} }

@ -35,6 +35,8 @@ namespace Ombi.Core.Models
public string Subject { get; set; } public string Subject { get; set; }
public string Body { get; set; } public string Body { get; set; }
public bool Bcc { get; set; }
public List<OmbiUser> Users { get; set; } public List<OmbiUser> Users { get; set; }
} }
} }

@ -25,7 +25,9 @@
// ************************************************************************/ // ************************************************************************/
#endregion #endregion
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -64,6 +66,63 @@ namespace Ombi.Core.Senders
var customization = await _customizationService.GetSettingsAsync(); var customization = await _customizationService.GetSettingsAsync();
var email = await _emailService.GetSettingsAsync(); var email = await _emailService.GetSettingsAsync();
var messagesSent = new List<Task>(); var messagesSent = new List<Task>();
if (model.Bcc)
{
await SendBccMails(model, customization, email, messagesSent);
}
else
{
await SendIndividualEmails(model, customization, email, messagesSent);
}
await Task.WhenAll(messagesSent);
return true;
}
private async Task SendBccMails(MassEmailModel model, CustomizationSettings customization, EmailNotificationSettings email, List<Task> messagesSent)
{
var resolver = new NotificationMessageResolver();
var curlys = new NotificationMessageCurlys();
var validUsers = new List<OmbiUser>();
foreach (var user in model.Users)
{
var fullUser = await _userManager.Users.FirstOrDefaultAsync(x => x.Id == user.Id);
if (!fullUser.Email.HasValue())
{
_log.LogInformation("User {0} has no email, cannot send mass email to this user", fullUser.UserName);
continue;
}
validUsers.Add(fullUser);
}
if (!validUsers.Any())
{
return;
}
var firstUser = validUsers.FirstOrDefault();
var bccAddress = string.Join(',', validUsers.Select(x => x.Email));
curlys.Setup(firstUser, customization);
var template = new NotificationTemplates() { Message = model.Body, Subject = model.Subject };
var content = resolver.ParseMessage(template, curlys);
var msg = new NotificationMessage
{
Message = content.Message,
Subject = content.Subject,
Other = new Dictionary<string, string> { { "bcc", bccAddress } }
};
messagesSent.Add(_email.SendAdHoc(msg, email));
}
private async Task SendIndividualEmails(MassEmailModel model, CustomizationSettings customization, EmailNotificationSettings email, List<Task> messagesSent)
{
var resolver = new NotificationMessageResolver();
var curlys = new NotificationMessageCurlys();
foreach (var user in model.Users) foreach (var user in model.Users)
{ {
var fullUser = await _userManager.Users.FirstOrDefaultAsync(x => x.Id == user.Id); var fullUser = await _userManager.Users.FirstOrDefaultAsync(x => x.Id == user.Id);
@ -72,8 +131,6 @@ namespace Ombi.Core.Senders
_log.LogInformation("User {0} has no email, cannot send mass email to this user", fullUser.UserName); _log.LogInformation("User {0} has no email, cannot send mass email to this user", fullUser.UserName);
continue; continue;
} }
var resolver = new NotificationMessageResolver();
var curlys = new NotificationMessageCurlys();
curlys.Setup(fullUser, customization); curlys.Setup(fullUser, customization);
var template = new NotificationTemplates() { Message = model.Body, Subject = model.Subject }; var template = new NotificationTemplates() { Message = model.Body, Subject = model.Subject };
var content = resolver.ParseMessage(template, curlys); var content = resolver.ParseMessage(template, curlys);
@ -83,13 +140,19 @@ namespace Ombi.Core.Senders
To = fullUser.Email, To = fullUser.Email,
Subject = content.Subject Subject = content.Subject
}; };
messagesSent.Add(_email.SendAdHoc(msg, email)); messagesSent.Add(DelayEmail(msg, email));
_log.LogInformation("Sent mass email to user {0} @ {1}", fullUser.UserName, fullUser.Email); _log.LogInformation("Sent mass email to user {0} @ {1}", fullUser.UserName, fullUser.Email);
} }
}
await Task.WhenAll(messagesSent); /// <summary>
/// This will add a 2 second delay, this is to help with concurrent connection limits
return true; /// <see href="https://github.com/Ombi-app/Ombi/issues/4377"/>
/// </summary>
private async Task DelayEmail(NotificationMessage msg, EmailNotificationSettings email)
{
await Task.Delay(2000);
await _email.SendAdHoc(msg, email);
} }
} }
} }

@ -64,7 +64,20 @@ namespace Ombi.Notifications
MessageId = messageId MessageId = messageId
}; };
message.From.Add(new MailboxAddress(string.IsNullOrEmpty(settings.SenderName) ? settings.SenderAddress : settings.SenderName, settings.SenderAddress)); message.From.Add(new MailboxAddress(string.IsNullOrEmpty(settings.SenderName) ? settings.SenderAddress : settings.SenderName, settings.SenderAddress));
message.To.Add(new MailboxAddress(model.To, model.To)); if (model.To.HasValue())
{
message.To.Add(new MailboxAddress(model.To, model.To));
}
// Check for BCC
if (model.Other.TryGetValue("bcc", out var bcc))
{
var bccList = bcc.Split(',', StringSplitOptions.RemoveEmptyEntries);
foreach (var item in bccList)
{
message.Bcc.Add(new MailboxAddress(item, item));
}
}
using (var client = new SmtpClient()) using (var client = new SmtpClient())
{ {

@ -10,13 +10,13 @@
"cSpell.words": [ "cSpell.words": [
"usermanagement" "usermanagement"
], ],
"discord.enabled": true,
"conventionalCommits.scopes": [ "conventionalCommits.scopes": [
"discover", "discover",
"request-limits", "request-limits",
"notifications", "notifications",
"settings", "settings",
"user-management", "user-management",
"newsletter" "newsletter",
"mass-email"
] ]
} }

@ -121,6 +121,7 @@ export interface IMassEmailModel {
subject: string; subject: string;
body: string; body: string;
users: IUser[]; users: IUser[];
bcc: boolean;
} }
export interface INotificationPreferences { export interface INotificationPreferences {

@ -3,15 +3,15 @@
<wiki></wiki> <wiki></wiki>
<fieldset> <fieldset>
<legend>Mass Email</legend> <legend>Mass Email</legend>
<div class="row">
<div class="col-md-6"> <div class="col-md-6">
<div class="form-group"> <div class="form-group">
<input type="text" class="form-control form-control-custom " id="subject" name="subject" placeholder="Subject" [(ngModel)]="subject" [ngClass]="{'form-error': missingSubject}"> <input type="text" class="form-control form-control-custom " id="subject" name="subject" placeholder="Subject" [(ngModel)]="subject" [ngClass]="{'form-error': missingSubject}">
<small *ngIf="missingSubject" class="error-text">Hey! We need a subject!</small> <small *ngIf="missingSubject" class="error-text">Hey! We need a subject!</small>
</div> </div>
<div class="form-group" > <div class="form-group" >
<textarea rows="10" type="text" class="form-control-custom form-control " id="themeContent" name="themeContent" [(ngModel)]="message"></textarea> <textarea rows="10" type="text" class="form-control-custom form-control" id="themeContent" name="themeContent" [(ngModel)]="message" placeholder="This supports HTML"></textarea>
</div> </div>
<div class="form-group"> <div class="form-group">
@ -20,7 +20,14 @@
<small>May appear differently on email clients</small> <small>May appear differently on email clients</small>
<hr/> <hr/>
<div [innerHTML]="message"></div> <div [innerHTML]="message"></div>
<hr/>
</div> </div>
<small>This will send out the Mass email BCC'ing all of the selected users rather than sending individual messages</small>
<div class="md-form-field">
<mat-slide-toggle [(ngModel)]="bcc">BCC</mat-slide-toggle>
</div>
<br>
<br>
<div class="form-group"> <div class="form-group">
<div> <div>
<button type="submit" id="save" (click)="send()" class="mat-focus-indicator btn-spacing mat-raised-button mat-button-base mat-accent">Send</button> <button type="submit" id="save" (click)="send()" class="mat-focus-indicator btn-spacing mat-raised-button mat-button-base mat-accent">Send</button>
@ -28,23 +35,21 @@
</div> </div>
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<!--Users Section--> <!--Users Section-->
<label class="control-label">Recipients</label> <label class="control-label">Recipients with Email Addresses</label>
<div class="form-group"> <div class="form-group">
<div class="checkbox"> <div class="md-form-field">
<input type="checkbox" id="all" (click)="selectAllUsers()"> <mat-slide-toggle (change)="selectAllUsers($event)">Select All</mat-slide-toggle>
<label for="all">Select All</label> </div>
</div>
</div> </div>
<div class="form-group" *ngFor="let u of users"> <div class="form-group" *ngFor="let u of users">
<div class="checkbox"> <div class="md-form-field">
<input type="checkbox" id="user{{u.user.id}}" [(ngModel)]="u.selected"> <mat-slide-toggle id="user{{u.user.id}}" [(ngModel)]="u.selected">{{u.user.userName}} ({{u.user.emailAddress}})</mat-slide-toggle>
<label for="user{{u.user.id}}">{{u.user.userName}}</label> </div>
</div>
</div> </div>
</div> </div>
</div>
</fieldset> </fieldset>

@ -12,6 +12,7 @@ export class MassEmailComponent implements OnInit {
public users: IMassEmailUserModel[] = []; public users: IMassEmailUserModel[] = [];
public message: string; public message: string;
public subject: string; public subject: string;
public bcc: boolean;
public missingSubject = false; public missingSubject = false;
@ -26,17 +27,19 @@ export class MassEmailComponent implements OnInit {
public ngOnInit(): void { public ngOnInit(): void {
this.identityService.getUsers().subscribe(x => { this.identityService.getUsers().subscribe(x => {
x.forEach(u => { x.forEach(u => {
this.users.push({ if (u.emailAddress) {
user: u, this.users.push({
selected: false, user: u,
}); selected: false,
});
}
}); });
}); });
this.settingsService.getEmailSettingsEnabled().subscribe(x => this.emailEnabled = x); this.settingsService.getEmailSettingsEnabled().subscribe(x => this.emailEnabled = x);
} }
public selectAllUsers() { public selectAllUsers(event: any) {
this.users.forEach(u => u.selected = !u.selected); this.users.forEach(u => u.selected = event.checked);
} }
public send() { public send() {
@ -44,10 +47,10 @@ export class MassEmailComponent implements OnInit {
this.missingSubject = true; this.missingSubject = true;
return; return;
} }
if(!this.emailEnabled) { // if(!this.emailEnabled) {
this.notification.error("You have not yet setup your email notifications, do that first!"); // this.notification.error("You have not yet setup your email notifications, do that first!");
return; // return;
} // }
this.missingSubject = false; this.missingSubject = false;
// Where(x => x.selected).Select(x => x.user) // Where(x => x.selected).Select(x => x.user)
const selectedUsers = this.users.filter(u => { const selectedUsers = this.users.filter(u => {
@ -63,6 +66,7 @@ export class MassEmailComponent implements OnInit {
users: selectedUsers, users: selectedUsers,
subject: this.subject, subject: this.subject,
body: this.message, body: this.message,
bcc: this.bcc,
}; };
this.notification.info("Sending","Sending mass email... Please wait"); this.notification.info("Sending","Sending mass email... Please wait");
this.notificationMessageService.sendMassEmail(model).subscribe(x => { this.notificationMessageService.sendMassEmail(model).subscribe(x => {

Loading…
Cancel
Save