imap-backup 5.2.0 → 6.0.1

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 (102) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +9 -2
  3. data/docs/development.md +10 -4
  4. data/imap-backup.gemspec +5 -1
  5. data/lib/cli_coverage.rb +11 -11
  6. data/lib/email/provider/base.rb +2 -0
  7. data/lib/email/provider/unknown.rb +2 -0
  8. data/lib/email/provider.rb +2 -0
  9. data/lib/imap/backup/account/connection/backup_folders.rb +27 -0
  10. data/lib/imap/backup/account/connection/client_factory.rb +54 -0
  11. data/lib/imap/backup/account/connection/folder_names.rb +26 -0
  12. data/lib/imap/backup/account/connection.rb +17 -105
  13. data/lib/imap/backup/account/folder.rb +9 -6
  14. data/lib/imap/backup/account.rb +36 -16
  15. data/lib/imap/backup/cli/backup.rb +1 -3
  16. data/lib/imap/backup/cli/folders.rb +3 -3
  17. data/lib/imap/backup/cli/helpers.rb +24 -22
  18. data/lib/imap/backup/cli/local.rb +20 -13
  19. data/lib/imap/backup/cli/migrate.rb +5 -11
  20. data/lib/imap/backup/cli/restore.rb +8 -7
  21. data/lib/imap/backup/cli/setup.rb +10 -8
  22. data/lib/imap/backup/cli/stats.rb +78 -0
  23. data/lib/imap/backup/cli/status.rb +2 -2
  24. data/lib/imap/backup/cli/utils.rb +6 -8
  25. data/lib/imap/backup/cli.rb +24 -3
  26. data/lib/imap/backup/configuration.rb +9 -21
  27. data/lib/imap/backup/downloader.rb +56 -34
  28. data/lib/imap/backup/migrator.rb +5 -5
  29. data/lib/imap/backup/sanitizer.rb +3 -2
  30. data/lib/imap/backup/serializer/appender.rb +49 -0
  31. data/lib/imap/backup/serializer/directory.rb +37 -0
  32. data/lib/imap/backup/serializer/imap.rb +144 -0
  33. data/lib/imap/backup/serializer/mbox.rb +33 -88
  34. data/lib/imap/backup/serializer/mbox_enumerator.rb +2 -0
  35. data/lib/imap/backup/serializer/message_enumerator.rb +29 -0
  36. data/lib/imap/backup/serializer/unused_name_finder.rb +25 -0
  37. data/lib/imap/backup/serializer.rb +160 -3
  38. data/lib/imap/backup/setup/account/header.rb +75 -0
  39. data/lib/imap/backup/setup/account.rb +41 -95
  40. data/lib/imap/backup/setup/asker.rb +4 -15
  41. data/lib/imap/backup/setup/backup_path.rb +41 -0
  42. data/lib/imap/backup/setup/email.rb +45 -0
  43. data/lib/imap/backup/setup/folder_chooser.rb +3 -3
  44. data/lib/imap/backup/setup/helpers.rb +2 -2
  45. data/lib/imap/backup/setup.rb +5 -4
  46. data/lib/imap/backup/thunderbird/mailbox_exporter.rb +41 -22
  47. data/lib/imap/backup/uploader.rb +46 -8
  48. data/lib/imap/backup/utils.rb +1 -1
  49. data/lib/imap/backup/version.rb +3 -3
  50. data/lib/imap/backup.rb +0 -2
  51. metadata +31 -105
  52. data/lib/imap/backup/serializer/mbox_store.rb +0 -217
  53. data/spec/features/backup_spec.rb +0 -108
  54. data/spec/features/configuration/minimal_configuration.rb +0 -15
  55. data/spec/features/configuration/missing_configuration.rb +0 -14
  56. data/spec/features/folders_spec.rb +0 -36
  57. data/spec/features/helper.rb +0 -2
  58. data/spec/features/local/list_accounts_spec.rb +0 -12
  59. data/spec/features/local/list_emails_spec.rb +0 -21
  60. data/spec/features/local/list_folders_spec.rb +0 -21
  61. data/spec/features/local/show_an_email_spec.rb +0 -34
  62. data/spec/features/migrate_spec.rb +0 -35
  63. data/spec/features/remote/list_account_folders_spec.rb +0 -16
  64. data/spec/features/restore_spec.rb +0 -162
  65. data/spec/features/status_spec.rb +0 -43
  66. data/spec/features/support/aruba.rb +0 -77
  67. data/spec/features/support/backup_directory.rb +0 -43
  68. data/spec/features/support/email_server.rb +0 -110
  69. data/spec/features/support/shared/connection_context.rb +0 -14
  70. data/spec/features/support/shared/message_fixtures.rb +0 -16
  71. data/spec/fixtures/connection.yml +0 -7
  72. data/spec/spec_helper.rb +0 -15
  73. data/spec/support/fixtures.rb +0 -11
  74. data/spec/support/higline_test_helpers.rb +0 -8
  75. data/spec/support/silence_logging.rb +0 -7
  76. data/spec/unit/email/mboxrd/message_spec.rb +0 -177
  77. data/spec/unit/email/provider/apple_mail_spec.rb +0 -7
  78. data/spec/unit/email/provider/base_spec.rb +0 -11
  79. data/spec/unit/email/provider/fastmail_spec.rb +0 -7
  80. data/spec/unit/email/provider/gmail_spec.rb +0 -7
  81. data/spec/unit/email/provider_spec.rb +0 -27
  82. data/spec/unit/imap/backup/account/connection_spec.rb +0 -405
  83. data/spec/unit/imap/backup/account/folder_spec.rb +0 -251
  84. data/spec/unit/imap/backup/cli/accounts_spec.rb +0 -47
  85. data/spec/unit/imap/backup/cli/helpers_spec.rb +0 -87
  86. data/spec/unit/imap/backup/cli/local_spec.rb +0 -81
  87. data/spec/unit/imap/backup/cli/utils_spec.rb +0 -62
  88. data/spec/unit/imap/backup/client/default_spec.rb +0 -22
  89. data/spec/unit/imap/backup/configuration_spec.rb +0 -238
  90. data/spec/unit/imap/backup/downloader_spec.rb +0 -44
  91. data/spec/unit/imap/backup/logger_spec.rb +0 -48
  92. data/spec/unit/imap/backup/migrator_spec.rb +0 -58
  93. data/spec/unit/imap/backup/serializer/mbox_enumerator_spec.rb +0 -45
  94. data/spec/unit/imap/backup/serializer/mbox_spec.rb +0 -222
  95. data/spec/unit/imap/backup/serializer/mbox_store_spec.rb +0 -329
  96. data/spec/unit/imap/backup/setup/account_spec.rb +0 -366
  97. data/spec/unit/imap/backup/setup/asker_spec.rb +0 -137
  98. data/spec/unit/imap/backup/setup/connection_tester_spec.rb +0 -51
  99. data/spec/unit/imap/backup/setup/folder_chooser_spec.rb +0 -146
  100. data/spec/unit/imap/backup/setup_spec.rb +0 -301
  101. data/spec/unit/imap/backup/uploader_spec.rb +0 -54
  102. data/spec/unit/imap/backup/utils_spec.rb +0 -92
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 164962561883b82372b32826843681c6a802e88982f4906778758a073e1479d8
4
- data.tar.gz: 8a71ad18952f675f61bc6342a3a9a60c2fc71797444142c2af5f98ef18ac13e9
3
+ metadata.gz: 6d4b9386097e77af34706992fb42b6ff9edd3a62a3cfb98e0b3363d864ee75dc
4
+ data.tar.gz: d72ec8493ea2436ccb033f2957d44236b71d3c607e25b15b0b755e93f011da10
5
5
  SHA512:
6
- metadata.gz: 600ff4b72954cecfb2037b9ef12b16029248a6c6e27b2b801661b5c746264a8aed7a99aa689cbeecc8e89f571b9f4c26350669e582bc087a518981bceb677141
7
- data.tar.gz: b706026f0b8231f10dfbb0a0c39162ccdf6a8671c513b6cbf637352dcd8d99987a3f93d2b523db7f4a8e3ff7178877dc3d84c1ad506245a9773330abea2a792f
6
+ metadata.gz: ab38ba548fa009c6f718f8da9b9fbb971751297eac03d2839e14f247c06d6858a155350291619090ef8165ffeb865674a19ef51c13dbf1220928106abfc79fa5
7
+ data.tar.gz: ad4e17a2b6b0b5a9ee1cc4737c324981069525a129018278514e51e8664008c71fc610539df58a2da590084bb7e3c46cd635fe1409ba0b53ad56bd241edfb9c8
data/README.md CHANGED
@@ -1,3 +1,8 @@
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)
5
+
1
6
  # imap-backup
2
7
 
3
8
  *Backup GMail (or other IMAP) accounts to disk*
@@ -5,10 +10,12 @@
5
10
  * [Source Code]
6
11
  * [API documentation]
7
12
  * [Rubygem]
13
+ * [CI Status]
8
14
 
9
15
  [Source Code]: https://github.com/joeyates/imap-backup "Source code at GitHub"
10
- [API documentation]: http://rubydoc.info/gems/imap-backup/frames "RDoc API Documentation at Rubydoc.info"
11
- [Rubygem]: http://rubygems.org/gems/imap-backup "Ruby gem at rubygems.org"
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
12
19
 
13
20
  # Installation
14
21
 
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
@@ -18,7 +18,6 @@ Gem::Specification.new do |gem|
18
18
  gem.files += %w[LICENSE README.md]
19
19
 
20
20
  gem.executables = gem.files.grep(%r{^bin/}).map { |f| File.basename(f) }
21
- gem.test_files = Dir.glob("spec/**/*{.rb,.yml}")
22
21
  gem.require_paths = ["lib"]
23
22
  gem.required_ruby_version = ">= 2.5"
24
23
 
@@ -34,4 +33,9 @@ Gem::Specification.new do |gem|
34
33
  gem.add_development_dependency "rspec", ">= 3.0.0"
35
34
  gem.add_development_dependency "rubocop-rspec"
36
35
  gem.add_development_dependency "simplecov"
36
+ gem.add_development_dependency "yard"
37
+
38
+ gem.metadata = {
39
+ "rubygems_mfa_required" => "true"
40
+ }
37
41
  end
data/lib/cli_coverage.rb CHANGED
@@ -1,18 +1,18 @@
1
1
  class CliCoverage
2
2
  def self.conditionally_activate
3
- if ENV["COVERAGE"]
4
- require "simplecov"
3
+ return if !ENV["COVERAGE"]
5
4
 
6
- # Collect coverage separately
7
- SimpleCov.command_name "#{ENV['COVERAGE']} coverage"
5
+ require "simplecov"
8
6
 
9
- # Silence output
10
- SimpleCov.formatter = SimpleCov::Formatter::SimpleFormatter
11
- SimpleCov.print_error_status = false
7
+ # Collect coverage separately
8
+ SimpleCov.command_name "#{ENV['COVERAGE']} #{ARGV.join(' ')} coverage"
12
9
 
13
- # Ensure SimpleCov doesn't filter out all out code
14
- project_root = File.expand_path("..", __dir__)
15
- SimpleCov.root project_root
16
- end
10
+ # Silence output
11
+ SimpleCov.formatter = SimpleCov::Formatter::SimpleFormatter
12
+ SimpleCov.print_error_status = false
13
+
14
+ # Ensure SimpleCov doesn't filter out all out code
15
+ project_root = File.expand_path("..", __dir__)
16
+ SimpleCov.root project_root
17
17
  end
18
18
  end
@@ -3,6 +3,8 @@ class Email::Provider; end
3
3
 
4
4
  class Email::Provider::Base
5
5
  def options
6
+ # rubocop:disable Naming/VariableNumber
6
7
  {port: 993, ssl: {ssl_version: :TLSv1_2}}
8
+ # rubocop:enable Naming/VariableNumber
7
9
  end
8
10
  end
@@ -6,6 +6,8 @@ class Email::Provider::Unknown < Email::Provider::Base
6
6
  end
7
7
 
8
8
  def options
9
+ # rubocop:disable Naming/VariableNumber
9
10
  {port: 993, ssl: {ssl_version: :TLSv1_2}}
11
+ # rubocop:enable Naming/VariableNumber
10
12
  end
11
13
  end
@@ -7,6 +7,7 @@ module Email; end
7
7
 
8
8
  class Email::Provider
9
9
  def self.for_address(address)
10
+ # rubocop:disable Lint/DuplicateBranch
10
11
  case
11
12
  when address.end_with?("@fastmail.com")
12
13
  Email::Provider::Fastmail.new
@@ -23,5 +24,6 @@ class Email::Provider
23
24
  else
24
25
  Email::Provider::Unknown.new
25
26
  end
27
+ # rubocop:enable Lint/DuplicateBranch
26
28
  end
27
29
  end
@@ -0,0 +1,27 @@
1
+ module Imap::Backup
2
+ class Account; end
3
+ class Account::Connection; end
4
+
5
+ class Account::Connection::BackupFolders
6
+ attr_reader :account
7
+ attr_reader :client
8
+
9
+ def initialize(client:, account:)
10
+ @client = client
11
+ @account = account
12
+ end
13
+
14
+ def run
15
+ names =
16
+ if account.folders&.any?
17
+ account.folders.map { |af| af[:name] }
18
+ else
19
+ Account::Connection::FolderNames.new(client: client, account: account).run
20
+ end
21
+
22
+ names.map do |name|
23
+ Account::Folder.new(account.connection, name)
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,54 @@
1
+ require "retry_on_error"
2
+
3
+ module Imap::Backup
4
+ class Account::Connection::ClientFactory
5
+ include RetryOnError
6
+
7
+ LOGIN_RETRY_CLASSES = [EOFError, Errno::ECONNRESET, SocketError].freeze
8
+
9
+ attr_reader :account
10
+
11
+ def initialize(account:)
12
+ @account = account
13
+ @provider = nil
14
+ @server = nil
15
+ end
16
+
17
+ def run
18
+ retry_on_error(errors: LOGIN_RETRY_CLASSES) do
19
+ options = provider_options
20
+ Logger.logger.debug(
21
+ "Creating IMAP instance: #{server}, options: #{options.inspect}"
22
+ )
23
+ client =
24
+ if provider.is_a?(Email::Provider::AppleMail)
25
+ Client::AppleMail.new(server, options)
26
+ else
27
+ Client::Default.new(server, options)
28
+ end
29
+ Logger.logger.debug "Logging in: #{account.username}/#{masked_password}"
30
+ client.login(account.username, account.password)
31
+ Logger.logger.debug "Login complete"
32
+ client
33
+ end
34
+ end
35
+
36
+ private
37
+
38
+ def masked_password
39
+ account.password.gsub(/./, "x")
40
+ end
41
+
42
+ def provider
43
+ @provider ||= Email::Provider.for_address(account.username)
44
+ end
45
+
46
+ def provider_options
47
+ provider.options.merge(account.connection_options || {})
48
+ end
49
+
50
+ def server
51
+ @server ||= account.server || provider.host
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,26 @@
1
+ module Imap::Backup
2
+ class Account; end
3
+ class Account::Connection; end
4
+
5
+ class Account::Connection::FolderNames
6
+ attr_reader :account
7
+ attr_reader :client
8
+
9
+ def initialize(client:, account:)
10
+ @client = client
11
+ @account = account
12
+ end
13
+
14
+ def run
15
+ folder_names = client.list
16
+
17
+ if folder_names.empty?
18
+ message = "Unable to get folder list for account #{account.username}"
19
+ Logger.logger.info message
20
+ raise message
21
+ end
22
+
23
+ folder_names
24
+ end
25
+ end
26
+ end
@@ -1,16 +1,15 @@
1
+ require "email/provider"
1
2
  require "imap/backup/client/apple_mail"
2
3
  require "imap/backup/client/default"
3
-
4
- require "retry_on_error"
4
+ require "imap/backup/account/connection/backup_folders"
5
+ require "imap/backup/account/connection/client_factory"
6
+ require "imap/backup/account/connection/folder_names"
7
+ require "imap/backup/serializer/directory"
5
8
 
6
9
  module Imap::Backup
7
10
  class Account; end
8
11
 
9
12
  class Account::Connection
10
- include RetryOnError
11
-
12
- LOGIN_RETRY_CLASSES = [EOFError, Errno::ECONNRESET, SocketError].freeze
13
-
14
13
  attr_reader :account
15
14
 
16
15
  def initialize(account)
@@ -19,57 +18,35 @@ module Imap::Backup
19
18
  end
20
19
 
21
20
  def folder_names
22
- @folder_names ||=
23
- begin
24
- folder_names = client.list
25
-
26
- if folder_names.empty?
27
- message = "Unable to get folder list for account #{account.username}"
28
- Imap::Backup::Logger.logger.info message
29
- raise message
30
- end
31
-
32
- folder_names
33
- end
21
+ @folder_names ||= Account::Connection::FolderNames.new(client: client, account: account).run
34
22
  end
35
23
 
36
24
  def backup_folders
37
25
  @backup_folders ||=
38
- begin
39
- names =
40
- if account.folders&.any?
41
- account.folders.map { |af| af[:name] }
42
- else
43
- folder_names
44
- end
45
-
46
- names.map do |name|
47
- Account::Folder.new(self, name)
48
- end
49
- end
26
+ Account::Connection::BackupFolders.new(client: client, account: account).run
50
27
  end
51
28
 
52
29
  def status
53
30
  ensure_account_folder
54
31
  backup_folders.map do |folder|
55
- s = Serializer::Mbox.new(account.local_path, folder.name)
32
+ s = Serializer.new(account.local_path, folder.name)
56
33
  {name: folder.name, local: s.uids, remote: folder.uids}
57
34
  end
58
35
  end
59
36
 
60
37
  def run_backup
61
- Imap::Backup::Logger.logger.debug "Running backup of account: #{account.username}"
38
+ Logger.logger.debug "Running backup of account: #{account.username}"
62
39
  # start the connection so we get logging messages in the right order
63
40
  client
64
41
  ensure_account_folder
65
42
  each_folder do |folder, serializer|
66
43
  next if !folder.exist?
67
44
 
68
- Imap::Backup::Logger.logger.debug "[#{folder.name}] running backup"
45
+ Logger.logger.debug "[#{folder.name}] running backup"
69
46
  serializer.apply_uid_validity(folder.uid_validity)
70
47
  begin
71
48
  Downloader.new(
72
- folder, serializer, block_size: config.download_block_size
49
+ folder, serializer, multi_fetch_size: account.multi_fetch_size
73
50
  ).run
74
51
  rescue Net::IMAP::ByeResponseError
75
52
  reconnect
@@ -86,7 +63,7 @@ module Imap::Backup
86
63
  base = Pathname.new(account.local_path)
87
64
  Pathname.glob(glob) do |path|
88
65
  name = path.relative_path_from(base).to_s[0..-6]
89
- serializer = Serializer::Mbox.new(account.local_path, name)
66
+ serializer = Serializer.new(account.local_path, name)
90
67
  folder = Account::Folder.new(self, name)
91
68
  yield serializer, folder
92
69
  end
@@ -94,7 +71,7 @@ module Imap::Backup
94
71
 
95
72
  def restore
96
73
  local_folders do |serializer, folder|
97
- restore_folder serializer, folder
74
+ Uploader.new(folder, serializer).run
98
75
  end
99
76
  end
100
77
 
@@ -110,94 +87,29 @@ module Imap::Backup
110
87
  def reset
111
88
  @backup_folders = nil
112
89
  @client = nil
113
- @config = nil
114
90
  @folder_names = nil
115
- @provider = nil
116
- @server = nil
117
91
  end
118
92
 
93
+ # TODO: make this private
119
94
  def client
120
- @client ||=
121
- retry_on_error(errors: LOGIN_RETRY_CLASSES) do
122
- options = provider_options
123
- Imap::Backup::Logger.logger.debug(
124
- "Creating IMAP instance: #{server}, options: #{options.inspect}"
125
- )
126
- client =
127
- if provider.is_a?(Email::Provider::AppleMail)
128
- Client::AppleMail.new(server, options)
129
- else
130
- Client::Default.new(server, options)
131
- end
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"
135
- client
136
- end
137
- end
138
-
139
- def server
140
- @server ||= account.server || provider.host
95
+ @client ||= Account::Connection::ClientFactory.new(account: account).run
141
96
  end
142
97
 
143
98
  private
144
99
 
145
100
  def each_folder
146
101
  backup_folders.each do |folder|
147
- serializer = Serializer::Mbox.new(account.local_path, folder.name)
102
+ serializer = Serializer.new(account.local_path, folder.name)
148
103
  yield folder, serializer
149
104
  end
150
105
  end
151
106
 
152
- def restore_folder(serializer, folder)
153
- existing_uids = folder.uids
154
- if existing_uids.any?
155
- Imap::Backup::Logger.logger.debug(
156
- "There's already a '#{folder.name}' folder with emails"
157
- )
158
- new_name = serializer.apply_uid_validity(folder.uid_validity)
159
- old_name = serializer.folder
160
- if new_name
161
- Imap::Backup::Logger.logger.debug(
162
- "Backup '#{old_name}' renamed and restored to '#{new_name}'"
163
- )
164
- new_serializer = Serializer::Mbox.new(account.local_path, new_name)
165
- new_folder = Account::Folder.new(self, new_name)
166
- new_folder.create
167
- new_serializer.force_uid_validity(new_folder.uid_validity)
168
- Uploader.new(new_folder, new_serializer).run
169
- else
170
- Uploader.new(folder, serializer).run
171
- end
172
- else
173
- folder.create
174
- serializer.force_uid_validity(folder.uid_validity)
175
- Uploader.new(folder, serializer).run
176
- end
177
- end
178
-
179
107
  def ensure_account_folder
180
108
  Utils.make_folder(
181
109
  File.dirname(account.local_path),
182
110
  File.basename(account.local_path),
183
- Serializer::DIRECTORY_PERMISSIONS
111
+ Serializer::Directory::DIRECTORY_PERMISSIONS
184
112
  )
185
113
  end
186
-
187
- def masked_password
188
- account.password.gsub(/./, "x")
189
- end
190
-
191
- def provider
192
- @provider ||= Email::Provider.for_address(account.username)
193
- end
194
-
195
- def provider_options
196
- provider.options.merge(account.connection_options || {})
197
- end
198
-
199
- def config
200
- @config ||= Configuration.new
201
- end
202
114
  end
203
115
  end
@@ -12,7 +12,8 @@ module Imap::Backup
12
12
  include RetryOnError
13
13
 
14
14
  BODY_ATTRIBUTE = "BODY[]".freeze
15
- UID_FETCH_RETRY_CLASSES = [EOFError].freeze
15
+ UID_FETCH_RETRY_CLASSES = [EOFError, Errno::ECONNRESET, IOError].freeze
16
+ APPEND_RETRY_CLASSES = [Net::IMAP::BadResponseError].freeze
16
17
 
17
18
  attr_reader :connection
18
19
  attr_reader :name
@@ -64,7 +65,7 @@ module Imap::Backup
64
65
  in `search_internal` in stdlib net/imap.rb.
65
66
  This is caused by `@responses["SEARCH"] being unset/undefined
66
67
  MESSAGE
67
- Imap::Backup::Logger.logger.warn message
68
+ Logger.logger.warn message
68
69
  []
69
70
  end
70
71
 
@@ -91,8 +92,10 @@ module Imap::Backup
91
92
  def append(message)
92
93
  body = message.imap_body
93
94
  date = message.date&.to_time
94
- response = client.append(utf7_encoded_name, body, nil, date)
95
- extract_uid(response)
95
+ retry_on_error(errors: APPEND_RETRY_CLASSES, limit: 3) do
96
+ response = client.append(utf7_encoded_name, body, nil, date)
97
+ extract_uid(response)
98
+ end
96
99
  end
97
100
 
98
101
  def clear
@@ -108,12 +111,12 @@ module Imap::Backup
108
111
  def examine
109
112
  client.examine(utf7_encoded_name)
110
113
  rescue Net::IMAP::NoResponseError
111
- Imap::Backup::Logger.logger.warn "Folder '#{name}' does not exist on server"
114
+ Logger.logger.warn "Folder '#{name}' does not exist on server"
112
115
  raise FolderNotFound, "Folder '#{name}' does not exist on server"
113
116
  end
114
117
 
115
118
  def extract_uid(response)
116
- @uid_validity, uid = response.data.code.data.split(" ").map(&:to_i)
119
+ @uid_validity, uid = response.data.code.data.split.map(&:to_i)
117
120
  uid
118
121
  end
119
122
 
@@ -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,27 +17,29 @@ 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]
21
+ @connection = nil
19
22
  @changes = {}
20
23
  @marked_for_deletion = false
21
24
  end
22
25
 
23
26
  def connection
24
- Account::Connection.new(self)
27
+ @connection ||= Account::Connection.new(self)
25
28
  end
26
29
 
27
30
  def valid?
28
- username && password
31
+ username && password ? true : false
29
32
  end
30
33
 
31
34
  def modified?
32
35
  changes.any?
33
36
  end
34
37
 
35
- def clear_changes!
38
+ def clear_changes
36
39
  @changes = {}
37
40
  end
38
41
 
39
- def mark_for_deletion!
42
+ def mark_for_deletion
40
43
  @marked_for_deletion = true
41
44
  end
42
45
 
@@ -45,14 +48,12 @@ module Imap::Backup
45
48
  end
46
49
 
47
50
  def to_h
48
- h = {
49
- username: @username,
50
- password: @password,
51
- }
51
+ h = {username: @username, password: @password}
52
52
  h[:local_path] = @local_path if @local_path
53
53
  h[:folders] = @folders if @folders
54
54
  h[:server] = @server if @server
55
55
  h[:connection_options] = @connection_options if @connection_options
56
+ h[:multi_fetch_size] = multi_fetch_size if @multi_fetch_size
56
57
  h
57
58
  end
58
59
 
@@ -70,6 +71,7 @@ module Imap::Backup
70
71
 
71
72
  def folders=(value)
72
73
  raise "folders must be an Array" if !value.is_a?(Array)
74
+
73
75
  update(:folders, value)
74
76
  end
75
77
 
@@ -82,20 +84,38 @@ module Imap::Backup
82
84
  update(:connection_options, parsed)
83
85
  end
84
86
 
87
+ def multi_fetch_size
88
+ int = @multi_fetch_size.to_i
89
+ if int.positive?
90
+ int
91
+ else
92
+ DEFAULT_MULTI_FETCH_SIZE
93
+ end
94
+ end
95
+
96
+ def multi_fetch_size=(value)
97
+ parsed = value.to_i
98
+ parsed = DEFAULT_MULTI_FETCH_SIZE if !parsed.positive?
99
+ update(:multi_fetch_size, parsed)
100
+ end
101
+
85
102
  private
86
103
 
87
104
  def update(field, value)
105
+ key = :"@#{field}"
88
106
  if changes[field]
89
107
  change = changes[field]
90
- changes.delete(field) if change[:from] == value
108
+ if change[:from] == value
109
+ changes.delete(field)
110
+ else
111
+ change[:to] = value
112
+ end
113
+ else
114
+ current = instance_variable_get(key)
115
+ changes[field] = {from: current, to: value} if value != current
91
116
  end
92
- set_field!(field, value)
93
- end
94
117
 
95
- def set_field!(field, value)
96
- key = :"@#{field}"
97
- current = instance_variable_get(key)
98
- changes[field] = {from: current, to: value}
118
+ @connection = nil
99
119
  instance_variable_set(key, value)
100
120
  end
101
121
  end
@@ -12,9 +12,7 @@ module Imap::Backup
12
12
 
13
13
  no_commands do
14
14
  def run
15
- each_connection(account_names) do |connection|
16
- connection.run_backup
17
- end
15
+ each_connection(account_names, &:run_backup)
18
16
  end
19
17
  end
20
18
  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