Hi Martin
Email marketing can send emails either by using SMTP (SendMessageProvider) or using an API based solution like SendGrid or Mandrill.
When mails are send using SMTP, Dynamicweb does not have a way of getting the non-delivery reports.
When using the APIs, we can ask the API for the information when needed.
When sending emails, if you need to send a lot of emails, SMTP is not a good option - because it takes a long time and can be limited in how many messages can be send. So using Office 365 to send i.e. 10000 emails, will probably not work. And Dynamicweb cannot get the non-delivery reports since it is SMTP.
It might be possible to create a MessageDeliveryProvider for Office 365 that use their API (if it exists), and by implementing IBlockedEmails, IBouncedEmails, IInvalidEmails, ISpamEmails interfaces on that provider, it will be possible to get those information into DW.
An example below
Our sendgrid provider:
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Globalization;
using System.IO;
using System.Net;
using System.Net.Mail;
using System.Net.Mime;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Xml;
using Dynamicweb.Extensibility.AddIns;
using Dynamicweb.Extensibility.Editors;
using Dynamicweb.Logging;
using SendGrid;
using SendGrid.Helpers.Mail;
using Attachment = SendGrid.Helpers.Mail.Attachment;
namespace Dynamicweb.Mailing.DeliveryProviders.SendGridProvider
{
/// <summary>
/// SendGrid Email service provider
/// </summary>
[AddInName("SendGrid")]
public class SendGridProvider : MessageDeliveryProvider, IBlockedEmails, IBouncedEmails, IInvalidEmails, ISpamEmails, IDropDownOptions
{
private int _timeWindow;
private string _timeZoneId;
[AddInLabel("API key"), AddInParameter("ApiKey"), AddInParameterEditor(typeof(TextParameterEditor), "NewGUI=true")]
public string ApiKey { get; set; }
[AddInParameter("User name"), AddInParameterEditor(typeof(TextParameterEditor), "NewGUI=true;infoText=Obsolete")]
public string UserName { get; set; }
[AddInParameter("Password"), AddInParameterEditor(typeof(TextParameterEditor), "NewGUI=true;infoText=Obsolete")]
public string Password { get; set; }
[AddInParameter("TimeZone"), AddInParameterEditor(typeof(DropDownParameterEditor), "NewGUI=true;none=false;SortBy=Value")]
public string TimeZoneId
{
get
{
if (string.IsNullOrEmpty(_timeZoneId))
{
_timeZoneId = TimeZoneInfo.Local.Id;
}
return _timeZoneId;
}
set
{
_timeZoneId = value;
}
}
public TimeZoneInfo TimeZone
{
get
{
return TimeZoneInfo.FindSystemTimeZoneById(TimeZoneId);
}
}
[AddInParameter("Time window"), AddInDescription("Defines a time window (in minutes) to allow for discrepancies in time configuration between SendGrid and Dynamicweb. Cannot be 0 (zero), default is 60."), AddInParameterEditor(typeof(TextParameterEditor), "NewGUI=true")]
public int TimeWindowInMinutes
{
get
{
if (_timeWindow <= 0)
{
_timeWindow = 60;
}
return _timeWindow;
}
set
{
_timeWindow = value;
}
}
/// <summary>
/// Delivers MailMessage using SmtpClient
/// </summary>
/// <param name="mailMessage">email message</param>
/// <returns>True if message delivery was successfull</returns>
public override bool Deliver(MailMessage mailMessage)
{
if (UseApiV3())
{
var client = new SendGridClient(ApiKey);
var message = new SendGridMessage()
{
From = new EmailAddress(mailMessage.From.Address, mailMessage.From.DisplayName),
Subject = mailMessage.Subject
};
message.AddContent(mailMessage.IsBodyHtml ? "text/html" : "text/plain", mailMessage.Body);
foreach (var address in mailMessage.To)
{
message.AddTo(new EmailAddress(address.Address, address.DisplayName));
}
foreach (var attachment in mailMessage.Attachments)
{
var fileContent = ConvertAttachment(attachment);
var bytes = fileContent.ReadAsByteArrayAsync().GetAwaiter().GetResult();
var file = Convert.ToBase64String(bytes);
message.AddAttachment(new Attachment { Filename = attachment.Name, Content = file });
}
Response response = client.SendEmailAsync(message).GetAwaiter().GetResult();
if (response.StatusCode != HttpStatusCode.Accepted)
{
var bodyText = response.Body.ReadAsStringAsync().GetAwaiter().GetResult();
throw LogException(string.Format("SendGrid returned an error '{0}' for message '{1}' - <{2}>", bodyText, mailMessage.Subject, mailMessage.To.First().Address));
}
}
else if (UseApiV2())
{
LogManager.System.GetLogger(LogCategory.Provider, nameof(SendGridProvider)).Warn("Your SendGrid provider is configured to use deprecated API v2. Specify API key instead of UserName and Password to use API v3.");
string url = "https://api.sendgrid.com/api/mail.send.xml";
HttpClient client = new HttpClient();
MultipartFormDataContent content = new MultipartFormDataContent();
Dictionary<string, string> mailParams = new Dictionary<string, string>();
string text = !mailMessage.IsBodyHtml ? mailMessage.Body : null;
string html = mailMessage.IsBodyHtml ?
mailMessage.Body :
new StreamReader(mailMessage.AlternateViews.First(view => view.ContentType.MediaType == MediaTypeNames.Text.Html).ContentStream).ReadToEnd();
mailParams.Add("api_user", UserName);
mailParams.Add("api_key", Password);
mailParams.Add("to", mailMessage.To.First().Address);
mailParams.Add("toname", mailMessage.To.First().DisplayName);
mailParams.Add("from", mailMessage.From.Address);
mailParams.Add("fromname", mailMessage.From.DisplayName);
mailParams.Add("subject", mailMessage.Subject);
mailParams.Add("html", html);
if (text != null)
{
mailParams.Add("text", text);
}
foreach (KeyValuePair<string, string> currentKeyValuePair in mailParams)
{
content.Add(new StringContent(currentKeyValuePair.Value), currentKeyValuePair.Key);
}
foreach (var attachment in mailMessage.Attachments)
{
content.Add(ConvertAttachment(attachment));
}
HttpResponseMessage response = client.PostAsync(url, content).GetAwaiter().GetResult();
if (!response.IsSuccessStatusCode)
{
throw LogException(string.Format("SendGrid returned an error '{0}' for message '{1}' - <{2}>", response.ReasonPhrase, mailMessage.Subject, mailMessage.To.First().Address));
}
}
else
{
throw LogException("API key or UserName and/or Password contains no value. SendGrid requires a valid account. Please configure this DeliveryProvider again.");
}
return true;
}
/// <summary>
/// Gets bounced email infos
/// </summary>
/// <param name="message">Message instance</param>
/// <returns>Collection of MessageRecipientInfo</returns>
public MessageRecipientInfoCollection GetBouncedEmailInfos(Message message)
{
return GetSendGridInfo(message, "bounces", "bounce");
}
/// <summary>
/// Gets blocked email infos
/// </summary>
/// <param name="message">Message instance</param>
/// <returns>Collection of MessageRecipientInfo</returns>
public MessageRecipientInfoCollection GetBlockedEmailInfos(Message message)
{
return GetSendGridInfo(message, "blocks", "block");
}
/// <summary>
/// Gets invalid email infos
/// </summary>
/// <param name="message">Message instance</param>
/// <returns>Collection of MessageRecipientInfo</returns>
public MessageRecipientInfoCollection GetInvalidEmailInfos(Message message)
{
return GetSendGridInfo(message, "invalidemails", "invalidemail");
}
/// <summary>
/// Gets span email infos
/// </summary>
/// <param name="message">Message instance</param>
/// <returns>Collection of MessageRecipientInfo</returns>
public MessageRecipientInfoCollection GetSpamEmailInfos(Message message)
{
return GetSendGridInfo(message, "spamreports", "spamreport");
}
private MessageRecipientInfoCollection GetSendGridInfo(Message message, string apiMethod, string responseElement)
{
MessageRecipientInfoCollection result = new MessageRecipientInfoCollection();
if (message == null || string.IsNullOrEmpty(apiMethod) || string.IsNullOrEmpty(responseElement))
{
return result;
}
DateTime convertedStartTime = message.SentTime.StartTime;
DateTime convertedEndTime = message.SentTime.EndTime;
if (TimeZone.Id != TimeZoneInfo.Local.Id)
{
convertedStartTime = TimeZoneInfo.ConvertTime(convertedStartTime, TimeZoneInfo.Local, TimeZone);
convertedEndTime = TimeZoneInfo.ConvertTime(convertedEndTime, TimeZoneInfo.Local, TimeZone);
}
DateTime startTimeWithGrace = convertedStartTime.AddMinutes(-TimeWindowInMinutes);
DateTime endTimeWithGrace = convertedEndTime.AddMinutes(TimeWindowInMinutes);
string url = string.Empty;
if (UseApiV3())
{
int startTime = (int)startTimeWithGrace.Subtract(new DateTime(1970, 1, 1)).TotalSeconds;
int endTime = (int)endTimeWithGrace.Subtract(new DateTime(1970, 1, 1)).TotalSeconds;
string newApiMethod;
if (apiMethod.Equals("invalidemails", StringComparison.OrdinalIgnoreCase))
{
newApiMethod = "invalid_emails";
}
else if (apiMethod.Equals("spamreports", StringComparison.OrdinalIgnoreCase))
{
newApiMethod = "spam_reports";
}
else
{
newApiMethod = apiMethod;
}
url = string.Format("https://api.sendgrid.com/v3/suppression/{0}?start_time={1}&end_time={2}",
newApiMethod, startTime, endTime
);
}
else if (UseApiV2())
{
string startTime = startTimeWithGrace.ToString("yyyy-MM-dd");
string endTime = endTimeWithGrace.ToString("yyyy-MM-dd");
url = string.Format("https://sendgrid.com/api/{0}.get.xml?api_user={1}&api_key={2}&start_date={3}&end_date={4}&date=1",
apiMethod, UserName, Password, startTime, endTime
);
}
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
if (UseApiV3())
{
request.Headers.Add(HttpRequestHeader.Authorization, "Bearer " + ApiKey);
}
HttpWebResponse response = (HttpWebResponse)request.GetResponse();
using (StreamReader streamReader = new StreamReader(response.GetResponseStream()))
{
using (XmlTextReader xmlTextReader = new XmlTextReader(streamReader))
{
while (xmlTextReader.ReadToFollowing(responseElement))
{
xmlTextReader.ReadToFollowing("created");
DateTime created = DateTime.ParseExact(xmlTextReader.ReadString(), "yyyy-MM-dd HH:mm:ss", null, DateTimeStyles.None);
if (created < startTimeWithGrace || created > endTimeWithGrace)
{
continue;
}
xmlTextReader.ReadToFollowing("reason");
string reason = xmlTextReader.ReadString();
xmlTextReader.ReadToFollowing("email");
string email = xmlTextReader.ReadString();
result.Add(new MessageRecipientInfo()
{
EmailAddress = email,
EmailInfo = reason,
EmailMessage = message
});
}
}
}
return result;
}
private bool UseApiV3()
{
return !string.IsNullOrEmpty(ApiKey);
}
private bool UseApiV2()
{
return string.IsNullOrEmpty(ApiKey) && !string.IsNullOrEmpty(UserName) && !string.IsNullOrEmpty(Password);
}
private static StreamContent ConvertAttachment(System.Net.Mail.Attachment attachment)
{
StreamContent fileContent = new StreamContent(attachment.ContentStream);
fileContent.Headers.ContentType = MediaTypeHeaderValue.Parse("application/octet-stream");
fileContent.Headers.ContentDisposition = new ContentDispositionHeaderValue("form-data");
fileContent.Headers.ContentDisposition.Name = "files[" + Context.Current.Server.UrlEncode(attachment.Name).Replace("+", "%20") + "]";
fileContent.Headers.ContentDisposition.FileName = attachment.Name;
return fileContent;
}
private static ArgumentException LogException(string message)
{
var exception = new ArgumentException(message);
LogManager.System.GetLogger(LogCategory.Provider, nameof(SendGridProvider)).Error(exception.Message, exception);
return exception;
}
/// <summary>
/// Gets TimeZone options
/// </summary>
/// <param name="Name">TimeZone</param>
/// <returns>HashTable with result data</returns>
public Hashtable GetOptions(string name)
{
Hashtable result = new Hashtable();
switch (name)
{
case "TimeZone":
foreach (TimeZoneInfo tz in TimeZoneInfo.GetSystemTimeZones())
{
result.Add(tz.Id, tz.DisplayName);
}
break;
default:
break;
}
return result;
}
}
}