diff --git a/Rnwood.Smtp4dev/TUI/ManagementDialogs.cs b/Rnwood.Smtp4dev/TUI/ManagementDialogs.cs index 1c7dea36a..a7eee5149 100644 --- a/Rnwood.Smtp4dev/TUI/ManagementDialogs.cs +++ b/Rnwood.Smtp4dev/TUI/ManagementDialogs.cs @@ -16,12 +16,14 @@ public class UsersDialog : Dialog private readonly string dataDir; private ListView userListView; private SettingsManager settingsManager; + private ServerOptions serverOptions; public UsersDialog(IHost host, string dataDir) : base("Manage Users", 60, 20) { this.host = host; this.dataDir = dataDir; this.settingsManager = new SettingsManager(host, dataDir); + this.serverOptions = settingsManager.GetServerOptions(); CreateUI(); } @@ -65,7 +67,6 @@ private void CreateUI() private void RefreshList() { - var serverOptions = settingsManager.GetServerOptions(); var users = serverOptions.Users?.Select(u => u.Username).ToList() ?? new System.Collections.Generic.List(); userListView.SetSource(users); } @@ -89,7 +90,6 @@ private void AddUser() if (!string.IsNullOrWhiteSpace(username) && !string.IsNullOrWhiteSpace(password)) { - var serverOptions = settingsManager.GetServerOptions(); var usersList = serverOptions.Users?.ToList() ?? new List(); usersList.Add(new UserOptions @@ -99,7 +99,6 @@ private void AddUser() }); serverOptions.Users = usersList.ToArray(); - settingsManager.SaveSettings(serverOptions, settingsManager.GetRelayOptions()).Wait(); RefreshList(); Application.RequestStop(); } @@ -121,7 +120,6 @@ private void RemoveUser() { if (userListView.SelectedItem >= 0) { - var serverOptions = settingsManager.GetServerOptions(); if (serverOptions.Users != null && userListView.SelectedItem < serverOptions.Users.Length) { var username = serverOptions.Users[userListView.SelectedItem].Username; @@ -134,7 +132,6 @@ private void RemoveUser() var usersList = serverOptions.Users.ToList(); usersList.RemoveAt(userListView.SelectedItem); serverOptions.Users = usersList.ToArray(); - settingsManager.SaveSettings(serverOptions, settingsManager.GetRelayOptions()).Wait(); RefreshList(); } } @@ -151,12 +148,14 @@ public class MailboxesDialog : Dialog private readonly string dataDir; private ListView mailboxListView; private SettingsManager settingsManager; + private ServerOptions serverOptions; public MailboxesDialog(IHost host, string dataDir) : base("Manage Mailboxes", 60, 20) { this.host = host; this.dataDir = dataDir; this.settingsManager = new SettingsManager(host, dataDir); + this.serverOptions = settingsManager.GetServerOptions(); CreateUI(); } @@ -200,7 +199,6 @@ private void CreateUI() private void RefreshList() { - var serverOptions = settingsManager.GetServerOptions(); var mailboxes = serverOptions.Mailboxes?.Select(m => $"{m.Name} ({m.Recipients})").ToList() ?? new System.Collections.Generic.List(); mailboxListView.SetSource(mailboxes); @@ -225,7 +223,6 @@ private void AddMailbox() if (!string.IsNullOrWhiteSpace(name) && !string.IsNullOrWhiteSpace(pattern)) { - var serverOptions = settingsManager.GetServerOptions(); var mailboxesList = serverOptions.Mailboxes?.ToList() ?? new List(); mailboxesList.Add(new MailboxOptions @@ -235,8 +232,6 @@ private void AddMailbox() }); serverOptions.Mailboxes = mailboxesList.ToArray(); - - settingsManager.SaveSettings(serverOptions, settingsManager.GetRelayOptions()).Wait(); RefreshList(); Application.RequestStop(); } @@ -258,7 +253,6 @@ private void RemoveMailbox() { if (mailboxListView.SelectedItem >= 0) { - var serverOptions = settingsManager.GetServerOptions(); if (serverOptions.Mailboxes != null && mailboxListView.SelectedItem < serverOptions.Mailboxes.Length) { var mailbox = serverOptions.Mailboxes[mailboxListView.SelectedItem]; @@ -271,7 +265,6 @@ private void RemoveMailbox() var mailboxesList = serverOptions.Mailboxes.ToList(); mailboxesList.RemoveAt(mailboxListView.SelectedItem); serverOptions.Mailboxes = mailboxesList.ToArray(); - settingsManager.SaveSettings(serverOptions, settingsManager.GetRelayOptions()).Wait(); RefreshList(); } } diff --git a/Rnwood.Smtp4dev/TUI/MessagesTab.cs b/Rnwood.Smtp4dev/TUI/MessagesTab.cs index 78e887cd8..0d7ed6abd 100644 --- a/Rnwood.Smtp4dev/TUI/MessagesTab.cs +++ b/Rnwood.Smtp4dev/TUI/MessagesTab.cs @@ -19,7 +19,7 @@ public class MessagesTab { private readonly IHost host; private View container; - private ListView messageListView; + private TableView messageTableView; private View detailsPanel; private TextView overviewBodyTextView; private TableView headersTableView; @@ -32,7 +32,7 @@ public class MessagesTab private List filteredMessages = new List(); private Message selectedMessage; private string searchFilter = string.Empty; - private int lastSelectedIndex = -1; + private int lastSelectedRow = -1; public MessagesTab(IHost host) { @@ -51,47 +51,67 @@ private void CreateUI() Height = Dim.Fill() }; - // Status label and search field at top + // Status label at top statusLabel = new Label("Loading messages...") { X = 0, Y = 0, - Width = Dim.Percent(50) + Width = Dim.Fill() }; + container.Add(statusLabel); + + // Left panel - Message table (40% width) - no frame + messageTableView = new TableView() + { + X = 0, + Y = 1, + Width = Dim.Percent(40), + Height = Dim.Fill() - 4, + FullRowSelect = true + }; + + // Setup table columns + var table = new DataTable(); + table.Columns.Add("", typeof(string)); // Unread indicator + table.Columns.Add("Date", typeof(string)); + table.Columns.Add("From", typeof(string)); + table.Columns.Add("Subject", typeof(string)); + messageTableView.Table = table; + + messageTableView.SelectedCellChanged += OnMessageSelected; + messageTableView.KeyPress += (e) => { + if (e.KeyEvent.Key == Key.DeleteChar || e.KeyEvent.Key == Key.Backspace) + { + DeleteSelected(); + e.Handled = true; + } + }; + + container.Add(messageTableView); + + // Search box below the list var searchLabel = new Label("Search:") { - X = Pos.Right(statusLabel) + 2, - Y = 0 + X = 0, + Y = Pos.Bottom(messageTableView) }; searchField = new TextField("") { X = Pos.Right(searchLabel) + 1, - Y = 0, - Width = Dim.Fill() - 1 + Y = Pos.Bottom(messageTableView), + Width = Dim.Fill() - 10 }; searchField.TextChanged += (old) => ApplyFilter(); - - container.Add(statusLabel, searchLabel, searchField); - - // Left panel - Message list (40% width) - no frame - messageListView = new ListView() - { - X = 0, - Y = 1, - Width = Dim.Percent(40), - Height = Dim.Fill() - 3, - AllowsMarking = false, - CanFocus = true - }; - messageListView.SelectedItemChanged += OnMessageSelected; + container.Add(searchLabel, searchField); + // Action buttons below search var deleteButton = new Button("Delete") { X = 0, - Y = Pos.Bottom(messageListView), + Y = Pos.Bottom(searchField), Width = 10 }; deleteButton.Clicked += () => DeleteSelected(); @@ -99,7 +119,7 @@ private void CreateUI() var deleteAllButton = new Button("Delete All") { X = Pos.Right(deleteButton) + 1, - Y = Pos.Bottom(messageListView), + Y = Pos.Bottom(searchField), Width = 12 }; deleteAllButton.Clicked += () => DeleteAll(); @@ -107,17 +127,17 @@ private void CreateUI() var composeButton = new Button("Compose") { X = Pos.Right(deleteAllButton) + 1, - Y = Pos.Bottom(messageListView), + Y = Pos.Bottom(searchField), Width = 10 }; composeButton.Clicked += () => ComposeMessage(); - container.Add(messageListView, deleteButton, deleteAllButton, composeButton); + container.Add(deleteButton, deleteAllButton, composeButton); // Right panel - Message details (60% width) - no frame detailsPanel = new View() { - X = Pos.Right(messageListView) + 1, + X = Pos.Right(messageTableView) + 1, Y = 1, Width = Dim.Fill(), Height = Dim.Fill() - 1 @@ -253,7 +273,10 @@ public View GetView() public void Refresh() { // Save current selection - lastSelectedIndex = messageListView.SelectedItem; + if (messageTableView.Table != null && messageTableView.SelectedRow >= 0) + { + lastSelectedRow = messageTableView.SelectedRow; + } var dbContext = host.Services.GetRequiredService(); messages = dbContext.Messages @@ -282,28 +305,39 @@ private void ApplyFilter() ).ToList(); } - var messageStrings = filteredMessages.Select(m => + var table = new DataTable(); + table.Columns.Add("", typeof(string)); // Unread indicator + table.Columns.Add("Date", typeof(string)); + table.Columns.Add("From", typeof(string)); + table.Columns.Add("Subject", typeof(string)); + + foreach (var message in filteredMessages) { - var unreadIndicator = m.IsUnread ? "* " : " "; - return $"{unreadIndicator}{m.ReceivedDate:yyyy-MM-dd HH:mm} | {TruncateString(m.From ?? "", 20)} | {TruncateString(m.Subject ?? "", 35)}"; - }).ToList(); + table.Rows.Add( + message.IsUnread ? "*" : " ", + message.ReceivedDate.ToString("yyyy-MM-dd HH:mm"), + TruncateString(message.From ?? "", 20), + TruncateString(message.Subject ?? "", 35) + ); + } - messageListView.SetSource(messageStrings); + messageTableView.Table = table; + statusLabel.Text = $"Messages: {filteredMessages.Count}/{messages.Count}"; // Restore selection if possible - if (lastSelectedIndex >= 0 && lastSelectedIndex < filteredMessages.Count) + if (lastSelectedRow >= 0 && lastSelectedRow < filteredMessages.Count) { - messageListView.SelectedItem = lastSelectedIndex; + messageTableView.SelectedRow = lastSelectedRow; } } - private void OnMessageSelected(ListViewItemEventArgs args) + private void OnMessageSelected(TableView.SelectedCellChangedEventArgs args) { - if (args.Item >= 0 && args.Item < filteredMessages.Count) + if (args.NewRow >= 0 && args.NewRow < filteredMessages.Count) { - lastSelectedIndex = args.Item; - selectedMessage = filteredMessages[args.Item]; + lastSelectedRow = args.NewRow; + selectedMessage = filteredMessages[args.NewRow]; ShowMessageDetails(); } } diff --git a/Rnwood.Smtp4dev/TUI/SessionsTab.cs b/Rnwood.Smtp4dev/TUI/SessionsTab.cs index abcabffe0..8dfa137e9 100644 --- a/Rnwood.Smtp4dev/TUI/SessionsTab.cs +++ b/Rnwood.Smtp4dev/TUI/SessionsTab.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Data; using System.Linq; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; @@ -18,7 +19,7 @@ public class SessionsTab { private readonly IHost host; private View container; - private ListView sessionListView; + private TableView sessionTableView; private TextView logTextView; private Label statusLabel; private TextField searchField; @@ -28,7 +29,7 @@ public class SessionsTab private Session selectedSession; private string searchFilter = string.Empty; private bool showErrorsOnly = false; - private int lastSelectedIndex = -1; + private int lastSelectedRow = -1; public SessionsTab(IHost host) { @@ -47,24 +48,56 @@ private void CreateUI() Height = Dim.Fill() }; - // Status label, search field, and filter checkbox at top + // Status label at top statusLabel = new Label("Loading sessions...") { X = 0, Y = 0, - Width = Dim.Percent(30) + Width = Dim.Fill() }; + container.Add(statusLabel); + + // Left panel - Session table (40% width) - no frame + sessionTableView = new TableView() + { + X = 0, + Y = 1, + Width = Dim.Percent(40), + Height = Dim.Fill() - 5, + FullRowSelect = true + }; + + // Setup table columns + var table = new DataTable(); + table.Columns.Add("", typeof(string)); // Error indicator + table.Columns.Add("Date", typeof(string)); + table.Columns.Add("Client", typeof(string)); + table.Columns.Add("Msgs", typeof(string)); + sessionTableView.Table = table; + + sessionTableView.SelectedCellChanged += OnSessionSelected; + sessionTableView.KeyPress += (e) => { + if (e.KeyEvent.Key == Key.DeleteChar || e.KeyEvent.Key == Key.Backspace) + { + DeleteSelected(); + e.Handled = true; + } + }; + + container.Add(sessionTableView); + + // Search box and filter below the list var searchLabel = new Label("Search:") { - X = Pos.Right(statusLabel) + 2, - Y = 0 + X = 0, + Y = Pos.Bottom(sessionTableView) }; searchField = new TextField("") { X = Pos.Right(searchLabel) + 1, - Y = 0, + Y = Pos.Bottom(sessionTableView), Width = Dim.Percent(30) }; searchField.TextChanged += (old) => ApplyFilter(); @@ -72,32 +105,20 @@ private void CreateUI() errorOnlyCheckbox = new CheckBox("Errors Only") { X = Pos.Right(searchField) + 2, - Y = 0 + Y = Pos.Bottom(sessionTableView) }; errorOnlyCheckbox.Toggled += (old) => { showErrorsOnly = errorOnlyCheckbox.Checked; ApplyFilter(); }; - container.Add(statusLabel, searchLabel, searchField, errorOnlyCheckbox); - - // Left panel - Session list (40% width) - no frame - sessionListView = new ListView() - { - X = 0, - Y = 1, - Width = Dim.Percent(40), - Height = Dim.Fill() - 3, - AllowsMarking = false, - CanFocus = true - }; - - sessionListView.SelectedItemChanged += OnSessionSelected; + container.Add(searchLabel, searchField, errorOnlyCheckbox); + // Action buttons below search var deleteButton = new Button("Delete") { X = 0, - Y = Pos.Bottom(sessionListView), + Y = Pos.Bottom(searchField), Width = 10 }; deleteButton.Clicked += () => DeleteSelected(); @@ -105,17 +126,17 @@ private void CreateUI() var deleteAllButton = new Button("Delete All") { X = Pos.Right(deleteButton) + 1, - Y = Pos.Bottom(sessionListView), + Y = Pos.Bottom(searchField), Width = 12 }; deleteAllButton.Clicked += () => DeleteAll(); - container.Add(sessionListView, deleteButton, deleteAllButton); + container.Add(deleteButton, deleteAllButton); // Right panel - Session log (60% width) - no frame logTextView = new TextView() { - X = Pos.Right(sessionListView) + 1, + X = Pos.Right(sessionTableView) + 1, Y = 1, Width = Dim.Fill(), Height = Dim.Fill() - 1, @@ -137,7 +158,10 @@ public View GetView() public void Refresh() { // Save current selection - lastSelectedIndex = sessionListView.SelectedItem; + if (sessionTableView.Table != null && sessionTableView.SelectedRow >= 0) + { + lastSelectedRow = sessionTableView.SelectedRow; + } var dbContext = host.Services.GetRequiredService(); sessions = dbContext.Sessions @@ -172,29 +196,42 @@ private void ApplyFilter() } var dbContext = host.Services.GetRequiredService(); - var sessionStrings = filteredSessions.Select(s => + + var table = new DataTable(); + table.Columns.Add("", typeof(string)); // Error indicator + table.Columns.Add("Date", typeof(string)); + table.Columns.Add("Client", typeof(string)); + table.Columns.Add("Msgs", typeof(string)); + + foreach (var session in filteredSessions) { - var errorIndicator = !string.IsNullOrEmpty(s.SessionError) ? "[ERR] " : " "; - var messageCount = dbContext.Messages.Count(m => m.Session.Id == s.Id); - return $"{errorIndicator}{s.StartDate:yyyy-MM-dd HH:mm} | {TruncateString(s.ClientAddress, 20)} | Msgs: {messageCount}"; - }).ToList(); + var errorIndicator = !string.IsNullOrEmpty(session.SessionError) ? "[ERR]" : ""; + var messageCount = dbContext.Messages.Count(m => m.Session.Id == session.Id); + table.Rows.Add( + errorIndicator, + session.StartDate.ToString("yyyy-MM-dd HH:mm"), + TruncateString(session.ClientAddress, 20), + messageCount.ToString() + ); + } - sessionListView.SetSource(sessionStrings); + sessionTableView.Table = table; + statusLabel.Text = $"Sessions: {filteredSessions.Count}/{sessions.Count}"; // Restore selection if possible - if (lastSelectedIndex >= 0 && lastSelectedIndex < filteredSessions.Count) + if (lastSelectedRow >= 0 && lastSelectedRow < filteredSessions.Count) { - sessionListView.SelectedItem = lastSelectedIndex; + sessionTableView.SelectedRow = lastSelectedRow; } } - private void OnSessionSelected(ListViewItemEventArgs args) + private void OnSessionSelected(TableView.SelectedCellChangedEventArgs args) { - if (args.Item >= 0 && args.Item < filteredSessions.Count) + if (args.NewRow >= 0 && args.NewRow < filteredSessions.Count) { - lastSelectedIndex = args.Item; - selectedSession = filteredSessions[args.Item]; + lastSelectedRow = args.NewRow; + selectedSession = filteredSessions[args.NewRow]; ShowSessionLog(); } } diff --git a/Rnwood.Smtp4dev/TUI/SettingsDialog.cs b/Rnwood.Smtp4dev/TUI/SettingsDialog.cs index 45c443a26..d40d019c7 100644 --- a/Rnwood.Smtp4dev/TUI/SettingsDialog.cs +++ b/Rnwood.Smtp4dev/TUI/SettingsDialog.cs @@ -1,7 +1,9 @@ using System; +using System.Collections.Generic; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Terminal.Gui; +using Rnwood.Smtp4dev.Server; using Rnwood.Smtp4dev.Server.Settings; namespace Rnwood.Smtp4dev.TUI @@ -13,21 +15,48 @@ public class SettingsDialog : Dialog { private readonly IHost host; private readonly string dataDir; + + // Server settings private TextField smtpPortField; private TextField hostnameField; private CheckBox remoteConnectionsCheck; - private CheckBox requireAuthCheck; private TextField imapPortField; private TextField pop3PortField; + private TextField basePathField; + private TextField bindAddressField; + private CheckBox disableIPv6Check; + + // Storage settings + private TextField messagesToKeepField; + private TextField sessionsToKeepField; + private TextField databaseField; + + // TLS settings + private ComboBox tlsModeCombo; + private TextField tlsCertificateField; + private TextField tlsCertificatePasswordField; + private CheckBox secureConnectionRequiredCheck; + + // Authentication settings + private CheckBox authenticationRequiredCheck; + private CheckBox smtpAllowAnyCredentialsCheck; + private CheckBox webAuthenticationRequiredCheck; + + // Message processing settings + private CheckBox disableMessageSanitisationCheck; + private CheckBox disableHtmlValidationCheck; + private CheckBox disableHtmlCompatibilityCheckCheck; + private TextField maxMessageSizeField; + + // Relay settings private TextField relayServerField; private TextField relayPortField; private TextField relayUsernameField; private TextField relayPasswordField; - private TextField messagesToKeepField; - private TextField sessionsToKeepField; - private TextField basePathField; + private TextField relaySenderAddressField; + private ComboBox relayTlsModeCombo; - public SettingsDialog(IHost host, string dataDir) : base("Settings", 90, 30) + public SettingsDialog(IHost host, string dataDir) : base("Settings", 100, 40) { this.host = host; this.dataDir = dataDir; @@ -36,232 +65,439 @@ public SettingsDialog(IHost host, string dataDir) : base("Settings", 90, 30) private void CreateUI() { + // Create a scrollable view for all settings + var scrollView = new ScrollView() + { + X = 0, + Y = 0, + Width = Dim.Fill(), + Height = Dim.Fill() - 4, + ContentSize = new Size(90, 60), + ShowVerticalScrollIndicator = true, + ShowHorizontalScrollIndicator = false + }; + + var contentView = new View() + { + X = 0, + Y = 0, + Width = 90, + Height = 60 + }; + var y = 0; - // SMTP Settings section - var smtpLabel = new Label("SMTP Server Settings:") + // SMTP Server Settings section + contentView.Add(new Label("═══ SMTP Server Settings ═══") { X = 1, Y = y++, - ColorScheme = Colors.TopLevel - }; - Add(smtpLabel); + ColorScheme = Colors.Dialog + }); - Add(new Label("Port:") + contentView.Add(new Label("Port:") { X = 3, Y = y }); smtpPortField = new TextField("") { - X = 25, + X = 30, Y = y++, Width = 10 }; - Add(smtpPortField); + contentView.Add(smtpPortField); - Add(new Label("Hostname:") + contentView.Add(new Label("Hostname:") { X = 3, Y = y }); hostnameField = new TextField("") { - X = 25, + X = 30, Y = y++, Width = 40 }; - Add(hostnameField); + contentView.Add(hostnameField); - Add(new Label("Base Path:") + contentView.Add(new Label("Base Path:") { X = 3, Y = y }); basePathField = new TextField("") { - X = 25, + X = 30, Y = y++, Width = 40 }; - Add(basePathField); + contentView.Add(basePathField); + + contentView.Add(new Label("Bind Address:") + { + X = 3, + Y = y + }); + bindAddressField = new TextField("") + { + X = 30, + Y = y++, + Width = 30 + }; + contentView.Add(bindAddressField); remoteConnectionsCheck = new CheckBox("Allow Remote Connections") { X = 3, Y = y++ }; - Add(remoteConnectionsCheck); + contentView.Add(remoteConnectionsCheck); - requireAuthCheck = new CheckBox("Require Authentication") + disableIPv6Check = new CheckBox("Disable IPv6") { X = 3, Y = y++ }; - Add(requireAuthCheck); + contentView.Add(disableIPv6Check); y++; // Blank line // IMAP/POP3 Settings section - var imapLabel = new Label("IMAP/POP3 Server Settings:") + contentView.Add(new Label("═══ IMAP/POP3 Settings ═══") { X = 1, Y = y++, - ColorScheme = Colors.TopLevel - }; - Add(imapLabel); + ColorScheme = Colors.Dialog + }); - Add(new Label("IMAP Port:") + contentView.Add(new Label("IMAP Port:") { X = 3, Y = y }); imapPortField = new TextField("") { - X = 25, + X = 30, Y = y++, Width = 10 }; - Add(imapPortField); + contentView.Add(imapPortField); - Add(new Label("POP3 Port:") + contentView.Add(new Label("POP3 Port:") { X = 3, Y = y }); pop3PortField = new TextField("") { - X = 25, + X = 30, Y = y++, Width = 10 }; - Add(pop3PortField); + contentView.Add(pop3PortField); y++; // Blank line - // Relay Settings section - var relayLabel = new Label("Relay Settings:") + // TLS Settings section + contentView.Add(new Label("═══ TLS/Security Settings ═══") { X = 1, Y = y++, - ColorScheme = Colors.TopLevel + ColorScheme = Colors.Dialog + }); + + contentView.Add(new Label("TLS Mode:") + { + X = 3, + Y = y + }); + tlsModeCombo = new ComboBox() + { + X = 30, + Y = y++, + Width = 20, + Height = 5 }; - Add(relayLabel); + tlsModeCombo.SetSource(new List { "None", "StartTls", "ImplicitTls" }); + contentView.Add(tlsModeCombo); - Add(new Label("Server:") + contentView.Add(new Label("TLS Certificate:") { X = 3, Y = y }); - relayServerField = new TextField("") + tlsCertificateField = new TextField("") { - X = 25, + X = 30, Y = y++, Width = 40 }; - Add(relayServerField); + contentView.Add(tlsCertificateField); - Add(new Label("Port:") + contentView.Add(new Label("TLS Certificate Password:") { X = 3, Y = y }); - relayPortField = new TextField("") + tlsCertificatePasswordField = new TextField("") + { + X = 30, + Y = y++, + Width = 30, + Secret = true + }; + contentView.Add(tlsCertificatePasswordField); + + secureConnectionRequiredCheck = new CheckBox("Require Secure Connection") + { + X = 3, + Y = y++ + }; + contentView.Add(secureConnectionRequiredCheck); + + y++; // Blank line + + // Authentication Settings section + contentView.Add(new Label("═══ Authentication Settings ═══") + { + X = 1, + Y = y++, + ColorScheme = Colors.Dialog + }); + + authenticationRequiredCheck = new CheckBox("Authentication Required (SMTP/IMAP)") + { + X = 3, + Y = y++ + }; + contentView.Add(authenticationRequiredCheck); + + smtpAllowAnyCredentialsCheck = new CheckBox("SMTP Allow Any Credentials") + { + X = 3, + Y = y++ + }; + contentView.Add(smtpAllowAnyCredentialsCheck); + + webAuthenticationRequiredCheck = new CheckBox("Web Authentication Required") + { + X = 3, + Y = y++ + }; + contentView.Add(webAuthenticationRequiredCheck); + + y++; // Blank line + + // Storage Settings section + contentView.Add(new Label("═══ Storage Settings ═══") + { + X = 1, + Y = y++, + ColorScheme = Colors.Dialog + }); + + contentView.Add(new Label("Database File:") + { + X = 3, + Y = y + }); + databaseField = new TextField("") + { + X = 30, + Y = y++, + Width = 40 + }; + contentView.Add(databaseField); + + contentView.Add(new Label("Messages to Keep:") + { + X = 3, + Y = y + }); + messagesToKeepField = new TextField("") { - X = 25, + X = 30, Y = y++, Width = 10 }; - Add(relayPortField); + contentView.Add(messagesToKeepField); - Add(new Label("Username:") + contentView.Add(new Label("Sessions to Keep:") { X = 3, Y = y }); - relayUsernameField = new TextField("") + sessionsToKeepField = new TextField("") { - X = 25, + X = 30, Y = y++, - Width = 30 + Width = 10 }; - Add(relayUsernameField); + contentView.Add(sessionsToKeepField); + + y++; // Blank line + + // Message Processing Settings section + contentView.Add(new Label("═══ Message Processing Settings ═══") + { + X = 1, + Y = y++, + ColorScheme = Colors.Dialog + }); - Add(new Label("Password:") + contentView.Add(new Label("Max Message Size (bytes):") { X = 3, Y = y }); - relayPasswordField = new TextField("") + maxMessageSizeField = new TextField("") { - X = 25, + X = 30, Y = y++, - Width = 30, - Secret = true + Width = 15 }; - Add(relayPasswordField); + contentView.Add(maxMessageSizeField); + + disableMessageSanitisationCheck = new CheckBox("Disable Message Sanitisation") + { + X = 3, + Y = y++ + }; + contentView.Add(disableMessageSanitisationCheck); + + disableHtmlValidationCheck = new CheckBox("Disable HTML Validation") + { + X = 3, + Y = y++ + }; + contentView.Add(disableHtmlValidationCheck); + + disableHtmlCompatibilityCheckCheck = new CheckBox("Disable HTML Compatibility Check") + { + X = 3, + Y = y++ + }; + contentView.Add(disableHtmlCompatibilityCheckCheck); y++; // Blank line - // Storage Settings section - var storageLabel = new Label("Storage Settings:") + // Relay Settings section + contentView.Add(new Label("═══ Relay Settings ═══") { X = 1, Y = y++, - ColorScheme = Colors.TopLevel + ColorScheme = Colors.Dialog + }); + + contentView.Add(new Label("Server:") + { + X = 3, + Y = y + }); + relayServerField = new TextField("") + { + X = 30, + Y = y++, + Width = 40 }; - Add(storageLabel); + contentView.Add(relayServerField); - Add(new Label("Messages to Keep:") + contentView.Add(new Label("Port:") { X = 3, Y = y }); - messagesToKeepField = new TextField("") + relayPortField = new TextField("") { - X = 25, + X = 30, Y = y++, Width = 10 }; - Add(messagesToKeepField); + contentView.Add(relayPortField); - Add(new Label("Sessions to Keep:") + contentView.Add(new Label("TLS Mode:") { X = 3, Y = y }); - sessionsToKeepField = new TextField("") + relayTlsModeCombo = new ComboBox() { - X = 25, + X = 30, Y = y++, - Width = 10 + Width = 20, + Height = 5 + }; + relayTlsModeCombo.SetSource(new List { "None", "Auto", "SslOnConnect", "StartTls", "StartTlsWhenAvailable" }); + contentView.Add(relayTlsModeCombo); + + contentView.Add(new Label("Username:") + { + X = 3, + Y = y + }); + relayUsernameField = new TextField("") + { + X = 30, + Y = y++, + Width = 30 + }; + contentView.Add(relayUsernameField); + + contentView.Add(new Label("Password:") + { + X = 3, + Y = y + }); + relayPasswordField = new TextField("") + { + X = 30, + Y = y++, + Width = 30, + Secret = true }; - Add(sessionsToKeepField); + contentView.Add(relayPasswordField); + + contentView.Add(new Label("Sender Address:") + { + X = 3, + Y = y + }); + relaySenderAddressField = new TextField("") + { + X = 30, + Y = y++, + Width = 40 + }; + contentView.Add(relaySenderAddressField); + + scrollView.Add(contentView); + Add(scrollView); // Load current settings LoadSettings(); - // Main action buttons - prominently placed + // Main action buttons at bottom var usersButton = new Button("Manage Users") { - X = Pos.Center() - 30, - Y = Pos.Bottom(this) - 4 + X = 2, + Y = Pos.Bottom(this) - 3 }; usersButton.Clicked += ManageUsers; AddButton(usersButton); var mailboxesButton = new Button("Manage Mailboxes") { - X = Pos.Center() - 10, - Y = Pos.Bottom(this) - 4 + X = Pos.Right(usersButton) + 2, + Y = Pos.Bottom(this) - 3 }; mailboxesButton.Clicked += ManageMailboxes; AddButton(mailboxesButton); var saveButton = new Button("Save") { - X = Pos.Center() + 15, - Y = Pos.Bottom(this) - 4, + X = Pos.Right(mailboxesButton) + 5, + Y = Pos.Bottom(this) - 3, IsDefault = true }; saveButton.Clicked += SaveSettings; @@ -269,8 +505,8 @@ private void CreateUI() var cancelButton = new Button("Cancel") { - X = Pos.Center() + 25, - Y = Pos.Bottom(this) - 4 + X = Pos.Right(saveButton) + 2, + Y = Pos.Bottom(this) - 3 }; cancelButton.Clicked += () => Application.RequestStop(); AddButton(cancelButton); @@ -281,22 +517,46 @@ private void LoadSettings() var settingsManager = new SettingsManager(host, dataDir); var serverOptions = settingsManager.GetServerOptions(); + // Server settings smtpPortField.Text = serverOptions.Port.ToString(); hostnameField.Text = serverOptions.HostName ?? ""; basePathField.Text = serverOptions.BasePath ?? "/"; + bindAddressField.Text = serverOptions.BindAddress ?? ""; remoteConnectionsCheck.Checked = serverOptions.AllowRemoteConnections; - requireAuthCheck.Checked = serverOptions.DisableMessageSanitisation; // Using as proxy for auth requirement - imapPortField.Text = serverOptions.ImapPort.ToString(); - pop3PortField.Text = serverOptions.Pop3Port.ToString(); + disableIPv6Check.Checked = serverOptions.DisableIPv6; + imapPortField.Text = serverOptions.ImapPort?.ToString() ?? "143"; + pop3PortField.Text = serverOptions.Pop3Port?.ToString() ?? "110"; + + // TLS settings + tlsModeCombo.SelectedItem = (int)serverOptions.TlsMode; + tlsCertificateField.Text = serverOptions.TlsCertificate ?? ""; + tlsCertificatePasswordField.Text = serverOptions.TlsCertificatePassword ?? ""; + secureConnectionRequiredCheck.Checked = serverOptions.SecureConnectionRequired; + + // Authentication settings + authenticationRequiredCheck.Checked = serverOptions.AuthenticationRequired; + smtpAllowAnyCredentialsCheck.Checked = serverOptions.SmtpAllowAnyCredentials; + webAuthenticationRequiredCheck.Checked = serverOptions.WebAuthenticationRequired; + + // Storage settings + databaseField.Text = serverOptions.Database ?? "database.db"; + messagesToKeepField.Text = serverOptions.NumberOfMessagesToKeep.ToString(); + sessionsToKeepField.Text = serverOptions.NumberOfSessionsToKeep.ToString(); + + // Message processing settings + maxMessageSizeField.Text = serverOptions.MaxMessageSize?.ToString() ?? ""; + disableMessageSanitisationCheck.Checked = serverOptions.DisableMessageSanitisation; + disableHtmlValidationCheck.Checked = serverOptions.DisableHtmlValidation; + disableHtmlCompatibilityCheckCheck.Checked = serverOptions.DisableHtmlCompatibilityCheck; + // Relay settings var relayOptions = settingsManager.GetRelayOptions(); relayServerField.Text = relayOptions.SmtpServer ?? ""; relayPortField.Text = relayOptions.SmtpPort.ToString(); + relayTlsModeCombo.SelectedItem = (int)relayOptions.TlsMode; relayUsernameField.Text = relayOptions.Login ?? ""; relayPasswordField.Text = relayOptions.Password ?? ""; - - messagesToKeepField.Text = serverOptions.NumberOfMessagesToKeep.ToString(); - sessionsToKeepField.Text = serverOptions.NumberOfSessionsToKeep.ToString(); + relaySenderAddressField.Text = relayOptions.SenderAddress ?? ""; } private void SaveSettings() @@ -313,8 +573,9 @@ private void SaveSettings() serverOptions.HostName = hostnameField.Text.ToString(); serverOptions.BasePath = basePathField.Text.ToString(); + serverOptions.BindAddress = string.IsNullOrWhiteSpace(bindAddressField.Text.ToString()) ? null : bindAddressField.Text.ToString(); serverOptions.AllowRemoteConnections = remoteConnectionsCheck.Checked; - serverOptions.DisableMessageSanitisation = requireAuthCheck.Checked; + serverOptions.DisableIPv6 = disableIPv6Check.Checked; if (int.TryParse(imapPortField.Text.ToString(), out int imapPort)) serverOptions.ImapPort = imapPort; @@ -322,18 +583,43 @@ private void SaveSettings() if (int.TryParse(pop3PortField.Text.ToString(), out int pop3Port)) serverOptions.Pop3Port = pop3Port; + // TLS settings + serverOptions.TlsMode = (TlsMode)tlsModeCombo.SelectedItem; + serverOptions.TlsCertificate = tlsCertificateField.Text.ToString(); + serverOptions.TlsCertificatePassword = tlsCertificatePasswordField.Text.ToString(); + serverOptions.SecureConnectionRequired = secureConnectionRequiredCheck.Checked; + + // Authentication settings + serverOptions.AuthenticationRequired = authenticationRequiredCheck.Checked; + serverOptions.SmtpAllowAnyCredentials = smtpAllowAnyCredentialsCheck.Checked; + serverOptions.WebAuthenticationRequired = webAuthenticationRequiredCheck.Checked; + + // Storage settings + serverOptions.Database = databaseField.Text.ToString(); if (int.TryParse(messagesToKeepField.Text.ToString(), out int messagesToKeep)) serverOptions.NumberOfMessagesToKeep = messagesToKeep; if (int.TryParse(sessionsToKeepField.Text.ToString(), out int sessionsToKeep)) serverOptions.NumberOfSessionsToKeep = sessionsToKeep; + // Message processing settings + if (long.TryParse(maxMessageSizeField.Text.ToString(), out long maxMessageSize)) + serverOptions.MaxMessageSize = maxMessageSize; + else if (string.IsNullOrWhiteSpace(maxMessageSizeField.Text.ToString())) + serverOptions.MaxMessageSize = null; + + serverOptions.DisableMessageSanitisation = disableMessageSanitisationCheck.Checked; + serverOptions.DisableHtmlValidation = disableHtmlValidationCheck.Checked; + serverOptions.DisableHtmlCompatibilityCheck = disableHtmlCompatibilityCheckCheck.Checked; + // Update relay options relayOptions.SmtpServer = relayServerField.Text.ToString(); if (int.TryParse(relayPortField.Text.ToString(), out int relayPort)) relayOptions.SmtpPort = relayPort; + relayOptions.TlsMode = (MailKit.Security.SecureSocketOptions)relayTlsModeCombo.SelectedItem; relayOptions.Login = relayUsernameField.Text.ToString(); relayOptions.Password = relayPasswordField.Text.ToString(); + relayOptions.SenderAddress = relaySenderAddressField.Text.ToString(); // Save settings settingsManager.SaveSettings(serverOptions, relayOptions).Wait(); @@ -355,12 +641,18 @@ private void ManageUsers() { var usersDialog = new UsersDialog(host, dataDir); Application.Run(usersDialog); + + // Reload settings after the subdialog closes to reflect any changes + LoadSettings(); } private void ManageMailboxes() { var mailboxesDialog = new MailboxesDialog(host, dataDir); Application.Run(mailboxesDialog); + + // Reload settings after the subdialog closes to reflect any changes + LoadSettings(); } } } diff --git a/Rnwood.Smtp4dev/TUI/SplashScreen.cs b/Rnwood.Smtp4dev/TUI/SplashScreen.cs new file mode 100644 index 000000000..40b0cc1be --- /dev/null +++ b/Rnwood.Smtp4dev/TUI/SplashScreen.cs @@ -0,0 +1,66 @@ +using System; +using System.Threading.Tasks; +using Terminal.Gui; + +namespace Rnwood.Smtp4dev.TUI +{ + /// + /// Displays a splash screen with ANSI art logo + /// + public class SplashScreen + { + private const string Logo = @" + ███████╗███╗ ███╗████████╗██████╗ ██╗ ██╗██████╗ ███████╗██╗ ██╗ + ██╔════╝████╗ ████║╚══██╔══╝██╔══██╗██║ ██║██╔══██╗██╔════╝██║ ██║ + ███████╗██╔████╔██║ ██║ ██████╔╝███████║██║ ██║█████╗ ██║ ██║ + ╚════██║██║╚██╔╝██║ ██║ ██╔═══╝ ╚════██║██║ ██║██╔══╝ ╚██╗ ██╔╝ + ███████║██║ ╚═╝ ██║ ██║ ██║ ██║██████╔╝███████╗ ╚████╔╝ + ╚══════╝╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝╚═════╝ ╚══════╝ ╚═══╝ + + Fake SMTP Server for Development & Testing + Version 3.x +"; + + /// + /// Shows a splash screen as a modal dialog within an existing Application context + /// + /// Duration to show the splash in milliseconds + public static void Show(int durationMs = 2000) + { + var dialog = new Dialog("smtp4dev", 80, 15) + { + Border = new Border() + { + BorderStyle = BorderStyle.Double + } + }; + + // Create a label with the logo centered + var logoLabel = new Label(Logo) + { + X = Pos.Center(), + Y = 0, + TextAlignment = TextAlignment.Centered + }; + + dialog.Add(logoLabel); + + var loadingLabel = new Label("Loading...") + { + X = Pos.Center(), + Y = Pos.Bottom(dialog) - 3, + TextAlignment = TextAlignment.Centered + }; + + dialog.Add(loadingLabel); + + // Use a timeout to auto-close the splash + var timeout = Application.MainLoop.AddTimeout(TimeSpan.FromMilliseconds(durationMs), (_) => { + Application.RequestStop(); + return false; + }); + + Application.Run(dialog); + } + } +} diff --git a/Rnwood.Smtp4dev/TUI/TerminalGuiApp.cs b/Rnwood.Smtp4dev/TUI/TerminalGuiApp.cs index f075ce4cf..3b60ff0f0 100644 --- a/Rnwood.Smtp4dev/TUI/TerminalGuiApp.cs +++ b/Rnwood.Smtp4dev/TUI/TerminalGuiApp.cs @@ -37,11 +37,18 @@ public void Run() try { - // Set default theme to Base (more modern look) + // Show splash screen first + SplashScreen.Show(2000); + + // Set default theme to Base with improved focus visibility Colors.Base.Normal = Application.Driver.MakeAttribute(Color.White, Color.Black); - Colors.Base.Focus = Application.Driver.MakeAttribute(Color.Black, Color.Cyan); + Colors.Base.Focus = Application.Driver.MakeAttribute(Color.Black, Color.BrightCyan); Colors.Base.HotNormal = Application.Driver.MakeAttribute(Color.BrightCyan, Color.Black); - Colors.Base.HotFocus = Application.Driver.MakeAttribute(Color.BrightBlue, Color.Cyan); + Colors.Base.HotFocus = Application.Driver.MakeAttribute(Color.BrightYellow, Color.BrightCyan); + + // Improve TextView contrast + Colors.TopLevel.Normal = Application.Driver.MakeAttribute(Color.White, Color.Black); + Colors.TopLevel.Focus = Application.Driver.MakeAttribute(Color.Black, Color.BrightCyan); // Create tab view directly (no outer window frame) tabView = new TabView() @@ -153,21 +160,27 @@ private void ChangeTheme() { case 0: // Base Colors.Base.Normal = Application.Driver.MakeAttribute(Color.White, Color.Black); - Colors.Base.Focus = Application.Driver.MakeAttribute(Color.Black, Color.Cyan); + Colors.Base.Focus = Application.Driver.MakeAttribute(Color.Black, Color.BrightCyan); Colors.Base.HotNormal = Application.Driver.MakeAttribute(Color.BrightCyan, Color.Black); - Colors.Base.HotFocus = Application.Driver.MakeAttribute(Color.BrightBlue, Color.Cyan); + Colors.Base.HotFocus = Application.Driver.MakeAttribute(Color.BrightYellow, Color.BrightCyan); + Colors.TopLevel.Normal = Application.Driver.MakeAttribute(Color.White, Color.Black); + Colors.TopLevel.Focus = Application.Driver.MakeAttribute(Color.Black, Color.BrightCyan); break; case 1: // Dark Colors.Base.Normal = Application.Driver.MakeAttribute(Color.BrightGreen, Color.Black); - Colors.Base.Focus = Application.Driver.MakeAttribute(Color.Black, Color.Green); + Colors.Base.Focus = Application.Driver.MakeAttribute(Color.Black, Color.BrightGreen); Colors.Base.HotNormal = Application.Driver.MakeAttribute(Color.BrightYellow, Color.Black); - Colors.Base.HotFocus = Application.Driver.MakeAttribute(Color.BrightYellow, Color.Green); + Colors.Base.HotFocus = Application.Driver.MakeAttribute(Color.BrightYellow, Color.BrightGreen); + Colors.TopLevel.Normal = Application.Driver.MakeAttribute(Color.BrightGreen, Color.Black); + Colors.TopLevel.Focus = Application.Driver.MakeAttribute(Color.Black, Color.BrightGreen); break; case 2: // Light Colors.Base.Normal = Application.Driver.MakeAttribute(Color.Black, Color.Gray); - Colors.Base.Focus = Application.Driver.MakeAttribute(Color.White, Color.Blue); - Colors.Base.HotNormal = Application.Driver.MakeAttribute(Color.Blue, Color.Gray); - Colors.Base.HotFocus = Application.Driver.MakeAttribute(Color.BrightCyan, Color.Blue); + Colors.Base.Focus = Application.Driver.MakeAttribute(Color.White, Color.BrightBlue); + Colors.Base.HotNormal = Application.Driver.MakeAttribute(Color.BrightBlue, Color.Gray); + Colors.Base.HotFocus = Application.Driver.MakeAttribute(Color.BrightYellow, Color.BrightBlue); + Colors.TopLevel.Normal = Application.Driver.MakeAttribute(Color.Black, Color.Gray); + Colors.TopLevel.Focus = Application.Driver.MakeAttribute(Color.White, Color.BrightBlue); break; } Application.Top.SetNeedsDisplay(); diff --git a/Rnwood.Smtp4dev/appsettings.json b/Rnwood.Smtp4dev/appsettings.json index cfc405312..89f06a4e0 100644 --- a/Rnwood.Smtp4dev/appsettings.json +++ b/Rnwood.Smtp4dev/appsettings.json @@ -382,8 +382,7 @@ "path": "logs/smtp4dev-.log", "rollingInterval": "Day", "retainedFileCountLimit": 7, - "formatter": "Serilog.Formatting.Compact.CompactJsonFormatter, Serilog.Formatting.Compact" - "args": { + "formatter": "Serilog.Formatting.Compact.CompactJsonFormatter, Serilog.Formatting.Compact", "outputTemplate": "{Message:lj}{NewLine}{Exception}" } }