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 } }