imap-backup 4.1.2 → 5.0.0

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