diff --git a/src/Sinch/Core/UrlResolver.cs b/src/Sinch/Core/UrlResolver.cs index 4fe03cc..c8dd677 100644 --- a/src/Sinch/Core/UrlResolver.cs +++ b/src/Sinch/Core/UrlResolver.cs @@ -1,5 +1,6 @@ using System; using Sinch.Conversation; +using Sinch.Mailgun; using Sinch.SMS; using Sinch.Voice; @@ -83,5 +84,26 @@ public Uri ResolveSmsServicePlanIdUrl(SmsServicePlanIdRegion smsServicePlanIdReg return new Uri(string.Format(smsApiServicePlanIdUrlTemplate, smsServicePlanIdRegion.Value.ToLowerInvariant())); } + + public Uri ResolveMailgunUrl(MailgunRegion mailgunRegion) + { + if (!string.IsNullOrEmpty(_apiUrlOverrides?.MailgunUrl)) return new Uri(_apiUrlOverrides.MailgunUrl); + + string? mailgunUrl; + switch (mailgunRegion) + { + case MailgunRegion.Us: + mailgunUrl = "https://api.mailgun.net"; + break; + case MailgunRegion.Eu: + mailgunUrl = "https://api.eu.mailgun.net"; + break; + default: + throw new ArgumentOutOfRangeException(nameof(mailgunRegion), mailgunRegion, + "Unexpected enum value."); + } + + return new Uri(mailgunUrl); + } } } diff --git a/src/Sinch/Mailgun/MailgunRegion.cs b/src/Sinch/Mailgun/MailgunRegion.cs new file mode 100644 index 0000000..b5ac210 --- /dev/null +++ b/src/Sinch/Mailgun/MailgunRegion.cs @@ -0,0 +1,18 @@ +namespace Sinch.Mailgun +{ + /// + /// Mailgun allows the ability to send and receive email in both US and EU regions. + /// Be sure to use the appropriate region on which you have created your domain. + /// + public enum MailgunRegion + { + /// + /// United States region + /// + Us, + /// + /// Europe region + /// + Eu + } +} diff --git a/src/Sinch/Mailgun/SinchMailgunClient.cs b/src/Sinch/Mailgun/SinchMailgunClient.cs new file mode 100644 index 0000000..874a00a --- /dev/null +++ b/src/Sinch/Mailgun/SinchMailgunClient.cs @@ -0,0 +1,23 @@ +using System; +using Sinch.Core; +using Sinch.Logger; + +namespace Sinch.Mailgun +{ + /// + /// The Mailgun API is part of the Sinch family and enables you to send, track, and receive email effortlessly. + /// + public interface ISinchMailgunClient + { + // TBD: add domains + } + + /// + internal class SinchMailgunClient : ISinchMailgunClient + { + public SinchMailgunClient(Uri baseUrl, Http http, LoggerFactory? loggerFactory = null) + { + // TBD: implement domains + } + } +} diff --git a/src/Sinch/SinchClient.cs b/src/Sinch/SinchClient.cs index c3dc7ba..7bb6e81 100644 --- a/src/Sinch/SinchClient.cs +++ b/src/Sinch/SinchClient.cs @@ -7,6 +7,7 @@ using Sinch.Conversation; using Sinch.Core; using Sinch.Logger; +using Sinch.Mailgun; using Sinch.Numbers; using Sinch.SMS; using Sinch.Verification; @@ -111,6 +112,17 @@ public ISinchVerificationClient Verification(string appKey, string appSecret, /// See . Defaults to /// public ISinchVoiceClient Voice(string appKey, string appSecret, VoiceRegion? voiceRegion = null); + + /// + /// APIs are at the heart of Mailgun. + /// Our goal is to provide developers worldwide with an accessible and straightforward way to send, + /// receive, and track emails effortlessly. + /// + /// When you sign up for Mailgun, a primary account API key is generated. + /// This key allows you to perform all CRUD operations via our various API endpoints and for any of your sending domains. + /// + /// + public ISinchMailgunClient Mailgun(string apiKey, MailgunRegion region); } public class SinchClient : ISinchClient @@ -131,6 +143,32 @@ public class SinchClient : ISinchClient private readonly ILoggerAdapter? _logger; private readonly UrlResolver _urlResolver; + /// + /// Initialize a new without providing common credentials. + /// Intended to be used for APIs which requires their own credentials: + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// If you plan to use other APIs or not sure about what API you are planning to use, consider using main constructor + /// + /// + /// + /// var mailgunClient = new SinchClient().Mailgun("apikey", MailgunRegion.Eu); + /// + /// + /// + /// Optional. See: + public SinchClient(Action? options = default) : this(null, null, null, options) + { + } /// /// Initialize a new @@ -274,6 +312,22 @@ public ISinchVoiceClient Voice(string appKey, string appSecret, _urlResolver.ResolveVoiceApplicationManagementUrl()); } + /// + public ISinchMailgunClient Mailgun(string apiKey, MailgunRegion region) + { + if (string.IsNullOrEmpty(apiKey)) + { + throw new ArgumentNullException(nameof(apiKey), "apiKey shouldn't be null or empty"); + } + + var baseUrl = _urlResolver.ResolveMailgunUrl(region); + var mailgunAuth = new BasicAuth(appKey: "api", appSecret: apiKey); + // NOTE: jsonNamingPolicy will not play a role here as property naming of mailgun is inconsistent + // meaning, all lifting will be done through JsonPropertyNamingAttribute + var http = new Http(mailgunAuth, _httpClient, _loggerFactory?.Create(), JsonNamingPolicy.CamelCase); + return new SinchMailgunClient(baseUrl, http, _loggerFactory); + } + private void ValidateCommonCredentials() { var exceptions = new List(); diff --git a/src/Sinch/SinchOptions.cs b/src/Sinch/SinchOptions.cs index 5a49124..eb90c25 100644 --- a/src/Sinch/SinchOptions.cs +++ b/src/Sinch/SinchOptions.cs @@ -137,5 +137,10 @@ public sealed class ApiUrlOverrides /// Overrides Numbers api base url /// public string? NumbersUrl { get; init; } + + /// + /// Overrides Mailgun api base url + /// + public string? MailgunUrl { get; init; } } } diff --git a/tests/Sinch.Tests/Core/UrlResolverTests.cs b/tests/Sinch.Tests/Core/UrlResolverTests.cs index 203b161..fec49e0 100644 --- a/tests/Sinch.Tests/Core/UrlResolverTests.cs +++ b/tests/Sinch.Tests/Core/UrlResolverTests.cs @@ -1,8 +1,10 @@ using System; using System.Collections.Generic; +using System.Linq; using FluentAssertions; using Sinch.Conversation; using Sinch.Core; +using Sinch.Mailgun; using Sinch.SMS; using Sinch.Voice; using Xunit; @@ -306,7 +308,8 @@ public void ResolveSmsUrl(SmsRegion smsRegion, ApiUrlOverrides apiUrlOverrides) [Theory] [MemberData(nameof(SmsServicePlanIdUrlData))] - public void ResolveSmsServicePlanIdUrl(SmsServicePlanIdRegion smsServicePlanIdRegion, ApiUrlOverrides apiUrlOverrides) + public void ResolveSmsServicePlanIdUrl(SmsServicePlanIdRegion smsServicePlanIdRegion, + ApiUrlOverrides apiUrlOverrides) { var smsUrl = new UrlResolver(apiUrlOverrides).ResolveSmsServicePlanIdUrl(smsServicePlanIdRegion); var expectedUrl = string.IsNullOrEmpty(apiUrlOverrides?.SmsUrl) @@ -314,5 +317,50 @@ public void ResolveSmsServicePlanIdUrl(SmsServicePlanIdRegion smsServicePlanIdRe : new Uri(apiUrlOverrides.SmsUrl); smsUrl.Should().BeEquivalentTo(expectedUrl); } + + public record ResolveMailgunUrlTestData( + string TestName, + MailgunRegion Region, + ApiUrlOverrides ApiUrlOverrides, + string ExpectedUrl) + { + private static readonly ResolveMailgunUrlTestData[] TestCases = + { + new("Default US Mailgun address", MailgunRegion.Us, null, "https://api.mailgun.net"), + new("Default EU Mailgun address", MailgunRegion.Eu, null, "https://api.eu.mailgun.net"), + new("Default EU region even if override set but null", MailgunRegion.Eu, new ApiUrlOverrides() + { + MailgunUrl = null + }, "https://api.eu.mailgun.net"), + new("Override url", MailgunRegion.Eu, new ApiUrlOverrides() + { + MailgunUrl = "https://my-mailgun-proxy.net" + }, "https://my-mailgun-proxy.net"), + }; + + public static IEnumerable TestCasesData => + TestCases.Select(testCase => new object[] { testCase }); + + public override string ToString() + { + return TestName; + } + } + + [Theory] + [MemberData(nameof(ResolveMailgunUrlTestData.TestCasesData), MemberType = typeof(ResolveMailgunUrlTestData))] + public void ResolveMailgunUrl(ResolveMailgunUrlTestData data) + { + var actual = new UrlResolver(data.ApiUrlOverrides).ResolveMailgunUrl(data.Region); + + actual.Should().BeEquivalentTo(new Uri(data.ExpectedUrl)); + } + + [Fact] + public void ThrowIfRegionIsUnexpected() + { + var op = () => new UrlResolver(new ApiUrlOverrides()).ResolveMailgunUrl((MailgunRegion)(-1)); + op.Should().ThrowExactly(); + } } } diff --git a/tests/Sinch.Tests/Mailgun/MailgunClientTests.cs b/tests/Sinch.Tests/Mailgun/MailgunClientTests.cs new file mode 100644 index 0000000..9618e6e --- /dev/null +++ b/tests/Sinch.Tests/Mailgun/MailgunClientTests.cs @@ -0,0 +1,28 @@ +using System; +using FluentAssertions; +using Sinch.Mailgun; +using Xunit; + +namespace Sinch.Tests.Mailgun +{ + public class MailgunClientTests + { + [Fact] + public void InitMailgunClient() + { + var mailgun = new SinchClient().Mailgun("apikey", MailgunRegion.Eu); + + mailgun.Should().NotBeNull(); + } + + [Theory] + [InlineData("")] + [InlineData(null)] + public void FailEmptyApiKey(string apiKey) + { + var mailgunCreation = () => new SinchClient().Mailgun(apiKey, MailgunRegion.Eu); + + mailgunCreation.Should().Throw(); + } + } +}