imap-backup 5.0.0 → 6.0.0.rc2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (70) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +8 -7
  3. data/bin/imap-backup +4 -0
  4. data/docs/development.md +10 -4
  5. data/imap-backup.gemspec +2 -7
  6. data/lib/cli_coverage.rb +18 -0
  7. data/lib/imap/backup/account/connection.rb +7 -11
  8. data/lib/imap/backup/account/folder.rb +0 -16
  9. data/lib/imap/backup/account.rb +31 -11
  10. data/lib/imap/backup/cli/folders.rb +3 -3
  11. data/lib/imap/backup/cli/migrate.rb +3 -3
  12. data/lib/imap/backup/cli/restore.rb +20 -4
  13. data/lib/imap/backup/cli/utils.rb +2 -2
  14. data/lib/imap/backup/cli.rb +6 -7
  15. data/lib/imap/backup/configuration.rb +1 -11
  16. data/lib/imap/backup/downloader.rb +13 -9
  17. data/lib/imap/backup/serializer/directory.rb +37 -0
  18. data/lib/imap/backup/serializer/imap.rb +120 -0
  19. data/lib/imap/backup/serializer/mbox.rb +23 -94
  20. data/lib/imap/backup/serializer/mbox_enumerator.rb +2 -0
  21. data/lib/imap/backup/serializer.rb +180 -3
  22. data/lib/imap/backup/setup/account.rb +52 -29
  23. data/lib/imap/backup/setup/helpers.rb +1 -1
  24. data/lib/imap/backup/thunderbird/mailbox_exporter.rb +1 -1
  25. data/lib/imap/backup/version.rb +2 -2
  26. data/lib/imap/backup.rb +0 -1
  27. data/spec/features/backup_spec.rb +22 -29
  28. data/spec/features/restore_spec.rb +8 -6
  29. data/spec/features/support/aruba.rb +12 -3
  30. data/spec/features/support/backup_directory.rb +0 -4
  31. data/spec/features/support/email_server.rb +0 -1
  32. data/spec/spec_helper.rb +4 -9
  33. data/spec/unit/imap/backup/account/connection_spec.rb +36 -8
  34. data/spec/unit/imap/backup/account/folder_spec.rb +18 -16
  35. data/spec/unit/imap/backup/account_spec.rb +246 -0
  36. data/spec/unit/imap/backup/cli/accounts_spec.rb +12 -1
  37. data/spec/unit/imap/backup/cli/backup_spec.rb +19 -0
  38. data/spec/unit/imap/backup/cli/folders_spec.rb +39 -0
  39. data/spec/unit/imap/backup/cli/local_spec.rb +26 -7
  40. data/spec/unit/imap/backup/cli/migrate_spec.rb +80 -0
  41. data/spec/unit/imap/backup/cli/restore_spec.rb +67 -0
  42. data/spec/unit/imap/backup/cli/setup_spec.rb +17 -0
  43. data/spec/unit/imap/backup/cli/utils_spec.rb +68 -5
  44. data/spec/unit/imap/backup/cli_spec.rb +93 -0
  45. data/spec/unit/imap/backup/client/apple_mail_spec.rb +9 -0
  46. data/spec/unit/imap/backup/configuration_spec.rb +2 -2
  47. data/spec/unit/imap/backup/downloader_spec.rb +60 -8
  48. data/spec/unit/imap/backup/logger_spec.rb +1 -1
  49. data/spec/unit/imap/backup/migrator_spec.rb +1 -1
  50. data/spec/unit/imap/backup/sanitizer_spec.rb +42 -0
  51. data/spec/unit/imap/backup/serializer/directory_spec.rb +37 -0
  52. data/spec/unit/imap/backup/serializer/imap_spec.rb +218 -0
  53. data/spec/unit/imap/backup/serializer/mbox_spec.rb +62 -183
  54. data/spec/unit/imap/backup/serializer_spec.rb +296 -0
  55. data/spec/unit/imap/backup/setup/account_spec.rb +120 -25
  56. data/spec/unit/imap/backup/setup/helpers_spec.rb +15 -0
  57. data/spec/unit/imap/backup/thunderbird/mailbox_exporter_spec.rb +116 -0
  58. data/spec/unit/imap/backup/uploader_spec.rb +1 -1
  59. data/spec/unit/retry_on_error_spec.rb +34 -0
  60. metadata +44 -37
  61. data/lib/imap/backup/serializer/mbox_store.rb +0 -217
  62. data/lib/thunderbird/install.rb +0 -16
  63. data/lib/thunderbird/local_folder.rb +0 -65
  64. data/lib/thunderbird/profile.rb +0 -30
  65. data/lib/thunderbird/profiles.rb +0 -71
  66. data/lib/thunderbird/subdirectory.rb +0 -93
  67. data/lib/thunderbird/subdirectory_placeholder.rb +0 -21
  68. data/lib/thunderbird.rb +0 -14
  69. data/spec/gather_rspec_coverage.rb +0 -1
  70. data/spec/unit/imap/backup/serializer/mbox_store_spec.rb +0 -329
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 16911b1cea1983b844c3bbf7f74c03ff7436990fb5aa4d5b49fa18f03ae2e229
4
- data.tar.gz: 265f63995e70ba3f68b98ec72d3a12b3e95100f7e6b320026b18bcc156888a92
3
+ metadata.gz: 4f9cac7966fac9858fdc380d8e9638c8d479035ee4053444a104b8c3eb5a304c
4
+ data.tar.gz: 5d9785b5e34d90535c8052d216d9b3dbf46ea743672f05129604729662f42f34
5
5
  SHA512:
6
- metadata.gz: 4a541a7830ddab458f2366bdad4924f3e6cbc3ba858651839e6dcd1a530acbbeb01e2c59a44331af73a2d77f606f2d6b39eacd2b5a705d78ec20e93d2c2f2527
7
- data.tar.gz: 7b60170e3d29e9882f3bcbf13b04daa162434944d59311a5a03a4b1c1022a7a9501b292c3eb929d6e07039c47cc433316a338e062ced4e8a6b7ea424e6c0678b
6
+ metadata.gz: a4595e4316dcdc20123a8ed2bec524d37c1de95da49281a30484349c317a996cf3c6946617a489880a0c41db41698b2ee862d9aee4f9c9b8fa34d67707ba924c
7
+ data.tar.gz: ccee268de9cb6225c72fe0cb11a2552fc793f664429d8dfa918e5b4697961c8e5e15e47ed653dac241f377385806443f17683614b8ff76075a8ff2f12e07f98c
data/README.md CHANGED
@@ -1,6 +1,7 @@
1
- [![Build Status](https://circleci.com/gh/joeyates/imap-backup.svg?style=svg)][Continuous Integration]
2
- [![Source Analysis](https://codeclimate.com/github/joeyates/imap-backup/badges/gpa.svg)](https://codeclimate.com/github/joeyates/imap-backup)
3
- [![Test Coverage](https://codeclimate.com/github/joeyates/imap-backup/badges/coverage.svg)](https://codeclimate.com/github/joeyates/imap-backup/coverage)
1
+ ![Version](https://img.shields.io/gem/v/imap-backup?label=Version&logo=rubygems)
2
+ [![Build Status](https://github.com/joeyates/imap-backup/actions/workflows/main.yml/badge.svg)][CI Status]
3
+ ![Coverage](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/joeyates/b54fe758bfb405c04bef72dad293d707/raw/coverage.json)
4
+ ![License](https://img.shields.io/github/license/joeyates/imap-backup?color=brightgreen&label=License)
4
5
 
5
6
  # imap-backup
6
7
 
@@ -9,12 +10,12 @@
9
10
  * [Source Code]
10
11
  * [API documentation]
11
12
  * [Rubygem]
12
- * [Continuous Integration]
13
+ * [CI Status]
13
14
 
14
15
  [Source Code]: https://github.com/joeyates/imap-backup "Source code at GitHub"
15
- [API documentation]: http://rubydoc.info/gems/imap-backup/frames "RDoc API Documentation at Rubydoc.info"
16
- [Rubygem]: http://rubygems.org/gems/imap-backup "Ruby gem at rubygems.org"
17
- [Continuous Integration]: https://circleci.com/gh/joeyates/imap-backup "Build status by CirceCI"
16
+ [API documentation]: https://rubydoc.info/gems/imap-backup/frames "RDoc API Documentation at Rubydoc.info"
17
+ [Rubygem]: https://rubygems.org/gems/imap-backup "Ruby gem at rubygems.org"
18
+ [CI Status]: https://github.com/joeyates/imap-backup/actions/workflows/main.yml
18
19
 
19
20
  # Installation
20
21
 
data/bin/imap-backup CHANGED
@@ -1,6 +1,10 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
3
  $LOAD_PATH.unshift(File.expand_path("../lib/", __dir__))
4
+
5
+ require "cli_coverage"
6
+ CliCoverage.conditionally_activate
7
+
4
8
  require "imap/backup/cli"
5
9
  require "imap/backup/logger"
6
10
 
data/docs/development.md CHANGED
@@ -1,10 +1,10 @@
1
1
  # Testing
2
2
 
3
- ## Integration Tests
3
+ ## Feature Specs
4
4
 
5
- Integration tests (feature specs) are run against a local IMAP server
6
- controlled by Docker Compose, which needs to be started
7
- before running the test suite.
5
+ Specs under `specs/features` are integration specs run against a local IMAP server
6
+ controlled by Docker Compose.
7
+ Before running the test suite, it needs to be started:
8
8
 
9
9
  ```sh
10
10
  $ docker-compose up -d
@@ -26,6 +26,12 @@ or
26
26
  $ rspec --tag ~docker
27
27
  ```
28
28
 
29
+ ### Debugging
30
+
31
+ The feature specs are run 'out of process' via the Aruba gem.
32
+ In order to see debugging output from the process,
33
+ use `last_command_started.output`.
34
+
29
35
  ## Access Docker imap server
30
36
 
31
37
  ```ruby
data/imap-backup.gemspec CHANGED
@@ -27,16 +27,11 @@ Gem::Specification.new do |gem|
27
27
  gem.add_runtime_dependency "os"
28
28
  gem.add_runtime_dependency "rake"
29
29
  gem.add_runtime_dependency "thor", "~> 1.1"
30
+ gem.add_runtime_dependency "thunderbird", ">= 0.0.0"
30
31
 
31
32
  gem.add_development_dependency "aruba", ">= 0.0.0"
32
- gem.add_development_dependency "codeclimate-test-reporter", "~> 0.4.8"
33
- if RUBY_ENGINE == "jruby"
34
- gem.add_development_dependency "pry-debugger-jruby"
35
- else
36
- gem.add_development_dependency "pry-byebug"
37
- end
33
+ gem.add_development_dependency "pry-byebug"
38
34
  gem.add_development_dependency "rspec", ">= 3.0.0"
39
- gem.add_development_dependency "rspec_junit_formatter"
40
35
  gem.add_development_dependency "rubocop-rspec"
41
36
  gem.add_development_dependency "simplecov"
42
37
  end
@@ -0,0 +1,18 @@
1
+ class CliCoverage
2
+ def self.conditionally_activate
3
+ if ENV["COVERAGE"]
4
+ require "simplecov"
5
+
6
+ # Collect coverage separately
7
+ SimpleCov.command_name "#{ENV['COVERAGE']} #{ARGV.join(' ')} coverage"
8
+
9
+ # Silence output
10
+ SimpleCov.formatter = SimpleCov::Formatter::SimpleFormatter
11
+ SimpleCov.print_error_status = false
12
+
13
+ # Ensure SimpleCov doesn't filter out all out code
14
+ project_root = File.expand_path("..", __dir__)
15
+ SimpleCov.root project_root
16
+ end
17
+ end
18
+ end
@@ -1,5 +1,6 @@
1
1
  require "imap/backup/client/apple_mail"
2
2
  require "imap/backup/client/default"
3
+ require "imap/backup/serializer/directory"
3
4
 
4
5
  require "retry_on_error"
5
6
 
@@ -52,7 +53,7 @@ module Imap::Backup
52
53
  def status
53
54
  ensure_account_folder
54
55
  backup_folders.map do |folder|
55
- s = Serializer::Mbox.new(account.local_path, folder.name)
56
+ s = Serializer.new(account.local_path, folder.name)
56
57
  {name: folder.name, local: s.uids, remote: folder.uids}
57
58
  end
58
59
  end
@@ -69,7 +70,7 @@ module Imap::Backup
69
70
  serializer.apply_uid_validity(folder.uid_validity)
70
71
  begin
71
72
  Downloader.new(
72
- folder, serializer, block_size: config.download_block_size
73
+ folder, serializer, multi_fetch_size: account.multi_fetch_size
73
74
  ).run
74
75
  rescue Net::IMAP::ByeResponseError
75
76
  reconnect
@@ -86,7 +87,7 @@ module Imap::Backup
86
87
  base = Pathname.new(account.local_path)
87
88
  Pathname.glob(glob) do |path|
88
89
  name = path.relative_path_from(base).to_s[0..-6]
89
- serializer = Serializer::Mbox.new(account.local_path, name)
90
+ serializer = Serializer.new(account.local_path, name)
90
91
  folder = Account::Folder.new(self, name)
91
92
  yield serializer, folder
92
93
  end
@@ -110,7 +111,6 @@ module Imap::Backup
110
111
  def reset
111
112
  @backup_folders = nil
112
113
  @client = nil
113
- @config = nil
114
114
  @folder_names = nil
115
115
  @provider = nil
116
116
  @server = nil
@@ -144,7 +144,7 @@ module Imap::Backup
144
144
 
145
145
  def each_folder
146
146
  backup_folders.each do |folder|
147
- serializer = Serializer::Mbox.new(account.local_path, folder.name)
147
+ serializer = Serializer.new(account.local_path, folder.name)
148
148
  yield folder, serializer
149
149
  end
150
150
  end
@@ -161,7 +161,7 @@ module Imap::Backup
161
161
  Imap::Backup::Logger.logger.debug(
162
162
  "Backup '#{old_name}' renamed and restored to '#{new_name}'"
163
163
  )
164
- new_serializer = Serializer::Mbox.new(account.local_path, new_name)
164
+ new_serializer = Serializer.new(account.local_path, new_name)
165
165
  new_folder = Account::Folder.new(self, new_name)
166
166
  new_folder.create
167
167
  new_serializer.force_uid_validity(new_folder.uid_validity)
@@ -180,7 +180,7 @@ module Imap::Backup
180
180
  Utils.make_folder(
181
181
  File.dirname(account.local_path),
182
182
  File.basename(account.local_path),
183
- Serializer::DIRECTORY_PERMISSIONS
183
+ Serializer::Directory::DIRECTORY_PERMISSIONS
184
184
  )
185
185
  end
186
186
 
@@ -195,9 +195,5 @@ module Imap::Backup
195
195
  def provider_options
196
196
  provider.options.merge(account.connection_options || {})
197
197
  end
198
-
199
- def config
200
- @config ||= Configuration.new
201
- end
202
198
  end
203
199
  end
@@ -68,22 +68,6 @@ module Imap::Backup
68
68
  []
69
69
  end
70
70
 
71
- def fetch(uid)
72
- examine
73
- fetch_data_items =
74
- retry_on_error(errors: UID_FETCH_RETRY_CLASSES) do
75
- client.uid_fetch([uid.to_i], [BODY_ATTRIBUTE])
76
- end
77
- return nil if fetch_data_items.nil?
78
-
79
- fetch_data_item = fetch_data_items[0]
80
- attributes = fetch_data_item.attr
81
-
82
- attributes[BODY_ATTRIBUTE]
83
- rescue FolderNotFound
84
- nil
85
- end
86
-
87
71
  def fetch_multi(uids)
88
72
  examine
89
73
  fetch_data_items =
@@ -1,5 +1,7 @@
1
1
  module Imap::Backup
2
2
  class Account
3
+ DEFAULT_MULTI_FETCH_SIZE = 1
4
+
3
5
  attr_reader :username
4
6
  attr_reader :password
5
7
  attr_reader :local_path
@@ -7,7 +9,6 @@ module Imap::Backup
7
9
  attr_reader :server
8
10
  attr_reader :connection_options
9
11
  attr_reader :changes
10
- attr_reader :marked_for_deletion
11
12
 
12
13
  def initialize(options)
13
14
  @username = options[:username]
@@ -16,6 +17,7 @@ module Imap::Backup
16
17
  @folders = options[:folders]
17
18
  @server = options[:server]
18
19
  @connection_options = options[:connection_options]
20
+ @multi_fetch_size = options[:multi_fetch_size]
19
21
  @changes = {}
20
22
  @marked_for_deletion = false
21
23
  end
@@ -25,18 +27,18 @@ module Imap::Backup
25
27
  end
26
28
 
27
29
  def valid?
28
- username && password
30
+ username && password ? true : false
29
31
  end
30
32
 
31
33
  def modified?
32
34
  changes.any?
33
35
  end
34
36
 
35
- def clear_changes!
37
+ def clear_changes
36
38
  @changes = {}
37
39
  end
38
40
 
39
- def mark_for_deletion!
41
+ def mark_for_deletion
40
42
  @marked_for_deletion = true
41
43
  end
42
44
 
@@ -53,6 +55,7 @@ module Imap::Backup
53
55
  h[:folders] = @folders if @folders
54
56
  h[:server] = @server if @server
55
57
  h[:connection_options] = @connection_options if @connection_options
58
+ h[:multi_fetch_size] = multi_fetch_size if @multi_fetch_size
56
59
  h
57
60
  end
58
61
 
@@ -82,20 +85,37 @@ module Imap::Backup
82
85
  update(:connection_options, parsed)
83
86
  end
84
87
 
88
+ def multi_fetch_size
89
+ int = @multi_fetch_size.to_i
90
+ if int.positive?
91
+ int
92
+ else
93
+ DEFAULT_MULTI_FETCH_SIZE
94
+ end
95
+ end
96
+
97
+ def multi_fetch_size=(value)
98
+ parsed = value.to_i
99
+ parsed = DEFAULT_MULTI_FETCH_SIZE if !parsed.positive?
100
+ update(:multi_fetch_size, parsed)
101
+ end
102
+
85
103
  private
86
104
 
87
105
  def update(field, value)
106
+ key = :"@#{field}"
88
107
  if changes[field]
89
108
  change = changes[field]
90
- changes.delete(field) if change[:from] == value
109
+ if change[:from] == value
110
+ changes.delete(field)
111
+ else
112
+ change[:to] = value
113
+ end
114
+ else
115
+ current = instance_variable_get(key)
116
+ changes[field] = {from: current, to: value}
91
117
  end
92
- set_field!(field, value)
93
- end
94
118
 
95
- def set_field!(field, value)
96
- key = :"@#{field}"
97
- current = instance_variable_get(key)
98
- changes[field] = {from: current, to: value}
99
119
  instance_variable_set(key, value)
100
120
  end
101
121
  end
@@ -13,15 +13,15 @@ module Imap::Backup
13
13
  no_commands do
14
14
  def run
15
15
  each_connection(account_names) do |connection|
16
- puts connection.account.username
16
+ Kernel.puts connection.account.username
17
17
  # TODO: Make folder_names private once this command
18
18
  # has been removed.
19
19
  folders = connection.folder_names
20
20
  if folders.nil?
21
- warn "Unable to list account folders"
21
+ Kernel.warn "Unable to list account folders"
22
22
  return false
23
23
  end
24
- folders.each { |f| puts "\t#{f}" }
24
+ folders.each { |f| Kernel.puts "\t#{f}" }
25
25
  end
26
26
  end
27
27
  end
@@ -39,11 +39,11 @@ module Imap::Backup
39
39
  end
40
40
 
41
41
  if !destination_account
42
- raise "Account #{destination_email} does not exist"
42
+ raise "Account '#{destination_email}' does not exist"
43
43
  end
44
44
 
45
45
  if !source_account
46
- raise "Account #{source_email} does not exist"
46
+ raise "Account '#{source_email}' does not exist"
47
47
  end
48
48
  end
49
49
 
@@ -61,7 +61,7 @@ module Imap::Backup
61
61
  glob = File.join(source_local_path, "**", "*.imap")
62
62
  Pathname.glob(glob) do |path|
63
63
  name = source_folder_name(path)
64
- serializer = Serializer::Mbox.new(source_local_path, name)
64
+ serializer = Serializer.new(source_local_path, name)
65
65
  folder = folder_for(name)
66
66
  yield serializer, folder
67
67
  end
@@ -3,16 +3,32 @@ module Imap::Backup
3
3
  include Thor::Actions
4
4
  include CLI::Helpers
5
5
 
6
+ attr_reader :email
6
7
  attr_reader :account_names
7
8
 
8
- def initialize(options)
9
- super([])
10
- @account_names = (options[:accounts] || "").split(",")
9
+ def initialize(email = nil, options)
10
+ @email = email
11
+ @account_names =
12
+ if options.key?(:accounts)
13
+ options[:accounts].split(",")
14
+ end
11
15
  end
12
16
 
13
17
  no_commands do
14
18
  def run
15
- each_connection(account_names) { |connection| connection.restore }
19
+ case
20
+ when email && !account_names
21
+ connection = connection(email)
22
+ connection.restore
23
+ when !email && !account_names
24
+ Logger.logger.info "Calling restore without an EMAIL parameter is deprecated"
25
+ each_connection([]) { |connection| connection.restore }
26
+ when email && account_names.any?
27
+ raise "Pass either an email or the --accounts option, not both"
28
+ when account_names.any?
29
+ Logger.logger.info "Calling restore with the --account option is deprected, please pass a single EMAIL argument"
30
+ each_connection(account_names) { |connection| connection.restore }
31
+ end
16
32
  end
17
33
  end
18
34
  end
@@ -14,7 +14,7 @@ module Imap::Backup
14
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
+ serializer = Serializer.new(connection.account.local_path, folder.name)
18
18
  do_ignore_folder_history(folder, serializer)
19
19
  end
20
20
  end
@@ -75,7 +75,7 @@ module Imap::Backup
75
75
  Skipped #{uid}
76
76
  MESSAGE
77
77
 
78
- serializer.save(uid, message)
78
+ serializer.append uid, message
79
79
  end
80
80
  end
81
81
 
@@ -100,21 +100,20 @@ module Imap::Backup
100
100
  aliases: ["-s"]
101
101
  )
102
102
  def migrate(source_email, destination_email)
103
- Migrate.new(source_email, destination_email, symbolized(options)).run
103
+ Migrate.new(source_email, destination_email, **symbolized(options)).run
104
104
  end
105
105
 
106
106
  desc "remote SUBCOMMAND [OPTIONS]", "View info about online accounts"
107
107
  subcommand "remote", Remote
108
108
 
109
- desc "restore [OPTIONS]", "This command is deprecated, use `imap-backup restore ACCOUNT`"
109
+ desc "restore EMAIL", "Restores a single account"
110
110
  long_desc <<~DESC
111
- By default, restores all local emails to their respective servers.
112
- This command is deprecated.
113
- Instead, use `imap-backup restore ACCOUNT` to restore a single account.
111
+ Restores all backed-up emails for the supplied account to
112
+ their original server.
114
113
  DESC
115
114
  accounts_option
116
- def restore
117
- Restore.new(symbolized(options)).run
115
+ def restore(email = nil)
116
+ Restore.new(email, symbolized(options)).run
118
117
  end
119
118
 
120
119
  desc "setup", "Configure imap-backup"
@@ -6,7 +6,6 @@ require "imap/backup/account"
6
6
  module Imap::Backup
7
7
  class Configuration
8
8
  CONFIGURATION_DIRECTORY = File.expand_path("~/.imap-backup")
9
- DEFAULT_DOWNLOAD_BLOCK_SIZE = 1
10
9
  VERSION = "2.0"
11
10
 
12
11
  attr_reader :pathname
@@ -52,15 +51,6 @@ module Imap::Backup
52
51
  end
53
52
  end
54
53
 
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
62
- end
63
-
64
54
  def modified?
65
55
  ensure_loaded!
66
56
  return true if @saved_debug != @debug
@@ -103,7 +93,7 @@ module Imap::Backup
103
93
  end
104
94
 
105
95
  def remove_modified_flags
106
- accounts.each { |a| a.clear_changes! }
96
+ accounts.each { |a| a.clear_changes }
107
97
  end
108
98
 
109
99
  def remove_deleted_accounts
@@ -1,33 +1,34 @@
1
1
  module Imap::Backup
2
+ class MultiFetchFailedError < StandardError; end
3
+
2
4
  class Downloader
3
5
  attr_reader :folder
4
6
  attr_reader :serializer
5
- attr_reader :block_size
7
+ attr_reader :multi_fetch_size
6
8
 
7
- def initialize(folder, serializer, block_size: 1)
9
+ def initialize(folder, serializer, multi_fetch_size: 1)
8
10
  @folder = folder
9
11
  @serializer = serializer
10
- @block_size = block_size
12
+ @multi_fetch_size = multi_fetch_size
11
13
  end
12
14
 
13
15
  def run
14
16
  uids = folder.uids - serializer.uids
15
17
  count = uids.count
16
18
  debug "#{count} new messages"
17
- uids.each_slice(block_size).with_index do |block, i|
19
+ uids.each_slice(multi_fetch_size).with_index do |block, i|
18
20
  uids_and_bodies = folder.fetch_multi(block)
19
21
  if uids_and_bodies.nil?
20
- if block_size > 1
22
+ if multi_fetch_size > 1
21
23
  debug("Multi fetch failed for UIDs #{block.join(", ")}, switching to single fetches")
22
- @block_size = 1
23
- redo
24
+ raise MultiFetchFailedError
24
25
  else
25
26
  debug("Fetch failed for UID #{block[0]} - skipping")
26
27
  next
27
28
  end
28
29
  end
29
30
 
30
- offset = i * block_size + 1
31
+ offset = i * multi_fetch_size + 1
31
32
  uids_and_bodies.each.with_index do |uid_and_body, j|
32
33
  uid = uid_and_body[:uid]
33
34
  body = uid_and_body[:body]
@@ -38,10 +39,13 @@ module Imap::Backup
38
39
  info("Fetch returned empty UID - skipping")
39
40
  else
40
41
  debug("uid: #{uid} (#{offset + j}/#{count}) - #{body.size} bytes")
41
- serializer.save(uid, body)
42
+ serializer.append uid, body
42
43
  end
43
44
  end
44
45
  end
46
+ rescue MultiFetchFailedError
47
+ @multi_fetch_size = 1
48
+ retry
45
49
  end
46
50
 
47
51
  private
@@ -0,0 +1,37 @@
1
+ require "os"
2
+
3
+ module Imap::Backup
4
+ class Serializer; end
5
+
6
+ class Serializer::Directory
7
+ DIRECTORY_PERMISSIONS = 0o700
8
+
9
+ attr_reader :relative
10
+ attr_reader :path
11
+
12
+ def initialize(path, relative)
13
+ @path = path
14
+ @relative = relative
15
+ end
16
+
17
+ def ensure_exists
18
+ if !File.directory?(full_path)
19
+ Utils.make_folder(
20
+ path, relative, DIRECTORY_PERMISSIONS
21
+ )
22
+ end
23
+
24
+ return if OS.windows?
25
+ return if Utils.mode(full_path) == DIRECTORY_PERMISSIONS
26
+
27
+ FileUtils.chmod DIRECTORY_PERMISSIONS, full_path
28
+ end
29
+
30
+ private
31
+
32
+ def full_path
33
+ containing_directory = File.join(path, relative)
34
+ File.expand_path(containing_directory)
35
+ end
36
+ end
37
+ end