Skip to content

Commit

Permalink
Merge branch 'feature/avoid_duplicate_recipients' into develop
Browse files Browse the repository at this point in the history
  • Loading branch information
Jericho committed May 5, 2018
2 parents a33711b + 89dcd24 commit b5d7a26
Show file tree
Hide file tree
Showing 3 changed files with 100 additions and 19 deletions.
16 changes: 11 additions & 5 deletions Source/StrongGrid.IntegrationTests/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -170,13 +170,17 @@ private static async Task Mail(IClient client, TextWriter log, CancellationToken
var to1 = new MailAddress("recipient1@mailinator.com", "Recipient1");
var to2 = new MailAddress("recipient2@mailinator.com", "Recipient2");
var subject = "Dear {{customer_type}}";
var textContent = new MailContent("text/plain", "Hello world!");
var htmlContent = new MailContent("text/html", "<html><body>Hello <b><i>{{first_name}}!</i></b><br/></body></html>");
var text = "Hello world!";
var html = "<html><body>Hello <b><i>{{first_name}}!</i></b><br/></body></html>";
var textContent = new MailContent("text/plain", text);
var htmlContent = new MailContent("text/html", html);
var personalizations = new[]
{
new MailPersonalization
{
To = new[] { to1 },
To = new[] { to1, to1 },
Cc = new[] { to1 },
Bcc = new[] { to1 },
Substitutions = new KeyValuePair<string, string>[]
{
new KeyValuePair<string, string>("{{customer_type}}", "friend"),
Expand All @@ -190,6 +194,8 @@ private static async Task Mail(IClient client, TextWriter log, CancellationToken
new MailPersonalization
{
To = new[] { to2 },
Cc = new[] { to2, to2 },
Bcc = new[] { to2 },
Substitutions = new KeyValuePair<string, string>[]
{
new KeyValuePair<string, string>("{{customer_type}}", "customer"),
Expand Down Expand Up @@ -261,10 +267,10 @@ private static async Task Mail(IClient client, TextWriter log, CancellationToken

/******
Here's the simplified way to send a single email to a single recipient:
var messageId = await client.Mail.SendToSingleRecipientAsync(to, from, subject, htmlContent, textContent, cancellationToken: cancellationToken).ConfigureAwait(false);
var messageId = await client.Mail.SendToSingleRecipientAsync(to, from, subject, html, text, cancellationToken: cancellationToken).ConfigureAwait(false);
Here's the simplified way to send the same email to multiple recipients:
var messageId = await client.Mail.SendToMultipleRecipientsAsync(new[] { to1, to2, to3 }, from, subject, htmlContent, textContent, cancellationToken: cancellationToken).ConfigureAwait(false);
var messageId = await client.Mail.SendToMultipleRecipientsAsync(new[] { to1, to2, to3 }, from, subject, html, text, cancellationToken: cancellationToken).ConfigureAwait(false);
******/
}

Expand Down
61 changes: 47 additions & 14 deletions Source/StrongGrid/Resources/Mail.cs
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,10 @@ public Task<string> SendToMultipleRecipientsAsync(
MailSettings mailSettings = null,
CancellationToken cancellationToken = default(CancellationToken))
{
var personalizations = recipients.Select(r => new MailPersonalization { To = new[] { r } });
var personalizations = new[]
{
new MailPersonalization { To = recipients.ToArray() }
};

var contents = new List<MailContent>();
if (!string.IsNullOrEmpty(textContent)) contents.Add(new MailContent("text/plain", textContent));
Expand Down Expand Up @@ -223,12 +226,52 @@ public async Task<string> SendAsync(
throw new ArgumentNullException(nameof(personalizations));
}

// This comparer is used to perform case-insensitive comparisons of email addresses
var emailAddressComparer = new LambdaComparer<MailAddress>((address1, address2) => address1.Email.Equals(address2.Email, StringComparison.OrdinalIgnoreCase));

// It's important to make a copy of the personalizations to ensure we don't modify the original array
var personalizationsCopy = personalizations.ToArray();
foreach (var personalization in personalizationsCopy)
{
// Avoid duplicate addresses. This is important because SendGrid does not throw any
// exception when a recipient is duplicated (which gives you the impression the email
// was sent) but it does not actually send the email
personalization.To = personalization.To?
.Distinct(emailAddressComparer)
.ToArray();
personalization.Cc = personalization.Cc?
.Distinct(emailAddressComparer)
.Except(personalization.To, emailAddressComparer)
.ToArray();
personalization.Bcc = personalization.Bcc?
.Distinct(emailAddressComparer)
.Except(personalization.To, emailAddressComparer)
.Except(personalization.Cc, emailAddressComparer)
.ToArray();

// SendGrid doesn't like empty arrays
if (!(personalization.To?.Any() ?? true)) personalization.To = null;
if (!(personalization.Cc?.Any() ?? true)) personalization.Cc = null;
if (!(personalization.Bcc?.Any() ?? true)) personalization.Bcc = null;

// Surround recipient names with double-quotes if necessary
personalization.To = EnsureRecipientsNamesAreQuoted(personalization.To);
personalization.Cc = EnsureRecipientsNamesAreQuoted(personalization.Cc);
personalization.Bcc = EnsureRecipientsNamesAreQuoted(personalization.Bcc);
}

// The total number of recipients must be less than 1000. This includes all recipients defined within the to, cc, and bcc parameters, across each object that you include in the personalizations array.
var numberOfRecipients = personalizations.Sum(p => p?.To?.Count(r => r != null) ?? 0);
numberOfRecipients += personalizations.Sum(p => p?.Cc?.Count(r => r != null) ?? 0);
numberOfRecipients += personalizations.Sum(p => p?.Bcc?.Count(r => r != null) ?? 0);
var numberOfRecipients = personalizationsCopy.Sum(p => p?.To?.Count(r => r != null) ?? 0);
numberOfRecipients += personalizationsCopy.Sum(p => p?.Cc?.Count(r => r != null) ?? 0);
numberOfRecipients += personalizationsCopy.Sum(p => p?.Bcc?.Count(r => r != null) ?? 0);
if (numberOfRecipients >= 1000) throw new ArgumentOutOfRangeException("The total number of recipients must be less than 1000");

// SendGrid throws an unhelpful error when the Bcc email address is an empty string
if (mailSettings?.Bcc != null && mailSettings.Bcc.EmailAddress?.Trim() == string.Empty)
{
mailSettings.Bcc.EmailAddress = null;
}

var data = new JObject();
data.AddPropertyIfValue("from", from);
data.AddPropertyIfValue("reply_to", replyTo);
Expand All @@ -243,16 +286,6 @@ public async Task<string> SendAsync(
data.AddPropertyIfValue("ip_pool_name", ipPoolName);
data.AddPropertyIfValue("mail_settings", mailSettings);
data.AddPropertyIfValue("tracking_settings", trackingSettings);

// It's important to make a copy of the personalizations to ensure we don't modify the original array
var personalizationsCopy = personalizations.ToArray();
foreach (var personalization in personalizationsCopy)
{
personalization.To = EnsureRecipientsNamesAreQuoted(personalization.To);
personalization.Cc = EnsureRecipientsNamesAreQuoted(personalization.Cc);
personalization.Bcc = EnsureRecipientsNamesAreQuoted(personalization.Bcc);
}

data.AddPropertyIfValue("personalizations", personalizationsCopy);

if (sections != null && sections.Any())
Expand Down
42 changes: 42 additions & 0 deletions Source/StrongGrid/Utilities/LambdaComparer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
using System;
using System.Collections.Generic;

namespace StrongGrid.Utilities
{
/// <summary>
/// LambdaComparer - avoids the need for writing custom IEqualityComparers
/// Usage:
/// List&lt;MyObject&gt; x = myCollection.Except(otherCollection, new LambdaComparer&lt;MyObject&gt;((x, y) => x.Id == y.Id)).ToList();
/// or
/// IEqualityComparer comparer = new LambdaComparer&lt;MyObject&gt;((x, y) => x.Id == y.Id);
/// List&lt;MyObject&gt; x = myCollection.Except(otherCollection, comparer).ToList();
/// </summary>
/// <typeparam name="T">The type to compare</typeparam>
/// <remarks>From: http://toreaurstad.blogspot.ca/2014/06/a-generic-iequalitycomparer-of-t.html</remarks>
internal class LambdaComparer<T> : IEqualityComparer<T>
{
private readonly Func<T, T, bool> _lambdaComparer;
private readonly Func<T, int> _lambdaHash;

public LambdaComparer(Func<T, T, bool> lambdaComparer)
: this(lambdaComparer, o => 0)
{
}

public LambdaComparer(Func<T, T, bool> lambdaComparer, Func<T, int> lambdaHash)
{
_lambdaComparer = lambdaComparer ?? throw new ArgumentNullException(nameof(lambdaComparer));
_lambdaHash = lambdaHash ?? throw new ArgumentNullException(nameof(lambdaHash));
}

public bool Equals(T x, T y)
{
return _lambdaComparer(x, y);
}

public int GetHashCode(T obj)
{
return _lambdaHash(obj);
}
}
}

0 comments on commit b5d7a26

Please sign in to comment.