From c36b0c16b04b1a8448d67b4c141c255e00f0b825 Mon Sep 17 00:00:00 2001 From: Wouter Samaey Date: Fri, 11 Mar 2022 10:17:40 +0100 Subject: [PATCH] New API endpoint: Send email using store SMTP (#3181) Co-authored-by: Kukks --- .../BTCPayServerClient.StoreEmail.cs | 19 +++++ .../Models/SendEmailRequest.cs | 10 +++ BTCPayServer.Tests/GreenfieldAPITests.cs | 20 ++++- .../GreenfieldStoreEmailController.cs | 47 ++++++++++++ .../GreenField/LocalBTCPayServerClient.cs | 16 +++- .../v1/swagger.template.stores-email.json | 74 +++++++++++++++++++ 6 files changed, 184 insertions(+), 2 deletions(-) create mode 100644 BTCPayServer.Client/BTCPayServerClient.StoreEmail.cs create mode 100644 BTCPayServer.Client/Models/SendEmailRequest.cs create mode 100644 BTCPayServer/Controllers/GreenField/GreenfieldStoreEmailController.cs create mode 100644 BTCPayServer/wwwroot/swagger/v1/swagger.template.stores-email.json diff --git a/BTCPayServer.Client/BTCPayServerClient.StoreEmail.cs b/BTCPayServer.Client/BTCPayServerClient.StoreEmail.cs new file mode 100644 index 000000000..128cc63a0 --- /dev/null +++ b/BTCPayServer.Client/BTCPayServerClient.StoreEmail.cs @@ -0,0 +1,19 @@ +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using BTCPayServer.Client.Models; + +namespace BTCPayServer.Client +{ + public partial class BTCPayServerClient + { + public virtual async Task SendEmail(string storeId, SendEmailRequest request, + CancellationToken token = default) + { + using var response = await _httpClient.SendAsync( + CreateHttpRequest($"api/v1/stores/{storeId}/email/send", bodyPayload: request, method: HttpMethod.Post), + token); + await HandleResponse(response); + } + } +} diff --git a/BTCPayServer.Client/Models/SendEmailRequest.cs b/BTCPayServer.Client/Models/SendEmailRequest.cs new file mode 100644 index 000000000..524cc5e0e --- /dev/null +++ b/BTCPayServer.Client/Models/SendEmailRequest.cs @@ -0,0 +1,10 @@ + +namespace BTCPayServer.Client.Models +{ + public class SendEmailRequest + { + public string Email; + public string Subject; + public string Body; + } +} diff --git a/BTCPayServer.Tests/GreenfieldAPITests.cs b/BTCPayServer.Tests/GreenfieldAPITests.cs index 7270493eb..f77bdad56 100644 --- a/BTCPayServer.Tests/GreenfieldAPITests.cs +++ b/BTCPayServer.Tests/GreenfieldAPITests.cs @@ -2265,6 +2265,24 @@ namespace BTCPayServer.Tests await AssertAPIError("store-user-role-orphaned", async () => await user2Client.RemoveStoreUser(user.StoreId, user2.UserId)); } - + + + [Fact(Timeout = 60 * 2 * 1000)] + [Trait("Integration", "Integration")] + public async Task StoreEmailTests() + { + using var tester = CreateServerTester(); + await tester.StartAsync(); + var admin = tester.NewAccount(); + await admin.GrantAccessAsync(true); + var adminClient = await admin.CreateClient(Policies.Unrestricted); + + await adminClient.SendEmail(admin.StoreId, new SendEmailRequest() + { + Body = "lol", + Subject = "subj", + Email = "sdasdas" + }); + } } } diff --git a/BTCPayServer/Controllers/GreenField/GreenfieldStoreEmailController.cs b/BTCPayServer/Controllers/GreenField/GreenfieldStoreEmailController.cs new file mode 100644 index 000000000..84d4de7fc --- /dev/null +++ b/BTCPayServer/Controllers/GreenField/GreenfieldStoreEmailController.cs @@ -0,0 +1,47 @@ +#nullable enable +using System; +using System.Threading.Tasks; +using BTCPayServer.Abstractions.Constants; +using BTCPayServer.Abstractions.Extensions; +using BTCPayServer.Client; +using BTCPayServer.Client.Models; +using BTCPayServer.Services.Mails; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Cors; +using Microsoft.AspNetCore.Mvc; + +namespace BTCPayServer.Controllers.GreenField +{ + [ApiController] + [Authorize(AuthenticationSchemes = AuthenticationSchemes.Greenfield)] + [EnableCors(CorsPolicies.All)] + public class GreenfieldStoreEmailController : Controller + { + private readonly EmailSenderFactory _emailSenderFactory; + + public GreenfieldStoreEmailController(EmailSenderFactory emailSenderFactory) + { + _emailSenderFactory = emailSenderFactory; + } + + [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)] + [HttpPost("~/api/v1/stores/{storeId}/email/send")] + public async Task SendEmailFromStore(string storeId, + [FromBody] SendEmailRequest request) + { + var store = HttpContext.GetStoreData(); + if (store == null) + { + return this.CreateAPIError(404, "store-not-found", "The store was not found"); + } + var emailSender = await _emailSenderFactory.GetEmailSender(storeId); + if (emailSender is null ) + { + return this.CreateAPIError(404,"smtp-not-configured", "Store does not have an SMTP server configured."); + } + + emailSender.SendEmail(request.Email, request.Subject, request.Body); + return Ok(); + } + } +} diff --git a/BTCPayServer/Controllers/GreenField/LocalBTCPayServerClient.cs b/BTCPayServer/Controllers/GreenField/LocalBTCPayServerClient.cs index 58d2ae49f..a657fa2df 100644 --- a/BTCPayServer/Controllers/GreenField/LocalBTCPayServerClient.cs +++ b/BTCPayServer/Controllers/GreenField/LocalBTCPayServerClient.cs @@ -9,6 +9,7 @@ using System.Threading.Tasks; using BTCPayServer.Abstractions.Contracts; using BTCPayServer.Client; using BTCPayServer.Client.Models; +using BTCPayServer.Controllers.GreenField; using BTCPayServer.Data; using BTCPayServer.Security.Greenfield; using BTCPayServer.Services.Stores; @@ -56,6 +57,7 @@ namespace BTCPayServer.Controllers.Greenfield private readonly GreenfieldPullPaymentController _greenfieldPullPaymentController; private readonly UIHomeController _homeController; private readonly GreenfieldStorePaymentMethodsController _storePaymentMethodsController; + private readonly GreenfieldStoreEmailController _greenfieldStoreEmailController; private readonly IServiceProvider _serviceProvider; public BTCPayServerClientFactory(StoreRepository storeRepository, @@ -79,6 +81,7 @@ namespace BTCPayServer.Controllers.Greenfield GreenfieldPullPaymentController greenfieldPullPaymentController, UIHomeController homeController, GreenfieldStorePaymentMethodsController storePaymentMethodsController, + GreenfieldStoreEmailController greenfieldStoreEmailController, IServiceProvider serviceProvider) { _storeRepository = storeRepository; @@ -102,6 +105,7 @@ namespace BTCPayServer.Controllers.Greenfield _greenfieldPullPaymentController = greenfieldPullPaymentController; _homeController = homeController; _storePaymentMethodsController = storePaymentMethodsController; + _greenfieldStoreEmailController = greenfieldStoreEmailController; _serviceProvider = serviceProvider; } @@ -158,6 +162,7 @@ namespace BTCPayServer.Controllers.Greenfield _greenfieldPullPaymentController, _homeController, _storePaymentMethodsController, + _greenfieldStoreEmailController, new LocalHttpContextAccessor() { HttpContext = context } ); } @@ -188,6 +193,7 @@ namespace BTCPayServer.Controllers.Greenfield private readonly GreenfieldPullPaymentController _greenfieldPullPaymentController; private readonly UIHomeController _homeController; private readonly GreenfieldStorePaymentMethodsController _storePaymentMethodsController; + private readonly GreenfieldStoreEmailController _greenfieldStoreEmailController; public LocalBTCPayServerClient( IServiceProvider serviceProvider, @@ -209,6 +215,7 @@ namespace BTCPayServer.Controllers.Greenfield GreenfieldPullPaymentController greenfieldPullPaymentController, UIHomeController homeController, GreenfieldStorePaymentMethodsController storePaymentMethodsController, + GreenfieldStoreEmailController greenfieldStoreEmailController, IHttpContextAccessor httpContextAccessor) : base(new Uri("https://dummy.local"), "", "") { _chainPaymentMethodsController = chainPaymentMethodsController; @@ -229,6 +236,7 @@ namespace BTCPayServer.Controllers.Greenfield _greenfieldPullPaymentController = greenfieldPullPaymentController; _homeController = homeController; _storePaymentMethodsController = storePaymentMethodsController; + _greenfieldStoreEmailController = greenfieldStoreEmailController; var controllers = new[] { @@ -236,7 +244,8 @@ namespace BTCPayServer.Controllers.Greenfield paymentRequestController, apiKeysController, notificationsController, usersController, storeLightningNetworkPaymentMethodsController, greenFieldInvoiceController, storeWebhooksController, greenFieldServerInfoController, greenfieldPullPaymentController, storesController, homeController, - lightningNodeApiController, storeLightningNodeApiController as ControllerBase, storePaymentMethodsController + lightningNodeApiController, storeLightningNodeApiController as ControllerBase, storePaymentMethodsController, + greenfieldStoreEmailController }; var authoverride = new DefaultAuthorizationService( @@ -999,5 +1008,10 @@ namespace BTCPayServer.Controllers.Greenfield ImportKeysToRPC = request.ImportKeysToRPC })); } + + public override async Task SendEmail(string storeId, SendEmailRequest request, CancellationToken token = default) + { + HandleActionResult(await _greenfieldStoreEmailController.SendEmailFromStore(storeId, request)); + } } } diff --git a/BTCPayServer/wwwroot/swagger/v1/swagger.template.stores-email.json b/BTCPayServer/wwwroot/swagger/v1/swagger.template.stores-email.json new file mode 100644 index 000000000..ec530bf02 --- /dev/null +++ b/BTCPayServer/wwwroot/swagger/v1/swagger.template.stores-email.json @@ -0,0 +1,74 @@ +{ + "paths": { + "/api/v1/stores/{storeId}/email/send": { + "post": { + "tags": [ + "Stores" + ], + "summary": "Send an email for a store", + "description": "Send an email using the store's SMTP server", + "requestBody": { + "x-name": "request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EmailData" + } + } + }, + "required": true, + "x-position": 1 + }, + "responses": { + "201": { + "description": "The email was sent (scheduled) successfully" + }, + "400": { + "description": "The store's SMTP is not configured" + }, + "403": { + "description": "If you are authenticated but forbidden to add new stores" + }, + "404": { + "description": "The store was not found" + } + }, + "security": [ + { + "API Key": [ + "btcpay.store.canmodifystoresettings" + ], + "Basic": [] + } + ] + } + } + }, + "components": { + "schemas": { + "EmailData": { + "type": "object", + "additionalProperties": false, + "properties": { + "email": { + "type": "string", + "description": "Email of the recipient" + }, + "subject": { + "type": "string", + "description": "Subject of the email" + }, + "body": { + "type": "string", + "description": "Body of the email to send as plain text." + } + } + } + } + }, + "tags": [ + { + "name": "Store Email" + } + ] +}