Compare commits

..

No commits in common. "506511a870ee1d260a2012f5fc4c0fc24719c9e0" and "14d2aafbf08112207a0436cd38487655a40654e7" have entirely different histories.

6 changed files with 133 additions and 253 deletions

View file

@ -86,7 +86,6 @@ 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
@ -96,7 +95,6 @@ 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")
@ -148,7 +146,7 @@ _%s_completions() {
if [[ $cur == -* ]]; then if [[ $cur == -* ]]; then
# Complete with available options # Complete with available options
local opts="-c --config -m --max-messages -n --dry-run -h --help --generate-bash-completion" local opts="-c --config -m --max-messages -h --help --generate-bash-completion"
COMPREPLY=($(compgen -W "$opts" -- "$cur")) COMPREPLY=($(compgen -W "$opts" -- "$cur"))
return return
fi fi
@ -219,8 +217,5 @@ 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)
} }

View file

@ -19,14 +19,10 @@ func main() {
log.Fatalf("Failed to load configuration: %v", err) log.Fatalf("Failed to load configuration: %v", err)
} }
// Initialize CouchDB client (skip in dry-run mode) // Initialize CouchDB client
var couchClient *couch.Client couchClient, err := couch.NewClient(&cfg.CouchDb)
if !args.DryRun { if err != nil {
var err error log.Fatalf("Failed to create CouchDB client: %v", err)
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))
@ -38,25 +34,21 @@ 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 (skip in dry-run mode) // Ensure the account-specific database exists
if !args.DryRun { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) err = couchClient.EnsureDB(ctx, dbName)
err = couchClient.EnsureDB(ctx, dbName) cancel()
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("DRY-RUN: Would ensure CouchDB database '%s' exists for account: %s\n", dbName, source.Name) fmt.Printf("CouchDB database '%s' is ready 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, args.DryRun) err := processImapSource(&source, couchClient, dbName, args.MaxMessages)
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)
} }
@ -64,7 +56,7 @@ func main() {
} }
} }
func processImapSource(source *config.MailSource, couchClient *couch.Client, dbName string, maxMessages int, dryRun bool) error { func processImapSource(source *config.MailSource, couchClient *couch.Client, dbName string, maxMessages int) 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 {
@ -100,17 +92,13 @@ 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 (skip in dry-run mode) // Get sync metadata to determine incremental sync date
var syncMetadata *couch.SyncMetadata syncCtx, syncCancel := context.WithTimeout(context.Background(), 10*time.Second)
if !dryRun { syncMetadata, err := couchClient.GetSyncMetadata(syncCtx, dbName, mailbox)
syncCtx, syncCancel := context.WithTimeout(context.Background(), 10*time.Second) syncCancel()
var err error if err != nil {
syncMetadata, err = couchClient.GetSyncMetadata(syncCtx, dbName, mailbox) log.Printf(" ERROR: Failed to get sync metadata for %s: %v", mailbox, err)
syncCancel() continue
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
@ -126,11 +114,7 @@ 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 {
if dryRun { fmt.Printf(" First full sync (no date filter)\n")
fmt.Printf(" DRY-RUN: Would perform first full sync (no date filter)\n")
} else {
fmt.Printf(" First full sync (no date filter)\n")
}
} }
} }
@ -141,18 +125,13 @@ func processImapSource(source *config.MailSource, couchClient *couch.Client, dbN
continue continue
} }
// Perform sync/archive logic (skip in dry-run mode) // Perform sync/archive logic
if !dryRun { mailboxSyncCtx, mailboxSyncCancel := context.WithTimeout(context.Background(), 30*time.Second)
mailboxSyncCtx, mailboxSyncCancel := context.WithTimeout(context.Background(), 30*time.Second) err = couchClient.SyncMailbox(mailboxSyncCtx, dbName, mailbox, currentUIDs, source.IsSyncMode())
err = couchClient.SyncMailbox(mailboxSyncCtx, dbName, mailbox, currentUIDs, source.IsSyncMode()) mailboxSyncCancel()
mailboxSyncCancel() if err != nil {
if err != nil { log.Printf(" ERROR: Failed to sync mailbox %s: %v", mailbox, err)
log.Printf(" ERROR: Failed to sync mailbox %s: %v", mailbox, err) continue
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 {
@ -170,32 +149,23 @@ func processImapSource(source *config.MailSource, couchClient *couch.Client, dbN
docs = append(docs, doc) docs = append(docs, doc)
} }
// Store messages in CouchDB with attachments (skip in dry-run mode) // Store messages in CouchDB with attachments
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
stored := 0 stored := 0
if !dryRun { for i, doc := range docs {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) err := couchClient.StoreMessage(ctx, dbName, doc, messages[i])
for i, doc := range docs { if err != nil {
err := couchClient.StoreMessage(ctx, dbName, doc, messages[i]) log.Printf(" ERROR: Failed to store message %s: %v", doc.ID, err)
if err != nil { } else {
log.Printf(" ERROR: Failed to store message %s: %v", doc.ID, err) stored++
} 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 (skip in dry-run mode) // Update sync metadata after successful processing
if len(messages) > 0 { if len(messages) > 0 {
// Find the highest UID processed // Find the highest UID processed
var maxUID uint32 var maxUID uint32
@ -205,34 +175,26 @@ func processImapSource(source *config.MailSource, couchClient *couch.Client, dbN
} }
} }
if !dryRun { // Create/update sync metadata
// Create/update sync metadata newMetadata := &couch.SyncMetadata{
newMetadata := &couch.SyncMetadata{ Mailbox: mailbox,
Mailbox: mailbox, LastSyncTime: time.Now(),
LastSyncTime: time.Now(), LastMessageUID: maxUID,
LastMessageUID: maxUID, MessageCount: stored,
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(" DRY-RUN: Would update sync metadata (last UID: %d, %d messages)\n", maxUID, stored) fmt.Printf(" Updated sync metadata (last UID: %d)\n", maxUID)
} }
} }
} }
if dryRun { fmt.Printf(" Summary: Processed %d messages, stored %d new messages\n", totalMessages, totalStored)
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
} }

View file

@ -28,11 +28,6 @@ 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")
@ -49,7 +44,6 @@ 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
} }
@ -89,7 +83,7 @@ _{}_completions() {{
if [[ $cur == -* ]]; then if [[ $cur == -* ]]; then
# Complete with available options # Complete with available options
local opts="-c --config -m --max-messages -n --dry-run -h --help --generate-bash-completion" local opts="-c --config -m --max-messages -h --help --generate-bash-completion"
COMPREPLY=($(compgen -W "$opts" -- "$cur")) COMPREPLY=($(compgen -W "$opts" -- "$cur"))
return return
fi fi

View file

@ -107,7 +107,6 @@ 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,
} }
@ -284,7 +283,6 @@ 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,
}; };

View file

@ -36,10 +36,6 @@ 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);

View file

@ -119,12 +119,8 @@ 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 (skip in dry-run mode) // Create database if it doesn't exist
if !self.args.dry_run { self.couch_client.create_database(&db_name).await?;
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?;
@ -216,33 +212,22 @@ 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 (skip in dry-run mode) // Get last sync metadata
let since_date = if !self.args.dry_run { let since_date = match self.couch_client.get_sync_metadata(db_name, mailbox).await {
match self.couch_client.get_sync_metadata(db_name, mailbox).await { Ok(metadata) => {
Ok(metadata) => { info!(" Found sync metadata, last sync: {}", metadata.last_sync_time);
info!(" Found sync metadata, last sync: {}", metadata.last_sync_time); Some(metadata.last_sync_time)
Some(metadata.last_sync_time) }
} Err(_) => {
Err(_) => { info!(" No sync metadata found, performing full sync");
info!(" No sync metadata found, performing full sync"); // Parse since date from message filter if provided
// Parse since date from message filter if provided source.message_filter.since.as_ref()
source.message_filter.since.as_ref() .and_then(|since_str| {
.and_then(|since_str| { DateTime::parse_from_str(&format!("{} 00:00:00 +0000", since_str), "%Y-%m-%d %H:%M:%S %z")
DateTime::parse_from_str(&format!("{} 00:00:00 +0000", since_str), "%Y-%m-%d %H:%M:%S %z") .map(|dt| dt.with_timezone(&Utc))
.map(|dt| dt.with_timezone(&Utc)) .ok()
.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
@ -272,21 +257,17 @@ 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 (skip in dry-run mode) // Handle sync mode - check for deleted messages
let mut messages_deleted = 0; let mut messages_deleted = 0;
if source.mode == "sync" { if source.mode == "sync" {
if !self.args.dry_run { messages_deleted = self.handle_deleted_messages(db_name, mailbox, &message_uids).await
messages_deleted = self.handle_deleted_messages(db_name, mailbox, &message_uids).await .unwrap_or_else(|e| {
.unwrap_or_else(|e| { warn!(" Failed to handle deleted messages: {}", e);
warn!(" Failed to handle deleted messages: {}", e); 0
0 });
});
if messages_deleted > 0 {
if messages_deleted > 0 { info!(" 🗑️ Deleted {} messages that no longer exist on server", messages_deleted);
info!(" 🗑️ Deleted {} messages that no longer exist on server", messages_deleted);
}
} else {
info!(" 🔍 DRY-RUN: Would check for deleted messages in sync mode");
} }
} }
@ -331,74 +312,54 @@ 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 (skip in dry-run mode) // Store the message document first
if !self.args.dry_run { match self.couch_client.store_mail_document(db_name, mail_doc).await {
match self.couch_client.store_mail_document(db_name, mail_doc).await { Ok(doc_id) => {
Ok(doc_id) => { messages_stored += 1;
messages_stored += 1;
// Store attachments if any exist
// Store attachments if any exist if !attachments.is_empty() {
if !attachments.is_empty() { for (filename, content_type, data) in attachments {
for (filename, content_type, data) in attachments { match self.couch_client.store_attachment(
match self.couch_client.store_attachment( db_name,
db_name, &doc_id,
&doc_id, &filename,
&filename, &content_type,
&content_type, &data,
&data, ).await {
).await { Ok(_) => {
Ok(_) => { debug!(" Stored attachment: {}", filename);
debug!(" Stored attachment: {}", filename); }
} Err(e) => {
Err(e) => { warn!(" Failed to store attachment {}: {}", filename, 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) => {
warn!(" Failed to store message {}: {}", uid_str, e); // Parse UID from source_uid
messages_skipped += 1; if let Ok(uid) = uid_str.parse::<u32>() {
last_uid = Some(last_uid.map_or(uid, |prev: u32| prev.max(uid)));
} }
} }
} else { Err(e) => {
// In dry-run mode, simulate successful storage warn!(" Failed to store message {}: {}", uid_str, e);
messages_stored += 1; messages_skipped += 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 (skip in dry-run mode) // Update sync metadata
if let Some(uid) = last_uid { if let Some(uid) = last_uid {
if !self.args.dry_run { let sync_metadata = SyncMetadata::new(
let sync_metadata = SyncMetadata::new( mailbox.to_string(),
mailbox.to_string(), start_time,
start_time, uid,
uid, messages_stored,
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);
} }
} }
@ -461,11 +422,7 @@ 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]) {
if self.args.dry_run { info!("\n🎉 Synchronization completed!");
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;
@ -477,45 +434,24 @@ impl SyncCoordinator {
total_mailboxes += result.mailboxes_processed; total_mailboxes += result.mailboxes_processed;
total_messages += result.total_messages; total_messages += result.total_messages;
if self.args.dry_run { info!(
info!( "📧 {}: {} mailboxes, {} messages (database: {})",
"📧 {}: {} mailboxes, {} messages found (database: {})", result.source_name,
result.source_name, result.mailboxes_processed,
result.mailboxes_processed, result.total_messages,
result.total_messages, result.database
result.database );
);
} else {
info!(
"📧 {}: {} mailboxes, {} messages (database: {})",
result.source_name,
result.mailboxes_processed,
result.total_messages,
result.database
);
}
} }
info!("{}", "=".repeat(50)); info!("{}", "=".repeat(50));
if self.args.dry_run { info!(
info!( "📊 Total: {} sources, {} mailboxes, {} messages",
"📊 DRY-RUN Total: {} sources, {} mailboxes, {} messages found", total_sources, total_mailboxes, total_messages
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");
}
} }
} }
@ -557,7 +493,6 @@ 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,
}; };