using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Reflection;
using System.Threading.Tasks;
using System.Xml;
using Exceptions;
using SendGrid.SmtpApi;
// ReSharper disable MemberCanBePrivate.Global
namespace SendGrid
{
public class Web : ITransport
{
#region Properties
//TODO: Make this configurable
public const String Endpoint = "https://api.sendgrid.com/api/mail.send.xml";
private readonly NetworkCredential _credentials;
private readonly HttpClient _client;
#endregion
///
/// Creates a new Web interface for sending mail
///
/// SendGridMessage user parameters
public Web(NetworkCredential credentials)
: this(credentials, TimeSpan.FromSeconds(100)) { }
///
/// Creates a new Web interface for sending mail.
///
/// SendGridMessage user parameters
/// HTTP request timeout
public Web(NetworkCredential credentials, TimeSpan httpTimeout)
{
_credentials = credentials;
_client = new HttpClient();
var version = Assembly.GetExecutingAssembly().GetName().Version.ToString();
_client.DefaultRequestHeaders.TryAddWithoutValidation("User-Agent", "sendgrid/" + version + ";csharp");
_client.Timeout = httpTimeout;
}
///
/// Asynchronously delivers a message over SendGrid's Web interface
///
///
public async Task DeliverAsync(ISendGrid message)
{
var content = new MultipartFormDataContent();
AttachFormParams(message, content);
AttachFiles(message, content);
var response = await _client.PostAsync(Endpoint, content);
await CheckForErrorsAsync(response);
}
#region Support Methods
private void AttachFormParams(ISendGrid message, MultipartFormDataContent content)
{
var formParams = FetchFormParams(message);
foreach (var keyValuePair in formParams)
{
content.Add(new StringContent(keyValuePair.Value), keyValuePair.Key);
}
}
private void AttachFiles(ISendGrid message, MultipartFormDataContent content)
{
var files = FetchFileBodies(message);
foreach (var file in files)
{
var fs = new FileStream(file.Key, FileMode.Open, FileAccess.Read);
var fileContent = new StreamContent(fs);
fileContent.Headers.ContentDisposition = new ContentDispositionHeaderValue("form-data")
{
Name = "files[" + Path.GetFileName(file.Key) + "]",
FileName = Path.GetFileName(file.Key)
};
fileContent.Headers.ContentType = MediaTypeHeaderValue.Parse("application/octet-stream");
content.Add(fileContent);
}
var streamingFiles = FetchStreamingFileBodies(message);
foreach (var file in streamingFiles)
{
var stream = file.Value;
var fileContent = new StreamContent(stream);
fileContent.Headers.ContentDisposition = new ContentDispositionHeaderValue("form-data")
{
Name = "files[" + Path.GetFileName(file.Key) + "]",
FileName = Path.GetFileName(file.Key)
};
fileContent.Headers.ContentType = MediaTypeHeaderValue.Parse("application/octet-stream");
content.Add(fileContent);
}
}
private static void FindErrorsInResponse(Stream content)
{
using (var reader = XmlReader.Create(content))
{
while (reader.Read())
{
if (!reader.IsStartElement()) continue;
switch (reader.Name)
{
case "result":
break;
case "message": // success
if (reader.ReadToNextSibling("errors"))
throw new ProtocolViolationException();
return;
case "error": // failure
throw new ProtocolViolationException();
default:
throw new ArgumentException("Unknown element: " + reader.Name);
}
}
}
}
private static string[] GetErrorsInResponse(Stream content)
{
var xmlDoc = new XmlDocument();
xmlDoc.Load(content);
return (from XmlNode errorNode in xmlDoc.SelectNodes("//error") select errorNode.InnerText).ToArray();
}
private static async Task CheckForErrorsAsync(HttpResponseMessage response)
{
var content = await response.Content.ReadAsStreamAsync();
var errors = GetErrorsInResponse(content);
// API error
if (errors.Any())
throw new InvalidApiRequestException(response.StatusCode, errors, response.ReasonPhrase);
// Other error
if (response.StatusCode != HttpStatusCode.OK)
FindErrorsInResponse(content);
}
internal List> FetchFormParams(ISendGrid message)
{
var result = new List>
{
new KeyValuePair("api_user", _credentials.UserName),
new KeyValuePair("api_key", _credentials.Password),
new KeyValuePair("headers",
message.Headers.Count == 0 ? null : Utils.SerializeDictionary(message.Headers)),
new KeyValuePair("replyto",
message.ReplyTo.Length == 0 ? null : message.ReplyTo.ToList().First().Address),
new KeyValuePair("from", message.From.Address),
new KeyValuePair("fromname", message.From.DisplayName),
new KeyValuePair("subject", message.Subject),
new KeyValuePair("text", message.Text),
new KeyValuePair("html", message.Html),
new KeyValuePair("x-smtpapi", message.Header.JsonString() ?? "")
};
if (message.To != null)
{
result = result.Concat(message.To.ToList().Select(a => new KeyValuePair("to[]", a.Address)))
.Concat(message.To.ToList().Select(a => new KeyValuePair("toname[]", a.DisplayName)))
.ToList();
}
if (message.Cc != null)
{
result.AddRange(message.Cc.Select(c => new KeyValuePair("cc[]", c.Address)));
}
if (message.Bcc != null)
{
result.AddRange(message.Bcc.Select(c => new KeyValuePair("bcc[]", c.Address)));
}
if (message.GetEmbeddedImages().Count > 0) {
result = result.Concat(message.GetEmbeddedImages().ToList().Select(x => new KeyValuePair(string.Format("content[{0}]", x.Key), x.Value)))
.ToList();
}
return result.Where(r => !String.IsNullOrEmpty(r.Value)).ToList();
}
internal IEnumerable> FetchStreamingFileBodies(ISendGrid message)
{
return message.StreamedAttachments.Select(kvp => kvp).ToList();
}
internal List> FetchFileBodies(ISendGrid message)
{
return message.Attachments == null
? new List>()
: message.Attachments.Select(name => new KeyValuePair(name, new FileInfo(name))).ToList();
}
#endregion
}
}