imap-backup 4.0.5 → 4.1.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (53) hide show
  1. checksums.yaml +4 -4
  2. data/bin/imap-backup +5 -2
  3. data/lib/imap/backup/account/connection.rb +70 -58
  4. data/lib/imap/backup/account/folder.rb +23 -3
  5. data/lib/imap/backup/account.rb +6 -7
  6. data/lib/imap/backup/cli/accounts.rb +43 -0
  7. data/lib/imap/backup/cli/folders.rb +3 -1
  8. data/lib/imap/backup/cli/helpers.rb +8 -9
  9. data/lib/imap/backup/cli/local.rb +4 -2
  10. data/lib/imap/backup/cli/setup.rb +1 -1
  11. data/lib/imap/backup/cli/status.rb +1 -1
  12. data/lib/imap/backup/cli/utils.rb +3 -2
  13. data/lib/imap/backup/{configuration/store.rb → configuration.rb} +49 -14
  14. data/lib/imap/backup/downloader.rb +26 -12
  15. data/lib/imap/backup/logger.rb +42 -0
  16. data/lib/imap/backup/sanitizer.rb +42 -0
  17. data/lib/imap/backup/serializer/mbox_store.rb +2 -2
  18. data/lib/imap/backup/{configuration → setup}/account.rb +59 -41
  19. data/lib/imap/backup/{configuration → setup}/asker.rb +5 -5
  20. data/lib/imap/backup/setup/connection_tester.rb +26 -0
  21. data/lib/imap/backup/{configuration → setup}/folder_chooser.rb +25 -17
  22. data/lib/imap/backup/setup/helpers.rb +15 -0
  23. data/lib/imap/backup/{configuration/setup.rb → setup.rb} +33 -25
  24. data/lib/imap/backup/uploader.rb +2 -2
  25. data/lib/imap/backup/version.rb +2 -2
  26. data/lib/imap/backup.rb +7 -33
  27. data/lib/retry_on_error.rb +1 -1
  28. data/spec/features/backup_spec.rb +1 -0
  29. data/spec/features/status_spec.rb +43 -0
  30. data/spec/features/support/email_server.rb +5 -2
  31. data/spec/features/support/shared/connection_context.rb +7 -5
  32. data/spec/support/higline_test_helpers.rb +1 -1
  33. data/spec/support/silence_logging.rb +1 -1
  34. data/spec/unit/email/provider/base_spec.rb +1 -1
  35. data/spec/unit/email/provider_spec.rb +2 -2
  36. data/spec/unit/imap/backup/account/connection_spec.rb +22 -26
  37. data/spec/unit/imap/backup/cli/accounts_spec.rb +47 -0
  38. data/spec/unit/imap/backup/cli/local_spec.rb +15 -4
  39. data/spec/unit/imap/backup/cli/utils_spec.rb +54 -42
  40. data/spec/unit/imap/backup/{configuration/store_spec.rb → configuration_spec.rb} +23 -24
  41. data/spec/unit/imap/backup/downloader_spec.rb +1 -1
  42. data/spec/unit/imap/backup/logger_spec.rb +48 -0
  43. data/spec/unit/imap/backup/{configuration → setup}/account_spec.rb +78 -70
  44. data/spec/unit/imap/backup/{configuration → setup}/asker_spec.rb +2 -2
  45. data/spec/unit/imap/backup/{configuration → setup}/connection_tester_spec.rb +10 -10
  46. data/spec/unit/imap/backup/{configuration → setup}/folder_chooser_spec.rb +25 -26
  47. data/spec/unit/imap/backup/{configuration/setup_spec.rb → setup_spec.rb} +81 -52
  48. metadata +51 -48
  49. data/lib/imap/backup/configuration/connection_tester.rb +0 -14
  50. data/lib/imap/backup/configuration/list.rb +0 -53
  51. data/spec/support/shared_examples/account_flagging.rb +0 -23
  52. data/spec/unit/imap/backup/configuration/list_spec.rb +0 -89
  53. data/spec/unit/imap/backup_spec.rb +0 -28
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6eebcf60acbb2e97a007e5da03c63fb7c32e24413397f28d3ec9f94154b8def5
4
- data.tar.gz: 5f81961b55811c8c9fadabf2def11e1e00ab8572fd844f44b8fda371aa190bbb
3
+ metadata.gz: 00a744249ff80a26be2c57655709be59d9678c9bb998dc1c9f3ea4bf3539d747
4
+ data.tar.gz: 4f44c050c66fe2ef2e136b46dec581a91f77ab4b650e9b3466a893540e481a24
5
5
  SHA512:
6
- metadata.gz: 6ebb0d93413c0911487235584837f76f6409d66cc06a64ad9e7db7ec0a4ccca2694b9be363e02922bbf1c981d944a9772e6705190ad92b461252d7b57bf1e4b0
7
- data.tar.gz: 6f29640122bf11c098050476e60caa073ebb868a0859624d6e7b74df435e85a1d6ebbcf964840058c6978a42d16034d86341741caead66d99722554ffc2ca4a3
6
+ metadata.gz: 66067924fda33810aff4b02aa047c20cd26ce3fc31d8e752fc43a8f78af3e1c952871fa0ba372c670a5ee399e7fc686f9df9601fc5cbb7caa6f2065b4ac95b90
7
+ data.tar.gz: 443a86c3bda9cc1652fc9d1c5619347c1d82498befed16851a6f8a227eaf3299813dd6a4084e1bb0d180170adb1b63ccf8e5d2a8bb19f0457611e7916d796c4d
data/bin/imap-backup CHANGED
@@ -2,7 +2,10 @@
2
2
 
3
3
  $LOAD_PATH.unshift(File.expand_path("../lib/", __dir__))
4
4
  require "imap/backup/cli"
5
+ require "imap/backup/logger"
5
6
 
6
- Imap::Backup::Configuration::List.new.setup_logging
7
+ Imap::Backup::Logger.setup_logging
7
8
 
8
- Imap::Backup::CLI.start(ARGV)
9
+ Imap::Backup::Logger.sanitize_stderr do
10
+ Imap::Backup::CLI.start(ARGV)
11
+ end
@@ -4,63 +4,74 @@ require "imap/backup/client/default"
4
4
  require "retry_on_error"
5
5
 
6
6
  module Imap::Backup
7
- module Account; end
7
+ class Account; end
8
8
 
9
9
  class Account::Connection
10
10
  include RetryOnError
11
11
 
12
12
  LOGIN_RETRY_CLASSES = [EOFError, Errno::ECONNRESET, SocketError].freeze
13
13
 
14
- attr_reader :connection_options
15
- attr_reader :local_path
16
- attr_reader :password
17
- attr_reader :username
18
-
19
- def initialize(options)
20
- @username = options[:username]
21
- @password = options[:password]
22
- @local_path = options[:local_path]
23
- @config_folders = options[:folders]
24
- @server = options[:server]
25
- @connection_options = options[:connection_options] || {}
26
- @folders = nil
14
+ attr_reader :account
15
+
16
+ def initialize(account)
17
+ @account = account
18
+ reset
27
19
  create_account_folder
28
20
  end
29
21
 
30
- def folders
31
- @folders ||=
22
+ # TODO: Make this private once the 'folders' command
23
+ # has been removed.
24
+ def folder_names
25
+ @folder_names ||=
32
26
  begin
33
- folders = client.list
27
+ folder_names = client.list
34
28
 
35
- if folders.empty?
36
- message = "Unable to get folder list for account #{username}"
37
- Imap::Backup.logger.info message
29
+ if folder_names.empty?
30
+ message = "Unable to get folder list for account #{account.username}"
31
+ Imap::Backup::Logger.logger.info message
38
32
  raise message
39
33
  end
40
34
 
41
- folders
35
+ folder_names
36
+ end
37
+ end
38
+
39
+ def backup_folders
40
+ @backup_folders ||=
41
+ begin
42
+ names =
43
+ if account.folders&.any?
44
+ account.folders.map { |af| af[:name] }
45
+ else
46
+ folder_names
47
+ end
48
+
49
+ names.map do |name|
50
+ Account::Folder.new(self, name)
51
+ end
42
52
  end
43
53
  end
44
54
 
45
55
  def status
46
- backup_folders.map do |backup_folder|
47
- f = Account::Folder.new(self, backup_folder[:name])
48
- s = Serializer::Mbox.new(local_path, backup_folder[:name])
49
- {name: backup_folder[:name], local: s.uids, remote: f.uids}
56
+ backup_folders.map do |folder|
57
+ s = Serializer::Mbox.new(account.local_path, folder.name)
58
+ {name: folder.name, local: s.uids, remote: folder.uids}
50
59
  end
51
60
  end
52
61
 
53
62
  def run_backup
54
- Imap::Backup.logger.debug "Running backup of account: #{username}"
63
+ Imap::Backup::Logger.logger.debug "Running backup of account: #{account.username}"
55
64
  # start the connection so we get logging messages in the right order
56
65
  client
57
66
  each_folder do |folder, serializer|
58
67
  next if !folder.exist?
59
68
 
60
- Imap::Backup.logger.debug "[#{folder.name}] running backup"
69
+ Imap::Backup::Logger.logger.debug "[#{folder.name}] running backup"
61
70
  serializer.apply_uid_validity(folder.uid_validity)
62
71
  begin
63
- Downloader.new(folder, serializer).run
72
+ Downloader.new(
73
+ folder, serializer, block_size: config.download_block_size
74
+ ).run
64
75
  rescue Net::IMAP::ByeResponseError
65
76
  reconnect
66
77
  retry
@@ -71,11 +82,11 @@ module Imap::Backup
71
82
  def local_folders
72
83
  return enum_for(:local_folders) if !block_given?
73
84
 
74
- glob = File.join(local_path, "**", "*.imap")
75
- base = Pathname.new(local_path)
85
+ glob = File.join(account.local_path, "**", "*.imap")
86
+ base = Pathname.new(account.local_path)
76
87
  Pathname.glob(glob) do |path|
77
88
  name = path.relative_path_from(base).to_s[0..-6]
78
- serializer = Serializer::Mbox.new(local_path, name)
89
+ serializer = Serializer::Mbox.new(account.local_path, name)
79
90
  folder = Account::Folder.new(self, name)
80
91
  yield serializer, folder
81
92
  end
@@ -89,18 +100,27 @@ module Imap::Backup
89
100
 
90
101
  def disconnect
91
102
  client.disconnect if @client
103
+ reset
92
104
  end
93
105
 
94
106
  def reconnect
95
107
  disconnect
108
+ end
109
+
110
+ def reset
111
+ @backup_folders = nil
96
112
  @client = nil
113
+ @config = nil
114
+ @folder_names = nil
115
+ @provider = nil
116
+ @server = nil
97
117
  end
98
118
 
99
119
  def client
100
120
  @client ||=
101
121
  retry_on_error(errors: LOGIN_RETRY_CLASSES) do
102
122
  options = provider_options
103
- Imap::Backup.logger.debug(
123
+ Imap::Backup::Logger.logger.debug(
104
124
  "Creating IMAP instance: #{server}, options: #{options.inspect}"
105
125
  )
106
126
  client =
@@ -109,23 +129,22 @@ module Imap::Backup
109
129
  else
110
130
  Client::Default.new(server, options)
111
131
  end
112
- Imap::Backup.logger.debug "Logging in: #{username}/#{masked_password}"
113
- client.login(username, password)
114
- Imap::Backup.logger.debug "Login complete"
132
+ Imap::Backup::Logger.logger.debug "Logging in: #{account.username}/#{masked_password}"
133
+ client.login(account.username, account.password)
134
+ Imap::Backup::Logger.logger.debug "Login complete"
115
135
  client
116
136
  end
117
137
  end
118
138
 
119
139
  def server
120
- @server ||= provider.host
140
+ @server ||= account.server || provider.host
121
141
  end
122
142
 
123
143
  private
124
144
 
125
145
  def each_folder
126
- backup_folders.each do |backup_folder|
127
- folder = Account::Folder.new(self, backup_folder[:name])
128
- serializer = Serializer::Mbox.new(local_path, backup_folder[:name])
146
+ backup_folders.each do |folder|
147
+ serializer = Serializer::Mbox.new(account.local_path, folder.name)
129
148
  yield folder, serializer
130
149
  end
131
150
  end
@@ -133,16 +152,16 @@ module Imap::Backup
133
152
  def restore_folder(serializer, folder)
134
153
  existing_uids = folder.uids
135
154
  if existing_uids.any?
136
- Imap::Backup.logger.debug(
155
+ Imap::Backup::Logger.logger.debug(
137
156
  "There's already a '#{folder.name}' folder with emails"
138
157
  )
139
158
  new_name = serializer.apply_uid_validity(folder.uid_validity)
140
159
  old_name = serializer.folder
141
160
  if new_name
142
- Imap::Backup.logger.debug(
161
+ Imap::Backup::Logger.logger.debug(
143
162
  "Backup '#{old_name}' renamed and restored to '#{new_name}'"
144
163
  )
145
- new_serializer = Serializer::Mbox.new(local_path, new_name)
164
+ new_serializer = Serializer::Mbox.new(account.local_path, new_name)
146
165
  new_folder = Account::Folder.new(self, new_name)
147
166
  new_folder.create
148
167
  new_serializer.force_uid_validity(new_folder.uid_validity)
@@ -159,33 +178,26 @@ module Imap::Backup
159
178
 
160
179
  def create_account_folder
161
180
  Utils.make_folder(
162
- File.dirname(local_path),
163
- File.basename(local_path),
181
+ File.dirname(account.local_path),
182
+ File.basename(account.local_path),
164
183
  Serializer::DIRECTORY_PERMISSIONS
165
184
  )
166
185
  end
167
186
 
168
187
  def masked_password
169
- password.gsub(/./, "x")
170
- end
171
-
172
- def backup_folders
173
- @backup_folders ||=
174
- begin
175
- if @config_folders&.any?
176
- @config_folders
177
- else
178
- folders.map { |name| {name: name} }
179
- end
180
- end
188
+ account.password.gsub(/./, "x")
181
189
  end
182
190
 
183
191
  def provider
184
- @provider ||= Email::Provider.for_address(username)
192
+ @provider ||= Email::Provider.for_address(account.username)
185
193
  end
186
194
 
187
195
  def provider_options
188
- provider.options.merge(connection_options)
196
+ provider.options.merge(account.connection_options || {})
197
+ end
198
+
199
+ def config
200
+ @config ||= Configuration.new
189
201
  end
190
202
  end
191
203
  end
@@ -3,7 +3,7 @@ require "forwardable"
3
3
  require "retry_on_error"
4
4
 
5
5
  module Imap::Backup
6
- module Account; end
6
+ class Account; end
7
7
 
8
8
  class FolderNotFound < StandardError; end
9
9
 
@@ -64,7 +64,7 @@ module Imap::Backup
64
64
  in `search_internal` in stdlib net/imap.rb.
65
65
  This is caused by `@responses["SEARCH"] being unset/undefined
66
66
  MESSAGE
67
- Imap::Backup.logger.warn message
67
+ Imap::Backup::Logger.logger.warn message
68
68
  []
69
69
  end
70
70
 
@@ -84,6 +84,26 @@ module Imap::Backup
84
84
  nil
85
85
  end
86
86
 
87
+ def fetch_multi(uids)
88
+ examine
89
+ fetch_data_items =
90
+ retry_on_error(errors: UID_FETCH_RETRY_CLASSES) do
91
+ client.uid_fetch(uids, [BODY_ATTRIBUTE])
92
+ end
93
+ return nil if fetch_data_items.nil?
94
+
95
+ fetch_data_items.map do |item|
96
+ attributes = item.attr
97
+
98
+ {
99
+ uid: attributes["UID"],
100
+ body: attributes[BODY_ATTRIBUTE]
101
+ }
102
+ end
103
+ rescue FolderNotFound
104
+ nil
105
+ end
106
+
87
107
  def append(message)
88
108
  body = message.imap_body
89
109
  date = message.date&.to_time
@@ -96,7 +116,7 @@ module Imap::Backup
96
116
  def examine
97
117
  client.examine(utf7_encoded_name)
98
118
  rescue Net::IMAP::NoResponseError
99
- Imap::Backup.logger.warn "Folder '#{name}' does not exist on server"
119
+ Imap::Backup::Logger.logger.warn "Folder '#{name}' does not exist on server"
100
120
  raise FolderNotFound, "Folder '#{name}' does not exist on server"
101
121
  end
102
122
 
@@ -20,6 +20,10 @@ module Imap::Backup
20
20
  @marked_for_deletion = false
21
21
  end
22
22
 
23
+ def connection
24
+ Account::Connection.new(self)
25
+ end
26
+
23
27
  def valid?
24
28
  username && password
25
29
  end
@@ -83,14 +87,9 @@ module Imap::Backup
83
87
  def update(field, value)
84
88
  if changes[field]
85
89
  change = changes[field]
86
- if change[:from] == value
87
- changes.delete(field)
88
- else
89
- set_field!(field, value)
90
- end
91
- else
92
- set_field!(field, value)
90
+ changes.delete(field) if change[:from] == value
93
91
  end
92
+ set_field!(field, value)
94
93
  end
95
94
 
96
95
  def set_field!(field, value)
@@ -0,0 +1,43 @@
1
+ module Imap::Backup
2
+ class CLI; end
3
+
4
+ class CLI::Accounts
5
+ include Enumerable
6
+
7
+ attr_reader :required_accounts
8
+
9
+ def initialize(required_accounts = [])
10
+ @required_accounts = required_accounts
11
+ end
12
+
13
+ def each(&block)
14
+ return enum_for(:each) if !block
15
+
16
+ accounts.each(&block)
17
+ end
18
+
19
+ private
20
+
21
+ def accounts
22
+ @accounts ||=
23
+ if required_accounts.empty?
24
+ config.accounts
25
+ else
26
+ config.accounts.select do |account|
27
+ required_accounts.include?(account.username)
28
+ end
29
+ end
30
+ end
31
+
32
+ def config
33
+ @config ||= begin
34
+ exists = Configuration.exist?
35
+ if !exists
36
+ path = Configuration.default_pathname
37
+ raise ConfigurationNotFound, "Configuration file '#{path}' not found"
38
+ end
39
+ Configuration.new
40
+ end
41
+ end
42
+ end
43
+ end
@@ -14,7 +14,9 @@ module Imap::Backup
14
14
  def run
15
15
  each_connection(account_names) do |connection|
16
16
  puts connection.username
17
- folders = connection.folders
17
+ # TODO: Make folder_names private once this command
18
+ # has been removed.
19
+ folders = connection.folder_names
18
20
  if folders.nil?
19
21
  warn "Unable to list account folders"
20
22
  return false
@@ -1,4 +1,5 @@
1
1
  require "imap/backup"
2
+ require "imap/backup/cli/accounts"
2
3
 
3
4
  module Imap::Backup::CLI::Helpers
4
5
  def symbolized(options)
@@ -6,8 +7,8 @@ module Imap::Backup::CLI::Helpers
6
7
  end
7
8
 
8
9
  def account(email)
9
- connections = Imap::Backup::Configuration::List.new
10
- account = connections.accounts.find { |a| a[:username] == email }
10
+ accounts = Imap::Backup::CLI::Accounts.new
11
+ account = accounts.find { |a| a.username == email }
11
12
  raise "#{email} is not a configured account" if !account
12
13
 
13
14
  account
@@ -20,14 +21,12 @@ module Imap::Backup::CLI::Helpers
20
21
  end
21
22
 
22
23
  def each_connection(names)
23
- begin
24
- connections = Imap::Backup::Configuration::List.new(names)
25
- rescue Imap::Backup::ConfigurationNotFound
26
- raise "imap-backup is not configured. Run `imap-backup setup`"
27
- end
24
+ accounts = Imap::Backup::CLI::Accounts.new(names)
28
25
 
29
- connections.each_connection do |connection|
30
- yield connection
26
+ accounts.each do |account|
27
+ yield account.connection
31
28
  end
29
+ rescue Imap::Backup::ConfigurationNotFound
30
+ raise "imap-backup is not configured. Run `imap-backup setup`"
32
31
  end
33
32
  end
@@ -1,3 +1,5 @@
1
+ require "imap/backup/cli/accounts"
2
+
1
3
  module Imap::Backup
2
4
  class CLI::Local < Thor
3
5
  include Thor::Actions
@@ -5,8 +7,8 @@ module Imap::Backup
5
7
 
6
8
  desc "accounts", "List locally backed-up accounts"
7
9
  def accounts
8
- connections = Imap::Backup::Configuration::List.new
9
- connections.accounts.each { |a| Kernel.puts a[:username] }
10
+ accounts = CLI::Accounts.new
11
+ accounts.each { |a| Kernel.puts a.username }
10
12
  end
11
13
 
12
14
  desc "folders EMAIL", "List account folders"
@@ -7,7 +7,7 @@ class Imap::Backup::CLI::Setup < Thor
7
7
 
8
8
  no_commands do
9
9
  def run
10
- Imap::Backup::Configuration::Setup.new.run
10
+ Imap::Backup::Setup.new.run
11
11
  end
12
12
  end
13
13
  end
@@ -13,7 +13,7 @@ module Imap::Backup
13
13
  no_commands do
14
14
  def run
15
15
  each_connection(account_names) do |connection|
16
- puts connection.username
16
+ puts connection.account.username
17
17
  folders = connection.status
18
18
  folders.each do |f|
19
19
  missing_locally = f[:remote] - f[:local]
@@ -11,9 +11,10 @@ module Imap::Backup
11
11
  def ignore_history(email)
12
12
  connection = connection(email)
13
13
 
14
- connection.local_folders.each do |serializer, folder|
14
+ connection.backup_folders.each do |folder|
15
15
  next if !folder.exist?
16
16
 
17
+ serializer = Serializer::Mbox.new(connection.account.local_path, folder.name)
17
18
  do_ignore_folder_history(folder, serializer)
18
19
  end
19
20
  end
@@ -63,7 +64,7 @@ module Imap::Backup
63
64
  no_commands do
64
65
  def do_ignore_folder_history(folder, serializer)
65
66
  uids = folder.uids - serializer.uids
66
- Imap::Backup.logger.info "Folder '#{folder.name}' - #{uids.length} messages"
67
+ Imap::Backup::Logger.logger.info "Folder '#{folder.name}' - #{uids.length} messages"
67
68
 
68
69
  serializer.apply_uid_validity(folder.uid_validity)
69
70
 
@@ -1,11 +1,13 @@
1
1
  require "json"
2
2
  require "os"
3
3
 
4
- module Imap::Backup
5
- module Configuration; end
4
+ require "imap/backup/account"
6
5
 
7
- class Configuration::Store
6
+ module Imap::Backup
7
+ class Configuration
8
8
  CONFIGURATION_DIRECTORY = File.expand_path("~/.imap-backup")
9
+ DEFAULT_DOWNLOAD_BLOCK_SIZE = 1
10
+ VERSION = "2.0"
9
11
 
10
12
  attr_reader :pathname
11
13
 
@@ -19,6 +21,8 @@ module Imap::Backup
19
21
 
20
22
  def initialize(pathname = self.class.default_pathname)
21
23
  @pathname = pathname
24
+ @saved_debug = nil
25
+ @debug = nil
22
26
  end
23
27
 
24
28
  def path
@@ -26,53 +30,84 @@ module Imap::Backup
26
30
  end
27
31
 
28
32
  def save
33
+ ensure_loaded!
29
34
  FileUtils.mkdir(path) if !File.directory?(path)
30
35
  make_private(path) if !windows?
31
36
  remove_modified_flags
32
37
  remove_deleted_accounts
33
- File.open(pathname, "w") { |f| f.write(JSON.pretty_generate(data)) }
38
+ save_data = {
39
+ version: VERSION,
40
+ accounts: accounts.map(&:to_h),
41
+ debug: debug?
42
+ }
43
+ File.open(pathname, "w") { |f| f.write(JSON.pretty_generate(save_data)) }
34
44
  FileUtils.chmod(0o600, pathname) if !windows?
45
+ @data = nil
35
46
  end
36
47
 
37
48
  def accounts
38
- data[:accounts]
49
+ @accounts ||= begin
50
+ ensure_loaded!
51
+ data[:accounts].map { |data| Account.new(data) }
52
+ end
53
+ end
54
+
55
+ def download_block_size
56
+ size = ENV["DOWNLOAD_BLOCK_SIZE"].to_i
57
+ if size > 0
58
+ size
59
+ else
60
+ DEFAULT_DOWNLOAD_BLOCK_SIZE
61
+ end
39
62
  end
40
63
 
41
64
  def modified?
42
- accounts.any? { |a| a[:modified] || a[:delete] }
65
+ ensure_loaded!
66
+ return true if @saved_debug != @debug
67
+
68
+ accounts.any? { |a| a.modified? || a.marked_for_deletion? }
43
69
  end
44
70
 
45
71
  def debug?
46
- data[:debug]
72
+ ensure_loaded!
73
+ @debug
47
74
  end
48
75
 
49
76
  def debug=(value)
50
- data[:debug] = [true, false].include?(value) ? value : false
77
+ ensure_loaded!
78
+ @debug = [true, false].include?(value) ? value : false
51
79
  end
52
80
 
53
81
  private
54
82
 
83
+ def ensure_loaded!
84
+ return true if @data
85
+
86
+ data
87
+ @debug = data.key?(:debug) ? data[:debug] == true : false
88
+ @saved_debug = @debug
89
+ true
90
+ end
91
+
55
92
  def data
56
93
  @data ||=
57
94
  begin
58
95
  if File.exist?(pathname)
59
96
  Utils.check_permissions(pathname, 0o600) if !windows?
60
97
  contents = File.read(pathname)
61
- data = JSON.parse(contents, symbolize_names: true)
98
+ JSON.parse(contents, symbolize_names: true)
62
99
  else
63
- data = {accounts: []}
100
+ {accounts: []}
64
101
  end
65
- data[:debug] = data.key?(:debug) ? data[:debug] == true : false
66
- data
67
102
  end
68
103
  end
69
104
 
70
105
  def remove_modified_flags
71
- accounts.each { |a| a.delete(:modified) }
106
+ accounts.each { |a| a.clear_changes! }
72
107
  end
73
108
 
74
109
  def remove_deleted_accounts
75
- accounts.reject! { |a| a[:delete] }
110
+ accounts.reject! { |a| a.marked_for_deletion? }
76
111
  end
77
112
 
78
113
  def make_private(path)
@@ -2,27 +2,41 @@ module Imap::Backup
2
2
  class Downloader
3
3
  attr_reader :folder
4
4
  attr_reader :serializer
5
+ attr_reader :block_size
5
6
 
6
- def initialize(folder, serializer)
7
+ def initialize(folder, serializer, block_size: 1)
7
8
  @folder = folder
8
9
  @serializer = serializer
10
+ @block_size = block_size
9
11
  end
10
12
 
11
13
  def run
12
14
  uids = folder.uids - serializer.uids
13
15
  count = uids.count
14
- Imap::Backup.logger.debug "[#{folder.name}] #{count} new messages"
15
- uids.each.with_index do |uid, i|
16
- body = folder.fetch(uid)
17
- log_prefix = "[#{folder.name}] uid: #{uid} (#{i + 1}/#{count}) -"
18
- if body.nil?
19
- Imap::Backup.logger.debug("#{log_prefix} not available - skipped")
20
- next
16
+ Imap::Backup::Logger.logger.debug "[#{folder.name}] #{count} new messages"
17
+ uids.each_slice(block_size).with_index do |block, i|
18
+ offset = i * block_size + 1
19
+ uids_and_bodies = folder.fetch_multi(block)
20
+ if uids_and_bodies.nil?
21
+ if block_size > 1
22
+ Imap::Backup::Logger.logger.debug("[#{folder.name}] Multi fetch failed for UIDs #{block.join(", ")}, switching to single fetches")
23
+ @block_size = 1
24
+ redo
25
+ else
26
+ Imap::Backup::Logger.logger.debug("[#{folder.name}] Fetch failed for UID #{block[0]} - skipping")
27
+ next
28
+ end
29
+ end
30
+
31
+ uids_and_bodies.each.with_index do |uid_and_body, j|
32
+ uid = uid_and_body[:uid]
33
+ body = uid_and_body[:body]
34
+ Imap::Backup::Logger.logger.debug(
35
+ "[#{folder.name}] uid: #{uid} (#{offset +j}/#{count}) - " \
36
+ "#{body.size} bytes"
37
+ )
38
+ serializer.save(uid, body)
21
39
  end
22
- Imap::Backup.logger.debug(
23
- "#{log_prefix} #{body.size} bytes"
24
- )
25
- serializer.save(uid, body)
26
40
  end
27
41
  end
28
42
  end