#![cfg_attr(all(windows, not(debug_assertions)), windows_subsystem = "windows")]
use eframe::egui;
use smtp_test_tool::config::{default_save_path, discover_config_path, Config};
use smtp_test_tool::diagnostics::smtp_hints_for;
use smtp_test_tool::keystore::{default_keystore, Keystore};
use smtp_test_tool::providers::{self, Provider};
use smtp_test_tool::runner::{TestOutcome, TestResults};
use smtp_test_tool::theme::{detect as detect_appearance, Appearance, ThemeChoice};
use smtp_test_tool::tls::Security;
use smtp_test_tool::{outlook_defaults, run_tests, Profile};
use std::path::PathBuf;
use std::sync::mpsc::{Receiver, Sender};
use std::sync::{Arc, Mutex};
use std::thread;
use tracing::level_filters::LevelFilter;
use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::util::SubscriberInitExt;
use tracing_subscriber::Layer;
#[derive(Clone, Copy, Debug)]
enum LogLevel {
Trace,
Debug,
Info,
Warn,
Error,
}
#[derive(Clone, Debug)]
struct LogLine {
level: LogLevel,
text: String,
}
#[derive(Default)]
struct LogSink {
lines: Mutex<Vec<LogLine>>,
}
impl LogSink {
fn push(&self, level: LogLevel, text: String) {
if let Ok(mut g) = self.lines.lock() {
if g.len() > 5000 {
g.drain(..1000);
}
g.push(LogLine { level, text });
}
}
fn drain_into(&self, dst: &mut Vec<LogLine>) {
if let Ok(mut g) = self.lines.lock() {
dst.extend(g.drain(..));
}
}
}
struct GuiLayer {
sink: Arc<LogSink>,
}
impl<S> Layer<S> for GuiLayer
where
S: tracing::Subscriber,
{
fn on_event(
&self,
event: &tracing::Event<'_>,
_ctx: tracing_subscriber::layer::Context<'_, S>,
) {
let mut visitor = FieldFmt::default();
event.record(&mut visitor);
let lvl = match *event.metadata().level() {
tracing::Level::TRACE => LogLevel::Trace,
tracing::Level::DEBUG => LogLevel::Debug,
tracing::Level::INFO => LogLevel::Info,
tracing::Level::WARN => LogLevel::Warn,
tracing::Level::ERROR => LogLevel::Error,
};
self.sink.push(lvl, visitor.message);
}
}
#[derive(Default)]
struct FieldFmt {
message: String,
}
impl tracing::field::Visit for FieldFmt {
fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug) {
if field.name() == "message" {
self.message = format!("{value:?}");
if self.message.starts_with('"') && self.message.ends_with('"') {
self.message = self.message[1..self.message.len() - 1].to_string();
}
} else {
self.message
.push_str(&format!(" {}={value:?}", field.name()));
}
}
fn record_str(&mut self, field: &tracing::field::Field, value: &str) {
if field.name() == "message" {
self.message = value.to_string();
} else {
self.message.push_str(&format!(" {}={value}", field.name()));
}
}
}
struct App {
cfg_path: Option<PathBuf>,
cfg: Config,
profile_name: String,
profile: Profile,
log_sink: Arc<LogSink>,
log_buf: Vec<LogLine>,
show_pwd: bool,
busy: bool,
keystore: Box<dyn Keystore>,
password_from_keychain: bool,
result_rx: Option<Receiver<TestResults>>,
last_results: TestResults,
to_csv: String,
cc_csv: String,
bcc_csv: String,
tab: Tab,
diagnose_input: String,
diagnose_hints: Vec<String>,
os_appearance: Appearance,
applied_appearance: Appearance,
}
#[derive(PartialEq, Copy, Clone)]
enum Tab {
Servers,
Send,
Tls,
Advanced,
Diagnose,
}
impl App {
fn new(sink: Arc<LogSink>, cc: &eframe::CreationContext<'_>) -> Self {
let cfg_path = discover_config_path();
let cfg = cfg_path
.as_ref()
.and_then(|p| match Config::load(p) {
Ok(c) => {
tracing::info!("loaded config {}", p.display());
Some(c)
}
Err(e) => {
tracing::warn!("failed to load config {}: {:#}", p.display(), e);
None
}
})
.unwrap_or_else(|| Config {
active: "default".into(),
profiles: [("default".into(), outlook_defaults())]
.into_iter()
.collect(),
});
let profile_name = cfg.active.clone();
let mut profile = cfg
.profile(&profile_name)
.cloned()
.unwrap_or_else(outlook_defaults);
let keystore = default_keystore();
let mut password_from_keychain = false;
if profile.password.is_none() {
if let Some(user) = profile.user.as_deref() {
match keystore.load(user) {
Ok(Some(pwd)) => {
tracing::info!("loaded password for {user} from OS keychain");
profile.password = Some(pwd);
password_from_keychain = true;
}
Ok(None) => {}
Err(e) => {
tracing::warn!("keychain load failed for {user}: {e:#}");
}
}
}
}
let os_appearance = detect_appearance();
let initial_choice = ThemeChoice::from_config_str(&profile.theme);
let initial = initial_choice.resolve(os_appearance);
cc.egui_ctx.set_visuals(visuals_for(initial));
if os_appearance == Appearance::Unknown && initial_choice == ThemeChoice::Auto {
tracing::info!("OS did not advertise a colour scheme; defaulting to dark");
}
let to_csv = profile.to.join(", ");
let cc_csv = profile.cc.join(", ");
let bcc_csv = profile.bcc.join(", ");
Self {
cfg_path,
cfg,
profile_name,
profile,
log_sink: sink,
log_buf: Vec::new(),
show_pwd: false,
busy: false,
result_rx: None,
last_results: TestResults::default(),
to_csv,
cc_csv,
bcc_csv,
tab: Tab::Servers,
diagnose_input: String::new(),
diagnose_hints: Vec::new(),
os_appearance,
applied_appearance: initial,
keystore,
password_from_keychain,
}
}
fn refresh_theme(&mut self, ctx: &egui::Context) {
let choice = ThemeChoice::from_config_str(&self.profile.theme);
let target = choice.resolve(self.os_appearance);
if target != self.applied_appearance {
ctx.set_visuals(visuals_for(target));
self.applied_appearance = target;
tracing::info!(
"theme: now {} (choice={}, os={:?})",
target_label(target),
choice.as_str(),
self.os_appearance
);
}
}
fn apply_provider(&mut self, p: &Provider) {
self.profile.smtp_host = p.smtp.host.into();
self.profile.smtp_port = p.smtp.port;
self.profile.smtp_security = p.smtp.security;
self.profile.imap_host = p.imap.host.into();
self.profile.imap_port = p.imap.port;
self.profile.imap_security = p.imap.security;
match p.pop {
Some(pop) => {
self.profile.pop_host = pop.host.into();
self.profile.pop_port = pop.port;
self.profile.pop_security = pop.security;
}
None => {
self.profile.pop_enabled = false;
}
}
tracing::info!("applied provider preset: {}", p.name);
}
fn run_tests_async(&mut self) {
self.profile.to = csv_to_vec(&self.to_csv);
self.profile.cc = csv_to_vec(&self.cc_csv);
self.profile.bcc = csv_to_vec(&self.bcc_csv);
let (tx, rx): (Sender<TestResults>, Receiver<TestResults>) = std::sync::mpsc::channel();
self.result_rx = Some(rx);
self.busy = true;
let profile = self.profile.clone();
thread::spawn(move || {
let r = run_tests(&profile);
let _ = tx.send(r);
});
}
fn save_config(&mut self) -> anyhow::Result<()> {
self.profile.to = csv_to_vec(&self.to_csv);
self.profile.cc = csv_to_vec(&self.cc_csv);
self.profile.bcc = csv_to_vec(&self.bcc_csv);
self.cfg
.upsert_profile(&self.profile_name, self.profile.clone());
self.cfg.active = self.profile_name.clone();
let target = self.cfg_path.clone().unwrap_or_else(default_save_path);
self.cfg.save(&target)?;
self.cfg_path = Some(target);
Ok(())
}
}
fn csv_to_vec(s: &str) -> Vec<String> {
s.split(',')
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect()
}
impl eframe::App for App {
fn ui(&mut self, root_ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
let ctx = root_ui.ctx().clone();
self.refresh_theme(&ctx);
self.log_sink.drain_into(&mut self.log_buf);
if self.log_buf.len() > 5000 {
let drop = self.log_buf.len() - 5000;
self.log_buf.drain(..drop);
}
if let Some(rx) = &self.result_rx {
if let Ok(r) = rx.try_recv() {
self.last_results = r;
self.busy = false;
self.result_rx = None;
}
}
if self.busy {
ctx.request_repaint_after(std::time::Duration::from_millis(150));
}
egui::Panel::top("top").show_inside(root_ui, |ui| {
ui.horizontal(|ui| {
ui.label("Profile:");
let names = self.cfg.profile_names();
egui::ComboBox::from_id_salt("profile")
.selected_text(&self.profile_name)
.show_ui(ui, |ui| {
for n in &names {
if ui.selectable_label(&self.profile_name == n, n).clicked() {
self.profile_name = n.clone();
if let Some(p) = self.cfg.profile(n) {
self.profile = p.clone();
self.to_csv = self.profile.to.join(", ");
self.cc_csv = self.profile.cc.join(", ");
self.bcc_csv = self.profile.bcc.join(", ");
}
}
}
});
if ui.button("Save Config").clicked() {
if let Err(e) = self.save_config() {
tracing::error!("save failed: {e:#}");
} else {
tracing::info!(
"Saved profile [{}] to {}",
self.profile_name,
self.cfg_path
.as_ref()
.map(|p| p.display().to_string())
.unwrap_or_default()
);
}
}
let mut chosen: Option<&'static Provider> = None;
ui.menu_button("Provider preset...", |ui| {
for p in providers::PROVIDERS {
let mut label = p.name.to_string();
if p.pop.is_none() {
label.push_str(" (no POP3)");
}
if ui.button(label).clicked() {
chosen = Some(p);
ui.close();
}
}
});
if let Some(p) = chosen {
self.apply_provider(p);
if let Some(note) = p.note {
tracing::info!("note: {note}");
}
}
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
ui.label(
self.cfg_path
.as_ref()
.map(|p| format!("{}", p.display()))
.unwrap_or_else(|| "no config file".into()),
);
});
});
});
egui::Panel::bottom("bottom").show_inside(root_ui, |ui| {
ui.horizontal(|ui| {
let run = ui.add_enabled(
!self.busy,
egui::Button::new(if self.busy { "Running..." } else { "Run Test" }),
);
if run.clicked() {
self.run_tests_async();
}
ui.separator();
outcome_chip(ui, "SMTP", self.last_results.smtp);
outcome_chip(ui, "IMAP", self.last_results.imap);
outcome_chip(ui, "POP3", self.last_results.pop3);
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
if ui.button("Clear Log").clicked() {
self.log_buf.clear();
}
});
});
});
egui::Panel::bottom("log")
.resizable(true)
.default_size(260.0)
.min_size(120.0)
.show_inside(root_ui, |ui| {
ui.label(egui::RichText::new("Log").strong());
egui::ScrollArea::vertical()
.stick_to_bottom(true)
.auto_shrink([false, false])
.show(ui, |ui| {
for line in &self.log_buf {
let (color, tag) = level_style(line.level, ui.visuals().dark_mode);
ui.horizontal_wrapped(|ui| {
ui.label(egui::RichText::new(tag).color(color).monospace());
ui.label(egui::RichText::new(&line.text).monospace());
});
}
});
});
egui::CentralPanel::default().show_inside(root_ui, |ui| {
ui.horizontal(|ui| {
ui.selectable_value(&mut self.tab, Tab::Servers, "Servers");
ui.selectable_value(&mut self.tab, Tab::Send, "Send Mail");
ui.selectable_value(&mut self.tab, Tab::Tls, "TLS / Auth");
ui.selectable_value(&mut self.tab, Tab::Advanced, "Advanced");
ui.selectable_value(&mut self.tab, Tab::Diagnose, "Diagnose bounce");
});
ui.separator();
egui::ScrollArea::vertical()
.auto_shrink([false, false])
.show(ui, |ui| match self.tab {
Tab::Servers => tab_servers(ui, self),
Tab::Send => tab_send(ui, self),
Tab::Tls => tab_tls(ui, self),
Tab::Advanced => tab_advanced(ui, self),
Tab::Diagnose => tab_diagnose(ui, self),
});
});
}
}
fn visuals_for(a: Appearance) -> egui::Visuals {
match a {
Appearance::Light => egui::Visuals::light(),
Appearance::Dark | Appearance::Unknown => egui::Visuals::dark(),
}
}
fn target_label(a: Appearance) -> &'static str {
match a {
Appearance::Dark => "dark",
Appearance::Light => "light",
Appearance::Unknown => "dark (fallback)",
}
}
fn theme_label(choice: ThemeChoice, os: Appearance) -> String {
match choice {
ThemeChoice::Auto => format!("Follow OS ({})", target_label(os)),
ThemeChoice::Dark => "Dark".to_string(),
ThemeChoice::Light => "Light".to_string(),
}
}
fn outcome_chip(ui: &mut egui::Ui, name: &str, o: Option<TestOutcome>) {
let (txt, col) = match o {
Some(TestOutcome::Pass) => (
format!("{name}: PASS"),
egui::Color32::from_rgb(0x0e, 0x7c, 0x0e),
),
Some(TestOutcome::Fail) => (
format!("{name}: FAIL"),
egui::Color32::from_rgb(0xa3, 0x00, 0x00),
),
Some(TestOutcome::Skipped) => (format!("{name}: skip"), egui::Color32::GRAY),
None => (format!("{name}: -"), egui::Color32::GRAY),
};
ui.label(egui::RichText::new(txt).color(col).monospace());
}
fn level_style(lvl: LogLevel, dark: bool) -> (egui::Color32, &'static str) {
if dark {
match lvl {
LogLevel::Trace => (egui::Color32::from_rgb(0xa0, 0xa0, 0xa0), "[TRACE]"),
LogLevel::Debug => (egui::Color32::from_rgb(0xa0, 0xa0, 0xa0), "[DEBUG]"),
LogLevel::Info => (egui::Color32::from_rgb(0xf0, 0xf0, 0xf0), "[INFO ]"),
LogLevel::Warn => (egui::Color32::from_rgb(0xff, 0xd1, 0x66), "[WARN ]"),
LogLevel::Error => (egui::Color32::from_rgb(0xff, 0x6b, 0x6b), "[ERROR]"),
}
} else {
match lvl {
LogLevel::Trace => (egui::Color32::from_rgb(0x55, 0x55, 0x55), "[TRACE]"),
LogLevel::Debug => (egui::Color32::from_rgb(0x55, 0x55, 0x55), "[DEBUG]"),
LogLevel::Info => (egui::Color32::from_rgb(0x11, 0x11, 0x11), "[INFO ]"),
LogLevel::Warn => (egui::Color32::from_rgb(0x8a, 0x4b, 0x00), "[WARN ]"),
LogLevel::Error => (egui::Color32::from_rgb(0xa3, 0x00, 0x00), "[ERROR]"),
}
}
}
fn tab_servers(ui: &mut egui::Ui, a: &mut App) {
const LABEL_W: f32 = 100.0;
const SHOW_W: f32 = 70.0; const HINT_W: f32 = 160.0;
ui.horizontal(|ui| {
ui.add_sized([LABEL_W, 0.0], egui::Label::new("Username:"));
let mut u = a.profile.user.clone().unwrap_or_default();
let resp = ui.add_sized(
[ui.available_width(), 0.0],
egui::TextEdit::singleline(&mut u),
);
if resp.changed() {
a.profile.user = Some(u).filter(|s| !s.is_empty());
}
});
ui.horizontal(|ui| {
ui.add_sized([LABEL_W, 0.0], egui::Label::new("Password:"));
let mut pwd = a.profile.password.clone().unwrap_or_default();
let entry_w = (ui.available_width() - SHOW_W).max(80.0);
let resp = ui.add_sized(
[entry_w, 0.0],
egui::TextEdit::singleline(&mut pwd).password(!a.show_pwd),
);
if resp.changed() {
a.profile.password = Some(pwd).filter(|s| !s.is_empty());
}
ui.checkbox(&mut a.show_pwd, "Show");
});
ui.horizontal(|ui| {
ui.add_sized([LABEL_W, 0.0], egui::Label::new("OAuth token:"));
let mut t = a.profile.oauth_token.clone().unwrap_or_default();
let entry_w = (ui.available_width() - HINT_W).max(80.0);
let resp = ui.add_sized(
[entry_w, 0.0],
egui::TextEdit::singleline(&mut t).password(true),
);
if resp.changed() {
a.profile.oauth_token = Some(t).filter(|s| !s.is_empty());
}
ui.add_sized(
[HINT_W, 0.0],
egui::Label::new(egui::RichText::new("(XOAUTH2, optional)").weak()),
);
});
ui.horizontal(|ui| {
ui.add_sized([LABEL_W, 0.0], egui::Label::new(""));
let user_set = a
.profile
.user
.as_deref()
.map(|s| !s.is_empty())
.unwrap_or(false);
let pwd_set = a
.profile
.password
.as_deref()
.map(|s| !s.is_empty())
.unwrap_or(false);
if ui
.add_enabled(
user_set && pwd_set,
egui::Button::new("Save password to keychain"),
)
.on_hover_text(
"Stores the password in your OS keychain (Windows Credential \
Manager / macOS Keychain / Linux Secret Service). Never \
written to the config file.",
)
.clicked()
{
if let (Some(u), Some(p)) = (&a.profile.user, &a.profile.password) {
match a.keystore.save(u, p) {
Ok(()) => {
tracing::info!("saved password for {u} to OS keychain");
a.password_from_keychain = true;
}
Err(e) => tracing::error!("keychain save failed for {u}: {e:#}"),
}
}
}
if ui
.add_enabled(user_set, egui::Button::new("Forget keychain entry"))
.on_hover_text(
"Deletes the smtp-test-tool entry for this user from the OS keychain. \
The password field above is also cleared.",
)
.clicked()
{
if let Some(u) = &a.profile.user.clone() {
match a.keystore.forget(u) {
Ok(()) => {
tracing::info!("forgot keychain entry for {u}");
a.profile.password = None;
a.password_from_keychain = false;
}
Err(e) => tracing::error!("keychain forget failed for {u}: {e:#}"),
}
}
}
if a.password_from_keychain {
ui.label(egui::RichText::new("(loaded from keychain)").weak());
}
});
ui.separator();
proto_block(
ui,
"SMTP",
&mut a.profile.smtp_enabled,
&mut a.profile.smtp_host,
&mut a.profile.smtp_port,
&mut a.profile.smtp_security,
);
proto_block(
ui,
"IMAP",
&mut a.profile.imap_enabled,
&mut a.profile.imap_host,
&mut a.profile.imap_port,
&mut a.profile.imap_security,
);
proto_block(
ui,
"POP3",
&mut a.profile.pop_enabled,
&mut a.profile.pop_host,
&mut a.profile.pop_port,
&mut a.profile.pop_security,
);
}
fn proto_block(
ui: &mut egui::Ui,
name: &str,
enabled: &mut bool,
host: &mut String,
port: &mut u16,
sec: &mut Security,
) {
ui.horizontal(|ui| {
ui.checkbox(enabled, format!("Test {name}"));
ui.label("Host:");
ui.text_edit_singleline(host);
ui.label("Port:");
ui.add(egui::DragValue::new(port).range(1..=65535));
ui.label("Security:");
egui::ComboBox::from_id_salt(format!("{name}-sec"))
.selected_text(sec.as_str())
.show_ui(ui, |ui| {
ui.selectable_value(sec, Security::None, "none");
ui.selectable_value(sec, Security::StartTls, "starttls");
ui.selectable_value(sec, Security::Implicit, "ssl");
});
});
}
fn tab_send(ui: &mut egui::Ui, a: &mut App) {
ui.checkbox(
&mut a.profile.send_test,
"Actually send a test email (otherwise only AUTH is tested)",
);
ui.separator();
egui::Grid::new("msg").num_columns(2).show(ui, |ui| {
opt_line(ui, "MAIL FROM (envelope):", &mut a.profile.mail_from);
opt_line(ui, "From: (header) :", &mut a.profile.from_addr);
ui.label("To (comma sep):");
ui.text_edit_singleline(&mut a.to_csv);
ui.end_row();
ui.label("Cc (comma sep):");
ui.text_edit_singleline(&mut a.cc_csv);
ui.end_row();
ui.label("Bcc (comma sep):");
ui.text_edit_singleline(&mut a.bcc_csv);
ui.end_row();
opt_line(ui, "Reply-To:", &mut a.profile.reply_to);
ui.label("Subject:");
ui.text_edit_singleline(&mut a.profile.subject);
ui.end_row();
});
ui.label("Body:");
ui.add(
egui::TextEdit::multiline(&mut a.profile.body)
.desired_rows(6)
.desired_width(f32::INFINITY),
);
}
fn opt_line(ui: &mut egui::Ui, label: &str, v: &mut Option<String>) {
ui.label(label);
let mut buf = v.clone().unwrap_or_default();
if ui.text_edit_singleline(&mut buf).changed() {
*v = if buf.is_empty() { None } else { Some(buf) };
}
ui.end_row();
}
fn tab_tls(ui: &mut egui::Ui, a: &mut App) {
ui.checkbox(
&mut a.profile.insecure_tls,
"Disable certificate verification (INSECURE - testing only)",
);
ui.horizontal(|ui| {
ui.label("CA bundle (PEM):");
let mut buf = a
.profile
.ca_file
.as_ref()
.map(|p| p.display().to_string())
.unwrap_or_default();
if ui.text_edit_singleline(&mut buf).changed() {
a.profile.ca_file = if buf.is_empty() {
None
} else {
Some(buf.into())
};
}
});
}
fn tab_advanced(ui: &mut egui::Ui, a: &mut App) {
egui::Grid::new("adv").num_columns(2).show(ui, |ui| {
ui.label("Timeout (s):");
ui.add(egui::DragValue::new(&mut a.profile.timeout_secs).range(1..=600));
ui.end_row();
ui.label("EHLO/HELO name:");
let mut e = a.profile.ehlo_name.clone().unwrap_or_default();
if ui.text_edit_singleline(&mut e).changed() {
a.profile.ehlo_name = if e.is_empty() { None } else { Some(e) };
}
ui.end_row();
ui.label("IMAP folder:");
ui.text_edit_singleline(&mut a.profile.imap_folder);
ui.end_row();
ui.label("Log level:");
egui::ComboBox::from_id_salt("loglvl")
.selected_text(&a.profile.log_level)
.show_ui(ui, |ui| {
for lv in ["trace", "debug", "info", "warn", "error"] {
ui.selectable_value(&mut a.profile.log_level, lv.into(), lv);
}
});
ui.end_row();
ui.label("Theme:");
let mut current = ThemeChoice::from_config_str(&a.profile.theme);
let previous = current;
egui::ComboBox::from_id_salt("themechoice")
.selected_text(theme_label(current, a.os_appearance))
.show_ui(ui, |ui| {
ui.selectable_value(
&mut current,
ThemeChoice::Auto,
theme_label(ThemeChoice::Auto, a.os_appearance),
);
ui.selectable_value(&mut current, ThemeChoice::Dark, "Dark");
ui.selectable_value(&mut current, ThemeChoice::Light, "Light");
});
if current != previous {
a.profile.theme = current.as_str().to_string();
}
ui.end_row();
});
}
fn tab_diagnose(ui: &mut egui::Ui, a: &mut App) {
ui.label(
"Paste a delivery-failure body below to extract IT-actionable hints. \
Detects SMTP enhanced status codes (e.g. 5.7.139, 5.7.60), Microsoft \
365 phrases (\"basic authentication is disabled\"), and webmail-side \
bounces such as Gmail's \"Send mail as / Mail sturen als\" failure. \
Nothing is sent over the network and the text is not stored on disk.",
);
ui.add_space(6.0);
let avail_h = ui.available_height();
let input_h = (avail_h * 0.4).clamp(120.0, 260.0);
ui.add_sized(
[ui.available_width(), input_h],
egui::TextEdit::multiline(&mut a.diagnose_input)
.hint_text("Paste the bounce body here...")
.desired_rows(8),
);
ui.horizontal(|ui| {
let has_input = !a.diagnose_input.trim().is_empty();
if ui
.add_enabled(has_input, egui::Button::new("Analyse"))
.clicked()
{
a.diagnose_hints = smtp_hints_for(&a.diagnose_input);
if a.diagnose_hints.is_empty() {
tracing::info!("diagnose: no known patterns matched");
} else {
tracing::info!(
"diagnose: {} hint line(s) generated",
a.diagnose_hints.len()
);
}
}
if ui.button("Clear").clicked() {
a.diagnose_input.clear();
a.diagnose_hints.clear();
}
});
ui.add_space(8.0);
ui.separator();
ui.label(egui::RichText::new("Hints").strong());
if a.diagnose_hints.is_empty() {
ui.label(
egui::RichText::new("(no hints yet - paste a bounce above and click Analyse)").weak(),
);
} else {
egui::ScrollArea::vertical()
.auto_shrink([false, true])
.show(ui, |ui| {
for line in &a.diagnose_hints {
ui.label(egui::RichText::new(line).monospace());
}
});
}
}
fn main() -> eframe::Result<()> {
let sink = Arc::new(LogSink::default());
let layer = GuiLayer { sink: sink.clone() };
let filter = tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| {
tracing_subscriber::EnvFilter::new("info,eframe=warn,winit=warn,wgpu_core=warn,naga=warn")
});
tracing_subscriber::registry()
.with(filter)
.with(layer)
.init();
let _keep = LevelFilter::INFO;
let opts = eframe::NativeOptions {
viewport: egui::ViewportBuilder::default()
.with_inner_size([960.0, 760.0])
.with_min_inner_size([720.0, 520.0])
.with_title("SMTP Test Tool"),
..Default::default()
};
eframe::run_native(
"SMTP Test Tool",
opts,
Box::new(|cc| Ok(Box::new(App::new(sink, cc)))),
)
}