Skip to content

Commit

Permalink
Update
Browse files Browse the repository at this point in the history
* `DiscoveryConfig.GetDiscoveryConfig` is now asynchronous and named `GetDiscoveryConfigAsync`
* `DomainConfig.LoginNameMapping` is now a `Dictionary<string, string>`
+ Added `DiscoveryConfig.EmailMappings`
+ Added `DomainConfig.GetConfig`
+ Added `EmailMapping`
+ Added `CommandLineInterface.ParsePostfixEmailMappingsAsync`
  • Loading branch information
nd1012 committed Apr 6, 2024
1 parent 29090dd commit 86429c5
Show file tree
Hide file tree
Showing 6 changed files with 172 additions and 21 deletions.
48 changes: 45 additions & 3 deletions src/wan24-AutoDiscover Shared/Models/DiscoveryConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using System.Collections.Frozen;
using System.ComponentModel.DataAnnotations;
using System.Net;
using System.Net.Mail;
using System.Text.Json.Serialization;
using wan24.Core;

Expand Down Expand Up @@ -55,12 +56,17 @@ public DiscoveryConfig() { }
/// </summary>
public IReadOnlySet<IPAddress> KnownProxies { get; init; } = new HashSet<IPAddress>();

/// <summary>
/// JSON file path which contains the email mappings list
/// </summary>
public string? EmailMappings { get; init; }

/// <summary>
/// Get the discovery configuration
/// </summary>
/// <param name="config">Configuration</param>
/// <returns>Discovery configuration</returns>
public virtual IReadOnlyDictionary<string, DomainConfig> GetDiscoveryConfig(IConfigurationRoot config)
public virtual async Task<IReadOnlyDictionary<string, DomainConfig>> GetDiscoveryConfigAsync(IConfigurationRoot config)
{
Type discoveryType = DiscoveryType;
if (!typeof(IDictionary).IsAssignableFrom(discoveryType))
Expand All @@ -82,9 +88,45 @@ public virtual IReadOnlyDictionary<string, DomainConfig> GetDiscoveryConfig(ICon
values = new object[discovery.Count];
discovery.Keys.CopyTo(keys, index: 0);
discovery.Values.CopyTo(values, index: 0);
return new Dictionary<string, DomainConfig>(
Dictionary<string, DomainConfig> discoveryDomains = new(
Enumerable.Range(0, discovery.Count).Select(i => new KeyValuePair<string, DomainConfig>((string)keys[i], (DomainConfig)values[i]))
).ToFrozenDictionary();
);
// Apply email mappings
if (EmailMappings is not null)
if (File.Exists(EmailMappings))
{
Logging.WriteInfo($"Loading email mappings from \"{EmailMappings}\"");
FileStream fs = FsHelper.CreateFileStream(EmailMappings, FileMode.Open, FileAccess.Read, FileShare.Read);
await using (fs.DynamicContext())
{
EmailMapping[] mappings = await JsonHelper.DecodeAsync<EmailMapping[]>(fs).DynamicContext()
?? throw new InvalidDataException("Invalid email mappings");
foreach(EmailMapping mapping in mappings)
{
if (!mapping.Email.Contains('@'))
continue;
string email = mapping.Email.ToLower();
if (
!MailAddress.TryCreate(mapping.Email, out MailAddress? emailAddress) ||
(emailAddress.User.Length == 1 && (emailAddress.User[0] == '*' || emailAddress.User[0] == '@')) ||
EmailMapping.GetLoginUser(mappings, email) is not string loginUser
)
continue;
string[] emailParts = mapping.Email.ToLower().Split('@', 2);
if (emailParts.Length != 2 || DomainConfig.GetConfig(string.Empty, emailParts) is not DomainConfig domain)
continue;
if (Logging.Debug)
Logging.WriteDebug($"Mapping email address \"{email}\" to login user \"{loginUser}\"");
domain.LoginNameMapping ??= [];
domain.LoginNameMapping[email] = loginUser;
}
}
}
else
{
Logging.WriteWarning($"Email mappings file \"{EmailMappings}\" not found");
}
return discoveryDomains.ToFrozenDictionary();
}
}
}
22 changes: 20 additions & 2 deletions src/wan24-AutoDiscover Shared/Models/DomainConfig.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Xml;
using System.Net.Http;
using System.Xml;
using wan24.ObjectValidation;

namespace wan24.AutoDiscover.Models
Expand Down Expand Up @@ -34,7 +35,7 @@ public DomainConfig() { }
/// Login name mapping (key is the email address or alias, value the mapped login name)
/// </summary>
[RequiredIf(nameof(LoginNameMappingRequired), true)]
public IReadOnlyDictionary<string, string>? LoginNameMapping { get; init; }
public Dictionary<string, string>? LoginNameMapping { get; set; }

/// <summary>
/// If a successfule login name mapping is required (if no mapping was possible, the email address will be used as login name)
Expand All @@ -51,5 +52,22 @@ public virtual void CreateXml(XmlDocument xml, XmlNode account, string[] emailPa
{
foreach (Protocol protocol in Protocols) protocol.CreateXml(xml, account, emailParts, this);
}

/// <summary>
/// Get a domain configuration which matches an email address
/// </summary>
/// <param name="host">Hostname</param>
/// <param name="emailParts">Splitted email parts</param>
/// <returns>Domain configuration</returns>
public static DomainConfig? GetConfig(string host, string[] emailParts)
=> !Registered.TryGetValue(emailParts[1], out DomainConfig? config) &&
(host.Length == 0 || !Registered.TryGetValue(host, out config)) &&
!Registered.TryGetValue(
Registered.Where(kvp => kvp.Value.AcceptedDomains?.Contains(emailParts[1], StringComparer.OrdinalIgnoreCase) ?? false)
.Select(kvp => kvp.Key)
.FirstOrDefault() ?? string.Empty,
out config)
? null
: config;
}
}
57 changes: 57 additions & 0 deletions src/wan24-AutoDiscover Shared/Models/EmailMapping.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
using System.ComponentModel.DataAnnotations;
using wan24.ObjectValidation;

namespace wan24.AutoDiscover.Models
{
/// <summary>
/// Email mapping
/// </summary>
public record class EmailMapping
{
/// <summary>
/// Constructor
/// </summary>
public EmailMapping() { }

/// <summary>
/// Emailaddress
/// </summary>
[EmailAddress]
public required string Email { get; init; }

/// <summary>
/// Target email addresses or user names
/// </summary>
[CountLimit(1, int.MaxValue)]
public required IReadOnlyList<string> Targets { get; init; }

/// <summary>
/// Get the login user from email mappings for an email address
/// </summary>
/// <param name="mappings">Mappings</param>
/// <param name="email">Email address</param>
/// <returns>Login user</returns>
public static string? GetLoginUser(IEnumerable<EmailMapping> mappings, string email)
{
if (mappings.FirstOrDefault(m => m.Email.Equals(email, StringComparison.OrdinalIgnoreCase)) is not EmailMapping mapping)
return null;
if (mapping.Targets.FirstOrDefault(t => !t.Contains('@')) is string loginName)
return loginName;
HashSet<string> seen = [email];
Queue<string> emails = [];
foreach (string target in mapping.Targets) emails.Enqueue(target.ToLower());
while(emails.TryDequeue(out string? target))
{
if (
!seen.Add(target) ||
mappings.FirstOrDefault(m => m.Email.Equals(email, StringComparison.OrdinalIgnoreCase)) is not EmailMapping targetMapping
)
continue;
if (targetMapping.Targets.FirstOrDefault(t => !t.Contains('@')) is string targetLoginName)
return targetLoginName;
foreach (string subTarget in targetMapping.Targets) emails.Enqueue(subTarget.ToLower());
}
return null;
}
}
}
11 changes: 1 addition & 10 deletions src/wan24-AutoDiscover/Controllers/DiscoveryController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -108,16 +108,7 @@ public async Task<ContentResult> AutoDiscoverAsync()
if (Logging.Debug)
Logging.WriteDebug($"Creating POX response for {emailAddress.ToQuotedLiteral()} request from {HttpContext.Connection.RemoteIpAddress}:{HttpContext.Connection.RemotePort}");
XmlDocument xml = await Responses.GetOneAsync(HttpContext.RequestAborted).DynamicContext();
if (
!DomainConfig.Registered.TryGetValue(emailParts[1], out DomainConfig? config) &&
!DomainConfig.Registered.TryGetValue(HttpContext.Request.Host.Host, out config) &&
!DomainConfig.Registered.TryGetValue(
DomainConfig.Registered.Where(kvp => kvp.Value.AcceptedDomains?.Contains(emailParts[1], StringComparer.OrdinalIgnoreCase) ?? false)
.Select(kvp => kvp.Key)
.FirstOrDefault() ?? string.Empty,
out config
)
)
if (DomainConfig.GetConfig(HttpContext.Request.Host.Host,emailParts) is not DomainConfig config)
throw new BadHttpRequestException($"Unknown request domain name \"{HttpContext.Request.Host.Host}\"/{emailParts[1].ToQuotedLiteral()}");
config.CreateXml(xml, xml.SelectSingleNode(ACCOUNT_NODE_XPATH) ?? throw new InvalidProgramException("Missing response XML account node"), emailParts);
return new()
Expand Down
10 changes: 5 additions & 5 deletions src/wan24-AutoDiscover/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,17 @@

// Load the configuration
string configFile = Path.Combine(ENV.AppFolder, "appsettings.json");
IConfigurationRoot LoadConfig()
async Task<IConfigurationRoot> LoadConfigAsync()
{
ConfigurationBuilder configBuilder = new();
configBuilder.AddJsonFile(configFile, optional: false);
IConfigurationRoot config = configBuilder.Build();
DiscoveryConfig.Current = config.GetRequiredSection("DiscoveryConfig").Get<DiscoveryConfig>()
?? throw new InvalidDataException($"Failed to get a {typeof(DiscoveryConfig)} from the \"DiscoveryConfig\" section");
DomainConfig.Registered = DiscoveryConfig.Current.GetDiscoveryConfig(config);
DomainConfig.Registered = await DiscoveryConfig.Current.GetDiscoveryConfigAsync(config).DynamicContext();
return config;
}
IConfigurationRoot config = LoadConfig();
IConfigurationRoot config = await LoadConfigAsync().DynamicContext();

// Initialize wan24-Core
await Bootstrap.Async().DynamicContext();
Expand All @@ -45,15 +45,15 @@ IConfigurationRoot LoadConfig()

// Watch configuration changes
using ConfigChangeEventThrottle fswThrottle = new();
ConfigChangeEventThrottle.OnConfigChange += () =>
ConfigChangeEventThrottle.OnConfigChange += async () =>
{
try
{
Logging.WriteDebug("Handling configuration change");
if (File.Exists(configFile))
{
Logging.WriteInfo($"Auto-reloading changed configuration from \"{configFile}\"");
LoadConfig();
await LoadConfigAsync().DynamicContext();
}
else
{
Expand Down
45 changes: 44 additions & 1 deletion src/wan24-AutoDiscover/Services/CommandLineInterface.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using System.Text;
using System.Text.RegularExpressions;
using wan24.AutoDiscover.Models;
using wan24.CLI;
using wan24.Core;

Expand All @@ -8,8 +10,13 @@ namespace wan24.AutoDiscover.Services
/// CLI API
/// </summary>
[CliApi("autodiscover")]
public class CommandLineInterface
public partial class CommandLineInterface
{
/// <summary>
/// Regular expression to match a Postfix email mapping (<c>$1</c> contains the email address, <c>$2</c> contains the comma separated targets)
/// </summary>
private static readonly Regex RX_POSTFIX = RX_POSTFIX_Generator();

/// <summary>
/// Constructor
/// </summary>
Expand All @@ -27,5 +34,41 @@ public static async Task CreateSystemdServiceAsync()
using (StreamWriter writer = new(stdOut, Encoding.UTF8, leaveOpen: true))
await writer.WriteLineAsync(new SystemdServiceFile().ToString().Trim()).DynamicContext();
}

/// <summary>
/// Parse Postfix email mappings
/// </summary>
[CliApi("postfix")]
[StdIn("/etc/postfix/virtual")]
[StdOut("/home/autodiscover/postfix.json")]
public static async Task ParsePostfixEmailMappingsAsync()
{
HashSet<EmailMapping> mappings = [];
Stream stdIn = Console.OpenStandardInput();
await using (stdIn.DynamicContext())
{
using StreamReader reader = new(stdIn, Encoding.UTF8, leaveOpen: true);
while(await reader.ReadLineAsync().DynamicContext() is string line)
{
if (!RX_POSTFIX.IsMatch(line)) continue;
string[] info = RX_POSTFIX.Replace(line, "$1\t$2").Split('\t', 2);
mappings.Add(new()
{
Email = info[0].ToLower(),
Targets = new List<string>((from target in info[1].Split(',') select target.Trim()).Distinct())
});
}
}
Stream stdOut = Console.OpenStandardOutput();
await using (stdOut.DynamicContext())
await JsonHelper.EncodeAsync(mappings, stdOut, prettify: true).DynamicContext();
}

/// <summary>
/// Regular expression to match a Postfix email mapping (<c>$1</c> contains the email address, <c>$2</c> contains the comma separated targets)
/// </summary>
/// <returns>Regular expression</returns>
[GeneratedRegex(@"^\s*([^\*\@#][^\s]+)\s*([^\s]+)\s*$", RegexOptions.Compiled)]
private static partial Regex RX_POSTFIX_Generator();
}
}

0 comments on commit 86429c5

Please sign in to comment.