initial import

This commit is contained in:
2025-11-20 14:59:04 +01:00
commit 4ee1ef0a81
12 changed files with 1392 additions and 0 deletions

11
.env.example Normal file
View File

@@ -0,0 +1,11 @@
# POP3 Source Server Configuration
POP3_HOST=pop.example.com
POP3_PORT=995
POP3_USERNAME=your_pop3_username
POP3_PASSWORD=your_pop3_password
# IMAP Destination Server Configuration
IMAP_HOST=imap.example.com
IMAP_PORT=993
IMAP_USERNAME=your_imap_username
IMAP_PASSWORD=your_imap_password

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
/target
.env

1051
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 = "rs_pop_imap_importer"
version = "0.1.0"
edition = "2024"
[dependencies]
clap = { version = "4.5.53", features = ["derive"] }
dotenvy = "0.15.7"
imap = "2.4.1"
imap-proto = "0.16.6"
native-tls = "0.2.14"
rust-pop3-client = "0.2.2"

26
Dockerfile Normal file
View File

@@ -0,0 +1,26 @@
# Build stage
FROM rust:1.91 AS builder
WORKDIR /usr/src/pop_imap_importer
# Copy manifests first to leverage Docker layer caching
COPY src/ Cargo.* ./
# Build the actual application
RUN cargo build --release
RUN cargo install --path .
# Runtime stage
FROM debian:bookworm-slim
# Install CA certificates and OpenSSL runtime libraries
RUN apt-get update && \
apt-get install -y ca-certificates libssl3 && \
rm -rf /var/lib/apt/lists/*
# RUN apt-get update && apt-get install -y extra-runtime-dependencies && rm -rf /var/lib/apt/lists/*
COPY --from=builder /usr/local/cargo/bin/pop_imap_importer /usr/local/bin/pop_imap_importer
CMD ["pop_imap_importer"]

41
Makefile Normal file
View File

@@ -0,0 +1,41 @@
# Makefile for building and running the POP3 to IMAP Importer Docker image
# Variables
IMAGE_NAME = pop-imap-importer
DOCKER_PLATFORM = linux/amd64
# Default target
.PHONY: help
help: ## Show this help message
@echo "Usage: make [target]"
@echo ""
@echo "Targets:"
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}'
.PHONY: build
build: ## Build the Docker image for linux/amd64 platform
docker build --platform=$(DOCKER_PLATFORM) -t $(IMAGE_NAME) .
.PHONY: run
run: ## Run the Docker container
docker run --platform=$(DOCKER_PLATFORM) --rm -it $(IMAGE_NAME)
.PHONY: run-with-env
run-with-env: ## Run the Docker container with a custom .env file
docker run --platform=$(DOCKER_PLATFORM) --rm -it -v $(PWD)/.env:/app/.env $(IMAGE_NAME)
.PHONY: clean
clean: ## Remove the Docker image
docker rmi $(IMAGE_NAME)
.PHONY: push
push: ## Push the Docker image to a registry (requires REPOSITORY variable)
ifndef REPOSITORY
$(error REPOSITORY is not set. Please set it with: make push REPOSITORY=your-repo/image-name)
endif
docker tag $(IMAGE_NAME) $(REPOSITORY):latest
docker push $(REPOSITORY):latest
.PHONY: shell
shell: ## Run a shell in the Docker container
docker run --platform=$(DOCKER_PLATFORM) --rm -it $(IMAGE_NAME) sh

42
README.md Normal file
View File

@@ -0,0 +1,42 @@
# POP to IMAP Importer
A Rust application that downloads emails from a POP3 server and imports them into an IMAP server's INBOX.
## Features
- Downloads all emails from a POP3 server
- Imports emails to the INBOX of an IMAP server
- Secure TLS connections
- Environment-based configuration
## Setup
1. Copy `.env.example` to `.env`:
```bash
cp .env.example .env
```
2. Edit `.env` with your server credentials:
```env
# POP3 Source Server Configuration
POP3_HOST=pop.example.com
POP3_PORT=995
POP3_USERNAME=your_pop3_username
POP3_PASSWORD=your_pop3_password
# IMAP Destination Server Configuration
IMAP_HOST=imap.example.com
IMAP_PORT=993
IMAP_USERNAME=your_imap_username
IMAP_PASSWORD=your_imap_password
```
## Usage
Run the application:
```bash
cargo run
```

3
src/config/mod.rs Normal file
View File

@@ -0,0 +1,3 @@
pub mod settings;
pub use settings::Settings;

51
src/config/settings.rs Normal file
View File

@@ -0,0 +1,51 @@
use dotenvy::dotenv;
use dotenvy::from_filename;
use std::env;
#[derive(Debug, Clone)]
pub struct Pop3Config {
pub host: String,
pub port: u16,
pub username: String,
pub password: String,
}
#[derive(Debug, Clone)]
pub struct ImapConfig {
pub host: String,
pub port: u16,
pub username: String,
pub password: String,
}
#[derive(Debug, Clone)]
pub struct Settings {
pub pop3: Pop3Config,
pub imap: ImapConfig,
}
impl Settings {
pub fn from_env_file(filename: &str) -> Result<Self, Box<dyn std::error::Error>> {
if filename == ".env" {
dotenv().ok();
} else {
from_filename(filename).ok();
}
let pop3 = Pop3Config {
host: env::var("POP3_HOST")?,
port: env::var("POP3_PORT")?.parse()?,
username: env::var("POP3_USERNAME")?,
password: env::var("POP3_PASSWORD")?,
};
let imap = ImapConfig {
host: env::var("IMAP_HOST")?,
port: env::var("IMAP_PORT")?.parse()?,
username: env::var("IMAP_USERNAME")?,
password: env::var("IMAP_PASSWORD")?,
};
Ok(Settings { pop3, imap })
}
}

43
src/imap_client/mod.rs Normal file
View File

@@ -0,0 +1,43 @@
use crate::config::settings::ImapConfig;
use native_tls::TlsConnector;
pub struct ImapClient {
session: Option<imap::Session<native_tls::TlsStream<std::net::TcpStream>>>,
}
impl ImapClient {
pub fn new(_config: &ImapConfig) -> Result<Self, Box<dyn std::error::Error>> {
Ok(ImapClient {
session: None,
})
}
pub fn login(&mut self, config: &ImapConfig) -> Result<(), Box<dyn std::error::Error>> {
let tls = TlsConnector::builder().build()?;
let client = imap::connect((config.host.as_str(), config.port), &config.host, &tls)?;
let session = client.login(&config.username, &config.password).map_err(|e| e.0)?;
self.session = Some(session);
Ok(())
}
pub fn select_inbox(&mut self) -> Result<(), Box<dyn std::error::Error>> {
if let Some(ref mut session) = self.session {
session.select("INBOX")?;
}
Ok(())
}
pub fn append_message(&mut self, message: &str) -> Result<(), Box<dyn std::error::Error>> {
if let Some(ref mut session) = self.session {
session.append("INBOX", message.as_bytes())?;
}
Ok(())
}
pub fn logout(self) -> Result<(), Box<dyn std::error::Error>> {
if let Some(mut session) = self.session {
session.logout()?;
}
Ok(())
}
}

67
src/main.rs Normal file
View File

@@ -0,0 +1,67 @@
mod config;
mod pop3_client;
mod imap_client;
use clap::Parser;
use config::Settings;
use pop3_client::Pop3Client;
use imap_client::ImapClient;
/// POP3 to IMAP Email Importer
///
/// This utility migrates emails from a POP3 server to an IMAP server.
#[derive(Parser, Debug)]
#[clap(version, about, long_about = None)]
struct Args {
/// Path to the .env file containing server configurations
#[clap(short, long, default_value = ".env")]
env_file: String,
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let args = Args::parse();
println!("Starting POP3 to IMAP email importer...");
// Load configuration from specified .env file
let settings = Settings::from_env_file(&args.env_file)?;
// Connect to POP3 server
println!("Connecting to POP3 server at {}:{}...", settings.pop3.host, settings.pop3.port);
let mut pop3_client = Pop3Client::new(&settings.pop3)?;
pop3_client.login(&settings.pop3)?;
println!("Successfully connected to POP3 server");
// Connect to IMAP server
println!("Connecting to IMAP server at {}:{}...", settings.imap.host, settings.imap.port);
let mut imap_client = ImapClient::new(&settings.imap)?;
imap_client.login(&settings.imap)?;
imap_client.select_inbox()?;
println!("Successfully connected to IMAP server");
// List messages in POP3 inbox
let messages = pop3_client.list_messages()?;
println!("Found {} messages in POP3 inbox", messages.len());
// Process each message
for (msg_id, _) in messages {
println!("Processing message ID: {}", msg_id);
// Retrieve message content
let message_content = pop3_client.retrieve_message(msg_id)?;
// Append message to IMAP inbox
imap_client.append_message(&message_content)?;
println!("Message {} imported successfully", msg_id);
// Optionally delete message from POP3 server after successful import
// pop3_client.delete_message(msg_id)?;
}
// Clean up connections
pop3_client.quit()?;
imap_client.logout()?;
println!("Email import completed successfully!");
Ok(())
}

43
src/pop3_client/mod.rs Normal file
View File

@@ -0,0 +1,43 @@
use crate::config::settings::Pop3Config;
use rust_pop3_client::Pop3Connection;
pub struct Pop3Client {
connection: Pop3Connection,
}
impl Pop3Client {
pub fn new(config: &Pop3Config) -> Result<Self, Box<dyn std::error::Error>> {
let connection = Pop3Connection::new(&config.host, config.port)?;
Ok(Pop3Client { connection })
}
pub fn login(&mut self, config: &Pop3Config) -> Result<(), Box<dyn std::error::Error>> {
self.connection.login(&config.username, &config.password)?;
Ok(())
}
pub fn list_messages(&mut self) -> Result<Vec<(u32, u32)>, Box<dyn std::error::Error>> {
let infos = self.connection.list()?;
let list = infos.into_iter().map(|info| (info.message_id, info.message_size)).collect();
Ok(list)
}
pub fn retrieve_message(&mut self, msg_id: u32) -> Result<String, Box<dyn std::error::Error>> {
let mut buffer = Vec::new();
self.connection.retrieve(msg_id, &mut buffer)?;
let message = String::from_utf8(buffer)?;
Ok(message)
}
#[allow(dead_code)]
pub fn delete_message(&mut self, msg_id: u32) -> Result<(), Box<dyn std::error::Error>> {
self.connection.delete(msg_id)?;
Ok(())
}
pub fn quit(&mut self) -> Result<(), Box<dyn std::error::Error>> {
// The rust-pop3-client doesn't seem to have an explicit quit method
// The connection should be closed when the object is dropped
Ok(())
}
}