mirror of
https://github.com/aliasvault/aliasvault.git
synced 2026-05-09 07:46:13 -04:00
Add option to configure SMTP advertised hostname for self-hosted setups (#1877)
* feat(smtp): make advertised hostname configurable for PTR/banner alignment * test(integration): align SMTP TestHostBuilder with AdvertisedHostnameConfiguration and IConfiguration * test(import): expect Dashlane notes newline per Environment.NewLine * docs: document SMTP advertised hostname and PTR/banner alignment * restore original .vscode folder content * Use env-only SMTP advertised hostname in tests and service * revert * remove unused reference * remove unused methods * Use aliasvault when SMTP advertised hostname is empty * Update SMTP advertised hostname docs * Update install.sh SMTP advertised hostname prompt * Update .env.example * Update docs --------- Co-authored-by: Arnaud Dartois <opensource.fork@tordais.cc> Co-authored-by: Leendert de Borst <ldeborst@xivisoft.com>
This commit is contained in:
@@ -50,6 +50,11 @@ PRIVATE_EMAIL_DOMAINS=
|
||||
# Note: Domains listed here should ALSO be included in PRIVATE_EMAIL_DOMAINS above.
|
||||
HIDDEN_PRIVATE_EMAIL_DOMAINS=
|
||||
|
||||
# Hostname announced in the SMTP banner and EHLO. Should match the PTR (reverse DNS) for your
|
||||
# server's public IP for best deliverability. If empty, "aliasvault" is used.
|
||||
# Example: SMTP_ADVERTISED_HOSTNAME=mail.mydomain.net
|
||||
SMTP_ADVERTISED_HOSTNAME=
|
||||
|
||||
# Enable TLS for SMTP (STARTTLS).
|
||||
# When enabled, the SMTP server will offer STARTTLS to connecting mail servers.
|
||||
#
|
||||
|
||||
2
.vscode/tasks.json
vendored
2
.vscode/tasks.json
vendored
@@ -225,4 +225,4 @@
|
||||
"dependsOn": []
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@ using AliasVault.SmtpService.Handlers;
|
||||
using AliasVault.SmtpService.Workers;
|
||||
using AliasVault.WorkerStatus.ServiceExtensions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using SmtpServer;
|
||||
using SmtpServer.Storage;
|
||||
|
||||
@@ -41,6 +42,12 @@ config.SmtpTlsEnabled = tlsEnabled;
|
||||
var certPath = Environment.GetEnvironmentVariable("SMTP_CERTIFICATES_PATH") ?? "/certificates/smtp";
|
||||
config.SmtpCertificatesPath = certPath;
|
||||
|
||||
var advertisedHostname = Environment.GetEnvironmentVariable("SMTP_ADVERTISED_HOSTNAME");
|
||||
if (string.IsNullOrWhiteSpace(advertisedHostname))
|
||||
{
|
||||
advertisedHostname = "aliasvault";
|
||||
}
|
||||
|
||||
// Check if TLS is requested but certificates are not available, if so, fallback to non-TLS mode.
|
||||
var tlsAvailable = false;
|
||||
X509Certificate2? loadedCertificate = null;
|
||||
@@ -70,8 +77,10 @@ builder.Services.AddSingleton(
|
||||
{
|
||||
// Use SmtpServerWorker logger so logs appear in the database (it's in the allowed sources list).
|
||||
var logger = provider.GetRequiredService<ILogger<SmtpServerWorker>>();
|
||||
var configuration = provider.GetRequiredService<IConfiguration>();
|
||||
logger.LogInformation("SMTP advertised hostname (banner / EHLO): {AdvertisedHostname}", advertisedHostname);
|
||||
var options = new SmtpServerOptionsBuilder()
|
||||
.ServerName("aliasvault");
|
||||
.ServerName(advertisedHostname);
|
||||
|
||||
if (tlsAvailable && loadedCertificate != null)
|
||||
{
|
||||
|
||||
@@ -106,6 +106,30 @@ public class AbstractTestHostBuilder : IAsyncDisposable
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds extra in-memory configuration keys before the test <see cref="IConfiguration"/> is built.
|
||||
/// </summary>
|
||||
/// <param name="settings">Mutable dictionary merged into the configuration (includes database keys).</param>
|
||||
protected virtual void AddIntegrationTestConfiguration(IDictionary<string, string?> settings)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds the standard in-memory settings for integration tests, including database connection.
|
||||
/// </summary>
|
||||
/// <param name="aliasServerConnectionString">Connection string for <c>AliasServerDbContext</c>.</param>
|
||||
/// <returns>Dictionary passed to ConfigurationBuilder.AddInMemoryCollection.</returns>
|
||||
protected Dictionary<string, string?> CreateMemoryConfigurationSettings(string aliasServerConnectionString)
|
||||
{
|
||||
var settings = new Dictionary<string, string?>
|
||||
{
|
||||
["DatabaseProvider"] = "postgresql",
|
||||
["ConnectionStrings:AliasServerDbContext"] = aliasServerConnectionString,
|
||||
};
|
||||
AddIntegrationTestConfiguration(settings);
|
||||
return settings;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new test host builder with test database connection already configured.
|
||||
/// </summary>
|
||||
@@ -136,13 +160,10 @@ public class AbstractTestHostBuilder : IAsyncDisposable
|
||||
.ConfigureServices((context, services) =>
|
||||
{
|
||||
// Override configuration
|
||||
var memorySettings = CreateMemoryConfigurationSettings(dbConnection.ConnectionString);
|
||||
var configuration = new ConfigurationBuilder()
|
||||
.AddJsonFile("appsettings.json", optional: true)
|
||||
.AddInMemoryCollection(new Dictionary<string, string?>
|
||||
{
|
||||
["DatabaseProvider"] = "postgresql",
|
||||
["ConnectionStrings:AliasServerDbContext"] = dbConnection.ConnectionString,
|
||||
})
|
||||
.AddInMemoryCollection(memorySettings)
|
||||
.Build();
|
||||
|
||||
services.AddSingleton<IConfiguration>(configuration);
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
namespace AliasVault.IntegrationTests.SmtpServer;
|
||||
|
||||
using System.Net.Sockets;
|
||||
using System.Text;
|
||||
using AliasServerDb;
|
||||
using AliasVault.Cryptography.Server;
|
||||
@@ -120,6 +121,42 @@ public class SmtpServerTests
|
||||
await _testHostBuilder.DisposeAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The SMTP greeting line must include the configured advertised hostname (same path as production resolver).
|
||||
/// </summary>
|
||||
/// <returns>Task.</returns>
|
||||
[Test]
|
||||
public async Task SmtpBanner_ContainsConfiguredHostname()
|
||||
{
|
||||
using var client = new TcpClient();
|
||||
const int maxRetries = 10;
|
||||
const int retryDelayMs = 100;
|
||||
|
||||
for (var attempt = 1; attempt <= maxRetries; attempt++)
|
||||
{
|
||||
try
|
||||
{
|
||||
await client.ConnectAsync("127.0.0.1", 2525);
|
||||
break;
|
||||
}
|
||||
catch (SocketException) when (attempt < maxRetries)
|
||||
{
|
||||
await Task.Delay(retryDelayMs);
|
||||
}
|
||||
}
|
||||
|
||||
using var stream = client.GetStream();
|
||||
var buffer = new byte[512];
|
||||
var n = await stream.ReadAsync(buffer.AsMemory(0, buffer.Length));
|
||||
var line = Encoding.ASCII.GetString(buffer, 0, n);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(line, Does.StartWith("220 "));
|
||||
Assert.That(line, Does.Contain(TestHostBuilder.IntegrationAdvertisedHostname));
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests sending a single email in plain format to the SMTP server with valid claim to check if it is processed correctly.
|
||||
/// </summary>
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
namespace AliasVault.IntegrationTests.SmtpServer;
|
||||
|
||||
using System.Data.Common;
|
||||
using System.Net;
|
||||
using AliasVault.SmtpService;
|
||||
using AliasVault.SmtpService.Handlers;
|
||||
using AliasVault.SmtpService.Workers;
|
||||
@@ -22,6 +23,11 @@ using Microsoft.Extensions.Hosting;
|
||||
/// </summary>
|
||||
public class TestHostBuilder : AbstractTestHostBuilder
|
||||
{
|
||||
/// <summary>
|
||||
/// Hostname advertised in SMTP banner / EHLO for integration tests (must match production resolver usage).
|
||||
/// </summary>
|
||||
public const string IntegrationAdvertisedHostname = "mail.integration.test";
|
||||
|
||||
/// <summary>
|
||||
/// Builds the SmtpService test host with a provided database connection.
|
||||
/// </summary>
|
||||
@@ -29,6 +35,8 @@ public class TestHostBuilder : AbstractTestHostBuilder
|
||||
/// <returns>IHost.</returns>
|
||||
public IHost Build(DbConnection dbConnection)
|
||||
{
|
||||
Environment.SetEnvironmentVariable("SMTP_ADVERTISED_HOSTNAME", IntegrationAdvertisedHostname);
|
||||
|
||||
// Get base builder with database connection already configured.
|
||||
var builder = CreateBuilder();
|
||||
|
||||
@@ -37,13 +45,10 @@ public class TestHostBuilder : AbstractTestHostBuilder
|
||||
{
|
||||
// Override database connection with provided connection.
|
||||
services.Remove(services.First(x => x.ServiceType == typeof(IConfiguration)));
|
||||
var memorySettings = CreateMemoryConfigurationSettings(dbConnection.ConnectionString);
|
||||
var configuration = new ConfigurationBuilder()
|
||||
.AddJsonFile("appsettings.json", optional: true)
|
||||
.AddInMemoryCollection(new Dictionary<string, string?>
|
||||
{
|
||||
["DatabaseProvider"] = "postgresql",
|
||||
["ConnectionStrings:AliasServerDbContext"] = dbConnection.ConnectionString,
|
||||
})
|
||||
.AddInMemoryCollection(memorySettings)
|
||||
.Build();
|
||||
|
||||
services.AddSingleton<IConfiguration>(configuration);
|
||||
@@ -60,6 +65,8 @@ public class TestHostBuilder : AbstractTestHostBuilder
|
||||
/// <returns>IHost.</returns>
|
||||
public IHost Build()
|
||||
{
|
||||
Environment.SetEnvironmentVariable("SMTP_ADVERTISED_HOSTNAME", IntegrationAdvertisedHostname);
|
||||
|
||||
// Get base builder with database connection already configured.
|
||||
var builder = CreateBuilder();
|
||||
|
||||
@@ -78,18 +85,25 @@ public class TestHostBuilder : AbstractTestHostBuilder
|
||||
/// <param name="services">The service collection to configure.</param>
|
||||
private static void ConfigureSmtpServices(IServiceCollection services)
|
||||
{
|
||||
services.AddSingleton(new Config
|
||||
services.AddSingleton(provider =>
|
||||
{
|
||||
AllowedToDomains = new List<string> { "example.tld" },
|
||||
SmtpTlsEnabled = "false",
|
||||
var configuration = provider.GetRequiredService<IConfiguration>();
|
||||
return new Config
|
||||
{
|
||||
AllowedToDomains = new List<string> { "example.tld" },
|
||||
SmtpTlsEnabled = "false",
|
||||
};
|
||||
});
|
||||
|
||||
services.AddTransient<IMessageStore, DatabaseMessageStore>();
|
||||
services.AddSingleton<SmtpServer>(
|
||||
provider =>
|
||||
{
|
||||
var advertisedHostname = ResolveAdvertisedHostname(
|
||||
Environment.GetEnvironmentVariable("SMTP_ADVERTISED_HOSTNAME"),
|
||||
Dns.GetHostName);
|
||||
var options = new SmtpServerOptionsBuilder()
|
||||
.ServerName("aliasvault");
|
||||
.ServerName(advertisedHostname);
|
||||
|
||||
// Note: port 25 doesn't work in GitHub actions so we use these instead for the integration tests:
|
||||
// - 2525 for the SMTP server
|
||||
@@ -106,4 +120,28 @@ public class TestHostBuilder : AbstractTestHostBuilder
|
||||
|
||||
services.AddHostedService<SmtpServerWorker>();
|
||||
}
|
||||
|
||||
private static string ResolveAdvertisedHostname(
|
||||
string? environmentValue,
|
||||
Func<string> dnsHostNameFallback)
|
||||
{
|
||||
var fromEnvironment = TrimOrNull(environmentValue);
|
||||
if (fromEnvironment != null)
|
||||
{
|
||||
return fromEnvironment;
|
||||
}
|
||||
|
||||
return dnsHostNameFallback();
|
||||
}
|
||||
|
||||
private static string? TrimOrNull(string? value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var trimmed = value.Trim();
|
||||
return trimmed.Length == 0 ? null : trimmed;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -189,6 +189,21 @@ Important: DNS propagation can take up to 24-48 hours. During this time, email d
|
||||
|
||||
If you encounter any issues, feel free to join the [Discord chat](https://discord.gg/DsaXMTEtpF) to get help from other users and maintainers.
|
||||
|
||||
### Optional: SMTP advertised hostname
|
||||
|
||||
When you expose SMTP (ports 25 and 587), remote clients see a **hostname** in the SMTP **banner** and **EHLO** responses. This value defaults to `aliasvault` which works fine for general email deliverability, but some diagnostic tools may check that this name is consistent with **reverse DNS (PTR)** with your email MX record.
|
||||
|
||||
If you wish to set this value, update the `docker-compose.yml` file:
|
||||
|
||||
```bash
|
||||
# ...
|
||||
environment:
|
||||
SMTP_ADVERTISED_HOSTNAME: "mail.yourdomain1.com"
|
||||
# ...
|
||||
```
|
||||
|
||||
Restart the container after changing this value.
|
||||
|
||||
### Optional: SMTP TLS (STARTTLS)
|
||||
|
||||
By default, SMTP TLS is disabled (`SMTP_TLS_ENABLED: "false"`). This does NOT significantly impact email deliverability: most email providers will still deliver to your server. However, if you want to enable TLS for SMTP connections:
|
||||
|
||||
@@ -163,8 +163,9 @@ After setting up your DNS, continue with configuring AliasVault to let it know w
|
||||
./install.sh configure-email
|
||||
````
|
||||
2. Follow the interactive prompts to:
|
||||
- Configure your domain(s)
|
||||
- Restart required services
|
||||
1. Configure your domain(s)
|
||||
2. Set the SMTP advertised hostname to your main domain (helps with improving deliverability)
|
||||
3. Restart required services
|
||||
|
||||
3. Once configured, you can:
|
||||
- Create new aliases in the AliasVault client
|
||||
|
||||
25
install.sh
25
install.sh
@@ -1300,6 +1300,24 @@ update_env_var() {
|
||||
printf " ${GREEN}> $key has been set in $ENV_FILE.${NC}\n"
|
||||
}
|
||||
|
||||
# Prompt for SMTP_ADVERTISED_HOSTNAME (banner / EHLO). Empty value defaults to "aliasvault" at runtime.
|
||||
prompt_smtp_advertised_hostname() {
|
||||
local current
|
||||
current=$(grep "^SMTP_ADVERTISED_HOSTNAME=" "$ENV_FILE" 2>/dev/null | cut -d'=' -f2- | tr -d '\r' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
|
||||
|
||||
printf "\n${CYAN}Enter SMTP advertised hostname (banner / EHLO)${NC}\n"
|
||||
printf "This is shown to other mail servers when they connect, setting it can improve email deliverability.\n"
|
||||
printf "Example: mail.example.com. Leave blank to default to \"aliasvault\" (which is fine for general use).\n"
|
||||
if [ -n "$current" ]; then
|
||||
printf "Current value: ${CYAN}${current}${NC}\n"
|
||||
fi
|
||||
read -r -p "Hostname: " user_in
|
||||
|
||||
local trimmed
|
||||
trimmed=$(echo "$user_in" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
|
||||
update_env_var "SMTP_ADVERTISED_HOSTNAME" "$trimmed"
|
||||
}
|
||||
|
||||
# Helper function to write secrets to files instead of .env
|
||||
write_secret_to_file() {
|
||||
local secret_name=$1
|
||||
@@ -2182,6 +2200,8 @@ handle_email_configuration() {
|
||||
fi
|
||||
done
|
||||
|
||||
prompt_smtp_advertised_hostname
|
||||
|
||||
# Update .env file and restart
|
||||
if ! update_env_var "PRIVATE_EMAIL_DOMAINS" "$new_domains"; then
|
||||
printf "${RED}Failed to update configuration.${NC}\n"
|
||||
@@ -3343,6 +3363,11 @@ check_and_populate_env() {
|
||||
update_env_var "SMTP_TLS_PORT" "587"
|
||||
printf " Set SMTP_TLS_PORT\n"
|
||||
fi
|
||||
|
||||
# SMTP_ADVERTISED_HOSTNAME
|
||||
if ! grep -q "^SMTP_ADVERTISED_HOSTNAME=" "$ENV_FILE" 2>/dev/null; then
|
||||
update_env_var "SMTP_ADVERTISED_HOSTNAME" ""
|
||||
fi
|
||||
}
|
||||
|
||||
main "$@"
|
||||
|
||||
Reference in New Issue
Block a user