basic gui and login to imap

This commit is contained in:
Shautvast 2026-02-17 17:24:35 +01:00
commit 78f5c4655c
11 changed files with 2449 additions and 0 deletions

4
.gitignore vendored Normal file
View file

@ -0,0 +1,4 @@
/target
docker-data/
config.toml
.idea/

70
CLAUDE.md Normal file
View file

@ -0,0 +1,70 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
This is a Rust terminal user interface (TUI) application built with Ratatui.
It will evolve to become a tui mail client
## Build and Run Commands
```bash
# Build the project
cargo build
# Run the application
cargo run
# Build optimized release version
cargo build --release
# Check code without building
cargo check
# Format code
cargo fmt
# Run clippy linter
cargo clippy
```
## Test Mail Server
A Docker-based IMAP mail server is available for testing:
```bash
# Start the mail server
docker-compose up -d
# Create a test user
docker exec -it mailserver setup email add test@example.com password123
# Stop the mail server
docker-compose down
```
Connection details: localhost:143 (IMAP) or localhost:993 (IMAPS). See `MAIL_SERVER_SETUP.md` for detailed usage.
## Architecture
This is a single-file application (`src/main.rs`) following the standard terminal application lifecycle:
1. **Terminal Setup**: Enable raw mode and enter alternate screen
2. **Event Loop**:
- Render UI using Ratatui's declarative widget system
- Poll for keyboard events (200ms timeout)
- Exit on 'q' or Escape key
3. **Cleanup**: Disable raw mode, leave alternate screen, restore cursor
## Key Dependencies
- **ratatui (0.29)**: TUI framework providing widgets, layouts, and rendering
- **crossterm (0.28)**: Cross-platform terminal manipulation (raw mode, events, alternate screen)
## Development Notes
- Uses Rust edition 2024
- The application uses a constraint-based layout system to center content
- Terminal is set to raw mode to capture individual key presses
- The alternate screen prevents terminal history pollution

2124
Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

12
Cargo.toml Normal file
View file

@ -0,0 +1,12 @@
[package]
name = "hello-ratatui"
version = "0.1.0"
edition = "2024"
[dependencies]
ratatui = "0.30"
crossterm = "0.29"
imap = "2.4"
native-tls = "0.2"
serde = { version = "1.0", features = ["derive"] }
toml = "1.0"

78
MAIL_SERVER_SETUP.md Normal file
View file

@ -0,0 +1,78 @@
# Mail Server Setup for Testing
## Quick Start
1. **Start the mail server:**
```bash
docker-compose up -d
```
2. **Create a test user:**
```bash
docker exec -it mailserver setup email add test@example.com password123
```
3. **Verify the server is running:**
```bash
docker-compose ps
```
## IMAP Connection Details
- **Host:** localhost
- **IMAP Port:** 143 (unencrypted) or 993 (SSL/TLS)
- **Username:** test@example.com
- **Password:** password123
## Useful Commands
```bash
# Stop the mail server
docker-compose down
# View logs
docker-compose logs -f mailserver
# List all email accounts
docker exec -it mailserver setup email list
# Add another user
docker exec -it mailserver setup email add user2@example.com pass456
# Delete a user
docker exec -it mailserver setup email del test@example.com
# Access the container shell
docker exec -it mailserver bash
```
## Testing with telnet
You can test IMAP connectivity:
```bash
telnet localhost 143
```
Then try IMAP commands:
```
a1 LOGIN test@example.com password123
a2 LIST "" "*"
a3 SELECT INBOX
a4 LOGOUT
```
## Send Test Email
```bash
# From within the container
docker exec -it mailserver bash
echo "Test email body" | mail -s "Test Subject" test@example.com
```
Or use SMTP (port 25/587) from your application.
## Troubleshooting
- Check logs: `docker-compose logs mailserver`
- Ensure ports aren't already in use
- Data persists in `./docker-data/` directory

6
config.toml Normal file
View file

@ -0,0 +1,6 @@
[imap]
host = "localhost"
port = 143
username = "sander@sanderhautvast.net"
password = "boompje"
use_tls = false

6
config.toml.example Normal file
View file

@ -0,0 +1,6 @@
[imap]
host = "localhost"
port = 143
username = "test@example.com"
password = "password123"
use_tls = false

30
docker-compose.yml Normal file
View file

@ -0,0 +1,30 @@
version: '3.8'
services:
mailserver:
image: mailserver/docker-mailserver:latest
container_name: mailserver
hostname: mail.example.com
ports:
- "25:25" # SMTP
- "143:143" # IMAP
- "587:587" # SMTP Submission
- "993:993" # IMAPS
volumes:
- ./docker-data/mail-data:/var/mail
- ./docker-data/mail-state:/var/mail-state
- ./docker-data/mail-logs:/var/log/mail
- ./docker-data/config:/tmp/docker-mailserver
- /etc/localtime:/etc/localtime:ro
environment:
- ENABLE_SPAMASSASSIN=0
- ENABLE_CLAMAV=0
- ENABLE_FAIL2BAN=0
- ENABLE_POSTGREY=0
- ONE_DIR=1
- DMS_DEBUG=0
- PERMIT_DOCKER=network
- SSL_TYPE=
cap_add:
- NET_ADMIN
restart: unless-stopped

24
src/config.rs Normal file
View file

@ -0,0 +1,24 @@
use serde::Deserialize;
use std::fs;
#[derive(Debug, Deserialize, Clone)]
pub struct Config {
pub imap: ImapConfig,
}
#[derive(Debug, Deserialize, Clone)]
pub struct ImapConfig {
pub host: String,
pub port: u16,
pub username: String,
pub password: String,
pub use_tls: bool,
}
impl Config {
pub fn load() -> Result<Self, Box<dyn std::error::Error>> {
let content = fs::read_to_string("config.toml")?;
let config: Config = toml::from_str(&content)?;
Ok(config)
}
}

1
src/lib.rs Normal file
View file

@ -0,0 +1 @@
pub mod config;

94
src/main.rs Normal file
View file

@ -0,0 +1,94 @@
use std::{io, net::TcpStream, time::Duration};
use crossterm::{
event::{self, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{
backend::CrosstermBackend,
layout::{Alignment, Constraint, Direction, Layout},
style::{Color, Style},
widgets::{Block, Borders, Paragraph},
Terminal,
};
use hello_ratatui::config::Config;
fn imap_login(config: &Config) -> Result<String, String> {
let imap = &config.imap;
let stream = TcpStream::connect((&*imap.host, imap.port)).map_err(|e| e.to_string())?;
let client = imap::Client::new(stream);
let mut session = client
.login(&imap.username, &imap.password)
.map_err(|(e, _)| e.to_string())?;
let _ = session.logout();
Ok(format!("Logged in as {}", imap.username))
}
fn main() -> io::Result<()> {
let config = Config::load().unwrap();
let imap_status = match imap_login(&config) {
Ok(msg) => msg,
Err(e) => format!("IMAP error: {}", e),
};
// --- Setup terminal ---
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
// --- Main loop ---
loop {
terminal.draw(|frame| {
// Split the screen into a centered area
let area = frame.area();
let vertical = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Percentage(40),
Constraint::Percentage(20),
Constraint::Percentage(40),
])
.split(area);
let horizontal = Layout::default()
.direction(Direction::Horizontal)
.constraints([
Constraint::Percentage(25),
Constraint::Percentage(50),
Constraint::Percentage(25),
])
.split(vertical[1]);
let center = horizontal[1];
// Render a bordered box with "Hello, World!"
let paragraph = Paragraph::new(imap_status.as_str())
.block(Block::default().title("Ratatui").borders(Borders::ALL))
.style(Style::default().fg(Color::Green))
.alignment(Alignment::Center);
frame.render_widget(paragraph, center);
})?;
// --- Input handling: quit on 'q' or Escape ---
if event::poll(Duration::from_millis(200))? {
if let Event::Key(key) = event::read()? {
match key.code {
KeyCode::Char('q') | KeyCode::Esc => break,
_ => {}
}
}
}
}
// --- Restore terminal ---
disable_raw_mode()?;
execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
terminal.show_cursor()?;
Ok(())
}