basic gui and login to imap
This commit is contained in:
commit
78f5c4655c
11 changed files with 2449 additions and 0 deletions
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
/target
|
||||||
|
docker-data/
|
||||||
|
config.toml
|
||||||
|
.idea/
|
||||||
70
CLAUDE.md
Normal file
70
CLAUDE.md
Normal 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
2124
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
12
Cargo.toml
Normal file
12
Cargo.toml
Normal 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
78
MAIL_SERVER_SETUP.md
Normal 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
6
config.toml
Normal 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
6
config.toml.example
Normal 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
30
docker-compose.yml
Normal 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
24
src/config.rs
Normal 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
1
src/lib.rs
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
pub mod config;
|
||||||
94
src/main.rs
Normal file
94
src/main.rs
Normal 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(())
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue