imap-backup 4.0.5 → 4.1.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 (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