public void BulkEmailSendRequestFormTest_ValidPassesValidation()
        {
            var model = new BulkEmailSendRequestForm()
            {
                Title          = "This is a dummy email title",
                HTMLBody       = "This is a dummy email body that is long enough to pass validation",
                RecipientsMode = BulkEmailRecipientsMode.DevCenterUsers,
                IgnoreMode     = BulkEmailIgnoreMode.CLASigned,
            };

            model.PlainBody = model.HTMLBody;

            var errors = new List <ValidationResult>();

            Assert.True(Validator.TryValidateObject(model, new ValidationContext(model), errors));
            Assert.Empty(errors);
        }
        public void BulkEmailSendRequestFormTest_DisallowSendToNoOne()
        {
            var model = new BulkEmailSendRequestForm()
            {
                Title          = "This is a dummy email title",
                HTMLBody       = "This is a dummy email body that is long enough to pass validation",
                RecipientsMode = BulkEmailRecipientsMode.DevCenterUsers,
                IgnoreMode     = BulkEmailIgnoreMode.DevCenterUsers,
            };

            model.PlainBody = model.HTMLBody;

            var errors = new List <ValidationResult>();

            Assert.False(Validator.TryValidateObject(model, new ValidationContext(model), errors));
            Assert.NotEmpty(errors);

            Assert.NotNull(errors[0].ErrorMessage);
            Assert.Contains("no one will receive", errors[0].ErrorMessage !);
        }
Example #3
0
        public async Task <IActionResult> SendBulkEmail([Required][FromBody] BulkEmailSendRequestForm request)
        {
            // Rate limit to just a few per day
            var cutoff = DateTime.UtcNow - AppInfo.BulkEmailRateLimitInterval;

            var count = await database.SentBulkEmails.CountAsync(b => b.CreatedAt >= cutoff);

            if (count >= AppInfo.MaxBulkEmailsPerInterval)
            {
                return(StatusCode((int)HttpStatusCode.TooManyRequests, "Too many bulk emails have been sent recently"));
            }

            var    user    = HttpContext.AuthenticatedUser() !;
            string?replyTo = null;

            switch (request.ReplyMode)
            {
            case BulkEmailReplyToMode.SendingUser:
                replyTo = $"{user.NameOrEmail} <{user.Email}>";
                break;

            case BulkEmailReplyToMode.DevCenterSendingAddress:
                // We don't set a reply to so just the plain sender address is who gets the replies
                break;

            default:
                return(BadRequest("Invalid reply mode"));
            }

            var recipientsList = await ComputeRecipientsList(request);

            // Fail if any address doesn't contain a "@" in it
            foreach (var recipient in recipientsList)
            {
                if (!recipient.Contains("@"))
                {
                    return(BadRequest($"A recipient doesn't appear to be valid email address: {recipient}"));
                }
            }

            if (recipientsList.Count < 1)
            {
                return(BadRequest("No recipients were left after taking ignores into account"));
            }

            var bulkModel = new SentBulkEmail()
            {
                Title      = request.Title,
                Recipients = recipientsList.Count,
                SentById   = user.Id,
                HtmlBody   = request.HTMLBody,
                PlainBody  = request.PlainBody,
            };

            // Make sure count is still good
            if (count != await database.SentBulkEmails.CountAsync(b => b.CreatedAt >= cutoff))
            {
                return(Problem("The number of sent bulk emails changed while processing, please try again"));
            }

            await database.AdminActions.AddAsync(new AdminAction()
            {
                Message       = $"A bulk email was sent to {bulkModel.Recipients} people",
                PerformedById = user.Id,
            });

            await database.SentBulkEmails.AddAsync(bulkModel);

            await database.SaveChangesAsync();

            logger.LogInformation("Bulk email ({Id}) send started by {Email}", bulkModel.Id, user.Email);

            // Hopefully starting the jobs here doesn't take too long as this request processing needs to finish in
            // reasonable time
            StartEmailSends(bulkModel.Id, recipientsList, replyTo);

            return(Ok());
        }
Example #4
0
        private async Task <List <string> > ComputeRecipientsList(BulkEmailSendRequestForm request)
        {
            IEnumerable <string> recipients;

            Lazy <Task <List <string> > > devCenterUsers =
                new(() => database.Users.Select(u => u.Email).ToListAsync());

            Lazy <Task <List <string> > > devCenterDevelopers =
                new(() => database.Users.Where(u => u.Developer == true).Select(u => u.Email)
                    .ToListAsync());

            Lazy <Task <List <string> > > associationMembers =
                new(() => database.AssociationMembers.Select(a => a.Email)
                    .ToListAsync());

            switch (request.RecipientsMode)
            {
            case BulkEmailRecipientsMode.ManualList:
                if (request.ManualRecipients == null)
                {
                    throw new Exception("Manual recipients list is missing");
                }

                recipients = request.ManualRecipients.Split('\n').Select(r => r.Trim().TrimEnd(','))
                             .Where(r => !string.IsNullOrWhiteSpace(r));
                break;

            case BulkEmailRecipientsMode.DevCenterUsers:
                recipients = await devCenterUsers.Value;
                break;

            case BulkEmailRecipientsMode.DevCenterDevelopers:
                recipients = await devCenterDevelopers.Value;
                break;

            case BulkEmailRecipientsMode.AssociationMembers:
                recipients = await associationMembers.Value;
                break;

            default:
                throw new ArgumentOutOfRangeException();
            }

            switch (request.IgnoreMode)
            {
            case BulkEmailIgnoreMode.Nobody:
                break;

            case BulkEmailIgnoreMode.DevCenterUsers:
            {
                var ignoreData = new HashSet <string>(await devCenterUsers.Value);
                recipients = recipients.Where(r => !ignoreData.Contains(r));
                break;
            }

            case BulkEmailIgnoreMode.DevCenterDevelopers:
            {
                var ignoreData = new HashSet <string>(await devCenterDevelopers.Value);
                recipients = recipients.Where(r => !ignoreData.Contains(r));
                break;
            }

            case BulkEmailIgnoreMode.AssociationMembers:
            {
                var ignoreData = new HashSet <string>(await associationMembers.Value);
                recipients = recipients.Where(r => !ignoreData.Contains(r));
                break;
            }

            case BulkEmailIgnoreMode.CLASigned:
            {
                var activeCLA = await database.Clas.FirstOrDefaultAsync(c => c.Active);

                if (activeCLA == null)
                {
                    logger.LogWarning("No active CLA, not able to ignore emails with CLA signature");
                    break;
                }

                var ignoreData = new HashSet <string>(await database.ClaSignatures
                                                      .Where(s => s.ClaId == activeCLA.Id && s.ValidUntil == null).Select(s => s.Email)
                                                      .ToListAsync());
                recipients = recipients.Where(r => !ignoreData.Contains(r));
                break;
            }

            default:
                throw new ArgumentOutOfRangeException();
            }

            return(recipients.Distinct().ToList());
        }