imap-backup 16.3.0 → 16.4.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 (50) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +3 -92
  3. data/docs/development.md +8 -0
  4. data/docs/testing.md +93 -0
  5. data/imap-backup.gemspec +12 -11
  6. data/lib/imap/backup/account/backup.rb +32 -10
  7. data/lib/imap/backup/account/backup_folders.rb +2 -0
  8. data/lib/imap/backup/account/client_factory.rb +1 -0
  9. data/lib/imap/backup/account/folder.rb +9 -0
  10. data/lib/imap/backup/account/folder_backup.rb +3 -0
  11. data/lib/imap/backup/account/folder_ensurer.rb +1 -0
  12. data/lib/imap/backup/account/folder_mapper.rb +6 -0
  13. data/lib/imap/backup/account/local_only_folder_deleter.rb +1 -0
  14. data/lib/imap/backup/account/locker.rb +40 -0
  15. data/lib/imap/backup/account/restore.rb +13 -2
  16. data/lib/imap/backup/account/serialized_folders.rb +1 -0
  17. data/lib/imap/backup/account.rb +50 -18
  18. data/lib/imap/backup/cli/backup.rb +6 -0
  19. data/lib/imap/backup/cli/local/check.rb +5 -0
  20. data/lib/imap/backup/cli/local.rb +1 -0
  21. data/lib/imap/backup/cli/options.rb +1 -0
  22. data/lib/imap/backup/cli/remote.rb +5 -2
  23. data/lib/imap/backup/cli/restore.rb +12 -5
  24. data/lib/imap/backup/cli/setup.rb +3 -0
  25. data/lib/imap/backup/cli/single/backup.rb +20 -0
  26. data/lib/imap/backup/cli/stats.rb +11 -0
  27. data/lib/imap/backup/cli/transfer.rb +32 -8
  28. data/lib/imap/backup/cli/utils.rb +17 -9
  29. data/lib/imap/backup/client/automatic_login_wrapper.rb +1 -0
  30. data/lib/imap/backup/client/default.rb +2 -0
  31. data/lib/imap/backup/configuration.rb +5 -3
  32. data/lib/imap/backup/downloader.rb +4 -0
  33. data/lib/imap/backup/email/mboxrd/message.rb +1 -0
  34. data/lib/imap/backup/file_mode.rb +1 -0
  35. data/lib/imap/backup/flag_refresher.rb +2 -0
  36. data/lib/imap/backup/local_only_message_deleter.rb +2 -0
  37. data/lib/imap/backup/lockfile.rb +94 -0
  38. data/lib/imap/backup/logger.rb +1 -1
  39. data/lib/imap/backup/migrator.rb +3 -0
  40. data/lib/imap/backup/mirror/map.rb +2 -1
  41. data/lib/imap/backup/mirror.rb +6 -2
  42. data/lib/imap/backup/serializer/mbox.rb +1 -1
  43. data/lib/imap/backup/serializer/message.rb +1 -1
  44. data/lib/imap/backup/serializer/permission_checker.rb +1 -1
  45. data/lib/imap/backup/serializer/version2_migrator.rb +1 -1
  46. data/lib/imap/backup/setup/asker.rb +1 -0
  47. data/lib/imap/backup/setup.rb +1 -0
  48. data/lib/imap/backup/thunderbird/mailbox_exporter.rb +1 -0
  49. data/lib/imap/backup/version.rb +2 -2
  50. metadata +20 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1dbb91a882b2679944560fe5d0f16fd11b59dce626386626d151463a8354a981
4
- data.tar.gz: 7b57013410b24ed4625a0405cc9b950f6cdf528e5d50d51b2919a81048f92947
3
+ metadata.gz: f3ffe8d1bd32587d59e12a514b005d68175244e2cb73a9d138dd3a5d273c785f
4
+ data.tar.gz: 62d6364641799f1ce20466affc34052f8614a2a2d5633f53a05b4cac31a4e0f3
5
5
  SHA512:
6
- metadata.gz: '04508eb2303ed44b9fdd8cd361958ed66eb5279a91dc310e10e404432a9e504094b4ff2d3972a645dc35ea2eff47f79f408466e3cf23cf6ac761ef1e5d3601fe'
7
- data.tar.gz: a96050615e046863111e79a3350c38dc3d6e3e05f7be967fdd5003e295c1d896f07b48d0aad719c0d2571f0f7aa315833f343bb7b0432c0e3cef556f915b1cfc
6
+ metadata.gz: f4fdd4254948ef5dd7fba17e3683aa13bf84a19a77ffd935bad3f830355d4bf0d14ff48c731be2542ce56332ed8b4903b1b7971a16e7e947f4406ba4473b4a44
7
+ data.tar.gz: 4ca3ca9bb9eda64a9b8b9a6cc0889e5815f446e2a798f762f1d55e228928e99988c186bdaad4dfd7d033cd21667de7d906ea3cbd1f11d947dc77d68f4bbd1090
data/README.md CHANGED
@@ -25,102 +25,13 @@ The {file:CHANGELOG.md CHANGELOG} has a history of the changes to the program.
25
25
  * Restartable - calculate start point based on already downloaded messages
26
26
  * Standalone - do not rely on an email client or MTA
27
27
 
28
- # Repository
28
+ # Development
29
29
 
30
- After cloning the repo, run the following command to get
31
- better `git blame` output:
32
-
33
- ```sh
34
- git config --local blame.ignoreRevsFile .git-blame-ignore-revs
35
- ```
30
+ See the [development documentation](/docs/development.md).
36
31
 
37
32
  # Testing
38
33
 
39
- ## Feature Specs
40
-
41
- Specs under `specs/features` are integration specs.
42
- Some of these specs run against two local IMAP servers
43
- controlled by Podman (or Docker) Compose.
44
-
45
- Start them before running the test suite
46
-
47
- ```sh
48
- $ podman-compose -f dev/compose.yml up -d
49
- ```
50
-
51
- or, with Docker
52
-
53
- ```sh
54
- $ docker-compose -f dev/compose.yml up -d
55
- ```
56
-
57
- Then, run all specs
58
-
59
- ```sh
60
- $ rspec
61
- ```
62
-
63
- To exclude container-based tests
64
-
65
- ```sh
66
- $ rspec --tag ~container
67
- ```
68
-
69
- To run **just** the feature specs
70
-
71
- ```sh
72
- rspec spec/features/**/*_spec.rb
73
- ```
74
-
75
- ## Full Test Run
76
-
77
- The full test run includes RSpec specs **and** Rubocop checks
78
-
79
- ```sh
80
- rake
81
- ```
82
-
83
- # Test Debugging
84
-
85
- The feature specs are run 'out of process' via the Aruba gem.
86
- In order to see debugging output from the process,
87
- use `last_command_started.output`.
88
-
89
- # Older Rubies
90
-
91
- A Containerfile is available to allow testing with all available Ruby versions,
92
- see the README in the `dev` directory.
93
-
94
- # Performance Specs
95
-
96
- ```sh
97
- PERFORMANCE=1 rspec --order=defined
98
- ```
99
-
100
- Beware: the performance spec (just backup for now) takes a very
101
- long time to run, approximately 24 hours!
102
-
103
- # Access Docker imap server
104
-
105
- ```ruby
106
- require "net/imap"
107
- require_relative "spec/features/support/30_email_server_helpers"
108
-
109
- include EmailServerHelpers
110
-
111
- test_connection = test_server_connection_parameters
112
-
113
- test_imap = Net::IMAP.new(test_connection[:server], test_connection[:connection_options])
114
- test_imap.login(test_connection[:username], test_connection[:password])
115
-
116
- message = "From: #{test_connection[:username]}\nSubject: Some Subject\n\nHello!\n"
117
- response = test_imap.append("INBOX", message, nil, nil)
118
-
119
- test_imap.examine("INBOX")
120
- uids = test_imap.uid_search(["ALL"]).sort
121
-
122
- fetch_data_items = test_imap.uid_fetch(uids, ["BODY[]"])
123
- ```
34
+ See the [testing documentation](/docs/testing.md).
124
35
 
125
36
  # Contributing
126
37
 
@@ -0,0 +1,8 @@
1
+ # Repository
2
+
3
+ After cloning the repo, run the following command to get
4
+ better `git blame` output:
5
+
6
+ ```sh
7
+ git config --local blame.ignoreRevsFile .git-blame-ignore-revs
8
+ ```
data/docs/testing.md ADDED
@@ -0,0 +1,93 @@
1
+ # Setup
2
+
3
+ Specs under `spec/features` are integration specs.
4
+ Some of these specs run against two local IMAP servers
5
+ controlled by Podman (or Docker) Compose.
6
+
7
+ Start them before running the test suite
8
+
9
+ ```sh
10
+ $ podman-compose -f dev/compose.yml up -d
11
+ ```
12
+
13
+ or, with Docker
14
+
15
+ ```sh
16
+ $ docker-compose -f dev/compose.yml up -d
17
+ ```
18
+
19
+ # Invocations
20
+
21
+ Run all specs
22
+
23
+ ```sh
24
+ $ rake spec
25
+ ```
26
+
27
+ Run **just** the unit specs
28
+
29
+ ```sh
30
+ $ rake spec_unit
31
+ ```
32
+
33
+ Run **just** the feature specs
34
+
35
+ ```sh
36
+ $ rake spec_feature
37
+ ```
38
+
39
+ To exclude the slow container-based tests
40
+
41
+ ```sh
42
+ $ rake spec_non_container
43
+ ```
44
+
45
+ ## Full Test Run
46
+
47
+ The full test run includes RSpec specs **and** Rubocop checks
48
+
49
+ ```sh
50
+ rake test
51
+ ```
52
+
53
+ # Debugging
54
+
55
+ The feature specs are run 'out of process' via the Aruba gem.
56
+ In order to see debugging output from the process,
57
+ use `last_command_started.output`.
58
+
59
+ # Older Rubies
60
+
61
+ A Containerfile is available to allow testing with all available Ruby versions,
62
+ see the README in the `dev` directory.
63
+
64
+ # Performance Specs
65
+
66
+ ```sh
67
+ PERFORMANCE=1 rspec --order=defined
68
+ ```
69
+
70
+ Beware: the performance spec (just backup for now) takes a very
71
+ long time to run, approximately 24 hours!
72
+
73
+ # Access Docker imap server
74
+
75
+ ```ruby
76
+ require "net/imap"
77
+ require_relative "spec/features/support/30_email_server_helpers"
78
+
79
+ include EmailServerHelpers
80
+
81
+ test_connection = test_server_connection_parameters
82
+
83
+ test_imap = Net::IMAP.new(test_connection[:server], test_connection[:connection_options])
84
+ test_imap.login(test_connection[:username], test_connection[:password])
85
+
86
+ message = "From: #{test_connection[:username]}\nSubject: Some Subject\n\nHello!\n"
87
+ response = test_imap.append("INBOX", message, nil, nil)
88
+
89
+ test_imap.examine("INBOX")
90
+ uids = test_imap.uid_search(["ALL"]).sort
91
+
92
+ fetch_data_items = test_imap.uid_fetch(uids, ["BODY[]"])
93
+ ```
data/imap-backup.gemspec CHANGED
@@ -18,18 +18,19 @@ Gem::Specification.new do |gem|
18
18
 
19
19
  gem.executables = gem.files.grep(%r{^bin/}).map { |f| File.basename(f) }
20
20
  gem.require_paths = ["lib"]
21
- gem.required_ruby_version = ">= 3.0"
21
+ gem.required_ruby_version = ">= 3.2"
22
22
 
23
- gem.add_runtime_dependency "highline"
24
- gem.add_runtime_dependency "logger"
25
- gem.add_runtime_dependency "mail", "2.7.1"
26
- gem.add_runtime_dependency "net-imap", ">= 0.3.2"
27
- gem.add_runtime_dependency "net-smtp"
28
- gem.add_runtime_dependency "os"
29
- gem.add_runtime_dependency "ostruct"
30
- gem.add_runtime_dependency "rake"
31
- gem.add_runtime_dependency "thor", "~> 1.1"
32
- gem.add_runtime_dependency "thunderbird", "0.3.0"
23
+ gem.add_dependency "highline"
24
+ gem.add_dependency "logger"
25
+ gem.add_dependency "mail", "2.7.1"
26
+ gem.add_dependency "net-imap", ">= 0.3.2"
27
+ gem.add_dependency "net-smtp"
28
+ gem.add_dependency "os"
29
+ gem.add_dependency "ostruct"
30
+ gem.add_dependency "rake"
31
+ gem.add_dependency "sys-proctable"
32
+ gem.add_dependency "thor", "~> 1.1"
33
+ gem.add_dependency "thunderbird", "0.3.0"
33
34
 
34
35
  gem.metadata = {
35
36
  "rubygems_mfa_required" => "true"
@@ -2,6 +2,7 @@ require "imap/backup/account/backup_folders"
2
2
  require "imap/backup/account/folder_backup"
3
3
  require "imap/backup/account/folder_ensurer"
4
4
  require "imap/backup/account/local_only_folder_deleter"
5
+ require "imap/backup/account/locker"
5
6
 
6
7
  module Imap; end
7
8
 
@@ -10,6 +11,8 @@ module Imap::Backup
10
11
 
11
12
  # Carries out the backup of the configured folders of the account
12
13
  class Account::Backup
14
+ # @param account [Account] the account to back up
15
+ # @param refresh [Boolean] true to refresh folder metadata even if mirror is disabled
13
16
  def initialize(account:, refresh: false)
14
17
  @account = account
15
18
  @refresh = refresh
@@ -22,19 +25,17 @@ module Imap::Backup
22
25
  # start the connection so we get logging messages in the right order
23
26
  account.client.login
24
27
 
25
- run_pre_backup_tasks
26
- backup_folders = Account::BackupFolders.new(
27
- client: account.client, account: account
28
- ).to_a
28
+ ensure_folder
29
+ delete_local_only_folders if account.mirror_mode
30
+
29
31
  if backup_folders.none?
30
32
  Logger.logger.warn "No folders found to backup for account '#{account.username}'"
31
33
  return
32
34
  end
33
- Logger.logger.debug "Starting backup of #{backup_folders.count} folders"
34
- backup_folders.each do |folder|
35
- Account::FolderBackup.new(account: account, folder: folder, refresh: refresh).run
35
+
36
+ locker.with_lock do
37
+ perform_backup
36
38
  end
37
- Logger.logger.debug "Backup of account '#{account.username}' complete"
38
39
  end
39
40
 
40
41
  private
@@ -42,9 +43,30 @@ module Imap::Backup
42
43
  attr_reader :account
43
44
  attr_reader :refresh
44
45
 
45
- def run_pre_backup_tasks
46
+ def backup_folders
47
+ @backup_folders ||= Account::BackupFolders.new(
48
+ client: account.client, account: account
49
+ ).to_a
50
+ end
51
+
52
+ def delete_local_only_folders
53
+ Account::LocalOnlyFolderDeleter.new(account: account).run
54
+ end
55
+
56
+ def ensure_folder
46
57
  Account::FolderEnsurer.new(account: account).run
47
- Account::LocalOnlyFolderDeleter.new(account: account).run if account.mirror_mode
58
+ end
59
+
60
+ def locker
61
+ @locker ||= Account::Locker.new(account: account)
62
+ end
63
+
64
+ def perform_backup
65
+ Logger.logger.debug "Starting backup of #{backup_folders.count} folders"
66
+ backup_folders.each do |folder|
67
+ Account::FolderBackup.new(account: account, folder: folder, refresh: refresh).run
68
+ end
69
+ Logger.logger.debug "Backup of account '#{account.username}' complete"
48
70
  end
49
71
  end
50
72
  end
@@ -9,6 +9,8 @@ module Imap::Backup
9
9
  class Account::BackupFolders
10
10
  include Enumerable
11
11
 
12
+ # @param client [Client::AutomaticLoginWrapper] the IMAP client that lists folders
13
+ # @param account [Account] the account whose folders are being backed up
12
14
  def initialize(client:, account:)
13
15
  @client = client
14
16
  @account = account
@@ -10,6 +10,7 @@ module Imap::Backup
10
10
 
11
11
  # Returns an IMAP client set up for the supplied account
12
12
  class Account::ClientFactory
13
+ # @param account [Account] the account whose credentials configure the client
13
14
  def initialize(account:)
14
15
  @account = account
15
16
  end
@@ -23,6 +23,8 @@ module Imap::Backup
23
23
  # @return [String] the name of the folder
24
24
  attr_reader :name
25
25
 
26
+ # @param client [Client::Default] the IMAP client used to talk to the server
27
+ # @param name [String] the UTF-7 encoded folder name
26
28
  def initialize(client, name)
27
29
  @client = client
28
30
  @name = name
@@ -184,6 +186,13 @@ module Imap::Backup
184
186
  CREATE_RETRY_CLASSES = [::Net::IMAP::BadResponseError].freeze
185
187
  EXAMINE_RETRY_CLASSES = [::Net::IMAP::BadResponseError].freeze
186
188
  PERMITTED_FLAGS = %i(Answered Draft Flagged Seen).freeze
189
+ private_constant :BODY_ATTRIBUTE,
190
+ :UID_FETCH_RETRY_CLASSES,
191
+ :UID_SEARCH_RETRY_CLASSES,
192
+ :APPEND_RETRY_CLASSES,
193
+ :CREATE_RETRY_CLASSES,
194
+ :EXAMINE_RETRY_CLASSES,
195
+ :PERMITTED_FLAGS
187
196
 
188
197
  def examine
189
198
  client.examine(utf7_encoded_name)
@@ -12,6 +12,9 @@ module Imap::Backup
12
12
 
13
13
  # Implements backup for a single folder
14
14
  class Account::FolderBackup
15
+ # @param account [Account] the account that owns the folder
16
+ # @param folder [Account::Folder] the online folder being backed up
17
+ # @param refresh [Boolean] true to refresh metadata regardless of mirror mode
15
18
  def initialize(account:, folder:, refresh: false)
16
19
  @account = account
17
20
  @folder = folder
@@ -8,6 +8,7 @@ module Imap::Backup
8
8
 
9
9
  # Handles creation of directories for backup storage
10
10
  class Account::FolderEnsurer
11
+ # @param account [Account] the account to check
11
12
  def initialize(account:)
12
13
  @account = account
13
14
  end
@@ -11,6 +11,12 @@ module Imap::Backup
11
11
 
12
12
  # Implements a folder enumerator for backed-up accounts
13
13
  class Account::FolderMapper
14
+ # @param account [Account] the account whose local folders are being iterated
15
+ # @param destination [Account] the destination account
16
+ # @param destination_delimiter [String] the delimiter to use for destination folder names
17
+ # @param destination_prefix [String] a prefix applied to destination folder names
18
+ # @param source_delimiter [String] the delimiter used in the source account
19
+ # @param source_prefix [String] a prefix applied to source folder names
14
20
  def initialize(
15
21
  account:,
16
22
  destination:,
@@ -10,6 +10,7 @@ module Imap::Backup
10
10
  # This is used in mirror mode, where local copies are only kept as long as they
11
11
  # exist on the server.
12
12
  class Account::LocalOnlyFolderDeleter
13
+ # @param account [Account] the account whose serialized folders are being pruned
13
14
  def initialize(account:)
14
15
  @account = account
15
16
  end
@@ -0,0 +1,40 @@
1
+ require "imap/backup/account/folder_ensurer"
2
+ require "imap/backup/lockfile"
3
+
4
+ module Imap; end
5
+
6
+ module Imap::Backup
7
+ class Account; end
8
+
9
+ class Account::Locker
10
+ attr_reader :account
11
+
12
+ # @param account [Account] the account whose backup must be locked
13
+ def initialize(account:)
14
+ @account = account
15
+ end
16
+
17
+ def with_lock(&block)
18
+ lockfile = Lockfile.new(path: account.lockfile_path)
19
+ if lockfile.exists?
20
+ if !lockfile.stale?
21
+ raise Lockfile::LockfileExistsError,
22
+ "Lockfile '#{account.lockfile_path}' exists and is not stale."
23
+ end
24
+
25
+ Logger.logger.info("Stale lockfile '#{account.lockfile_path}' found. Removing it.")
26
+ lockfile.remove
27
+ else
28
+ Account::FolderEnsurer.new(account: account).run
29
+ end
30
+
31
+ begin
32
+ lockfile.with_lock do
33
+ block.call
34
+ end
35
+ rescue Lockfile::ProcessStartTimeUnavailableError
36
+ block.call
37
+ end
38
+ end
39
+ end
40
+ end
@@ -1,4 +1,5 @@
1
1
  require "imap/backup/account/folder_mapper"
2
+ require "imap/backup/account/locker"
2
3
  require "imap/backup/uploader"
3
4
 
4
5
  module Imap; end
@@ -8,17 +9,23 @@ module Imap::Backup
8
9
 
9
10
  # Restores all backed up folders to the server
10
11
  class Account::Restore
12
+ # @param account [Account] the account whose backups will be restored
13
+ # @param delimiter [String] the destination folder delimiter
14
+ # @param prefix [String] a prefix applied to restored folder names
11
15
  def initialize(account:, delimiter: "/", prefix: "")
12
16
  @account = account
13
17
  @destination_delimiter = delimiter
14
18
  @destination_prefix = prefix
19
+ @locker = nil
15
20
  end
16
21
 
17
22
  # Runs the restore operation
18
23
  # @return [void]
19
24
  def run
20
- folders.each do |serializer, folder|
21
- Uploader.new(folder, serializer).run
25
+ locker.with_lock do
26
+ folders.each do |serializer, folder|
27
+ Uploader.new(folder, serializer).run
28
+ end
22
29
  end
23
30
  end
24
31
 
@@ -40,5 +47,9 @@ module Imap::Backup
40
47
  def folders
41
48
  Account::FolderMapper.new(**enumerator_options)
42
49
  end
50
+
51
+ def locker
52
+ @locker ||= Account::Locker.new(account: account)
53
+ end
43
54
  end
44
55
  end
@@ -13,6 +13,7 @@ module Imap::Backup
13
13
  class Account::SerializedFolders
14
14
  include Enumerable
15
15
 
16
+ # @param account [Account] the account whose serialized folders are iterated
16
17
  def initialize(account:)
17
18
  @account = account
18
19
  end
@@ -4,6 +4,7 @@ require "imap/backup/account/client_factory"
4
4
 
5
5
  module Imap; end
6
6
 
7
+ # rubocop:disable Metrics/ClassLength
7
8
  module Imap::Backup
8
9
  # Contains the attributes relating to an email account.
9
10
  class Account
@@ -64,6 +65,25 @@ module Imap::Backup
64
65
  # @return [String] one of "active" (the default), "archived", or "offline"
65
66
  attr_reader :status
66
67
 
68
+ # @param options [Hash] Account attributes
69
+ # @option opts [String] :username the username of the account (usually the same as the email
70
+ # address)
71
+ # @option opts [String] :password the password of the account
72
+ # @option opts [String] :local_path the path where backups will be saved
73
+ # @option opts [Array<String>] :folders (nil) the list of folders that have been configured for
74
+ # the Account
75
+ # @option opts [Boolean] :folder_blacklist (false) whether the folders attribute is a blacklist
76
+ # @option opts [Boolean] :mirror_mode (false) whether to run in mirror mode
77
+ # @option opts [String] :server the address of the IMAP server
78
+ # @option opts [Hash] :connection_options (nil) additional connection options for the IMAP
79
+ # server
80
+ # @option opts [String] :download_strategy (nil) the name of the download strategy to adopt
81
+ # during backups
82
+ # @option opts [Integer] :multi_fetch_size (nil) the number of emails to fetch from the IMAP
83
+ # server at a time
84
+ # @option opts [Boolean] :reset_seen_flags_after_fetch (false) whether to reset seen flags after
85
+ # fetching messages
86
+ # @option opts [String] :status ("active") the status of the account
67
87
  def initialize(options)
68
88
  check_options!(options)
69
89
  @username = options[:username]
@@ -130,19 +150,19 @@ module Imap::Backup
130
150
  # @return [Hash] all Account data for serialization
131
151
  def to_h
132
152
  h = {
133
- username: @username,
134
- password: @password,
153
+ username: username,
154
+ password: password,
135
155
  status: status
136
156
  }
137
- h[:local_path] = @local_path if @local_path
138
- h[:folders] = @folders if @folders
139
- h[:folder_blacklist] = true if @folder_blacklist
140
- h[:mirror_mode] = true if @mirror_mode
141
- h[:server] = @server if @server
142
- h[:connection_options] = @connection_options if connection_options
157
+ h[:local_path] = local_path if local_path
158
+ h[:folders] = folders if folders
159
+ h[:folder_blacklist] = true if folder_blacklist
160
+ h[:mirror_mode] = true if mirror_mode
161
+ h[:server] = server if server
162
+ h[:connection_options] = connection_options if connection_options
143
163
  h[:multi_fetch_size] = multi_fetch_size
144
- if @reset_seen_flags_after_fetch
145
- h[:reset_seen_flags_after_fetch] = @reset_seen_flags_after_fetch
164
+ if reset_seen_flags_after_fetch
165
+ h[:reset_seen_flags_after_fetch] = reset_seen_flags_after_fetch
146
166
  end
147
167
  h
148
168
  end
@@ -168,6 +188,14 @@ module Imap::Backup
168
188
  update(:local_path, value)
169
189
  end
170
190
 
191
+ # @raise [RuntimeError] if the local_path is not set
192
+ # @return [String] the path to the lockfile for the account
193
+ def lockfile_path
194
+ raise "local_path is not set" if !local_path
195
+
196
+ File.join(local_path, "imap-backup.lock")
197
+ end
198
+
171
199
  # @raise [RuntimeError] if the supplied value is not an Array
172
200
  # @return [void]
173
201
  def folders=(value)
@@ -207,12 +235,11 @@ module Imap::Backup
207
235
  def connection_options=(value)
208
236
  # Ensure we've loaded the connection_options
209
237
  connection_options
210
- parsed =
211
- if value == ""
212
- nil
213
- else
214
- JSON.parse(value, symbolize_names: true)
215
- end
238
+ parsed = if value == ""
239
+ nil
240
+ else
241
+ JSON.parse(value, symbolize_names: true)
242
+ end
216
243
  update(:connection_options, parsed)
217
244
  end
218
245
 
@@ -283,14 +310,19 @@ module Imap::Backup
283
310
 
284
311
  attr_reader :changes
285
312
 
286
- REQUIRED_ATTRIBUTES = %i[password username].freeze
313
+ REQUIRED_ATTRIBUTES = %i[password username local_path].freeze
287
314
  OPTIONAL_ATTRIBUTES = %i[
288
- connection_options download_strategy folders folder_blacklist local_path mirror_mode
315
+ connection_options download_strategy folders folder_blacklist mirror_mode
289
316
  multi_fetch_size reset_seen_flags_after_fetch server status
290
317
  ].freeze
291
318
  KNOWN_ATTRIBUTES = REQUIRED_ATTRIBUTES + OPTIONAL_ATTRIBUTES
292
319
  VALID_STATUSES = %w[active archived offline].freeze
293
320
  DEFAULT_STATUS = "active".freeze
321
+ private_constant :REQUIRED_ATTRIBUTES,
322
+ :OPTIONAL_ATTRIBUTES,
323
+ :KNOWN_ATTRIBUTES,
324
+ :VALID_STATUSES,
325
+ :DEFAULT_STATUS
294
326
 
295
327
  def check_options!(options)
296
328
  missing_required = REQUIRED_ATTRIBUTES - options.keys
@@ -15,6 +15,10 @@ module Imap::Backup
15
15
  include Thor::Actions
16
16
  include CLI::Helpers
17
17
 
18
+ # @param options [Hash] CLI options controlling output
19
+ # @option opts [String] :config (nil) the path to the configuration file
20
+ # @option opts [String] :erb_configuration (nil) the path to the ERB configuration file
21
+ # @option opts [Boolean] :refresh (false) whether to force refresh of folder metadata
18
22
  def initialize(options)
19
23
  super([])
20
24
  @options = options
@@ -64,6 +68,8 @@ module Imap::Backup
64
68
  case exception
65
69
  when Net::IMAP::NoResponseError, Errno::ECONNREFUSED
66
70
  111
71
+ when Lockfile::LockfileExistsError
72
+ 112
67
73
  else
68
74
  1
69
75
  end
@@ -14,6 +14,11 @@ module Imap::Backup
14
14
  class CLI::Local::Check
15
15
  include CLI::Helpers
16
16
 
17
+ # @param options [Hash] CLI options controlling output
18
+ # @option opts [String] :config (nil) the path to the configuration file
19
+ # @option opts [String] :erb_configuration (nil) the path to the ERB configuration file
20
+ # @option opts [Boolean] :delete_corrupt (false) whether to delete corrupt folders
21
+ # @option opts [String] :format ("text") the output format, either "text" or "json"
17
22
  def initialize(options)
18
23
  @options = options
19
24
  end
@@ -141,6 +141,7 @@ module Imap::Backup
141
141
  private
142
142
 
143
143
  MAX_SUBJECT = 60
144
+ private_constant :MAX_SUBJECT
144
145
 
145
146
  def list_emails_as_json(serializer)
146
147
  emails = serializer.each_message.map do |message|
@@ -70,6 +70,7 @@ module Imap::Backup
70
70
  }
71
71
  ].freeze
72
72
 
73
+ # @param base [Thor] the command class receiving the options
73
74
  def initialize(base:)
74
75
  @base = base
75
76
  end