Skip to content

Commit

Permalink
(GH-391) Add MailSettingsConverter and use it instead of manually ser…
Browse files Browse the repository at this point in the history
…ializing the mail settings
  • Loading branch information
Jericho committed May 8, 2021
1 parent c23c218 commit 05eb3c9
Show file tree
Hide file tree
Showing 4 changed files with 169 additions and 45 deletions.
17 changes: 16 additions & 1 deletion Source/StrongGrid.UnitTests/Resources/MailTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,23 @@ public async Task SendAsync()
}
};

var mailSettings = new MailSettings()
{
BypassBounceManagement = true,
BypassListManagement = true,
BypassSpamManagement = true,
BypassUnsubscribeManagement = true,
SandboxModeEnabled = true,
Footer = new FooterSettings()
{
Enabled = true,
HtmlContent = "abc123",
TextContent = "yzx890"
}
};

// Act
var result = await mail.SendAsync(personalizations, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, MailPriority.Normal, CancellationToken.None).ConfigureAwait(false);
var result = await mail.SendAsync(personalizations, null, null, null, null, null, null, null, null, null, null, null, null, null, mailSettings, null, MailPriority.Normal, CancellationToken.None).ConfigureAwait(false);

// Assert
mockHttp.VerifyNoOutstandingExpectation();
Expand Down
4 changes: 4 additions & 0 deletions Source/StrongGrid/Models/MailSettings.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
using Newtonsoft.Json;
using StrongGrid.Utilities;

namespace StrongGrid.Models
{
/// <summary>
/// Mail settings.
/// </summary>
[JsonConverter(typeof(MailSettingsConverter))]
public class MailSettings
{
/// <summary>
Expand Down
73 changes: 29 additions & 44 deletions Source/StrongGrid/Resources/Mail.cs
Original file line number Diff line number Diff line change
Expand Up @@ -188,19 +188,18 @@ public async Task<string> SendAsync(
numberOfRecipients += personalizationsCopy.Sum(p => p.Bcc?.Count(r => r != null) ?? 0);
if (numberOfRecipients >= 1000) throw new ArgumentOutOfRangeException(nameof(numberOfRecipients), numberOfRecipients, "The total number of recipients must be less than 1000");

var isDynamicTemplate = Template.IsDynamic(templateId);
var personalizationConverter = new MailPersonalizationConverter(isDynamicTemplate);
// Get the priority headers
if (!_priorityHeaders.TryGetValue(priority, out KeyValuePair<string, string>[] priorityHeaders))
{
throw new Exception($"{priority} is an unknown priority");
}

// Manually serialize the MailSettings object because the shape of the C# class does not match the shape of the desired JSON.
// The C# class was intentionaly simplified to improve developers experience.
var mailSettingsJsonObject = new JObject();
mailSettingsJsonObject.AddPropertyIfValue("bypass_list_management/enable", mailSettings.BypassListManagement);
mailSettingsJsonObject.AddPropertyIfValue("bypass_spam_management/enable", mailSettings.BypassSpamManagement);
mailSettingsJsonObject.AddPropertyIfValue("bypass_bounce_management/enable", mailSettings.BypassBounceManagement);
mailSettingsJsonObject.AddPropertyIfValue("bypass_unsubscribe_management/enable", mailSettings.BypassUnsubscribeManagement);
mailSettingsJsonObject.AddPropertyIfValue("footer", mailSettings.Footer);
mailSettingsJsonObject.AddPropertyIfValue("sandbox_mode/enable", mailSettings.SandboxModeEnabled);
// Combine headers with priority headers making sure not to duplicate priority headers
var combinedHeaders = (headers ?? Array.Empty<KeyValuePair<string, string>>())
.Where(kvp => !priorityHeaders.Any(p => p.Key.Equals(kvp.Key, StringComparison.OrdinalIgnoreCase)))
.Concat(priorityHeaders);

// Serialize the mail message
var data = new JObject();
data.AddPropertyIfValue("from", from);
data.AddPropertyIfValue("reply_to", replyTo);
Expand All @@ -213,52 +212,25 @@ public async Task<string> SendAsync(
data.AddPropertyIfValue("batch_id", batchId);
data.AddPropertyIfValue("asm", unsubscribeOptions);
data.AddPropertyIfValue("ip_pool_name", ipPoolName);
data.AddPropertyIfValue("mail_settings", mailSettingsJsonObject);
data.AddPropertyIfValue("mail_settings", mailSettings);
data.AddPropertyIfValue("tracking_settings", trackingSettings);
data.AddPropertyIfValue("personalizations", personalizationsCopy, personalizationConverter);

if (_priorityHeaders.TryGetValue(priority, out KeyValuePair<string, string>[] priorityHeaders))
{
headers = (headers ?? Array.Empty<KeyValuePair<string, string>>()).Concat(priorityHeaders);
}
else
{
throw new Exception($"{priority} is an unknown priority");
}

if (headers != null && headers.Any())
{
var hdrs = new JObject();
foreach (var header in headers)
{
hdrs.Add(header.Key, header.Value);
}

data.Add("headers", hdrs);
}

if (customArgs != null && customArgs.Any())
{
var args = new JObject();
foreach (var customArg in customArgs)
{
args.Add(customArg.Key, customArg.Value);
}

data.Add("custom_args", args);
}
data.AddPropertyIfValue("personalizations", personalizationsCopy, new MailPersonalizationConverter(Template.IsDynamic(templateId)));
data.AddPropertyIfValue("headers", ConvertEnumerationToJObject(combinedHeaders));
data.AddPropertyIfValue("custom_args", ConvertEnumerationToJObject(customArgs));

// SendGrid does not allow emails that exceed 30MB
var contentSize = JsonConvert.SerializeObject(data, Formatting.None).Length;
if (contentSize > MAX_EMAIL_SIZE) throw new Exception("Email exceeds the size limit");

// Send the request
var response = await _client
.PostAsync($"{_endpoint}/send")
.WithJsonBody(data)
.WithCancellationToken(cancellationToken)
.AsResponse()
.ConfigureAwait(false);

// Get the messageId from the response
if (response.Message.Headers.TryGetValues("X-Message-Id", out IEnumerable<string> values))
{
return values?.FirstOrDefault();
Expand Down Expand Up @@ -288,5 +260,18 @@ private static MailAddress[] EnsureRecipientsNamesAreQuoted(MailAddress[] recipi
.Union(recipientsWithoutName)
.ToArray();
}

private static JObject ConvertEnumerationToJObject(IEnumerable<KeyValuePair<string, string>> items)
{
if (items == null || !items.Any()) return null;

var obj = new JObject();
foreach (var item in items)
{
obj.Add(item.Key, item.Value);
}

return obj;
}
}
}
120 changes: 120 additions & 0 deletions Source/StrongGrid/Utilities/MailSettingsConverter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
using Newtonsoft.Json;
using StrongGrid.Models;
using System;

namespace StrongGrid.Utilities
{
/// <summary>
/// Converts a MailSettings object to and from JSON.
/// </summary>
/// <seealso cref="Newtonsoft.Json.JsonConverter" />
internal class MailSettingsConverter : JsonConverter
{
public MailSettingsConverter()
{
}

/// <summary>
/// Determines whether this instance can convert the specified object type.
/// </summary>
/// <param name="objectType">Type of the object.</param>
/// <returns>
/// <c>true</c> if this instance can convert the specified object type; otherwise, <c>false</c>.
/// </returns>
public override bool CanConvert(Type objectType)
{
return objectType == typeof(MailSettings);
}

/// <summary>
/// Gets a value indicating whether this <see cref="T:Newtonsoft.Json.JsonConverter" /> can read JSON.
/// </summary>
/// <value>
/// <c>true</c> if this <see cref="T:Newtonsoft.Json.JsonConverter" /> can read JSON; otherwise, <c>false</c>.
/// </value>
public override bool CanRead
{
get { return false; }
}

/// <summary>
/// Gets a value indicating whether this <see cref="T:Newtonsoft.Json.JsonConverter" /> can write JSON.
/// </summary>
/// <value>
/// <c>true</c> if this <see cref="T:Newtonsoft.Json.JsonConverter" /> can write JSON; otherwise, <c>false</c>.
/// </value>
public override bool CanWrite
{
get { return true; }
}

/// <summary>
/// Writes the JSON representation of the object.
/// </summary>
/// <param name="writer">The <see cref="T:Newtonsoft.Json.JsonWriter" /> to write to.</param>
/// <param name="value">The value.</param>
/// <param name="serializer">The calling serializer.</param>
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
if (value == null) return;

writer.WriteStartObject();

var mailSettings = (MailSettings)value;

writer.WritePropertyName("bypass_list_management");
writer.WriteStartObject();
writer.WritePropertyName("enable");
serializer.Serialize(writer, mailSettings.BypassListManagement);
writer.WriteEndObject();

writer.WritePropertyName("bypass_spam_management");
writer.WriteStartObject();
writer.WritePropertyName("enable");
serializer.Serialize(writer, mailSettings.BypassSpamManagement);
writer.WriteEndObject();

writer.WritePropertyName("bypass_bounce_management");
writer.WriteStartObject();
writer.WritePropertyName("enable");
serializer.Serialize(writer, mailSettings.BypassBounceManagement);
writer.WriteEndObject();

writer.WritePropertyName("bypass_unsubscribe_management");
writer.WriteStartObject();
writer.WritePropertyName("enable");
serializer.Serialize(writer, mailSettings.BypassUnsubscribeManagement);
writer.WriteEndObject();

if (mailSettings.Footer != null)
{
writer.WritePropertyName("footer");
serializer.Serialize(writer, mailSettings.Footer);
}

writer.WritePropertyName("sandbox_mode");
writer.WriteStartObject();
writer.WritePropertyName("enable");
serializer.Serialize(writer, mailSettings.SandboxModeEnabled);
writer.WriteEndObject();

// End of JSON serialization
writer.WriteEndObject();
}

/// <summary>
/// Reads the JSON representation of the object.
/// </summary>
/// <param name="reader">The <see cref="T:Newtonsoft.Json.JsonReader" /> to read from.</param>
/// <param name="objectType">Type of the object.</param>
/// <param name="existingValue">The existing value of object being read.</param>
/// <param name="serializer">The calling serializer.</param>
/// <returns>
/// The object value.
/// </returns>
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
throw new NotSupportedException("The MailSettingsConverter can only be used to write JSON");
}
}
}

0 comments on commit 05eb3c9

Please sign in to comment.