imap-backup 5.0.0 → 6.0.0.rc2

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 (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