diff --git a/src/SparkPost.Acceptance/MessageEventsSteps.cs b/src/SparkPost.Acceptance/MessageEventsSteps.cs index 2c072aa..5defd0a 100644 --- a/src/SparkPost.Acceptance/MessageEventsSteps.cs +++ b/src/SparkPost.Acceptance/MessageEventsSteps.cs @@ -1,4 +1,5 @@ -using System.Threading.Tasks; +using System; +using System.Threading.Tasks; using TechTalk.SpecFlow; namespace SparkPost.Acceptance @@ -13,6 +14,7 @@ public MessageEventsSteps(ScenarioContext scenarioContext) _scenarioContext = scenarioContext; } + [Obsolete()] [When(@"I ask for samples of '(.*)'")] public async Task WhenIAskForSamplesOf(string events) { diff --git a/src/SparkPost.Acceptance/SparkPost.Acceptance.csproj b/src/SparkPost.Acceptance/SparkPost.Acceptance.csproj index d16c5e2..ee7b1ab 100644 --- a/src/SparkPost.Acceptance/SparkPost.Acceptance.csproj +++ b/src/SparkPost.Acceptance/SparkPost.Acceptance.csproj @@ -1,28 +1,24 @@ - + - netcoreapp2.1 + net6.0 + SparkPost.Acceptance + Library - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - + + + + + - + + Always + diff --git a/src/SparkPost.Acceptance/TransmissionsSteps.cs b/src/SparkPost.Acceptance/TransmissionsSteps.cs index 7553e3c..b335f51 100644 --- a/src/SparkPost.Acceptance/TransmissionsSteps.cs +++ b/src/SparkPost.Acceptance/TransmissionsSteps.cs @@ -1,8 +1,9 @@ -using System; +using System; using TechTalk.SpecFlow; namespace SparkPost.Acceptance { + [Obsolete] [Binding] public class TransmissionsSteps { @@ -11,49 +12,49 @@ public void GivenIHaveANewTransmission() { ScenarioContext.Current.Pending(); } - + [Given(@"the transmission is meant to be sent from '(.*)'")] public void GivenTheTransmissionIsMeantToBeSentFrom(string p0) { ScenarioContext.Current.Pending(); } - + [Given(@"the transmission is meant to be sent to '(.*)'")] public void GivenTheTransmissionIsMeantToBeSentTo(string p0) { ScenarioContext.Current.Pending(); } - + [Given(@"the transmission content is")] public void GivenTheTransmissionContentIs(Table table) { ScenarioContext.Current.Pending(); } - + [Given(@"the transmission has a text file attachment")] public void GivenTheTransmissionHasATextFileAttachment() { ScenarioContext.Current.Pending(); } - + [Given(@"the transmission template id is set to '(.*)'")] public void GivenTheTransmissionTemplateIdIsSetTo(string p0) { ScenarioContext.Current.Pending(); } - + [Given(@"the transmission is meant to be CCd to '(.*)'")] public void GivenTheTransmissionIsMeantToBeCCdTo(string p0) { ScenarioContext.Current.Pending(); } - + [Given(@"the transmission is meant to be BCCd to '(.*)'")] public void GivenTheTransmissionIsMeantToBeBCCdTo(string p0) { ScenarioContext.Current.Pending(); } - + [When(@"I send the transmission")] public void WhenISendTheTransmission() { diff --git a/src/SparkPost.Tests/ClientTests.cs b/src/SparkPost.Tests/ClientTests.cs index f190201..6a95605 100644 --- a/src/SparkPost.Tests/ClientTests.cs +++ b/src/SparkPost.Tests/ClientTests.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Net.Http; using Xunit; diff --git a/src/SparkPost.Tests/EventsQueryTests.cs b/src/SparkPost.Tests/EventsQueryTests.cs new file mode 100644 index 0000000..560043c --- /dev/null +++ b/src/SparkPost.Tests/EventsQueryTests.cs @@ -0,0 +1,58 @@ +using Xunit; + +namespace SparkPost.Tests +{ + public class EventsQueryTests + { + public class Defaults + { + [Fact] + public void It_should_have_a_default_build_list() + { + Assert.NotNull(new EventsQuery().BounceClasses); + } + + [Fact] + public void It_should_have_a_default_campaigns_list() + { + Assert.NotNull(new EventsQuery().Campaigns); + } + + [Fact] + public void It_should_have_a_default_from_addresses_list() + { + Assert.NotNull(new EventsQuery().FromAddresses); + } + + [Fact] + public void It_should_have_a_default_messages_list() + { + Assert.NotNull(new EventsQuery().Messages); + } + + [Fact] + public void It_should_have_a_recipients_list() + { + Assert.NotNull(new EventsQuery().Recipients); + } + + [Fact] + public void It_should_have_a_Subaccounts_list() + { + Assert.NotNull(new EventsQuery().Subaccounts); + } + + [Fact] + public void It_should_have_templates_list() + { + Assert.NotNull(new EventsQuery().Templates); + } + + [Fact] + public void It_should_have_transmissions_list() + { + Assert.NotNull(new EventsQuery().Transmissions); + } + } + } +} diff --git a/src/SparkPost.Tests/ListEventsResponseTests.cs b/src/SparkPost.Tests/ListEventsResponseTests.cs new file mode 100644 index 0000000..505a287 --- /dev/null +++ b/src/SparkPost.Tests/ListEventsResponseTests.cs @@ -0,0 +1,24 @@ +using Xunit; + +namespace SparkPost.Tests +{ + public class ListEventsResponseTests + { + public class DefaultTests + { + [Fact] + public void It_should_not_have_nil_links() + { + var response = new ListEventsResponse(); + Assert.NotNull(response.Link); + } + + [Fact] + public void It_should_not_have_nil_events() + { + var response = new ListEventsResponse(); + Assert.NotNull(response.Events); + } + } + } +} diff --git a/src/SparkPost.Tests/ListMessageEventsResponseTests.cs b/src/SparkPost.Tests/ListMessageEventsResponseTests.cs index a7bc654..712c7af 100644 --- a/src/SparkPost.Tests/ListMessageEventsResponseTests.cs +++ b/src/SparkPost.Tests/ListMessageEventsResponseTests.cs @@ -1,7 +1,9 @@ -using Xunit; +using System; +using Xunit; namespace SparkPost.Tests { + [Obsolete] public class ListMessageEventsResponseTests { public class DefaultTests diff --git a/src/SparkPost.Tests/MessageEventsQueryTests.cs b/src/SparkPost.Tests/MessageEventsQueryTests.cs index 9143795..5d33b54 100644 --- a/src/SparkPost.Tests/MessageEventsQueryTests.cs +++ b/src/SparkPost.Tests/MessageEventsQueryTests.cs @@ -1,7 +1,9 @@ -using Xunit; +using System; +using Xunit; namespace SparkPost.Tests { + [Obsolete] public class MessageEventsQueryTests { public class Defaults diff --git a/src/SparkPost.Tests/SparkPost.Tests.csproj b/src/SparkPost.Tests/SparkPost.Tests.csproj index 95dea2a..07fae3b 100644 --- a/src/SparkPost.Tests/SparkPost.Tests.csproj +++ b/src/SparkPost.Tests/SparkPost.Tests.csproj @@ -1,28 +1,19 @@ - netcoreapp2.1 - + net6.0 + SparkPost.Tests Library - - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/src/SparkPost.Tests/SuppressionTests.cs b/src/SparkPost.Tests/SuppressionTests.cs index e7f141c..b548a9d 100644 --- a/src/SparkPost.Tests/SuppressionTests.cs +++ b/src/SparkPost.Tests/SuppressionTests.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Net; using System.Threading.Tasks; @@ -35,14 +35,14 @@ public DeleteTests() email = Guid.NewGuid().ToString(); } - [Test] + [Fact] public async Task It_should_return_true_if_the_web_request_returns_no_content() { var result = await suppressions.Delete(email); Assert.True(result); } - [Test] + [Fact] public async Task It_should_return_false_if_the_web_request_returns_anything_but_no_content() { response.StatusCode = HttpStatusCode.Accepted; @@ -58,7 +58,7 @@ public async Task It_should_return_false_if_the_web_request_returns_anything_but Assert.False(deleted); } - [Test] + [Fact] public async Task It_should_build_the_web_request_parameters_correctly() { var version = Guid.NewGuid().ToString(); @@ -79,7 +79,7 @@ public async Task It_should_build_the_web_request_parameters_correctly() await suppressions.Delete(email); } - [Test] + [Fact] public async Task It_should_encode_the_email_address() { var version = Guid.NewGuid().ToString(); @@ -127,14 +127,14 @@ public CreateOrUpdateTests() suppressions = new Suppressions(client.Object, requestSender.Object, dataMapper.Object); } - [Test] + [Fact] public async Task It_should_return_a_response_when_the_web_request_is_ok() { var result = await suppressions.CreateOrUpdate(suppressionsList); Assert.NotNull(result); } - [Test] + [Fact] public async Task It_should_return_the_reason_phrase() { response.ReasonPhrase = Guid.NewGuid().ToString(); @@ -142,7 +142,7 @@ public async Task It_should_return_the_reason_phrase() Assert.Equal(response.ReasonPhrase, result.ReasonPhrase); } - [Test] + [Fact] public async Task It_should_return_the_content() { response.Content = Guid.NewGuid().ToString(); @@ -150,7 +150,7 @@ public async Task It_should_return_the_content() Assert.Equal(response.Content, result.Content); } - [Test] + [Fact] public async Task It_should_make_a_properly_formed_request() { client.Setup(x => x.Version).Returns(Guid.NewGuid().ToString()); @@ -168,7 +168,7 @@ public async Task It_should_make_a_properly_formed_request() await suppressions.CreateOrUpdate(suppressionsList); } - [Test] + [Fact] public async Task It_should_throw_if_the_http_status_code_is_not_ok() { response.StatusCode = HttpStatusCode.Accepted; diff --git a/src/SparkPost/BounceClass.cs b/src/SparkPost/BounceClass.cs index a570cf7..e14c6d9 100644 --- a/src/SparkPost/BounceClass.cs +++ b/src/SparkPost/BounceClass.cs @@ -1,4 +1,4 @@ -namespace SparkPost +namespace SparkPost { // The list was taken from: // https://support.sparkpost.com/customer/portal/articles/1929896 @@ -66,6 +66,13 @@ public enum BounceClass /// AdminFailure = 25, + /// + /// Smart Send Suppression + /// The message was suppressed by Smart Send policy. + /// Category: Admin. + /// + SmartSendSuppression = 26, + /// /// Generic Bounce: No RCPT /// No recipient could be determined for the message. diff --git a/src/SparkPost/Client.cs b/src/SparkPost/Client.cs index f581003..502f502 100644 --- a/src/SparkPost/Client.cs +++ b/src/SparkPost/Client.cs @@ -30,7 +30,6 @@ public Client(string apiKey, string apiHost, long subAccountId) Suppressions = new Suppressions(this, requestSender, dataMapper); Webhooks = new Webhooks(this, requestSender, dataMapper); Subaccounts = new Subaccounts(this, requestSender, dataMapper); - MessageEvents = new MessageEvents(this, requestSender); InboundDomains = new InboundDomains(this, requestSender, dataMapper); RelayWebhooks = new RelayWebhooks(this, requestSender, dataMapper); RecipientValidations = new RecipientValidation(this, requestSender); @@ -39,6 +38,10 @@ public Client(string apiKey, string apiHost, long subAccountId) Metrics = new Metrics(this, requestSender); Events = new Events(this, requestSender); CustomSettings = new Settings(); + +#pragma warning disable CS0618 // Type or member is obsolete + MessageEvents = new MessageEvents(this, requestSender); +#pragma warning restore CS0618 // Type or member is obsolete } public string ApiKey { get; set; } @@ -50,7 +53,7 @@ public Client(string apiKey, string apiHost, long subAccountId) public ISuppressions Suppressions { get; } public IWebhooks Webhooks { get; } public ISubaccounts Subaccounts { get; } - public IMessageEvents MessageEvents { get; } + public IEvents Events { get; } public IInboundDomains InboundDomains { get; } public IRelayWebhooks RelayWebhooks { get; } public IRecipientLists RecipientLists { get; } @@ -59,7 +62,10 @@ public Client(string apiKey, string apiHost, long subAccountId) public IRecipientValidation RecipientValidations { get; } - public Events Events { get; } +#pragma warning disable CS0618 // Type or member is obsolete + public IMessageEvents MessageEvents { get; } +#pragma warning restore CS0618 // Type or member is obsolete + public string Version => "v1"; public Settings CustomSettings { get; } diff --git a/src/SparkPost/DataMapper.cs b/src/SparkPost/DataMapper.cs index 7037849..ce5cf6d 100644 --- a/src/SparkPost/DataMapper.cs +++ b/src/SparkPost/DataMapper.cs @@ -1,9 +1,8 @@ -using System; +using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Reflection; -using System.Text.RegularExpressions; using SparkPost.Utilities; using SparkPost.ValueMappers; @@ -26,6 +25,7 @@ public interface IDataMapper IDictionary ToDictionary(Suppression suppression); IDictionary ToDictionary(Webhook webhook); IDictionary ToDictionary(Subaccount subaccount); + IDictionary ToDictionary(EventsQuery eventsQuery); IDictionary ToDictionary(RelayWebhook relayWebhook); IDictionary ToDictionary(InboundDomain inboundDomain); IDictionary ToDictionary(RelayWebhookMatch relayWebhookMatch); @@ -177,6 +177,27 @@ public IDictionary ToDictionary(RelayWebhookMatch relayWebhookMa return WithCommonConventions(relayWebhookMatch); } + public IDictionary ToDictionary(EventsQuery query) + { + return WithCommonConventions( + query, + new Dictionary() + { + ["events"] = string.Join(",", query.Events), + ["campaigns"] = string.Join(",", query.Campaigns), + ["bounce_classes"] = string.Join(",", query.BounceClasses), + ["campaigns"] = string.Join(",", query.Campaigns), + ["from_addresses"] = string.Join(",", query.FromAddresses), + ["messages"] = string.Join(",", query.Messages), + ["recipients"] = string.Join(",", query.Recipients), + ["subaccounts"] = string.Join(",", query.Subaccounts), + ["templates"] = string.Join(",", query.Templates), + ["transmissions"] = string.Join(",", query.Transmissions) + } + ); + } + + [Obsolete("Deprecated in 2019")] public IDictionary ToDictionary(MessageEventsQuery query) { return WithCommonConventions( diff --git a/src/SparkPost/Event.cs b/src/SparkPost/Event.cs new file mode 100644 index 0000000..08898b7 --- /dev/null +++ b/src/SparkPost/Event.cs @@ -0,0 +1,325 @@ +using SparkPost.Utilities; +using System.Collections.Generic; +using System.ComponentModel; +using System; + +namespace SparkPost +{ + public class Event + { + /// + /// "type": { + /// "description": "Type of event this record describes", + /// "sampleValue": "bounce" + /// } + /// "type": "out_of_band", + /// + public string Type { get; set; } + + /// + /// Type of event this record describes + /// + public EventType TypeEnum + { + get + { + foreach (var typeName in Enum.GetNames(typeof(EventType))) + { + var typeNameSnakeCase = SnakeCase.Convert(typeName); + if (string.Equals(Type, typeNameSnakeCase, StringComparison.InvariantCultureIgnoreCase)) + // check for an unmapped message event type here + return (EventType)Enum.Parse(typeof(EventType), typeName); + } + return EventType.Undefined; + } + } + + /// + /// "bounce_class": { + /// "description": "Classification code for a given message (see [Bounce Classification Codes](https://support.sparkpost.com/customer/portal/articles/1929896))", + /// "sampleValue": "1" + /// }, + /// "bounce_class": "10", + /// + public string BounceClass { get; set; } + + /// + /// Classification code for a given message (see [Bounce Classification Codes](https://support.sparkpost.com/customer/portal/articles/1929896)) + /// + public BounceClass BounceClassEnum + { + get + { + int bounceClassAsInt; + if (!int.TryParse(BounceClass, out bounceClassAsInt)) + return SparkPost.BounceClass.Undefined; + // note: these scare me, perhaps we should check that it is valid? + var bounceClass = (BounceClass)bounceClassAsInt; + return bounceClass.ToString() == bounceClassAsInt.ToString() ? SparkPost.BounceClass.Undefined : bounceClass; + } + } + + /// + /// Classification code for a given message (see [Bounce Classification Codes](https://support.sparkpost.com/customer/portal/articles/1929896)) + /// + public BounceClassDetails BounceClassDetails => BounceClassesDetails.AllBounceClasses[BounceClassEnum]; + + /// + /// "campaign_id": { + /// "description": "Campaign of which this message was a part", + /// "sampleValue": "Example Campaign Name" + /// }, + /// "campaign_id": "My campaign name", + /// + public string CampaignId { get; set; } + + /// + /// "customer_id": { + /// "description": "SparkPost-customer identifier through which this message was sent", + /// "sampleValue": "1" + /// }, + /// "customer_id": "12345", + /// + public string CustomerId { get; set; } + + /// + /// "delv_method": { + /// "description": "Protocol by which SparkPost delivered this message", + /// "sampleValue": "esmtp" + /// }, + /// "delv_method": "esmtp", + /// + public string DeliveryMethod { get; set; } + + /// + /// "device_token": { + /// "description": "Token of the device / application targeted by this PUSH notification message. Applies only when delv_method is gcm or apn.", + /// "sampleValue": "45c19189783f867973f6e6a5cca60061ffe4fa77c547150563a1192fa9847f8a" + /// }, + /// + public string DeviceToken { get; set; } + + /// + /// "error_code": { + /// "description": "Error code by which the remote server described a failed delivery attempt", + /// "sampleValue": "554" + /// }, + /// "error_code": "550", + /// + public string ErrorCode { get; set; } + + /// + /// "ip_address": { + /// "description": "IP address of the host to which SparkPost delivered this message; in engagement events, the IP address of the host where the HTTP request originated", + /// "sampleValue": "127.0.0.1" + /// }, + /// + public string IpAddress { get; set; } + + /// + /// "message_id": { + /// "description": "SparkPost-cluster-wide unique identifier for this message", + /// "sampleValue": "0e0d94b7-9085-4e3c-ab30-e3f2cd9c273e" + /// }, + /// "message_id": "00021f9a27476a273c57", + /// + public string MessageId { get; set; } + + /// + /// "msg_from": { + /// "description": "Sender address used on this message's SMTP envelope", + /// "sampleValue": "sender@example.com" + /// }, + /// "msg_from": "msprvs1=17827RA6TC8Pz=bounces-12345@sparkpostmail1.com", + /// + public string MessageFrom { get; set; } + + /// + /// "msg_size": { + /// "description": "Message's size in bytes", + /// "sampleValue": "1337" + /// }, + /// "msg_size": "3168", + /// + public string MessageSize { get; set; } + + /// + /// "num_retries": { + /// "description": "Number of failed attempts before this message was successfully delivered; when the first attempt succeeds, zero", + /// "sampleValue": "2" + /// }, + /// "num_retries": "0", + /// + public string NumberOfRetries { get; set; } + + /// + /// "rcpt_meta": { + /// "description": "Metadata describing the message recipient", + /// "sampleValue": { + /// "customKey": "customValue" + /// } + /// }, + ///"rcpt_meta": { + /// "CustomKey1": "Custom Value 1", + /// "CustomKey2": "Custom Value 2" + ///}, + /// + public Dictionary Metadata { get; set; } + + /// + /// "rcpt_tags": { + /// "description": "Tags applied to the message which generated this event", + /// "sampleValue": [ + /// "male", + /// "US" + /// ] + /// }, + /// "rcpt_tags": [ "CustomTag1" ], + /// + public List Tags { get; set; } + + /// + /// "rcpt_to": { + /// "description": "Recipient address used on this message's SMTP envelope", + /// "sampleValue": "recipient@example.com" + /// }, + /// "rcpt_to": "to@domain.com", + /// + public string RecipientTo { get; set; } + + /// + /// "rcpt_type": { + /// "description": "Indicates that a recipient address appeared in the Cc or Bcc header or the archive JSON array", + /// "sampleValue": "cc" + /// }, + /// + public string RecipientType { get; set; } + + /// + /// "raw_reason": { + /// "description": "Unmodified, exact response returned by the remote server due to a failed delivery attempt", + /// "sampleValue": "MAIL REFUSED - IP (17.99.99.99) is in black list" + /// }, + /// "raw_reason": "550 [internal] [oob] The recipient is invalid.", + /// + public string RawReason { get; set; } + + /// + /// "reason": { + /// "description": "Canonicalized text of the response returned by the remote server due to a failed delivery attempt", + /// "sampleValue": "MAIL REFUSED - IP (a.b.c.d) is in black list" + /// }, + /// "reason": "550 [internal] [oob] The recipient is invalid.", + /// + public string Reason { get; set; } + + /// + /// "routing_domain": { + /// "description": "Domain receiving this message", + /// "sampleValue": "example.com" + /// }, + /// "routing_domain": "domain.com", + /// + public string RoutingDomain { get; set; } + + /// + /// "subject": { + /// "description": "Subject line from the email header", + /// "sampleValue": "Summer deals are here!" + /// }, + /// "subject": "My email subject", + /// + public string Subject { get; set; } + + /// + /// "template_id": { + /// "description": "Slug of the template used to construct this message", + /// "sampleValue": "templ-1234" + /// }, + /// "template_id": "smtp_47287131967131576", + /// + public string TemplateId { get; set; } + + /// + /// "template_version": { + /// "description": "Version of the template used to construct this message", + /// "sampleValue": "1" + /// }, + /// "template_version": "0", + /// + public string TemplateVersion { get; set; } + + /// + /// "timestamp": { + /// "description": "Event date and time, in Unix timestamp format (integer seconds since 00:00:00 GMT 1970-01-01)", + /// "sampleValue": 1427736822 + /// }, + /// "timestamp": "2016-04-27T10:54:25.000+00:00" + /// + public DateTimeOffset Timestamp { get; set; } + + /// + /// "transmission_id": { + /// "description": "Transmission which originated this message", + /// "sampleValue": "64836157861974168" + /// } + /// "transmission_id": "47287131967131576", + /// + public string TransmissionId { get; set; } + + /// + /// Not documented. + /// "event_id": "84320004715329757", + /// + public string EventId { get; set; } + + /// + /// Not documented. + /// "friendly_from": "from@domain.com", + /// + public string FriendlyFrom { get; set; } + + /// + /// Not documented. + /// "ip_pool": "shared", + /// + public string IpPool { get; set; } + + /// + /// Not documented. + /// "queue_time": "3004", + /// + public string QueueTime { get; set; } + + /// + /// Not documented. + /// "raw_rcpt_to": "to@domain.com", + /// + public string RawRecipientTo { get; set; } + + /// + /// Not documented. + /// "sending_ip": "shared", + /// + public string SendingIp { get; set; } + + /// + /// Not documented. + /// "transactional": "1", + /// + public string Transactional { get; set; } + + /// + /// Not documented. + /// "remote_addr": "A.B.C.D:25512", + /// + public string RemoteAddress { get; set; } + + public string TargetLinkUrl { get; set; } + + public override string ToString() + { + return $"{TypeEnum} from {FriendlyFrom} to {RecipientTo}"; + } + } +} diff --git a/src/SparkPost/EventPageLink.cs b/src/SparkPost/EventPageLink.cs new file mode 100644 index 0000000..2553fd3 --- /dev/null +++ b/src/SparkPost/EventPageLink.cs @@ -0,0 +1,7 @@ +namespace SparkPost +{ + public class EventPageLink + { + public string Next { get; set; } + } +} diff --git a/src/SparkPost/EventSampleResponse.cs b/src/SparkPost/EventSampleResponse.cs new file mode 100644 index 0000000..17ae4df --- /dev/null +++ b/src/SparkPost/EventSampleResponse.cs @@ -0,0 +1,4 @@ +namespace SparkPost +{ + public class EventSampleResponse : Response { } +} diff --git a/src/SparkPost/EventType.cs b/src/SparkPost/EventType.cs new file mode 100644 index 0000000..bc987fc --- /dev/null +++ b/src/SparkPost/EventType.cs @@ -0,0 +1,176 @@ +namespace SparkPost +{ + // Values taken from: + // https://developers.sparkpost.com/api/events/ + // Additional values and descriptions taken from: + // https://support.sparkpost.com/customer/portal/articles/1976204-webhook-event-reference + + public enum EventType + { + /// + /// Undefined/unknown or unable to parse. + /// + Undefined, + + /// + /// delivery + /// Delivery. + /// Remote MTA acknowledged receipt of a message. + /// + Delivery, + + /// + /// injection + /// Injection. + /// Message is received by or injected into SparkPost. + /// + Injection, + + /// + /// bounce + /// Bounce. + /// Remote MTA has permanently rejected a message. + /// + Bounce, + + /// + /// delay + /// Delay. + /// Remote MTA has temporarily rejected a message. + /// + Delay, + + /// + /// policy_rejection + /// Policy Rejection. + /// Due to policy, SparkPost rejected a message or failed to generate a message. + /// + PolicyRejection, + + /// + /// out_of_band + /// Out of Band. + /// Remote MTA initially reported acceptance of a message, but it has since asynchronously reported that the message was not delivered. + /// + OutOfBand, + + /// + /// open + /// Open. + /// Recipient opened a message in a mail client, thus rendering a tracking pixel. + /// + Open, + + /// + /// initial_open + /// Initial Open. + /// Recipient opened a message in a mail client, thus rendering a tracking pixel at the top of the message. + /// + InitialOpen, + + /// + /// amp_click + /// AMP Click. + /// Recipient clicked a tracked link in an AMP message, thus prompting a redirect through the SparkPost click-tracking server to the link's destination. + /// + AmpClick, + + /// + /// amp_open + /// AMP Open. + /// Recipient opened an AMP message in a mail client, thus rendering a tracking pixel at the bottom of the message. + /// + AmpOpen, + + /// + /// amp_initial_open + /// AMP Initial Open. + /// Recipient opened an AMP message in a mail client, thus rendering a tracking pixel at the top of the message. + /// + AmpInitialOpen, + + /// + /// click + /// Click. + /// Recipient clicked a tracked link in a message, thus prompting a redirect through the SparkPost click-tracking server to the link's destination. + /// + Click, + + /// + /// generation_failure + /// Generation Failure. + /// Message generation failed for an intended recipient. + /// + GenerationFailure, + + /// + /// generation_rejection + /// Generation Rejection. + /// SparkPost rejected message generation due to policy. + /// + GenerationRejection, + + /// + /// spam_complaint + /// Spam Complaint. + /// Message was classified as spam by the recipient. + /// + SpamComplaint, + + /// + /// list_unsubscribe + /// List Unsubscribe. + /// User clicked the 'unsubscribe' button on an email client. + /// + ListUnsubscribe, + + /// + /// link_unsubscribe + /// Link Unsubscribe. + /// User clicked a hyperlink in a received email. + /// + LinkUnsubscribe, + + /// + /// sms_status + /// SMS Status. + /// SMPP/SMS message produced a status log output. + /// + SmsStatus, + + /// + /// relay_injection + /// Relay Injection. + /// Relayed message is received by or injected into SparkPost. + /// + RelayInjection, + + /// + /// relay_rejection + /// Relay Rejection. + /// SparkPost rejected a relayed message or failed to generate a relayed message. + /// + RelayRejection, + + /// + /// relay_delivery + /// Relay Delivery. + /// Remote HTTP Endpoint acknowledged receipt of a relayed message. + /// + RelayDelivery, + + /// + /// relay_tempfail + /// Relay Temporary Failure. + /// Remote HTTP Endpoint has failed to accept a relayed message. + /// + RelayTempfail, + + /// + /// relay_permfail + /// Relay Permanent Failure. + /// Relayed message has reached the maximum retry threshold and will be removed from the system. + /// + RelayPermfail + } +} diff --git a/src/SparkPost/Events.cs b/src/SparkPost/Events.cs index 8c4ccf1..d602d76 100644 --- a/src/SparkPost/Events.cs +++ b/src/SparkPost/Events.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Net; using System.Threading.Tasks; using Newtonsoft.Json; @@ -8,7 +9,7 @@ namespace SparkPost { - public class Events + public class Events : IEvents { private readonly Client client; private readonly IRequestSender requestSender; @@ -19,6 +20,129 @@ public Events(Client client, IRequestSender requestSender) this.requestSender = requestSender; } + public async Task List() + { + return await List((EventsQuery)null); + } + + public async Task List(EventsQuery eventsQuery) + { + return await this.List($"/api/{client.Version}/events/message", eventsQuery); + } + + public async Task List(string url) + { + return await this.List(url, null); + } + + public async Task List(string url, EventsQuery eventsQuery) + { + var request = new Request + { + Url = url, + Method = "GET", + Data = (object)eventsQuery ?? new { } + }; + + var response = await requestSender.Send(request); + if (response.StatusCode != HttpStatusCode.OK) + throw new ResponseException(response); + + dynamic content = Jsonification.DeserializeObject(response.Content); + + var listMessageEventsResponse = new ListEventsResponse + { + ReasonPhrase = response.ReasonPhrase, + StatusCode = response.StatusCode, + Content = response.Content, + Events = ConvertResultsToAListOfEvents(content.results), + TotalCount = content.total_count, + Link = ConvertToLinks(content.links) + }; + + return listMessageEventsResponse; + } + + public async Task SamplesOf(string events) + { + var request = new Request { Url = $"/api/{client.Version}/events/message/samples?events={events}", Method = "GET" }; + + var response = await requestSender.Send(request); + if (response.StatusCode != HttpStatusCode.OK) + throw new ResponseException(response); + + return new EventSampleResponse + { + ReasonPhrase = response.ReasonPhrase, + StatusCode = response.StatusCode, + Content = response.Content, + }; + } + + private static EventPageLink ConvertToLinks(dynamic page_links) + { + var links = new EventPageLink(); + + if (page_links != null) + links.Next = page_links.next; + + return links; + } + + private static IEnumerable ConvertResultsToAListOfEvents(dynamic results) + { + var events = new List(); + + if (results == null) + return events; + + foreach (var result in results) + { + var metadata = Jsonification.DeserializeObject>(Jsonification.SerializeObject(result.rcpt_meta)); + var tags = Jsonification.DeserializeObject>(Jsonification.SerializeObject(result.rcpt_tags)); + + events.Add( + new Event + { + Type = result.type, + BounceClass = result.bounce_class, + CampaignId = result.campaign_id, + CustomerId = result.customer_id, + DeliveryMethod = result.delv_method, + DeviceToken = result.device_token, + ErrorCode = result.error_code, + IpAddress = result.ip_address, + MessageId = result.message_id, + MessageFrom = result.msg_from, + MessageSize = result.msg_size, + NumberOfRetries = result.num_retries, + RecipientTo = result.rcpt_to, + RecipientType = result.rcpt_type, + RawReason = result.raw_reason, + Reason = result.reason, + RoutingDomain = result.routing_domain, + Subject = result.subject, + TemplateId = result.template_id, + TemplateVersion = result.template_version, + Timestamp = result.timestamp, + TransmissionId = result.transmission_id, + EventId = result.event_id, + FriendlyFrom = result.friendly_from, + IpPool = result.ip_pool, + QueueTime = result.queue_time, + RawRecipientTo = result.raw_rcpt_to, + SendingIp = result.sending_ip, + Transactional = result.transactional, + RemoteAddress = result.remote_addr, + Metadata = metadata, + TargetLinkUrl = result.target_link_url, + Tags = tags + } + ); + } + return events; + } + public async Task Get(string cursor = "initial", int perPage = 1000) { var request = new Request { Url = $"api/{client.Version}/events/message?perPage={perPage}&cursor={cursor}", Method = "GET" }; diff --git a/src/SparkPost/EventsQuery.cs b/src/SparkPost/EventsQuery.cs new file mode 100644 index 0000000..cd55678 --- /dev/null +++ b/src/SparkPost/EventsQuery.cs @@ -0,0 +1,116 @@ +using System; +using System.Collections.Generic; + +namespace SparkPost +{ + public class EventsQuery + { + public EventsQuery() + { + this.Events = new List(); + this.BounceClasses = new List(); + this.Campaigns = new List(); + this.FromAddresses = new List(); + this.Messages = new List(); + this.Recipients = new List(); + this.Subaccounts = new List(); + this.Templates = new List(); + this.Transmissions = new List(); + } + + /// + /// bounce_classes : Number : Comma-delimited list of bounce classification codes to search. + /// See Bounce Classification Codes at https://support.sparkpost.com/customer/portal/articles/1929896. + /// Example: 1,10,20. + /// + public IList BounceClasses { get; set; } + + /// + /// campaigns : ? : (optional, string, `Example Campaign Name`) ... Comma-delimited list of campaign ID's to search (i.e. campaign_id used during creation of a transmission). + /// + public IList Campaigns { get; set; } + + /// + /// events : List : Comma-delimited list of event types to search. Defaults to all event types. + /// Example: delivery, injection, bounce, delay, policy_rejection, out_of_band, open, click, generation_failure, generation_rejection, spam_complaint, list_unsubscribe, link_unsubscribe. + /// + public IList Events { get; set; } + + /// + /// from_addresses : ? : (optional, list, `sender@mail.example.com`) ... Comma-delimited list of friendly_froms to search. + /// + public IList FromAddresses { get; set; } + + /// + /// from : Datetime : Datetime in format of YYYY-MM-DDTHH:MM. + /// Example: 2014-07-20T08:00. + /// Default: One hour ago. + /// + public DateTime? From { get; set; } + + /// + /// messages : List : Comma-delimited list of message ID's to search. + /// Example: 0e0d94b7-9085-4e3c-ab30-e3f2cd9c273e. + /// + public IList Messages { get; set; } + + /// + /// cursor : String : Results cursor for pagination. Used in conjunction with per_page parameter. See Pagination section for details. + /// Example: WycyMDE4LTExLTA1VDIyOjQ1OjM5LjAwMFonLCAnc3BjLTM4MTQ1MjY3MjMyNTA2NTEwJ10=. + /// Default: initial. + /// + public string Cursor { get; set; } + + /// + /// per_page : Number : Maximum number of results to return per page. Must be between 1 and 10,000. + /// Example: 5000. + /// Default: 1000. + /// Note: Pagination requests count towards the number of requests allowed by rate limiting, the same as non-paginated requests. + /// + public int? PerPage { get; set; } + + /// + /// reason : String :Bounce/failure/rejection reason that will be matched using a wildcard(e.g., %reason%). + /// Example: bounce. + /// + public string Reason { get; set; } + + /// + /// recipients : List : Comma-delimited list of recipients to search. + /// Example: recipient @example.com. + /// + public IList Recipients { get; set; } + + /// + /// subaccounts : List : Comma-delimited list of subaccount ID's to search. + /// Example: 101. + /// + public IList Subaccounts { get; set; } + + /// + /// templates : List : Comma-delimited list of template ID's to search. + /// Example: templ-1234. + /// + public IList Templates { get; set; } + + /// + /// timezone : String : Standard timezone identification string. + /// Example: America/New_York. + /// Default: UTC. + /// + public string Timezone { get; set; } + + /// + /// to : Datetime : Datetime in format of YYYY-MM-DDTHH:MM. + /// Example: 2014-07-20T09:00. + /// Default: now. + /// + public DateTime? To { get; set; } + + /// + /// transmission_ids : List : Comma-delimited list of transmission ID's to search (i.e. id generated during creation of a transmission). + /// Example: 65832150921904138. + /// + public IList Transmissions { get; set; } + } +} diff --git a/src/SparkPost/IClient.cs b/src/SparkPost/IClient.cs index 1adc256..c9250cd 100644 --- a/src/SparkPost/IClient.cs +++ b/src/SparkPost/IClient.cs @@ -1,3 +1,5 @@ +using System; + namespace SparkPost { /// @@ -38,8 +40,14 @@ public interface IClient /// /// Gets access to the message events resource of the SparkPost API. /// + [Obsolete("Deprecated since 2019")] IMessageEvents MessageEvents { get; } + /// + /// Gets access to the events resource of the SparkPost API. + /// + IEvents Events { get; } + /// /// Gets access to the inbound domains resource of the SparkPost API. /// @@ -86,7 +94,5 @@ public interface IClient /// Gets the sub account. /// long SubaccountId { get; } - - Events Events { get; } } } diff --git a/src/SparkPost/IEvents.cs b/src/SparkPost/IEvents.cs new file mode 100644 index 0000000..b1a2b40 --- /dev/null +++ b/src/SparkPost/IEvents.cs @@ -0,0 +1,16 @@ +using System; +using System.Threading.Tasks; + +namespace SparkPost +{ + public interface IEvents + { + Task List(); + Task List(EventsQuery query); + Task List(string url); + Task SamplesOf(string events); + Task Get(string cursor = "initial", int perPage = 1000); + Task GetEventsSince(DateTime fromTime, int perPage = 1000); + Task GetEventsNext(string nextUri); + } +} diff --git a/src/SparkPost/IMessageEvents.cs b/src/SparkPost/IMessageEvents.cs index a82b3e6..b650b76 100644 --- a/src/SparkPost/IMessageEvents.cs +++ b/src/SparkPost/IMessageEvents.cs @@ -1,7 +1,9 @@ +using System; using System.Threading.Tasks; namespace SparkPost { + [Obsolete("Deprecated in 2019")] public interface IMessageEvents { Task List(); diff --git a/src/SparkPost/ListEventsResponse.cs b/src/SparkPost/ListEventsResponse.cs new file mode 100644 index 0000000..8411af2 --- /dev/null +++ b/src/SparkPost/ListEventsResponse.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; + +namespace SparkPost +{ + public class ListEventsResponse : Response + { + public ListEventsResponse() + { + Events = new Event[] { }; + Link = new EventPageLink(); + } + + public IEnumerable Events { get; set; } + + public EventPageLink Link { get; set; } + + public int TotalCount { get; set; } + } +} diff --git a/src/SparkPost/ListMessageEventsResponse.cs b/src/SparkPost/ListMessageEventsResponse.cs index 2456590..53c2d36 100644 --- a/src/SparkPost/ListMessageEventsResponse.cs +++ b/src/SparkPost/ListMessageEventsResponse.cs @@ -1,7 +1,9 @@ +using System; using System.Collections.Generic; namespace SparkPost { + [Obsolete("Deprecated in 2019")] public class ListMessageEventsResponse : Response { public ListMessageEventsResponse() diff --git a/src/SparkPost/MessageEvent.cs b/src/SparkPost/MessageEvent.cs index 7042153..76a542b 100644 --- a/src/SparkPost/MessageEvent.cs +++ b/src/SparkPost/MessageEvent.cs @@ -1,10 +1,11 @@ -using SparkPost.Utilities; +using SparkPost.Utilities; using System; using System.Collections.Generic; using System.ComponentModel; namespace SparkPost { + [Obsolete("Deprecated in 2019")] public class MessageEvent { /// diff --git a/src/SparkPost/MessageEventSampleResponse.cs b/src/SparkPost/MessageEventSampleResponse.cs index 071c287..557a6a6 100644 --- a/src/SparkPost/MessageEventSampleResponse.cs +++ b/src/SparkPost/MessageEventSampleResponse.cs @@ -1,4 +1,7 @@ -namespace SparkPost +using System; + +namespace SparkPost { + [Obsolete("Deprecated in 2019")] public class MessageEventSampleResponse : Response { } } diff --git a/src/SparkPost/MessageEvents.cs b/src/SparkPost/MessageEvents.cs index 33b9317..ce79185 100644 --- a/src/SparkPost/MessageEvents.cs +++ b/src/SparkPost/MessageEvents.cs @@ -3,9 +3,11 @@ using System.Net; using System.Threading.Tasks; using SparkPost.Utilities; +using System; namespace SparkPost { + [Obsolete("Deprecated in 2019")] public class MessageEvents : IMessageEvents { private readonly IClient client; diff --git a/src/SparkPost/MessageEventsQuery.cs b/src/SparkPost/MessageEventsQuery.cs index 97ec016..c7a0d60 100644 --- a/src/SparkPost/MessageEventsQuery.cs +++ b/src/SparkPost/MessageEventsQuery.cs @@ -5,6 +5,7 @@ namespace SparkPost { + [Obsolete("Deprecated in 2019")] public class MessageEventsQuery { public MessageEventsQuery() diff --git a/src/SparkPost/PageLink.cs b/src/SparkPost/PageLink.cs index 11e02e4..f4f0581 100644 --- a/src/SparkPost/PageLink.cs +++ b/src/SparkPost/PageLink.cs @@ -1,5 +1,8 @@ -namespace SparkPost +using System; + +namespace SparkPost { + [Obsolete("Deprecated in 2019")] public class PageLink { public string Href { get; set; } diff --git a/src/SparkPost/SparkPost.csproj b/src/SparkPost/SparkPost.csproj index 220f44c..4805490 100644 --- a/src/SparkPost/SparkPost.csproj +++ b/src/SparkPost/SparkPost.csproj @@ -5,7 +5,7 @@ Library - 1.14.0.5 + 2.0.2.1 true SparkPost.NetCore Lean Sparkpost API client port to .net standard 2.0 diff --git a/src/SparkPost/Templates.cs b/src/SparkPost/Templates.cs index 2e2d4bf..a1ca63a 100644 --- a/src/SparkPost/Templates.cs +++ b/src/SparkPost/Templates.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Threading.Tasks; using System.Net; using System.Reflection; @@ -140,10 +140,7 @@ public async Task Delete(string templateId) public async Task Preview(string templateId, IDictionary substitutionData, bool? draft = null) { - var transmission = new Transmission - { - SubstitutionData = substitutionData - }; + var transmission = new Transmission { SubstitutionData = substitutionData }; var url = $"api/{client.Version}/templates/{templateId}/preview"; if (draft.HasValue) @@ -155,11 +152,12 @@ public async Task Preview(string templateId, IDictionary(response.Content).results; @@ -174,11 +172,7 @@ public async Task Preview(string templateId, IDictionary _map = - new Dictionary(StringComparer.OrdinalIgnoreCase) - { - {".323", "text/h323"}, - {".3g2", "video/3gpp2"}, - {".3gp2", "video/3gpp2"}, - {".3gp", "video/3gpp"}, - {".3gpp", "video/3gpp"}, - {".aac", "audio/aac"}, - {".aaf", "application/octet-stream"}, - {".aca", "application/octet-stream"}, - {".accdb", "application/msaccess"}, - {".accde", "application/msaccess"}, - {".accdt", "application/msaccess"}, - {".acx", "application/internet-property-stream"}, - {".adt", "audio/vnd.dlna.adts"}, - {".adts", "audio/vnd.dlna.adts"}, - {".afm", "application/octet-stream"}, - {".ai", "application/postscript"}, - {".aif", "audio/x-aiff"}, - {".aifc", "audio/aiff"}, - {".aiff", "audio/aiff"}, - {".appcache", "text/cache-manifest"}, - {".application", "application/x-ms-application"}, - {".art", "image/x-jg"}, - {".asd", "application/octet-stream"}, - {".asf", "video/x-ms-asf"}, - {".asi", "application/octet-stream"}, - {".asm", "text/plain"}, - {".asr", "video/x-ms-asf"}, - {".asx", "video/x-ms-asf"}, - {".atom", "application/atom+xml"}, - {".au", "audio/basic"}, - {".avi", "video/x-msvideo"}, - {".axs", "application/olescript"}, - {".bas", "text/plain"}, - {".bcpio", "application/x-bcpio"}, - {".bin", "application/octet-stream"}, - {".bmp", "image/bmp"}, - {".c", "text/plain"}, - {".cab", "application/vnd.ms-cab-compressed"}, - {".calx", "application/vnd.ms-office.calx"}, - {".cat", "application/vnd.ms-pki.seccat"}, - {".cdf", "application/x-cdf"}, - {".chm", "application/octet-stream"}, - {".class", "application/x-java-applet"}, - {".clp", "application/x-msclip"}, - {".cmx", "image/x-cmx"}, - {".cnf", "text/plain"}, - {".cod", "image/cis-cod"}, - {".cpio", "application/x-cpio"}, - {".cpp", "text/plain"}, - {".crd", "application/x-mscardfile"}, - {".crl", "application/pkix-crl"}, - {".crt", "application/x-x509-ca-cert"}, - {".csh", "application/x-csh"}, - {".css", "text/css"}, - {".csv", "application/octet-stream"}, - {".cur", "application/octet-stream"}, - {".dcr", "application/x-director"}, - {".deploy", "application/octet-stream"}, - {".der", "application/x-x509-ca-cert"}, - {".dib", "image/bmp"}, - {".dir", "application/x-director"}, - {".disco", "text/xml"}, - {".dlm", "text/dlm"}, - {".doc", "application/msword"}, - {".docm", "application/vnd.ms-word.document.macroEnabled.12"}, - {".docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document"}, - {".dot", "application/msword"}, - {".dotm", "application/vnd.ms-word.template.macroEnabled.12"}, - {".dotx", "application/vnd.openxmlformats-officedocument.wordprocessingml.template"}, - {".dsp", "application/octet-stream"}, - {".dtd", "text/xml"}, - {".dvi", "application/x-dvi"}, - {".dvr-ms", "video/x-ms-dvr"}, - {".dwf", "drawing/x-dwf"}, - {".dwp", "application/octet-stream"}, - {".dxr", "application/x-director"}, - {".eml", "message/rfc822"}, - {".emz", "application/octet-stream"}, - {".eot", "application/vnd.ms-fontobject"}, - {".eps", "application/postscript"}, - {".etx", "text/x-setext"}, - {".evy", "application/envoy"}, - {".fdf", "application/vnd.fdf"}, - {".fif", "application/fractals"}, - {".fla", "application/octet-stream"}, - {".flr", "x-world/x-vrml"}, - {".flv", "video/x-flv"}, - {".gif", "image/gif"}, - {".gtar", "application/x-gtar"}, - {".gz", "application/x-gzip"}, - {".h", "text/plain"}, - {".hdf", "application/x-hdf"}, - {".hdml", "text/x-hdml"}, - {".hhc", "application/x-oleobject"}, - {".hhk", "application/octet-stream"}, - {".hhp", "application/octet-stream"}, - {".hlp", "application/winhlp"}, - {".hqx", "application/mac-binhex40"}, - {".hta", "application/hta"}, - {".htc", "text/x-component"}, - {".htm", "text/html"}, - {".html", "text/html"}, - {".htt", "text/webviewhtml"}, - {".hxt", "text/html"}, - {".ical", "text/calendar"}, - {".icalendar", "text/calendar"}, - {".ico", "image/x-icon"}, - {".ics", "text/calendar"}, - {".ief", "image/ief"}, - {".ifb", "text/calendar"}, - {".iii", "application/x-iphone"}, - {".inf", "application/octet-stream"}, - {".ins", "application/x-internet-signup"}, - {".isp", "application/x-internet-signup"}, - {".IVF", "video/x-ivf"}, - {".jar", "application/java-archive"}, - {".java", "application/octet-stream"}, - {".jck", "application/liquidmotion"}, - {".jcz", "application/liquidmotion"}, - {".jfif", "image/pjpeg"}, - {".jpb", "application/octet-stream"}, - {".jpe", "image/jpeg"}, - {".jpeg", "image/jpeg"}, - {".jpg", "image/jpeg"}, - {".js", "application/javascript"}, - {".json", "application/json"}, - {".jsx", "text/jscript"}, - {".latex", "application/x-latex"}, - {".lit", "application/x-ms-reader"}, - {".lpk", "application/octet-stream"}, - {".lsf", "video/x-la-asf"}, - {".lsx", "video/x-la-asf"}, - {".lzh", "application/octet-stream"}, - {".m13", "application/x-msmediaview"}, - {".m14", "application/x-msmediaview"}, - {".m1v", "video/mpeg"}, - {".m2ts", "video/vnd.dlna.mpeg-tts"}, - {".m3u", "audio/x-mpegurl"}, - {".m4a", "audio/mp4"}, - {".m4v", "video/mp4"}, - {".man", "application/x-troff-man"}, - {".manifest", "application/x-ms-manifest"}, - {".map", "text/plain"}, - {".markdown", "text/markdown"}, - {".md", "text/markdown"}, - {".mdb", "application/x-msaccess"}, - {".mdp", "application/octet-stream"}, - {".me", "application/x-troff-me"}, - {".mht", "message/rfc822"}, - {".mhtml", "message/rfc822"}, - {".mid", "audio/mid"}, - {".midi", "audio/mid"}, - {".mix", "application/octet-stream"}, - {".mmf", "application/x-smaf"}, - {".mno", "text/xml"}, - {".mny", "application/x-msmoney"}, - {".mov", "video/quicktime"}, - {".movie", "video/x-sgi-movie"}, - {".mp2", "video/mpeg"}, - {".mp3", "audio/mpeg"}, - {".mp4", "video/mp4"}, - {".mp4v", "video/mp4"}, - {".mpa", "video/mpeg"}, - {".mpe", "video/mpeg"}, - {".mpeg", "video/mpeg"}, - {".mpg", "video/mpeg"}, - {".mpp", "application/vnd.ms-project"}, - {".mpv2", "video/mpeg"}, - {".ms", "application/x-troff-ms"}, - {".msi", "application/octet-stream"}, - {".mso", "application/octet-stream"}, - {".mvb", "application/x-msmediaview"}, - {".mvc", "application/x-miva-compiled"}, - {".nc", "application/x-netcdf"}, - {".nsc", "video/x-ms-asf"}, - {".nws", "message/rfc822"}, - {".ocx", "application/octet-stream"}, - {".oda", "application/oda"}, - {".odc", "text/x-ms-odc"}, - {".ods", "application/oleobject"}, - {".oga", "audio/ogg"}, - {".ogg", "video/ogg"}, - {".ogv", "video/ogg"}, - {".ogx", "application/ogg"}, - {".one", "application/onenote"}, - {".onea", "application/onenote"}, - {".onetoc", "application/onenote"}, - {".onetoc2", "application/onenote"}, - {".onetmp", "application/onenote"}, - {".onepkg", "application/onenote"}, - {".osdx", "application/opensearchdescription+xml"}, - {".otf", "font/otf"}, - {".p10", "application/pkcs10"}, - {".p12", "application/x-pkcs12"}, - {".p7b", "application/x-pkcs7-certificates"}, - {".p7c", "application/pkcs7-mime"}, - {".p7m", "application/pkcs7-mime"}, - {".p7r", "application/x-pkcs7-certreqresp"}, - {".p7s", "application/pkcs7-signature"}, - {".pbm", "image/x-portable-bitmap"}, - {".pcx", "application/octet-stream"}, - {".pcz", "application/octet-stream"}, - {".pdf", "application/pdf"}, - {".pfb", "application/octet-stream"}, - {".pfm", "application/octet-stream"}, - {".pfx", "application/x-pkcs12"}, - {".pgm", "image/x-portable-graymap"}, - {".pko", "application/vnd.ms-pki.pko"}, - {".pma", "application/x-perfmon"}, - {".pmc", "application/x-perfmon"}, - {".pml", "application/x-perfmon"}, - {".pmr", "application/x-perfmon"}, - {".pmw", "application/x-perfmon"}, - {".png", "image/png"}, - {".pnm", "image/x-portable-anymap"}, - {".pnz", "image/png"}, - {".pot", "application/vnd.ms-powerpoint"}, - {".potm", "application/vnd.ms-powerpoint.template.macroEnabled.12"}, - {".potx", "application/vnd.openxmlformats-officedocument.presentationml.template"}, - {".ppam", "application/vnd.ms-powerpoint.addin.macroEnabled.12"}, - {".ppm", "image/x-portable-pixmap"}, - {".pps", "application/vnd.ms-powerpoint"}, - {".ppsm", "application/vnd.ms-powerpoint.slideshow.macroEnabled.12"}, - {".ppsx", "application/vnd.openxmlformats-officedocument.presentationml.slideshow"}, - {".ppt", "application/vnd.ms-powerpoint"}, - {".pptm", "application/vnd.ms-powerpoint.presentation.macroEnabled.12"}, - {".pptx", "application/vnd.openxmlformats-officedocument.presentationml.presentation"}, - {".prf", "application/pics-rules"}, - {".prm", "application/octet-stream"}, - {".prx", "application/octet-stream"}, - {".ps", "application/postscript"}, - {".psd", "application/octet-stream"}, - {".psm", "application/octet-stream"}, - {".psp", "application/octet-stream"}, - {".pub", "application/x-mspublisher"}, - {".qt", "video/quicktime"}, - {".qtl", "application/x-quicktimeplayer"}, - {".qxd", "application/octet-stream"}, - {".ra", "audio/x-pn-realaudio"}, - {".ram", "audio/x-pn-realaudio"}, - {".rar", "application/octet-stream"}, - {".ras", "image/x-cmu-raster"}, - {".rf", "image/vnd.rn-realflash"}, - {".rgb", "image/x-rgb"}, - {".rm", "application/vnd.rn-realmedia"}, - {".rmi", "audio/mid"}, - {".roff", "application/x-troff"}, - {".rpm", "audio/x-pn-realaudio-plugin"}, - {".rtf", "application/rtf"}, - {".rtx", "text/richtext"}, - {".scd", "application/x-msschedule"}, - {".sct", "text/scriptlet"}, - {".sea", "application/octet-stream"}, - {".setpay", "application/set-payment-initiation"}, - {".setreg", "application/set-registration-initiation"}, - {".sgml", "text/sgml"}, - {".sh", "application/x-sh"}, - {".shar", "application/x-shar"}, - {".sit", "application/x-stuffit"}, - {".sldm", "application/vnd.ms-powerpoint.slide.macroEnabled.12"}, - {".sldx", "application/vnd.openxmlformats-officedocument.presentationml.slide"}, - {".smd", "audio/x-smd"}, - {".smi", "application/octet-stream"}, - {".smx", "audio/x-smd"}, - {".smz", "audio/x-smd"}, - {".snd", "audio/basic"}, - {".snp", "application/octet-stream"}, - {".spc", "application/x-pkcs7-certificates"}, - {".spl", "application/futuresplash"}, - {".spx", "audio/ogg"}, - {".src", "application/x-wais-source"}, - {".ssm", "application/streamingmedia"}, - {".sst", "application/vnd.ms-pki.certstore"}, - {".stl", "application/vnd.ms-pki.stl"}, - {".sv4cpio", "application/x-sv4cpio"}, - {".sv4crc", "application/x-sv4crc"}, - {".svg", "image/svg+xml"}, - {".svgz", "image/svg+xml"}, - {".swf", "application/x-shockwave-flash"}, - {".t", "application/x-troff"}, - {".tar", "application/x-tar"}, - {".tcl", "application/x-tcl"}, - {".tex", "application/x-tex"}, - {".texi", "application/x-texinfo"}, - {".texinfo", "application/x-texinfo"}, - {".tgz", "application/x-compressed"}, - {".thmx", "application/vnd.ms-officetheme"}, - {".thn", "application/octet-stream"}, - {".tif", "image/tiff"}, - {".tiff", "image/tiff"}, - {".toc", "application/octet-stream"}, - {".tr", "application/x-troff"}, - {".trm", "application/x-msterminal"}, - {".ts", "video/vnd.dlna.mpeg-tts"}, - {".tsv", "text/tab-separated-values"}, - {".ttc", "application/x-font-ttf"}, - {".ttf", "application/x-font-ttf"}, - {".tts", "video/vnd.dlna.mpeg-tts"}, - {".txt", "text/plain"}, - {".u32", "application/octet-stream"}, - {".uls", "text/iuls"}, - {".ustar", "application/x-ustar"}, - {".vbs", "text/vbscript"}, - {".vcf", "text/x-vcard"}, - {".vcs", "text/plain"}, - {".vdx", "application/vnd.ms-visio.viewer"}, - {".vml", "text/xml"}, - {".vsd", "application/vnd.visio"}, - {".vss", "application/vnd.visio"}, - {".vst", "application/vnd.visio"}, - {".vsto", "application/x-ms-vsto"}, - {".vsw", "application/vnd.visio"}, - {".vsx", "application/vnd.visio"}, - {".vtx", "application/vnd.visio"}, - {".wasm", "application/wasm"}, - {".wav", "audio/wav"}, - {".wax", "audio/x-ms-wax"}, - {".wbmp", "image/vnd.wap.wbmp"}, - {".wcm", "application/vnd.ms-works"}, - {".wdb", "application/vnd.ms-works"}, - {".webm", "video/webm"}, - {".webp", "image/webp"}, - {".wks", "application/vnd.ms-works"}, - {".wm", "video/x-ms-wm"}, - {".wma", "audio/x-ms-wma"}, - {".wmd", "application/x-ms-wmd"}, - {".wmf", "application/x-msmetafile"}, - {".wml", "text/vnd.wap.wml"}, - {".wmlc", "application/vnd.wap.wmlc"}, - {".wmls", "text/vnd.wap.wmlscript"}, - {".wmlsc", "application/vnd.wap.wmlscriptc"}, - {".wmp", "video/x-ms-wmp"}, - {".wmv", "video/x-ms-wmv"}, - {".wmx", "video/x-ms-wmx"}, - {".wmz", "application/x-ms-wmz"}, - {".woff", "application/font-woff"}, // https://www.w3.org/TR/WOFF/#appendix-b - {".woff2", "font/woff2"}, // https://www.w3.org/TR/WOFF2/#IMT - {".wps", "application/vnd.ms-works"}, - {".wri", "application/x-mswrite"}, - {".wrl", "x-world/x-vrml"}, - {".wrz", "x-world/x-vrml"}, - {".wsdl", "text/xml"}, - {".wtv", "video/x-ms-wtv"}, - {".wvx", "video/x-ms-wvx"}, - {".x", "application/directx"}, - {".xaf", "x-world/x-vrml"}, - {".xaml", "application/xaml+xml"}, - {".xap", "application/x-silverlight-app"}, - {".xbap", "application/x-ms-xbap"}, - {".xbm", "image/x-xbitmap"}, - {".xdr", "text/plain"}, - {".xht", "application/xhtml+xml"}, - {".xhtml", "application/xhtml+xml"}, - {".xla", "application/vnd.ms-excel"}, - {".xlam", "application/vnd.ms-excel.addin.macroEnabled.12"}, - {".xlc", "application/vnd.ms-excel"}, - {".xlm", "application/vnd.ms-excel"}, - {".xls", "application/vnd.ms-excel"}, - {".xlsb", "application/vnd.ms-excel.sheet.binary.macroEnabled.12"}, - {".xlsm", "application/vnd.ms-excel.sheet.macroEnabled.12"}, - {".xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"}, - {".xlt", "application/vnd.ms-excel"}, - {".xltm", "application/vnd.ms-excel.template.macroEnabled.12"}, - {".xltx", "application/vnd.openxmlformats-officedocument.spreadsheetml.template"}, - {".xlw", "application/vnd.ms-excel"}, - {".xml", "text/xml"}, - {".xof", "x-world/x-vrml"}, - {".xpm", "image/x-xpixmap"}, - {".xps", "application/vnd.ms-xpsdocument"}, - {".xsd", "text/xml"}, - {".xsf", "text/xml"}, - {".xsl", "text/xml"}, - {".xslt", "text/xml"}, - {".xsn", "application/octet-stream"}, - {".xtp", "application/octet-stream"}, - {".xwd", "image/x-xwindowdump"}, - {".z", "application/x-compress"}, - {".zip", "application/x-zip-compressed"}, - }; + private static readonly IDictionary _map = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + { ".323", "text/h323" }, + { ".3g2", "video/3gpp2" }, + { ".3gp2", "video/3gpp2" }, + { ".3gp", "video/3gpp" }, + { ".3gpp", "video/3gpp" }, + { ".aac", "audio/aac" }, + { ".aaf", "application/octet-stream" }, + { ".aca", "application/octet-stream" }, + { ".accdb", "application/msaccess" }, + { ".accde", "application/msaccess" }, + { ".accdt", "application/msaccess" }, + { ".acx", "application/internet-property-stream" }, + { ".adt", "audio/vnd.dlna.adts" }, + { ".adts", "audio/vnd.dlna.adts" }, + { ".afm", "application/octet-stream" }, + { ".ai", "application/postscript" }, + { ".aif", "audio/x-aiff" }, + { ".aifc", "audio/aiff" }, + { ".aiff", "audio/aiff" }, + { ".appcache", "text/cache-manifest" }, + { ".application", "application/x-ms-application" }, + { ".art", "image/x-jg" }, + { ".asd", "application/octet-stream" }, + { ".asf", "video/x-ms-asf" }, + { ".asi", "application/octet-stream" }, + { ".asm", "text/plain" }, + { ".asr", "video/x-ms-asf" }, + { ".asx", "video/x-ms-asf" }, + { ".atom", "application/atom+xml" }, + { ".au", "audio/basic" }, + { ".avi", "video/x-msvideo" }, + { ".axs", "application/olescript" }, + { ".bas", "text/plain" }, + { ".bcpio", "application/x-bcpio" }, + { ".bin", "application/octet-stream" }, + { ".bmp", "image/bmp" }, + { ".c", "text/plain" }, + { ".cab", "application/vnd.ms-cab-compressed" }, + { ".calx", "application/vnd.ms-office.calx" }, + { ".cat", "application/vnd.ms-pki.seccat" }, + { ".cdf", "application/x-cdf" }, + { ".chm", "application/octet-stream" }, + { ".class", "application/x-java-applet" }, + { ".clp", "application/x-msclip" }, + { ".cmx", "image/x-cmx" }, + { ".cnf", "text/plain" }, + { ".cod", "image/cis-cod" }, + { ".cpio", "application/x-cpio" }, + { ".cpp", "text/plain" }, + { ".crd", "application/x-mscardfile" }, + { ".crl", "application/pkix-crl" }, + { ".crt", "application/x-x509-ca-cert" }, + { ".csh", "application/x-csh" }, + { ".css", "text/css" }, + { ".csv", "application/octet-stream" }, + { ".cur", "application/octet-stream" }, + { ".dcr", "application/x-director" }, + { ".deploy", "application/octet-stream" }, + { ".der", "application/x-x509-ca-cert" }, + { ".dib", "image/bmp" }, + { ".dir", "application/x-director" }, + { ".disco", "text/xml" }, + { ".dlm", "text/dlm" }, + { ".doc", "application/msword" }, + { ".docm", "application/vnd.ms-word.document.macroEnabled.12" }, + { ".docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document" }, + { ".dot", "application/msword" }, + { ".dotm", "application/vnd.ms-word.template.macroEnabled.12" }, + { ".dotx", "application/vnd.openxmlformats-officedocument.wordprocessingml.template" }, + { ".dsp", "application/octet-stream" }, + { ".dtd", "text/xml" }, + { ".dvi", "application/x-dvi" }, + { ".dvr-ms", "video/x-ms-dvr" }, + { ".dwf", "drawing/x-dwf" }, + { ".dwp", "application/octet-stream" }, + { ".dxr", "application/x-director" }, + { ".eml", "message/rfc822" }, + { ".emz", "application/octet-stream" }, + { ".eot", "application/vnd.ms-fontobject" }, + { ".eps", "application/postscript" }, + { ".etx", "text/x-setext" }, + { ".evy", "application/envoy" }, + { ".fdf", "application/vnd.fdf" }, + { ".fif", "application/fractals" }, + { ".fla", "application/octet-stream" }, + { ".flr", "x-world/x-vrml" }, + { ".flv", "video/x-flv" }, + { ".gif", "image/gif" }, + { ".gtar", "application/x-gtar" }, + { ".gz", "application/x-gzip" }, + { ".h", "text/plain" }, + { ".hdf", "application/x-hdf" }, + { ".hdml", "text/x-hdml" }, + { ".hhc", "application/x-oleobject" }, + { ".hhk", "application/octet-stream" }, + { ".hhp", "application/octet-stream" }, + { ".hlp", "application/winhlp" }, + { ".hqx", "application/mac-binhex40" }, + { ".hta", "application/hta" }, + { ".htc", "text/x-component" }, + { ".htm", "text/html" }, + { ".html", "text/html" }, + { ".htt", "text/webviewhtml" }, + { ".hxt", "text/html" }, + { ".ical", "text/calendar" }, + { ".icalendar", "text/calendar" }, + { ".ico", "image/x-icon" }, + { ".ics", "text/calendar" }, + { ".ief", "image/ief" }, + { ".ifb", "text/calendar" }, + { ".iii", "application/x-iphone" }, + { ".inf", "application/octet-stream" }, + { ".ins", "application/x-internet-signup" }, + { ".isp", "application/x-internet-signup" }, + { ".IVF", "video/x-ivf" }, + { ".jar", "application/java-archive" }, + { ".java", "application/octet-stream" }, + { ".jck", "application/liquidmotion" }, + { ".jcz", "application/liquidmotion" }, + { ".jfif", "image/pjpeg" }, + { ".jpb", "application/octet-stream" }, + { ".jpe", "image/jpeg" }, + { ".jpeg", "image/jpeg" }, + { ".jpg", "image/jpeg" }, + { ".js", "application/javascript" }, + { ".json", "application/json" }, + { ".jsx", "text/jscript" }, + { ".latex", "application/x-latex" }, + { ".lit", "application/x-ms-reader" }, + { ".lpk", "application/octet-stream" }, + { ".lsf", "video/x-la-asf" }, + { ".lsx", "video/x-la-asf" }, + { ".lzh", "application/octet-stream" }, + { ".m13", "application/x-msmediaview" }, + { ".m14", "application/x-msmediaview" }, + { ".m1v", "video/mpeg" }, + { ".m2ts", "video/vnd.dlna.mpeg-tts" }, + { ".m3u", "audio/x-mpegurl" }, + { ".m4a", "audio/mp4" }, + { ".m4v", "video/mp4" }, + { ".man", "application/x-troff-man" }, + { ".manifest", "application/x-ms-manifest" }, + { ".map", "text/plain" }, + { ".markdown", "text/markdown" }, + { ".md", "text/markdown" }, + { ".mdb", "application/x-msaccess" }, + { ".mdp", "application/octet-stream" }, + { ".me", "application/x-troff-me" }, + { ".mht", "message/rfc822" }, + { ".mhtml", "message/rfc822" }, + { ".mid", "audio/mid" }, + { ".midi", "audio/mid" }, + { ".mix", "application/octet-stream" }, + { ".mmf", "application/x-smaf" }, + { ".mno", "text/xml" }, + { ".mny", "application/x-msmoney" }, + { ".mov", "video/quicktime" }, + { ".movie", "video/x-sgi-movie" }, + { ".mp2", "video/mpeg" }, + { ".mp3", "audio/mpeg" }, + { ".mp4", "video/mp4" }, + { ".mp4v", "video/mp4" }, + { ".mpa", "video/mpeg" }, + { ".mpe", "video/mpeg" }, + { ".mpeg", "video/mpeg" }, + { ".mpg", "video/mpeg" }, + { ".mpp", "application/vnd.ms-project" }, + { ".mpv2", "video/mpeg" }, + { ".ms", "application/x-troff-ms" }, + { ".msi", "application/octet-stream" }, + { ".mso", "application/octet-stream" }, + { ".mvb", "application/x-msmediaview" }, + { ".mvc", "application/x-miva-compiled" }, + { ".nc", "application/x-netcdf" }, + { ".nsc", "video/x-ms-asf" }, + { ".nws", "message/rfc822" }, + { ".ocx", "application/octet-stream" }, + { ".oda", "application/oda" }, + { ".odc", "text/x-ms-odc" }, + { ".ods", "application/oleobject" }, + { ".oga", "audio/ogg" }, + { ".ogg", "video/ogg" }, + { ".ogv", "video/ogg" }, + { ".ogx", "application/ogg" }, + { ".one", "application/onenote" }, + { ".onea", "application/onenote" }, + { ".onetoc", "application/onenote" }, + { ".onetoc2", "application/onenote" }, + { ".onetmp", "application/onenote" }, + { ".onepkg", "application/onenote" }, + { ".osdx", "application/opensearchdescription+xml" }, + { ".otf", "font/otf" }, + { ".p10", "application/pkcs10" }, + { ".p12", "application/x-pkcs12" }, + { ".p7b", "application/x-pkcs7-certificates" }, + { ".p7c", "application/pkcs7-mime" }, + { ".p7m", "application/pkcs7-mime" }, + { ".p7r", "application/x-pkcs7-certreqresp" }, + { ".p7s", "application/pkcs7-signature" }, + { ".pbm", "image/x-portable-bitmap" }, + { ".pcx", "application/octet-stream" }, + { ".pcz", "application/octet-stream" }, + { ".pdf", "application/pdf" }, + { ".pfb", "application/octet-stream" }, + { ".pfm", "application/octet-stream" }, + { ".pfx", "application/x-pkcs12" }, + { ".pgm", "image/x-portable-graymap" }, + { ".pko", "application/vnd.ms-pki.pko" }, + { ".pma", "application/x-perfmon" }, + { ".pmc", "application/x-perfmon" }, + { ".pml", "application/x-perfmon" }, + { ".pmr", "application/x-perfmon" }, + { ".pmw", "application/x-perfmon" }, + { ".png", "image/png" }, + { ".pnm", "image/x-portable-anymap" }, + { ".pnz", "image/png" }, + { ".pot", "application/vnd.ms-powerpoint" }, + { ".potm", "application/vnd.ms-powerpoint.template.macroEnabled.12" }, + { ".potx", "application/vnd.openxmlformats-officedocument.presentationml.template" }, + { ".ppam", "application/vnd.ms-powerpoint.addin.macroEnabled.12" }, + { ".ppm", "image/x-portable-pixmap" }, + { ".pps", "application/vnd.ms-powerpoint" }, + { ".ppsm", "application/vnd.ms-powerpoint.slideshow.macroEnabled.12" }, + { ".ppsx", "application/vnd.openxmlformats-officedocument.presentationml.slideshow" }, + { ".ppt", "application/vnd.ms-powerpoint" }, + { ".pptm", "application/vnd.ms-powerpoint.presentation.macroEnabled.12" }, + { ".pptx", "application/vnd.openxmlformats-officedocument.presentationml.presentation" }, + { ".prf", "application/pics-rules" }, + { ".prm", "application/octet-stream" }, + { ".prx", "application/octet-stream" }, + { ".ps", "application/postscript" }, + { ".psd", "application/octet-stream" }, + { ".psm", "application/octet-stream" }, + { ".psp", "application/octet-stream" }, + { ".pub", "application/x-mspublisher" }, + { ".qt", "video/quicktime" }, + { ".qtl", "application/x-quicktimeplayer" }, + { ".qxd", "application/octet-stream" }, + { ".ra", "audio/x-pn-realaudio" }, + { ".ram", "audio/x-pn-realaudio" }, + { ".rar", "application/octet-stream" }, + { ".ras", "image/x-cmu-raster" }, + { ".rf", "image/vnd.rn-realflash" }, + { ".rgb", "image/x-rgb" }, + { ".rm", "application/vnd.rn-realmedia" }, + { ".rmi", "audio/mid" }, + { ".roff", "application/x-troff" }, + { ".rpm", "audio/x-pn-realaudio-plugin" }, + { ".rtf", "application/rtf" }, + { ".rtx", "text/richtext" }, + { ".scd", "application/x-msschedule" }, + { ".sct", "text/scriptlet" }, + { ".sea", "application/octet-stream" }, + { ".setpay", "application/set-payment-initiation" }, + { ".setreg", "application/set-registration-initiation" }, + { ".sgml", "text/sgml" }, + { ".sh", "application/x-sh" }, + { ".shar", "application/x-shar" }, + { ".sit", "application/x-stuffit" }, + { ".sldm", "application/vnd.ms-powerpoint.slide.macroEnabled.12" }, + { ".sldx", "application/vnd.openxmlformats-officedocument.presentationml.slide" }, + { ".smd", "audio/x-smd" }, + { ".smi", "application/octet-stream" }, + { ".smx", "audio/x-smd" }, + { ".smz", "audio/x-smd" }, + { ".snd", "audio/basic" }, + { ".snp", "application/octet-stream" }, + { ".spc", "application/x-pkcs7-certificates" }, + { ".spl", "application/futuresplash" }, + { ".spx", "audio/ogg" }, + { ".src", "application/x-wais-source" }, + { ".ssm", "application/streamingmedia" }, + { ".sst", "application/vnd.ms-pki.certstore" }, + { ".stl", "application/vnd.ms-pki.stl" }, + { ".sv4cpio", "application/x-sv4cpio" }, + { ".sv4crc", "application/x-sv4crc" }, + { ".svg", "image/svg+xml" }, + { ".svgz", "image/svg+xml" }, + { ".swf", "application/x-shockwave-flash" }, + { ".t", "application/x-troff" }, + { ".tar", "application/x-tar" }, + { ".tcl", "application/x-tcl" }, + { ".tex", "application/x-tex" }, + { ".texi", "application/x-texinfo" }, + { ".texinfo", "application/x-texinfo" }, + { ".tgz", "application/x-compressed" }, + { ".thmx", "application/vnd.ms-officetheme" }, + { ".thn", "application/octet-stream" }, + { ".tif", "image/tiff" }, + { ".tiff", "image/tiff" }, + { ".toc", "application/octet-stream" }, + { ".tr", "application/x-troff" }, + { ".trm", "application/x-msterminal" }, + { ".ts", "video/vnd.dlna.mpeg-tts" }, + { ".tsv", "text/tab-separated-values" }, + { ".ttc", "application/x-font-ttf" }, + { ".ttf", "application/x-font-ttf" }, + { ".tts", "video/vnd.dlna.mpeg-tts" }, + { ".txt", "text/plain" }, + { ".u32", "application/octet-stream" }, + { ".uls", "text/iuls" }, + { ".ustar", "application/x-ustar" }, + { ".vbs", "text/vbscript" }, + { ".vcf", "text/x-vcard" }, + { ".vcs", "text/plain" }, + { ".vdx", "application/vnd.ms-visio.viewer" }, + { ".vml", "text/xml" }, + { ".vsd", "application/vnd.visio" }, + { ".vss", "application/vnd.visio" }, + { ".vst", "application/vnd.visio" }, + { ".vsto", "application/x-ms-vsto" }, + { ".vsw", "application/vnd.visio" }, + { ".vsx", "application/vnd.visio" }, + { ".vtx", "application/vnd.visio" }, + { ".wasm", "application/wasm" }, + { ".wav", "audio/wav" }, + { ".wax", "audio/x-ms-wax" }, + { ".wbmp", "image/vnd.wap.wbmp" }, + { ".wcm", "application/vnd.ms-works" }, + { ".wdb", "application/vnd.ms-works" }, + { ".webm", "video/webm" }, + { ".webp", "image/webp" }, + { ".wks", "application/vnd.ms-works" }, + { ".wm", "video/x-ms-wm" }, + { ".wma", "audio/x-ms-wma" }, + { ".wmd", "application/x-ms-wmd" }, + { ".wmf", "application/x-msmetafile" }, + { ".wml", "text/vnd.wap.wml" }, + { ".wmlc", "application/vnd.wap.wmlc" }, + { ".wmls", "text/vnd.wap.wmlscript" }, + { ".wmlsc", "application/vnd.wap.wmlscriptc" }, + { ".wmp", "video/x-ms-wmp" }, + { ".wmv", "video/x-ms-wmv" }, + { ".wmx", "video/x-ms-wmx" }, + { ".wmz", "application/x-ms-wmz" }, + { ".woff", "application/font-woff" }, // https://www.w3.org/TR/WOFF/#appendix-b + { ".woff2", "font/woff2" }, // https://www.w3.org/TR/WOFF2/#IMT + { ".wps", "application/vnd.ms-works" }, + { ".wri", "application/x-mswrite" }, + { ".wrl", "x-world/x-vrml" }, + { ".wrz", "x-world/x-vrml" }, + { ".wsdl", "text/xml" }, + { ".wtv", "video/x-ms-wtv" }, + { ".wvx", "video/x-ms-wvx" }, + { ".x", "application/directx" }, + { ".xaf", "x-world/x-vrml" }, + { ".xaml", "application/xaml+xml" }, + { ".xap", "application/x-silverlight-app" }, + { ".xbap", "application/x-ms-xbap" }, + { ".xbm", "image/x-xbitmap" }, + { ".xdr", "text/plain" }, + { ".xht", "application/xhtml+xml" }, + { ".xhtml", "application/xhtml+xml" }, + { ".xla", "application/vnd.ms-excel" }, + { ".xlam", "application/vnd.ms-excel.addin.macroEnabled.12" }, + { ".xlc", "application/vnd.ms-excel" }, + { ".xlm", "application/vnd.ms-excel" }, + { ".xls", "application/vnd.ms-excel" }, + { ".xlsb", "application/vnd.ms-excel.sheet.binary.macroEnabled.12" }, + { ".xlsm", "application/vnd.ms-excel.sheet.macroEnabled.12" }, + { ".xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" }, + { ".xlt", "application/vnd.ms-excel" }, + { ".xltm", "application/vnd.ms-excel.template.macroEnabled.12" }, + { ".xltx", "application/vnd.openxmlformats-officedocument.spreadsheetml.template" }, + { ".xlw", "application/vnd.ms-excel" }, + { ".xml", "text/xml" }, + { ".xof", "x-world/x-vrml" }, + { ".xpm", "image/x-xpixmap" }, + { ".xps", "application/vnd.ms-xpsdocument" }, + { ".xsd", "text/xml" }, + { ".xsf", "text/xml" }, + { ".xsl", "text/xml" }, + { ".xslt", "text/xml" }, + { ".xsn", "application/octet-stream" }, + { ".xtp", "application/octet-stream" }, + { ".xwd", "image/x-xwindowdump" }, + { ".z", "application/x-compress" }, + { ".zip", "application/x-zip-compressed" }, + }; /// /// The cross reference table of file extensions and content-types.