From 5582c56e74721bfdc12d18f753bbde9eefa54c44 Mon Sep 17 00:00:00 2001 From: smunuswamiac Date: Tue, 7 Jul 2026 18:46:01 -0500 Subject: [PATCH 1/2] PMK-2661 - Add CancellationToken support to all async methods MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every async method on PostmarkClient and PostmarkAdminClient (plus the PostmarkClientExtensions convenience overloads) now accepts an optional CancellationToken, threaded through PostmarkClientBase.ProcessRequestAsync / ProcessNoBodyRequestAsync and ISimpleHttpClient.SendAsync to HttpClient.SendAsync(request, cancellationToken). Non-breaking for callers: the parameter defaults to `default`. For the batch and templated-send methods that end in a `params` array (where C# forbids a trailing optional parameter), a `(CancellationToken, params T[])` overload is added and the original delegates to it with CancellationToken.None. Note: ISimpleHttpClient.SendAsync gains a CancellationToken parameter (with a default). Callers are unaffected; the rare consumer that implements this mocking seam directly must add the parameter. Adds ClientCancellationTests, which pass an already-canceled token and assert the call throws OperationCanceledException before any network I/O — proving the token reaches HttpClient.SendAsync without needing a live server or mutating the global ClientFactory. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/Postmark.Tests/ClientCancellationTests.cs | 95 ++++++ src/Postmark/ISimpleHttpClient.cs | 5 +- src/Postmark/PostmarkAdminClient.cs | 93 +++--- src/Postmark/PostmarkClient.cs | 315 +++++++++++------- src/Postmark/PostmarkClientBase.cs | 11 +- src/Postmark/PostmarkClientExtensions.cs | 11 +- src/Postmark/SimpleHttpClient.cs | 5 +- 7 files changed, 355 insertions(+), 180 deletions(-) create mode 100644 src/Postmark.Tests/ClientCancellationTests.cs diff --git a/src/Postmark.Tests/ClientCancellationTests.cs b/src/Postmark.Tests/ClientCancellationTests.cs new file mode 100644 index 0000000..2ed6a94 --- /dev/null +++ b/src/Postmark.Tests/ClientCancellationTests.cs @@ -0,0 +1,95 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using PostmarkDotNet; +using PostmarkDotNet.Model; +using Xunit; + +namespace Postmark.Tests +{ + /// + /// Verifies that the CancellationToken accepted by the async client methods is actually + /// threaded down to the underlying HttpClient.SendAsync call. + /// + /// These tests pass an already-canceled token: HttpClient.SendAsync short-circuits and + /// throws an OperationCanceledException before any network I/O occurs. If the token were + /// not threaded through, the call would instead attempt a real request and fail with a + /// different error, so this reliably distinguishes "threaded" from "dropped" without a + /// live server, network access, or mutating the global ClientFactory (which would break + /// the parallel integration tests). + /// + public class ClientCancellationTests + { + // Non-routable base URL: if the token were ever dropped, the call would fail here + // rather than silently contacting the real Postmark API. + private const string BaseUrl = "http://localhost:9"; + + private static PostmarkClient CanceledClient() => new PostmarkClient("test-server-token", BaseUrl); + private static PostmarkAdminClient CanceledAdminClient() => new PostmarkAdminClient("test-account-token", BaseUrl); + + private static CancellationToken Canceled() + { + var cts = new CancellationTokenSource(); + cts.Cancel(); + return cts.Token; + } + + [Fact] + public async Task GetRequest_HonorsCancellationToken() + { + var client = CanceledClient(); + await Assert.ThrowsAnyAsync( + () => client.GetServerAsync(Canceled())); + } + + [Fact] + public async Task GetRequestWithQueryParams_HonorsCancellationToken() + { + var client = CanceledClient(); + await Assert.ThrowsAnyAsync( + () => client.GetBouncesAsync(cancellationToken: Canceled())); + } + + [Fact] + public async Task PostRequestWithBody_HonorsCancellationToken() + { + var client = CanceledClient(); + await Assert.ThrowsAnyAsync( + () => client.SendMessageAsync(new PostmarkMessage(), Canceled())); + } + + [Fact] + public async Task ParamsBatchOverload_HonorsCancellationToken() + { + var client = CanceledClient(); + await Assert.ThrowsAnyAsync( + () => client.SendMessagesAsync(Canceled(), new PostmarkMessage())); + } + + [Fact] + public async Task TemplatedParamsOverload_HonorsCancellationToken() + { + var client = CanceledClient(); + await Assert.ThrowsAnyAsync( + () => client.SendEmailWithTemplateAsync(Canceled(), "template-alias", new { name = "x" }, + "to@example.com", "from@example.com")); + } + + [Fact] + public async Task AdminClient_HonorsCancellationToken() + { + var admin = CanceledAdminClient(); + await Assert.ThrowsAnyAsync( + () => admin.GetServersAsync(cancellationToken: Canceled())); + } + + [Fact] + public async Task ExtensionMethod_HonorsCancellationToken() + { + var client = CanceledClient(); + var messages = new List { new PostmarkMessage() }; + await Assert.ThrowsAnyAsync( + () => client.SendMessagesAsync(messages, Canceled())); + } + } +} diff --git a/src/Postmark/ISimpleHttpClient.cs b/src/Postmark/ISimpleHttpClient.cs index 5cdd2fd..da6da7b 100644 --- a/src/Postmark/ISimpleHttpClient.cs +++ b/src/Postmark/ISimpleHttpClient.cs @@ -1,10 +1,11 @@ using System.Net.Http; +using System.Threading; using System.Threading.Tasks; namespace PostmarkDotNet { public interface ISimpleHttpClient { - Task SendAsync(HttpRequestMessage request); + Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken = default); } -} \ No newline at end of file +} diff --git a/src/Postmark/PostmarkAdminClient.cs b/src/Postmark/PostmarkAdminClient.cs index dcd297a..3941948 100644 --- a/src/Postmark/PostmarkAdminClient.cs +++ b/src/Postmark/PostmarkAdminClient.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Net.Http; +using System.Threading; using System.Threading.Tasks; namespace PostmarkDotNet @@ -36,9 +37,9 @@ public PostmarkAdminClient(string accountToken, string apiBaseUri = "https://api /// /// /// - public async Task GetServerAsync(int serverId) + public async Task GetServerAsync(int serverId, CancellationToken cancellationToken = default) { - var retval = await this.ProcessNoBodyRequestAsync("/servers/" + serverId); + var retval = await this.ProcessNoBodyRequestAsync("/servers/" + serverId, cancellationToken: cancellationToken); //the API doesn't return the server ID here, which would be helpful. retval.ID = serverId; return retval; @@ -51,7 +52,7 @@ public async Task GetServerAsync(int serverId) /// /// To protected your account, you must first request access to use this endpont from support@postmarkapp.com /// - public async Task DeleteServerAsync(int serverId) + public async Task DeleteServerAsync(int serverId, CancellationToken cancellationToken = default) { // Adding a retry mechanism because server deletion currently fails intermittently due to deadlocks. // This is a temporary fix, and should be removed once the server delete operation is more reliable. @@ -60,7 +61,7 @@ public async Task DeleteServerAsync(int serverId) { try { - return await this.ProcessNoBodyRequestAsync("/servers/" + serverId, verb: HttpMethod.Delete); + return await this.ProcessNoBodyRequestAsync("/servers/" + serverId, verb: HttpMethod.Delete, cancellationToken: cancellationToken); } catch { @@ -81,7 +82,7 @@ public async Task CreateServerAsync(String name, string color = string bounceHookUrl = null, string openHookUrl = null, bool? postFirstOpenOnly = null, bool? trackOpens = null, string inboundDomain = null, int? inboundSpamThreshold = null, LinkTrackingOptions? trackLinks = null, string clickHookUrl = null, string deliveryHookUrl = null, - bool? enableSmtpApiErrorHooks = null, string deliveryType = null) + bool? enableSmtpApiErrorHooks = null, string deliveryType = null, CancellationToken cancellationToken = default) { var body = new Dictionary(); @@ -102,7 +103,7 @@ public async Task CreateServerAsync(String name, string color = body["EnableSmtpApiErrorHooks"] = enableSmtpApiErrorHooks; body["DeliveryType"] = deliveryType; - return await this.ProcessRequestAsync, PostmarkServer>("/servers/", HttpMethod.Post, body); + return await this.ProcessRequestAsync, PostmarkServer>("/servers/", HttpMethod.Post, body, cancellationToken: cancellationToken); } /// @@ -129,7 +130,7 @@ public async Task EditServerAsync(int serverId, String name = nu bool? rawEmailEnabled = null, bool? smtpApiActivated = null, string inboundHookUrl = null, string bounceHookUrl = null, string openHookUrl = null, bool? postFirstOpenOnly = null, bool? trackOpens = null, string inboundDomain = null, int? inboundSpamThreshold = null, - LinkTrackingOptions? trackLinks = null, string clickHookUrl = null, string deliveryHookUrl = null, bool? enableSmtpApiErrorHooks = null) + LinkTrackingOptions? trackLinks = null, string clickHookUrl = null, string deliveryHookUrl = null, bool? enableSmtpApiErrorHooks = null, CancellationToken cancellationToken = default) { var body = new Dictionary(); @@ -150,7 +151,7 @@ public async Task EditServerAsync(int serverId, String name = nu body["EnableSmtpApiErrorHooks"] = enableSmtpApiErrorHooks; return await this.ProcessRequestAsync, PostmarkServer> - ("/servers/" + serverId, HttpMethod.Put, body); + ("/servers/" + serverId, HttpMethod.Put, body, cancellationToken: cancellationToken); } /// @@ -159,12 +160,12 @@ public async Task EditServerAsync(int serverId, String name = nu /// /// /// - public async Task GetSenderSignaturesAsync(int offset = 0, int count = 100) + public async Task GetSenderSignaturesAsync(int offset = 0, int count = 100, CancellationToken cancellationToken = default) { var parameters = new Dictionary(); parameters["offset"] = offset; parameters["count"] = count; - return await this.ProcessNoBodyRequestAsync("/senders", parameters); + return await this.ProcessNoBodyRequestAsync("/senders", parameters, cancellationToken: cancellationToken); } @@ -173,9 +174,9 @@ public async Task GetSenderSignaturesAsync(int offs /// /// /// - public async Task GetSenderSignatureAsync(int signatureId) + public async Task GetSenderSignatureAsync(int signatureId, CancellationToken cancellationToken = default) { - return await this.ProcessNoBodyRequestAsync("/senders/" + signatureId); + return await this.ProcessNoBodyRequestAsync("/senders/" + signatureId, cancellationToken: cancellationToken); } /// @@ -183,10 +184,10 @@ public async Task GetSenderSignatureAsync(int s /// /// /// - public async Task DeleteSignatureAsync(int signatureId) + public async Task DeleteSignatureAsync(int signatureId, CancellationToken cancellationToken = default) { return await this.ProcessNoBodyRequestAsync - ("/senders/" + signatureId, verb: HttpMethod.Delete); + ("/senders/" + signatureId, verb: HttpMethod.Delete, cancellationToken: cancellationToken); } /// @@ -194,10 +195,10 @@ public async Task DeleteSignatureAsync(int signatureId) /// /// /// - public async Task ResendSignatureVerificationEmailAsync(int signatureId) + public async Task ResendSignatureVerificationEmailAsync(int signatureId, CancellationToken cancellationToken = default) { return await this.ProcessNoBodyRequestAsync - ("/senders/" + signatureId + "/resend", verb: HttpMethod.Post); + ("/senders/" + signatureId + "/resend", verb: HttpMethod.Post, cancellationToken: cancellationToken); } /// @@ -205,10 +206,10 @@ public async Task ResendSignatureVerificationEmailAsync(int si /// /// /// - public async Task RequestNewSignatureDKIMAsync(int signatureId) + public async Task RequestNewSignatureDKIMAsync(int signatureId, CancellationToken cancellationToken = default) { return await this.ProcessNoBodyRequestAsync - ("/senders/" + signatureId + "/requestnewdkim", verb: HttpMethod.Post); + ("/senders/" + signatureId + "/requestnewdkim", verb: HttpMethod.Post, cancellationToken: cancellationToken); } /// @@ -217,10 +218,10 @@ public async Task RequestNewSignatureDKIMAsync(int signatureId /// /// [Obsolete("We no longer require SPF verification. See here for details: https://postmarkapp.com/blog/why-we-no-longer-ask-for-spf-records")] - public async Task VerifySignatureSPF(int signatureId) + public async Task VerifySignatureSPF(int signatureId, CancellationToken cancellationToken = default) { return await this.ProcessNoBodyRequestAsync - ("/senders/" + signatureId + "/verifyspf", verb: HttpMethod.Post); + ("/senders/" + signatureId + "/verifyspf", verb: HttpMethod.Post, cancellationToken: cancellationToken); } /// @@ -233,7 +234,7 @@ public async Task VerifySignatureSPF(int signat /// /// /// - public async Task CreateSignatureAsync(string fromEmail, string name, string replyToEmail = null, string returnPathDomain = null, string confirmationPersonalNote = null) + public async Task CreateSignatureAsync(string fromEmail, string name, string replyToEmail = null, string returnPathDomain = null, string confirmationPersonalNote = null, CancellationToken cancellationToken = default) { var parameters = new Dictionary(); parameters["FromEmail"] = fromEmail; @@ -243,7 +244,7 @@ public async Task CreateSignatureAsync(string f parameters["ConfirmationPersonalNote"] = confirmationPersonalNote; return await this.ProcessRequestAsync, PostmarkCompleteSenderSignature> - ("/senders/", HttpMethod.Post, parameters); + ("/senders/", HttpMethod.Post, parameters, cancellationToken: cancellationToken); } /// @@ -256,7 +257,7 @@ public async Task CreateSignatureAsync(string f /// /// public async Task UpdateSignatureAsync - (int signatureId, string name = null, string replyToEmail = null, string returnPathDomain = null, string confirmationPersonalNote = null) + (int signatureId, string name = null, string replyToEmail = null, string returnPathDomain = null, string confirmationPersonalNote = null, CancellationToken cancellationToken = default) { var parameters = new Dictionary(); parameters["Name"] = name; @@ -265,7 +266,7 @@ public async Task UpdateSignatureAsync parameters["ConfirmationPersonalNote"] = confirmationPersonalNote; return await this.ProcessRequestAsync, PostmarkCompleteSenderSignature> - ("/senders/" + signatureId, HttpMethod.Put, parameters); + ("/senders/" + signatureId, HttpMethod.Put, parameters, cancellationToken: cancellationToken); } /// @@ -274,12 +275,12 @@ public async Task UpdateSignatureAsync /// /// /// - public async Task GetDomainsAsync(int offset = 0, int count = 100) + public async Task GetDomainsAsync(int offset = 0, int count = 100, CancellationToken cancellationToken = default) { var parameters = new Dictionary(); parameters["offset"] = offset; parameters["count"] = count; - return await this.ProcessNoBodyRequestAsync("/domains", parameters); + return await this.ProcessNoBodyRequestAsync("/domains", parameters, cancellationToken: cancellationToken); } @@ -288,9 +289,9 @@ public async Task GetDomainsAsync(int offset = 0, int count /// /// /// - public async Task GetDomainAsync(int domainId) + public async Task GetDomainAsync(int domainId, CancellationToken cancellationToken = default) { - return await this.ProcessNoBodyRequestAsync($"/domains/{domainId}"); + return await this.ProcessNoBodyRequestAsync($"/domains/{domainId}", cancellationToken: cancellationToken); } /// @@ -298,10 +299,10 @@ public async Task GetDomainAsync(int domainId) /// /// /// - public async Task DeleteDomainAsync(int domainId) + public async Task DeleteDomainAsync(int domainId, CancellationToken cancellationToken = default) { return await this.ProcessNoBodyRequestAsync - ("/domains/" + domainId, verb: HttpMethod.Delete); + ("/domains/" + domainId, verb: HttpMethod.Delete, cancellationToken: cancellationToken); } /// @@ -309,10 +310,10 @@ public async Task DeleteDomainAsync(int domainId) /// /// /// - public async Task RequestNewDomainDKIMAsync(int domainId) + public async Task RequestNewDomainDKIMAsync(int domainId, CancellationToken cancellationToken = default) { return await this.ProcessNoBodyRequestAsync - ("/domains/" + domainId + "/rotatedkim", verb: HttpMethod.Post); + ("/domains/" + domainId + "/rotatedkim", verb: HttpMethod.Post, cancellationToken: cancellationToken); } /// @@ -321,14 +322,14 @@ public async Task RequestNewDomainDKIMAsync(int domainId) /// /// /// - public async Task CreateDomainAsync(string name, string returnPathDomain = null) + public async Task CreateDomainAsync(string name, string returnPathDomain = null, CancellationToken cancellationToken = default) { var parameters = new Dictionary(); parameters["Name"] = name; parameters["ReturnPathDomain"] = returnPathDomain; return await this.ProcessRequestAsync, PostmarkCompleteDomain> - ("/domains/", HttpMethod.Post, parameters); + ("/domains/", HttpMethod.Post, parameters, cancellationToken: cancellationToken); } /// @@ -339,13 +340,13 @@ public async Task CreateDomainAsync(string name, string /// Setting this to null or empty will clear your Return-Path /// public async Task UpdateDomainAsync - (int domainId, string returnPathDomain) + (int domainId, string returnPathDomain, CancellationToken cancellationToken = default) { var parameters = new Dictionary(); parameters["ReturnPathDomain"] = returnPathDomain ?? ""; return await this.ProcessRequestAsync, PostmarkCompleteDomain> - ($"/domains/{domainId}", HttpMethod.Put, parameters); + ($"/domains/{domainId}", HttpMethod.Put, parameters, cancellationToken: cancellationToken); } /// @@ -353,10 +354,10 @@ public async Task UpdateDomainAsync /// /// /// - public async Task VerifyDomainDkim(int domainId) + public async Task VerifyDomainDkim(int domainId, CancellationToken cancellationToken = default) { return await this.ProcessNoBodyRequestAsync - ($"/domains/{domainId}/verifydkim", verb: HttpMethod.Put); + ($"/domains/{domainId}/verifydkim", verb: HttpMethod.Put, cancellationToken: cancellationToken); } /// @@ -364,10 +365,10 @@ public async Task VerifyDomainDkim(int domainId) /// /// /// - public async Task VerifyDomainReturnPath(int domainId) + public async Task VerifyDomainReturnPath(int domainId, CancellationToken cancellationToken = default) { return await this.ProcessNoBodyRequestAsync - ($"/domains/{domainId}/verifyreturnpath", verb: HttpMethod.Put); + ($"/domains/{domainId}/verifyreturnpath", verb: HttpMethod.Put, cancellationToken: cancellationToken); } @@ -378,14 +379,14 @@ public async Task VerifyDomainReturnPath(int domainId) /// /// /// - public async Task GetServersAsync(int offset = 0, int count = 100, string name = null) + public async Task GetServersAsync(int offset = 0, int count = 100, string name = null, CancellationToken cancellationToken = default) { var parameters = new Dictionary(); parameters["offset"] = offset; parameters["count"] = count; parameters["name"] = name; - return await this.ProcessNoBodyRequestAsync("/servers", parameters); + return await this.ProcessNoBodyRequestAsync("/servers", parameters, cancellationToken: cancellationToken); } /// @@ -395,14 +396,14 @@ public async Task GetServersAsync(int offset = 0, int count /// The email address of the recipient who's asking for their data to be removed. This must be a valid email address. /// Specifies whether the RequestedBy email address is notified when the data removal request is complete. /// - public async Task RequestDataRemoval(string requestedBy, string requestedFor, bool notifyWhenCompleted) + public async Task RequestDataRemoval(string requestedBy, string requestedFor, bool notifyWhenCompleted, CancellationToken cancellationToken = default) { var parameters = new Dictionary(); parameters["RequestedBy"] = requestedBy; parameters["RequestedFor"] = requestedFor; parameters["NotifyWhenCompleted"] = notifyWhenCompleted; - return await this.ProcessRequestAsync, PostmarkDataRemoval>("/data-removals", HttpMethod.Post, parameters); + return await this.ProcessRequestAsync, PostmarkDataRemoval>("/data-removals", HttpMethod.Post, parameters, cancellationToken: cancellationToken); } /// @@ -410,9 +411,9 @@ public async Task RequestDataRemoval(string requestedBy, st /// /// /// - public async Task GetDataRemovalStatus(long id) + public async Task GetDataRemovalStatus(long id, CancellationToken cancellationToken = default) { - return await this.ProcessNoBodyRequestAsync($"/data-removals/{id}"); + return await this.ProcessNoBodyRequestAsync($"/data-removals/{id}", cancellationToken: cancellationToken); } } } \ No newline at end of file diff --git a/src/Postmark/PostmarkClient.cs b/src/Postmark/PostmarkClient.cs index d7f7bde..ddb74c8 100644 --- a/src/Postmark/PostmarkClient.cs +++ b/src/Postmark/PostmarkClient.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Net.Http; using System.Text.Json; +using System.Threading; using System.Threading.Tasks; using Postmark.Model.MessageStreams; using Postmark.Model.Suppressions; @@ -49,9 +50,9 @@ public PostmarkClient(string serverToken, string apiBaseUri = "https://api.postm /// /// A prepared message. /// - public async Task SendMessageAsync(PostmarkMessage message) + public async Task SendMessageAsync(PostmarkMessage message, CancellationToken cancellationToken = default) { - return await ProcessRequestAsync("/email", HttpMethod.Post, message); + return await ProcessRequestAsync("/email", HttpMethod.Post, message, cancellationToken: cancellationToken); } /// @@ -65,7 +66,22 @@ public async Task SendMessageAsync(PostmarkMessage message) /// The processed messages (Complete with system assigned message IDs) public async Task> SendMessagesAsync(params PostmarkMessage[] messages) { - return await ProcessRequestAsync("/email/batch", HttpMethod.Post, messages); + return await SendMessagesAsync(CancellationToken.None, messages); + } + + /// + /// Sends a batch of up to 500 messages through the Postmark API, observing the provided cancellation token. + /// All email addresses must be valid, and the sender must be + /// a valid sender signature according to Postmark. To obtain a valid + /// sender signature, log in to Postmark and navigate to: + /// http://postmarkapp.com/signatures. + /// + /// Token used to cancel the pending request. + /// A prepared batch of messages. + /// The processed messages (Complete with system assigned message IDs) + public async Task> SendMessagesAsync(CancellationToken cancellationToken, params PostmarkMessage[] messages) + { + return await ProcessRequestAsync("/email/batch", HttpMethod.Post, messages, cancellationToken: cancellationToken); } /// @@ -80,9 +96,9 @@ public async Task> SendMessagesAsync(params Postma /// The initial status of the accepted bulk request, including the id /// () used to track its progress. /// - public async Task SendBulkEmailAsync(PostmarkBulkMessage message) + public async Task SendBulkEmailAsync(PostmarkBulkMessage message, CancellationToken cancellationToken = default) { - return await ProcessRequestAsync("/email/bulk", HttpMethod.Post, message); + return await ProcessRequestAsync("/email/bulk", HttpMethod.Post, message, cancellationToken: cancellationToken); } /// @@ -92,9 +108,9 @@ public async Task SendBulkEmailAsync(PostmarkBulkMessag /// The id returned from . /// The current status and completion progress of the bulk request. /// - public async Task GetBulkEmailStatusAsync(string bulkRequestId) + public async Task GetBulkEmailStatusAsync(string bulkRequestId, CancellationToken cancellationToken = default) { - return await ProcessNoBodyRequestAsync("/email/bulk/" + bulkRequestId); + return await ProcessNoBodyRequestAsync("/email/bulk/" + bulkRequestId, cancellationToken: cancellationToken); } #endregion @@ -105,9 +121,9 @@ public async Task GetBulkEmailStatusAsync(string bulkRe /// associated mail server. /// /// - public async Task GetDeliveryStatsAsync() + public async Task GetDeliveryStatsAsync(CancellationToken cancellationToken = default) { - return await this.ProcessNoBodyRequestAsync("/deliverystats"); + return await this.ProcessNoBodyRequestAsync("/deliverystats", cancellationToken: cancellationToken); } /// @@ -126,7 +142,7 @@ public async Task GetDeliveryStatsAsync() /// /// public async Task GetBouncesAsync(int offset = 0, int count = 100, PostmarkBounceType? type = null, - bool? inactive = null, string emailFilter = null, string tag = null, string messageID = null, string fromDate = null, string toDate = null) + bool? inactive = null, string emailFilter = null, string tag = null, string messageID = null, string fromDate = null, string toDate = null, CancellationToken cancellationToken = default) { var parameters = new Dictionary(); parameters["type"] = type; @@ -139,7 +155,7 @@ public async Task GetBouncesAsync(int offset = 0, int count = 1 parameters["fromDate"] = fromDate; parameters["toDate"] = toDate; - return await ProcessNoBodyRequestAsync("/bounces", parameters); + return await ProcessNoBodyRequestAsync("/bounces", parameters, cancellationToken: cancellationToken); } /// @@ -148,9 +164,9 @@ public async Task GetBouncesAsync(int offset = 0, int count = 1 /// The bounce ID of the bounce to retrieve. /// /// - public async Task GetBounceAsync(long bounceId) + public async Task GetBounceAsync(long bounceId, CancellationToken cancellationToken = default) { - return await ProcessNoBodyRequestAsync("/bounces/" + bounceId); + return await ProcessNoBodyRequestAsync("/bounces/" + bounceId, cancellationToken: cancellationToken); } /// @@ -160,9 +176,9 @@ public async Task GetBounceAsync(long bounceId) /// The bounce ID of the bounce dump to retrieve. /// /// - public async Task GetBounceDumpAsync(long bounceId) + public async Task GetBounceDumpAsync(long bounceId, CancellationToken cancellationToken = default) { - return await ProcessNoBodyRequestAsync("/bounces/" + bounceId + "/dump"); + return await ProcessNoBodyRequestAsync("/bounces/" + bounceId + "/dump", cancellationToken: cancellationToken); } /// @@ -171,9 +187,9 @@ public async Task GetBounceDumpAsync(long bounceId) /// The bounce ID of the bounce to Activate /// /// - public async Task ActivateBounceAsync(long bounceId) + public async Task ActivateBounceAsync(long bounceId, CancellationToken cancellationToken = default) { - return await ProcessNoBodyRequestAsync("/bounces/" + bounceId + "/activate", verb: HttpMethod.Put); + return await ProcessNoBodyRequestAsync("/bounces/" + bounceId + "/activate", verb: HttpMethod.Put, cancellationToken: cancellationToken); } #endregion @@ -195,7 +211,7 @@ public async Task ActivateBounceAsync(long bounceId) /// PostmarkOutboundMessageList public async Task GetOutboundMessagesAsync(int offset = 0, int count = 100, string recipient = null, string fromemail = null, string tag = null, string subject = null, - OutboundMessageStatus status = OutboundMessageStatus.Sent, string toDate = null, string fromDate = null, IDictionary metadata = null, string messagestream = null) + OutboundMessageStatus status = OutboundMessageStatus.Sent, string toDate = null, string fromDate = null, IDictionary metadata = null, string messagestream = null, CancellationToken cancellationToken = default) { var parameters = new Dictionary(); parameters["count"] = count; @@ -217,7 +233,7 @@ public async Task GetOutboundMessagesAsync(int offs } } - return await ProcessNoBodyRequestAsync("/messages/outbound", parameters); + return await ProcessNoBodyRequestAsync("/messages/outbound", parameters, cancellationToken: cancellationToken); } /// @@ -225,9 +241,9 @@ public async Task GetOutboundMessagesAsync(int offs /// /// The MessageID of a message which can be optained either from the initial API send call or a GetOutboundMessages call. /// OutboundMessageDetail - public async Task GetOutboundMessageDetailsAsync(string messageID) + public async Task GetOutboundMessageDetailsAsync(string messageID, CancellationToken cancellationToken = default) { - return await ProcessNoBodyRequestAsync("/messages/outbound/" + messageID + "/details"); + return await ProcessNoBodyRequestAsync("/messages/outbound/" + messageID + "/details", cancellationToken: cancellationToken); } /// @@ -235,9 +251,9 @@ public async Task GetOutboundMessageDetailsAsync(string m /// /// The MessageID of a message which can be optained either from the initial API send call or a GetOutboundMessages call. /// MessageDump - public async Task GetOutboundMessageDumpAsync(string messageID) + public async Task GetOutboundMessageDumpAsync(string messageID, CancellationToken cancellationToken = default) { - return await ProcessNoBodyRequestAsync("/messages/outbound/" + messageID + "/dump"); + return await ProcessNoBodyRequestAsync("/messages/outbound/" + messageID + "/dump", cancellationToken: cancellationToken); } #endregion @@ -258,7 +274,7 @@ public async Task GetOutboundMessageDumpAsync(string messageID) /// PostmarkInboundMessageList public async Task GetInboundMessagesAsync(int offset = 0, int count = 100, string recipient = null, string fromemail = null, string subject = null, - string mailboxhash = null, InboundMessageStatus? status = InboundMessageStatus.Processed, String toDate = null, String fromDate = null) + string mailboxhash = null, InboundMessageStatus? status = InboundMessageStatus.Processed, String toDate = null, String fromDate = null, CancellationToken cancellationToken = default) { var parameters = new Dictionary(); parameters["count"] = count; @@ -271,7 +287,7 @@ public async Task GetInboundMessagesAsync(int offset parameters["fromdate"] = fromDate; parameters["status"] = status.ToString().ToLower(); - return await ProcessNoBodyRequestAsync("/messages/inbound", parameters); + return await ProcessNoBodyRequestAsync("/messages/inbound", parameters, cancellationToken: cancellationToken); } /// @@ -279,9 +295,9 @@ public async Task GetInboundMessagesAsync(int offset /// /// The MessageID of a message which can be optained either from the initial API send call or a GetInboundMessages call. /// InboundMessageDetail - public async Task GetInboundMessageDetailsAsync(string messageID) + public async Task GetInboundMessageDetailsAsync(string messageID, CancellationToken cancellationToken = default) { - return await ProcessNoBodyRequestAsync("/messages/inbound/" + messageID + "/details"); + return await ProcessNoBodyRequestAsync("/messages/inbound/" + messageID + "/details", cancellationToken: cancellationToken); } /// @@ -289,9 +305,9 @@ public async Task GetInboundMessageDetailsAsync(string m /// /// /// - public async Task BypassBlockedInboundMessage(string messageid) + public async Task BypassBlockedInboundMessage(string messageid, CancellationToken cancellationToken = default) { - return await this.ProcessNoBodyRequestAsync(String.Format("/messages/inbound/{0}/bypass", messageid), verb: HttpMethod.Put); + return await this.ProcessNoBodyRequestAsync(String.Format("/messages/inbound/{0}/bypass", messageid), verb: HttpMethod.Put, cancellationToken: cancellationToken); } /// @@ -299,9 +315,9 @@ public async Task BypassBlockedInboundMessage(string messageid /// /// /// - public async Task RetryInboundHookForMessage(string messageId) + public async Task RetryInboundHookForMessage(string messageId, CancellationToken cancellationToken = default) { - return await this.ProcessNoBodyRequestAsync(String.Format("/messages/inbound/{0}/retry", messageId), verb: HttpMethod.Put); + return await this.ProcessNoBodyRequestAsync(String.Format("/messages/inbound/{0}/retry", messageId), verb: HttpMethod.Put, cancellationToken: cancellationToken); } @@ -314,9 +330,9 @@ public async Task RetryInboundHookForMessage(string messageId) /// the ServerToken supplied when the client was constructed. /// /// - public async Task GetServerAsync() + public async Task GetServerAsync(CancellationToken cancellationToken = default) { - return await this.ProcessNoBodyRequestAsync("/server"); + return await this.ProcessNoBodyRequestAsync("/server", cancellationToken: cancellationToken); } /// @@ -330,7 +346,7 @@ public async Task EditServerAsync( string bounceHookUrl = null, string openHookUrl = null, bool? postFirstOpenOnly = null, bool? trackOpens = null, string inboundDomain = null, int? inboundSpamThreshold = null, LinkTrackingOptions? trackLinks = null, - string clickHookUrl = null, string deliveryHookUrl = null) + string clickHookUrl = null, string deliveryHookUrl = null, CancellationToken cancellationToken = default) { var body = new Dictionary(); body["Name"] = name; @@ -350,7 +366,7 @@ public async Task EditServerAsync( body = body.Where(kv => kv.Value != null).ToDictionary(k => k.Key, v => v.Value); - return await this.ProcessRequestAsync, PostmarkServer>("/server", HttpMethod.Put, body); + return await this.ProcessRequestAsync, PostmarkServer>("/server", HttpMethod.Put, body, cancellationToken: cancellationToken); } #endregion @@ -406,7 +422,7 @@ public async Task GetOpenEventsForMessagesAsync( int offset = 0, int count = 100, string recipient = null, string tag = null, string clientName = null, string clientCompany = null, string clientFamily = null, string operatingSystemName = null, string operatingSystemFamily = null, string operatingSystemCompany = null, - string platform = null, string country = null, string region = null, string city = null) + string platform = null, string country = null, string region = null, string city = null, CancellationToken cancellationToken = default) { var parameters = new Dictionary(); parameters["offset"] = offset; @@ -425,7 +441,7 @@ public async Task GetOpenEventsForMessagesAsync( parameters["city"] = city; return await this - .ProcessNoBodyRequestAsync("/messages/outbound/opens", parameters); + .ProcessNoBodyRequestAsync("/messages/outbound/opens", parameters, cancellationToken: cancellationToken); } @@ -452,7 +468,7 @@ public async Task GetClickEventsForMessagesAsync( int offset = 0, int count = 100, string recipient = null, string tag = null, string clientName = null, string clientCompany = null, string clientFamily = null, string operatingSystemName = null, string operatingSystemFamily = null, string operatingSystemCompany = null, - string platform = null, string country = null, string region = null, string city = null) + string platform = null, string country = null, string region = null, string city = null, CancellationToken cancellationToken = default) { var parameters = new Dictionary(); parameters["offset"] = offset; @@ -471,7 +487,7 @@ public async Task GetClickEventsForMessagesAsync( parameters["city"] = city; return await this - .ProcessNoBodyRequestAsync("/messages/outbound/clicks", parameters); + .ProcessNoBodyRequestAsync("/messages/outbound/clicks", parameters, cancellationToken: cancellationToken); } @@ -483,7 +499,7 @@ public async Task GetClickEventsForMessagesAsync( /// /// public async Task GetOpenEventsForMessageAsync( - string messageId, int offset = 0, int count = 100) + string messageId, int offset = 0, int count = 100, CancellationToken cancellationToken = default) { var parameters = new Dictionary(); @@ -491,7 +507,7 @@ public async Task GetOpenEventsForMessageAsync( parameters["count"] = count; return await this.ProcessNoBodyRequestAsync - (String.Format("/messages/outbound/opens/{0}", messageId), parameters); + (String.Format("/messages/outbound/opens/{0}", messageId), parameters, cancellationToken: cancellationToken); } /// @@ -502,7 +518,7 @@ public async Task GetOpenEventsForMessageAsync( /// /// public async Task GetClickEventsForMessageAsync( - string messageId, int offset = 0, int count = 100) + string messageId, int offset = 0, int count = 100, CancellationToken cancellationToken = default) { var parameters = new Dictionary(); @@ -510,7 +526,7 @@ public async Task GetClickEventsForMessageAsync( parameters["count"] = count; return await this.ProcessNoBodyRequestAsync - (String.Format("/messages/outbound/clicks/{0}", messageId), parameters); + (String.Format("/messages/outbound/clicks/{0}", messageId), parameters, cancellationToken: cancellationToken); } /// @@ -521,11 +537,11 @@ public async Task GetClickEventsForMessageAsync( /// /// public async Task GetOutboundOverviewStatsAsync( - string tag = null, DateTime? fromDate = null, DateTime? toDate = null) + string tag = null, DateTime? fromDate = null, DateTime? toDate = null, CancellationToken cancellationToken = default) { var parameters = ConstructSentStatsFilter(tag, fromDate, toDate); return await this.ProcessNoBodyRequestAsync - ("/stats/outbound", parameters); + ("/stats/outbound", parameters, cancellationToken: cancellationToken); } /// @@ -536,11 +552,11 @@ public async Task GetOutboundOverviewStatsAsync( /// /// public async Task - GetOutboundSentCountsAsync(string tag = null, DateTime? fromDate = null, DateTime? toDate = null) + GetOutboundSentCountsAsync(string tag = null, DateTime? fromDate = null, DateTime? toDate = null, CancellationToken cancellationToken = default) { var parameters = ConstructSentStatsFilter(tag, fromDate, toDate); return await this.ProcessNoBodyRequestAsync - ("/stats/outbound/sends", parameters); + ("/stats/outbound/sends", parameters, cancellationToken: cancellationToken); } /// @@ -551,11 +567,11 @@ public async Task /// /// public async Task - GetOutboundBounceCountsAsync(string tag = null, DateTime? fromDate = null, DateTime? toDate = null) + GetOutboundBounceCountsAsync(string tag = null, DateTime? fromDate = null, DateTime? toDate = null, CancellationToken cancellationToken = default) { var parameters = ConstructSentStatsFilter(tag, fromDate, toDate); return await this.ProcessNoBodyRequestAsync - ("/stats/outbound/bounces", parameters); + ("/stats/outbound/bounces", parameters, cancellationToken: cancellationToken); } /// @@ -565,11 +581,11 @@ public async Task /// /// /// - public async Task GetOutboundSpamComplaintCountsAsync(string tag = null, DateTime? fromDate = null, DateTime? toDate = null) + public async Task GetOutboundSpamComplaintCountsAsync(string tag = null, DateTime? fromDate = null, DateTime? toDate = null, CancellationToken cancellationToken = default) { var parameters = ConstructSentStatsFilter(tag, fromDate, toDate); return await this.ProcessNoBodyRequestAsync - ("/stats/outbound/spam", parameters); + ("/stats/outbound/spam", parameters, cancellationToken: cancellationToken); } /// @@ -579,11 +595,11 @@ public async Task GetOutboundSpamComplaintCo /// /// /// - public async Task GetOutboundTrackingCountsAsync(string tag = null, DateTime? fromDate = null, DateTime? toDate = null) + public async Task GetOutboundTrackingCountsAsync(string tag = null, DateTime? fromDate = null, DateTime? toDate = null, CancellationToken cancellationToken = default) { var parameters = ConstructSentStatsFilter(tag, fromDate, toDate); return await this.ProcessNoBodyRequestAsync - ("/stats/outbound/tracked", parameters); + ("/stats/outbound/tracked", parameters, cancellationToken: cancellationToken); } /// @@ -593,11 +609,11 @@ public async Task GetOutboundTrackingCountsAsync(s /// /// /// - public async Task GetOutboundOpenCountsAsync(string tag = null, DateTime? fromDate = null, DateTime? toDate = null) + public async Task GetOutboundOpenCountsAsync(string tag = null, DateTime? fromDate = null, DateTime? toDate = null, CancellationToken cancellationToken = default) { var parameters = ConstructSentStatsFilter(tag, fromDate, toDate); return await this.ProcessNoBodyRequestAsync - ("/stats/outbound/opens", parameters); + ("/stats/outbound/opens", parameters, cancellationToken: cancellationToken); } /// @@ -607,11 +623,11 @@ public async Task GetOutboundOpenCountsAsync(string t /// /// /// - public async Task GetOutboundPlatformCountsAsync(string tag = null, DateTime? fromDate = null, DateTime? toDate = null) + public async Task GetOutboundPlatformCountsAsync(string tag = null, DateTime? fromDate = null, DateTime? toDate = null, CancellationToken cancellationToken = default) { var parameters = ConstructSentStatsFilter(tag, fromDate, toDate); return await this.ProcessNoBodyRequestAsync - ("/stats/outbound/opens/platforms", parameters); + ("/stats/outbound/opens/platforms", parameters, cancellationToken: cancellationToken); } /// @@ -621,10 +637,10 @@ public async Task GetOutboundPlatformCountsAsync( /// /// /// - public async Task GetOutboundClientUsageCountsAsync(string tag = null, DateTime? fromDate = null, DateTime? toDate = null) + public async Task GetOutboundClientUsageCountsAsync(string tag = null, DateTime? fromDate = null, DateTime? toDate = null, CancellationToken cancellationToken = default) { var parameters = ConstructSentStatsFilter(tag, fromDate, toDate); - var result = await this.ProcessNoBodyRequestAsync>("/stats/outbound/opens/emailclients", parameters); + var result = await this.ProcessNoBodyRequestAsync>("/stats/outbound/opens/emailclients", parameters, cancellationToken: cancellationToken); var retval = new PostmarkOutboundClientStats(); var clientCounts = new Dictionary(); @@ -667,10 +683,10 @@ public async Task GetOutboundClientUsageCountsAsync /// /// /// - public async Task GetOutboundReadtimeStatsAsync(string tag = null, DateTime? fromDate = null, DateTime? toDate = null) + public async Task GetOutboundReadtimeStatsAsync(string tag = null, DateTime? fromDate = null, DateTime? toDate = null, CancellationToken cancellationToken = default) { var parameters = ConstructSentStatsFilter(tag, fromDate, toDate); - var result = await this.ProcessNoBodyRequestAsync>("/stats/outbound/opens/readtimes", parameters); + var result = await this.ProcessNoBodyRequestAsync>("/stats/outbound/opens/readtimes", parameters, cancellationToken: cancellationToken); var retval = new PostmarkOutboundReadStats(); var clientCounts = new Dictionary(); @@ -714,13 +730,13 @@ public async Task GetOutboundReadtimeStatsAsync(strin /// /// /// - public async Task CreateInboundRuleTriggerAsync(string rule) + public async Task CreateInboundRuleTriggerAsync(string rule, CancellationToken cancellationToken = default) { var parameters = new Dictionary(); parameters["Rule"] = rule; return await this.ProcessRequestAsync, PostmarkInboundRuleTriggerInfo> - ("/triggers/inboundrules", HttpMethod.Post, parameters); + ("/triggers/inboundrules", HttpMethod.Post, parameters, cancellationToken: cancellationToken); } /// @@ -728,11 +744,11 @@ public async Task CreateInboundRuleTriggerAsync( /// /// /// - public async Task DeleteInboundRuleTrigger(int triggerId) + public async Task DeleteInboundRuleTrigger(int triggerId, CancellationToken cancellationToken = default) { return await this .ProcessNoBodyRequestAsync("/triggers/inboundrules/" + triggerId, - verb: HttpMethod.Delete); + verb: HttpMethod.Delete, cancellationToken: cancellationToken); } /// @@ -741,13 +757,13 @@ public async Task DeleteInboundRuleTrigger(int triggerId) /// /// /// - public async Task GetAllInboundRuleTriggers(int offset = 0, int count = 100) + public async Task GetAllInboundRuleTriggers(int offset = 0, int count = 100, CancellationToken cancellationToken = default) { var parameters = new Dictionary(); parameters["offset"] = offset; parameters["count"] = count; - return await this.ProcessNoBodyRequestAsync("/triggers/inboundrules", parameters); + return await this.ProcessNoBodyRequestAsync("/triggers/inboundrules", parameters, cancellationToken: cancellationToken); } #endregion @@ -759,9 +775,9 @@ public async Task GetAllInboundRuleTriggers(int /// /// The ID of the template you wish to retrive. /// - public async Task GetTemplateAsync(long templateId) + public async Task GetTemplateAsync(long templateId, CancellationToken cancellationToken = default) { - return await ProcessNoBodyRequestAsync("/templates/" + templateId, null, HttpMethod.Get); + return await ProcessNoBodyRequestAsync("/templates/" + templateId, null, HttpMethod.Get, cancellationToken: cancellationToken); } /// @@ -769,9 +785,9 @@ public async Task GetTemplateAsync(long templateId) /// /// The alias of the template you wish to retrive. /// - public async Task GetTemplateAsync(string alias) + public async Task GetTemplateAsync(string alias, CancellationToken cancellationToken = default) { - return await ProcessNoBodyRequestAsync("/templates/" + alias, null, HttpMethod.Get); + return await ProcessNoBodyRequestAsync("/templates/" + alias, null, HttpMethod.Get, cancellationToken: cancellationToken); } /// @@ -783,7 +799,7 @@ public async Task GetTemplateAsync(string alias) /// Filter results by layout template alias. /// public async Task GetTemplatesAsync(int offset = 0, int count = 100, TemplateTypeFilter templateType = TemplateTypeFilter.All, - string layoutTemplate = null) + string layoutTemplate = null, CancellationToken cancellationToken = default) { var query = new Dictionary(); query["Count"] = count; @@ -791,7 +807,7 @@ public async Task GetTemplatesAsync(int offset query["TemplateType"] = Enum.GetName(typeof(TemplateTypeFilter), templateType); query["LayoutTemplate"] = layoutTemplate; - return await ProcessNoBodyRequestAsync("/templates/", query, HttpMethod.Get); + return await ProcessNoBodyRequestAsync("/templates/", query, HttpMethod.Get, cancellationToken: cancellationToken); } /// @@ -806,7 +822,7 @@ public async Task GetTemplatesAsync(int offset /// The alias of the Layout template that you want to use as layout for this Standard template. /// public async Task CreateTemplateAsync(string name, string subject, string htmlBody = null, string textBody = null, string alias = null, - TemplateType templateType = TemplateType.Standard, string layoutTemplate = null) + TemplateType templateType = TemplateType.Standard, string layoutTemplate = null, CancellationToken cancellationToken = default) { var body = new Dictionary(); body["Name"] = name; @@ -817,11 +833,11 @@ public async Task CreateTemplateAsync(string name, str body["TemplateType"] = Enum.GetName(typeof(TemplateType), templateType); body["LayoutTemplate"] = layoutTemplate; - return await ProcessRequestAsync, BasicTemplateInformation>("/templates/", HttpMethod.Post, body); + return await ProcessRequestAsync, BasicTemplateInformation>("/templates/", HttpMethod.Post, body, cancellationToken: cancellationToken); } public async Task EditTemplateAsync(string alias, string name = null, string subject = null, string htmlBody = null, string textBody = null, - string layoutTemplate = null) + string layoutTemplate = null, CancellationToken cancellationToken = default) { var body = new Dictionary(); body["Name"] = name; @@ -830,11 +846,11 @@ public async Task EditTemplateAsync(string alias, stri body["Subject"] = subject; body["LayoutTemplate"] = layoutTemplate; - return await ProcessRequestAsync, BasicTemplateInformation>("/templates/" + alias, HttpMethod.Put, body); + return await ProcessRequestAsync, BasicTemplateInformation>("/templates/" + alias, HttpMethod.Put, body, cancellationToken: cancellationToken); } public async Task EditTemplateAsync(long templateId, string name = null, string subject = null, string htmlBody = null, string textBody = null, string alias = null, - string layoutTemplate = null) + string layoutTemplate = null, CancellationToken cancellationToken = default) { var body = new Dictionary(); body["Name"] = name; @@ -844,7 +860,7 @@ public async Task EditTemplateAsync(long templateId, s body["Alias"] = alias; body["LayoutTemplate"] = layoutTemplate; - return await ProcessRequestAsync, BasicTemplateInformation>("/templates/" + templateId, HttpMethod.Put, body); + return await ProcessRequestAsync, BasicTemplateInformation>("/templates/" + templateId, HttpMethod.Put, body, cancellationToken: cancellationToken); } /// @@ -852,9 +868,9 @@ public async Task EditTemplateAsync(long templateId, s /// /// The ID of the template you wish to delete from the server. /// - public async Task DeleteTemplateAsync(long templateId) + public async Task DeleteTemplateAsync(long templateId, CancellationToken cancellationToken = default) { - return await ProcessNoBodyRequestAsync("/templates/" + templateId, null, HttpMethod.Delete); + return await ProcessNoBodyRequestAsync("/templates/" + templateId, null, HttpMethod.Delete, cancellationToken: cancellationToken); } /// @@ -862,19 +878,19 @@ public async Task DeleteTemplateAsync(long templateId) /// /// The Alias of the template you wish to delete from the server. /// - public async Task DeleteTemplateAsync(string templateAlias) + public async Task DeleteTemplateAsync(string templateAlias, CancellationToken cancellationToken = default) { - return await ProcessNoBodyRequestAsync("/templates/" + templateAlias, null, HttpMethod.Delete); + return await ProcessNoBodyRequestAsync("/templates/" + templateAlias, null, HttpMethod.Delete, cancellationToken: cancellationToken); } - public async Task SendMessageAsync(TemplatedPostmarkMessage emailToSend) + public async Task SendMessageAsync(TemplatedPostmarkMessage emailToSend, CancellationToken cancellationToken = default) { - return await SendEmailWithTemplateAsync(emailToSend); + return await SendEmailWithTemplateAsync(emailToSend, cancellationToken); } - public async Task SendEmailWithTemplateAsync(TemplatedPostmarkMessage emailToSend) + public async Task SendEmailWithTemplateAsync(TemplatedPostmarkMessage emailToSend, CancellationToken cancellationToken = default) { - return await ProcessRequestAsync("/email/withTemplate", HttpMethod.Post, emailToSend); + return await ProcessRequestAsync("/email/withTemplate", HttpMethod.Post, emailToSend, cancellationToken: cancellationToken); } /// @@ -886,7 +902,20 @@ public async Task SendEmailWithTemplateAsync(TemplatedPostmark /// The processed messages (Complete with system assigned message IDs) public async Task> SendMessagesAsync(params TemplatedPostmarkMessage[] messages) { - return await SendEmailsWithTemplateAsync(messages); + return await SendEmailsWithTemplateAsync(CancellationToken.None, messages); + } + + /// + /// Sends a batch of up to 500 templated messages through the Postmark API, observing the provided cancellation token. + /// All email addresses must be valid, and the sender must be a valid sender signature according to Postmark. + /// Either the TemplateId or the TemplateAlias must be provided for each TemplatedPostmarkMessage. + /// + /// Token used to cancel the pending request. + /// A prepared batch of templated messages. + /// The processed messages (Complete with system assigned message IDs) + public async Task> SendMessagesAsync(CancellationToken cancellationToken, params TemplatedPostmarkMessage[] messages) + { + return await SendEmailsWithTemplateAsync(cancellationToken, messages); } /// @@ -897,10 +926,23 @@ public async Task> SendMessagesAsync(params Templa /// A prepared batch of templated messages. /// The processed messages (Complete with system assigned message IDs) public async Task> SendEmailsWithTemplateAsync(params TemplatedPostmarkMessage[] messages) + { + return await SendEmailsWithTemplateAsync(CancellationToken.None, messages); + } + + /// + /// Sends a batch of up to 500 templated messages through the Postmark API, observing the provided cancellation token. + /// All email addresses must be valid, and the sender must be a valid sender signature according to Postmark. + /// Either the TemplateId or the TemplateAlias must be provided for each TemplatedPostmarkMessage. + /// + /// Token used to cancel the pending request. + /// A prepared batch of templated messages. + /// The processed messages (Complete with system assigned message IDs) + public async Task> SendEmailsWithTemplateAsync(CancellationToken cancellationToken, params TemplatedPostmarkMessage[] messages) { var body = new Dictionary { ["Messages"] = messages.ToList() }; - return await ProcessRequestAsync, PostmarkResponse[]>("/email/batchWithTemplates", HttpMethod.Post, body); + return await ProcessRequestAsync, PostmarkResponse[]>("/email/batchWithTemplates", HttpMethod.Post, body, cancellationToken: cancellationToken); } public async Task SendEmailWithTemplateAsync(string templateAlias, T templateModel, @@ -914,7 +956,21 @@ public async Task SendEmailWithTemplateAsync(string templat params PostmarkMessageAttachment[] attachments) { return await InternalSendEmailWithTemplateAsync(templateAlias, templateModel, to, from, inlineCss, cc, - bcc, replyTo, trackOpens, headers, metadata, messageStream, attachments); + bcc, replyTo, trackOpens, headers, metadata, messageStream, CancellationToken.None, attachments); + } + + public async Task SendEmailWithTemplateAsync(CancellationToken cancellationToken, string templateAlias, T templateModel, + string to, string from, + bool? inlineCss = null, string cc = null, + string bcc = null, string replyTo = null, + bool? trackOpens = null, + IDictionary headers = null, + IDictionary metadata = null, + string messageStream = null, + params PostmarkMessageAttachment[] attachments) + { + return await InternalSendEmailWithTemplateAsync(templateAlias, templateModel, to, from, inlineCss, cc, + bcc, replyTo, trackOpens, headers, metadata, messageStream, cancellationToken, attachments); } public async Task SendEmailWithTemplateAsync(long templateId, T templateModel, @@ -928,7 +984,21 @@ public async Task SendEmailWithTemplateAsync(long templateI params PostmarkMessageAttachment[] attachments) { return await InternalSendEmailWithTemplateAsync(templateId, templateModel, to, from, inlineCss, cc, - bcc, replyTo, trackOpens, headers, metadata, messageStream, attachments); + bcc, replyTo, trackOpens, headers, metadata, messageStream, CancellationToken.None, attachments); + } + + public async Task SendEmailWithTemplateAsync(CancellationToken cancellationToken, long templateId, T templateModel, + string to, string from, + bool? inlineCss = null, string cc = null, + string bcc = null, string replyTo = null, + bool? trackOpens = null, + IDictionary headers = null, + IDictionary metadata = null, + string messageStream = null, + params PostmarkMessageAttachment[] attachments) + { + return await InternalSendEmailWithTemplateAsync(templateId, templateModel, to, from, inlineCss, cc, + bcc, replyTo, trackOpens, headers, metadata, messageStream, cancellationToken, attachments); } private async Task InternalSendEmailWithTemplateAsync(object templateReference, T templateModel, @@ -939,6 +1009,7 @@ private async Task InternalSendEmailWithTemplateAsync(objec IDictionary headers = null, IDictionary metadata = null, string messageStream = null, + CancellationToken cancellationToken = default, params PostmarkMessageAttachment[] attachments) { @@ -975,7 +1046,7 @@ private async Task InternalSendEmailWithTemplateAsync(objec { email.Attachments = attachments; } - return await SendEmailWithTemplateAsync(email); + return await SendEmailWithTemplateAsync(email, cancellationToken); } /// @@ -992,7 +1063,7 @@ private async Task InternalSendEmailWithTemplateAsync(objec /// public async Task ValidateTemplateAsync(string subject = null, string htmlBody = null, string textBody = null, T testRenderModel = default(T), bool inlineCssForHtmlTestRender = true, - TemplateType templateType = TemplateType.Standard, string layoutTemplate = null) + TemplateType templateType = TemplateType.Standard, string layoutTemplate = null, CancellationToken cancellationToken = default) { var body = new Dictionary(); body["TestRenderModel"] = testRenderModel; @@ -1003,7 +1074,7 @@ public async Task ValidateTemplateAsync(string su body["TemplateType"] = Enum.GetName(typeof(TemplateType), templateType); body["LayoutTemplate"] = layoutTemplate; - return await ProcessRequestAsync, TemplateValidationResponse>("/templates/validate", HttpMethod.Post, body); + return await ProcessRequestAsync, TemplateValidationResponse>("/templates/validate", HttpMethod.Post, body, cancellationToken: cancellationToken); } #endregion @@ -1014,9 +1085,9 @@ public async Task ValidateTemplateAsync(string su /// /// Configuration Id to search for /// - public async Task GetWebhookConfigurationAsync(long configurationId) + public async Task GetWebhookConfigurationAsync(long configurationId, CancellationToken cancellationToken = default) { - return await ProcessNoBodyRequestAsync($"/webhooks/{configurationId}", null, HttpMethod.Get); + return await ProcessNoBodyRequestAsync($"/webhooks/{configurationId}", null, HttpMethod.Get, cancellationToken: cancellationToken); } /// @@ -1025,11 +1096,11 @@ public async Task GetWebhookConfigurationAsync(long config /// Optional message stream to search for. /// If not provided, all configurations for the server will be returned. /// - public async Task GetWebhookConfigurationsAsync(string messageStream = null) + public async Task GetWebhookConfigurationsAsync(string messageStream = null, CancellationToken cancellationToken = default) { var query = new Dictionary { ["MessageStream"] = messageStream }; - return await ProcessNoBodyRequestAsync("/webhooks/", query, HttpMethod.Get); + return await ProcessNoBodyRequestAsync("/webhooks/", query, HttpMethod.Get, cancellationToken: cancellationToken); } /// @@ -1037,9 +1108,9 @@ public async Task GetWebhookConfigurationsA /// /// Configuration id to search for /// - public async Task DeleteWebhookConfigurationAsync(long configurationId) + public async Task DeleteWebhookConfigurationAsync(long configurationId, CancellationToken cancellationToken = default) { - return await ProcessNoBodyRequestAsync($"/webhooks/{configurationId}", null, HttpMethod.Delete); + return await ProcessNoBodyRequestAsync($"/webhooks/{configurationId}", null, HttpMethod.Delete, cancellationToken: cancellationToken); } /// @@ -1053,7 +1124,7 @@ public async Task DeleteWebhookConfigurationAsync(long configu /// Optional triggers for this webhook configuration /// public async Task CreateWebhookConfigurationAsync(string url, string messageStream = null, - HttpAuth httpAuth = null, IEnumerable httpHeaders = null, WebhookConfigurationTriggers triggers = null) + HttpAuth httpAuth = null, IEnumerable httpHeaders = null, WebhookConfigurationTriggers triggers = null, CancellationToken cancellationToken = default) { var body = new Dictionary(); body["Url"] = url; @@ -1062,7 +1133,7 @@ public async Task CreateWebhookConfigurationAsync(string u body["HttpHeaders"] = httpHeaders; body["Triggers"] = triggers; - return await ProcessRequestAsync, WebhookConfiguration>("/webhooks/", HttpMethod.Post, body); + return await ProcessRequestAsync, WebhookConfiguration>("/webhooks/", HttpMethod.Post, body, cancellationToken: cancellationToken); } /// @@ -1075,7 +1146,7 @@ public async Task CreateWebhookConfigurationAsync(string u /// Optional triggers for this webhook configuration /// public async Task EditWebhookConfigurationAsync(long configurationId, string url, - HttpAuth httpAuth = null, IEnumerable httpHeaders = null, WebhookConfigurationTriggers triggers = null) + HttpAuth httpAuth = null, IEnumerable httpHeaders = null, WebhookConfigurationTriggers triggers = null, CancellationToken cancellationToken = default) { var body = new Dictionary(); body["Url"] = url; @@ -1084,7 +1155,7 @@ public async Task EditWebhookConfigurationAsync(long confi body["Triggers"] = triggers; return await ProcessRequestAsync, WebhookConfiguration>($"/webhooks/{configurationId}", - HttpMethod.Put, body); + HttpMethod.Put, body, cancellationToken: cancellationToken); } #endregion @@ -1096,7 +1167,7 @@ public async Task EditWebhookConfigurationAsync(long confi /// Set of query parameters used to filter Suppressions. /// MessageStream used to search for Suppressions. Defaults to "outbound". /// Listing of active Suppressions matching the provided query parameters. - public async Task ListSuppressions(PostmarkSuppressionQuery query, string messageStream = DefaultTransactionalStream) + public async Task ListSuppressions(PostmarkSuppressionQuery query, string messageStream = DefaultTransactionalStream, CancellationToken cancellationToken = default) { var parameters = new Dictionary { @@ -1109,7 +1180,7 @@ public async Task ListSuppressions(PostmarkSuppressi var apiUrl = $"/message-streams/{messageStream}/suppressions/dump"; - return await ProcessNoBodyRequestAsync(apiUrl, parameters); + return await ProcessNoBodyRequestAsync(apiUrl, parameters, cancellationToken: cancellationToken); } /// @@ -1120,13 +1191,13 @@ public async Task ListSuppressions(PostmarkSuppressi /// Suppressions will be generated with a Customer Origin and will have a ManualSuppression reason. /// The status of the request for each recipient. public async Task CreateSuppressions(IEnumerable suppressionChanges, - string messageStream = DefaultTransactionalStream) + string messageStream = DefaultTransactionalStream, CancellationToken cancellationToken = default) { var body = new Dictionary { ["Suppressions"] = suppressionChanges.ToList() }; var apiUrl = $"/message-streams/{messageStream}/suppressions"; - return await ProcessRequestAsync, PostmarkBulkSuppressionResult>(apiUrl, HttpMethod.Post, body); + return await ProcessRequestAsync, PostmarkBulkSuppressionResult>(apiUrl, HttpMethod.Post, body, cancellationToken: cancellationToken); } /// @@ -1137,13 +1208,13 @@ public async Task CreateSuppressions(IEnumerable< /// Suppressions will only be reactivated if you have the required authority. /// The status of the request for each recipient. public async Task DeleteSuppressions(IEnumerable suppressionChanges, - string messageStream = DefaultTransactionalStream) + string messageStream = DefaultTransactionalStream, CancellationToken cancellationToken = default) { var body = new Dictionary { ["Suppressions"] = suppressionChanges.ToList() }; var apiUrl = $"/message-streams/{messageStream}/suppressions/delete"; - return await ProcessRequestAsync, PostmarkBulkReactivationResult>(apiUrl, HttpMethod.Post, body); + return await ProcessRequestAsync, PostmarkBulkReactivationResult>(apiUrl, HttpMethod.Post, body, cancellationToken: cancellationToken); } #endregion @@ -1159,7 +1230,7 @@ public async Task DeleteSuppressions(IEnumerable /// Friendly description for your message stream. (optional) /// The unsubscribe management option for the stream. (optional) /// Currently, you cannot create multiple inbound streams. - public async Task CreateMessageStream(string id, MessageStreamType type, string name, string description = null, UnsubscribeHandlingType? unsubscriptionHandlingType = null) + public async Task CreateMessageStream(string id, MessageStreamType type, string name, string description = null, UnsubscribeHandlingType? unsubscriptionHandlingType = null, CancellationToken cancellationToken = default) { var body = new Dictionary { @@ -1179,7 +1250,7 @@ public async Task CreateMessageStream(string id, MessageS var apiUrl = "/message-streams/"; - return await ProcessRequestAsync, PostmarkMessageStream>(apiUrl, HttpMethod.Post, body); + return await ProcessRequestAsync, PostmarkMessageStream>(apiUrl, HttpMethod.Post, body, cancellationToken: cancellationToken); } /// @@ -1189,7 +1260,7 @@ public async Task CreateMessageStream(string id, MessageS /// New friendly name to use. (optional) /// New description to use. (optional) /// New unsubscribe management option for the stream. (optional) - public async Task EditMessageStream(string id, string name = null, string description = null, UnsubscribeHandlingType? unsubscriptionHandlingType = null) + public async Task EditMessageStream(string id, string name = null, string description = null, UnsubscribeHandlingType? unsubscriptionHandlingType = null, CancellationToken cancellationToken = default) { var body = new Dictionary { @@ -1207,16 +1278,16 @@ public async Task EditMessageStream(string id, string nam var apiUrl = $"/message-streams/{id}"; - return await ProcessRequestAsync, PostmarkMessageStream>(apiUrl, new HttpMethod("PATCH"), body); + return await ProcessRequestAsync, PostmarkMessageStream>(apiUrl, new HttpMethod("PATCH"), body, cancellationToken: cancellationToken); } /// /// Retrieve details about a message stream. /// /// Identifier of the stream to retrieve details for. - public async Task GetMessageStream(string id) + public async Task GetMessageStream(string id, CancellationToken cancellationToken = default) { - return await ProcessNoBodyRequestAsync($"/message-streams/{id}"); + return await ProcessNoBodyRequestAsync($"/message-streams/{id}", cancellationToken: cancellationToken); } /// @@ -1225,7 +1296,7 @@ public async Task GetMessageStream(string id) /// Filter by stream type. E.g.: Transactional. Defaults to: All. /// Include archived streams in the result. Defaults to: false. public async Task ListMessageStreams(MessageStreamTypeFilter messageStreamType = MessageStreamTypeFilter.All, - bool includeArchivedStreams = false) + bool includeArchivedStreams = false, CancellationToken cancellationToken = default) { var parameters = new Dictionary { @@ -1233,7 +1304,7 @@ public async Task ListMessageStreams(MessageStream ["IncludeArchivedStreams"] = includeArchivedStreams }; - return await ProcessNoBodyRequestAsync("/message-streams/", parameters); + return await ProcessNoBodyRequestAsync("/message-streams/", parameters, cancellationToken: cancellationToken); } /// @@ -1242,11 +1313,11 @@ public async Task ListMessageStreams(MessageStream /// Once a stream has been archived, it will be deleted (alongside associated data) at the ExpectedPurgeDate in the response. /// /// Identifier of the stream to archive. - public async Task ArchiveMessageStream(string id) + public async Task ArchiveMessageStream(string id, CancellationToken cancellationToken = default) { var apiUrl = $"/message-streams/{id}/archive"; - return await ProcessNoBodyRequestAsync(apiUrl, verb: HttpMethod.Post); + return await ProcessNoBodyRequestAsync(apiUrl, verb: HttpMethod.Post, cancellationToken: cancellationToken); } /// @@ -1255,11 +1326,11 @@ public async Task ArchiveMessageStrea /// A stream can be unarchived only before the stream ExpectedPurgeDate. /// /// Identifier of the stream to unArchive. - public async Task UnArchiveMessageStream(string id) + public async Task UnArchiveMessageStream(string id, CancellationToken cancellationToken = default) { var apiUrl = $"/message-streams/{id}/unarchive"; - return await ProcessNoBodyRequestAsync(apiUrl, verb: HttpMethod.Post); + return await ProcessNoBodyRequestAsync(apiUrl, verb: HttpMethod.Post, cancellationToken: cancellationToken); } #endregion diff --git a/src/Postmark/PostmarkClientBase.cs b/src/Postmark/PostmarkClientBase.cs index 4fe9a8b..78fa7f6 100644 --- a/src/Postmark/PostmarkClientBase.cs +++ b/src/Postmark/PostmarkClientBase.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Net; using System.Net.Http; +using System.Threading; using System.Threading.Tasks; using Postmark.Exceptions; @@ -66,7 +67,8 @@ public PostmarkClientBase(string apiBaseUri = "https://api.postmarkapp.com") /// protected async Task ProcessRequestAsync( string apiPath, - HttpMethod verb, TRequestBody message = null) where TRequestBody : class + HttpMethod verb, TRequestBody message = null, + CancellationToken cancellationToken = default) where TRequestBody : class { var client = ClientFactory(); @@ -84,7 +86,7 @@ protected async Task ProcessRequestAsync( request.Headers.Add(AuthHeaderName, _authToken); request.Headers.Add("User-Agent", _agent); - var result = await client.SendAsync(request); + var result = await client.SendAsync(request, cancellationToken); var body = await result.Content.ReadAsStringAsync(); @@ -126,7 +128,8 @@ protected async Task ProcessRequestAsync( protected async Task ProcessNoBodyRequestAsync( string apiPath, IDictionary parameters = null, - HttpMethod verb = null) + HttpMethod verb = null, + CancellationToken cancellationToken = default) { parameters = parameters ?? new Dictionary(); @@ -148,7 +151,7 @@ protected async Task ProcessNoBodyRequestAsync( apiPath = apiPath + "?" + query; } - return await ProcessRequestAsync(apiPath, verb ?? HttpMethod.Get); + return await ProcessRequestAsync(apiPath, verb ?? HttpMethod.Get, cancellationToken: cancellationToken); } } diff --git a/src/Postmark/PostmarkClientExtensions.cs b/src/Postmark/PostmarkClientExtensions.cs index 09775cb..60facca 100644 --- a/src/Postmark/PostmarkClientExtensions.cs +++ b/src/Postmark/PostmarkClientExtensions.cs @@ -1,6 +1,7 @@ using PostmarkDotNet.Model; using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; namespace PostmarkDotNet @@ -30,11 +31,12 @@ public static async Task SendMessageAsync(this PostmarkClient string from, string to, string subject, string textBody, string htmlBody, IDictionary headers = null, IDictionary metadata = null, - string messageStream = null) + string messageStream = null, + CancellationToken cancellationToken = default) { var message = new PostmarkMessage(from, to, subject, textBody, htmlBody, new HeaderCollection(headers), metadata, messageStream); - return await client.SendMessageAsync(message); + return await client.SendMessageAsync(message, cancellationToken); } /// @@ -47,9 +49,10 @@ public static async Task SendMessageAsync(this PostmarkClient /// The client to use when sending the batch. /// A prepared message batch. /// - public static async Task> SendMessagesAsync(this PostmarkClient client, IEnumerable messages) + public static async Task> SendMessagesAsync(this PostmarkClient client, IEnumerable messages, + CancellationToken cancellationToken = default) { - return await client.SendMessagesAsync(messages.ToArray()); + return await client.SendMessagesAsync(cancellationToken, messages.ToArray()); } } } \ No newline at end of file diff --git a/src/Postmark/SimpleHttpClient.cs b/src/Postmark/SimpleHttpClient.cs index f816f36..e78baef 100644 --- a/src/Postmark/SimpleHttpClient.cs +++ b/src/Postmark/SimpleHttpClient.cs @@ -1,5 +1,6 @@ using System; using System.Net.Http; +using System.Threading; using System.Threading.Tasks; namespace PostmarkDotNet @@ -12,9 +13,9 @@ public SimpleHttpClient(TimeSpan? timeoutLength = null){ _client.Timeout = timeoutLength ?? TimeSpan.FromSeconds(60); } - public Task SendAsync(HttpRequestMessage request) + public Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken = default) { - return _client.SendAsync(request); + return _client.SendAsync(request, cancellationToken); } ~SimpleHttpClient(){ From 53d9974d78211e48a512e20de51f610175d15da0 Mon Sep 17 00:00:00 2001 From: smunuswamiac Date: Tue, 7 Jul 2026 18:46:10 -0500 Subject: [PATCH 2/2] PMK-2661 - Prep 5.4.2 release: bump version, README notes, fix CI readme rendering - Bump to 5.4.2. - Document the CancellationToken support under "What's New" in the package README. - Fix the CI release job so the package README actually renders on NuGet. The job packed with the .NET Core 3.1 SDK, which predates (added in SDK 5.0.300): the README file was bundled but the element was dropped, so 5.4.1 still shows no readme on NuGet. Bump only the release job's image to dotnet/sdk:8.0 (build/test stay on 3.1 since the test suite targets netcoreapp3.1). Co-Authored-By: Claude Opus 4.8 (1M context) --- .circleci/config.yml | 5 ++++- src/Postmark/Postmark.csproj | 2 +- src/Postmark/README.md | 7 +++++++ 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 9a3eb70..51daf56 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -45,7 +45,10 @@ jobs: --org=$SNYK_ORG_ID release: docker: - - image: mcr.microsoft.com/dotnet/core/sdk:3.1 + # Pack with a modern SDK (>= 5.0.300) so is honored and the + # README is embedded/registered in the package. The build/test jobs stay on 3.1 + # because the test suite targets netcoreapp3.1 and needs the 3.1 runtime. + - image: mcr.microsoft.com/dotnet/sdk:8.0 steps: - checkout - run: diff --git a/src/Postmark/Postmark.csproj b/src/Postmark/Postmark.csproj index b1b05fc..ffafcb2 100644 --- a/src/Postmark/Postmark.csproj +++ b/src/Postmark/Postmark.csproj @@ -6,7 +6,7 @@ https://github.com/wildbit/postmark-dotnet/raw/master/postmark-logo.png https://github.com/wildbit/postmark-dotnet.git git - 5.4.1 + 5.4.2 Wildbit, LLC. The official .net client for Postmark. MIT diff --git a/src/Postmark/README.md b/src/Postmark/README.md index 83c1743..4284558 100644 --- a/src/Postmark/README.md +++ b/src/Postmark/README.md @@ -39,6 +39,13 @@ sending email, using the bounce API, templates, and additional options. ## What's New +### 5.4.2 + +- All async methods on `PostmarkClient` and `PostmarkAdminClient` now accept an optional + `CancellationToken`, threaded through to the underlying HTTP request. Existing calls are + unaffected (the parameter defaults to `default`). For the batch/templated-send methods that + use `params`, a `(CancellationToken, params …)` overload is provided. + ### 5.4.1 - Added missing webhook payload models for type-safe deserialization of incoming