mirror of
https://github.com/Ombi-app/Ombi.git
synced 2025-08-19 21:03:17 -07:00
commit
c374729a53
11 changed files with 358 additions and 38 deletions
228
src/Ombi.Core.Tests/Senders/MassEmailSenderTests.cs
Normal file
228
src/Ombi.Core.Tests/Senders/MassEmailSenderTests.cs
Normal file
|
@ -0,0 +1,228 @@
|
|||
using Microsoft.Extensions.Logging;
|
||||
using MockQueryable.Moq;
|
||||
using Moq;
|
||||
using Moq.AutoMock;
|
||||
using NUnit.Framework;
|
||||
using Ombi.Core.Authentication;
|
||||
using Ombi.Core.Models;
|
||||
using Ombi.Core.Senders;
|
||||
using Ombi.Notifications;
|
||||
using Ombi.Notifications.Models;
|
||||
using Ombi.Settings.Settings.Models.Notifications;
|
||||
using Ombi.Store.Entities;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Ombi.Core.Tests.Senders
|
||||
{
|
||||
[TestFixture]
|
||||
public class MassEmailSenderTests
|
||||
{
|
||||
|
||||
private MassEmailSender _subject;
|
||||
private AutoMocker _mocker;
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
_mocker = new AutoMocker();
|
||||
_subject = _mocker.CreateInstance<MassEmailSender>();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task SendMassEmail_SingleUser()
|
||||
{
|
||||
var model = new MassEmailModel
|
||||
{
|
||||
Body = "Test",
|
||||
Subject = "Subject",
|
||||
Users = new List<OmbiUser>
|
||||
{
|
||||
new OmbiUser
|
||||
{
|
||||
Id = "a"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
_mocker.Setup<OmbiUserManager, IQueryable<OmbiUser>>(x => x.Users).Returns(new List<OmbiUser>
|
||||
{
|
||||
new OmbiUser
|
||||
{
|
||||
Id = "a",
|
||||
Email = "Test@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.To == "Test@test.com"), It.IsAny<EmailNotificationSettings>()), Times.Once);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task SendMassEmail_MultipleUsers()
|
||||
{
|
||||
var model = new MassEmailModel
|
||||
{
|
||||
Body = "Test",
|
||||
Subject = "Subject",
|
||||
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.To == "Test@test.com"), It.IsAny<EmailNotificationSettings>()), Times.Once);
|
||||
_mocker.Verify<IEmailProvider>(x => x.SendAdHoc(It.Is<NotificationMessage>(m => m.Subject == model.Subject
|
||||
&& m.Message == model.Body
|
||||
&& m.To == "b@test.com"), It.IsAny<EmailNotificationSettings>()), Times.Once);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task SendMassEmail_UserNoEmail()
|
||||
{
|
||||
var model = new MassEmailModel
|
||||
{
|
||||
Body = "Test",
|
||||
Subject = "Subject",
|
||||
Users = new List<OmbiUser>
|
||||
{
|
||||
new OmbiUser
|
||||
{
|
||||
Id = "a"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
_mocker.Setup<OmbiUserManager, IQueryable<OmbiUser>>(x => x.Users).Returns(new List<OmbiUser>
|
||||
{
|
||||
new OmbiUser
|
||||
{
|
||||
Id = "a",
|
||||
}
|
||||
}.AsQueryable().BuildMock().Object);
|
||||
|
||||
var result = await _subject.SendMassEmail(model);
|
||||
_mocker.Verify<ILogger<MassEmailSender>>(
|
||||
x => x.Log(
|
||||
LogLevel.Information,
|
||||
It.IsAny<EventId>(),
|
||||
It.IsAny<It.IsAnyType>(),
|
||||
It.IsAny<Exception>(),
|
||||
It.IsAny<Func<It.IsAnyType, Exception, string>>()),
|
||||
Times.Once);
|
||||
|
||||
_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 Body { get; set; }
|
||||
|
||||
public bool Bcc { get; set; }
|
||||
|
||||
public List<OmbiUser> Users { get; set; }
|
||||
}
|
||||
}
|
|
@ -25,7 +25,9 @@
|
|||
// ************************************************************************/
|
||||
#endregion
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
@ -64,6 +66,63 @@ namespace Ombi.Core.Senders
|
|||
var customization = await _customizationService.GetSettingsAsync();
|
||||
var email = await _emailService.GetSettingsAsync();
|
||||
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)
|
||||
{
|
||||
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);
|
||||
continue;
|
||||
}
|
||||
var resolver = new NotificationMessageResolver();
|
||||
var curlys = new NotificationMessageCurlys();
|
||||
curlys.Setup(fullUser, customization);
|
||||
var template = new NotificationTemplates() { Message = model.Body, Subject = model.Subject };
|
||||
var content = resolver.ParseMessage(template, curlys);
|
||||
|
@ -83,13 +140,19 @@ namespace Ombi.Core.Senders
|
|||
To = fullUser.Email,
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
await Task.WhenAll(messagesSent);
|
||||
|
||||
return true;
|
||||
/// <summary>
|
||||
/// This will add a 2 second delay, this is to help with concurrent connection limits
|
||||
/// <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
|
||||
};
|
||||
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())
|
||||
{
|
||||
|
|
4
src/Ombi/.vscode/settings.json
vendored
4
src/Ombi/.vscode/settings.json
vendored
|
@ -10,13 +10,13 @@
|
|||
"cSpell.words": [
|
||||
"usermanagement"
|
||||
],
|
||||
"discord.enabled": true,
|
||||
"conventionalCommits.scopes": [
|
||||
"discover",
|
||||
"request-limits",
|
||||
"notifications",
|
||||
"settings",
|
||||
"user-management",
|
||||
"newsletter"
|
||||
"newsletter",
|
||||
"mass-email"
|
||||
]
|
||||
}
|
||||
|
|
|
@ -121,6 +121,7 @@ export interface IMassEmailModel {
|
|||
subject: string;
|
||||
body: string;
|
||||
users: IUser[];
|
||||
bcc: boolean;
|
||||
}
|
||||
|
||||
export interface INotificationPreferences {
|
||||
|
|
|
@ -60,7 +60,7 @@
|
|||
<th mat-header-cell *matHeaderCellDef> </th>
|
||||
<td mat-cell *matCellDef="let element">
|
||||
<button mat-raised-button color="accent" [routerLink]="'/details/artist/' + element.foreignArtistId">{{ 'Requests.Details' | translate}}</button>
|
||||
<button mat-raised-button color="warn" (click)="openOptions(element)" *ngIf="isAdmin"> {{ 'Requests.Options' | translate}}</button>
|
||||
<button mat-raised-button color="warn" (click)="openOptions(element)" *ngIf="isAdmin || manageOwnRequests"> {{ 'Requests.Options' | translate}}</button>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
|
|
|
@ -25,6 +25,7 @@ export class AlbumsGridComponent implements OnInit, AfterViewInit {
|
|||
public defaultSort: string = "requestedDate";
|
||||
public defaultOrder: string = "desc";
|
||||
public currentFilter: RequestFilterType = RequestFilterType.All;
|
||||
public manageOwnRequests: boolean;
|
||||
|
||||
public RequestFilter = RequestFilterType;
|
||||
|
||||
|
@ -46,6 +47,7 @@ export class AlbumsGridComponent implements OnInit, AfterViewInit {
|
|||
|
||||
public ngOnInit() {
|
||||
this.isAdmin = this.auth.hasRole("admin") || this.auth.hasRole("poweruser");
|
||||
this.manageOwnRequests = this.auth.hasRole("ManageOwnRequests")
|
||||
|
||||
const defaultCount = this.storageService.get(this.storageKeyGridCount);
|
||||
const defaultSort = this.storageService.get(this.storageKey);
|
||||
|
@ -117,16 +119,17 @@ export class AlbumsGridComponent implements OnInit, AfterViewInit {
|
|||
|
||||
public openOptions(request: IAlbumRequest) {
|
||||
const filter = () => {
|
||||
this.dataSource = this.dataSource.filter((req) => {
|
||||
return req.id !== request.id;
|
||||
})
|
||||
this.dataSource = this.dataSource.filter((req) => {
|
||||
return req.id !== request.id;
|
||||
});
|
||||
};
|
||||
|
||||
const onChange = () => {
|
||||
this.ref.detectChanges();
|
||||
};
|
||||
|
||||
this.onOpenOptions.emit({ request: request, filter: filter, onChange: onChange });
|
||||
const data = { request: request, filter: filter, onChange: onChange, manageOwnRequests: this.manageOwnRequests, isAdmin: this.isAdmin };
|
||||
this.onOpenOptions.emit(data);
|
||||
}
|
||||
|
||||
public switchFilter(type: RequestFilterType) {
|
||||
|
|
|
@ -128,6 +128,7 @@ export class RequestService extends ServiceHelpers {
|
|||
public approveChild(child: ITvUpdateModel): Observable<IRequestEngineResult> {
|
||||
return this.http.post<IRequestEngineResult>(`${this.url}tv/approve`, JSON.stringify(child), {headers: this.headers});
|
||||
}
|
||||
|
||||
public deleteChild(childId: number): Observable<boolean> {
|
||||
return this.http.delete<boolean>(`${this.url}tv/child/${childId}`, {headers: this.headers});
|
||||
}
|
||||
|
|
|
@ -3,15 +3,15 @@
|
|||
<wiki></wiki>
|
||||
<fieldset>
|
||||
<legend>Mass Email</legend>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<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}">
|
||||
<small *ngIf="missingSubject" class="error-text">Hey! We need a subject!</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group" >
|
||||
<textarea rows="10" type="text" class="form-control-custom form-control " id="themeContent" name="themeContent" [(ngModel)]="message"></textarea>
|
||||
<div class="form-group" >
|
||||
<textarea rows="10" type="text" class="form-control-custom form-control" id="themeContent" name="themeContent" [(ngModel)]="message" placeholder="This supports HTML"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
|
@ -20,7 +20,14 @@
|
|||
<small>May appear differently on email clients</small>
|
||||
<hr/>
|
||||
<div [innerHTML]="message"></div>
|
||||
<hr/>
|
||||
</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>
|
||||
<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 class="col-md-6">
|
||||
<!--Users Section-->
|
||||
<label class="control-label">Recipients</label>
|
||||
<!--Users Section-->
|
||||
<label class="control-label">Recipients with Email Addresses</label>
|
||||
<div class="form-group">
|
||||
<div class="checkbox">
|
||||
<input type="checkbox" id="all" (click)="selectAllUsers()">
|
||||
<label for="all">Select All</label>
|
||||
</div>
|
||||
<div class="md-form-field">
|
||||
<mat-slide-toggle (change)="selectAllUsers($event)">Select All</mat-slide-toggle>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" *ngFor="let u of users">
|
||||
<div class="checkbox">
|
||||
<input type="checkbox" id="user{{u.user.id}}" [(ngModel)]="u.selected">
|
||||
<label for="user{{u.user.id}}">{{u.user.userName}}</label>
|
||||
</div>
|
||||
<div class="md-form-field">
|
||||
<mat-slide-toggle id="user{{u.user.id}}" [(ngModel)]="u.selected">{{u.user.userName}} ({{u.user.emailAddress}})</mat-slide-toggle>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
</fieldset>
|
||||
|
|
|
@ -12,6 +12,7 @@ export class MassEmailComponent implements OnInit {
|
|||
public users: IMassEmailUserModel[] = [];
|
||||
public message: string;
|
||||
public subject: string;
|
||||
public bcc: boolean;
|
||||
|
||||
public missingSubject = false;
|
||||
|
||||
|
@ -26,17 +27,19 @@ export class MassEmailComponent implements OnInit {
|
|||
public ngOnInit(): void {
|
||||
this.identityService.getUsers().subscribe(x => {
|
||||
x.forEach(u => {
|
||||
this.users.push({
|
||||
user: u,
|
||||
selected: false,
|
||||
});
|
||||
if (u.emailAddress) {
|
||||
this.users.push({
|
||||
user: u,
|
||||
selected: false,
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
this.settingsService.getEmailSettingsEnabled().subscribe(x => this.emailEnabled = x);
|
||||
}
|
||||
|
||||
public selectAllUsers() {
|
||||
this.users.forEach(u => u.selected = !u.selected);
|
||||
public selectAllUsers(event: any) {
|
||||
this.users.forEach(u => u.selected = event.checked);
|
||||
}
|
||||
|
||||
public send() {
|
||||
|
@ -44,10 +47,10 @@ export class MassEmailComponent implements OnInit {
|
|||
this.missingSubject = true;
|
||||
return;
|
||||
}
|
||||
if(!this.emailEnabled) {
|
||||
this.notification.error("You have not yet setup your email notifications, do that first!");
|
||||
return;
|
||||
}
|
||||
// if(!this.emailEnabled) {
|
||||
// this.notification.error("You have not yet setup your email notifications, do that first!");
|
||||
// return;
|
||||
// }
|
||||
this.missingSubject = false;
|
||||
// Where(x => x.selected).Select(x => x.user)
|
||||
const selectedUsers = this.users.filter(u => {
|
||||
|
@ -63,6 +66,7 @@ export class MassEmailComponent implements OnInit {
|
|||
users: selectedUsers,
|
||||
subject: this.subject,
|
||||
body: this.message,
|
||||
bcc: this.bcc,
|
||||
};
|
||||
this.notification.info("Sending","Sending mass email... Please wait");
|
||||
this.notificationMessageService.sendMassEmail(model).subscribe(x => {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue