diff --git a/crates/rustmail-smtp/src/session.rs b/crates/rustmail-smtp/src/session.rs index ec38392..81ba36b 100644 --- a/crates/rustmail-smtp/src/session.rs +++ b/crates/rustmail-smtp/src/session.rs @@ -31,6 +31,7 @@ pub struct Session { peer: SocketAddr, sender: mpsc::Sender, max_message_size: usize, + greeted: bool, mail_from: Option, rcpt_to: Vec, } @@ -47,6 +48,7 @@ impl Session { peer, sender, max_message_size, + greeted: false, mail_from: None, rcpt_to: Vec::new(), } @@ -79,15 +81,22 @@ impl Session { let upper = trimmed.to_ascii_uppercase(); if upper.starts_with("EHLO") || upper.starts_with("HELO") { + self.greeted = true; + self.mail_from = None; + self.rcpt_to.clear(); let ehlo = format!( "250-rustmail\r\n250-SIZE {}\r\n250-8BITMIME\r\n250-PIPELINING\r\n250-AUTH PLAIN LOGIN\r\n250 HELP\r\n", self.max_message_size ); self.write(&ehlo).await?; } else if upper.starts_with("MAIL FROM:") { - self.mail_from = Some(extract_address(trimmed)); - self.rcpt_to.clear(); - self.write(OK).await?; + if !self.greeted { + self.write(BAD_SEQUENCE).await?; + } else { + self.mail_from = Some(extract_address(trimmed)); + self.rcpt_to.clear(); + self.write(OK).await?; + } } else if upper.starts_with("RCPT TO:") { if self.rcpt_to.len() >= MAX_RECIPIENTS { self.write("452 Too many recipients\r\n").await?; diff --git a/ui/src/App.tsx b/ui/src/App.tsx index 67b410d..c60ae88 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -8,6 +8,7 @@ import ConfirmDialog, { confirm } from "./components/ConfirmDialog"; import { fetchMessages, connectWebSocket, + disconnectWebSocket, filteredMessages, total, selectedId, @@ -129,6 +130,7 @@ export default function App() { onCleanup(() => { document.removeEventListener("keydown", handleKeydown); + disconnectWebSocket(); }); return ( diff --git a/ui/src/components/Inbox.tsx b/ui/src/components/Inbox.tsx index 0d82bed..a29affc 100644 --- a/ui/src/components/Inbox.tsx +++ b/ui/src/components/Inbox.tsx @@ -86,6 +86,12 @@ export default function Inbox() { } } }} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + (e.currentTarget as HTMLElement).click(); + } + }} class={`w-full text-left px-4 py-3 border-b border-zinc-100 dark:border-zinc-800/50 transition cursor-pointer ${ isSelected() ? "bg-zinc-100 dark:bg-zinc-800/80" diff --git a/ui/src/stores/messages.ts b/ui/src/stores/messages.ts index 1399884..bd0428c 100644 --- a/ui/src/stores/messages.ts +++ b/ui/src/stores/messages.ts @@ -81,10 +81,15 @@ async function fetchMessages() { let reconnectDelay = 2000; const MAX_RECONNECT_DELAY = 30000; +let currentWs: WebSocket | null = null; +let reconnectTimer: ReturnType | null = null; function connectWebSocket() { + disconnectWebSocket(); + const protocol = location.protocol === "https:" ? "wss:" : "ws:"; const ws = new WebSocket(`${protocol}//${location.host}/api/v1/ws`); + currentWs = ws; ws.onopen = () => { reconnectDelay = 2000; @@ -145,14 +150,27 @@ function connectWebSocket() { }; ws.onclose = () => { + currentWs = null; const jitter = reconnectDelay * (0.5 + Math.random() * 0.5); - setTimeout(connectWebSocket, jitter); + reconnectTimer = setTimeout(connectWebSocket, jitter); reconnectDelay = Math.min(reconnectDelay * 2, MAX_RECONNECT_DELAY); }; return ws; } +function disconnectWebSocket() { + if (reconnectTimer !== null) { + clearTimeout(reconnectTimer); + reconnectTimer = null; + } + if (currentWs) { + currentWs.onclose = null; + currentWs.close(); + currentWs = null; + } +} + export { messages, filteredMessages, @@ -171,4 +189,5 @@ export { allTags, fetchMessages, connectWebSocket, + disconnectWebSocket, };