imap-backup 6.0.0.rc2 → 6.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (112) hide show
  1. checksums.yaml +4 -4
  2. data/imap-backup.gemspec +5 -1
  3. data/lib/cli_coverage.rb +11 -11
  4. data/lib/email/provider/apple_mail.rb +4 -0
  5. data/lib/email/provider/base.rb +6 -0
  6. data/lib/email/provider/purelymail.rb +11 -0
  7. data/lib/email/provider/unknown.rb +2 -0
  8. data/lib/email/provider.rb +5 -0
  9. data/lib/imap/backup/account/connection/backup_folders.rb +27 -0
  10. data/lib/imap/backup/account/connection/client_factory.rb +55 -0
  11. data/lib/imap/backup/account/connection/folder_names.rb +26 -0
  12. data/lib/imap/backup/account/connection.rb +16 -96
  13. data/lib/imap/backup/account/folder.rb +31 -9
  14. data/lib/imap/backup/account.rb +15 -6
  15. data/lib/imap/backup/cli/backup.rb +1 -3
  16. data/lib/imap/backup/cli/helpers.rb +24 -22
  17. data/lib/imap/backup/cli/local.rb +20 -13
  18. data/lib/imap/backup/cli/migrate.rb +4 -10
  19. data/lib/imap/backup/cli/restore.rb +8 -7
  20. data/lib/imap/backup/cli/setup.rb +10 -8
  21. data/lib/imap/backup/cli/stats.rb +78 -0
  22. data/lib/imap/backup/cli/status.rb +2 -2
  23. data/lib/imap/backup/cli/utils.rb +4 -6
  24. data/lib/imap/backup/cli.rb +24 -3
  25. data/lib/imap/backup/configuration.rb +9 -11
  26. data/lib/imap/backup/downloader.rb +75 -31
  27. data/lib/imap/backup/migrator.rb +5 -5
  28. data/lib/imap/backup/sanitizer.rb +3 -2
  29. data/lib/imap/backup/serializer/appender.rb +49 -0
  30. data/lib/imap/backup/serializer/imap.rb +27 -3
  31. data/lib/imap/backup/serializer/mbox.rb +18 -2
  32. data/lib/imap/backup/serializer/message_enumerator.rb +29 -0
  33. data/lib/imap/backup/serializer/unused_name_finder.rb +25 -0
  34. data/lib/imap/backup/serializer.rb +64 -84
  35. data/lib/imap/backup/setup/account/header.rb +81 -0
  36. data/lib/imap/backup/setup/account.rb +28 -91
  37. data/lib/imap/backup/setup/asker.rb +4 -15
  38. data/lib/imap/backup/setup/backup_path.rb +45 -0
  39. data/lib/imap/backup/setup/email.rb +45 -0
  40. data/lib/imap/backup/setup/folder_chooser.rb +3 -3
  41. data/lib/imap/backup/setup/helpers.rb +1 -1
  42. data/lib/imap/backup/setup.rb +7 -6
  43. data/lib/imap/backup/thunderbird/mailbox_exporter.rb +39 -20
  44. data/lib/imap/backup/uploader.rb +46 -8
  45. data/lib/imap/backup/utils.rb +1 -1
  46. data/lib/imap/backup/version.rb +2 -2
  47. data/lib/imap/backup.rb +0 -1
  48. metadata +32 -134
  49. data/spec/features/backup_spec.rb +0 -100
  50. data/spec/features/configuration/minimal_configuration.rb +0 -15
  51. data/spec/features/configuration/missing_configuration.rb +0 -14
  52. data/spec/features/folders_spec.rb +0 -36
  53. data/spec/features/helper.rb +0 -2
  54. data/spec/features/local/list_accounts_spec.rb +0 -12
  55. data/spec/features/local/list_emails_spec.rb +0 -21
  56. data/spec/features/local/list_folders_spec.rb +0 -21
  57. data/spec/features/local/show_an_email_spec.rb +0 -34
  58. data/spec/features/migrate_spec.rb +0 -35
  59. data/spec/features/remote/list_account_folders_spec.rb +0 -16
  60. data/spec/features/restore_spec.rb +0 -162
  61. data/spec/features/status_spec.rb +0 -43
  62. data/spec/features/support/aruba.rb +0 -78
  63. data/spec/features/support/backup_directory.rb +0 -43
  64. data/spec/features/support/email_server.rb +0 -110
  65. data/spec/features/support/shared/connection_context.rb +0 -14
  66. data/spec/features/support/shared/message_fixtures.rb +0 -16
  67. data/spec/fixtures/connection.yml +0 -7
  68. data/spec/spec_helper.rb +0 -15
  69. data/spec/support/fixtures.rb +0 -11
  70. data/spec/support/higline_test_helpers.rb +0 -8
  71. data/spec/support/silence_logging.rb +0 -7
  72. data/spec/unit/email/mboxrd/message_spec.rb +0 -177
  73. data/spec/unit/email/provider/apple_mail_spec.rb +0 -7
  74. data/spec/unit/email/provider/base_spec.rb +0 -11
  75. data/spec/unit/email/provider/fastmail_spec.rb +0 -7
  76. data/spec/unit/email/provider/gmail_spec.rb +0 -7
  77. data/spec/unit/email/provider_spec.rb +0 -27
  78. data/spec/unit/imap/backup/account/connection_spec.rb +0 -433
  79. data/spec/unit/imap/backup/account/folder_spec.rb +0 -261
  80. data/spec/unit/imap/backup/account_spec.rb +0 -246
  81. data/spec/unit/imap/backup/cli/accounts_spec.rb +0 -58
  82. data/spec/unit/imap/backup/cli/backup_spec.rb +0 -19
  83. data/spec/unit/imap/backup/cli/folders_spec.rb +0 -39
  84. data/spec/unit/imap/backup/cli/helpers_spec.rb +0 -87
  85. data/spec/unit/imap/backup/cli/local_spec.rb +0 -100
  86. data/spec/unit/imap/backup/cli/migrate_spec.rb +0 -80
  87. data/spec/unit/imap/backup/cli/restore_spec.rb +0 -67
  88. data/spec/unit/imap/backup/cli/setup_spec.rb +0 -17
  89. data/spec/unit/imap/backup/cli/utils_spec.rb +0 -125
  90. data/spec/unit/imap/backup/cli_spec.rb +0 -93
  91. data/spec/unit/imap/backup/client/apple_mail_spec.rb +0 -9
  92. data/spec/unit/imap/backup/client/default_spec.rb +0 -22
  93. data/spec/unit/imap/backup/configuration_spec.rb +0 -238
  94. data/spec/unit/imap/backup/downloader_spec.rb +0 -96
  95. data/spec/unit/imap/backup/logger_spec.rb +0 -48
  96. data/spec/unit/imap/backup/migrator_spec.rb +0 -58
  97. data/spec/unit/imap/backup/sanitizer_spec.rb +0 -42
  98. data/spec/unit/imap/backup/serializer/directory_spec.rb +0 -37
  99. data/spec/unit/imap/backup/serializer/imap_spec.rb +0 -218
  100. data/spec/unit/imap/backup/serializer/mbox_enumerator_spec.rb +0 -45
  101. data/spec/unit/imap/backup/serializer/mbox_spec.rb +0 -101
  102. data/spec/unit/imap/backup/serializer_spec.rb +0 -296
  103. data/spec/unit/imap/backup/setup/account_spec.rb +0 -461
  104. data/spec/unit/imap/backup/setup/asker_spec.rb +0 -137
  105. data/spec/unit/imap/backup/setup/connection_tester_spec.rb +0 -51
  106. data/spec/unit/imap/backup/setup/folder_chooser_spec.rb +0 -146
  107. data/spec/unit/imap/backup/setup/helpers_spec.rb +0 -15
  108. data/spec/unit/imap/backup/setup_spec.rb +0 -301
  109. data/spec/unit/imap/backup/thunderbird/mailbox_exporter_spec.rb +0 -116
  110. data/spec/unit/imap/backup/uploader_spec.rb +0 -54
  111. data/spec/unit/imap/backup/utils_spec.rb +0 -92
  112. data/spec/unit/retry_on_error_spec.rb +0 -34
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4f9cac7966fac9858fdc380d8e9638c8d479035ee4053444a104b8c3eb5a304c
4
- data.tar.gz: 5d9785b5e34d90535c8052d216d9b3dbf46ea743672f05129604729662f42f34
3
+ metadata.gz: 2a05fedc57275cadd2a1bd22efbe88ef5ec6ee72843400499cac8b8df8e80c35
4
+ data.tar.gz: f0e8e859b5fb1bb8480730b3ea62abc7470f5d0ddb469113bbdac0e4f2a88d1a
5
5
  SHA512:
6
- metadata.gz: a4595e4316dcdc20123a8ed2bec524d37c1de95da49281a30484349c317a996cf3c6946617a489880a0c41db41698b2ee862d9aee4f9c9b8fa34d67707ba924c
7
- data.tar.gz: ccee268de9cb6225c72fe0cb11a2552fc793f664429d8dfa918e5b4697961c8e5e15e47ed653dac241f377385806443f17683614b8ff76075a8ff2f12e07f98c
6
+ metadata.gz: bfb4de1a3048a9f29ee30fb56c7493524a7d688a6eab2ec3307005ba3c8d341a99c961acd1d8f588384dc91571b4528482931e52724df42dff19a26553568fb0
7
+ data.tar.gz: a411ad26b4ce3eb631c97d94c9242c1eb48fd57d23cffcec84f5c829d9354b506115b6c8278ee38430e27f0d6ca1400d55f74adce5eb99f94db2963f235c6d7a
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']} #{ARGV.join(' ')} 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
@@ -4,4 +4,8 @@ class Email::Provider::AppleMail < Email::Provider::Base
4
4
  def host
5
5
  "imap.mail.me.com"
6
6
  end
7
+
8
+ def sets_seen_flags_on_fetch?
9
+ true
10
+ end
7
11
  end
@@ -3,6 +3,12 @@ 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
9
+ end
10
+
11
+ def sets_seen_flags_on_fetch?
12
+ false
7
13
  end
8
14
  end
@@ -0,0 +1,11 @@
1
+ require "email/provider/base"
2
+
3
+ class Email::Provider::Purelymail < Email::Provider::Base
4
+ def host
5
+ "mailserver.purelymail.com"
6
+ end
7
+
8
+ def sets_seen_flags_on_fetch?
9
+ true
10
+ end
11
+ 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
@@ -1,12 +1,14 @@
1
1
  require "email/provider/apple_mail"
2
2
  require "email/provider/fastmail"
3
3
  require "email/provider/gmail"
4
+ require "email/provider/purelymail"
4
5
  require "email/provider/unknown"
5
6
 
6
7
  module Email; end
7
8
 
8
9
  class Email::Provider
9
10
  def self.for_address(address)
11
+ # rubocop:disable Lint/DuplicateBranch
10
12
  case
11
13
  when address.end_with?("@fastmail.com")
12
14
  Email::Provider::Fastmail.new
@@ -20,8 +22,11 @@ class Email::Provider
20
22
  Email::Provider::AppleMail.new
21
23
  when address.end_with?("@me.com")
22
24
  Email::Provider::AppleMail.new
25
+ when address.end_with?("@purelymail.com")
26
+ Email::Provider::Purelymail.new
23
27
  else
24
28
  Email::Provider::Unknown.new
25
29
  end
30
+ # rubocop:enable Lint/DuplicateBranch
26
31
  end
27
32
  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,55 @@
1
+ require "email/provider"
2
+ require "retry_on_error"
3
+
4
+ module Imap::Backup
5
+ class Account::Connection::ClientFactory
6
+ include RetryOnError
7
+
8
+ LOGIN_RETRY_CLASSES = [EOFError, Errno::ECONNRESET, SocketError].freeze
9
+
10
+ attr_reader :account
11
+
12
+ def initialize(account:)
13
+ @account = account
14
+ @provider = nil
15
+ @server = nil
16
+ end
17
+
18
+ def run
19
+ retry_on_error(errors: LOGIN_RETRY_CLASSES) do
20
+ options = provider_options
21
+ Logger.logger.debug(
22
+ "Creating IMAP instance: #{server}, options: #{options.inspect}"
23
+ )
24
+ client =
25
+ if provider.is_a?(Email::Provider::AppleMail)
26
+ Client::AppleMail.new(server, options)
27
+ else
28
+ Client::Default.new(server, options)
29
+ end
30
+ Logger.logger.debug "Logging in: #{account.username}/#{masked_password}"
31
+ client.login(account.username, account.password)
32
+ Logger.logger.debug "Login complete"
33
+ client
34
+ end
35
+ end
36
+
37
+ private
38
+
39
+ def masked_password
40
+ account.password.gsub(/./, "x")
41
+ end
42
+
43
+ def provider
44
+ @provider ||= Email::Provider.for_address(account.username)
45
+ end
46
+
47
+ def provider_options
48
+ provider.options.merge(account.connection_options || {})
49
+ end
50
+
51
+ def server
52
+ @server ||= account.server || provider.host
53
+ end
54
+ end
55
+ 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,17 +1,14 @@
1
1
  require "imap/backup/client/apple_mail"
2
2
  require "imap/backup/client/default"
3
+ require "imap/backup/account/connection/backup_folders"
4
+ require "imap/backup/account/connection/client_factory"
5
+ require "imap/backup/account/connection/folder_names"
3
6
  require "imap/backup/serializer/directory"
4
7
 
5
- require "retry_on_error"
6
-
7
8
  module Imap::Backup
8
9
  class Account; end
9
10
 
10
11
  class Account::Connection
11
- include RetryOnError
12
-
13
- LOGIN_RETRY_CLASSES = [EOFError, Errno::ECONNRESET, SocketError].freeze
14
-
15
12
  attr_reader :account
16
13
 
17
14
  def initialize(account)
@@ -20,34 +17,12 @@ module Imap::Backup
20
17
  end
21
18
 
22
19
  def folder_names
23
- @folder_names ||=
24
- begin
25
- folder_names = client.list
26
-
27
- if folder_names.empty?
28
- message = "Unable to get folder list for account #{account.username}"
29
- Imap::Backup::Logger.logger.info message
30
- raise message
31
- end
32
-
33
- folder_names
34
- end
20
+ @folder_names ||= Account::Connection::FolderNames.new(client: client, account: account).run
35
21
  end
36
22
 
37
23
  def backup_folders
38
24
  @backup_folders ||=
39
- begin
40
- names =
41
- if account.folders&.any?
42
- account.folders.map { |af| af[:name] }
43
- else
44
- folder_names
45
- end
46
-
47
- names.map do |name|
48
- Account::Folder.new(self, name)
49
- end
50
- end
25
+ Account::Connection::BackupFolders.new(client: client, account: account).run
51
26
  end
52
27
 
53
28
  def status
@@ -59,18 +34,21 @@ module Imap::Backup
59
34
  end
60
35
 
61
36
  def run_backup
62
- Imap::Backup::Logger.logger.debug "Running backup of account: #{account.username}"
37
+ Logger.logger.debug "Running backup of account: #{account.username}"
63
38
  # start the connection so we get logging messages in the right order
64
39
  client
65
40
  ensure_account_folder
66
41
  each_folder do |folder, serializer|
67
42
  next if !folder.exist?
68
43
 
69
- Imap::Backup::Logger.logger.debug "[#{folder.name}] running backup"
44
+ Logger.logger.debug "[#{folder.name}] running backup"
70
45
  serializer.apply_uid_validity(folder.uid_validity)
71
46
  begin
72
47
  Downloader.new(
73
- folder, serializer, multi_fetch_size: account.multi_fetch_size
48
+ folder,
49
+ serializer,
50
+ multi_fetch_size: account.multi_fetch_size,
51
+ reset_seen_flags_after_fetch: account.reset_seen_flags_after_fetch
74
52
  ).run
75
53
  rescue Net::IMAP::ByeResponseError
76
54
  reconnect
@@ -95,7 +73,7 @@ module Imap::Backup
95
73
 
96
74
  def restore
97
75
  local_folders do |serializer, folder|
98
- restore_folder serializer, folder
76
+ Uploader.new(folder, serializer).run
99
77
  end
100
78
  end
101
79
 
@@ -112,32 +90,11 @@ module Imap::Backup
112
90
  @backup_folders = nil
113
91
  @client = nil
114
92
  @folder_names = nil
115
- @provider = nil
116
- @server = nil
117
93
  end
118
94
 
95
+ # TODO: make this private
119
96
  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
97
+ @client ||= Account::Connection::ClientFactory.new(account: account).run
141
98
  end
142
99
 
143
100
  private
@@ -149,51 +106,14 @@ module Imap::Backup
149
106
  end
150
107
  end
151
108
 
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.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
109
  def ensure_account_folder
110
+ raise "The backup path for #{account.username} is not set" if !account.local_path
111
+
180
112
  Utils.make_folder(
181
113
  File.dirname(account.local_path),
182
114
  File.basename(account.local_path),
183
115
  Serializer::Directory::DIRECTORY_PERMISSIONS
184
116
  )
185
117
  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
118
  end
199
119
  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,29 +92,50 @@ 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
- def clear
99
- existing = uids
101
+ def set_flags(uids, flags)
100
102
  # Use read-write access, via `select`
101
103
  client.select(utf7_encoded_name)
102
- client.uid_store(existing, "+FLAGS", [:Deleted])
104
+ client.uid_store(uids, "+FLAGS", flags)
105
+ end
106
+
107
+ def unset_flags(uids, flags)
108
+ client.select(utf7_encoded_name)
109
+ client.uid_store(uids, "-FLAGS", flags)
110
+ end
111
+
112
+ def clear
113
+ set_flags(uids, [:Deleted])
103
114
  client.expunge
104
115
  end
105
116
 
117
+ def unseen(uids)
118
+ messages = uids.map(&:to_s).join(",")
119
+ examine
120
+ client.uid_search([messages, "UNSEEN"])
121
+ rescue NoMethodError
122
+ # Apple Mail returns an empty response when searches have no results
123
+ []
124
+ rescue FolderNotFound
125
+ nil
126
+ end
127
+
106
128
  private
107
129
 
108
130
  def examine
109
131
  client.examine(utf7_encoded_name)
110
132
  rescue Net::IMAP::NoResponseError
111
- Imap::Backup::Logger.logger.warn "Folder '#{name}' does not exist on server"
133
+ Logger.logger.warn "Folder '#{name}' does not exist on server"
112
134
  raise FolderNotFound, "Folder '#{name}' does not exist on server"
113
135
  end
114
136
 
115
137
  def extract_uid(response)
116
- @uid_validity, uid = response.data.code.data.split(" ").map(&:to_i)
138
+ @uid_validity, uid = response.data.code.data.split.map(&:to_i)
117
139
  uid
118
140
  end
119
141
 
@@ -8,6 +8,7 @@ module Imap::Backup
8
8
  attr_reader :folders
9
9
  attr_reader :server
10
10
  attr_reader :connection_options
11
+ attr_reader :reset_seen_flags_after_fetch
11
12
  attr_reader :changes
12
13
 
13
14
  def initialize(options)
@@ -18,12 +19,14 @@ module Imap::Backup
18
19
  @server = options[:server]
19
20
  @connection_options = options[:connection_options]
20
21
  @multi_fetch_size = options[:multi_fetch_size]
22
+ @reset_seen_flags_after_fetch = options[:reset_seen_flags_after_fetch]
23
+ @connection = nil
21
24
  @changes = {}
22
25
  @marked_for_deletion = false
23
26
  end
24
27
 
25
28
  def connection
26
- Account::Connection.new(self)
29
+ @connection ||= Account::Connection.new(self)
27
30
  end
28
31
 
29
32
  def valid?
@@ -47,15 +50,15 @@ module Imap::Backup
47
50
  end
48
51
 
49
52
  def to_h
50
- h = {
51
- username: @username,
52
- password: @password,
53
- }
53
+ h = {username: @username, password: @password}
54
54
  h[:local_path] = @local_path if @local_path
55
55
  h[:folders] = @folders if @folders
56
56
  h[:server] = @server if @server
57
57
  h[:connection_options] = @connection_options if @connection_options
58
58
  h[:multi_fetch_size] = multi_fetch_size if @multi_fetch_size
59
+ if @reset_seen_flags_after_fetch
60
+ h[:reset_seen_flags_after_fetch] = @reset_seen_flags_after_fetch
61
+ end
59
62
  h
60
63
  end
61
64
 
@@ -73,6 +76,7 @@ module Imap::Backup
73
76
 
74
77
  def folders=(value)
75
78
  raise "folders must be an Array" if !value.is_a?(Array)
79
+
76
80
  update(:folders, value)
77
81
  end
78
82
 
@@ -100,6 +104,10 @@ module Imap::Backup
100
104
  update(:multi_fetch_size, parsed)
101
105
  end
102
106
 
107
+ def reset_seen_flags_after_fetch=(value)
108
+ update(:reset_seen_flags_after_fetch, value)
109
+ end
110
+
103
111
  private
104
112
 
105
113
  def update(field, value)
@@ -113,9 +121,10 @@ module Imap::Backup
113
121
  end
114
122
  else
115
123
  current = instance_variable_get(key)
116
- changes[field] = {from: current, to: value}
124
+ changes[field] = {from: current, to: value} if value != current
117
125
  end
118
126
 
127
+ @connection = nil
119
128
  instance_variable_set(key, value)
120
129
  end
121
130
  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
@@ -1,35 +1,37 @@
1
1
  require "imap/backup"
2
2
  require "imap/backup/cli/accounts"
3
3
 
4
- module Imap::Backup::CLI::Helpers
5
- def symbolized(options)
6
- options.each.with_object({}) do |(k, v), acc|
7
- key = k.gsub("-", "_").intern
8
- acc[key] = v
4
+ module Imap::Backup
5
+ module CLI::Helpers
6
+ def symbolized(options)
7
+ options.each.with_object({}) do |(k, v), acc|
8
+ key = k.gsub("-", "_").intern
9
+ acc[key] = v
10
+ end
9
11
  end
10
- end
11
12
 
12
- def account(email)
13
- accounts = Imap::Backup::CLI::Accounts.new
14
- account = accounts.find { |a| a.username == email }
15
- raise "#{email} is not a configured account" if !account
13
+ def account(email)
14
+ accounts = CLI::Accounts.new
15
+ account = accounts.find { |a| a.username == email }
16
+ raise "#{email} is not a configured account" if !account
16
17
 
17
- account
18
- end
18
+ account
19
+ end
19
20
 
20
- def connection(email)
21
- account = account(email)
21
+ def connection(email)
22
+ account = account(email)
22
23
 
23
- Imap::Backup::Account::Connection.new(account)
24
- end
24
+ Account::Connection.new(account)
25
+ end
25
26
 
26
- def each_connection(names)
27
- accounts = Imap::Backup::CLI::Accounts.new(names)
27
+ def each_connection(names)
28
+ accounts = CLI::Accounts.new(names)
28
29
 
29
- accounts.each do |account|
30
- yield account.connection
30
+ accounts.each do |account|
31
+ yield account.connection
32
+ end
33
+ rescue ConfigurationNotFound
34
+ raise "imap-backup is not configured. Run `imap-backup setup`"
31
35
  end
32
- rescue Imap::Backup::ConfigurationNotFound
33
- raise "imap-backup is not configured. Run `imap-backup setup`"
34
36
  end
35
37
  end