mirror of
https://github.com/Ombi-app/Ombi.git
synced 2025-08-19 12:59:39 -07:00
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
This commit is contained in:
parent
69e8b5a7e2
commit
ca655ae570
8 changed files with 204 additions and 35 deletions
|
@ -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())
|
||||||
{
|
{
|
||||||
|
|
4
src/Ombi/.vscode/settings.json
vendored
4
src/Ombi/.vscode/settings.json
vendored
|
@ -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>
|
||||||
|
@ -29,22 +36,20 @@
|
||||||
</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…
Add table
Add a link
Reference in a new issue