imap-backup 5.2.0 → 6.0.1

Sign up to get free protection for your applications and to get access to all the features.
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