Rewrite from Python to Go for single-binary cross-platform builds

Replaces imapdown.py with a multi-file Go implementation using
github.com/emersion/go-imap/v2. All features preserved: SSL/STARTTLS,
incremental UID-based downloads, attachment extraction to zip,
modified UTF-7 folder name decoding, and full-mode safety checks.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-25 17:54:41 +00:00
parent 85db1ddba6
commit eb63d8cbc1
11 changed files with 1230 additions and 485 deletions
+78 -27
View File
@@ -4,42 +4,56 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
## Project Overview
This is a single-file Python script (`imapdown.py`) that downloads all emails from an IMAP server into individual EML files, preserving the folder hierarchy. It uses only Python's standard library and has no external dependencies.
This project downloads all emails from an IMAP server into individual EML files, preserving the folder hierarchy.
Built with Go as a single self-contained binary, fast and cross-platform.
## Development Environment
- Python 3.6+ required
- Virtual environment is set up in `.venv` - activate it before running:
```bash
source .venv/bin/activate
```
- Go 1.21+ required
- Dependencies: `github.com/emersion/go-imap/v2` (auto-installed via `go mod tidy`)
- Build with: `make build` or `go build`
- Cross-compile with: `make build-all`
## Running the Script
## Running the Application
First, build the binary:
```bash
make build
# Or cross-compile for all platforms:
make build-all
```
Basic usage (incremental mode - only downloads new emails):
```bash
./imapdown.py --server imap.example.com --email user@example.com --user user@example.com --password "password" --ssl
./imapdown -server imap.example.com -email user@example.com -user user@example.com -password "password" -ssl
```
Full download (ignores previous state, requires empty target directory):
```bash
./imapdown.py --server imap.example.com --email user@example.com --user user@example.com --password "password" --ssl --full
./imapdown -server imap.example.com -email user@example.com -user user@example.com -password "password" -ssl -full
```
Testing/debugging with limited emails:
```bash
./imapdown.py --server imap.example.com --email user@example.com --user user@example.com --password "password" --ssl --limit 10
./imapdown -server imap.example.com -email user@example.com -user user@example.com -password "password" -ssl -limit 10
```
Custom storage directory:
```bash
./imapdown.py --server imap.example.com --email user@example.com --user user@example.com --password "password" --ssl --output /path/to/backup
./imapdown -server imap.example.com -email user@example.com -user user@example.com -password "password" -ssl -output /path/to/backup
```
## Architecture
### Single-File Design
The entire application is contained in `imapdown.py` (13KB). This is intentional - no modules or packages.
### Implementation Structure
The code is organized into multiple files for clarity:
- `main.go` - Entry point, CLI parsing, orchestration
- `imap.go` - IMAP connection and folder operations
- `email.go` - Email parsing and attachment extraction
- `state.go` - State file management (JSON)
- `filename.go` - Filename sanitization and Modified UTF-7 decoding
- `Makefile` - Build targets
### State Tracking
- The script maintains a `.imapdown_state.json` file in each email account's download folder
@@ -62,15 +76,15 @@ The entire application is contained in `imapdown.py` (13KB). This is intentional
### Key Implementation Details
**Modified UTF-7 Decoding**: IMAP folder names use modified UTF-7 encoding (see `decode_modified_utf7()` at line 39). This is not standard base64 - it uses `,` instead of `/` and has special `&` handling.
**Modified UTF-7 Decoding**: IMAP folder names use modified UTF-7 encoding. This is not standard base64 - it uses `,` instead of `/` and has special `&` handling. Implemented in `DecodeModifiedUTF7()` in `filename.go`.
**Filename Sanitization**: Two-stage process:
- `sanitize_filename()`: Removes invalid filesystem characters, max 50 chars for subjects
- `sanitize_folder_path()`: Converts IMAP folder separators (`.` or `/`) to OS path separators
- `SanitizeFilename()`: Removes invalid filesystem characters, max 50 chars for subjects
- `SanitizeFolderPath()`: Converts IMAP folder separators (`.` or `/`) to OS path separators
**UID-Based Incremental Updates**: Uses IMAP UIDs (not sequence numbers) because UIDs are persistent. The search `UID {last_uid + 1}:*` fetches only new messages. Some servers return the highest UID even when searching for higher UIDs, so there's additional filtering at line 251.
**UID-Based Incremental Updates**: Uses IMAP UIDs (not sequence numbers) because UIDs are persistent. When `lastUID > 0`, searches for UIDs > lastUID. On first run (`lastUID == 0`), searches for all messages using an empty SearchCriteria. Some servers return the highest UID even when searching for higher UIDs, so there's additional filtering.
**Full Mode Safety**: `--full` mode checks if the download folder already contains `.eml` files and refuses to run (line 325). This prevents accidental duplicates. Users must delete the folder first.
**Full Mode Safety**: `-full` mode checks if the download folder already contains `.eml` files and refuses to run. This prevents accidental duplicates. Users must delete the folder first.
**Attachment Handling**:
- Walks message parts looking for `Content-Disposition: attachment` or `inline`
@@ -79,22 +93,59 @@ The entire application is contained in `imapdown.py` (13KB). This is intentional
## Output Structure
Without `-output` flag (default: `./{email_address}`):
```
{output_dir}/ # default: ./download
── {email_address}/ # sanitized email address
├── .imapdown_state.json
├── INBOX/
── 123_20240115_Meeting_notes.eml
│ └── 124_20240116_Report.zip
└── Sent/
└── 456_20240114_RE_Question.eml
{email_address}/ # sanitized email address in current directory
── .imapdown_state.json
├── INBOX/
├── 123_20240115_Meeting_notes.eml
── 124_20240116_Report.zip
└── Sent/
└── 456_20240114_RE_Question.eml
```
With `-output /path/to/backup`:
```
/path/to/backup/ # specified output directory used directly
├── .imapdown_state.json
├── INBOX/
│ ├── 123_20240115_Meeting_notes.eml
│ └── 124_20240116_Report.zip
└── Sent/
└── 456_20240114_RE_Question.eml
```
## Building and Installing
Build for current platform:
```bash
make build
```
Cross-compile for all platforms:
```bash
make build-all
# Produces: imapdown-linux-amd64, imapdown-linux-arm64,
# imapdown-darwin-amd64, imapdown-darwin-arm64,
# imapdown-windows-amd64.exe
```
Install to `$GOPATH/bin`:
```bash
make install
```
Clean build artifacts:
```bash
make clean
```
## Testing
No formal test suite exists. Manual testing approach:
- Use `--limit 10` to download a small batch for verification
- Use `-limit 10` to download a small batch for verification
- Test SSL vs STARTTLS connections
- Test incremental mode by running twice
- Verify `.eml` files open correctly in email clients
- Check that folders with special characters (non-ASCII) are handled correctly
- Test first run (no state file) to ensure all messages are downloaded