feature/add-dry-run-mode #2
6 changed files with 253 additions and 133 deletions
|
|
@ -86,6 +86,7 @@ func (ms *MailSource) IsArchiveMode() bool {
|
||||||
type CommandLineArgs struct {
|
type CommandLineArgs struct {
|
||||||
ConfigPath string
|
ConfigPath string
|
||||||
MaxMessages int
|
MaxMessages int
|
||||||
|
DryRun bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// ParseCommandLine parses command line arguments using GNU-style options
|
// ParseCommandLine parses command line arguments using GNU-style options
|
||||||
|
|
@ -95,6 +96,7 @@ func ParseCommandLine() *CommandLineArgs {
|
||||||
// Define long options with -- and short options with -
|
// Define long options with -- and short options with -
|
||||||
pflag.StringVarP(&args.ConfigPath, "config", "c", "", "Path to configuration file")
|
pflag.StringVarP(&args.ConfigPath, "config", "c", "", "Path to configuration file")
|
||||||
pflag.IntVarP(&args.MaxMessages, "max-messages", "m", 0, "Maximum number of messages to process per mailbox per run (0 = no limit)")
|
pflag.IntVarP(&args.MaxMessages, "max-messages", "m", 0, "Maximum number of messages to process per mailbox per run (0 = no limit)")
|
||||||
|
pflag.BoolVarP(&args.DryRun, "dry-run", "n", false, "Show what would be done without making any changes")
|
||||||
|
|
||||||
// Add utility options
|
// Add utility options
|
||||||
pflag.BoolP("help", "h", false, "Show help message")
|
pflag.BoolP("help", "h", false, "Show help message")
|
||||||
|
|
@ -146,7 +148,7 @@ _%s_completions() {
|
||||||
|
|
||||||
if [[ $cur == -* ]]; then
|
if [[ $cur == -* ]]; then
|
||||||
# Complete with available options
|
# Complete with available options
|
||||||
local opts="-c --config -m --max-messages -h --help --generate-bash-completion"
|
local opts="-c --config -m --max-messages -n --dry-run -h --help --generate-bash-completion"
|
||||||
COMPREPLY=($(compgen -W "$opts" -- "$cur"))
|
COMPREPLY=($(compgen -W "$opts" -- "$cur"))
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
|
|
@ -217,5 +219,8 @@ func LoadConfigWithDiscovery(args *CommandLineArgs) (*Config, error) {
|
||||||
} else {
|
} else {
|
||||||
fmt.Printf("Maximum messages per mailbox: unlimited\n")
|
fmt.Printf("Maximum messages per mailbox: unlimited\n")
|
||||||
}
|
}
|
||||||
|
if args.DryRun {
|
||||||
|
fmt.Printf("DRY-RUN MODE: No changes will be made to CouchDB\n")
|
||||||
|
}
|
||||||
return LoadConfig(configPath)
|
return LoadConfig(configPath)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
150
go/main.go
150
go/main.go
|
|
@ -19,10 +19,14 @@ func main() {
|
||||||
log.Fatalf("Failed to load configuration: %v", err)
|
log.Fatalf("Failed to load configuration: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize CouchDB client
|
// Initialize CouchDB client (skip in dry-run mode)
|
||||||
couchClient, err := couch.NewClient(&cfg.CouchDb)
|
var couchClient *couch.Client
|
||||||
if err != nil {
|
if !args.DryRun {
|
||||||
log.Fatalf("Failed to create CouchDB client: %v", err)
|
var err error
|
||||||
|
couchClient, err = couch.NewClient(&cfg.CouchDb)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to create CouchDB client: %v", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("Found %d mail source(s) to process.\n", len(cfg.MailSources))
|
fmt.Printf("Found %d mail source(s) to process.\n", len(cfg.MailSources))
|
||||||
|
|
@ -34,21 +38,25 @@ func main() {
|
||||||
// Generate per-account database name
|
// Generate per-account database name
|
||||||
dbName := couch.GenerateAccountDBName(source.Name, source.User)
|
dbName := couch.GenerateAccountDBName(source.Name, source.User)
|
||||||
|
|
||||||
// Ensure the account-specific database exists
|
// Ensure the account-specific database exists (skip in dry-run mode)
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
if !args.DryRun {
|
||||||
err = couchClient.EnsureDB(ctx, dbName)
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
cancel()
|
err = couchClient.EnsureDB(ctx, dbName)
|
||||||
|
cancel()
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Could not ensure CouchDB database '%s' exists (is it running?): %v", dbName, err)
|
log.Printf("Could not ensure CouchDB database '%s' exists (is it running?): %v", dbName, err)
|
||||||
continue
|
continue
|
||||||
|
} else {
|
||||||
|
fmt.Printf("CouchDB database '%s' is ready for account: %s\n", dbName, source.Name)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
fmt.Printf("CouchDB database '%s' is ready for account: %s\n", dbName, source.Name)
|
fmt.Printf("DRY-RUN: Would ensure CouchDB database '%s' exists for account: %s\n", dbName, source.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf(" - Processing source: %s\n", source.Name)
|
fmt.Printf(" - Processing source: %s\n", source.Name)
|
||||||
if source.Protocol == "imap" {
|
if source.Protocol == "imap" {
|
||||||
err := processImapSource(&source, couchClient, dbName, args.MaxMessages)
|
err := processImapSource(&source, couchClient, dbName, args.MaxMessages, args.DryRun)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf(" ERROR: Failed to process IMAP source %s: %v", source.Name, err)
|
log.Printf(" ERROR: Failed to process IMAP source %s: %v", source.Name, err)
|
||||||
}
|
}
|
||||||
|
|
@ -56,7 +64,7 @@ func main() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func processImapSource(source *config.MailSource, couchClient *couch.Client, dbName string, maxMessages int) error {
|
func processImapSource(source *config.MailSource, couchClient *couch.Client, dbName string, maxMessages int, dryRun bool) error {
|
||||||
fmt.Printf(" Connecting to IMAP server: %s:%d\n", source.Host, source.Port)
|
fmt.Printf(" Connecting to IMAP server: %s:%d\n", source.Host, source.Port)
|
||||||
imapClient, err := mail.NewImapClient(source)
|
imapClient, err := mail.NewImapClient(source)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -92,13 +100,17 @@ func processImapSource(source *config.MailSource, couchClient *couch.Client, dbN
|
||||||
for _, mailbox := range mailboxes {
|
for _, mailbox := range mailboxes {
|
||||||
fmt.Printf(" Processing mailbox: %s (mode: %s)\n", mailbox, source.Mode)
|
fmt.Printf(" Processing mailbox: %s (mode: %s)\n", mailbox, source.Mode)
|
||||||
|
|
||||||
// Get sync metadata to determine incremental sync date
|
// Get sync metadata to determine incremental sync date (skip in dry-run mode)
|
||||||
syncCtx, syncCancel := context.WithTimeout(context.Background(), 10*time.Second)
|
var syncMetadata *couch.SyncMetadata
|
||||||
syncMetadata, err := couchClient.GetSyncMetadata(syncCtx, dbName, mailbox)
|
if !dryRun {
|
||||||
syncCancel()
|
syncCtx, syncCancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
if err != nil {
|
var err error
|
||||||
log.Printf(" ERROR: Failed to get sync metadata for %s: %v", mailbox, err)
|
syncMetadata, err = couchClient.GetSyncMetadata(syncCtx, dbName, mailbox)
|
||||||
continue
|
syncCancel()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf(" ERROR: Failed to get sync metadata for %s: %v", mailbox, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine the since date for incremental sync
|
// Determine the since date for incremental sync
|
||||||
|
|
@ -114,7 +126,11 @@ func processImapSource(source *config.MailSource, couchClient *couch.Client, dbN
|
||||||
if sinceDate != nil {
|
if sinceDate != nil {
|
||||||
fmt.Printf(" First sync since: %s (from config)\n", sinceDate.Format("2006-01-02"))
|
fmt.Printf(" First sync since: %s (from config)\n", sinceDate.Format("2006-01-02"))
|
||||||
} else {
|
} else {
|
||||||
fmt.Printf(" First full sync (no date filter)\n")
|
if dryRun {
|
||||||
|
fmt.Printf(" DRY-RUN: Would perform first full sync (no date filter)\n")
|
||||||
|
} else {
|
||||||
|
fmt.Printf(" First full sync (no date filter)\n")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -125,13 +141,18 @@ func processImapSource(source *config.MailSource, couchClient *couch.Client, dbN
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Perform sync/archive logic
|
// Perform sync/archive logic (skip in dry-run mode)
|
||||||
mailboxSyncCtx, mailboxSyncCancel := context.WithTimeout(context.Background(), 30*time.Second)
|
if !dryRun {
|
||||||
err = couchClient.SyncMailbox(mailboxSyncCtx, dbName, mailbox, currentUIDs, source.IsSyncMode())
|
mailboxSyncCtx, mailboxSyncCancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||||
mailboxSyncCancel()
|
err = couchClient.SyncMailbox(mailboxSyncCtx, dbName, mailbox, currentUIDs, source.IsSyncMode())
|
||||||
if err != nil {
|
mailboxSyncCancel()
|
||||||
log.Printf(" ERROR: Failed to sync mailbox %s: %v", mailbox, err)
|
if err != nil {
|
||||||
continue
|
log.Printf(" ERROR: Failed to sync mailbox %s: %v", mailbox, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fmt.Printf(" DRY-RUN: Would sync mailbox %s with %d current UIDs (mode: %s)\n",
|
||||||
|
mailbox, len(currentUIDs), source.Mode)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(messages) == 0 {
|
if len(messages) == 0 {
|
||||||
|
|
@ -149,23 +170,32 @@ func processImapSource(source *config.MailSource, couchClient *couch.Client, dbN
|
||||||
docs = append(docs, doc)
|
docs = append(docs, doc)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store messages in CouchDB with attachments
|
// Store messages in CouchDB with attachments (skip in dry-run mode)
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
||||||
stored := 0
|
stored := 0
|
||||||
for i, doc := range docs {
|
if !dryRun {
|
||||||
err := couchClient.StoreMessage(ctx, dbName, doc, messages[i])
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||||
if err != nil {
|
for i, doc := range docs {
|
||||||
log.Printf(" ERROR: Failed to store message %s: %v", doc.ID, err)
|
err := couchClient.StoreMessage(ctx, dbName, doc, messages[i])
|
||||||
} else {
|
if err != nil {
|
||||||
stored++
|
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)
|
||||||
|
} 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)
|
||||||
|
// Show sample of what would be stored
|
||||||
|
if len(docs) > 0 {
|
||||||
|
fmt.Printf(" DRY-RUN: Sample message ID: %s (Subject: %s)\n",
|
||||||
|
docs[0].ID, docs[0].Subject)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
cancel()
|
|
||||||
|
|
||||||
fmt.Printf(" Stored %d/%d messages from %s\n", stored, len(messages), mailbox)
|
|
||||||
totalStored += stored
|
totalStored += stored
|
||||||
|
|
||||||
// Update sync metadata after successful processing
|
// Update sync metadata after successful processing (skip in dry-run mode)
|
||||||
if len(messages) > 0 {
|
if len(messages) > 0 {
|
||||||
// Find the highest UID processed
|
// Find the highest UID processed
|
||||||
var maxUID uint32
|
var maxUID uint32
|
||||||
|
|
@ -175,26 +205,34 @@ func processImapSource(source *config.MailSource, couchClient *couch.Client, dbN
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create/update sync metadata
|
if !dryRun {
|
||||||
newMetadata := &couch.SyncMetadata{
|
// Create/update sync metadata
|
||||||
Mailbox: mailbox,
|
newMetadata := &couch.SyncMetadata{
|
||||||
LastSyncTime: time.Now(),
|
Mailbox: mailbox,
|
||||||
LastMessageUID: maxUID,
|
LastSyncTime: time.Now(),
|
||||||
MessageCount: stored,
|
LastMessageUID: maxUID,
|
||||||
}
|
MessageCount: stored,
|
||||||
|
}
|
||||||
|
|
||||||
// Store sync metadata
|
// Store sync metadata
|
||||||
metadataCtx, metadataCancel := context.WithTimeout(context.Background(), 10*time.Second)
|
metadataCtx, metadataCancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
err = couchClient.StoreSyncMetadata(metadataCtx, dbName, newMetadata)
|
err = couchClient.StoreSyncMetadata(metadataCtx, dbName, newMetadata)
|
||||||
metadataCancel()
|
metadataCancel()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf(" WARNING: Failed to store sync metadata for %s: %v", mailbox, err)
|
log.Printf(" WARNING: Failed to store sync metadata for %s: %v", mailbox, err)
|
||||||
|
} else {
|
||||||
|
fmt.Printf(" Updated sync metadata (last UID: %d)\n", maxUID)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
fmt.Printf(" Updated sync metadata (last UID: %d)\n", maxUID)
|
fmt.Printf(" DRY-RUN: Would update sync metadata (last UID: %d, %d messages)\n", maxUID, stored)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf(" Summary: Processed %d messages, stored %d new messages\n", totalMessages, totalStored)
|
if dryRun {
|
||||||
|
fmt.Printf(" DRY-RUN Summary: Found %d messages, would store %d messages\n", totalMessages, totalStored)
|
||||||
|
} else {
|
||||||
|
fmt.Printf(" Summary: Processed %d messages, stored %d new messages\n", totalMessages, totalStored)
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,11 @@ pub fn parse_command_line() -> CommandLineArgs {
|
||||||
.help("Maximum number of messages to process per mailbox per run (0 = no limit)")
|
.help("Maximum number of messages to process per mailbox per run (0 = no limit)")
|
||||||
.value_parser(clap::value_parser!(u32))
|
.value_parser(clap::value_parser!(u32))
|
||||||
.action(ArgAction::Set))
|
.action(ArgAction::Set))
|
||||||
|
.arg(Arg::new("dry-run")
|
||||||
|
.short('n')
|
||||||
|
.long("dry-run")
|
||||||
|
.help("Show what would be done without making any changes")
|
||||||
|
.action(ArgAction::SetTrue))
|
||||||
.arg(Arg::new("generate-bash-completion")
|
.arg(Arg::new("generate-bash-completion")
|
||||||
.long("generate-bash-completion")
|
.long("generate-bash-completion")
|
||||||
.help("Generate bash completion script and exit")
|
.help("Generate bash completion script and exit")
|
||||||
|
|
@ -44,6 +49,7 @@ pub fn parse_command_line() -> CommandLineArgs {
|
||||||
CommandLineArgs {
|
CommandLineArgs {
|
||||||
config_path: matches.get_one::<String>("config").map(|s| s.clone()),
|
config_path: matches.get_one::<String>("config").map(|s| s.clone()),
|
||||||
max_messages: matches.get_one::<u32>("max-messages").copied(),
|
max_messages: matches.get_one::<u32>("max-messages").copied(),
|
||||||
|
dry_run: matches.get_flag("dry-run"),
|
||||||
generate_bash_completion: matches.get_flag("generate-bash-completion"),
|
generate_bash_completion: matches.get_flag("generate-bash-completion"),
|
||||||
help: false, // Using clap's built-in help
|
help: false, // Using clap's built-in help
|
||||||
}
|
}
|
||||||
|
|
@ -83,7 +89,7 @@ _{}_completions() {{
|
||||||
|
|
||||||
if [[ $cur == -* ]]; then
|
if [[ $cur == -* ]]; then
|
||||||
# Complete with available options
|
# Complete with available options
|
||||||
local opts="-c --config -m --max-messages -h --help --generate-bash-completion"
|
local opts="-c --config -m --max-messages -n --dry-run -h --help --generate-bash-completion"
|
||||||
COMPREPLY=($(compgen -W "$opts" -- "$cur"))
|
COMPREPLY=($(compgen -W "$opts" -- "$cur"))
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
|
|
|
||||||
|
|
@ -107,6 +107,7 @@ impl MailSource {
|
||||||
pub struct CommandLineArgs {
|
pub struct CommandLineArgs {
|
||||||
pub config_path: Option<String>,
|
pub config_path: Option<String>,
|
||||||
pub max_messages: Option<u32>,
|
pub max_messages: Option<u32>,
|
||||||
|
pub dry_run: bool,
|
||||||
pub generate_bash_completion: bool,
|
pub generate_bash_completion: bool,
|
||||||
pub help: bool,
|
pub help: bool,
|
||||||
}
|
}
|
||||||
|
|
@ -283,6 +284,7 @@ mod tests {
|
||||||
let args = CommandLineArgs {
|
let args = CommandLineArgs {
|
||||||
config_path: None,
|
config_path: None,
|
||||||
max_messages: None,
|
max_messages: None,
|
||||||
|
dry_run: false,
|
||||||
generate_bash_completion: false,
|
generate_bash_completion: false,
|
||||||
help: false,
|
help: false,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,10 @@ async fn run(args: mail2couch::config::CommandLineArgs) -> Result<()> {
|
||||||
info!("Maximum messages per mailbox: unlimited");
|
info!("Maximum messages per mailbox: unlimited");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if args.dry_run {
|
||||||
|
info!("🔍 DRY-RUN MODE: No changes will be made to CouchDB");
|
||||||
|
}
|
||||||
|
|
||||||
// Display configuration summary
|
// Display configuration summary
|
||||||
print_config_summary(&config);
|
print_config_summary(&config);
|
||||||
|
|
||||||
|
|
|
||||||
213
rust/src/sync.rs
213
rust/src/sync.rs
|
|
@ -119,8 +119,12 @@ impl SyncCoordinator {
|
||||||
let db_name = generate_database_name(&source.name, &source.user);
|
let db_name = generate_database_name(&source.name, &source.user);
|
||||||
info!("Using database: {}", db_name);
|
info!("Using database: {}", db_name);
|
||||||
|
|
||||||
// Create database if it doesn't exist
|
// Create database if it doesn't exist (skip in dry-run mode)
|
||||||
self.couch_client.create_database(&db_name).await?;
|
if !self.args.dry_run {
|
||||||
|
self.couch_client.create_database(&db_name).await?;
|
||||||
|
} else {
|
||||||
|
info!("🔍 DRY-RUN: Would create database {}", db_name);
|
||||||
|
}
|
||||||
|
|
||||||
// Connect to IMAP server
|
// Connect to IMAP server
|
||||||
let mut imap_client = ImapClient::connect(source.clone()).await?;
|
let mut imap_client = ImapClient::connect(source.clone()).await?;
|
||||||
|
|
@ -212,22 +216,33 @@ impl SyncCoordinator {
|
||||||
let mailbox_info = imap_client.select_mailbox(mailbox).await?;
|
let mailbox_info = imap_client.select_mailbox(mailbox).await?;
|
||||||
debug!("Selected mailbox {}: {} messages", mailbox, mailbox_info.exists);
|
debug!("Selected mailbox {}: {} messages", mailbox, mailbox_info.exists);
|
||||||
|
|
||||||
// Get last sync metadata
|
// Get last sync metadata (skip in dry-run mode)
|
||||||
let since_date = match self.couch_client.get_sync_metadata(db_name, mailbox).await {
|
let since_date = if !self.args.dry_run {
|
||||||
Ok(metadata) => {
|
match self.couch_client.get_sync_metadata(db_name, mailbox).await {
|
||||||
info!(" Found sync metadata, last sync: {}", metadata.last_sync_time);
|
Ok(metadata) => {
|
||||||
Some(metadata.last_sync_time)
|
info!(" Found sync metadata, last sync: {}", metadata.last_sync_time);
|
||||||
}
|
Some(metadata.last_sync_time)
|
||||||
Err(_) => {
|
}
|
||||||
info!(" No sync metadata found, performing full sync");
|
Err(_) => {
|
||||||
// Parse since date from message filter if provided
|
info!(" No sync metadata found, performing full sync");
|
||||||
source.message_filter.since.as_ref()
|
// Parse since date from message filter if provided
|
||||||
.and_then(|since_str| {
|
source.message_filter.since.as_ref()
|
||||||
DateTime::parse_from_str(&format!("{} 00:00:00 +0000", since_str), "%Y-%m-%d %H:%M:%S %z")
|
.and_then(|since_str| {
|
||||||
.map(|dt| dt.with_timezone(&Utc))
|
DateTime::parse_from_str(&format!("{} 00:00:00 +0000", since_str), "%Y-%m-%d %H:%M:%S %z")
|
||||||
.ok()
|
.map(|dt| dt.with_timezone(&Utc))
|
||||||
})
|
.ok()
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
info!(" 🔍 DRY-RUN: Would check for sync metadata");
|
||||||
|
// In dry-run mode, use config since date if available
|
||||||
|
source.message_filter.since.as_ref()
|
||||||
|
.and_then(|since_str| {
|
||||||
|
DateTime::parse_from_str(&format!("{} 00:00:00 +0000", since_str), "%Y-%m-%d %H:%M:%S %z")
|
||||||
|
.map(|dt| dt.with_timezone(&Utc))
|
||||||
|
.ok()
|
||||||
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
// Search for messages using server-side IMAP SEARCH with keyword filtering when possible
|
// Search for messages using server-side IMAP SEARCH with keyword filtering when possible
|
||||||
|
|
@ -257,17 +272,21 @@ impl SyncCoordinator {
|
||||||
};
|
};
|
||||||
info!(" Found {} messages matching search criteria", message_uids.len());
|
info!(" Found {} messages matching search criteria", message_uids.len());
|
||||||
|
|
||||||
// Handle sync mode - check for deleted messages
|
// Handle sync mode - check for deleted messages (skip in dry-run mode)
|
||||||
let mut messages_deleted = 0;
|
let mut messages_deleted = 0;
|
||||||
if source.mode == "sync" {
|
if source.mode == "sync" {
|
||||||
messages_deleted = self.handle_deleted_messages(db_name, mailbox, &message_uids).await
|
if !self.args.dry_run {
|
||||||
.unwrap_or_else(|e| {
|
messages_deleted = self.handle_deleted_messages(db_name, mailbox, &message_uids).await
|
||||||
warn!(" Failed to handle deleted messages: {}", e);
|
.unwrap_or_else(|e| {
|
||||||
0
|
warn!(" Failed to handle deleted messages: {}", e);
|
||||||
});
|
0
|
||||||
|
});
|
||||||
if messages_deleted > 0 {
|
|
||||||
info!(" 🗑️ Deleted {} messages that no longer exist on server", messages_deleted);
|
if messages_deleted > 0 {
|
||||||
|
info!(" 🗑️ Deleted {} messages that no longer exist on server", messages_deleted);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
info!(" 🔍 DRY-RUN: Would check for deleted messages in sync mode");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -312,54 +331,74 @@ impl SyncCoordinator {
|
||||||
// Extract UID before moving the document
|
// Extract UID before moving the document
|
||||||
let uid_str = mail_doc.source_uid.clone();
|
let uid_str = mail_doc.source_uid.clone();
|
||||||
|
|
||||||
// Store the message document first
|
// Store the message document first (skip in dry-run mode)
|
||||||
match self.couch_client.store_mail_document(db_name, mail_doc).await {
|
if !self.args.dry_run {
|
||||||
Ok(doc_id) => {
|
match self.couch_client.store_mail_document(db_name, mail_doc).await {
|
||||||
messages_stored += 1;
|
Ok(doc_id) => {
|
||||||
|
messages_stored += 1;
|
||||||
// Store attachments if any exist
|
|
||||||
if !attachments.is_empty() {
|
// Store attachments if any exist
|
||||||
for (filename, content_type, data) in attachments {
|
if !attachments.is_empty() {
|
||||||
match self.couch_client.store_attachment(
|
for (filename, content_type, data) in attachments {
|
||||||
db_name,
|
match self.couch_client.store_attachment(
|
||||||
&doc_id,
|
db_name,
|
||||||
&filename,
|
&doc_id,
|
||||||
&content_type,
|
&filename,
|
||||||
&data,
|
&content_type,
|
||||||
).await {
|
&data,
|
||||||
Ok(_) => {
|
).await {
|
||||||
debug!(" Stored attachment: {}", filename);
|
Ok(_) => {
|
||||||
}
|
debug!(" Stored attachment: {}", filename);
|
||||||
Err(e) => {
|
}
|
||||||
warn!(" Failed to store attachment {}: {}", filename, e);
|
Err(e) => {
|
||||||
|
warn!(" Failed to store attachment {}: {}", filename, e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Parse UID from source_uid
|
||||||
|
if let Ok(uid) = uid_str.parse::<u32>() {
|
||||||
|
last_uid = Some(last_uid.map_or(uid, |prev: u32| prev.max(uid)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Err(e) => {
|
||||||
// Parse UID from source_uid
|
warn!(" Failed to store message {}: {}", uid_str, e);
|
||||||
if let Ok(uid) = uid_str.parse::<u32>() {
|
messages_skipped += 1;
|
||||||
last_uid = Some(last_uid.map_or(uid, |prev: u32| prev.max(uid)));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(e) => {
|
} else {
|
||||||
warn!(" Failed to store message {}: {}", uid_str, e);
|
// In dry-run mode, simulate successful storage
|
||||||
messages_skipped += 1;
|
messages_stored += 1;
|
||||||
|
debug!(" 🔍 DRY-RUN: Would store message {} (Subject: {})",
|
||||||
|
uid_str, mail_doc.subject);
|
||||||
|
|
||||||
|
if !attachments.is_empty() {
|
||||||
|
debug!(" 🔍 DRY-RUN: Would store {} attachments", attachments.len());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse UID from source_uid
|
||||||
|
if let Ok(uid) = uid_str.parse::<u32>() {
|
||||||
|
last_uid = Some(last_uid.map_or(uid, |prev: u32| prev.max(uid)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update sync metadata
|
// Update sync metadata (skip in dry-run mode)
|
||||||
if let Some(uid) = last_uid {
|
if let Some(uid) = last_uid {
|
||||||
let sync_metadata = SyncMetadata::new(
|
if !self.args.dry_run {
|
||||||
mailbox.to_string(),
|
let sync_metadata = SyncMetadata::new(
|
||||||
start_time,
|
mailbox.to_string(),
|
||||||
uid,
|
start_time,
|
||||||
messages_stored,
|
uid,
|
||||||
);
|
messages_stored,
|
||||||
|
);
|
||||||
|
|
||||||
if let Err(e) = self.couch_client.store_sync_metadata(db_name, &sync_metadata).await {
|
if let Err(e) = self.couch_client.store_sync_metadata(db_name, &sync_metadata).await {
|
||||||
warn!(" Failed to store sync metadata: {}", e);
|
warn!(" Failed to store sync metadata: {}", e);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
info!(" 🔍 DRY-RUN: Would update sync metadata (last UID: {}, {} messages)", uid, messages_stored);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -422,7 +461,11 @@ impl SyncCoordinator {
|
||||||
|
|
||||||
/// Print summary of sync results
|
/// Print summary of sync results
|
||||||
pub fn print_sync_summary(&self, results: &[SourceSyncResult]) {
|
pub fn print_sync_summary(&self, results: &[SourceSyncResult]) {
|
||||||
info!("\n🎉 Synchronization completed!");
|
if self.args.dry_run {
|
||||||
|
info!("\n🔍 DRY-RUN completed!");
|
||||||
|
} else {
|
||||||
|
info!("\n🎉 Synchronization completed!");
|
||||||
|
}
|
||||||
info!("{}", "=".repeat(50));
|
info!("{}", "=".repeat(50));
|
||||||
|
|
||||||
let mut total_sources = 0;
|
let mut total_sources = 0;
|
||||||
|
|
@ -434,24 +477,45 @@ impl SyncCoordinator {
|
||||||
total_mailboxes += result.mailboxes_processed;
|
total_mailboxes += result.mailboxes_processed;
|
||||||
total_messages += result.total_messages;
|
total_messages += result.total_messages;
|
||||||
|
|
||||||
info!(
|
if self.args.dry_run {
|
||||||
"📧 {}: {} mailboxes, {} messages (database: {})",
|
info!(
|
||||||
result.source_name,
|
"📧 {}: {} mailboxes, {} messages found (database: {})",
|
||||||
result.mailboxes_processed,
|
result.source_name,
|
||||||
result.total_messages,
|
result.mailboxes_processed,
|
||||||
result.database
|
result.total_messages,
|
||||||
);
|
result.database
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
info!(
|
||||||
|
"📧 {}: {} mailboxes, {} messages (database: {})",
|
||||||
|
result.source_name,
|
||||||
|
result.mailboxes_processed,
|
||||||
|
result.total_messages,
|
||||||
|
result.database
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
info!("{}", "=".repeat(50));
|
info!("{}", "=".repeat(50));
|
||||||
info!(
|
if self.args.dry_run {
|
||||||
"📊 Total: {} sources, {} mailboxes, {} messages",
|
info!(
|
||||||
total_sources, total_mailboxes, total_messages
|
"📊 DRY-RUN Total: {} sources, {} mailboxes, {} messages found",
|
||||||
);
|
total_sources, total_mailboxes, total_messages
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
info!(
|
||||||
|
"📊 Total: {} sources, {} mailboxes, {} messages",
|
||||||
|
total_sources, total_mailboxes, total_messages
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(max) = self.args.max_messages {
|
if let Some(max) = self.args.max_messages {
|
||||||
info!("⚠️ Message limit was applied: {} per mailbox", max);
|
info!("⚠️ Message limit was applied: {} per mailbox", max);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if self.args.dry_run {
|
||||||
|
info!("🔍 No changes were made to CouchDB");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -493,6 +557,7 @@ mod tests {
|
||||||
let args = CommandLineArgs {
|
let args = CommandLineArgs {
|
||||||
config_path: None,
|
config_path: None,
|
||||||
max_messages: Some(10),
|
max_messages: Some(10),
|
||||||
|
dry_run: false,
|
||||||
generate_bash_completion: false,
|
generate_bash_completion: false,
|
||||||
help: false,
|
help: false,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue