From 2b717f1aa42761381345a00a688dee7ab3a9d95f Mon Sep 17 00:00:00 2001 From: LW <37078713+lwdeveloper@users.noreply.github.com> Date: Tue, 11 Aug 2020 16:23:20 +0100 Subject: [PATCH 1/2] Handle empty headers on inbound webhooks. Mandrill send an empty array rather than a dictionary when there are no headers --- .../WebHook/MandrillInboundMessageInfo.cs | 5 ++ .../EmptyArrayOrDictionaryConverter.cs | 47 +++++++++++++++++++ .../MandrillJsonContractResolver.cs | 1 - .../Serialization/MandrillSerializer.cs | 6 ++- tests/SerializationTests.cs | 15 +++++- tests/TestData.cs | 44 +++++++++++++++++ 6 files changed, 115 insertions(+), 3 deletions(-) create mode 100644 src/Mandrill.net/Serialization/EmptyArrayOrDictionaryConverter.cs diff --git a/src/Mandrill.net/Model/WebHook/MandrillInboundMessageInfo.cs b/src/Mandrill.net/Model/WebHook/MandrillInboundMessageInfo.cs index d7f8d00..6b853b1 100644 --- a/src/Mandrill.net/Model/WebHook/MandrillInboundMessageInfo.cs +++ b/src/Mandrill.net/Model/WebHook/MandrillInboundMessageInfo.cs @@ -1,5 +1,8 @@ using System; using System.Collections.Generic; +using Mandrill.net.Serialization; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; namespace Mandrill.Model { @@ -13,6 +16,8 @@ public class MandrillInboundMessageInfo public string FromName { get; set; } + /// Inbound webhooks contain either a dictionary or an empty array for the headers property + [JsonConverter(typeof(EmptyArrayOrDictionaryConverter))] public Dictionary Headers { get; set; } = new Dictionary(StringComparer.OrdinalIgnoreCase); public string Html { get; set; } diff --git a/src/Mandrill.net/Serialization/EmptyArrayOrDictionaryConverter.cs b/src/Mandrill.net/Serialization/EmptyArrayOrDictionaryConverter.cs new file mode 100644 index 0000000..f3e5514 --- /dev/null +++ b/src/Mandrill.net/Serialization/EmptyArrayOrDictionaryConverter.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace Mandrill.net.Serialization +{ + /// + /// This converter will allow an empty array to be converted to a dictionary + /// + internal class EmptyArrayOrDictionaryConverter : JsonConverter + { + public override bool CanConvert(Type objectType) + { + return objectType.IsAssignableFrom(typeof(Dictionary)); + } + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + JToken token = JToken.Load(reader); + + if (token.Type == JTokenType.Object) + { + return token.ToObject(objectType); + } + else if (token.Type == JTokenType.Array) + { + if (!token.HasValues) + { + // Create empty dictionary + return Activator.CreateInstance(objectType); + } + } + + throw new JsonSerializationException("Object or empty array expected"); + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + serializer.Serialize(writer, value); + } + } + +} diff --git a/src/Mandrill.net/Serialization/MandrillJsonContractResolver.cs b/src/Mandrill.net/Serialization/MandrillJsonContractResolver.cs index f2c552c..d794b08 100644 --- a/src/Mandrill.net/Serialization/MandrillJsonContractResolver.cs +++ b/src/Mandrill.net/Serialization/MandrillJsonContractResolver.cs @@ -46,7 +46,6 @@ protected override JsonProperty CreateProperty(MemberInfo member, MemberSerializ var dictionary = prop.GetValue(instance) as IDictionary; return dictionary?.Count > 0; }; - } if (typeof(IDictionary).GetTypeInfo().IsAssignableFrom(propertyType.GetTypeInfo())) diff --git a/src/Mandrill.net/Serialization/MandrillSerializer.cs b/src/Mandrill.net/Serialization/MandrillSerializer.cs index 3b8a6c7..0ba776a 100644 --- a/src/Mandrill.net/Serialization/MandrillSerializer.cs +++ b/src/Mandrill.net/Serialization/MandrillSerializer.cs @@ -13,12 +13,16 @@ public static class MandrillSerializer private static JsonSerializer CreateSerializer() { - var settings = new JsonSerializerSettings { ContractResolver = new MandrillJsonContractResolver() }; + var settings = new JsonSerializerSettings + { + ContractResolver = new MandrillJsonContractResolver() + }; settings.Converters.Add(new UnixDateTimeConverter()); settings.Converters.Add(new StringEnumConverter { NamingStrategy = new SnakeCaseNamingStrategy(), AllowIntegerValues = false }); settings.NullValueHandling = NullValueHandling.Ignore; settings.DateTimeZoneHandling = DateTimeZoneHandling.Utc; + return JsonSerializer.Create(settings); } } diff --git a/tests/SerializationTests.cs b/tests/SerializationTests.cs index 793379a..8e5d29b 100644 --- a/tests/SerializationTests.cs +++ b/tests/SerializationTests.cs @@ -389,7 +389,21 @@ public void Can_serialize_inbound_web_hook() events[1].Msg.FromName.Should().BeNullOrEmpty(); Output.WriteLine(JArray.FromObject(events, MandrillSerializer.Instance).ToString()); + } + + [Fact] + public void Can_serialize_inbound_web_hook_with_empty_headers() + { + string json = TestData.mandrill_inbound_empty_headers; + + var events = MandrillInboundEvent.ParseMandrillEvents(json); + events.Should().NotBeNullOrEmpty(); + events.Should().HaveCount(1); + + events[0].Msg.Headers.Should().BeEmpty(); + + Output.WriteLine(JArray.FromObject(events, MandrillSerializer.Instance).ToString()); } [Fact] @@ -406,7 +420,6 @@ public void Can_serialize_sync_web_hook() events[0].Type.Should().Be(MandrillSyncType.Blacklist); } - [Fact] public void Can_serialize_case_insensitive_header_dictionary() { diff --git a/tests/TestData.cs b/tests/TestData.cs index 4d35fdb..8f38292 100644 --- a/tests/TestData.cs +++ b/tests/TestData.cs @@ -294,6 +294,50 @@ public class TestData #endregion + #region mandrill_inbound_empty_headers + + public const string mandrill_inbound_empty_headers = @"[ + { + ""event"": ""inbound"", + ""msg"": { + ""dkim"": { + ""signed"": true, + ""valid"": true + }, + ""email"": ""test@inbound.example.com"", + ""from_email"": ""example.sender@mandrillapp.com"", + ""from_name"": ""Example Sender"", + ""headers"": [], + ""html"": ""

This is an example inbound message.

\n"", + ""raw_msg"": ""Received: from mail115.us4.mandrillapp.com (mail115.us4.mandrillapp.com [205.201.136.115])\n\tby mail.example.com (Postfix) with ESMTP id AAAAAAAAAAA\n\tfor ; Fri, 10 May 2013 19:28:20 +0000 (UTC)\nDKIM-Signature: v=1; a=rsa-sha1; c=relaxed/relaxed; s=mandrill; d=mail115.us4.mandrillapp.com;\n h=From:Sender:Subject:List-Unsubscribe:To:Message-Id:Date:MIME-Version:Content-Type; i=example.sender@mail115.us4.mandrillapp.com;\n bh=d60x72jf42gLILD7IiqBL0OBb40=;\n b=iJd7eBugdIjzqW84UZ2xynlg1SojANJR6xfz0JDD44h78EpbqJiYVcMIfRG7mkrn741Bd5YaMR6p\n 9j41OA9A5am+8BE1Ng2kLRGwou5hRInn+xXBAQm2NUt5FkoXSpvm4gC4gQSOxPbQcuzlLha9JqxJ\n 8ZZ89/20txUrRq9cYxk=\nDomainKey-Signature: a=rsa-sha1; c=nofws; q=dns; s=mandrill; d=mail115.us4.mandrillapp.com;\n b=X6qudHd4oOJvVQZcoAEUCJgB875SwzEO5UKf6NvpfqyCVjdaO79WdDulLlfNVELeuoK2m6alt2yw\n 5Qhp4TW5NegyFf6Ogr/Hy0Lt411r/0lRf0nyaVkqMM/9g13B6D9CS092v70wshX8+qdyxK8fADw8\n kEelbCK2cEl0AGIeAeo=;\nReceived: from localhost (127.0.0.1) by mail115.us4.mandrillapp.com id hhl55a14i282 for ; Fri, 10 May 2013 19:28:20 +0000 (envelope-from )\nDKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=c.mandrillapp.com; \n i=@c.mandrillapp.com; q=dns/txt; s=mandrill; t=1368214100; h=From : \n Sender : Subject : List-Unsubscribe : To : Message-Id : Date : \n MIME-Version : Content-Type : From : Subject : Date : X-Mandrill-User : \n List-Unsubscribe; bh=y5Vz+RDcKZmWzRc9s0xUJR6k4APvBNktBqy1EhSWM8o=; \n b=PLAUIuw8zk8kG5tPkmcnSanElxt6I5lp5t32nSvzVQE7R8u0AmIEjeIDZEt31+Q9PWt+nY\n sHHRsXUQ9SZpndT9Bk++/SmyA2ntU/2AKuqDpPkMZiTqxmGF80Wz4JJgx2aCEB1LeLVmFFwB\n 5Nr/LBSlsBlRcjT9QiWw0/iRvCn74=\nFrom: \nSender: \nSubject: This is an example webhook message\nList-Unsubscribe: \nTo: \nX-Report-Abuse: Please forward a copy of this message, including all headers, to abuse@mandrill.com\nX-Mandrill-User: md_999\nMessage-Id: <999.20130510192820.aaaaaaaaaaaaaa.aaaaaaaa@mail115.us4.mandrillapp.com>\nDate: Fri, 10 May 2013 19:28:20 +0000\nMIME-Version: 1.0\nContent-Type: multipart/alternative; boundary=\""_av-7r7zDhHxVEAo2yMWasfuFw\""\n\n--_av-7r7zDhHxVEAo2yMWasfuFw\nContent-Type: text/plain; charset=utf-8\nContent-Transfer-Encoding: 7bit\n\nThis is an example inbound message.\n--_av-7r7zDhHxVEAo2yMWasfuFw\nContent-Type: text/html; charset=utf-8\nContent-Transfer-Encoding: 7bit\n\n

This is an example inbound message.

\n--_av-7r7zDhHxVEAo2yMWasfuFw--"", + ""sender"": null, + ""spam_report"": { + ""matched_rules"": [], + ""score"": 0 + }, + ""spf"": { + ""detail"": ""sender SPF authorized"", + ""result"": ""pass"" + }, + ""subject"": ""This is an example webhook message"", + ""tags"": [], + ""template"": null, + ""text"": ""This is an example inbound message.\n"", + ""text_flowed"": false, + ""to"": [ + [ + ""test@inbound.example.com"", + null + ] + ], + ""cc"": [] + }, + ""ts"": 1368214102 + } +]"; + + #endregion + #region mandrill_webhook_example public const string mandrill_webhook_example = @"[ From e031c9c0ac4c3b8be615cc8f32313af562340f62 Mon Sep 17 00:00:00 2001 From: LW <37078713+lwdeveloper@users.noreply.github.com> Date: Tue, 11 Aug 2020 18:59:09 +0100 Subject: [PATCH 2/2] Revert MandrillSerializer as only formatting changes were made --- src/Mandrill.net/Serialization/MandrillSerializer.cs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/Mandrill.net/Serialization/MandrillSerializer.cs b/src/Mandrill.net/Serialization/MandrillSerializer.cs index 0ba776a..3b8a6c7 100644 --- a/src/Mandrill.net/Serialization/MandrillSerializer.cs +++ b/src/Mandrill.net/Serialization/MandrillSerializer.cs @@ -13,16 +13,12 @@ public static class MandrillSerializer private static JsonSerializer CreateSerializer() { - var settings = new JsonSerializerSettings - { - ContractResolver = new MandrillJsonContractResolver() - }; + var settings = new JsonSerializerSettings { ContractResolver = new MandrillJsonContractResolver() }; settings.Converters.Add(new UnixDateTimeConverter()); settings.Converters.Add(new StringEnumConverter { NamingStrategy = new SnakeCaseNamingStrategy(), AllowIntegerValues = false }); settings.NullValueHandling = NullValueHandling.Ignore; settings.DateTimeZoneHandling = DateTimeZoneHandling.Utc; - return JsonSerializer.Create(settings); } }