imap-backup 7.0.0.rc1 → 7.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +17 -21
  3. data/bin/imap-backup +0 -2
  4. data/docs/development.md +1 -1
  5. data/lib/email/mboxrd/message.rb +4 -3
  6. data/lib/imap/backup/account/connection.rb +16 -2
  7. data/lib/imap/backup/account/folder.rb +27 -12
  8. data/lib/imap/backup/account.rb +10 -1
  9. data/lib/imap/backup/cli/backup.rb +5 -1
  10. data/lib/imap/backup/cli/helpers.rb +22 -0
  11. data/lib/imap/backup/cli/local.rb +7 -7
  12. data/lib/imap/backup/cli/mirror.rb +118 -0
  13. data/lib/imap/backup/cli/utils.rb +6 -0
  14. data/lib/imap/backup/cli.rb +59 -0
  15. data/lib/imap/backup/configuration.rb +1 -17
  16. data/lib/imap/backup/flag_refresher.rb +32 -0
  17. data/lib/imap/backup/local_only_message_deleter.rb +23 -0
  18. data/lib/imap/backup/logger.rb +10 -5
  19. data/lib/imap/backup/migrator.rb +4 -4
  20. data/lib/imap/backup/mirror/map.rb +85 -0
  21. data/lib/imap/backup/mirror.rb +101 -0
  22. data/lib/imap/backup/serializer/appender.rb +4 -3
  23. data/lib/imap/backup/serializer/imap.rb +27 -32
  24. data/lib/imap/backup/serializer/message.rb +54 -0
  25. data/lib/imap/backup/serializer/message_enumerator.rb +2 -10
  26. data/lib/imap/backup/serializer/version2_migrator.rb +38 -15
  27. data/lib/imap/backup/serializer.rb +41 -27
  28. data/lib/imap/backup/setup/account/header.rb +9 -1
  29. data/lib/imap/backup/setup/account.rb +18 -1
  30. data/lib/imap/backup/setup.rb +0 -10
  31. data/lib/imap/backup/thunderbird/mailbox_exporter.rb +2 -4
  32. data/lib/imap/backup/uploader.rb +7 -7
  33. data/lib/imap/backup/version.rb +2 -2
  34. metadata +14 -9
  35. data/lib/imap/backup/serializer/mbox_enumerator.rb +0 -39
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d858a14c5bbb9300fd99fe6338cc2ff83fd596071dc2db48ca792f45eee430a3
4
- data.tar.gz: c28f18cd459cd9c2c9a62fb5a55234615218bcff2c3c46e0422af9c6893e6043
3
+ metadata.gz: cb709e6431c47afc24c55d39ef7d9213fc0c27b42929c0692843a254cfd83c1d
4
+ data.tar.gz: 702d93af210afa826e64c0b5b1a4d1e16780c6078201e4a8db13dd4703ac62d3
5
5
  SHA512:
6
- metadata.gz: c669d9a170ec1d500e898324bbdc2072cc42597e9c2d30b6b284b7095151afecd0451c9fa7cb0adb93bbf46f1f2b1ae188554507141161acb6ba125349c1cee9
7
- data.tar.gz: c53ecb54ed6d33ab574caa18e62ae57e214e76975af65c72b11b8b2612d45487422be234c025966c792260d86d20e5431bf27845772e40d82cc8fea0132f9963
6
+ metadata.gz: 04a0c173fea0dd80e87443db040383402eb9eacbc7f557b4db8339cfd16c20153b068da8f4ade84d82643e3e9d3b9a76f04d654cbbc931620ef197546b24492a
7
+ data.tar.gz: e3cb1c12f73c421a66e41016b826ecdf3d199fd5920f9e5cbb04e0ef41619984791c8ce436283a91e6264707b04db7ecef94703f5df9af4c64cbcbb75c0fd7d7
data/README.md CHANGED
@@ -11,21 +11,15 @@ The backups can then be restored, used to migrate to another service,
11
11
  inspected or exported.
12
12
 
13
13
  * [Source Code]
14
- * [Documentation]
14
+ * [Code Documentation]
15
15
  * [Rubygem]
16
16
  * [CI Status]
17
17
 
18
18
  [Source Code]: https://github.com/joeyates/imap-backup "Source code at GitHub"
19
- [Documentation]: https://rubydoc.info/gems/imap-backup/frames "RDoc API Documentation at Rubydoc.info"
19
+ [Code Documentation]: https://rubydoc.info/gems/imap-backup/frames "Code Documentation at Rubydoc.info"
20
20
  [Rubygem]: https://rubygems.org/gems/imap-backup "Ruby gem at rubygems.org"
21
21
  [CI Status]: https://github.com/joeyates/imap-backup/actions/workflows/main.yml
22
22
 
23
- # Backup Emails
24
-
25
- imap-backup downloads emails and stores them on disk.
26
-
27
- The backup is incremental and interruptable, so backups won't get messed if your connection goes down during an operation.
28
-
29
23
  # Installation
30
24
 
31
25
  ## Homebrew (macOS)
@@ -63,7 +57,7 @@ imap-backup setup
63
57
 
64
58
  To use imap-backup with GMail, you will need to enable 'App passwords' on your account.
65
59
 
66
- # Run Backup
60
+ # Backup
67
61
 
68
62
  Manually, from the command line:
69
63
 
@@ -73,14 +67,22 @@ imap-backup
73
67
 
74
68
  Alternatively, add it to your crontab.
75
69
 
76
- Emails are stored on disk in [Mbox files](./docs/files/mboxrd.md) for each folder, with metadata
77
- stored in [Imap files](./docs/files/imap.md).
78
-
79
70
  # Commands
80
71
 
81
- * [folders](./commands/folders.md)
82
- * [restore](./commands/restore.md)
83
- * [status](./commands/status.md)
72
+ * [backup](docs/commands/backup.md)
73
+ * [folders](docs/commands/folders.md)
74
+ * [local accounts](docs/commands/local-accounts.md)
75
+ * [local folders](docs/commands/local-folders.md)
76
+ * [local list](docs/commands/local-list.md)
77
+ * [local show](docs/commands/local-show.md)
78
+ * [migrate](docs/commands/migrate.md)
79
+ * [mirror](docs/commands/mirror.md)
80
+ * [remote folders](docs/commands/remote-folders.md)
81
+ * [restore](docs/commands/restore.md)
82
+ * [setup](docs/commands/setup.md)
83
+ * [status](docs/commands/status.md)
84
+ * [utils export-to-thunderbird](docs/commands/utils-export-to-thunderbird.md)
85
+ * [utils ignore-history](docs/commands/utils-ignore-history.md)
84
86
 
85
87
  For a full list of available commands, run
86
88
 
@@ -94,12 +96,6 @@ For more information about a command, run
94
96
  imap-backup help COMMAND
95
97
  ```
96
98
 
97
- ## Configuration
98
-
99
- `imap-backup setup` creates the file `~/.imap-backup/config.json`.
100
-
101
- [More information about configuration is available in the specific documentation](./docs/configuration.md).
102
-
103
99
  # Troubleshooting
104
100
 
105
101
  If you have problems:
data/bin/imap-backup CHANGED
@@ -8,8 +8,6 @@ CliCoverage.conditionally_activate
8
8
  require "imap/backup/cli"
9
9
  require "imap/backup/logger"
10
10
 
11
- Imap::Backup::Logger.setup_logging
12
-
13
11
  Imap::Backup::Logger.sanitize_stderr do
14
12
  Imap::Backup::CLI.start(ARGV)
15
13
  end
data/docs/development.md CHANGED
@@ -44,7 +44,7 @@ use `last_command_started.output`.
44
44
  require "net/imap"
45
45
 
46
46
  imap = Net::IMAP.new("localhost", {port: 8993, ssl: {verify_mode: 0}})
47
- username = "address@example.org"
47
+ username = "address@example.com"
48
48
  imap.login(username, "pass")
49
49
 
50
50
  message = "From: #{username}\nSubject: Some Subject\n\nHello!\n"
@@ -5,9 +5,6 @@ module Email; end
5
5
 
6
6
  module Email::Mboxrd
7
7
  class Message
8
- extend Forwardable
9
- def_delegators :@parsed, :subject
10
-
11
8
  attr_reader :supplied_body
12
9
 
13
10
  def self.clean_serialized(serialized)
@@ -40,6 +37,10 @@ module Email::Mboxrd
40
37
  nil
41
38
  end
42
39
 
40
+ def subject
41
+ parsed.subject
42
+ end
43
+
43
44
  def imap_body
44
45
  supplied_body.gsub(/(?<!\r)\n/, "\r\n")
45
46
  end
@@ -3,6 +3,8 @@ require "imap/backup/client/default"
3
3
  require "imap/backup/account/connection/backup_folders"
4
4
  require "imap/backup/account/connection/client_factory"
5
5
  require "imap/backup/account/connection/folder_names"
6
+ require "imap/backup/flag_refresher"
7
+ require "imap/backup/local_only_message_deleter"
6
8
  require "imap/backup/serializer/directory"
7
9
 
8
10
  module Imap::Backup
@@ -33,11 +35,18 @@ module Imap::Backup
33
35
  end
34
36
  end
35
37
 
36
- def run_backup
37
- Logger.logger.debug "Running backup of account: #{account.username}"
38
+ def run_backup(refresh: false)
39
+ Logger.logger.info "Running backup of account: #{account.username}"
38
40
  # start the connection so we get logging messages in the right order
39
41
  client
40
42
  ensure_account_folder
43
+ if account.mirror_mode
44
+ # Delete serialized folders that are not to be backed up
45
+ wanted = backup_folders.map(&:name)
46
+ local_folders do |serializer, _folder|
47
+ serializer.delete if !wanted.include?(serializer.folder)
48
+ end
49
+ end
41
50
  each_folder do |folder, serializer|
42
51
  begin
43
52
  next if !folder.exist?
@@ -57,6 +66,11 @@ module Imap::Backup
57
66
  multi_fetch_size: account.multi_fetch_size,
58
67
  reset_seen_flags_after_fetch: account.reset_seen_flags_after_fetch
59
68
  ).run
69
+ if account.mirror_mode
70
+ Logger.logger.info "Mirror mode - Deleting messages only present locally"
71
+ LocalOnlyMessageDeleter.new(folder, serializer).run
72
+ end
73
+ FlagRefresher.new(folder, serializer).run if account.mirror_mode || refresh
60
74
  rescue Net::IMAP::ByeResponseError
61
75
  reconnect
62
76
  retry
@@ -58,22 +58,22 @@ module Imap::Backup
58
58
  rescue FolderNotFound
59
59
  []
60
60
  rescue NoMethodError
61
- message = <<~MESSAGE
62
- Folder '#{name}' caused NoMethodError
63
- probably
64
- `undefined method `[]' for nil:NilClass (NoMethodError)`
65
- in `search_internal` in stdlib net/imap.rb.
66
- This is caused by `@responses["SEARCH"] being unset/undefined
67
- MESSAGE
61
+ message =
62
+ "Folder '#{name}' caused a NoMethodError. " \
63
+ "Probably this was `undefined method `[]' for nil:NilClass (NoMethodError)` " \
64
+ "in `search_internal` in stdlib net/imap.rb. " \
65
+ 'This is caused by `@responses["SEARCH"] being unset/undefined. ' \
66
+ "Among others, Apple Mail servers send empty responses when " \
67
+ "folders are empty, causing this error."
68
68
  Logger.logger.warn message
69
69
  []
70
70
  end
71
71
 
72
- def fetch_multi(uids)
72
+ def fetch_multi(uids, attr = [BODY_ATTRIBUTE, "FLAGS"])
73
73
  examine
74
74
  fetch_data_items =
75
75
  retry_on_error(errors: UID_FETCH_RETRY_CLASSES) do
76
- client.uid_fetch(uids, [BODY_ATTRIBUTE, "FLAGS"])
76
+ client.uid_fetch(uids, attr)
77
77
  end
78
78
  return nil if fetch_data_items.nil?
79
79
 
@@ -90,18 +90,30 @@ module Imap::Backup
90
90
  nil
91
91
  end
92
92
 
93
- def append(message, flags: nil)
93
+ def append(message)
94
94
  body = message.imap_body
95
95
  date = message.date&.to_time
96
96
  retry_on_error(errors: APPEND_RETRY_CLASSES, limit: 3) do
97
- response = client.append(utf7_encoded_name, body, flags, date)
97
+ response = client.append(utf7_encoded_name, body, message.flags, date)
98
98
  extract_uid(response)
99
99
  end
100
100
  end
101
101
 
102
+ def delete_multi(uids)
103
+ set_flags(uids, [:Deleted])
104
+ client.expunge
105
+ end
106
+
107
+ def apply_flags(uids, flags)
108
+ client.select(utf7_encoded_name)
109
+ flags.reject! { |f| f == :Recent }
110
+ client.uid_store(uids, "FLAGS", flags)
111
+ end
112
+
102
113
  def set_flags(uids, flags)
103
114
  # Use read-write access, via `select`
104
115
  client.select(utf7_encoded_name)
116
+ flags.reject! { |f| f == :Recent }
105
117
  client.uid_store(uids, "+FLAGS", flags)
106
118
  end
107
119
 
@@ -111,7 +123,10 @@ module Imap::Backup
111
123
  end
112
124
 
113
125
  def clear
114
- set_flags(uids, [:Deleted])
126
+ existing = uids
127
+ return if existing.empty?
128
+
129
+ set_flags(existing, [:Deleted])
115
130
  client.expunge
116
131
  end
117
132
 
@@ -1,3 +1,5 @@
1
+ module Imap; end
2
+
1
3
  module Imap::Backup
2
4
  class Account
3
5
  DEFAULT_MULTI_FETCH_SIZE = 1
@@ -6,6 +8,7 @@ module Imap::Backup
6
8
  attr_reader :password
7
9
  attr_reader :local_path
8
10
  attr_reader :folders
11
+ attr_reader :mirror_mode
9
12
  attr_reader :server
10
13
  attr_reader :connection_options
11
14
  attr_reader :reset_seen_flags_after_fetch
@@ -16,6 +19,7 @@ module Imap::Backup
16
19
  @password = options[:password]
17
20
  @local_path = options[:local_path]
18
21
  @folders = options[:folders]
22
+ @mirror_mode = options[:mirror_mode]
19
23
  @server = options[:server]
20
24
  @connection_options = options[:connection_options]
21
25
  @multi_fetch_size = options[:multi_fetch_size]
@@ -53,6 +57,7 @@ module Imap::Backup
53
57
  h = {username: @username, password: @password}
54
58
  h[:local_path] = @local_path if @local_path
55
59
  h[:folders] = @folders if @folders
60
+ h[:mirror_mode] = true if @mirror_mode
56
61
  h[:server] = @server if @server
57
62
  h[:connection_options] = @connection_options if @connection_options
58
63
  h[:multi_fetch_size] = multi_fetch_size if @multi_fetch_size
@@ -80,12 +85,16 @@ module Imap::Backup
80
85
  update(:folders, value)
81
86
  end
82
87
 
88
+ def mirror_mode=(value)
89
+ update(:mirror_mode, value)
90
+ end
91
+
83
92
  def server=(value)
84
93
  update(:server, value)
85
94
  end
86
95
 
87
96
  def connection_options=(value)
88
- parsed = JSON.parse(value)
97
+ parsed = JSON.parse(value, symbolize_names: true)
89
98
  update(:connection_options, parsed)
90
99
  end
91
100
 
@@ -4,15 +4,19 @@ module Imap::Backup
4
4
  include CLI::Helpers
5
5
 
6
6
  attr_reader :account_names
7
+ attr_reader :refresh
7
8
 
8
9
  def initialize(options)
9
10
  super([])
10
11
  @account_names = (options[:accounts] || "").split(",")
12
+ @refresh = options.key?(:refresh) ? !!options[:refresh] : false
11
13
  end
12
14
 
13
15
  no_commands do
14
16
  def run
15
- each_connection(account_names, &:run_backup)
17
+ each_connection(account_names) do |connection|
18
+ connection.run_backup(refresh: refresh)
19
+ end
16
20
  end
17
21
  end
18
22
  end
@@ -3,6 +3,28 @@ require "imap/backup/cli/accounts"
3
3
 
4
4
  module Imap::Backup
5
5
  module CLI::Helpers
6
+ def self.included(base)
7
+ base.class_eval do
8
+ def self.verbose_option
9
+ method_option(
10
+ "verbose",
11
+ type: :boolean,
12
+ desc: "increase the amount of logging",
13
+ aliases: ["-v"]
14
+ )
15
+ end
16
+
17
+ def self.quiet_option
18
+ method_option(
19
+ "quiet",
20
+ type: :boolean,
21
+ desc: "silence all output",
22
+ aliases: ["-q"]
23
+ )
24
+ end
25
+ end
26
+ end
27
+
6
28
  def symbolized(options)
7
29
  options.each.with_object({}) do |(k, v), acc|
8
30
  key = k.gsub("-", "_").intern
@@ -39,8 +39,8 @@ module Imap::Backup
39
39
 
40
40
  uids = folder_serializer.uids
41
41
 
42
- folder_serializer.each_message(uids).map do |uid, message|
43
- list_message uid, message
42
+ folder_serializer.each_message(uids).map do |message|
43
+ list_message message
44
44
  end
45
45
  end
46
46
 
@@ -59,22 +59,22 @@ module Imap::Backup
59
59
  raise "Folder '#{folder_name}' not found" if !folder_serializer
60
60
 
61
61
  uid_list = uids.split(",")
62
- folder_serializer.each_message(uid_list).each do |uid, message|
62
+ folder_serializer.each_message(uid_list).each do |message|
63
63
  if uid_list.count > 1
64
64
  Kernel.puts <<~HEADER
65
65
  #{'-' * 80}
66
- #{format('| UID: %-71s |', uid)}
66
+ #{format('| UID: %-71s |', message.uid)}
67
67
  #{'-' * 80}
68
68
  HEADER
69
69
  end
70
- Kernel.puts message.supplied_body
70
+ Kernel.puts message.body
71
71
  end
72
72
  end
73
73
 
74
74
  no_commands do
75
- def list_message(uid, message)
75
+ def list_message(message)
76
76
  m = {
77
- uid: uid,
77
+ uid: message.uid,
78
78
  date: message.date.to_s,
79
79
  subject: message.subject || ""
80
80
  }
@@ -0,0 +1,118 @@
1
+ require "imap/backup/mirror"
2
+
3
+ module Imap::Backup
4
+ class CLI::Mirror < Thor
5
+ include Thor::Actions
6
+
7
+ attr_reader :destination_email
8
+ attr_reader :destination_prefix
9
+ attr_reader :source_email
10
+ attr_reader :source_prefix
11
+
12
+ def initialize(
13
+ source_email,
14
+ destination_email,
15
+ destination_prefix: "",
16
+ source_prefix: ""
17
+ )
18
+ super([])
19
+ @destination_email = destination_email
20
+ @destination_prefix = destination_prefix
21
+ @source_email = source_email
22
+ @source_prefix = source_prefix
23
+ end
24
+
25
+ no_commands do
26
+ def run
27
+ check_accounts!
28
+ warn_if_source_account_is_not_in_mirror_mode
29
+
30
+ CLI::Backup.new(accounts: source_email).run
31
+
32
+ folders.each do |serializer, folder|
33
+ Mirror.new(serializer, folder).run
34
+ end
35
+ end
36
+
37
+ def check_accounts!
38
+ if destination_email == source_email
39
+ raise "Source and destination accounts cannot be the same!"
40
+ end
41
+
42
+ raise "Account '#{destination_email}' does not exist" if !destination_account
43
+
44
+ raise "Account '#{source_email}' does not exist" if !source_account
45
+ end
46
+
47
+ def warn_if_source_account_is_not_in_mirror_mode
48
+ return if source_account.mirror_mode
49
+
50
+ message =
51
+ "The account '#{source_account.username}' " \
52
+ "is not set up to make mirror backups"
53
+ Logger.logger.info message
54
+ end
55
+
56
+ def config
57
+ Configuration.new
58
+ end
59
+
60
+ def destination_account
61
+ config.accounts.find { |a| a.username == destination_email }
62
+ end
63
+
64
+ def folders
65
+ return enum_for(:folders) if !block_given?
66
+
67
+ glob = File.join(source_local_path, "**", "*.imap")
68
+ Pathname.glob(glob) do |path|
69
+ name = source_folder_name(path)
70
+ serializer = Serializer.new(source_local_path, name)
71
+ folder = folder_for(name)
72
+ yield serializer, folder
73
+ end
74
+ end
75
+
76
+ def folder_for(source_folder)
77
+ no_source_prefix =
78
+ if source_prefix != "" && source_folder.start_with?(source_prefix)
79
+ source_folder.delete_prefix(source_prefix)
80
+ else
81
+ source_folder.to_s
82
+ end
83
+
84
+ with_destination_prefix =
85
+ if destination_prefix && destination_prefix != ""
86
+ destination_prefix + no_source_prefix
87
+ else
88
+ no_source_prefix
89
+ end
90
+
91
+ Account::Folder.new(
92
+ destination_account.connection,
93
+ with_destination_prefix
94
+ )
95
+ end
96
+
97
+ def source_local_path
98
+ source_account.local_path
99
+ end
100
+
101
+ def source_account
102
+ config.accounts.find { |a| a.username == source_email }
103
+ end
104
+
105
+ def source_folder_name(imap_pathname)
106
+ base = Pathname.new(source_local_path)
107
+ imap_name = imap_pathname.relative_path_from(base).to_s
108
+ dir = File.dirname(imap_name)
109
+ stripped = File.basename(imap_name, ".imap")
110
+ if dir == "."
111
+ stripped
112
+ else
113
+ File.join(dir, stripped)
114
+ end
115
+ end
116
+ end
117
+ end
118
+ end
@@ -8,7 +8,10 @@ module Imap::Backup
8
8
  FAKE_EMAIL = "fake@email.com".freeze
9
9
 
10
10
  desc "ignore-history EMAIL", "Skip downloading emails up to today for all configured folders"
11
+ verbose_option
12
+ quiet_option
11
13
  def ignore_history(email)
14
+ Imap::Backup::Logger.setup_logging symbolized(options)
12
15
  connection = connection(email)
13
16
 
14
17
  connection.backup_folders.each do |folder|
@@ -26,6 +29,8 @@ module Imap::Backup
26
29
  A folder called 'imap-backup/EMAIL' is created under 'Local Folders'.
27
30
  DOC
28
31
  )
32
+ verbose_option
33
+ quiet_option
29
34
  method_option(
30
35
  "force",
31
36
  type: :boolean,
@@ -40,6 +45,7 @@ module Imap::Backup
40
45
  )
41
46
  def export_to_thunderbird(email)
42
47
  opts = symbolized(options)
48
+ Imap::Backup::Logger.setup_logging opts
43
49
  force = opts.key?(:force) ? opts[:force] : false
44
50
  profile_name = opts[:profile]
45
51
 
@@ -1,4 +1,5 @@
1
1
  require "thor"
2
+ require "imap/backup/logger"
2
3
 
3
4
  module Imap; end
4
5
 
@@ -10,6 +11,7 @@ module Imap::Backup
10
11
  autoload :Folders, "imap/backup/cli/folders"
11
12
  autoload :Local, "imap/backup/cli/local"
12
13
  autoload :Migrate, "imap/backup/cli/migrate"
14
+ autoload :Mirror, "imap/backup/cli/mirror"
13
15
  autoload :Remote, "imap/backup/cli/remote"
14
16
  autoload :Restore, "imap/backup/cli/restore"
15
17
  autoload :Setup, "imap/backup/cli/setup"
@@ -43,7 +45,16 @@ module Imap::Backup
43
45
  The setup tool can be used to choose a specific list of folders to back up.
44
46
  DESC
45
47
  accounts_option
48
+ verbose_option
49
+ quiet_option
50
+ method_option(
51
+ "refresh",
52
+ type: :boolean,
53
+ desc: "in 'keep all emails' mode, update flags for messages that are already downloaded",
54
+ aliases: ["-r"]
55
+ )
46
56
  def backup
57
+ Imap::Backup::Logger.setup_logging symbolized(options)
47
58
  Backup.new(symbolized(options)).run
48
59
  end
49
60
 
@@ -85,6 +96,8 @@ module Imap::Backup
85
96
  use the `--reset` option. In this case, all existing emails are
86
97
  deleted before uploading the migrated emails.
87
98
  DESC
99
+ verbose_option
100
+ quiet_option
88
101
  method_option(
89
102
  "destination-prefix",
90
103
  type: :string,
@@ -104,9 +117,49 @@ module Imap::Backup
104
117
  aliases: ["-s"]
105
118
  )
106
119
  def migrate(source_email, destination_email)
120
+ Imap::Backup::Logger.setup_logging symbolized(options)
107
121
  Migrate.new(source_email, destination_email, **symbolized(options)).run
108
122
  end
109
123
 
124
+ desc(
125
+ "mirror SOURCE_EMAIL DESTINATION_EMAIL [OPTIONS]",
126
+ "Keeps the DESTINATION_EMAIL account aligned with the SOURCE_EMAIL account"
127
+ )
128
+ long_desc <<~DESC
129
+ This command updates the DESTINATION_EMAIL account's folders to have the same contents
130
+ as those on the SOURCE_EMAIL account.
131
+
132
+ If a folder list is configured for the SOURCE_EMAIL account,
133
+ only the folders indicated by the setting are copied.
134
+
135
+ First, runs the download of the SOURCE_EMAIL account.
136
+ If the SOURCE_EMAIL account is **not** configured to be in 'mirror' mode,
137
+ a warning is printed.
138
+
139
+ When the mirror command is used, for each folder that is processed,
140
+ a new file is created alongside the normal backup files (.imap and .mbox)
141
+ This file has a '.mirror' extension. This file contains a mapping of
142
+ the known UIDs on the source account to those on the destination account.
143
+ DESC
144
+ verbose_option
145
+ quiet_option
146
+ method_option(
147
+ "destination-prefix",
148
+ type: :string,
149
+ desc: "the prefix (namespace) to add to destination folder names",
150
+ aliases: ["-d"]
151
+ )
152
+ method_option(
153
+ "source-prefix",
154
+ type: :string,
155
+ desc: "the prefix (namespace) to strip from source folder names",
156
+ aliases: ["-s"]
157
+ )
158
+ def mirror(source_email, destination_email)
159
+ Imap::Backup::Logger.setup_logging symbolized(options)
160
+ Mirror.new(source_email, destination_email, **symbolized(options)).run
161
+ end
162
+
110
163
  desc "remote SUBCOMMAND [OPTIONS]", "View info about online accounts"
111
164
  subcommand "remote", Remote
112
165
 
@@ -116,7 +169,10 @@ module Imap::Backup
116
169
  their original server.
117
170
  DESC
118
171
  accounts_option
172
+ verbose_option
173
+ quiet_option
119
174
  def restore(email = nil)
175
+ Imap::Backup::Logger.setup_logging symbolized(options)
120
176
  Restore.new(email, symbolized(options)).run
121
177
  end
122
178
 
@@ -125,7 +181,10 @@ module Imap::Backup
125
181
  A menu-driven command-line application used to configure imap-backup.
126
182
  Configure email accounts to back up.
127
183
  DESC
184
+ verbose_option
185
+ quiet_option
128
186
  def setup
187
+ Imap::Backup::Logger.setup_logging symbolized(options)
129
188
  Setup.new.run
130
189
  end
131
190
 
@@ -20,8 +20,6 @@ module Imap::Backup
20
20
 
21
21
  def initialize(pathname = self.class.default_pathname)
22
22
  @pathname = pathname
23
- @saved_debug = nil
24
- @debug = nil
25
23
  end
26
24
 
27
25
  def path
@@ -36,8 +34,7 @@ module Imap::Backup
36
34
  remove_deleted_accounts
37
35
  save_data = {
38
36
  version: VERSION,
39
- accounts: accounts.map(&:to_h),
40
- debug: debug?
37
+ accounts: accounts.map(&:to_h)
41
38
  }
42
39
  File.open(pathname, "w") { |f| f.write(JSON.pretty_generate(save_data)) }
43
40
  FileUtils.chmod(0o600, pathname) if !windows?
@@ -53,29 +50,16 @@ module Imap::Backup
53
50
 
54
51
  def modified?
55
52
  ensure_loaded!
56
- return true if @saved_debug != @debug
57
53
 
58
54
  accounts.any? { |a| a.modified? || a.marked_for_deletion? }
59
55
  end
60
56
 
61
- def debug?
62
- ensure_loaded!
63
- @debug
64
- end
65
-
66
- def debug=(value)
67
- ensure_loaded!
68
- @debug = [true, false].include?(value) ? value : false
69
- end
70
-
71
57
  private
72
58
 
73
59
  def ensure_loaded!
74
60
  return true if @data
75
61
 
76
62
  data
77
- @debug = data.key?(:debug) ? data[:debug] == true : false
78
- @saved_debug = @debug
79
63
  true
80
64
  end
81
65