imap-backup 4.1.2 → 5.0.0

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 (34) hide show
  1. checksums.yaml +4 -4
  2. data/imap-backup.gemspec +1 -0
  3. data/lib/email/mboxrd/message.rb +6 -2
  4. data/lib/imap/backup/account/connection.rb +4 -4
  5. data/lib/imap/backup/account/folder.rb +8 -0
  6. data/lib/imap/backup/cli/folders.rb +1 -1
  7. data/lib/imap/backup/cli/helpers.rb +4 -1
  8. data/lib/imap/backup/cli/local.rb +1 -1
  9. data/lib/imap/backup/cli/migrate.rb +114 -0
  10. data/lib/imap/backup/cli/remote.rb +15 -0
  11. data/lib/imap/backup/cli.rb +50 -4
  12. data/lib/imap/backup/client/default.rb +2 -2
  13. data/lib/imap/backup/downloader.rb +23 -9
  14. data/lib/imap/backup/migrator.rb +51 -0
  15. data/lib/imap/backup/sanitizer.rb +7 -4
  16. data/lib/imap/backup/thunderbird/mailbox_exporter.rb +10 -1
  17. data/lib/imap/backup/version.rb +3 -3
  18. data/lib/thunderbird/subdirectory.rb +3 -6
  19. data/spec/features/configuration/minimal_configuration.rb +15 -0
  20. data/spec/features/configuration/missing_configuration.rb +14 -0
  21. data/spec/features/folders_spec.rb +36 -0
  22. data/spec/features/local/list_accounts_spec.rb +12 -0
  23. data/spec/features/local/list_emails_spec.rb +21 -0
  24. data/spec/features/local/list_folders_spec.rb +21 -0
  25. data/spec/features/local/show_an_email_spec.rb +34 -0
  26. data/spec/features/migrate_spec.rb +35 -0
  27. data/spec/features/remote/list_account_folders_spec.rb +16 -0
  28. data/spec/features/support/aruba.rb +69 -0
  29. data/spec/features/support/email_server.rb +1 -0
  30. data/spec/unit/imap/backup/account/connection_spec.rb +6 -8
  31. data/spec/unit/imap/backup/account/folder_spec.rb +26 -5
  32. data/spec/unit/imap/backup/cli/helpers_spec.rb +87 -0
  33. data/spec/unit/imap/backup/migrator_spec.rb +58 -0
  34. metadata +77 -36
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 00a744249ff80a26be2c57655709be59d9678c9bb998dc1c9f3ea4bf3539d747
4
- data.tar.gz: 4f44c050c66fe2ef2e136b46dec581a91f77ab4b650e9b3466a893540e481a24
3
+ metadata.gz: 16911b1cea1983b844c3bbf7f74c03ff7436990fb5aa4d5b49fa18f03ae2e229
4
+ data.tar.gz: 265f63995e70ba3f68b98ec72d3a12b3e95100f7e6b320026b18bcc156888a92
5
5
  SHA512:
6
- metadata.gz: 66067924fda33810aff4b02aa047c20cd26ce3fc31d8e752fc43a8f78af3e1c952871fa0ba372c670a5ee399e7fc686f9df9601fc5cbb7caa6f2065b4ac95b90
7
- data.tar.gz: 443a86c3bda9cc1652fc9d1c5619347c1d82498befed16851a6f8a227eaf3299813dd6a4084e1bb0d180170adb1b63ccf8e5d2a8bb19f0457611e7916d796c4d
6
+ metadata.gz: 4a541a7830ddab458f2366bdad4924f3e6cbc3ba858651839e6dcd1a530acbbeb01e2c59a44331af73a2d77f606f2d6b39eacd2b5a705d78ec20e93d2c2f2527
7
+ data.tar.gz: 7b60170e3d29e9882f3bcbf13b04daa162434944d59311a5a03a4b1c1022a7a9501b292c3eb929d6e07039c47cc433316a338e062ced4e8a6b7ea424e6c0678b
data/imap-backup.gemspec CHANGED
@@ -28,6 +28,7 @@ Gem::Specification.new do |gem|
28
28
  gem.add_runtime_dependency "rake"
29
29
  gem.add_runtime_dependency "thor", "~> 1.1"
30
30
 
31
+ gem.add_development_dependency "aruba", ">= 0.0.0"
31
32
  gem.add_development_dependency "codeclimate-test-reporter", "~> 0.4.8"
32
33
  if RUBY_ENGINE == "jruby"
33
34
  gem.add_development_dependency "pry-debugger-jruby"
@@ -10,7 +10,7 @@ module Email::Mboxrd
10
10
 
11
11
  attr_reader :supplied_body
12
12
 
13
- def self.from_serialized(serialized)
13
+ def self.clean_serialized(serialized)
14
14
  cleaned = serialized.gsub(/^>(>*From)/, "\\1")
15
15
  # Serialized messages in this format *should* start with a line
16
16
  # From xxx yy zz
@@ -19,7 +19,11 @@ module Email::Mboxrd
19
19
  cleaned = cleaned.sub(/^From .*[\r\n]*/, "")
20
20
  end
21
21
  # rubocop:enable Style/IfUnlessModifier
22
- new(cleaned)
22
+ cleaned
23
+ end
24
+
25
+ def self.from_serialized(serialized)
26
+ new(clean_serialized(serialized))
23
27
  end
24
28
 
25
29
  def initialize(supplied_body)
@@ -16,11 +16,8 @@ module Imap::Backup
16
16
  def initialize(account)
17
17
  @account = account
18
18
  reset
19
- create_account_folder
20
19
  end
21
20
 
22
- # TODO: Make this private once the 'folders' command
23
- # has been removed.
24
21
  def folder_names
25
22
  @folder_names ||=
26
23
  begin
@@ -53,6 +50,7 @@ module Imap::Backup
53
50
  end
54
51
 
55
52
  def status
53
+ ensure_account_folder
56
54
  backup_folders.map do |folder|
57
55
  s = Serializer::Mbox.new(account.local_path, folder.name)
58
56
  {name: folder.name, local: s.uids, remote: folder.uids}
@@ -63,6 +61,7 @@ module Imap::Backup
63
61
  Imap::Backup::Logger.logger.debug "Running backup of account: #{account.username}"
64
62
  # start the connection so we get logging messages in the right order
65
63
  client
64
+ ensure_account_folder
66
65
  each_folder do |folder, serializer|
67
66
  next if !folder.exist?
68
67
 
@@ -82,6 +81,7 @@ module Imap::Backup
82
81
  def local_folders
83
82
  return enum_for(:local_folders) if !block_given?
84
83
 
84
+ ensure_account_folder
85
85
  glob = File.join(account.local_path, "**", "*.imap")
86
86
  base = Pathname.new(account.local_path)
87
87
  Pathname.glob(glob) do |path|
@@ -176,7 +176,7 @@ module Imap::Backup
176
176
  end
177
177
  end
178
178
 
179
- def create_account_folder
179
+ def ensure_account_folder
180
180
  Utils.make_folder(
181
181
  File.dirname(account.local_path),
182
182
  File.basename(account.local_path),
@@ -111,6 +111,14 @@ module Imap::Backup
111
111
  extract_uid(response)
112
112
  end
113
113
 
114
+ def clear
115
+ existing = uids
116
+ # Use read-write access, via `select`
117
+ client.select(utf7_encoded_name)
118
+ client.uid_store(existing, "+FLAGS", [:Deleted])
119
+ client.expunge
120
+ end
121
+
114
122
  private
115
123
 
116
124
  def examine
@@ -13,7 +13,7 @@ module Imap::Backup
13
13
  no_commands do
14
14
  def run
15
15
  each_connection(account_names) do |connection|
16
- puts connection.username
16
+ 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
@@ -3,7 +3,10 @@ require "imap/backup/cli/accounts"
3
3
 
4
4
  module Imap::Backup::CLI::Helpers
5
5
  def symbolized(options)
6
- options.each.with_object({}) { |(k, v), acc| acc[k.intern] = v }
6
+ options.each.with_object({}) do |(k, v), acc|
7
+ key = k.gsub("-", "_").intern
8
+ acc[key] = v
9
+ end
7
10
  end
8
11
 
9
12
  def account(email)
@@ -11,7 +11,7 @@ module Imap::Backup
11
11
  accounts.each { |a| Kernel.puts a.username }
12
12
  end
13
13
 
14
- desc "folders EMAIL", "List account folders"
14
+ desc "folders EMAIL", "List backed up folders"
15
15
  def folders(email)
16
16
  connection = connection(email)
17
17
 
@@ -0,0 +1,114 @@
1
+ require "imap/backup/migrator"
2
+
3
+ module Imap::Backup
4
+ class CLI::Migrate < Thor
5
+ include Thor::Actions
6
+
7
+ attr_reader :destination_email
8
+ attr_reader :destination_prefix
9
+ attr_reader :reset
10
+ attr_reader :source_email
11
+ attr_reader :source_prefix
12
+
13
+ def initialize(
14
+ source_email,
15
+ destination_email,
16
+ destination_prefix: "",
17
+ reset: false,
18
+ source_prefix: ""
19
+ )
20
+ super([])
21
+ @destination_email = destination_email
22
+ @destination_prefix = destination_prefix
23
+ @reset = reset
24
+ @source_email = source_email
25
+ @source_prefix = source_prefix
26
+ end
27
+
28
+ no_commands do
29
+ def run
30
+ check_accounts!
31
+ folders.each do |serializer, folder|
32
+ Migrator.new(serializer, folder, reset: reset).run
33
+ end
34
+ end
35
+
36
+ def check_accounts!
37
+ if destination_email == source_email
38
+ raise "Source and destination accounts cannot be the same!"
39
+ end
40
+
41
+ if !destination_account
42
+ raise "Account #{destination_email} does not exist"
43
+ end
44
+
45
+ if !source_account
46
+ raise "Account #{source_email} does not exist"
47
+ end
48
+ end
49
+
50
+ def config
51
+ Configuration.new
52
+ end
53
+
54
+ def destination_account
55
+ config.accounts.find { |a| a.username == destination_email }
56
+ end
57
+
58
+ def folders
59
+ return enum_for(:folders) if !block_given?
60
+
61
+ glob = File.join(source_local_path, "**", "*.imap")
62
+ Pathname.glob(glob) do |path|
63
+ name = source_folder_name(path)
64
+ serializer = Serializer::Mbox.new(source_local_path, name)
65
+ folder = folder_for(name)
66
+ yield serializer, folder
67
+ end
68
+ end
69
+
70
+ def folder_for(source_folder)
71
+ no_source_prefix =
72
+ if source_prefix != "" &&
73
+ source_folder.start_with?(source_prefix)
74
+ source_folder.delete_prefix(source_prefix)
75
+ else
76
+ source_folder.to_s
77
+ end
78
+
79
+ with_destination_prefix =
80
+ if destination_prefix &&
81
+ destination_prefix != ""
82
+ destination_prefix + no_source_prefix
83
+ else
84
+ no_source_prefix
85
+ end
86
+
87
+ Account::Folder.new(
88
+ destination_account.connection,
89
+ with_destination_prefix
90
+ )
91
+ end
92
+
93
+ def source_local_path
94
+ source_account.local_path
95
+ end
96
+
97
+ def source_account
98
+ config.accounts.find { |a| a.username == source_email }
99
+ end
100
+
101
+ def source_folder_name(imap_pathname)
102
+ base = Pathname.new(source_local_path)
103
+ imap_name = imap_pathname.relative_path_from(base).to_s
104
+ dir = File.dirname(imap_name)
105
+ stripped = File.basename(imap_name, ".imap")
106
+ if dir == "."
107
+ stripped
108
+ else
109
+ File.join(dir, stripped)
110
+ end
111
+ end
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,15 @@
1
+ module Imap::Backup
2
+ class CLI::Remote < Thor
3
+ include Thor::Actions
4
+ include CLI::Helpers
5
+
6
+ desc "folders EMAIL", "List account folders"
7
+ def folders(email)
8
+ connection = connection(email)
9
+
10
+ connection.folder_names.each do |name|
11
+ Kernel.puts %("#{name}")
12
+ end
13
+ end
14
+ end
15
+ end
@@ -9,6 +9,7 @@ module Imap::Backup
9
9
  autoload :Backup, "imap/backup/cli/backup"
10
10
  autoload :Folders, "imap/backup/cli/folders"
11
11
  autoload :Local, "imap/backup/cli/local"
12
+ autoload :Migrate, "imap/backup/cli/migrate"
12
13
  autoload :Remote, "imap/backup/cli/remote"
13
14
  autoload :Restore, "imap/backup/cli/restore"
14
15
  autoload :Setup, "imap/backup/cli/setup"
@@ -27,7 +28,7 @@ module Imap::Backup
27
28
  method_option(
28
29
  "accounts",
29
30
  type: :string,
30
- banner: "a comma-separated list of accounts (defaults to all configured accounts)",
31
+ desc: "a comma-separated list of accounts (defaults to all configured accounts)",
31
32
  aliases: ["-a"]
32
33
  )
33
34
  end
@@ -57,6 +58,54 @@ module Imap::Backup
57
58
  Folders.new(symbolized(options)).run
58
59
  end
59
60
 
61
+ desc "local SUBCOMMAND [OPTIONS]", "View local info"
62
+ subcommand "local", Local
63
+
64
+ desc "migrate SOURCE_EMAIL DESTINATION_EMAIL [OPTIONS]",
65
+ "[Experimental] Uploads backed-up emails from account SOURCE_EMAIL to account DESTINATION_EMAIL"
66
+ long_desc <<~DESC
67
+ All emails which have been backed up for the "source account" (SOURCE_EMAIL) are
68
+ uploaded to the "destination account" (DESTINATION_EMAIL).
69
+
70
+ When one or other account has namespaces (i.e. prefixes like "INBOX."),
71
+ use the `--source-prefix=` and/or `--destination-prefix=` options.
72
+
73
+ Usually, you should migrate to an account with empty folders.
74
+
75
+ Before migrating each folder, `imap-backup` checks if the destination
76
+ folder is empty.
77
+
78
+ If it finds a non-empty destination folder, it halts with an error.
79
+
80
+ If you are sure that these destination emails can be deleted,
81
+ use the `--reset` option. In this case, all existing emails are
82
+ deleted before uploading the migrated emails.
83
+ DESC
84
+ method_option(
85
+ "destination-prefix",
86
+ type: :string,
87
+ desc: "the prefix (namespace) to add to destination folder names",
88
+ aliases: ["-d"]
89
+ )
90
+ method_option(
91
+ "reset",
92
+ type: :boolean,
93
+ desc: "DANGER! This option deletes all messages from destination folders before uploading",
94
+ aliases: ["-r"]
95
+ )
96
+ method_option(
97
+ "source-prefix",
98
+ type: :string,
99
+ desc: "the prefix (namespace) to strip from source folder names",
100
+ aliases: ["-s"]
101
+ )
102
+ def migrate(source_email, destination_email)
103
+ Migrate.new(source_email, destination_email, symbolized(options)).run
104
+ end
105
+
106
+ desc "remote SUBCOMMAND [OPTIONS]", "View info about online accounts"
107
+ subcommand "remote", Remote
108
+
60
109
  desc "restore [OPTIONS]", "This command is deprecated, use `imap-backup restore ACCOUNT`"
61
110
  long_desc <<~DESC
62
111
  By default, restores all local emails to their respective servers.
@@ -86,9 +135,6 @@ module Imap::Backup
86
135
  Status.new(symbolized(options)).run
87
136
  end
88
137
 
89
- desc "local SUBCOMMAND [OPTIONS]", "View local info"
90
- subcommand "local", Local
91
-
92
138
  desc "utils SUBCOMMAND [OPTIONS]", "Various utilities"
93
139
  subcommand "utils", Utils
94
140
  end
@@ -7,8 +7,8 @@ module Imap::Backup
7
7
  class Client::Default
8
8
  extend Forwardable
9
9
  def_delegators :imap, *%i(
10
- append authenticate create disconnect examine
11
- login responses uid_fetch uid_search
10
+ append authenticate create disconnect examine expunge
11
+ login responses select uid_fetch uid_search uid_store
12
12
  )
13
13
 
14
14
  attr_reader :args
@@ -13,31 +13,45 @@ module Imap::Backup
13
13
  def run
14
14
  uids = folder.uids - serializer.uids
15
15
  count = uids.count
16
- Imap::Backup::Logger.logger.debug "[#{folder.name}] #{count} new messages"
16
+ debug "#{count} new messages"
17
17
  uids.each_slice(block_size).with_index do |block, i|
18
- offset = i * block_size + 1
19
18
  uids_and_bodies = folder.fetch_multi(block)
20
19
  if uids_and_bodies.nil?
21
20
  if block_size > 1
22
- Imap::Backup::Logger.logger.debug("[#{folder.name}] Multi fetch failed for UIDs #{block.join(", ")}, switching to single fetches")
21
+ debug("Multi fetch failed for UIDs #{block.join(", ")}, switching to single fetches")
23
22
  @block_size = 1
24
23
  redo
25
24
  else
26
- Imap::Backup::Logger.logger.debug("[#{folder.name}] Fetch failed for UID #{block[0]} - skipping")
25
+ debug("Fetch failed for UID #{block[0]} - skipping")
27
26
  next
28
27
  end
29
28
  end
30
29
 
30
+ offset = i * block_size + 1
31
31
  uids_and_bodies.each.with_index do |uid_and_body, j|
32
32
  uid = uid_and_body[:uid]
33
33
  body = uid_and_body[:body]
34
- Imap::Backup::Logger.logger.debug(
35
- "[#{folder.name}] uid: #{uid} (#{offset +j}/#{count}) - " \
36
- "#{body.size} bytes"
37
- )
38
- serializer.save(uid, body)
34
+ case
35
+ when !body
36
+ info("Fetch returned empty body - skipping")
37
+ when !uid
38
+ info("Fetch returned empty UID - skipping")
39
+ else
40
+ debug("uid: #{uid} (#{offset + j}/#{count}) - #{body.size} bytes")
41
+ serializer.save(uid, body)
42
+ end
39
43
  end
40
44
  end
41
45
  end
46
+
47
+ private
48
+
49
+ def info(message)
50
+ Imap::Backup::Logger.logger.info("[#{folder.name}] #{message}")
51
+ end
52
+
53
+ def debug(message)
54
+ Imap::Backup::Logger.logger.debug("[#{folder.name}] #{message}")
55
+ end
42
56
  end
43
57
  end
@@ -0,0 +1,51 @@
1
+ module Imap::Backup
2
+ class Migrator
3
+ attr_reader :folder
4
+ attr_reader :reset
5
+ attr_reader :serializer
6
+
7
+ def initialize(serializer, folder, reset: false)
8
+ @folder = folder
9
+ @reset = reset
10
+ @serializer = serializer
11
+ end
12
+
13
+ def run
14
+ count = serializer.uids.count
15
+ folder.create
16
+ ensure_destination_empty!
17
+
18
+ Imap::Backup::Logger.logger.debug "[#{folder.name}] #{count} to migrate"
19
+ serializer.each_message(serializer.uids).with_index do |(uid, message), i|
20
+ next if message.nil?
21
+
22
+ log_prefix = "[#{folder.name}] uid: #{uid} (#{i + 1}/#{count}) -"
23
+ Imap::Backup::Logger.logger.debug(
24
+ "#{log_prefix} #{message.supplied_body.size} bytes"
25
+ )
26
+
27
+ folder.append(message)
28
+ end
29
+ end
30
+
31
+ private
32
+
33
+ def ensure_destination_empty!
34
+ if reset
35
+ folder.clear
36
+ else
37
+ fail_if_destination_not_empty!
38
+ end
39
+ end
40
+
41
+ def fail_if_destination_not_empty!
42
+ return if folder.uids.empty?
43
+
44
+ raise <<~ERROR
45
+ The destination folder '#{folder.name}' is not empty.
46
+ Pass the --reset flag if you want to clear existing emails from destination
47
+ folders before uploading.
48
+ ERROR
49
+ end
50
+ end
51
+ end
@@ -1,16 +1,19 @@
1
+ require "forwardable"
2
+
1
3
  module Imap::Backup
2
4
  class Sanitizer
5
+ extend Forwardable
6
+
3
7
  attr_reader :output
4
8
 
9
+ delegate puts: :output
10
+ delegate write: :output
11
+
5
12
  def initialize(output)
6
13
  @output = output
7
14
  @current = ""
8
15
  end
9
16
 
10
- def write(*args)
11
- output.write(*args)
12
- end
13
-
14
17
  def print(*args)
15
18
  @current << args.join
16
19
  loop do
@@ -40,7 +40,16 @@ module Imap::Backup
40
40
  end
41
41
  end
42
42
 
43
- FileUtils.cp serializer.mbox_pathname, local_folder.full_path
43
+ File.open(local_folder.full_path, "w") do |f|
44
+ enumerator = Serializer::MboxEnumerator.new(serializer.mbox_pathname)
45
+ enumerator.each.with_index do |raw, i|
46
+ clean = Email::Mboxrd::Message.clean_serialized(raw)
47
+ timestamp = Time.now.strftime("%a %b %d %H:%M:%S %Y")
48
+ thunderbird_fom_line = "From - #{timestamp}"
49
+ output = "#{thunderbird_fom_line}\n#{clean}\n"
50
+ f.write output
51
+ end
52
+ end
44
53
 
45
54
  true
46
55
  end
@@ -1,9 +1,9 @@
1
1
  module Imap; end
2
2
 
3
3
  module Imap::Backup
4
- MAJOR = 4
5
- MINOR = 1
6
- REVISION = 2
4
+ MAJOR = 5
5
+ MINOR = 0
6
+ REVISION = 0
7
7
  PRE = nil
8
8
  VERSION = [MAJOR, MINOR, REVISION, PRE].compact.map(&:to_s).join(".")
9
9
  end
@@ -77,17 +77,14 @@ class Thunderbird::Subdirectory
77
77
 
78
78
  def check
79
79
  case
80
- when placeholder.exists? && !exists?
81
- Kernel.puts "Can't set up folder '#{folder_path}': '#{placeholder.path}' exists, but '#{full_path}' is missing"
82
- false
83
80
  when exists? && !placeholder.exists?
84
- Kernel.puts "Can't set up folder '#{folder_path}': '#{full_path}' exists, but '#{placeholder.path}' is missing"
81
+ Kernel.puts "Can't set up folder '#{full_path}': '#{full_path}' exists, but '#{placeholder.path}' is missing"
85
82
  false
86
83
  when placeholder.exists? && !placeholder.regular?
87
- Kernel.puts "Can't set up folder '#{folder_path}': '#{placeholder.path}' exists, but it is not a regular file"
84
+ Kernel.puts "Can't set up folder '#{full_path}': '#{placeholder.path}' exists, but it is not a regular file"
88
85
  false
89
86
  when exists? && !directory?
90
- Kernel.puts "Can't set up folder '#{folder_path}': '#{full_path}' exists, but it is not a directory"
87
+ Kernel.puts "Can't set up folder '#{full_path}': '#{full_path}' exists, but it is not a directory"
91
88
  false
92
89
  else
93
90
  true
@@ -0,0 +1,15 @@
1
+ require "features/helper"
2
+
3
+ RSpec.describe "Running commands", type: :aruba do
4
+ before do
5
+ create_config(accounts: [])
6
+ run_command("imap-backup local accounts")
7
+ last_command_started.stop
8
+ end
9
+
10
+ context "when no accounts are configured" do
11
+ it "succeeds" do
12
+ expect(last_command_started).to have_exit_status(0)
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,14 @@
1
+ require "features/helper"
2
+
3
+ RSpec.describe "Running commands", type: :aruba do
4
+ before do
5
+ run_command("imap-backup")
6
+ last_command_started.stop
7
+ end
8
+
9
+ context "when the configuration file is missing" do
10
+ it "fails" do
11
+ expect(last_command_started).not_to have_exit_status(0)
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,36 @@
1
+ require "features/helper"
2
+ require "imap/backup/cli/folders"
3
+
4
+ RSpec.describe "folders", type: :feature, docker: true do
5
+ include_context "imap-backup connection"
6
+
7
+ let(:options) do
8
+ {accounts: "address@example.org"}
9
+ end
10
+ let(:folder) { "my-stuff" }
11
+ let(:output) { StringIO.new }
12
+
13
+ before do
14
+ allow(Imap::Backup::CLI::Accounts).to receive(:new) { [account] }
15
+ server_create_folder folder
16
+ end
17
+
18
+ around do |example|
19
+ stdout = $stdout
20
+ $stdout = output
21
+ example.run
22
+ $stdout = stdout
23
+ end
24
+
25
+ after do
26
+ FileUtils.rm_rf local_backup_path
27
+ server_delete_folder folder
28
+ connection.disconnect
29
+ end
30
+
31
+ it "lists account folders" do
32
+ Imap::Backup::CLI::Folders.new(options).run
33
+
34
+ expect(output.string).to match(/^\tmy-stuff\n/)
35
+ end
36
+ end
@@ -0,0 +1,12 @@
1
+ require "features/helper"
2
+
3
+ RSpec.describe "Listing accounts", type: :aruba do
4
+ before do
5
+ create_config(accounts: [{"username": "me@example.com"}])
6
+ run_command_and_stop("imap-backup local accounts")
7
+ end
8
+
9
+ it "lists accounts" do
10
+ expect(last_command_started).to have_output("me@example.com")
11
+ end
12
+ end
@@ -0,0 +1,21 @@
1
+ require "features/helper"
2
+
3
+ RSpec.describe "Listing emails", type: :aruba do
4
+ let(:email) { "me@example.com" }
5
+ let(:account) do
6
+ {
7
+ username: email,
8
+ local_path: File.join(config_path, email.gsub("@", "_"))
9
+ }
10
+ end
11
+
12
+ before do
13
+ create_config(accounts: [account])
14
+ store_email(email: email, folder: "my_folder", subject: "Ciao")
15
+ run_command_and_stop("imap-backup local list #{email} my_folder")
16
+ end
17
+
18
+ it "lists emails" do
19
+ expect(last_command_started).to have_output(/1: Ciao/)
20
+ end
21
+ end
@@ -0,0 +1,21 @@
1
+ require "features/helper"
2
+
3
+ RSpec.describe "Listing account folders", type: :aruba do
4
+ let(:email) { "me@example.com" }
5
+ let(:account) do
6
+ {
7
+ username: email,
8
+ local_path: File.join(config_path, email.gsub("@", "_"))
9
+ }
10
+ end
11
+
12
+ before do
13
+ create_config(accounts: [account])
14
+ store_email(email: email, folder: "my_folder", body: "Hi")
15
+ run_command_and_stop("imap-backup local folders #{email}")
16
+ end
17
+
18
+ it "lists folders that have been backed up" do
19
+ expect(last_command_started).to have_output(%q("my_folder"))
20
+ end
21
+ end
@@ -0,0 +1,34 @@
1
+ require "features/helper"
2
+
3
+ RSpec.describe "Show an email", type: :aruba do
4
+ let(:email) { "me@example.com" }
5
+ let(:account) do
6
+ {
7
+ username: email,
8
+ local_path: File.join(config_path, email.gsub("@", "_"))
9
+ }
10
+ end
11
+
12
+ before do
13
+ create_config(accounts: [account])
14
+ store_email(
15
+ email: email,
16
+ folder: "my_folder",
17
+ uid: 99,
18
+ from: "me@example.com",
19
+ subject: "Hello",
20
+ body: "How're things?"
21
+ )
22
+ run_command_and_stop("imap-backup local show #{email} my_folder 99")
23
+ end
24
+
25
+ it "shows the email" do
26
+ expected = <<~BODY
27
+ From: me@example.com
28
+ Subject: Hello
29
+
30
+ How're things?
31
+ BODY
32
+ expect(last_command_started).to have_output(expected)
33
+ end
34
+ end
@@ -0,0 +1,35 @@
1
+ require "features/helper"
2
+
3
+ RSpec.describe "Migration", type: :aruba, docker: true do
4
+ let(:email) { "me@example.com" }
5
+ let(:folder) { "my_folder" }
6
+ let(:source_account) do
7
+ {
8
+ username: email,
9
+ local_path: File.join(config_path, email.gsub("@", "_"))
10
+ }
11
+ end
12
+ let(:destination_account) { fixture("connection") }
13
+
14
+ before do
15
+ create_config(accounts: [source_account, destination_account])
16
+ store_email(email: email, folder: folder, subject: "Ciao")
17
+ run_command_and_stop("imap-backup migrate #{email} #{destination_account[:username]}")
18
+ end
19
+
20
+ after do
21
+ delete_emails(folder)
22
+ end
23
+
24
+ it "copies email to the destination account" do
25
+ messages = server_messages(folder)
26
+ expected = <<~MESSAGE.gsub("\n", "\r\n")
27
+ From: sender@example.com
28
+ Subject: Ciao
29
+
30
+ body
31
+
32
+ MESSAGE
33
+ expect(messages[0]["BODY[]"]).to eq(expected)
34
+ end
35
+ end
@@ -0,0 +1,16 @@
1
+ require "features/helper"
2
+
3
+ RSpec.describe "List account folders", type: :aruba do
4
+ let(:account) do
5
+ fixture("connection")
6
+ end
7
+
8
+ before do
9
+ create_config(accounts: [account])
10
+ run_command_and_stop("imap-backup remote folders #{account[:username]}")
11
+ end
12
+
13
+ it "lists folders" do
14
+ expect(last_command_started).to have_output(/"INBOX"/)
15
+ end
16
+ end
@@ -0,0 +1,69 @@
1
+ require "aruba/rspec"
2
+
3
+ Aruba.configure do |config|
4
+ config.home_directory = File.expand_path("./tmp/home")
5
+ end
6
+
7
+ module ConfigurationHelpers
8
+ def config_path
9
+ File.expand_path("~/.imap-backup")
10
+ end
11
+
12
+ def create_config(accounts:, debug: false)
13
+ pathname = File.join(config_path, "config.json")
14
+ save_data = {
15
+ version: Imap::Backup::Configuration::VERSION,
16
+ accounts: accounts,
17
+ debug: debug
18
+ }
19
+ FileUtils.mkdir_p config_path
20
+ File.open(pathname, "w") { |f| f.write(JSON.pretty_generate(save_data)) }
21
+ FileUtils.chmod(0o600, pathname)
22
+ end
23
+ end
24
+
25
+ module StoreHelpers
26
+ def store_email(
27
+ email:, folder:,
28
+ uid: 1,
29
+ from: "sender@example.com",
30
+ subject: "The Subject",
31
+ body: "body"
32
+ )
33
+ account = config.accounts.find { |a| a.username == email }
34
+ raise "Account not found" if !account
35
+ FileUtils.mkdir_p account.local_path
36
+ store = Imap::Backup::Serializer::MboxStore.new(account.local_path, folder)
37
+ store.uid_validity = "42" if !store.uid_validity
38
+ serialized = to_serialized(from: from, subject: subject, body: body)
39
+ store.add(uid, serialized)
40
+ end
41
+
42
+ def to_serialized(from:, subject:, body:)
43
+ body_and_headers = <<~BODY
44
+ From: #{from}
45
+ Subject: #{subject}
46
+
47
+ #{body}
48
+ BODY
49
+ end
50
+
51
+ def config
52
+ Imap::Backup::Configuration.new(
53
+ File.expand_path("~/.imap-backup/config.json")
54
+ )
55
+ end
56
+ end
57
+
58
+ RSpec.configure do |config|
59
+ config.include ConfigurationHelpers, type: :aruba
60
+ config.include StoreHelpers, type: :aruba
61
+
62
+ config.before(:suite) do
63
+ FileUtils.rm_rf "./tmp/home"
64
+ end
65
+
66
+ config.after do
67
+ FileUtils.rm_rf "./tmp/home"
68
+ end
69
+ end
@@ -106,5 +106,6 @@ module EmailServerHelpers
106
106
  end
107
107
 
108
108
  RSpec.configure do |config|
109
+ config.include EmailServerHelpers, type: :aruba
109
110
  config.include EmailServerHelpers, type: :feature
110
111
  end
@@ -61,14 +61,6 @@ describe Imap::Backup::Account::Connection do
61
61
  end
62
62
  end
63
63
 
64
- describe "#initialize" do
65
- it "creates the path" do
66
- expect(Imap::Backup::Utils).to receive(:make_folder)
67
-
68
- subject
69
- end
70
- end
71
-
72
64
  describe "#client" do
73
65
  let(:result) { subject.client }
74
66
 
@@ -127,6 +119,12 @@ describe Imap::Backup::Account::Connection do
127
119
  allow(Imap::Backup::Serializer::Mbox).to receive(:new) { serializer }
128
120
  end
129
121
 
122
+ it "creates the path" do
123
+ expect(Imap::Backup::Utils).to receive(:make_folder)
124
+
125
+ subject.status
126
+ end
127
+
130
128
  it "returns the names of folders" do
131
129
  expect(subject.status[0][:name]).to eq(IMAP_FOLDER)
132
130
  end
@@ -12,7 +12,10 @@ describe Imap::Backup::Account::Folder do
12
12
  append: append_response,
13
13
  create: nil,
14
14
  examine: nil,
15
- responses: responses
15
+ expunge: nil,
16
+ responses: responses,
17
+ select: nil,
18
+ uid_store: nil
16
19
  )
17
20
  end
18
21
  let(:connection) do
@@ -27,12 +30,11 @@ describe Imap::Backup::Account::Folder do
27
30
  end
28
31
  let(:responses) { [] }
29
32
  let(:append_response) { nil }
33
+ let(:uids) { [5678, 123] }
30
34
 
31
- describe "#uids" do
32
- let(:uids) { [5678, 123] }
33
-
34
- before { allow(client).to receive(:uid_search) { uids } }
35
+ before { allow(client).to receive(:uid_search) { uids } }
35
36
 
37
+ describe "#uids" do
36
38
  it "lists available messages" do
37
39
  expect(subject.uids).to eq(uids.reverse)
38
40
  end
@@ -233,6 +235,25 @@ describe Imap::Backup::Account::Folder do
233
235
  expect(subject.uid_validity).to eq(1)
234
236
  end
235
237
  end
238
+
239
+ describe "#clear" do
240
+ before do
241
+ subject.clear
242
+ end
243
+
244
+ it "uses select to have read-write access" do
245
+ expect(client).to have_received(:select)
246
+ end
247
+
248
+ it "marks all emails as deleted" do
249
+ expect(client).
250
+ to have_received(:uid_store).with(uids.sort, "+FLAGS", [:Deleted])
251
+ end
252
+
253
+ it "deletes marked emails" do
254
+ expect(client).to have_received(:expunge)
255
+ end
256
+ end
236
257
  end
237
258
 
238
259
  # rubocop:enable RSpec/PredicateMatcher
@@ -0,0 +1,87 @@
1
+ require "imap/backup/cli/helpers"
2
+
3
+ module Imap::Backup
4
+ class WithHelpers
5
+ include CLI::Helpers
6
+ end
7
+
8
+ RSpec.describe CLI::Helpers do
9
+ subject { WithHelpers.new }
10
+
11
+ let(:accounts) { instance_double(CLI::Accounts) }
12
+ let(:email) { "email@example.com" }
13
+ let(:account1) { instance_double(Account, username: email, connection: "c1") }
14
+ let(:account2) { instance_double(Account, username: "foo", connection: "c2") }
15
+ let(:items) { [account1, account2] }
16
+
17
+ before do
18
+ allow(CLI::Accounts).to receive(:new) { accounts }
19
+ allow(accounts).to receive(:each).
20
+ and_yield(account1).
21
+ and_yield(account2)
22
+ allow(accounts).to receive(:find) do |&block|
23
+ items.find { |a| block.call(a) }
24
+ end
25
+ end
26
+
27
+ describe ".symbolized" do
28
+ let(:arguments) { {"foo" => 1, "bar" => 2} }
29
+ let(:result) { subject.symbolized(arguments) }
30
+
31
+ it "converts string keys to symbols" do
32
+ expect(result.keys).to eq([:foo, :bar])
33
+ end
34
+
35
+ context "when keys have hyphens" do
36
+ let(:arguments) { {"some-option"=> 3} }
37
+
38
+ it "replaces them with underscores" do
39
+ expect(result.keys).to eq([:some_option])
40
+ end
41
+ end
42
+ end
43
+
44
+ describe ".account" do
45
+ it "returns any account with a matching username" do
46
+ expect(subject.account(email)).to eq(account1)
47
+ end
48
+
49
+ context "when no match is found" do
50
+ let(:items) { [account2] }
51
+ it "fails" do
52
+ expect do
53
+ subject.account(email)
54
+ end.to raise_error(RuntimeError, /not a configured account/)
55
+ end
56
+ end
57
+ end
58
+
59
+ describe ".connection" do
60
+ it "returns the connection for any account with a matching username" do
61
+ result = subject.connection(email)
62
+ expect(result).to be_a(Account::Connection)
63
+ expect(result.account).to eq(account1)
64
+ end
65
+ end
66
+
67
+ describe ".each_connection" do
68
+ it "yields each connection" do
69
+ expect { |b| subject.each_connection([email, "foo"], &b) }.
70
+ to yield_successive_args("c1", "c2")
71
+ end
72
+
73
+ context "when there is no configuration" do
74
+ before do
75
+ allow(accounts).to receive(:each).
76
+ and_raise(Imap::Backup::ConfigurationNotFound)
77
+ end
78
+
79
+ it "fails" do
80
+ expect do
81
+ subject.each_connection([email])
82
+ end.to raise_error(RuntimeError, /not configured/)
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,58 @@
1
+ require "imap/backup/migrator"
2
+
3
+ module Imap::Backup
4
+ RSpec.describe Migrator do
5
+ subject { described_class.new(serializer, folder, reset: reset) }
6
+
7
+ let(:serializer) { instance_double(Serializer::MboxStore, uids: [1]) }
8
+ let(:folder) do
9
+ instance_double(
10
+ Account::Folder,
11
+ append: nil, clear: nil, create: nil, name: "name", uids: folder_uids
12
+ )
13
+ end
14
+ let(:folder_uids) { [] }
15
+ let(:reset) { false }
16
+ let(:messages) { [[1, message]] }
17
+ let(:message) { instance_double(Email::Mboxrd::Message, supplied_body: body) }
18
+ let(:body) { "body" }
19
+
20
+ before do
21
+ allow(serializer).to receive(:each_message) do
22
+ messages.enum_for(:each)
23
+ end
24
+ end
25
+
26
+ it "creates the folder" do
27
+ subject.run
28
+
29
+ expect(folder).to have_received(:create)
30
+ end
31
+
32
+ it "uploads messages" do
33
+ subject.run
34
+
35
+ expect(folder).to have_received(:append).with(message)
36
+ end
37
+
38
+ context "when the folder is not empty" do
39
+ let(:folder_uids) { [99] }
40
+
41
+ it "fails" do
42
+ expect do
43
+ subject.run
44
+ end.to raise_error(RuntimeError, /The destination folder 'name' is not empty/)
45
+ end
46
+
47
+ context "when `reset` is true" do
48
+ let(:reset) { true }
49
+
50
+ it "clears the folder" do
51
+ subject.run
52
+
53
+ expect(folder).to have_received(:clear)
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: imap-backup
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.1.2
4
+ version: 5.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Joe Yates
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-01-07 00:00:00.000000000 Z
11
+ date: 2022-02-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: highline
@@ -80,6 +80,20 @@ dependencies:
80
80
  - - "~>"
81
81
  - !ruby/object:Gem::Version
82
82
  version: '1.1'
83
+ - !ruby/object:Gem::Dependency
84
+ name: aruba
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: 0.0.0
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: 0.0.0
83
97
  - !ruby/object:Gem::Dependency
84
98
  name: codeclimate-test-reporter
85
99
  requirement: !ruby/object:Gem::Requirement
@@ -196,6 +210,8 @@ files:
196
210
  - lib/imap/backup/cli/folders.rb
197
211
  - lib/imap/backup/cli/helpers.rb
198
212
  - lib/imap/backup/cli/local.rb
213
+ - lib/imap/backup/cli/migrate.rb
214
+ - lib/imap/backup/cli/remote.rb
199
215
  - lib/imap/backup/cli/restore.rb
200
216
  - lib/imap/backup/cli/setup.rb
201
217
  - lib/imap/backup/cli/status.rb
@@ -205,6 +221,7 @@ files:
205
221
  - lib/imap/backup/configuration.rb
206
222
  - lib/imap/backup/downloader.rb
207
223
  - lib/imap/backup/logger.rb
224
+ - lib/imap/backup/migrator.rb
208
225
  - lib/imap/backup/sanitizer.rb
209
226
  - lib/imap/backup/serializer.rb
210
227
  - lib/imap/backup/serializer/mbox.rb
@@ -229,9 +246,19 @@ files:
229
246
  - lib/thunderbird/subdirectory.rb
230
247
  - lib/thunderbird/subdirectory_placeholder.rb
231
248
  - spec/features/backup_spec.rb
249
+ - spec/features/configuration/minimal_configuration.rb
250
+ - spec/features/configuration/missing_configuration.rb
251
+ - spec/features/folders_spec.rb
232
252
  - spec/features/helper.rb
253
+ - spec/features/local/list_accounts_spec.rb
254
+ - spec/features/local/list_emails_spec.rb
255
+ - spec/features/local/list_folders_spec.rb
256
+ - spec/features/local/show_an_email_spec.rb
257
+ - spec/features/migrate_spec.rb
258
+ - spec/features/remote/list_account_folders_spec.rb
233
259
  - spec/features/restore_spec.rb
234
260
  - spec/features/status_spec.rb
261
+ - spec/features/support/aruba.rb
235
262
  - spec/features/support/backup_directory.rb
236
263
  - spec/features/support/email_server.rb
237
264
  - spec/features/support/shared/connection_context.rb
@@ -251,12 +278,14 @@ files:
251
278
  - spec/unit/imap/backup/account/connection_spec.rb
252
279
  - spec/unit/imap/backup/account/folder_spec.rb
253
280
  - spec/unit/imap/backup/cli/accounts_spec.rb
281
+ - spec/unit/imap/backup/cli/helpers_spec.rb
254
282
  - spec/unit/imap/backup/cli/local_spec.rb
255
283
  - spec/unit/imap/backup/cli/utils_spec.rb
256
284
  - spec/unit/imap/backup/client/default_spec.rb
257
285
  - spec/unit/imap/backup/configuration_spec.rb
258
286
  - spec/unit/imap/backup/downloader_spec.rb
259
287
  - spec/unit/imap/backup/logger_spec.rb
288
+ - spec/unit/imap/backup/migrator_spec.rb
260
289
  - spec/unit/imap/backup/serializer/mbox_enumerator_spec.rb
261
290
  - spec/unit/imap/backup/serializer/mbox_spec.rb
262
291
  - spec/unit/imap/backup/serializer/mbox_store_spec.rb
@@ -271,7 +300,7 @@ homepage: https://github.com/joeyates/imap-backup
271
300
  licenses:
272
301
  - MIT
273
302
  metadata: {}
274
- post_install_message:
303
+ post_install_message:
275
304
  rdoc_options: []
276
305
  require_paths:
277
306
  - lib
@@ -287,46 +316,58 @@ required_rubygems_version: !ruby/object:Gem::Requirement
287
316
  version: '0'
288
317
  requirements: []
289
318
  rubygems_version: 3.1.6
290
- signing_key:
319
+ signing_key:
291
320
  specification_version: 4
292
321
  summary: Backup GMail (or other IMAP) accounts to disk
293
322
  test_files:
294
- - spec/features/helper.rb
295
- - spec/features/support/backup_directory.rb
296
- - spec/features/support/shared/message_fixtures.rb
297
- - spec/features/support/shared/connection_context.rb
298
- - spec/features/support/email_server.rb
299
- - spec/features/restore_spec.rb
300
- - spec/features/status_spec.rb
301
- - spec/features/backup_spec.rb
302
- - spec/support/higline_test_helpers.rb
303
- - spec/support/fixtures.rb
304
- - spec/support/silence_logging.rb
305
- - spec/gather_rspec_coverage.rb
306
- - spec/unit/email/provider/gmail_spec.rb
307
- - spec/unit/email/provider/fastmail_spec.rb
308
- - spec/unit/email/provider/base_spec.rb
309
- - spec/unit/email/provider/apple_mail_spec.rb
310
- - spec/unit/email/mboxrd/message_spec.rb
311
- - spec/unit/email/provider_spec.rb
312
- - spec/unit/imap/backup/cli/utils_spec.rb
313
- - spec/unit/imap/backup/cli/accounts_spec.rb
314
- - spec/unit/imap/backup/cli/local_spec.rb
323
+ - spec/spec_helper.rb
324
+ - spec/unit/imap/backup/serializer/mbox_spec.rb
325
+ - spec/unit/imap/backup/serializer/mbox_store_spec.rb
326
+ - spec/unit/imap/backup/serializer/mbox_enumerator_spec.rb
327
+ - spec/unit/imap/backup/setup_spec.rb
328
+ - spec/unit/imap/backup/logger_spec.rb
329
+ - spec/unit/imap/backup/configuration_spec.rb
315
330
  - spec/unit/imap/backup/setup/account_spec.rb
316
331
  - spec/unit/imap/backup/setup/connection_tester_spec.rb
317
- - spec/unit/imap/backup/setup/folder_chooser_spec.rb
318
332
  - spec/unit/imap/backup/setup/asker_spec.rb
319
- - spec/unit/imap/backup/uploader_spec.rb
320
- - spec/unit/imap/backup/utils_spec.rb
321
- - spec/unit/imap/backup/configuration_spec.rb
322
- - spec/unit/imap/backup/setup_spec.rb
323
- - spec/unit/imap/backup/logger_spec.rb
324
- - spec/unit/imap/backup/serializer/mbox_store_spec.rb
325
- - spec/unit/imap/backup/serializer/mbox_enumerator_spec.rb
326
- - spec/unit/imap/backup/serializer/mbox_spec.rb
333
+ - spec/unit/imap/backup/setup/folder_chooser_spec.rb
334
+ - spec/unit/imap/backup/cli/helpers_spec.rb
335
+ - spec/unit/imap/backup/cli/accounts_spec.rb
336
+ - spec/unit/imap/backup/cli/utils_spec.rb
337
+ - spec/unit/imap/backup/cli/local_spec.rb
327
338
  - spec/unit/imap/backup/downloader_spec.rb
339
+ - spec/unit/imap/backup/migrator_spec.rb
340
+ - spec/unit/imap/backup/uploader_spec.rb
328
341
  - spec/unit/imap/backup/account/folder_spec.rb
329
342
  - spec/unit/imap/backup/account/connection_spec.rb
343
+ - spec/unit/imap/backup/utils_spec.rb
330
344
  - spec/unit/imap/backup/client/default_spec.rb
331
- - spec/spec_helper.rb
345
+ - spec/unit/email/mboxrd/message_spec.rb
346
+ - spec/unit/email/provider/gmail_spec.rb
347
+ - spec/unit/email/provider/apple_mail_spec.rb
348
+ - spec/unit/email/provider/fastmail_spec.rb
349
+ - spec/unit/email/provider/base_spec.rb
350
+ - spec/unit/email/provider_spec.rb
351
+ - spec/features/folders_spec.rb
352
+ - spec/features/configuration/missing_configuration.rb
353
+ - spec/features/configuration/minimal_configuration.rb
354
+ - spec/features/backup_spec.rb
355
+ - spec/features/helper.rb
356
+ - spec/features/restore_spec.rb
357
+ - spec/features/local/list_emails_spec.rb
358
+ - spec/features/local/list_accounts_spec.rb
359
+ - spec/features/local/show_an_email_spec.rb
360
+ - spec/features/local/list_folders_spec.rb
361
+ - spec/features/status_spec.rb
362
+ - spec/features/support/email_server.rb
363
+ - spec/features/support/backup_directory.rb
364
+ - spec/features/support/aruba.rb
365
+ - spec/features/support/shared/connection_context.rb
366
+ - spec/features/support/shared/message_fixtures.rb
367
+ - spec/features/migrate_spec.rb
368
+ - spec/features/remote/list_account_folders_spec.rb
369
+ - spec/support/fixtures.rb
370
+ - spec/support/silence_logging.rb
371
+ - spec/support/higline_test_helpers.rb
332
372
  - spec/fixtures/connection.yml
373
+ - spec/gather_rspec_coverage.rb