diff --git a/CLAUDE.md b/CLAUDE.md index 295351e..ba23357 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -5,44 +5,121 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Project Overview mail2couch is a utility for backing up mail from various sources (primarily IMAP) to CouchDB. The project supports two implementations: -- **Go implementation**: Located in `/go/` directory (currently the active implementation) -- **Rust implementation**: Planned but not yet implemented +- **Go implementation**: Located in `/go/` directory, builds as `mail2couch-go` +- **Rust implementation**: Located in `/rust/` directory, builds as `mail2couch-rs` (fully functional, more advanced) ## Development Commands -### Go Implementation (Primary) +### Universal Build Commands (Recommended) + +```bash +# Build both implementations +just build + +# Build individual implementations +just build-go # Builds go/mail2couch-go +just build-rust # Builds rust/target/release/mail2couch-rs + +# Build optimized release versions +just build-release + +# Install both binaries to ~/bin (user-local) +just install + +# Install both binaries to /usr/local/bin (system-wide) +sudo just system-install + +# Uninstall from ~/bin +just uninstall + +# Uninstall from /usr/local/bin +sudo just system-uninstall + +# Run tests for both implementations +just test + +# Clean all build artifacts +just clean + +# Format and check code +just fmt +just check + +# Show binary sizes +just sizes + +# Compare versions +just versions + +# List all available recipes +just --list +``` + +### Go Implementation ```bash # Build the application -cd go && go build -o mail2couch . +cd go && go build -o mail2couch-go . # Run the application with automatic config discovery -cd go && ./mail2couch +cd go && ./mail2couch-go # Run with specific config file -cd go && ./mail2couch --config /path/to/config.json +cd go && ./mail2couch-go --config /path/to/config.json # Run with message limit (useful for large mailboxes) -cd go && ./mail2couch --max-messages 100 +cd go && ./mail2couch-go --max-messages 100 -# Run with both config and message limit -cd go && ./mail2couch --config /path/to/config.json --max-messages 50 +# Run with dry-run mode +cd go && ./mail2couch-go --dry-run # Run linting/static analysis cd go && go vet ./... +# Run unit tests +cd go && go test ./... + +# Check dependencies +cd go && go mod tidy +``` + +### Rust Implementation + +```bash +# Build the application (release mode recommended) +cd rust && cargo build --release + +# Run the application with automatic config discovery +cd rust && ./target/release/mail2couch-rs + +# Run with specific config file +cd rust && ./target/release/mail2couch-rs --config /path/to/config.json + +# Run with message limit +cd rust && ./target/release/mail2couch-rs --max-messages 100 + +# Run with dry-run mode +cd rust && ./target/release/mail2couch-rs --dry-run + +# Run linting/static analysis +cd rust && cargo clippy -- -D warnings + +# Run unit tests +cd rust && cargo test + +# Format code +cd rust && cargo fmt +``` + +### Integration Testing + +```bash # Run integration tests with Podman containers cd test && ./run-tests.sh # Run specialized tests cd test && ./test-wildcard-patterns.sh cd test && ./test-incremental-sync.sh - -# Run unit tests (none currently implemented) -cd go && go test ./... - -# Check dependencies -cd go && go mod tidy ``` ## Architecture @@ -90,11 +167,11 @@ This design ensures the same `config.json` format will work for both Go and Rust ### Current Implementation Status +#### Both Implementations - ✅ Configuration loading with automatic file discovery -- ✅ Command line flag support for config file path +- ✅ Command line flag support (--config/-c, --max-messages/-m, --dry-run/-n) - ✅ Per-account CouchDB database creation and management - ✅ IMAP connection and mailbox listing -- ✅ Build error fixes - ✅ Real IMAP message retrieval and parsing - ✅ Email storage to CouchDB framework with native attachments - ✅ Folder filtering logic with wildcard support (`*`, `?`, `[abc]` patterns) @@ -104,15 +181,40 @@ This design ensures the same `config.json` format will work for both Go and Rust - ✅ Sync vs Archive mode implementation - ✅ CouchDB attachment storage for email attachments - ✅ Full message body and attachment handling with MIME multipart support -- ✅ Command line argument support (GNU-style --max-messages/-m and --config/-c flags) - ✅ Per-account CouchDB databases for better organization - ✅ Incremental sync functionality with IMAP SEARCH and sync metadata tracking -- ❌ Rust implementation +- ✅ Comprehensive --dry-run mode for safe configuration testing + +#### Rust Implementation Additional Features +- ✅ Asynchronous processing with concurrent network operations +- ✅ Server-side IMAP keyword filtering (more efficient) +- ✅ Automatic retry logic with exponential backoff +- ✅ Structured error handling with detailed context +- ✅ Enhanced CLI with rich help system +- ✅ Comprehensive unit test coverage +- ✅ Emoji-enhanced logging for better user experience + +#### Go Implementation Characteristics +- ✅ Sequential processing (simple and reliable) +- ✅ Minimal dependencies and fast compilation +- ✅ Client-side keyword filtering +- ✅ Basic error handling with continue-on-error semantics ### Key Dependencies +#### Go Implementation - `github.com/emersion/go-imap/v2`: IMAP client library - `github.com/go-kivik/kivik/v4`: CouchDB client library +- `github.com/spf13/pflag`: GNU-style command line flags +- `github.com/emersion/go-message`: Email parsing + +#### Rust Implementation +- `async-imap`: Asynchronous IMAP client +- `reqwest`: HTTP client for CouchDB API +- `tokio`: Async runtime and utilities +- `clap`: Command line argument parsing +- `serde`: Serialization framework +- `mail-parser`: Email parsing with MIME support ### Incremental Sync Implementation diff --git a/IMPLEMENTATION_COMPARISON.md b/IMPLEMENTATION_COMPARISON.md index 4275ef5..4288f27 100644 --- a/IMPLEMENTATION_COMPARISON.md +++ b/IMPLEMENTATION_COMPARISON.md @@ -442,6 +442,8 @@ Both implementations currently share the same security limitations and features: ### Go Implementation Deployment +**Binary Name**: `mail2couch-go` + **Advantages**: - Single binary deployment - Minimal system dependencies @@ -450,16 +452,21 @@ Both implementations currently share the same security limitations and features: **Best Practices**: ```bash -# Build for production -cd go && go build -ldflags="-s -w" -o mail2couch . +# Build for production using justfile +just build-go-release + +# Or build directly +cd go && go build -ldflags="-s -w" -o mail2couch-go . # Deploy with systemd service -sudo cp mail2couch /usr/local/bin/ -sudo systemctl enable mail2couch.service +sudo cp go/mail2couch-go /usr/local/bin/ +sudo systemctl enable mail2couch-go.service ``` ### Rust Implementation Deployment +**Binary Name**: `mail2couch-rs` + **Advantages**: - Better resource utilization under load - Superior error recovery @@ -468,18 +475,33 @@ sudo systemctl enable mail2couch.service **Best Practices**: ```bash -# Build optimized release +# Build optimized release using justfile +just build-rust-release + +# Or build directly cd rust && cargo build --release # Deploy with enhanced monitoring -sudo cp target/release/mail2couch /usr/local/bin/ -sudo systemctl enable mail2couch.service +sudo cp rust/target/release/mail2couch-rs /usr/local/bin/ +sudo systemctl enable mail2couch-rs.service # Configure structured logging export RUST_LOG=info export MAIL2COUCH_LOG_FORMAT=json ``` +### Universal Installation + +```bash +# Build and install both implementations (user-local) +just install +# This installs to ~/bin/mail2couch-go and ~/bin/mail2couch-rs + +# Build and install both implementations (system-wide) +sudo just system-install +# This installs to /usr/local/bin/mail2couch-go and /usr/local/bin/mail2couch-rs +``` + --- ## Future Development Roadmap diff --git a/go/couch/couch.go b/go/couch/couch.go index c75c3b6..5ad565b 100644 --- a/go/couch/couch.go +++ b/go/couch/couch.go @@ -15,6 +15,15 @@ import ( "mail2couch/mail" ) +// DocumentSkippedError indicates that a document was skipped because it already exists +type DocumentSkippedError struct { + DocumentID string +} + +func (e *DocumentSkippedError) Error() string { + return fmt.Sprintf("document %s already exists, skipped", e.DocumentID) +} + // Client wraps the Kivik client type Client struct { *kivik.Client @@ -149,7 +158,7 @@ func (c *Client) StoreMessage(ctx context.Context, dbName string, doc *MailDocum } if exists { - return nil // Document already exists, skip + return &DocumentSkippedError{DocumentID: doc.ID} } // Store the document first (without attachments) diff --git a/go/mail2couch b/go/mail2couch-go similarity index 64% rename from go/mail2couch rename to go/mail2couch-go index cd300c5..11b890c 100755 Binary files a/go/mail2couch and b/go/mail2couch-go differ diff --git a/go/main.go b/go/main.go index 1523b09..fa04250 100644 --- a/go/main.go +++ b/go/main.go @@ -2,6 +2,7 @@ package main import ( "context" + "errors" "fmt" "log" "time" @@ -174,16 +175,26 @@ func processImapSource(source *config.MailSource, couchClient *couch.Client, dbN stored := 0 if !dryRun { ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + var skipped int for i, doc := range docs { err := couchClient.StoreMessage(ctx, dbName, doc, messages[i]) if err != nil { - log.Printf(" ERROR: Failed to store message %s: %v", doc.ID, err) + var skipErr *couch.DocumentSkippedError + if errors.As(err, &skipErr) { + skipped++ + } else { + log.Printf(" ERROR: Failed to store message %s: %v", doc.ID, err) + } } else { stored++ } } cancel() - fmt.Printf(" Stored %d/%d messages from %s\n", stored, len(messages), mailbox) + if skipped > 0 { + fmt.Printf(" Stored %d/%d messages from %s (%d skipped as duplicates)\n", stored, len(messages), mailbox, skipped) + } else { + fmt.Printf(" Stored %d/%d messages from %s\n", stored, len(messages), mailbox) + } } else { stored = len(messages) // In dry-run, assume all would be stored fmt.Printf(" DRY-RUN: Would store %d messages from %s\n", len(messages), mailbox) diff --git a/justfile b/justfile new file mode 100644 index 0000000..4a24fb5 --- /dev/null +++ b/justfile @@ -0,0 +1,229 @@ +# Justfile for mail2couch project +# Builds both Go and Rust implementations with distinct binary names + +# Default recipe +default: build + +# Build both implementations +build: build-go build-rust + +# Build Go implementation as mail2couch-go +build-go: + @echo "Building Go implementation..." + cd go && go build -o mail2couch-go . + @echo "✅ Built: go/mail2couch-go" + +# Build Rust implementation as mail2couch-rs +build-rust: + @echo "Building Rust implementation..." + cd rust && cargo build --release + @echo "✅ Built: rust/target/release/mail2couch-rs" + +# Build optimized release versions +build-release: build-go-release build-rust-release + +# Build optimized Go release +build-go-release: + @echo "Building optimized Go release..." + cd go && go build -ldflags="-s -w" -o mail2couch-go . + @echo "✅ Built optimized: go/mail2couch-go" + +# Build optimized Rust release (already built with --release above) +build-rust-release: build-rust + +# Install binaries to ~/bin (user-local installation) +install: build-release + @echo "Installing binaries to ~/bin..." + mkdir -p ~/bin + cp go/mail2couch-go ~/bin/ + cp rust/target/release/mail2couch-rs ~/bin/ + chmod +x ~/bin/mail2couch-go + chmod +x ~/bin/mail2couch-rs + @echo "✅ Installed mail2couch-go and mail2couch-rs to ~/bin" + @echo "💡 Make sure ~/bin is in your PATH" + +# Install binaries to /usr/local/bin (system-wide installation, requires sudo) +system-install: build-release + @echo "Installing binaries to /usr/local/bin..." + sudo cp go/mail2couch-go /usr/local/bin/ + sudo cp rust/target/release/mail2couch-rs /usr/local/bin/ + sudo chmod +x /usr/local/bin/mail2couch-go + sudo chmod +x /usr/local/bin/mail2couch-rs + @echo "✅ Installed mail2couch-go and mail2couch-rs to /usr/local/bin" + +# Uninstall binaries from ~/bin +uninstall: + @echo "Uninstalling binaries from ~/bin..." + rm -f ~/bin/mail2couch-go + rm -f ~/bin/mail2couch-rs + @echo "✅ Uninstalled mail2couch-go and mail2couch-rs from ~/bin" + +# Uninstall binaries from /usr/local/bin (requires sudo) +system-uninstall: + @echo "Uninstalling binaries from /usr/local/bin..." + sudo rm -f /usr/local/bin/mail2couch-go + sudo rm -f /usr/local/bin/mail2couch-rs + @echo "✅ Uninstalled mail2couch-go and mail2couch-rs from /usr/local/bin" + +# Run tests for both implementations +test: test-go test-rust + +# Test Go implementation +test-go: + @echo "Running Go tests..." + cd go && go test ./... + +# Test Rust implementation +test-rust: + @echo "Running Rust tests..." + cd rust && cargo test + +# Clean build artifacts +clean: + @echo "Cleaning build artifacts..." + cd go && rm -f mail2couch-go mail2couch + cd rust && cargo clean + @echo "✅ Cleaned build artifacts" + +# Check code formatting and linting +check: check-go check-rust + +# Check Go code +check-go: + @echo "Checking Go code..." + cd go && go fmt ./... + cd go && go vet ./... + +# Check Rust code +check-rust: + @echo "Checking Rust code..." + cd rust && cargo fmt --check + cd rust && cargo clippy -- -D warnings + +# Fix code formatting +fmt: fmt-go fmt-rust + +# Format Go code +fmt-go: + @echo "Formatting Go code..." + cd go && go fmt ./... + +# Format Rust code +fmt-rust: + @echo "Formatting Rust code..." + cd rust && cargo fmt + +# Development convenience - build and test Go version +dev-go: build-go + ./go/mail2couch-go --help + +# Development convenience - build and test Rust version +dev-rust: build-rust + ./rust/target/release/mail2couch-rs --help + +# Show binary sizes +sizes: build + @echo "Binary sizes:" + @ls -lh go/mail2couch-go rust/target/release/mail2couch-rs | awk '{print $5 "\t" $9}' + +# Run both binaries with --version to compare +versions: build + @echo "Go version:" + @./go/mail2couch-go --help | head -1 + @echo "" + @echo "Rust version:" + @./rust/target/release/mail2couch-rs --version + +# Clean and rebuild everything +rebuild: clean build + +# Systemd service management + +# Install systemd user service files +install-services: + @echo "Installing systemd user service files..." + mkdir -p ~/.config/systemd/user + cp systemd/*.service ~/.config/systemd/user/ + cp systemd/*.timer ~/.config/systemd/user/ + systemctl --user daemon-reload + @echo "✅ Installed systemd user services and timers" + @echo "💡 Enable with: systemctl --user enable mail2couch-{go,rs}.service" + @echo "💡 Enable timers with: systemctl --user enable mail2couch-{go,rs}.timer" + +# Uninstall systemd user service files +uninstall-services: + @echo "Uninstalling systemd user service files..." + systemctl --user stop mail2couch-go.service mail2couch-rs.service || true + systemctl --user stop mail2couch-go.timer mail2couch-rs.timer || true + systemctl --user stop mail2couch-go-hourly.timer mail2couch-rs-hourly.timer || true + systemctl --user stop mail2couch-go-daily.timer mail2couch-rs-daily.timer || true + systemctl --user disable mail2couch-go.service mail2couch-rs.service || true + systemctl --user disable mail2couch-go.timer mail2couch-rs.timer || true + systemctl --user disable mail2couch-go-hourly.timer mail2couch-rs-hourly.timer || true + systemctl --user disable mail2couch-go-daily.timer mail2couch-rs-daily.timer || true + rm -f ~/.config/systemd/user/mail2couch-*.service + rm -f ~/.config/systemd/user/mail2couch-*.timer + systemctl --user daemon-reload + @echo "✅ Uninstalled systemd user services and timers" + +# Show systemd service status +service-status: + @echo "Service status:" + systemctl --user status mail2couch-go.service || true + systemctl --user status mail2couch-rs.service || true + @echo "" + @echo "Timer status:" + systemctl --user list-timers mail2couch-* + +# Enable Go implementation with default 30-minute timer +enable-go: + systemctl --user enable mail2couch-go.service + systemctl --user enable mail2couch-go.timer + systemctl --user start mail2couch-go.timer + @echo "✅ Enabled Go implementation with 30-minute timer" + +# Enable Rust implementation with default 30-minute timer +enable-rust: + systemctl --user enable mail2couch-rs.service + systemctl --user enable mail2couch-rs.timer + systemctl --user start mail2couch-rs.timer + @echo "✅ Enabled Rust implementation with 30-minute timer" + +# Enable Go implementation with hourly timer +enable-go-hourly: + systemctl --user enable mail2couch-go.service + systemctl --user enable mail2couch-go-hourly.timer + systemctl --user start mail2couch-go-hourly.timer + @echo "✅ Enabled Go implementation with hourly timer" + +# Enable Rust implementation with hourly timer +enable-rust-hourly: + systemctl --user enable mail2couch-rs.service + systemctl --user enable mail2couch-rs-hourly.timer + systemctl --user start mail2couch-rs-hourly.timer + @echo "✅ Enabled Rust implementation with hourly timer" + +# Enable Go implementation with daily timer +enable-go-daily: + systemctl --user enable mail2couch-go.service + systemctl --user enable mail2couch-go-daily.timer + systemctl --user start mail2couch-go-daily.timer + @echo "✅ Enabled Go implementation with daily timer" + +# Enable Rust implementation with daily timer +enable-rust-daily: + systemctl --user enable mail2couch-rs.service + systemctl --user enable mail2couch-rs-daily.timer + systemctl --user start mail2couch-rs-daily.timer + @echo "✅ Enabled Rust implementation with daily timer" + +# Disable all timers and services +disable-all: + systemctl --user stop mail2couch-go.timer mail2couch-rs.timer || true + systemctl --user stop mail2couch-go-hourly.timer mail2couch-rs-hourly.timer || true + systemctl --user stop mail2couch-go-daily.timer mail2couch-rs-daily.timer || true + systemctl --user disable mail2couch-go.timer mail2couch-rs.timer || true + systemctl --user disable mail2couch-go-hourly.timer mail2couch-rs-hourly.timer || true + systemctl --user disable mail2couch-go-daily.timer mail2couch-rs-daily.timer || true + systemctl --user disable mail2couch-go.service mail2couch-rs.service || true + @echo "✅ Disabled all timers and services" \ No newline at end of file diff --git a/rust/Cargo.toml b/rust/Cargo.toml index b10a27e..2b070ac 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -65,5 +65,5 @@ name = "mail2couch" path = "src/lib.rs" [[bin]] -name = "mail2couch" +name = "mail2couch-rs" path = "src/main.rs" \ No newline at end of file diff --git a/rust/src/imap.rs b/rust/src/imap.rs index 6b3d553..0b24ed7 100644 --- a/rust/src/imap.rs +++ b/rust/src/imap.rs @@ -578,9 +578,9 @@ impl ImapClient { let headers = self.extract_headers(&parsed_message); // Extract attachments and their data - let (has_attachments, attachment_stubs, attachment_data) = self.extract_attachments_with_data(&parsed_message); + let (has_attachments, _attachment_stubs, attachment_data) = self.extract_attachments_with_data(&parsed_message); - let mut mail_doc = MailDocument::new( + let mail_doc = MailDocument::new( uid.to_string(), mailbox.to_string(), from, @@ -705,21 +705,14 @@ impl ImapClient { // Generate a filename for the attachment let filename = self.get_attachment_filename(part, index); - // Get the content data (try different methods based on content type) - let body_data = if let Some(text_content) = part.get_text_contents() { - // Text-based attachments - log::debug!("Found text attachment content: {} bytes", text_content.len()); - text_content.as_bytes().to_vec() - } else { - // For now, skip attachments without text content - // TODO: Implement binary attachment support with proper mail-parser API - log::debug!("Skipping non-text attachment for part {} (content-type: {})", index, content_type.c_type); - vec![] - }; + // Get the binary content data using the proper mail-parser API + // This works for both text and binary attachments (images, PDFs, etc.) + let body_data = part.get_contents().to_vec(); + log::debug!("Found attachment content: {} bytes (content-type: {})", body_data.len(), content_type.c_type); let content_type_str = content_type.c_type.to_string(); - // Only create attachment stub if we have actual data + // Create attachment stub - get_contents() always returns the full data if !body_data.is_empty() { let attachment_stub = AttachmentStub { content_type: content_type_str.clone(), @@ -738,12 +731,6 @@ impl ImapClient { (has_attachments, attachment_stubs, attachment_data) } - /// Extract attachments from a parsed message (deprecated - use extract_attachments_with_data) - /// Returns (has_attachments, attachment_stubs) - fn extract_attachments(&self, message: &Message) -> (bool, HashMap) { - let (has_attachments, attachment_stubs, _) = self.extract_attachments_with_data(message); - (has_attachments, attachment_stubs) - } /// Determine if a message part is an attachment fn is_attachment_part(&self, part: &mail_parser::MessagePart, content_type: &mail_parser::ContentType) -> bool { diff --git a/systemd/mail2couch-go-daily.timer b/systemd/mail2couch-go-daily.timer new file mode 100644 index 0000000..fab6817 --- /dev/null +++ b/systemd/mail2couch-go-daily.timer @@ -0,0 +1,17 @@ +[Unit] +Description=Run mail2couch Go Implementation daily +Documentation=https://kode.naiv.no/olemd/mail2couch +Requires=mail2couch-go.service + +[Timer] +# Run daily at 2:30 AM with some randomization +OnCalendar=*-*-* 02:30:00 +# Run 10 minutes after boot if we missed a scheduled run +OnBootSec=10min +# If the system was off, run shortly after startup +Persistent=true +# Add up to 30 minutes randomization to spread load +RandomizedDelaySec=1800 + +[Install] +WantedBy=timers.target \ No newline at end of file diff --git a/systemd/mail2couch-go-hourly.timer b/systemd/mail2couch-go-hourly.timer new file mode 100644 index 0000000..bfd159d --- /dev/null +++ b/systemd/mail2couch-go-hourly.timer @@ -0,0 +1,17 @@ +[Unit] +Description=Run mail2couch Go Implementation every hour +Documentation=https://kode.naiv.no/olemd/mail2couch +Requires=mail2couch-go.service + +[Timer] +# Run every hour at a random minute to spread load +OnCalendar=hourly +# Run 5 minutes after boot if we missed a scheduled run +OnBootSec=5min +# If the system was off, run shortly after startup +Persistent=true +# Add randomization to avoid all users hitting servers simultaneously +RandomizedDelaySec=600 + +[Install] +WantedBy=timers.target \ No newline at end of file diff --git a/systemd/mail2couch-go.service b/systemd/mail2couch-go.service new file mode 100644 index 0000000..fcd8660 --- /dev/null +++ b/systemd/mail2couch-go.service @@ -0,0 +1,35 @@ +[Unit] +Description=mail2couch Go Implementation - Email backup to CouchDB +Documentation=https://kode.naiv.no/olemd/mail2couch +After=network.target +Wants=network.target + +[Service] +Type=oneshot +User=%i +ExecStart=%h/bin/mail2couch-go +WorkingDirectory=%h/.config/mail2couch + +# Environment +Environment=PATH=%h/bin:/usr/local/bin:/usr/bin:/bin + +# Logging +StandardOutput=journal +StandardError=journal +SyslogIdentifier=mail2couch-go + +# Security settings +NoNewPrivileges=yes +PrivateTmp=yes +ProtectSystem=strict +ProtectHome=read-only +ReadOnlyPaths=/ +ReadWritePaths=%h/.config/mail2couch %h/.local/share/mail2couch + +# Resource limits +MemoryHigh=512M +MemoryMax=1G +CPUQuota=50% + +[Install] +WantedBy=default.target \ No newline at end of file diff --git a/systemd/mail2couch-go.timer b/systemd/mail2couch-go.timer new file mode 100644 index 0000000..d83e127 --- /dev/null +++ b/systemd/mail2couch-go.timer @@ -0,0 +1,17 @@ +[Unit] +Description=Run mail2couch Go Implementation every 30 minutes +Documentation=https://kode.naiv.no/olemd/mail2couch +Requires=mail2couch-go.service + +[Timer] +# Run every 30 minutes +OnCalendar=*:0/30 +# Run 2 minutes after boot if we missed a scheduled run +OnBootSec=2min +# If the system was off, run shortly after startup +Persistent=true +# Add some randomization to avoid all users hitting servers simultaneously +RandomizedDelaySec=300 + +[Install] +WantedBy=timers.target \ No newline at end of file diff --git a/systemd/mail2couch-rs-daily.timer b/systemd/mail2couch-rs-daily.timer new file mode 100644 index 0000000..1439f0e --- /dev/null +++ b/systemd/mail2couch-rs-daily.timer @@ -0,0 +1,17 @@ +[Unit] +Description=Run mail2couch Rust Implementation daily +Documentation=https://kode.naiv.no/olemd/mail2couch +Requires=mail2couch-rs.service + +[Timer] +# Run daily at 2:30 AM with some randomization +OnCalendar=*-*-* 02:30:00 +# Run 10 minutes after boot if we missed a scheduled run +OnBootSec=10min +# If the system was off, run shortly after startup +Persistent=true +# Add up to 30 minutes randomization to spread load +RandomizedDelaySec=1800 + +[Install] +WantedBy=timers.target \ No newline at end of file diff --git a/systemd/mail2couch-rs-hourly.timer b/systemd/mail2couch-rs-hourly.timer new file mode 100644 index 0000000..6f48658 --- /dev/null +++ b/systemd/mail2couch-rs-hourly.timer @@ -0,0 +1,17 @@ +[Unit] +Description=Run mail2couch Rust Implementation every hour +Documentation=https://kode.naiv.no/olemd/mail2couch +Requires=mail2couch-rs.service + +[Timer] +# Run every hour at a random minute to spread load +OnCalendar=hourly +# Run 5 minutes after boot if we missed a scheduled run +OnBootSec=5min +# If the system was off, run shortly after startup +Persistent=true +# Add randomization to avoid all users hitting servers simultaneously +RandomizedDelaySec=600 + +[Install] +WantedBy=timers.target \ No newline at end of file diff --git a/systemd/mail2couch-rs.service b/systemd/mail2couch-rs.service new file mode 100644 index 0000000..3fa4714 --- /dev/null +++ b/systemd/mail2couch-rs.service @@ -0,0 +1,36 @@ +[Unit] +Description=mail2couch Rust Implementation - Email backup to CouchDB +Documentation=https://kode.naiv.no/olemd/mail2couch +After=network.target +Wants=network.target + +[Service] +Type=oneshot +User=%i +ExecStart=%h/bin/mail2couch-rs +WorkingDirectory=%h/.config/mail2couch + +# Environment +Environment=PATH=%h/bin:/usr/local/bin:/usr/bin:/bin +Environment=RUST_LOG=info + +# Logging +StandardOutput=journal +StandardError=journal +SyslogIdentifier=mail2couch-rs + +# Security settings +NoNewPrivileges=yes +PrivateTmp=yes +ProtectSystem=strict +ProtectHome=read-only +ReadOnlyPaths=/ +ReadWritePaths=%h/.config/mail2couch %h/.local/share/mail2couch + +# Resource limits +MemoryHigh=512M +MemoryMax=1G +CPUQuota=50% + +[Install] +WantedBy=default.target \ No newline at end of file diff --git a/systemd/mail2couch-rs.timer b/systemd/mail2couch-rs.timer new file mode 100644 index 0000000..4483fe4 --- /dev/null +++ b/systemd/mail2couch-rs.timer @@ -0,0 +1,17 @@ +[Unit] +Description=Run mail2couch Rust Implementation every 30 minutes +Documentation=https://kode.naiv.no/olemd/mail2couch +Requires=mail2couch-rs.service + +[Timer] +# Run every 30 minutes +OnCalendar=*:0/30 +# Run 2 minutes after boot if we missed a scheduled run +OnBootSec=2min +# If the system was off, run shortly after startup +Persistent=true +# Add some randomization to avoid all users hitting servers simultaneously +RandomizedDelaySec=300 + +[Install] +WantedBy=timers.target \ No newline at end of file diff --git a/test-both-implementations.sh b/test-both-implementations.sh index 4319a72..35c8e05 100755 --- a/test-both-implementations.sh +++ b/test-both-implementations.sh @@ -36,7 +36,7 @@ build_implementations() { # Build Go implementation echo -e "${BLUE} Building Go implementation...${NC}" cd go - go build -o mail2couch . + go build -o mail2couch-go . cd .. echo -e "${GREEN} ✅ Go implementation built${NC}" @@ -53,7 +53,7 @@ run_go() { echo -e "${BLUE}🦬 Running Go implementation...${NC}" cd go echo -e "${BLUE} Using config: config-test-go.json${NC}" - ./mail2couch -c config-test-go.json + ./mail2couch-go -c config-test-go.json cd .. echo -e "${GREEN}✅ Go implementation completed${NC}" } @@ -63,7 +63,7 @@ run_rust() { echo -e "${BLUE}🦀 Running Rust implementation...${NC}" cd rust echo -e "${BLUE} Using config: config-test-rust.json${NC}" - ./target/release/mail2couch -c config-test-rust.json + ./target/release/mail2couch-rs -c config-test-rust.json cd .. echo -e "${GREEN}✅ Rust implementation completed${NC}" } diff --git a/test-config-go.json b/test-config-go.json new file mode 100644 index 0000000..1be5cce --- /dev/null +++ b/test-config-go.json @@ -0,0 +1,24 @@ +{ + "couchDb": { + "url": "http://localhost:5984", + "user": "admin", + "password": "password" + }, + "mailSources": [ + { + "name": "go_test_account", + "enabled": true, + "protocol": "imap", + "host": "localhost", + "port": 3143, + "user": "testuser1", + "password": "password123", + "mode": "archive", + "folderFilter": { + "include": ["INBOX"], + "exclude": [] + }, + "messageFilter": {} + } + ] +} \ No newline at end of file diff --git a/test-config-rust.json b/test-config-rust.json new file mode 100644 index 0000000..7229196 --- /dev/null +++ b/test-config-rust.json @@ -0,0 +1,24 @@ +{ + "couchDb": { + "url": "http://localhost:5984", + "user": "admin", + "password": "password" + }, + "mailSources": [ + { + "name": "rust_test_account", + "enabled": true, + "protocol": "imap", + "host": "localhost", + "port": 3143, + "user": "testuser1", + "password": "password123", + "mode": "archive", + "folderFilter": { + "include": ["INBOX"], + "exclude": [] + }, + "messageFilter": {} + } + ] +} \ No newline at end of file diff --git a/test-config-shared.json b/test-config-shared.json new file mode 100644 index 0000000..79fe83e --- /dev/null +++ b/test-config-shared.json @@ -0,0 +1,24 @@ +{ + "couchDb": { + "url": "http://localhost:5984", + "user": "admin", + "password": "password" + }, + "mailSources": [ + { + "name": "comparison_test_account", + "enabled": true, + "protocol": "imap", + "host": "localhost", + "port": 3143, + "user": "testuser1", + "password": "password123", + "mode": "archive", + "folderFilter": { + "include": ["INBOX"], + "exclude": [] + }, + "messageFilter": {} + } + ] +} \ No newline at end of file diff --git a/test-incremental-config.json b/test-incremental-config.json new file mode 100644 index 0000000..eed0415 --- /dev/null +++ b/test-incremental-config.json @@ -0,0 +1,24 @@ +{ + "couchDb": { + "url": "http://localhost:5984", + "user": "admin", + "password": "password" + }, + "mailSources": [ + { + "name": "incremental_test", + "enabled": true, + "protocol": "imap", + "host": "localhost", + "port": 3143, + "user": "testuser1", + "password": "password123", + "mode": "archive", + "folderFilter": { + "include": ["INBOX"], + "exclude": [] + }, + "messageFilter": {} + } + ] +} \ No newline at end of file