示例#1
0
        public void Multiple_senders_should_not_match()
        {
            var parser  = new SendgridEmailParser();
            var request = EmailRequest.Parse(Load("mail.txt"), parser);

            var config = GetConfig();

            config.Rules[0].Filters = new[]
            {
                new EmailFilter
                {
                    Type  = "!sender contains",
                    AllOf = new[]
                    {
                        "*****@*****.**",
                        "*****@*****.**"
                    }
                }
            };

            request.Email.From.Email = "*****@*****.**";
            var actions = EmailService.ActionsToPerform(request.Email, config);

            actions.Select(a => a.Id).Should().BeEmpty();

            request.Email.From.Name = request.Email.From.Email = "*****@*****.**";
            actions = EmailService.ActionsToPerform(request.Email, config);
            actions.Select(a => a.Id).Should().ContainInOrder("forward-all", "notify-all");
        }
示例#2
0
        public void Subject_filter_should_match()
        {
            var parser  = new SendgridEmailParser();
            var request = EmailRequest.Parse(Load("mail.txt"), parser);

            var config = GetConfig();

            config.Rules[0].Filters = new[]
            {
                new EmailFilter
                {
                    Type  = "subject contains",
                    OneOf = new[]
                    {
                        "Test 1",
                        "not found"
                    }
                },
                new EmailFilter
                {
                    Type  = "sender contains",
                    OneOf = new[]
                    {
                        "not found",
                        "sender@"
                    }
                }
            };

            var actions = EmailService.ActionsToPerform(request.Email, config);

            actions.Select(a => a.Id).Should().ContainInOrder("forward-all", "notify-all");
        }
示例#3
0
        public async Task Service_should_ignore_disabled_rule_entirely()
        {
            var status  = new Mock <IStatusService>();
            var vault   = new Mock <IKeyVaultHelper>();
            var config  = new Mock <IConfigService>();
            var storage = new Mock <IBlobStorageService>();
            var http    = new Mock <IHttpClient>();
            var parser  = new SendgridEmailParser();
            var request = EmailRequest.Parse(Load("mail.txt"), parser);

            status.Setup(x => x.GetStatiAsync(request, It.IsAny <CancellationToken>()))
            .ReturnsAsync(new Dictionary <string, StatusModel>());
            var cfg = GetConfig();

            cfg.Rules = new[]
            {
                new EmailRule
                {
                    Enabled = false,
                    Filters = new []
                    {
                        new EmailFilter
                        {
                            Type  = "subject contains",
                            OneOf = new[]
                            {
                                "not found"
                            }
                        }
                    },
                    Actions = new[]
                    {
                        new EmailAction
                        {
                            Id         = "executed",
                            Type       = ActionType.Forward,
                            Properties = JObject.FromObject(new
                            {
                                webhook = new
                                {
                                    secretName = "fwd"
                                }
                            })
                        }
                    }
                }
            };
            config.Setup(x => x.LoadAsync(It.IsAny <CancellationToken>()))
            .ReturnsAsync(cfg);

            var service = new EmailService(status.Object, vault.Object, config.Object, storage.Object, http.Object);

            var success = await service.ProcessMailAsync(request, CancellationToken.None);

            success.Should().BeTrue();

            status.Verify(x => x.GetStatiAsync(request, It.IsAny <CancellationToken>()));
            status.VerifyNoOtherCalls();
        }
示例#4
0
        public void No_action_filter_should_return_all()
        {
            var parser  = new SendgridEmailParser();
            var request = EmailRequest.Parse(Load("mail.txt"), parser);

            var config = GetConfig();

            var actions = EmailService.ActionsToPerform(request.Email, config);

            actions.Select(a => a.Id).Should().ContainInOrder("forward-all", "notify-all");
        }
        public void AttachmentsShouldBeCorrectlyParsed()
        {
            var parser = new SendgridEmailParser();

            using (var form = Build(new Dictionary <string, object>
            {
                { "from", "from <*****@*****.**>" },
                { "to", "no name <*****@*****.**>" },
                { "subject", "subj" },
                { "html", "other content" },
                { "attachment-info", JsonConvert.SerializeObject(new
                    {
                        attachment1 = new
                        {
                            filename = "en.txt",
                            type = "text/plain"
                        },
                        attachment2 = new
                        {
                            filename = "img.jpg",
                            type = "image/jpeg"
                        }
                    }) },
                { "attachment1", new EmailAttachment
                  {
                      FileName = "en.txt",
                      ContentType = "text/plain",
                      Base64Data = "This is the actual content"
                  } },
                { "attachment2", new EmailAttachment
                  {
                      FileName = "img.jpg",
                      ContentType = "image/jpeg",
                      Base64Data = "large base64 payload"
                  } }
            }))
            {
                var result = parser.Parse(form);
                result.From.Email.Should().Be("*****@*****.**");
                result.To[0].Email.Should().Be("*****@*****.**");
                result.Subject.Should().Be("subj");
                result.Html.Should().Be("other content");
                result.Attachments.Should().HaveCount(2);
                result.Attachments[0].ContentType.Should().Be("text/plain");
                result.Attachments[0].FileName.Should().Be("en.txt");
                result.Attachments[0].Base64Data.Should().Be(Convert.ToBase64String(Encoding.UTF8.GetBytes("This is the actual content")));
                result.Attachments[1].ContentType.Should().Be("image/jpeg");
                result.Attachments[1].FileName.Should().Be("img.jpg");
                result.Attachments[1].Base64Data.Should().Be(Convert.ToBase64String(Encoding.UTF8.GetBytes("large base64 payload")));
            }
        }
        public void HtmlShouldBePreferredOverText()
        {
            var parser = new SendgridEmailParser();

            const string actualContent = "Only this should be submitted";
            var          data          = new Dictionary <string, object>
            {
                { "from", "from <*****@*****.**>" },
                { "to", "no name <*****@*****.**>" },
                { "subject", "subj" },
                { "text", "other content" },
                { "html", @"<html>
<head>
<meta http-equiv=""Content-Type"" content=""text/html; charset=iso-8859-1"">
<style type=""text/css"" style=""display:none;""> P {margin-top:0;margin-bottom:0;} </style>
</head>
<body dir=""ltr"">
<div style=""font-family:Calibri,Helvetica,sans-serif; font-size:12pt; color:rgb(0,0,0)"">" +
                  actualContent +
                  @"<br>
</div>
<div style=""font-family:Calibri,Helvetica,sans-serif; font-size:12pt; color:rgb(0,0,0)"">
</div>
<div style=""font-family:Calibri,Helvetica,sans-serif; font-size:12pt; color:rgb(0,0,0)"">
<br>
</div>
<div id=""Signature"">
<div id=""divtagdefaultwrapper"" style=""font-size:12pt; color:#000000; font-family:Calibri,Arial,Helvetica,sans-serif"">
Regards,
<div>Sender</div>
</div>
</div>
</body>
</html>" }
            };

            using (var form = Build(data))
            {
                var result = parser.Parse(form);
                result.From.Email.Should().Be("*****@*****.**");
                result.To[0].Email.Should().Be("*****@*****.**");
                result.Subject.Should().Be("subj");
                result.Html.Should().Be((string)data["html"]);
            }
        }
示例#7
0
        public async Task Retry_should_only_invoke_non_successful_actions()
        {
            var status  = new Mock <IStatusService>();
            var vault   = new Mock <IKeyVaultHelper>();
            var config  = new Mock <IConfigService>();
            var storage = new Mock <IBlobStorageService>();
            var http    = new Mock <IHttpClient>();
            var parser  = new SendgridEmailParser();
            var request = EmailRequest.Parse(Load("mail.txt"), parser);

            // pretend email was received before and this is a retry
            status.Setup(x => x.GetStatiAsync(request, It.IsAny <CancellationToken>()))
            .ReturnsAsync(new Dictionary <string, StatusModel>
            {
                ["forward-all"] = new StatusModel
                {
                    ActionId = "forward-all",
                    Status   = EmailFanoutStatus.DeferredOrFailed.ToString()
                },
                ["notify-all"] = new StatusModel
                {
                    ActionId = "notify-all",
                    Status   = EmailFanoutStatus.Completed.ToString()
                }
            });
            config.Setup(x => x.LoadAsync(It.IsAny <CancellationToken>()))
            .ReturnsAsync(GetConfig());

            vault.Setup(x => x.GetSecretAsync("Webhook1", It.IsAny <CancellationToken>()))
            .ReturnsAsync("url1");

            http.Setup(x => x.SendAsync(It.Is <HttpRequestMessage>(r => r.RequestUri.ToString() == "url1"), It.IsAny <CancellationToken>()))
            .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK));

            var service = new EmailService(status.Object, vault.Object, config.Object, storage.Object, http.Object);

            var success = await service.ProcessMailAsync(request, CancellationToken.None);

            success.Should().BeTrue();

            status.Verify(x => x.GetStatiAsync(request, It.IsAny <CancellationToken>()));
            status.Verify(x => x.UpdateAsync(request, It.Is <EmailAction>(a => a.Id == "forward-all"), EmailFanoutStatus.Completed, It.IsAny <CancellationToken>()));
            status.VerifyNoOtherCalls();
        }
示例#8
0
        public async Task Multiple_exceptions_should_be_aggregated_when_more_than_one_service_fails()
        {
            var status  = new Mock <IStatusService>();
            var vault   = new Mock <IKeyVaultHelper>();
            var config  = new Mock <IConfigService>();
            var storage = new Mock <IBlobStorageService>();
            var http    = new Mock <IHttpClient>();
            var parser  = new SendgridEmailParser();
            var request = EmailRequest.Parse(Load("mail.txt"), parser);

            status.Setup(x => x.GetStatiAsync(request, It.IsAny <CancellationToken>()))
            .ReturnsAsync(new Dictionary <string, StatusModel>());
            config.Setup(x => x.LoadAsync(It.IsAny <CancellationToken>()))
            .ReturnsAsync(GetConfig());

            vault.Setup(x => x.GetSecretAsync("Webhook1", It.IsAny <CancellationToken>()))
            .Throws(new WebException("pretent secret not found"));

            vault.Setup(x => x.GetSecretAsync("Webhook2", It.IsAny <CancellationToken>()))
            .ReturnsAsync("url2");

            http.Setup(x => x.SendAsync(It.Is <HttpRequestMessage>(r => r.RequestUri.ToString() == "url2"), It.IsAny <CancellationToken>()))
            .Throws(new WebException("pretent secret not reachable"));

            var service = new EmailService(status.Object, vault.Object, config.Object, storage.Object, http.Object);

            try
            {
                await service.ProcessMailAsync(request, CancellationToken.None);

                Assert.Fail();
            }
            catch (AggregateException ex)
            {
                // expected due to action failure
                ex.InnerExceptions.Count.Should().Be(2, "because two services failed");
            }

            status.Verify(x => x.GetStatiAsync(request, It.IsAny <CancellationToken>()));
            status.Verify(x => x.UpdateAsync(request, It.Is <EmailAction>(a => a.Id == "forward-all"), EmailFanoutStatus.DeferredOrFailed, It.IsAny <CancellationToken>()));
            status.Verify(x => x.UpdateAsync(request, It.Is <EmailAction>(a => a.Id == "notify-all"), EmailFanoutStatus.DeferredOrFailed, It.IsAny <CancellationToken>()));
            status.VerifyNoOtherCalls();
        }
        public void ParsingDisplayNamesShouldWork()
        {
            var parser = new SendgridEmailParser();

            using (var form = Build(new Dictionary <string, object>
            {
                { "from", "from <*****@*****.**>" },
                { "to", "no name <*****@*****.**>" },
                { "subject", "subj" },
                { "html", "text" }
            }))
            {
                var result = parser.Parse(form);
                result.From.Email.Should().Be("*****@*****.**");
                result.To[0].Email.Should().Be("*****@*****.**");
                result.Subject.Should().Be("subj");
                result.Html.Should().Be("text");
            }
        }
        public void TextShouldBeFallbackIfHtmlFails()
        {
            var parser = new SendgridEmailParser();

            using (var form = Build(new Dictionary <string, object>
            {
                { "from", "from <*****@*****.**>" },
                { "to", "no name <*****@*****.**>" },
                { "subject", "subj" },
                { "text", "other content" },
                { "html", "" }
            }))
            {
                var result = parser.Parse(form);
                result.From.Email.Should().Be("*****@*****.**");
                result.To[0].Email.Should().Be("*****@*****.**");
                result.Subject.Should().Be("subj");
                result.Html.Should().Be("");
                result.Text.Should().Be("other content");
            }
        }
示例#11
0
        public void Unmatched_action_filter_should_ignore_all()
        {
            var parser  = new SendgridEmailParser();
            var request = EmailRequest.Parse(Load("mail.txt"), parser);

            var config = GetConfig();

            config.Rules[0].Filters = new[]
            {
                new EmailFilter
                {
                    Type  = "subject contains",
                    OneOf = new[]
                    {
                        "not found"
                    }
                }
            };

            var actions = EmailService.ActionsToPerform(request.Email, config);

            actions.Should().BeEmpty();
        }
示例#12
0
        public async Task Should_be_successful_even_if_all_filtered()
        {
            var status  = new Mock <IStatusService>();
            var vault   = new Mock <IKeyVaultHelper>();
            var config  = new Mock <IConfigService>();
            var storage = new Mock <IBlobStorageService>();
            var http    = new Mock <IHttpClient>();
            var parser  = new SendgridEmailParser();
            var request = EmailRequest.Parse(Load("mail.txt"), parser);

            status.Setup(x => x.GetStatiAsync(request, It.IsAny <CancellationToken>()))
            .ReturnsAsync(new Dictionary <string, StatusModel>());
            var cfg = GetConfig();

            cfg.Rules[0].Filters = new[]
            {
                new EmailFilter
                {
                    Type  = "subject contains",
                    OneOf = new[]
                    {
                        "not-found"
                    }
                }
            };
            config.Setup(x => x.LoadAsync(It.IsAny <CancellationToken>()))
            .ReturnsAsync(cfg);

            var service = new EmailService(status.Object, vault.Object, config.Object, storage.Object, http.Object);

            var success = await service.ProcessMailAsync(request, CancellationToken.None);

            success.Should().BeTrue();

            status.Verify(x => x.GetStatiAsync(request, It.IsAny <CancellationToken>()));
            status.VerifyNoOtherCalls();
        }
        public void AttachmentsShouldBeCorrectlyParsedFromActualPayload()
        {
            var parser = new SendgridEmailParser();

            using (var form = new MemoryStream())
            {
                var content = File.ReadAllText("Data/multipart.txt");
                var writer  = new StreamWriter(form);
                writer.Write(content);
                writer.Flush();
                form.Position = 0;
                var result = parser.Parse(form);
                result.From.Email.Should().Be("*****@*****.**");
                result.From.Name.Should().Be("Marc Stan");
                result.To[0].Email.Should().Be("*****@*****.**");
                result.To[0].Name.Should().Be("*****@*****.**");
                result.Subject.Should().Be("Subject1");
                result.Text.Should().Be("Content1");
                result.Attachments.Should().HaveCount(1);
                result.Attachments[0].ContentType.Should().Be("text/plain");
                result.Attachments[0].FileName.Should().Be("en.txt");
                result.Attachments[0].Base64Data.Should().Be(Convert.ToBase64String(Encoding.UTF8.GetBytes("this is plaintext")));
            }
        }
示例#14
0
        public async Task Service_should_ignore_disabled_filter_and_run_action()
        {
            var status  = new Mock <IStatusService>();
            var vault   = new Mock <IKeyVaultHelper>();
            var config  = new Mock <IConfigService>();
            var storage = new Mock <IBlobStorageService>();
            var http    = new Mock <IHttpClient>();
            var parser  = new SendgridEmailParser();
            var request = EmailRequest.Parse(Load("mail.txt"), parser);

            status.Setup(x => x.GetStatiAsync(request, It.IsAny <CancellationToken>()))
            .ReturnsAsync(new Dictionary <string, StatusModel>());
            var cfg = GetConfig();

            cfg.Rules = new[]
            {
                new EmailRule
                {
                    Filters = new []
                    {
                        new EmailFilter
                        {
                            Enabled = false,
                            Type    = "subject contains",
                            OneOf   = new[]
                            {
                                "not found"
                            }
                        }
                    },
                    Actions = new[]
                    {
                        new EmailAction
                        {
                            Id         = "executed",
                            Type       = ActionType.Forward,
                            Properties = JObject.FromObject(new
                            {
                                webhook = new
                                {
                                    secretName = "fwd"
                                }
                            })
                        }
                    }
                }
            };
            config.Setup(x => x.LoadAsync(It.IsAny <CancellationToken>()))
            .ReturnsAsync(cfg);

            vault.Setup(x => x.GetSecretAsync("fwd", It.IsAny <CancellationToken>()))
            .ReturnsAsync("url1");

            http.Setup(x => x.SendAsync(It.Is <HttpRequestMessage>(r => r.RequestUri.ToString() == "url1"), It.IsAny <CancellationToken>()))
            .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK));

            var service = new EmailService(status.Object, vault.Object, config.Object, storage.Object, http.Object);

            var success = await service.ProcessMailAsync(request, CancellationToken.None);

            success.Should().BeTrue();

            status.Verify(x => x.GetStatiAsync(request, It.IsAny <CancellationToken>()));
            status.Verify(x => x.UpdateAsync(request, It.Is <EmailAction>(a => a.Id == "executed"), EmailFanoutStatus.Completed, It.IsAny <CancellationToken>()));
            status.VerifyNoOtherCalls();
        }
示例#15
0
        public static async Task <IActionResult> Run(
            [HttpTrigger(AuthorizationLevel.Function, "post", Route = null)] HttpRequest req,
            Microsoft.Azure.WebJobs.ExecutionContext context,
            ILogger log,
            CancellationToken cancellationToken)
        {
            try
            {
                var config = LoadConfig(context.FunctionAppDirectory, log);

                var container = config["ArchiveContainerName"];
                var target    = config["RelayTargetEmail"];
                if (string.IsNullOrEmpty(container) && string.IsNullOrEmpty(target))
                {
                    throw new NotSupportedException("Neither email target nor container name where set. Please set either ArchiveContainerName or RelayTargetEmail");
                }

                Email email;
                using (var stream = new MemoryStream())
                {
                    // body can only be read once
                    req.Body.CopyTo(stream);
                    stream.Position = 0;
                    var parser = new SendgridEmailParser();
                    email = parser.Parse(stream);
                }
                if (!string.IsNullOrEmpty(container))
                {
                    IPersister auditLogger = new BlobStoragePersister(config["AzureWebJobsStorage"], container);

                    var d = DateTimeOffset.UtcNow;
                    // one folder per day is fine for now
                    var id = $"{d.ToString("yyyy-MM")}/{d.ToString("dd")}/{d.ToString("HH-mm-ss")}_{email.From.Email} - {email.Subject}";
                    await auditLogger.PersistAsync($"{id}.json", JsonConvert.SerializeObject(email, Formatting.Indented));

                    // save all attachments in subfolder
                    await Task.WhenAll(email.Attachments.Select(a => auditLogger.PersistAsync($"{id} (Attachments)/{a.FileName}", Convert.FromBase64String(a.Base64Data))));
                }
                if (!string.IsNullOrEmpty(target))
                {
                    var domain = config["Domain"];
                    var key    = config["SendgridApiKey"];
                    if (!string.IsNullOrEmpty(target) &&
                        string.IsNullOrEmpty(domain))
                    {
                        throw new NotSupportedException("Domain must be set as well when relay is used.");
                    }
                    if (!string.IsNullOrEmpty(target) &&
                        string.IsNullOrEmpty(key))
                    {
                        throw new NotSupportedException("SendgridApiKey must be set as well when relay is used.");
                    }

                    var client        = new SendGridClient(key);
                    var subjectParser = new SubjectParser(config["Prefix"]);
                    var relay         = new RelayLogic(client, subjectParser, log, new[]
                    {
                        new OutlookWebSanitizer(subjectParser)
                    }
                                                       );
                    var sendAsDomain = "true".Equals(config["SendAsDomain"], StringComparison.OrdinalIgnoreCase);
                    await relay.RelayAsync(email, target, domain, sendAsDomain, cancellationToken);
                }

                return(new OkResult());
            }
            catch (Exception e)
            {
                log.LogCritical(e, "Failed to process request!");
                return(new BadRequestResult());
            }
        }
示例#16
0
        public async Task Service_should_call_all_actions_correctly()
        {
            var status  = new Mock <IStatusService>();
            var vault   = new Mock <IKeyVaultHelper>();
            var config  = new Mock <IConfigService>();
            var storage = new Mock <IBlobStorageService>();
            var http    = new Mock <IHttpClient>();
            var parser  = new SendgridEmailParser();
            var request = EmailRequest.Parse(Load("mail.txt"), parser);

            status.Setup(x => x.GetStatiAsync(request, It.IsAny <CancellationToken>()))
            .ReturnsAsync(new Dictionary <string, StatusModel>());
            var cfg = GetConfig();

            cfg.Rules[0].Actions = new[]
            {
                new EmailAction
                {
                    Id         = "forward-all",
                    Type       = ActionType.Forward,
                    Properties = JObject.FromObject(new
                    {
                        webhook = new
                        {
                            secretName = "Webhook1",
                            body       = new
                            {
                                sender = "%sender%"
                            }
                        }
                    })
                },
                new EmailAction
                {
                    Id         = "notify-all",
                    Type       = ActionType.Webhook,
                    Properties = JObject.FromObject(new
                    {
                        webhook = new
                        {
                            secretName = "Webhook2"
                        },
                        body = new
                        {
                            subject = "You've got mail!",
                            body    = "%subject%"
                        }
                    })
                },
                new EmailAction
                {
                    Id         = "archive-all",
                    Type       = ActionType.Archive,
                    Properties = JObject.FromObject(new
                    {
                        containerName = "emails"
                    })
                }
            };
            config.Setup(x => x.LoadAsync(It.IsAny <CancellationToken>()))
            .ReturnsAsync(cfg);

            storage.Setup(x => x.UploadAsync("emails", It.IsAny <string>(), It.IsAny <string>(), It.IsAny <CancellationToken>()))
            .Returns(Task.CompletedTask);

            vault.Setup(x => x.GetSecretAsync("Webhook1", It.IsAny <CancellationToken>()))
            .ReturnsAsync("url1");
            vault.Setup(x => x.GetSecretAsync("Webhook2", It.IsAny <CancellationToken>()))
            .ReturnsAsync("url2");

            http.Setup(x => x.SendAsync(It.Is <HttpRequestMessage>(r => r.RequestUri.ToString() == "url1"), It.IsAny <CancellationToken>()))
            .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK));
            http.Setup(x => x.SendAsync(It.Is <HttpRequestMessage>(r => r.RequestUri.ToString() == "url2"), It.IsAny <CancellationToken>()))
            .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK));

            var service = new EmailService(status.Object, vault.Object, config.Object, storage.Object, http.Object);

            var success = await service.ProcessMailAsync(request, CancellationToken.None);

            success.Should().BeTrue();

            status.Verify(x => x.GetStatiAsync(request, It.IsAny <CancellationToken>()));
            status.Verify(x => x.UpdateAsync(request, It.Is <EmailAction>(a => a.Id == "forward-all"), EmailFanoutStatus.Completed, It.IsAny <CancellationToken>()));
            status.Verify(x => x.UpdateAsync(request, It.Is <EmailAction>(a => a.Id == "notify-all"), EmailFanoutStatus.Completed, It.IsAny <CancellationToken>()));
            status.Verify(x => x.UpdateAsync(request, It.Is <EmailAction>(a => a.Id == "archive-all"), EmailFanoutStatus.Completed, It.IsAny <CancellationToken>()));
            status.VerifyNoOtherCalls();

            storage.Verify(x => x.UploadAsync("emails", It.IsAny <string>(), It.IsAny <string>(), It.IsAny <CancellationToken>()));
            storage.VerifyNoOtherCalls();
        }
示例#17
0
        public void Subject_filter_should_match_only_its_actions(string rule, string match, string sender, string recipient, string subject, string body, bool shouldMatch)
        {
            var parser  = new SendgridEmailParser();
            var request = EmailRequest.Parse(Load("mail.txt"), parser);

            request.Email.From.Email  = sender;
            request.Email.To[0].Email = recipient;
            request.Email.Subject     = subject;
            request.Email.Text        = request.Email.Html = body;

            var config = GetConfig();

            config.Rules = new[]
            {
                new EmailRule
                {
                    Filters = new[]
                    {
                        new EmailFilter
                        {
                            Type  = rule,
                            OneOf = new[]
                            {
                                match,
                                "not found"
                            }
                        }
                    },
                    Actions = new[]
                    {
                        new EmailAction
                        {
                            Id         = "notify-all",
                            Type       = ActionType.Webhook,
                            Properties = JObject.FromObject(new
                            {
                                webhook = new
                                {
                                    secretName = "Webhook2"
                                },
                                subject = "You've got mail!",
                                body    = "%subject%"
                            })
                        }
                    }
                }
            };

            if (rule.StartsWith("!"))
            {
                config.Rules[0].Filters[0].AllOf = config.Rules[0].Filters[0].OneOf;
                config.Rules[0].Filters[0].OneOf = null;
            }

            var actions = EmailService.ActionsToPerform(request.Email, config);

            if (shouldMatch)
            {
                actions.Select(a => a.Id).Should().ContainInOrder("notify-all");
            }
            else
            {
                actions.Should().BeEmpty();
            }
        }