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
@@ -109,9 +109,12 @@ module Imap::Backup
109
109
  Kernel.puts list.to_json
110
110
  end
111
111
 
112
+ NAMESPACE_TEMPLATE = "%-10<name>s %-10<prefix>s %<delim>s".freeze
113
+ private_constant :NAMESPACE_TEMPLATE
114
+
112
115
  def list_namespaces(namespaces)
113
116
  Kernel.puts format(
114
- "%-10<name>s %-10<prefix>s %<delim>s",
117
+ NAMESPACE_TEMPLATE,
115
118
  {name: "Name", prefix: "Prefix", delim: "Delimiter"}
116
119
  )
117
120
  list_namespace namespaces, :personal
@@ -122,7 +125,7 @@ module Imap::Backup
122
125
  def list_namespace(namespaces, name)
123
126
  info = namespace_info(namespaces.send(name).first, quote: true)
124
127
  if info
125
- Kernel.puts format("%-10<name>s %-10<prefix>s %<delim>s", name: name, **info)
128
+ Kernel.puts format(NAMESPACE_TEMPLATE, name: name, **info)
126
129
  else
127
130
  Kernel.puts format("%-10<name>s (Not defined)", name: name)
128
131
  end
@@ -14,6 +14,13 @@ module Imap::Backup
14
14
  include Thor::Actions
15
15
  include CLI::Helpers
16
16
 
17
+ # @param email [String, nil] optional email address identifying the account to restore
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 [Array<String>] :accounts (nil) the accounts to restore
22
+ # @option opts [String] :delimiter ("/") the destination folder delimiter
23
+ # @option opts [String] :prefix ("") a prefix applied to restored folder names
17
24
  def initialize(email = nil, options)
18
25
  super([])
19
26
  @email = email
@@ -30,9 +37,6 @@ module Imap::Backup
30
37
  when email && !options.key?(:accounts)
31
38
  account = account(config, email)
32
39
  restore(account, **restore_options)
33
- when !email && !options.key?(:accounts)
34
- Logger.logger.info "Calling restore without an EMAIL parameter is deprecated"
35
- config.accounts.each { |a| restore(a) }
36
40
  when email && options.key?(:accounts)
37
41
  raise "Missing EMAIL parameter"
38
42
  when !email && options.key?(:accounts)
@@ -41,6 +45,9 @@ module Imap::Backup
41
45
  "please pass a single EMAIL parameter"
42
46
  )
43
47
  requested_accounts(config).each { |a| restore(a) }
48
+ else
49
+ Logger.logger.info "Calling restore without an EMAIL parameter is deprecated"
50
+ config.accounts.each { |a| restore(a) }
44
51
  end
45
52
  end
46
53
  end
@@ -50,8 +57,8 @@ module Imap::Backup
50
57
  attr_reader :email
51
58
  attr_reader :options
52
59
 
53
- def restore(account, **options)
54
- restore = Account::Restore.new(account: account, **options)
60
+ def restore(account, **)
61
+ restore = Account::Restore.new(account: account, **)
55
62
  restore.run
56
63
  end
57
64
 
@@ -13,6 +13,9 @@ module Imap::Backup
13
13
  include Thor::Actions
14
14
  include CLI::Helpers
15
15
 
16
+ # @param options [Hash] CLI options controlling output
17
+ # @option opts [String] :config (nil) the path to the configuration file
18
+ # @option opts [String] :erb_configuration (nil) the path to the ERB configuration file
16
19
  def initialize(options)
17
20
  super([])
18
21
  @options = options
@@ -12,6 +12,26 @@ module Imap::Backup
12
12
 
13
13
  # Runs a backup without relying on existing configuration
14
14
  class CLI::Single::Backup
15
+ # @param options [Hash] CLI options controlling output
16
+ # @option opts [String] :config (nil) the path to the configuration file
17
+ # @option opts [String] :erb_configuration (nil) the path to the ERB configuration file
18
+ # @option opts [String] :email the email address identifying the account to backup
19
+ # @option opts [String] :password (nil) the password for the account
20
+ # @option opts [String] :password_environment_variable (nil) the name of an environment variable
21
+ # containing the password
22
+ # @option opts [String] :password_file (nil) the path to a file containing the password
23
+ # @option opts [String] :server the IMAP server address
24
+ # @option opts [String] :download_strategy (nil) the download strategy to use, either "delay"
25
+ # or "direct"
26
+ # @option opts [Boolean] :folder_blacklist (false) whether to treat folder list as a blacklist
27
+ # @option opts [Array<String>] :folder ([]) the folders to backup
28
+ # @option opts [String] :path (nil) the local path to store the backup
29
+ # @option opts [Boolean] :mirror (false) whether to run in mirror mode
30
+ # @option opts [Integer] :multi_fetch_size (nil) the number of messages to fetch in a single
31
+ # operation
32
+ # @option opts [Boolean] :refresh (false) whether to force refresh of folder metadata
33
+ # @option opts [Boolean] :reset_seen_flags_after_fetch (false) whether to reset seen flags
34
+ # after fetching messages
15
35
  def initialize(options)
16
36
  @options = options
17
37
  @password = nil
@@ -1,14 +1,24 @@
1
+ require "thor"
2
+
1
3
  require "imap/backup/account/backup_folders"
4
+ require "imap/backup/cli/helpers"
2
5
  require "imap/backup/serializer"
3
6
 
4
7
  module Imap; end
5
8
 
6
9
  module Imap::Backup
10
+ class CLI < Thor; end
11
+
7
12
  # Prints various statistics about an account and its backup
8
13
  class CLI::Stats < Thor
9
14
  include Thor::Actions
10
15
  include CLI::Helpers
11
16
 
17
+ # @param email [String] the email address identifying the account to inspect
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 [String] :format ("text") the output format, either "text" or "json"
12
22
  def initialize(email, options)
13
23
  super([])
14
24
  @email = email
@@ -37,6 +47,7 @@ module Imap::Backup
37
47
  {name: :local, width: 8, alignment: :right}
38
48
  ].freeze
39
49
  ALIGNMENT_FORMAT_SYMBOL = {left: "-", right: " "}.freeze
50
+ private_constant :TEXT_COLUMNS, :ALIGNMENT_FORMAT_SYMBOL
40
51
 
41
52
  attr_reader :email
42
53
  attr_reader :options
@@ -1,4 +1,5 @@
1
1
  require "imap/backup/account/folder_mapper"
2
+ require "imap/backup/account/locker"
2
3
  require "imap/backup/cli/backup"
3
4
  require "imap/backup/cli/helpers"
4
5
  require "imap/backup/logger"
@@ -15,6 +16,9 @@ module Imap::Backup
15
16
  # The possible values for the action parameter
16
17
  ACTIONS = %i(copy migrate mirror).freeze
17
18
 
19
+ # @param action [Symbol] one of ACTIONS describing the transfer mode
20
+ # @param source_email [String] the email of the source account
21
+ # @param destination_email [String] the email of the destination account
18
22
  def initialize(action, source_email, destination_email, options)
19
23
  @action = action
20
24
  @source_email = source_email
@@ -23,9 +27,11 @@ module Imap::Backup
23
27
  @automatic_namespaces = nil
24
28
  @config_path = nil
25
29
  @destination_delimiter = nil
30
+ @destination_locker = nil
26
31
  @destination_prefix = nil
27
32
  @reset = nil
28
33
  @source_delimiter = nil
34
+ @source_locker = nil
29
35
  @source_prefix = nil
30
36
  end
31
37
 
@@ -34,22 +40,19 @@ module Imap::Backup
34
40
  # or the source and destination accounts are the same,
35
41
  # or either of the accounts is not configured,
36
42
  # or incompatible namespace/delimiter parameters have been supplied
43
+ # or one or both of the accounts is locked by another process.
37
44
  # @return [void]
38
45
  def run
39
46
  raise "Unknown action '#{action}'" if !ACTIONS.include?(action)
40
47
 
41
48
  process_options!
42
49
  warn_if_source_account_is_not_in_mirror_mode if action == :mirror
50
+
43
51
  run_backup if %i(copy mirror).include?(action)
44
52
 
45
- folders.each do |serializer, folder|
46
- case action
47
- when :copy
48
- Mirror.new(serializer, folder, reset: false).run
49
- when :migrate
50
- Migrator.new(serializer, folder, reset: reset).run
51
- when :mirror
52
- Mirror.new(serializer, folder, reset: true).run
53
+ source_locker.with_lock do
54
+ destination_locker.with_lock do
55
+ perform_action_on_folders
53
56
  end
54
57
  end
55
58
  end
@@ -68,6 +71,19 @@ module Imap::Backup
68
71
  attr_reader :source_email
69
72
  attr_accessor :source_prefix
70
73
 
74
+ def perform_action_on_folders
75
+ folders.each do |serializer, folder|
76
+ case action
77
+ when :copy
78
+ Mirror.new(serializer, folder, reset: false).run
79
+ when :migrate
80
+ Migrator.new(serializer, folder, reset: reset).run
81
+ when :mirror
82
+ Mirror.new(serializer, folder, reset: true).run
83
+ end
84
+ end
85
+ end
86
+
71
87
  def process_options!
72
88
  self.automatic_namespaces = options[:automatic_namespaces] || false
73
89
  self.config_path = options[:config]
@@ -175,5 +191,13 @@ module Imap::Backup
175
191
  def source_account
176
192
  config.accounts.find { |a| a.username == source_email }
177
193
  end
194
+
195
+ def source_locker
196
+ @source_locker ||= Account::Locker.new(account: source_account)
197
+ end
198
+
199
+ def destination_locker
200
+ @destination_locker ||= Account::Locker.new(account: destination_account)
201
+ end
178
202
  end
179
203
  end
@@ -33,15 +33,10 @@ module Imap::Backup
33
33
  Logger.setup_logging options
34
34
  config = load_config(**options)
35
35
  account = account(config, email)
36
+ locker ||= Account::Locker.new(account: account)
36
37
 
37
- backup_folders = Account::BackupFolders.new(
38
- client: account.client, account: account
39
- )
40
- backup_folders.each do |folder|
41
- next if !folder.exist?
42
-
43
- serializer = Serializer.new(account.local_path, folder.name)
44
- do_ignore_folder_history(folder, serializer)
38
+ locker.with_lock do
39
+ ignore_account_history(account)
45
40
  end
46
41
  end
47
42
 
@@ -99,8 +94,21 @@ module Imap::Backup
99
94
  private
100
95
 
101
96
  FAKE_EMAIL = "fake@email.com".freeze
97
+ private_constant :FAKE_EMAIL
98
+
99
+ def ignore_account_history(account)
100
+ backup_folders = Account::BackupFolders.new(
101
+ client: account.client, account: account
102
+ )
103
+ backup_folders.each do |folder|
104
+ next if !folder.exist?
105
+
106
+ serializer = Serializer.new(account.local_path, folder.name)
107
+ ignore_folder_history(folder, serializer)
108
+ end
109
+ end
102
110
 
103
- def do_ignore_folder_history(folder, serializer)
111
+ def ignore_folder_history(folder, serializer)
104
112
  uids = folder.uids - serializer.uids
105
113
  Logger.logger.info "Folder '#{folder.name}' - #{uids.length} messages"
106
114
 
@@ -15,6 +15,7 @@ module Imap::Backup
15
15
  # @return [Client]
16
16
  attr_reader :client
17
17
 
18
+ # @param client [Client::Default] the client to wrap and lazily log in
18
19
  def initialize(client:)
19
20
  @client = client
20
21
  @login_called = false
@@ -13,11 +13,13 @@ module Imap::Backup
13
13
  # Tracks the latest folder selection in order to avoid repeated calls
14
14
  class Client::Default
15
15
  extend Forwardable
16
+
16
17
  def_delegators :imap, *%i(
17
18
  append authenticate capability create expunge namespace
18
19
  responses uid_fetch uid_search uid_store
19
20
  )
20
21
 
22
+ # @param account [Account] the account whose server is accessed
21
23
  def initialize(account)
22
24
  @account = account
23
25
  @state = nil
@@ -32,6 +32,7 @@ module Imap::Backup
32
32
  File.exist?(path || default_pathname)
33
33
  end
34
34
 
35
+ # @param path [String, nil] optional path to the configuration file
35
36
  def initialize(path: nil)
36
37
  @pathname = path || self.class.default_pathname
37
38
  @download_strategy = nil
@@ -68,7 +69,10 @@ module Imap::Backup
68
69
  ensure_loaded!
69
70
  accounts = data[:accounts].map do |attr|
70
71
  Account.new(attr)
71
- end
72
+ rescue ArgumentError => e
73
+ Logger.logger.error("Skipping invalid account in config: #{e.message}")
74
+ nil
75
+ end.compact
72
76
  inject_global_attributes(accounts)
73
77
  end
74
78
  end
@@ -108,8 +112,6 @@ module Imap::Backup
108
112
 
109
113
  private
110
114
 
111
- VERSION_2_1 = "2.1".freeze
112
-
113
115
  attr_reader :pathname
114
116
 
115
117
  def ensure_loaded!
@@ -8,6 +8,10 @@ module Imap::Backup
8
8
  # @private
9
9
  class MultiFetchFailedError < StandardError; end
10
10
 
11
+ # @param folder [Account::Folder] the remote folder to download from
12
+ # @param serializer [Serializer] the local serializer that stores messages
13
+ # @param multi_fetch_size [Integer] how many messages to fetch per batch
14
+ # @param reset_seen_flags_after_fetch [Boolean] true to restore unseen flags after fetch
11
15
  def initialize(folder, serializer, multi_fetch_size: 1, reset_seen_flags_after_fetch: false)
12
16
  @folder = folder
13
17
  @serializer = serializer
@@ -35,6 +35,7 @@ module Imap::Backup
35
35
  # @return [String] the original message body
36
36
  attr_reader :supplied_body
37
37
 
38
+ # @param supplied_body [String] the original RFC 2822 message text
38
39
  def initialize(supplied_body)
39
40
  @supplied_body = supplied_body.clone
40
41
  end
@@ -3,6 +3,7 @@ module Imap; end
3
3
  module Imap::Backup
4
4
  # Accesses a file's access permissions
5
5
  class FileMode
6
+ # @param filename [String] the file whose permissions will be checked
6
7
  def initialize(filename:)
7
8
  @filename = filename
8
9
  end
@@ -6,6 +6,8 @@ module Imap::Backup
6
6
  # The number of messages to process at a time
7
7
  CHUNK_SIZE = 100
8
8
 
9
+ # @param folder [Account::Folder] the remote folder whose flags are refreshed
10
+ # @param serializer [Serializer] the local serializer providing stored UIDs
9
11
  def initialize(folder, serializer)
10
12
  @folder = folder
11
13
  @serializer = serializer
@@ -5,6 +5,8 @@ module Imap; end
5
5
  module Imap::Backup
6
6
  # Deletes locally backed-up emails that are no longer on the server
7
7
  class LocalOnlyMessageDeleter
8
+ # @param folder [Account::Folder] the remote folder used to detect missing UIDs
9
+ # @param serializer [Serializer] the local serializer that may contain stale messages
8
10
  def initialize(folder, serializer)
9
11
  @folder = folder
10
12
  @serializer = serializer
@@ -0,0 +1,94 @@
1
+ require "sys/proctable"
2
+ require "json"
3
+
4
+ module Imap; end
5
+
6
+ module Imap::Backup
7
+ class Lockfile
8
+ # An error that is thrown if a lockfile already exists
9
+ class LockfileExistsError < StandardError; end
10
+ class ProcessStartTimeUnavailableError < StandardError; end
11
+
12
+ attr_reader :path
13
+
14
+ # Initializes a new Lockfile instance.
15
+ # @param path [String] the path to the lockfile
16
+ def initialize(path:)
17
+ @path = path
18
+ end
19
+
20
+ # Creates the lockfile, yields to the given block
21
+ # and ensures the lockfile is removed afterwards.
22
+ def with_lock(&block)
23
+ raise LockfileExistsError, "Lockfile already exists at #{path}" if exists?
24
+
25
+ begin
26
+ create
27
+ block.call
28
+ ensure
29
+ remove
30
+ end
31
+ end
32
+
33
+ # Checks if the lockfile exists.
34
+ # @return [Boolean] true if the lockfile exists, false otherwise
35
+ def exists?
36
+ File.exist?(path)
37
+ end
38
+
39
+ # Removes the lockfile.
40
+ def remove
41
+ FileUtils.rm_f(path)
42
+ end
43
+
44
+ # Checks if the lockfile is stale (i.e., the process that created it is no longer running).
45
+ # @return [Boolean] true if the lockfile is stale, false otherwise
46
+ def stale?
47
+ return false if !exists?
48
+
49
+ file_content = File.read(path)
50
+ data = JSON.parse(file_content, symbolize_names: true)
51
+ pid = data[:pid]
52
+ starttime = data[:starttime]
53
+ proc_table_entry = Sys::ProcTable.ps(pid: pid)
54
+
55
+ return true if proc_table_entry.nil?
56
+
57
+ other_starttime = starttime(proc_table_entry)
58
+
59
+ other_starttime != starttime
60
+ end
61
+
62
+ private
63
+
64
+ def create
65
+ pid = Process.pid
66
+ proc_table_entry = Sys::ProcTable.ps(pid: pid)
67
+
68
+ if proc_table_entry.nil?
69
+ raise ProcessStartTimeUnavailableError, "Unable to get process info for PID #{pid}"
70
+ end
71
+
72
+ starttime = starttime(proc_table_entry)
73
+
74
+ data = {
75
+ pid: pid,
76
+ starttime: starttime
77
+ }
78
+
79
+ json_data = JSON.generate(data)
80
+ File.write(path, json_data)
81
+ end
82
+
83
+ def starttime(proc_table_entry)
84
+ case
85
+ when proc_table_entry.respond_to?(:starttime)
86
+ proc_table_entry.starttime
87
+ when proc_table_entry.respond_to?(:start_tvsec)
88
+ proc_table_entry.start_tvsec
89
+ else
90
+ raise ProcessStartTimeUnavailableError, "Proctable entry structure unknown"
91
+ end
92
+ end
93
+ end
94
+ end
@@ -49,7 +49,7 @@ module Imap::Backup
49
49
  end
50
50
 
51
51
  # Wraps a block, filtering output to standard error,
52
- # hidng passwords and outputs the results to standard out
52
+ # hiding passwords, and outputs the results to standard out
53
53
  # @return [void]
54
54
  def self.sanitize_stderr(&block)
55
55
  sanitizer = Text::Sanitizer.new($stdout)
@@ -5,6 +5,9 @@ module Imap; end
5
5
  module Imap::Backup
6
6
  # Copies a folder of backed-up emails to an online folder
7
7
  class Migrator
8
+ # @param serializer [Serializer] the local folder to migrate
9
+ # @param folder [Account::Folder] the destination folder on the server
10
+ # @param reset [Boolean] true to clear the destination before uploading
8
11
  def initialize(serializer, folder, reset: false)
9
12
  @folder = folder
10
13
  @reset = reset
@@ -7,6 +7,8 @@ module Imap::Backup
7
7
 
8
8
  # Keeps track of the mapping between source and destination UIDs
9
9
  class Mirror::Map
10
+ # @param pathname [String] the path to the on-disk UID map
11
+ # @param destination [String] the destination account identifier
10
12
  def initialize(pathname:, destination:)
11
13
  @pathname = pathname
12
14
  @destination = destination
@@ -18,7 +20,6 @@ module Imap::Backup
18
20
  end
19
21
 
20
22
  # @return [Boolean] whether the supplied values match the existing
21
- # UID validity values
22
23
  def check_uid_validities(source:, destination:)
23
24
  store
24
25
  return false if source != source_uid_validity
@@ -5,16 +5,19 @@ module Imap; end
5
5
  module Imap::Backup
6
6
  # Synchronises a folder between a source and destination
7
7
  class Mirror
8
+ # @param serializer [Serializer] the source of backed-up messages
9
+ # @param folder [Account::Folder] the destination folder to mirror into
10
+ # @param reset [Boolean] true to delete destination-only messages first
8
11
  def initialize(serializer, folder, reset: false)
9
12
  @serializer = serializer
10
13
  @folder = folder
11
14
  @reset = reset
12
15
  end
13
16
 
14
- # If necessary, reates the destination folder,
17
+ # If necessary, creates the destination folder,
15
18
  # then deletes any messages in the destination folder
16
19
  # that are not in the local store,
17
- # sets existing messages' flas
20
+ # sets existing messages' flags
18
21
  # then appends any missing messages
19
22
  # and saves the mapping file
20
23
  # @return [void]
@@ -29,6 +32,7 @@ module Imap::Backup
29
32
  private
30
33
 
31
34
  CHUNK_SIZE = 100
35
+ private_constant :CHUNK_SIZE
32
36
 
33
37
  attr_reader :serializer
34
38
  attr_reader :folder
@@ -109,7 +109,7 @@ module Imap::Backup
109
109
  # Sets the mailbox file's updated time to the current time
110
110
  # @return [void]
111
111
  def touch
112
- File.open(pathname, "a") {}
112
+ FileUtils.touch(pathname)
113
113
  end
114
114
 
115
115
  private
@@ -12,7 +12,7 @@ module Imap::Backup
12
12
  # @return [Array[Symbol]] the message's flags
13
13
  attr_accessor :flags
14
14
  # @return [Integer] the length of the message (as stored on disk)
15
- attr_reader :length
15
+ attr_accessor :length
16
16
  # @return [Integer] the start of the message inside the mailbox file
17
17
  attr_reader :offset
18
18
  # @return [Integer] the message's UID
@@ -22,7 +22,7 @@ module Imap::Backup
22
22
  return nil if actual.nil?
23
23
 
24
24
  mask = ~limit & 0o777
25
- return if (actual & mask).zero?
25
+ return if actual.nobits?(mask)
26
26
 
27
27
  message = format(
28
28
  "Permissions on '%<filename>s' " \
@@ -101,7 +101,7 @@ module Imap::Backup
101
101
  end
102
102
  end
103
103
 
104
- next if lines.count.zero?
104
+ next if lines.none?
105
105
 
106
106
  message = {
107
107
  uid: uids[messages.count],
@@ -56,6 +56,7 @@ module Imap::Backup
56
56
  private
57
57
 
58
58
  EMAIL_MATCHER = /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]+$/i
59
+ private_constant :EMAIL_MATCHER
59
60
 
60
61
  attr_reader :highline
61
62
  end
@@ -91,6 +91,7 @@ module Imap::Backup
91
91
  Imap::Backup::Account.new(
92
92
  username: username,
93
93
  password: "",
94
+ local_path: nil,
94
95
  folders: []
95
96
  ).tap do |a|
96
97
  provider = Imap::Backup::Email::Provider.for_address(username)
@@ -51,6 +51,7 @@ module Imap::Backup
51
51
  private
52
52
 
53
53
  EXPORT_PREFIX = "imap-backup".freeze
54
+ private_constant :EXPORT_PREFIX
54
55
 
55
56
  attr_reader :email
56
57
  attr_reader :serializer
@@ -4,9 +4,9 @@ module Imap::Backup
4
4
  # @private
5
5
  MAJOR = 16
6
6
  # @private
7
- MINOR = 3
7
+ MINOR = 4
8
8
  # @private
9
- REVISION = 0
9
+ REVISION = 1
10
10
  # @private
11
11
  PRE = nil
12
12
  # The application version