imap-backup 4.0.2 → 4.0.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +9 -1
  3. data/docs/development.md +1 -2
  4. data/imap-backup.gemspec +1 -0
  5. data/lib/email/provider/apple_mail.rb +7 -0
  6. data/lib/email/provider/default.rb +12 -0
  7. data/lib/email/provider/fastmail.rb +7 -0
  8. data/lib/email/provider/gmail.rb +7 -0
  9. data/lib/email/provider.rb +16 -26
  10. data/lib/imap/backup/account/connection.rb +19 -29
  11. data/lib/imap/backup/account/folder.rb +9 -10
  12. data/lib/imap/backup/cli/helpers.rb +1 -0
  13. data/lib/imap/backup/cli/local.rb +4 -1
  14. data/lib/imap/backup/cli/utils.rb +16 -5
  15. data/lib/imap/backup/client/apple_mail.rb +11 -0
  16. data/lib/imap/backup/client/default.rb +51 -0
  17. data/lib/imap/backup/configuration/account.rb +3 -1
  18. data/lib/imap/backup/configuration/connection_tester.rb +1 -1
  19. data/lib/imap/backup/configuration/store.rb +10 -5
  20. data/lib/imap/backup/downloader.rb +3 -4
  21. data/lib/imap/backup/thunderbird/mailbox_exporter.rb +12 -25
  22. data/lib/imap/backup/version.rb +1 -1
  23. data/lib/thunderbird/install.rb +16 -0
  24. data/lib/thunderbird/local_folder.rb +33 -65
  25. data/lib/thunderbird/profiles.rb +18 -10
  26. data/lib/thunderbird/subdirectory.rb +96 -0
  27. data/lib/thunderbird/{local_folder_placeholder.rb → subdirectory_placeholder.rb} +4 -4
  28. data/lib/thunderbird.rb +10 -2
  29. data/spec/features/restore_spec.rb +1 -1
  30. data/spec/features/support/email_server.rb +2 -2
  31. data/spec/unit/email/provider/apple_mail_spec.rb +7 -0
  32. data/spec/unit/email/provider/default_spec.rb +17 -0
  33. data/spec/unit/email/provider/fastmail_spec.rb +7 -0
  34. data/spec/unit/email/provider/gmail_spec.rb +7 -0
  35. data/spec/unit/email/provider_spec.rb +12 -25
  36. data/spec/unit/imap/backup/account/connection_spec.rb +26 -51
  37. data/spec/unit/imap/backup/account/folder_spec.rb +22 -22
  38. data/spec/unit/imap/backup/cli/utils_spec.rb +2 -2
  39. data/spec/unit/imap/backup/client/default_spec.rb +22 -0
  40. data/spec/unit/imap/backup/configuration/connection_tester_spec.rb +3 -3
  41. data/spec/unit/imap/backup/configuration/store_spec.rb +25 -12
  42. data/spec/unit/imap/backup/downloader_spec.rb +1 -2
  43. metadata +57 -26
  44. data/lib/thunderbird/mailbox.rb +0 -25
@@ -1,97 +1,65 @@
1
1
  require "thunderbird/profile"
2
- require "thunderbird/local_folder_placeholder"
2
+ require "thunderbird/subdirectory"
3
3
 
4
+ # A local folder is a file containing emails
4
5
  class Thunderbird::LocalFolder
5
- attr_reader :folder_path
6
+ attr_reader :path
6
7
  attr_reader :profile
7
8
 
8
- def initialize(profile, folder_path)
9
+ def initialize(profile, path)
9
10
  @profile = profile
10
- @folder_path = folder_path
11
+ @path = path
11
12
  end
12
13
 
13
- def local_folder_placeholder
14
- if parent
15
- path = File.join(parent.full_path, folder_path_elements[-1])
16
- Thunderbird::LocalFolderPlaceholder.new(path)
17
- end
18
- end
14
+ def set_up
15
+ return if path_elements.empty?
19
16
 
20
- def directory_is_directory?
21
- File.directory?(full_path)
22
- end
17
+ return true if !in_subdirectory?
23
18
 
24
- def full_path
25
- File.join(profile.local_folders_path, relative_path)
19
+ subdirectory.set_up
26
20
  end
27
21
 
28
- def parent
29
- if folder_path_elements.count > 0
30
- self.class.new(profile, File.join(folder_path_elements[0..-2]))
22
+ def full_path
23
+ if in_subdirectory?
24
+ File.join(subdirectory.full_path, folder_name)
25
+ else
26
+ folder_name
31
27
  end
32
28
  end
33
29
 
34
- def folder_path_elements
35
- folder_path.split(File::SEPARATOR)
30
+ def exists?
31
+ File.exist?(full_path)
36
32
  end
37
33
 
38
- def directory_exists?
39
- File.exists?(full_path)
34
+ def msf_path
35
+ "#{path}.msf"
40
36
  end
41
37
 
42
- def is_directory?
43
- File.directory?(full_path)
38
+ def msf_exists?
39
+ File.exist?(msf_path)
44
40
  end
45
41
 
46
- def subdirectories
47
- folder_path_elements.map { |p| "#{p}.sbd" }
48
- end
42
+ private
49
43
 
50
- def relative_path
51
- File.join(subdirectories)
44
+ def in_subdirectory?
45
+ path_elements.count > 1
52
46
  end
53
47
 
54
- def set_up
55
- ok = check
56
- return if !ok
48
+ def subdirectory
49
+ return nil if !in_subdirectory?
57
50
 
58
- ensure_initialized
51
+ Thunderbird::Subdirectory.new(profile, subdirectory_path)
59
52
  end
60
53
 
61
- private
62
-
63
- def ensure_initialized
64
- return true if !parent
65
-
66
- parent.ensure_initialized
67
-
68
- local_folder_placeholder.ensure_initialized
54
+ def path_elements
55
+ path.split(File::SEPARATOR)
56
+ end
69
57
 
70
- FileUtils.mkdir_p full_path
58
+ def subdirectory_path
59
+ File.join(path_elements[0..-2])
71
60
  end
72
61
 
73
- def check
74
- return true if !parent
75
-
76
- parent_ok = parent.check
77
-
78
- return if !parent_ok
79
-
80
- case
81
- when local_folder_placeholder.exists? && !directory_exists?
82
- Kernel.puts "Can't set up folder '#{folder_path}': '#{local_folder_placeholder.path}' exists, but '#{full_path}' is missing"
83
- false
84
- when directory_exists? && !local_folder_placeholder.exists?
85
- Kernel.puts "Can't set up folder '#{folder_path}': '#{full_path}' exists, but '#{local_folder_placeholder.path}' is missing"
86
- false
87
- when local_folder_placeholder.exists? && !local_folder_placeholder.is_regular?
88
- Kernel.puts "Can't set up folder '#{folder_path}': '#{local_folder_placeholder.path}' exists, but it is not a regular file"
89
- false
90
- when directory_exists? && !is_directory?
91
- Kernel.puts "Can't set up folder '#{folder_path}': '#{full_path}' exists, but it is not a directory"
92
- false
93
- else
94
- true
95
- end
62
+ def folder_name
63
+ path_elements[-1]
96
64
  end
97
65
  end
@@ -1,10 +1,11 @@
1
1
  require "thunderbird"
2
+ require "thunderbird/install"
2
3
  require "thunderbird/profile"
3
4
 
4
5
  # http://kb.mozillazine.org/Profiles.ini_file
5
6
  class Thunderbird::Profiles
6
- def default
7
- title, entries = blocks.find { |_name, entries| entries[:Default] == "1" }
7
+ def profile_for_path(path)
8
+ title, entries = blocks.find { |_name, entries| entries[:Path] == path }
8
9
 
9
10
  Thunderbird::Profile.new(title, entries) if title
10
11
  end
@@ -12,11 +13,16 @@ class Thunderbird::Profiles
12
13
  def profile(name)
13
14
  title, entries = blocks.find { |_name, entries| entries[:Name] == name }
14
15
 
15
- return nil if !title
16
-
17
16
  Thunderbird::Profile.new(title, entries) if title
18
17
  end
19
18
 
19
+ def installs
20
+ @installs ||= begin
21
+ pairs = blocks.filter { |name, _entries| name.start_with?("Install") }
22
+ pairs.map { |title, entries| Thunderbird::Install.new(title, entries) }
23
+ end
24
+ end
25
+
20
26
  private
21
27
 
22
28
  # Parse profiles.ini.
@@ -31,7 +37,11 @@ class Thunderbird::Profiles
31
37
 
32
38
  loop do
33
39
  line = f.gets
34
- break if !line
40
+ if !line
41
+ blocks[title] = entries if title
42
+ break
43
+ end
44
+
35
45
  line.chomp!
36
46
 
37
47
  # Is this line the start of a new block
@@ -43,12 +53,10 @@ class Thunderbird::Profiles
43
53
  # Start a new block
44
54
  title = match[1]
45
55
  entries = {}
46
- else
56
+ elsif line != ""
47
57
  # Collect entries until we get to the next title
48
- if line != ""
49
- key, value = line.split("=")
50
- entries[key.to_sym] = value
51
- end
58
+ key, value = line.split("=")
59
+ entries[key.to_sym] = value
52
60
  end
53
61
  end
54
62
  end
@@ -0,0 +1,96 @@
1
+ require "thunderbird/subdirectory_placeholder"
2
+
3
+ class Thunderbird::Subdirectory
4
+ # `path` is the UI path, it doesn't have the '.sbd' extensions
5
+ # that are present in the real, file system path
6
+ attr_reader :path
7
+ attr_reader :profile
8
+
9
+ def initialize(profile, path)
10
+ @profile = profile
11
+ @path = path
12
+ end
13
+
14
+ def set_up
15
+ raise "Cannot create a subdirectory without a path" if !sub_directory?
16
+
17
+ if sub_sub_directory?
18
+ parent_ok = parent.set_up
19
+ return false if !parent_ok
20
+ end
21
+
22
+ ok = check
23
+ return false if !ok
24
+
25
+ FileUtils.mkdir_p full_path
26
+ placeholder.touch
27
+
28
+ true
29
+ end
30
+
31
+ # subdirectory relative path is 'Foo.sbd/Bar.sbd/Baz.sbd'
32
+ def full_path
33
+ relative_path = File.join(subdirectories)
34
+ File.join(profile.local_folders_path, relative_path)
35
+ end
36
+
37
+ private
38
+
39
+ def sub_directory?
40
+ path_elements.any?
41
+ end
42
+
43
+ def sub_sub_directory?
44
+ path_elements.count > 1
45
+ end
46
+
47
+ def parent
48
+ return nil if !sub_sub_directory?
49
+
50
+ self.class.new(profile, File.join(path_elements[0..-2]))
51
+ end
52
+
53
+ # placeholder relative path is 'Foo.sbd/Bar.sbd/Baz'
54
+ def placeholder
55
+ @placeholder = begin
56
+ relative_path = File.join(subdirectories[0..-2], path_elements[-1])
57
+ path = File.join(profile.local_folders_path, relative_path)
58
+ Thunderbird::SubdirectoryPlaceholder.new(path)
59
+ end
60
+ end
61
+
62
+ def path_elements
63
+ path.split(File::SEPARATOR)
64
+ end
65
+
66
+ def exists?
67
+ File.exist?(full_path)
68
+ end
69
+
70
+ def directory?
71
+ File.directory?(full_path)
72
+ end
73
+
74
+ def subdirectories
75
+ path_elements.map { |p| "#{p}.sbd" }
76
+ end
77
+
78
+ def check
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
+ when exists? && !placeholder.exists?
84
+ Kernel.puts "Can't set up folder '#{folder_path}': '#{full_path}' exists, but '#{placeholder.path}' is missing"
85
+ false
86
+ 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"
88
+ false
89
+ when exists? && !directory?
90
+ Kernel.puts "Can't set up folder '#{folder_path}': '#{full_path}' exists, but it is not a directory"
91
+ false
92
+ else
93
+ true
94
+ end
95
+ end
96
+ end
@@ -1,6 +1,6 @@
1
1
  # Each subdirectory is "accompanied" by a blank
2
2
  # file of the same name (without the '.sbd' extension)
3
- class Thunderbird::LocalFolderPlaceholder
3
+ class Thunderbird::SubdirectoryPlaceholder
4
4
  attr_reader :path
5
5
 
6
6
  def initialize(path)
@@ -8,14 +8,14 @@ class Thunderbird::LocalFolderPlaceholder
8
8
  end
9
9
 
10
10
  def exists?
11
- File.exists?(path)
11
+ File.exist?(path)
12
12
  end
13
13
 
14
- def is_regular?
14
+ def regular?
15
15
  File.file?(path)
16
16
  end
17
17
 
18
- def ensure_initialized
18
+ def touch
19
19
  FileUtils.touch path
20
20
  end
21
21
  end
data/lib/thunderbird.rb CHANGED
@@ -1,6 +1,14 @@
1
+ require "os"
2
+
1
3
  class Thunderbird
2
4
  def data_path
3
- # TODO: Handle other OSes
4
- File.join(Dir.home, ".thunderbird")
5
+ case
6
+ when OS.linux?
7
+ File.join(Dir.home, ".thunderbird")
8
+ when OS.mac?
9
+ File.join(Dir.home, "Library", "Thunderbird")
10
+ when OS.windows?
11
+ File.join(ENV["APPDATA"].gsub("\\", "/"), "Thunderbird")
12
+ end
5
13
  end
6
14
  end
@@ -152,7 +152,7 @@ RSpec.describe "restore", type: :feature, docker: true do
152
152
  it "maintains encodings" do
153
153
  message =
154
154
  server_messages(folder).
155
- first["RFC822"]
155
+ first["BODY[]"]
156
156
 
157
157
  expect(message).to eq(server_message)
158
158
  end
@@ -1,5 +1,5 @@
1
1
  module EmailServerHelpers
2
- REQUESTED_ATTRIBUTES = %w(RFC822 FLAGS INTERNALDATE).freeze
2
+ REQUESTED_ATTRIBUTES = ["BODY[]"].freeze
3
3
  DEFAULT_EMAIL = "address@example.org".freeze
4
4
 
5
5
  def send_email(folder, options)
@@ -28,7 +28,7 @@ module EmailServerHelpers
28
28
  end
29
29
 
30
30
  def server_message_to_body(message)
31
- message["RFC822"]
31
+ message["BODY[]"]
32
32
  end
33
33
 
34
34
  def server_fetch_email(folder, uid)
@@ -0,0 +1,7 @@
1
+ describe Email::Provider::AppleMail do
2
+ describe "#host" do
3
+ it "returns host" do
4
+ expect(subject.host).to eq("imap.mail.me.com")
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,17 @@
1
+ describe Email::Provider::Default do
2
+ describe "#host" do
3
+ it "is unset" do
4
+ expect(subject.host).to be_nil
5
+ end
6
+ end
7
+
8
+ describe "#options" do
9
+ it "returns options" do
10
+ expect(subject.options).to be_a(Hash)
11
+ end
12
+
13
+ it "forces TLSv1_2" do
14
+ expect(subject.options[:ssl][:ssl_version]).to eq(:TLSv1_2)
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,7 @@
1
+ describe Email::Provider::Fastmail do
2
+ describe "#host" do
3
+ it "returns host" do
4
+ expect(subject.host).to eq("imap.fastmail.com")
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ describe Email::Provider::GMail do
2
+ describe "#host" do
3
+ it "returns host" do
4
+ expect(subject.host).to eq("imap.gmail.com")
5
+ end
6
+ end
7
+ end
@@ -1,40 +1,27 @@
1
1
  describe Email::Provider do
2
- subject { described_class.new(:gmail) }
3
-
4
2
  describe ".for_address" do
5
3
  context "with known providers" do
6
4
  [
7
- ["gmail.com", :gmail],
8
- ["fastmail.fm", :fastmail]
9
- ].each do |domain, provider|
10
- it "recognizes #{provider}" do
5
+ ["fastmail.com", "Fastmail .com", Email::Provider::Fastmail],
6
+ ["fastmail.fm", "Fastmail .fm", Email::Provider::Fastmail],
7
+ ["gmail.com", "GMail", Email::Provider::GMail],
8
+ ["icloud.com", "Apple Mail icloud.com", Email::Provider::AppleMail],
9
+ ["mac.com", "Apple Mail mac.com", Email::Provider::AppleMail],
10
+ ["me.com", "Apple Mail me.com", Email::Provider::AppleMail]
11
+ ].each do |domain, name, klass|
12
+ it "recognizes #{name} addresses" do
11
13
  address = "foo@#{domain}"
12
- expect(described_class.for_address(address).provider).to eq(provider)
14
+ expect(described_class.for_address(address)).to be_a(klass)
13
15
  end
14
16
  end
15
17
  end
16
18
 
17
19
  context "with unknown providers" do
18
20
  it "returns a default provider" do
19
- result = described_class.for_address("foo@unknown.com").provider
20
- expect(result).to eq(:default)
21
- end
22
- end
23
- end
24
-
25
- describe "#options" do
26
- it "returns options" do
27
- expect(subject.options).to be_a(Hash)
28
- end
21
+ result = described_class.for_address("foo@unknown.com")
29
22
 
30
- it "forces TLSv1_2" do
31
- expect(subject.options[:ssl][:ssl_version]).to eq(:TLSv1_2)
32
- end
33
- end
34
-
35
- describe "#host" do
36
- it "returns host" do
37
- expect(subject.host).to eq("imap.gmail.com")
23
+ expect(result).to be_a(Email::Provider::Default)
24
+ end
38
25
  end
39
26
  end
40
27
  end
@@ -14,8 +14,10 @@ describe Imap::Backup::Account::Connection do
14
14
 
15
15
  subject { described_class.new(options) }
16
16
 
17
- let(:imap) do
18
- instance_double(Net::IMAP, authenticate: nil, login: nil, disconnect: nil)
17
+ let(:client) do
18
+ instance_double(
19
+ Imap::Backup::Client::Default, authenticate: nil, login: nil, disconnect: nil
20
+ )
19
21
  end
20
22
  let(:imap_folders) { [] }
21
23
  let(:options) do
@@ -45,15 +47,14 @@ describe Imap::Backup::Account::Connection do
45
47
  let(:new_uid_validity) { nil }
46
48
 
47
49
  before do
48
- allow(Net::IMAP).to receive(:new) { imap }
49
- allow(imap).to receive(:list).with("", "") { [root_info] }
50
- allow(imap).to receive(:list).with(ROOT_NAME, "*") { imap_folders }
50
+ allow(Imap::Backup::Client::Default).to receive(:new) { client }
51
+ allow(client).to receive(:list) { imap_folders }
51
52
  allow(Imap::Backup::Utils).to receive(:make_folder)
52
53
  end
53
54
 
54
55
  shared_examples "connects to IMAP" do
55
56
  it "logs in to the imap server" do
56
- expect(imap).to have_received(:login)
57
+ expect(client).to have_received(:login)
57
58
  end
58
59
  end
59
60
 
@@ -76,48 +77,34 @@ describe Imap::Backup::Account::Connection do
76
77
  end
77
78
  end
78
79
 
79
- describe "#imap" do
80
- let(:result) { subject.imap }
80
+ describe "#client" do
81
+ let(:result) { subject.client }
81
82
 
82
83
  it "returns the IMAP connection" do
83
- expect(result).to eq(imap)
84
+ expect(result).to eq(client)
84
85
  end
85
86
 
86
87
  it "uses the password" do
87
88
  result
88
89
 
89
- expect(imap).to have_received(:login).with(USERNAME, PASSWORD)
90
- end
91
-
92
- context "with the GMail IMAP server" do
93
- let(:server) { GMAIL_IMAP_SERVER }
94
- let(:refresh_token) { true }
95
- let(:result) { nil }
96
-
97
- context "when the password is not our copy of a GMail refresh token" do
98
- it "uses the password" do
99
- subject.imap
100
-
101
- expect(imap).to have_received(:login).with(USERNAME, PASSWORD)
102
- end
103
- end
90
+ expect(client).to have_received(:login).with(USERNAME, PASSWORD)
104
91
  end
105
92
 
106
93
  context "when the first login attempt fails" do
107
94
  before do
108
95
  outcomes = [-> { raise EOFError }, -> { true }]
109
- allow(imap).to receive(:login) { outcomes.shift.call }
96
+ allow(client).to receive(:login) { outcomes.shift.call }
110
97
  end
111
98
 
112
99
  it "retries" do
113
- subject.imap
100
+ subject.client
114
101
 
115
- expect(imap).to have_received(:login).twice
102
+ expect(client).to have_received(:login).twice
116
103
  end
117
104
  end
118
105
 
119
106
  context "when run" do
120
- before { subject.imap }
107
+ before { subject.client }
121
108
 
122
109
  include_examples "connects to IMAP"
123
110
  end
@@ -125,22 +112,12 @@ describe Imap::Backup::Account::Connection do
125
112
 
126
113
  describe "#folders" do
127
114
  let(:imap_folders) do
128
- [instance_double(Net::IMAP::MailboxList, name: BACKUP_FOLDER)]
115
+ [BACKUP_FOLDER]
129
116
  end
130
117
 
131
118
  it "returns the list of folders" do
132
119
  expect(subject.folders).to eq([BACKUP_FOLDER])
133
120
  end
134
-
135
- context "with non-ASCII folder names" do
136
- let(:imap_folders) do
137
- [instance_double(Net::IMAP::MailboxList, name: "Gel&APY-scht")]
138
- end
139
-
140
- it "converts them to UTF-8" do
141
- expect(subject.folders).to eq(["Gelöscht"])
142
- end
143
- end
144
121
  end
145
122
 
146
123
  describe "#status" do
@@ -208,9 +185,7 @@ describe Imap::Backup::Account::Connection do
208
185
  end
209
186
 
210
187
  context "without supplied config_folders" do
211
- let(:imap_folders) do
212
- [instance_double(Net::IMAP::MailboxList, name: ROOT_NAME)]
213
- end
188
+ let(:imap_folders) { [ROOT_NAME] }
214
189
 
215
190
  before do
216
191
  allow(Imap::Backup::Account::Folder).to receive(:new).
@@ -241,7 +216,7 @@ describe Imap::Backup::Account::Connection do
241
216
 
242
217
  context "when the imap server doesn't return folders" do
243
218
  let(:config_folders) { nil }
244
- let(:imap_folders) { nil }
219
+ let(:imap_folders) { [] }
245
220
 
246
221
  it "fails" do
247
222
  expect do
@@ -389,10 +364,10 @@ describe Imap::Backup::Account::Connection do
389
364
 
390
365
  describe "#reconnect" do
391
366
  context "when the IMAP connection has been used" do
392
- before { subject.imap }
367
+ before { subject.client }
393
368
 
394
369
  it "disconnects from the server" do
395
- expect(imap).to receive(:disconnect)
370
+ expect(client).to receive(:disconnect)
396
371
 
397
372
  subject.reconnect
398
373
  end
@@ -400,26 +375,26 @@ describe Imap::Backup::Account::Connection do
400
375
 
401
376
  context "when the IMAP connection has not been used" do
402
377
  it "does not disconnect from the server" do
403
- expect(imap).to_not receive(:disconnect)
378
+ expect(client).to_not receive(:disconnect)
404
379
 
405
380
  subject.reconnect
406
381
  end
407
382
  end
408
383
 
409
384
  it "causes reconnection on future access" do
410
- expect(Net::IMAP).to receive(:new)
385
+ expect(Imap::Backup::Client::Default).to receive(:new)
411
386
 
412
387
  subject.reconnect
413
- subject.imap
388
+ subject.client
414
389
  end
415
390
  end
416
391
 
417
392
  describe "#disconnect" do
418
393
  context "when the IMAP connection has been used" do
419
394
  it "disconnects from the server" do
420
- subject.imap
395
+ subject.client
421
396
 
422
- expect(imap).to receive(:disconnect)
397
+ expect(client).to receive(:disconnect)
423
398
 
424
399
  subject.disconnect
425
400
  end
@@ -427,7 +402,7 @@ describe Imap::Backup::Account::Connection do
427
402
 
428
403
  context "when the IMAP connection has not been used" do
429
404
  it "does not disconnect from the server" do
430
- expect(imap).to_not receive(:disconnect)
405
+ expect(client).to_not receive(:disconnect)
431
406
 
432
407
  subject.disconnect
433
408
  end