Adding endpoint to set server email settings (#6601)

* Adding endpoint in Greenfield to allow server email settings

* Adding related swagger file

* Refactoring EmailSettingsData to be more readable

* Adding server email masking

* Adding tests

* Update BTCPayServer/wwwroot/swagger/v1/swagger.template.serveremail.json

Co-authored-by: d11n <mail@dennisreimann.de>

* Masking smtp server email returned over greenfield api and test

* Retaining password if password mask is used

* Remove magic string *****

* Flatten request for server's settings. Fix bug on shared setting instances

* Remove useless doc

* Simplify code

* Fix Store Email settings page

---------

Co-authored-by: d11n <mail@dennisreimann.de>
Co-authored-by: nicolas.dorier <nicolas.dorier@gmail.com>
This commit is contained in:
rockstardev
2025-02-27 01:59:17 -05:00
committed by GitHub
parent 8b5c5895f0
commit a7e3cbb105
16 changed files with 412 additions and 117 deletions

View File

@@ -0,0 +1,20 @@
using System.Collections.Generic;
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<ServerEmailSettingsData> GetServerEmailSettings(CancellationToken token = default)
{
return await SendHttpRequest<ServerEmailSettingsData>("api/v1/server/email", null, HttpMethod.Get, token);
}
public virtual async Task<bool> UpdateServerEmailSettings(ServerEmailSettingsData request, CancellationToken token = default)
{
return await SendHttpRequest<bool>("api/v1/server/email", request, HttpMethod.Put, token);
}
}

View File

@@ -4,29 +4,11 @@ namespace BTCPayServer.Client.Models;
public class EmailSettingsData
{
public string Server
{
get; set;
}
public int? Port
{
get; set;
}
public string Login
{
get; set;
}
public string Password
{
get; set;
}
public string From
{
get; set;
}
public string Server { get; set; }
public int? Port { get; set; }
public string Login { get; set; }
public string Password { get; set; }
public string From { get; set; }
public bool DisableCertificateCheck { get; set; }
[JsonIgnore]

View File

@@ -0,0 +1,7 @@
namespace BTCPayServer.Client.Models
{
public class ServerEmailSettingsData : EmailSettingsData
{
public bool EnableStoresToUseServerEmailSettings { get; set; }
}
}

View File

@@ -24,6 +24,7 @@ using BTCPayServer.Rating;
using BTCPayServer.Services;
using BTCPayServer.Services.Apps;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Mails;
using BTCPayServer.Services.Notifications;
using BTCPayServer.Services.Notifications.Blobs;
using BTCPayServer.Services.Stores;
@@ -1537,7 +1538,7 @@ namespace BTCPayServer.Tests
});
}
[Fact]
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
public async Task CanProcessPayoutsExternally()
{
@@ -4093,6 +4094,45 @@ namespace BTCPayServer.Tests
await AssertAPIError("store-user-role-orphaned", async () => await employeeClient.RemoveStoreUser(user.StoreId, employee.UserId));
}
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
public async Task ServerEmailTests()
{
using var tester = CreateServerTester();
await tester.StartAsync();
var admin = tester.NewAccount();
await admin.GrantAccessAsync(true);
var adminClient = await admin.CreateClient(Policies.Unrestricted);
var data = new EmailSettingsData
{
From = "admin@admin.com",
Login = "admin@admin.com",
Password = "admin@admin.com",
Port = 1234,
Server = "admin.com",
};
var serverEmailSettings = new ServerEmailSettingsData
{
EnableStoresToUseServerEmailSettings = false
};
await adminClient.UpdateServerEmailSettings(serverEmailSettings);
var s = await adminClient.GetServerEmailSettings();
// email password is masked and not returned from the server once set
serverEmailSettings.Password = null;
Assert.Equal(JsonConvert.SerializeObject(s), JsonConvert.SerializeObject(serverEmailSettings));
await AssertValidationError(new[] { nameof(EmailSettingsData.From) },
async () => await adminClient.UpdateServerEmailSettings(new ServerEmailSettingsData
{
From = "invalid"
}));
// NOTE: This email test fails silently in EmailSender.cs#31, can't test, but leaving for the future as reminder
//await adminClient.SendEmail(admin.StoreId,
// new SendEmailRequest { Body = "lol", Subject = "subj", Email = "to@example.org" });
}
[Fact(Timeout = 60 * 2 * 1000)]
[Trait("Integration", "Integration")]
public async Task StoreEmailTests()
@@ -4115,6 +4155,8 @@ namespace BTCPayServer.Tests
};
await adminClient.UpdateStoreEmailSettings(admin.StoreId, data);
var s = await adminClient.GetStoreEmailSettings(admin.StoreId);
// email password is masked and not returned from the server once set
data.Password = null;
Assert.Equal(JsonConvert.SerializeObject(s), JsonConvert.SerializeObject(data));
await AssertValidationError(new[] { nameof(EmailSettingsData.From) },
async () => await adminClient.UpdateStoreEmailSettings(admin.StoreId,

View File

@@ -691,7 +691,7 @@ namespace BTCPayServer.Tests
// Store Emails without server fallback
s.GoToStore(StoreNavPages.Emails);
s.Driver.ElementDoesNotExist(By.Id("UseCustomSMTP"));
s.Driver.ElementDoesNotExist(By.Id("IsCustomSMTP"));
s.Driver.FindElement(By.Id("ConfigureEmailRules")).Click();
Assert.Contains("You need to configure email settings before this feature works", s.Driver.PageSource);
@@ -706,12 +706,12 @@ namespace BTCPayServer.Tests
// Store Emails with server fallback
s.GoToStore(StoreNavPages.Emails);
Assert.False(s.Driver.FindElement(By.Id("UseCustomSMTP")).Selected);
Assert.False(s.Driver.FindElement(By.Id("IsCustomSMTP")).Selected);
s.Driver.FindElement(By.Id("ConfigureEmailRules")).Click();
Assert.DoesNotContain("You need to configure email settings before this feature works", s.Driver.PageSource);
s.GoToStore(StoreNavPages.Emails);
s.Driver.FindElement(By.Id("UseCustomSMTP")).Click();
s.Driver.FindElement(By.Id("IsCustomSMTP")).Click();
Thread.Sleep(250);
CanSetupEmailCore(s);
@@ -732,7 +732,7 @@ namespace BTCPayServer.Tests
Assert.Contains("Store email rules saved", s.FindAlertMessage().Text);
s.GoToStore(StoreNavPages.Emails);
Assert.True(s.Driver.FindElement(By.Id("UseCustomSMTP")).Selected);
Assert.True(s.Driver.FindElement(By.Id("IsCustomSMTP")).Selected);
}
[Fact(Timeout = TestTimeout)]

View File

@@ -0,0 +1,89 @@
#nullable enable
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Client;
using BTCPayServer.Client.Models;
using BTCPayServer.Models.ServerViewModels;
using BTCPayServer.Services;
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 GreenfieldServerEmailController : Controller
{
private readonly EmailSenderFactory _emailSenderFactory;
private readonly PoliciesSettings _policiesSettings;
readonly SettingsRepository _settingsRepository;
public GreenfieldServerEmailController(EmailSenderFactory emailSenderFactory, PoliciesSettings policiesSettings, SettingsRepository settingsRepository)
{
_emailSenderFactory = emailSenderFactory;
_policiesSettings = policiesSettings;
_settingsRepository = settingsRepository;
}
[Authorize(Policy = Policies.CanModifyServerSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpGet("~/api/v1/server/email")]
public async Task<IActionResult> ServerEmailSettings()
{
var email = await _emailSenderFactory.GetSettings() ?? new EmailSettings();
var model = new ServerEmailSettingsData
{
EnableStoresToUseServerEmailSettings = !_policiesSettings.DisableStoresToUseServerEmailSettings,
From = email.From,
Server = email.Server,
Port = email.Port,
Login = email.Login,
DisableCertificateCheck = email.DisableCertificateCheck,
// Password is not returned
Password = null
};
return Ok(model);
}
[Authorize(Policy = Policies.CanModifyServerSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpPut("~/api/v1/server/email")]
public async Task<IActionResult> ServerEmailSettings(ServerEmailSettingsData request)
{
if (_policiesSettings.DisableStoresToUseServerEmailSettings == request.EnableStoresToUseServerEmailSettings)
{
_policiesSettings.DisableStoresToUseServerEmailSettings = !request.EnableStoresToUseServerEmailSettings;
await _settingsRepository.UpdateSetting(_policiesSettings);
}
// save
if (request.From is not null && !MailboxAddressValidator.IsMailboxAddress(request.From))
{
request.AddModelError(e => e.From,
"Invalid email address", this);
return this.CreateValidationError(ModelState);
}
var oldSettings = await _emailSenderFactory.GetSettings() ?? new EmailSettings();
// retaining the password if it exists and was not provided in request
if (string.IsNullOrEmpty(request.Password) &&
!string.IsNullOrEmpty(oldSettings?.Password))
request.Password = oldSettings.Password;
// important to save as EmailSettings otherwise it won't be able to be fetched
await _settingsRepository.UpdateSetting(new EmailSettings
{
Server = request.Server,
Port = request.Port,
Login = request.Login,
Password = request.Password,
From = request.From,
DisableCertificateCheck = request.DisableCertificateCheck
});
return Ok(true);
}
}
}

View File

@@ -55,7 +55,6 @@ namespace BTCPayServer.Controllers.GreenField
[HttpGet("~/api/v1/stores/{storeId}/email")]
public IActionResult GetStoreEmailSettings()
{
var store = HttpContext.GetStoreData();
return store == null ? StoreNotFound() : Ok(FromModel(store));
}
@@ -76,7 +75,13 @@ namespace BTCPayServer.Controllers.GreenField
"Invalid email address", this);
return this.CreateValidationError(ModelState);
}
var blob = store.GetStoreBlob();
// retaining the password if it exists and was not provided in request
if (string.IsNullOrEmpty(request.Password) && blob.EmailSettings?.Password != null)
request.Password = blob.EmailSettings?.Password;
blob.EmailSettings = request;
if (store.SetStoreBlob(blob))
{
@@ -87,7 +92,11 @@ namespace BTCPayServer.Controllers.GreenField
}
private EmailSettings FromModel(Data.StoreData data)
{
return data.GetStoreBlob().EmailSettings ?? new();
var emailSettings = data.GetStoreBlob().EmailSettings;
if (emailSettings == null)
return new EmailSettings();
emailSettings.Password = null;
return emailSettings;
}
private IActionResult StoreNotFound()
{

View File

@@ -1263,10 +1263,8 @@ namespace BTCPayServer.Controllers
return View(model);
}
var oldSettings = await _emailSenderFactory.GetSettings() ?? new EmailSettings();
if (new ServerEmailsViewModel(oldSettings).PasswordSet)
{
if (!string.IsNullOrEmpty(oldSettings.Password))
model.Settings.Password = oldSettings.Password;
}
await _SettingsRepository.UpdateSetting(model.Settings);
TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["Email settings saved"].Value;

View File

@@ -174,51 +174,65 @@ public partial class UIStoresController
if (store == null)
return NotFound();
var emailSender = await _emailSenderFactory.GetEmailSender(store.Id);
var data = await emailSender.GetEmailSettings() ?? new EmailSettings();
var fallbackSettings = emailSender is StoreEmailSender { FallbackSender: { } fallbackSender }
? await fallbackSender.GetEmailSettings()
: null;
var settings = data != fallbackSettings ? data : new EmailSettings();
return View(new EmailsViewModel(settings, fallbackSettings));
var settings = await GetCustomSettings(store.Id);
return View(new EmailsViewModel(settings.Custom ?? new())
{
IsFallbackSetup = settings.Fallback is not null,
IsCustomSMTP = settings.Custom is not null || settings.Fallback is null
});
}
record AllEmailSettings(EmailSettings Custom, EmailSettings Fallback);
private async Task<AllEmailSettings> GetCustomSettings(string storeId)
{
var sender = await _emailSenderFactory.GetEmailSender(storeId) as StoreEmailSender;
if (sender is null)
return new(null, null);
var fallback = sender.FallbackSender is { } fb ? await fb.GetEmailSettings() : null;
if (fallback?.IsComplete() is not true)
fallback = null;
return new(await sender.GetCustomSettings(), fallback);
}
[HttpPost("{storeId}/email-settings")]
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
public async Task<IActionResult> StoreEmailSettings(string storeId, EmailsViewModel model, string command, [FromForm] bool useCustomSMTP = false)
public async Task<IActionResult> StoreEmailSettings(string storeId, EmailsViewModel model, string command)
{
var store = HttpContext.GetStoreData();
if (store == null)
return NotFound();
model.FallbackSettings = await _emailSenderFactory.GetEmailSender(store.Id) is StoreEmailSender { FallbackSender: not null } storeSender
? await storeSender.FallbackSender.GetEmailSettings()
: null;
if (model.FallbackSettings is null) useCustomSMTP = true;
ViewBag.UseCustomSMTP = useCustomSMTP;
if (useCustomSMTP)
var settings = await GetCustomSettings(store.Id);
model.IsFallbackSetup = settings.Fallback is not null;
if (!model.IsFallbackSetup)
model.IsCustomSMTP = true;
if (model.IsCustomSMTP)
{
model.Settings.Validate("Settings.", ModelState);
if (model.Settings.From is not null && !MailboxAddressValidator.IsMailboxAddress(model.Settings.From))
{
ModelState.AddModelError("Settings.From", StringLocalizer["Invalid email"]);
}
if (!ModelState.IsValid)
return View(model);
}
var storeBlob = store.GetStoreBlob();
var currentSettings = store.GetStoreBlob().EmailSettings;
if (model is { IsCustomSMTP: true, Settings: { Password: null } })
model.Settings.Password = currentSettings?.Password;
if (command == "Test")
{
try
{
if (useCustomSMTP)
{
if (model.PasswordSet)
{
model.Settings.Password = store.GetStoreBlob().EmailSettings.Password;
}
}
if (string.IsNullOrEmpty(model.TestEmail))
ModelState.AddModelError(nameof(model.TestEmail), new RequiredAttribute().FormatErrorMessage(nameof(model.TestEmail)));
if (!ModelState.IsValid)
return View(model);
var settings = useCustomSMTP ? model.Settings : model.FallbackSettings;
using var client = await settings.CreateSmtpClient();
var message = settings.CreateMailMessage(MailboxAddress.Parse(model.TestEmail), $"{store.StoreName}: Email test", StringLocalizer["You received it, the BTCPay Server SMTP settings work."], false);
var clientSettings = (model.IsCustomSMTP ? model.Settings : settings.Fallback) ?? new();
using var client = await clientSettings.CreateSmtpClient();
var message = clientSettings.CreateMailMessage(MailboxAddress.Parse(model.TestEmail), $"{store.StoreName}: Email test", StringLocalizer["You received it, the BTCPay Server SMTP settings work."], false);
await client.SendAsync(message);
await client.DisconnectAsync(true);
TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["Email sent to {0}. Please verify you received it.", model.TestEmail].Value;
@@ -229,29 +243,24 @@ public partial class UIStoresController
}
return View(model);
}
if (command == "ResetPassword")
else if (command == "ResetPassword")
{
var storeBlob = store.GetStoreBlob();
if (storeBlob.EmailSettings is not null)
storeBlob.EmailSettings.Password = null;
store.SetStoreBlob(storeBlob);
await _storeRepo.UpdateStore(store);
TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["Email server password reset"].Value;
}
var unsetCustomSMTP = !useCustomSMTP && store.GetStoreBlob().EmailSettings is not null;
if (useCustomSMTP || unsetCustomSMTP)
else if (!model.IsCustomSMTP && currentSettings is not null)
{
if (model.Settings.From is not null && !MailboxAddressValidator.IsMailboxAddress(model.Settings.From))
{
ModelState.AddModelError("Settings.From", StringLocalizer["Invalid email"]);
storeBlob.EmailSettings = null;
store.SetStoreBlob(storeBlob);
await _storeRepo.UpdateStore(store);
TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["You are now using server's email settings"].Value;
}
if (!ModelState.IsValid)
return View(model);
var storeBlob = store.GetStoreBlob();
if (storeBlob.EmailSettings != null && new EmailsViewModel(storeBlob.EmailSettings, model.FallbackSettings).PasswordSet)
else if (model.IsCustomSMTP)
{
model.Settings.Password = storeBlob.EmailSettings.Password;
}
storeBlob.EmailSettings = unsetCustomSMTP ? null : model.Settings;
storeBlob.EmailSettings = model.Settings;
store.SetStoreBlob(storeBlob);
await _storeRepo.UpdateStore(store);
TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["Email settings modified"].Value;

View File

@@ -7,7 +7,6 @@ namespace BTCPayServer.Models;
public class EmailsViewModel
{
public EmailSettings Settings { get; set; }
public EmailSettings FallbackSettings { get; set; }
public bool PasswordSet { get; set; }
[MailboxAddress]
@@ -18,14 +17,14 @@ public class EmailsViewModel
{
}
public EmailsViewModel(EmailSettings settings, EmailSettings fallbackSettings = null)
public EmailsViewModel(EmailSettings settings)
{
Settings = settings;
FallbackSettings = fallbackSettings;
PasswordSet = !string.IsNullOrEmpty(settings?.Password);
PasswordSet = !string.IsNullOrWhiteSpace(settings?.Password);
}
public bool IsSetup() => Settings?.IsComplete() is true;
public bool IsFallbackSetup() => FallbackSettings?.IsComplete() is true;
public bool UsesFallback() => IsFallbackSetup() && Settings == FallbackSettings;
public bool IsFallbackSetup { get; set; }
public bool IsCustomSMTP { get; set; }
}

View File

@@ -29,14 +29,27 @@ namespace BTCPayServer.Services.Mails
var store = await StoreRepository.FindStore(StoreId);
if (store is null)
return null;
var emailSettings = GetCustomSettings(store);
if (emailSettings is not null)
return emailSettings;
if (FallbackSender is not null)
return await FallbackSender.GetEmailSettings();
return null;
}
public async Task<EmailSettings?> GetCustomSettings()
{
var store = await StoreRepository.FindStore(StoreId);
if (store is null)
return null;
return GetCustomSettings(store);
}
EmailSettings? GetCustomSettings(StoreData store)
{
var emailSettings = store.GetStoreBlob().EmailSettings;
if (emailSettings?.IsComplete() is true)
{
return emailSettings;
}
if (FallbackSender is not null)
return await FallbackSender.GetEmailSettings();
return null;
}

View File

@@ -26,12 +26,13 @@ namespace BTCPayServer.Services
public async Task<T?> GetSettingAsync<T>(string? name = null) where T : class
{
name ??= typeof(T).FullName ?? string.Empty;
return await _memoryCache.GetOrCreateAsync(GetCacheKey(name), async entry =>
var data = await _memoryCache.GetOrCreateAsync(GetCacheKey(name), async entry =>
{
await using var ctx = _ContextFactory.CreateContext();
var data = await ctx.Settings.Where(s => s.Id == name).FirstOrDefaultAsync();
return data == null ? default : Deserialize<T>(data.Value);
return data?.Value;
});
return data is string ? Deserialize<T>(data) : null;
}
public async Task UpdateSetting<T>(T obj, string? name = null) where T : class
{
@@ -49,7 +50,7 @@ namespace BTCPayServer.Services
await ctx.SaveChangesAsync();
}
}
_memoryCache.Set(GetCacheKey(name), obj);
_memoryCache.Remove(GetCacheKey(name));
_EventAggregator.Publish(new SettingsChanged<T>()
{
Settings = obj,

View File

@@ -49,7 +49,7 @@
<label asp-for="Settings.Password" class="form-label" text-translate="true">Password</label>
@if (!Model.PasswordSet)
{
<input asp-for="Settings.Password" type="password" class="form-control"/>
<input asp-for="Settings.Password" type="password" value="@Model.Settings.Password" class="form-control" />
<span asp-validation-for="Settings.Password" class="text-danger"></span>
}
else

View File

@@ -3,7 +3,6 @@
@model BTCPayServer.Models.EmailsViewModel
@{
var storeId = Context.GetStoreData().Id;
var hasCustomSettings = (Model.IsSetup() && !Model.UsesFallback()) || ViewBag.UseCustomSMTP ?? false;
ViewData.SetActivePage(StoreNavPages.Emails, StringLocalizer["Email Rules"], storeId);
}
@@ -13,23 +12,23 @@
<button id="page-primary" type="submit" class="btn btn-primary" name="command" value="Save">Save</button>
</div>
<partial name="_StatusMessage" />
@if (Model.IsFallbackSetup())
@if (Model.IsFallbackSetup)
{
<label class="d-flex align-items-center mb-4">
<input type="checkbox" id="UseCustomSMTP" name="UseCustomSMTP" value="true" checked="@hasCustomSettings" class="btcpay-toggle me-3" data-bs-toggle="collapse" data-bs-target="#SmtpSettings" aria-expanded="@hasCustomSettings" aria-controls="SmtpSettings" />
<input type="checkbox" asp-for="IsCustomSMTP" class="btcpay-toggle me-3" data-bs-toggle="collapse" data-bs-target="#SmtpSettings" aria-expanded="@Model.IsCustomSMTP" aria-controls="SmtpSettings" />
<div>
<span text-translate="true">Use custom SMTP settings for this store</span>
<div class="form-text" text-translate="true">Otherwise, the server's SMTP settings will be used to send emails.</div>
</div>
</label>
<div class="collapse @(hasCustomSettings ? "show" : "")" id="SmtpSettings">
<div class="collapse @(Model.IsCustomSMTP ? "show" : "")" id="SmtpSettings">
<partial name="EmailsBody" model="Model" />
</div>
}
else
{
<input type="hidden" name="UseCustomSMTP" value="true" />
<input type="hidden" id="IsCustomSMTPHidden" asp-for="IsCustomSMTP" />
<partial name="EmailsBody" model="Model" />
}

View File

@@ -0,0 +1,127 @@
{
"paths": {
"/api/v1/server/email": {
"get": {
"tags": ["ServerEmail"],
"summary": "Get server email settings",
"description": "Retrieve the email settings configured for the server. The password field will be masked if present.",
"operationId": "ServerEmail_GetSettings",
"responses": {
"200": {
"description": "Server email settings",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ServerEmailSettingsData"
}
}
}
},
"403": {
"description": "Forbidden - Insufficient permissions"
}
},
"security": [
{
"API_Key": ["btcpay.server.canmodifyserversettings"],
"Basic": []
}
]
},
"put": {
"tags": ["ServerEmail"],
"summary": "Update server email settings",
"description": "Update server's email settings.",
"operationId": "ServerEmail_UpdateSettings",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ServerEmailSettingsData"
}
}
}
},
"responses": {
"200": {
"description": "Email settings updated successfully"
},
"400": {
"description": "Invalid request or email format",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ProblemDetails"
}
}
}
},
"403": {
"description": "Forbidden - Insufficient permissions"
}
},
"security": [
{
"API_Key": ["btcpay.server.canmodifyserversettings"],
"Basic": []
}
]
}
}
},
"components": {
"schemas": {
"ServerEmailSettingsData": {
"allOf": [
{ "$ref": "#/components/schemas/EmailSettings" },
{
"type": "object",
"properties": {
"enableStoresToUseServerEmailSettings": {
"type": "boolean",
"description": "Indicates if stores can use server email settings"
}
}
}
]
},
"EmailSettings": {
"type": "object",
"properties": {
"from": {
"type": "string",
"description": "The sender email address"
},
"server": {
"type": "string",
"description": "SMTP server host"
},
"port": {
"type": "integer",
"description": "SMTP server port"
},
"login": {
"type": "string",
"description": "SMTP username"
},
"password": {
"type": "string",
"description": "SMTP password, masked in responses and retained if not updated",
"nullable": true
},
"disableCertificateCheck": {
"type": "boolean",
"description": "Use SSL for SMTP connection"
}
}
}
}
},
"tags": [
{
"name": "ServerEmail",
"description": "Server Email Settings operations"
}
]
}

View File

@@ -11,7 +11,7 @@
"$ref": "#/components/parameters/StoreId"
}
],
"description": "View email settings of the specified store",
"description": "Retrieve the email settings configured for specific store. The password field will be masked if present.",
"operationId": "Stores_GetStoreEmailSettings",
"responses": {
"200": {