imap-backup 5.2.0 → 6.0.0.rc2
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.
- checksums.yaml +4 -4
- data/README.md +9 -2
- data/docs/development.md +10 -4
- data/lib/cli_coverage.rb +1 -1
- data/lib/imap/backup/account/connection.rb +7 -11
- data/lib/imap/backup/account.rb +31 -11
- data/lib/imap/backup/cli/folders.rb +3 -3
- data/lib/imap/backup/cli/migrate.rb +3 -3
- data/lib/imap/backup/cli/utils.rb +2 -2
- data/lib/imap/backup/configuration.rb +1 -11
- data/lib/imap/backup/downloader.rb +13 -9
- data/lib/imap/backup/serializer/directory.rb +37 -0
- data/lib/imap/backup/serializer/imap.rb +120 -0
- data/lib/imap/backup/serializer/mbox.rb +23 -94
- data/lib/imap/backup/serializer/mbox_enumerator.rb +2 -0
- data/lib/imap/backup/serializer.rb +180 -3
- data/lib/imap/backup/setup/account.rb +52 -29
- data/lib/imap/backup/setup/helpers.rb +1 -1
- data/lib/imap/backup/thunderbird/mailbox_exporter.rb +1 -1
- data/lib/imap/backup/version.rb +3 -3
- data/lib/imap/backup.rb +0 -1
- data/spec/features/backup_spec.rb +8 -16
- data/spec/features/support/aruba.rb +4 -3
- data/spec/unit/imap/backup/account/connection_spec.rb +36 -8
- data/spec/unit/imap/backup/account/folder_spec.rb +10 -0
- data/spec/unit/imap/backup/account_spec.rb +246 -0
- data/spec/unit/imap/backup/cli/accounts_spec.rb +12 -1
- data/spec/unit/imap/backup/cli/backup_spec.rb +19 -0
- data/spec/unit/imap/backup/cli/folders_spec.rb +39 -0
- data/spec/unit/imap/backup/cli/local_spec.rb +26 -7
- data/spec/unit/imap/backup/cli/migrate_spec.rb +80 -0
- data/spec/unit/imap/backup/cli/restore_spec.rb +67 -0
- data/spec/unit/imap/backup/cli/setup_spec.rb +17 -0
- data/spec/unit/imap/backup/cli/utils_spec.rb +68 -5
- data/spec/unit/imap/backup/cli_spec.rb +93 -0
- data/spec/unit/imap/backup/client/apple_mail_spec.rb +9 -0
- data/spec/unit/imap/backup/configuration_spec.rb +2 -2
- data/spec/unit/imap/backup/downloader_spec.rb +59 -7
- data/spec/unit/imap/backup/migrator_spec.rb +1 -1
- data/spec/unit/imap/backup/sanitizer_spec.rb +42 -0
- data/spec/unit/imap/backup/serializer/directory_spec.rb +37 -0
- data/spec/unit/imap/backup/serializer/imap_spec.rb +218 -0
- data/spec/unit/imap/backup/serializer/mbox_spec.rb +62 -183
- data/spec/unit/imap/backup/serializer_spec.rb +296 -0
- data/spec/unit/imap/backup/setup/account_spec.rb +120 -25
- data/spec/unit/imap/backup/setup/helpers_spec.rb +15 -0
- data/spec/unit/imap/backup/thunderbird/mailbox_exporter_spec.rb +116 -0
- data/spec/unit/imap/backup/uploader_spec.rb +1 -1
- data/spec/unit/retry_on_error_spec.rb +34 -0
- metadata +36 -7
- data/lib/imap/backup/serializer/mbox_store.rb +0 -217
- data/spec/unit/imap/backup/serializer/mbox_store_spec.rb +0 -329
@@ -0,0 +1,93 @@
|
|
1
|
+
module Imap::Backup
|
2
|
+
RSpec.describe CLI do
|
3
|
+
describe ".exit_on_failure?" do
|
4
|
+
it "is true" do
|
5
|
+
expect(described_class.exit_on_failure?).to be true
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
describe "#backup" do
|
10
|
+
let(:backup) { instance_double(CLI::Backup, run: nil) }
|
11
|
+
|
12
|
+
before do
|
13
|
+
allow(CLI::Backup).to receive(:new) { backup }
|
14
|
+
|
15
|
+
subject.backup
|
16
|
+
end
|
17
|
+
|
18
|
+
it "runs Backup" do
|
19
|
+
expect(backup).to have_received(:run)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
describe "#folders" do
|
24
|
+
let(:folders) { instance_double(CLI::Folders, run: nil) }
|
25
|
+
|
26
|
+
before do
|
27
|
+
allow(CLI::Folders).to receive(:new) { folders }
|
28
|
+
|
29
|
+
subject.folders
|
30
|
+
end
|
31
|
+
|
32
|
+
it "runs folders" do
|
33
|
+
expect(folders).to have_received(:run)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
describe "#migrate" do
|
38
|
+
let(:migrate) { instance_double(CLI::Migrate, run: nil) }
|
39
|
+
|
40
|
+
before do
|
41
|
+
allow(CLI::Migrate).to receive(:new) { migrate }
|
42
|
+
|
43
|
+
subject.migrate("source", "destination")
|
44
|
+
end
|
45
|
+
|
46
|
+
it "runs migrate" do
|
47
|
+
expect(migrate).to have_received(:run)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
describe "#restore" do
|
52
|
+
let(:restore) { instance_double(CLI::Restore, run: nil) }
|
53
|
+
|
54
|
+
before do
|
55
|
+
allow(CLI::Restore).to receive(:new) { restore }
|
56
|
+
|
57
|
+
subject.restore
|
58
|
+
end
|
59
|
+
|
60
|
+
it "runs restore" do
|
61
|
+
expect(restore).to have_received(:run)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
describe "#setup" do
|
66
|
+
let(:setup) { instance_double(CLI::Setup, run: nil) }
|
67
|
+
|
68
|
+
before do
|
69
|
+
allow(CLI::Setup).to receive(:new) { setup }
|
70
|
+
|
71
|
+
subject.setup
|
72
|
+
end
|
73
|
+
|
74
|
+
it "runs setup" do
|
75
|
+
expect(setup).to have_received(:run)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
describe "#status" do
|
80
|
+
let(:status) { instance_double(CLI::Status, run: nil) }
|
81
|
+
|
82
|
+
before do
|
83
|
+
allow(CLI::Status).to receive(:new) { status }
|
84
|
+
|
85
|
+
subject.status
|
86
|
+
end
|
87
|
+
|
88
|
+
it "runs status" do
|
89
|
+
expect(status).to have_received(:run)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -59,7 +59,7 @@ describe Imap::Backup::Configuration do
|
|
59
59
|
end
|
60
60
|
|
61
61
|
context "with accounts flagged 'delete'" do
|
62
|
-
before { subject.accounts[0].mark_for_deletion
|
62
|
+
before { subject.accounts[0].mark_for_deletion }
|
63
63
|
|
64
64
|
it "is true" do
|
65
65
|
expect(subject.modified?).to be_truthy
|
@@ -177,7 +177,7 @@ describe Imap::Backup::Configuration do
|
|
177
177
|
before do
|
178
178
|
allow(subject.accounts[0]).to receive(:to_h) { "Account1" }
|
179
179
|
allow(subject.accounts[1]).to receive(:to_h) { "Account2" }
|
180
|
-
subject.accounts[0].mark_for_deletion
|
180
|
+
subject.accounts[0].mark_for_deletion
|
181
181
|
end
|
182
182
|
|
183
183
|
it "does not save them" do
|
@@ -1,6 +1,6 @@
|
|
1
1
|
describe Imap::Backup::Downloader do
|
2
2
|
describe "#run" do
|
3
|
-
subject { described_class.new(folder, serializer) }
|
3
|
+
subject { described_class.new(folder, serializer, **options) }
|
4
4
|
|
5
5
|
let(:body) { "blah" }
|
6
6
|
let(:folder) do
|
@@ -8,17 +8,19 @@ describe Imap::Backup::Downloader do
|
|
8
8
|
Imap::Backup::Account::Folder,
|
9
9
|
fetch_multi: [{uid: "111", body: body}],
|
10
10
|
name: "folder",
|
11
|
-
uids:
|
11
|
+
uids: remote_uids
|
12
12
|
)
|
13
13
|
end
|
14
|
-
let(:
|
14
|
+
let(:remote_uids) { %w(111 222 333) }
|
15
15
|
let(:serializer) do
|
16
|
-
instance_double(Imap::Backup::Serializer
|
16
|
+
instance_double(Imap::Backup::Serializer, append: nil, uids: local_uids)
|
17
17
|
end
|
18
|
+
let(:local_uids) { ["222"] }
|
19
|
+
let(:options) { {} }
|
18
20
|
|
19
21
|
context "with fetched messages" do
|
20
22
|
specify "are saved" do
|
21
|
-
expect(serializer).to receive(:
|
23
|
+
expect(serializer).to receive(:append).with("111", body)
|
22
24
|
|
23
25
|
subject.run
|
24
26
|
end
|
@@ -26,7 +28,7 @@ describe Imap::Backup::Downloader do
|
|
26
28
|
|
27
29
|
context "with messages which are already present" do
|
28
30
|
specify "are skipped" do
|
29
|
-
expect(serializer).to_not receive(:
|
31
|
+
expect(serializer).to_not receive(:append).with("222", anything)
|
30
32
|
|
31
33
|
subject.run
|
32
34
|
end
|
@@ -35,10 +37,60 @@ describe Imap::Backup::Downloader do
|
|
35
37
|
context "with failed fetches" do
|
36
38
|
specify "are skipped" do
|
37
39
|
allow(folder).to receive(:fetch_multi) { nil }
|
38
|
-
expect(serializer).to_not receive(:
|
40
|
+
expect(serializer).to_not receive(:append)
|
39
41
|
|
40
42
|
subject.run
|
41
43
|
end
|
42
44
|
end
|
45
|
+
|
46
|
+
context "when the block size is greater than one" do
|
47
|
+
let(:remote_uids) { %w(111 999) }
|
48
|
+
let(:local_uids) { [] }
|
49
|
+
let(:options) { {multi_fetch_size: 2} }
|
50
|
+
|
51
|
+
context "when the first fetch fails" do
|
52
|
+
before do
|
53
|
+
allow(folder).to receive(:fetch_multi).with(["111", "999"]) { nil }
|
54
|
+
allow(folder).to receive(:fetch_multi).with(["111"]).
|
55
|
+
and_return([{uid: "111", body: body}]).
|
56
|
+
and_return([{uid: "999", body: body}])
|
57
|
+
|
58
|
+
subject.run
|
59
|
+
end
|
60
|
+
|
61
|
+
it "retries fetching messages singly" do
|
62
|
+
expect(serializer).to have_received(:append).with("111", body)
|
63
|
+
expect(serializer).to have_received(:append).with("999", body)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
context "when no body is returned by the fetch" do
|
69
|
+
let(:remote_uids) { %w(111) }
|
70
|
+
|
71
|
+
before do
|
72
|
+
allow(folder).to receive(:fetch_multi).with(["111"]) { [{uid: "111", body: nil}] }
|
73
|
+
|
74
|
+
subject.run
|
75
|
+
end
|
76
|
+
|
77
|
+
it "skips the append" do
|
78
|
+
expect(serializer).to_not have_received(:append)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
context "when the UID is not returned by the fetch" do
|
83
|
+
let(:remote_uids) { %w(111) }
|
84
|
+
|
85
|
+
before do
|
86
|
+
allow(folder).to receive(:fetch_multi).with(["111"]) { [{uid: nil, body: body}] }
|
87
|
+
|
88
|
+
subject.run
|
89
|
+
end
|
90
|
+
|
91
|
+
it "skips the append" do
|
92
|
+
expect(serializer).to_not have_received(:append)
|
93
|
+
end
|
94
|
+
end
|
43
95
|
end
|
44
96
|
end
|
@@ -4,7 +4,7 @@ module Imap::Backup
|
|
4
4
|
RSpec.describe Migrator do
|
5
5
|
subject { described_class.new(serializer, folder, reset: reset) }
|
6
6
|
|
7
|
-
let(:serializer) { instance_double(Serializer
|
7
|
+
let(:serializer) { instance_double(Serializer, uids: [1]) }
|
8
8
|
let(:folder) do
|
9
9
|
instance_double(
|
10
10
|
Account::Folder,
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module Imap::Backup
|
2
|
+
describe Sanitizer do
|
3
|
+
require "stringio"
|
4
|
+
|
5
|
+
subject { described_class.new(output) }
|
6
|
+
|
7
|
+
let(:output) { StringIO.new }
|
8
|
+
|
9
|
+
describe "#puts" do
|
10
|
+
it "delegates to output" do
|
11
|
+
subject.puts("x")
|
12
|
+
|
13
|
+
expect(output.string).to eq("x\n")
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
describe "#write" do
|
18
|
+
it "delegates to output" do
|
19
|
+
subject.write("x")
|
20
|
+
|
21
|
+
expect(output.string).to eq("x")
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
describe "#print" do
|
26
|
+
it "removes passwords from complete lines of text" do
|
27
|
+
subject.print("C: RUBY99 LOGIN xx) secret!!!!\netc")
|
28
|
+
|
29
|
+
expect(output.string).to eq("C: RUBY99 LOGIN xx) [PASSWORD REDACTED]\n")
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
describe "#flush" do
|
34
|
+
it "sanitizes remaining text" do
|
35
|
+
subject.print("before\nC: RUBY99 LOGIN xx) secret!!!!")
|
36
|
+
subject.flush
|
37
|
+
|
38
|
+
expect(output.string).to eq("before\nC: RUBY99 LOGIN xx) [PASSWORD REDACTED]\n")
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module Imap::Backup
|
2
|
+
describe Serializer::Directory do
|
3
|
+
subject { described_class.new("path", "relative") }
|
4
|
+
|
5
|
+
let(:windows) { false }
|
6
|
+
|
7
|
+
before do
|
8
|
+
allow(File).to receive(:directory?) { false }
|
9
|
+
allow(Utils).to receive(:make_folder)
|
10
|
+
allow(OS).to receive(:windows?) { windows }
|
11
|
+
allow(Utils).to receive(:mode) { 0o600 }
|
12
|
+
allow(FileUtils).to receive(:chmod)
|
13
|
+
|
14
|
+
subject.ensure_exists
|
15
|
+
end
|
16
|
+
|
17
|
+
describe "#ensure_exists" do
|
18
|
+
context "when the directory doesn't exist" do
|
19
|
+
it "makes the directory" do
|
20
|
+
expect(Utils).to have_received(:make_folder)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
it "sets permissions" do
|
25
|
+
expect(FileUtils).to have_received(:chmod)
|
26
|
+
end
|
27
|
+
|
28
|
+
context "when on Windows" do
|
29
|
+
let(:windows) { true }
|
30
|
+
|
31
|
+
it "doesn't set permissions" do
|
32
|
+
expect(FileUtils).to_not have_received(:chmod)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,218 @@
|
|
1
|
+
module Imap::Backup
|
2
|
+
describe Serializer::Imap do
|
3
|
+
subject { described_class.new(folder_path) }
|
4
|
+
|
5
|
+
let(:folder_path) { "folder_path" }
|
6
|
+
let(:pathname) { "folder_path.imap" }
|
7
|
+
let(:exists) { true }
|
8
|
+
let(:existing) { {uid_validity: 99, uids: [42]} }
|
9
|
+
let(:file) { instance_double(File, write: nil) }
|
10
|
+
|
11
|
+
before do
|
12
|
+
allow(File).to receive(:exist?).and_call_original
|
13
|
+
allow(File).to receive(:exist?).with(pathname) { exists }
|
14
|
+
allow(File).to receive(:open).and_call_original
|
15
|
+
allow(File).to receive(:open).with(pathname, "w").and_yield(file)
|
16
|
+
allow(File).to receive(:read).with(pathname) { existing.to_json }
|
17
|
+
end
|
18
|
+
|
19
|
+
describe "loading the metadata file" do
|
20
|
+
context "when it is malformed" do
|
21
|
+
before do
|
22
|
+
allow(File).to receive(:read).with(pathname).and_raise(JSON::ParserError)
|
23
|
+
end
|
24
|
+
|
25
|
+
it "ignores the file" do
|
26
|
+
subject.uid_validity
|
27
|
+
|
28
|
+
expect(subject.uids).to eq([])
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
describe "#append" do
|
34
|
+
context "when the metadata file exists" do
|
35
|
+
before { subject.append(123) }
|
36
|
+
|
37
|
+
it "loads the existing metadata" do
|
38
|
+
expect(subject.uids).to include(42)
|
39
|
+
end
|
40
|
+
|
41
|
+
it "appends the UID" do
|
42
|
+
expect(subject.uids).to include(123)
|
43
|
+
end
|
44
|
+
|
45
|
+
it "saves the file" do
|
46
|
+
expect(file).to have_received(:write).
|
47
|
+
with(/"uids":\[42,123\]/)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
context "when the metadata file doesn't exist" do
|
52
|
+
let(:exists) { false }
|
53
|
+
|
54
|
+
context "when the uid_validity is set" do
|
55
|
+
before do
|
56
|
+
subject.uid_validity = 999
|
57
|
+
end
|
58
|
+
|
59
|
+
it "appends the UID" do
|
60
|
+
subject.append(123)
|
61
|
+
|
62
|
+
expect(subject.uids).to include(123)
|
63
|
+
end
|
64
|
+
|
65
|
+
it "saves the file" do
|
66
|
+
subject.append(123)
|
67
|
+
|
68
|
+
expect(file).to have_received(:write).
|
69
|
+
with(/"uids":\[123\]/)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
context "when the uid_validity is not set" do
|
74
|
+
it "fails" do
|
75
|
+
expect do
|
76
|
+
subject.append(123)
|
77
|
+
end.to raise_error(RuntimeError, /without a uid_validity/)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
describe "#exist?" do
|
84
|
+
context "when the metadata file exists" do
|
85
|
+
it "is true" do
|
86
|
+
expect(subject.exist?).to be true
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
context "when the metadata file doesn't exist" do
|
91
|
+
let(:exists) { false }
|
92
|
+
|
93
|
+
it "is false" do
|
94
|
+
expect(subject.exist?).to be false
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
describe "#include?" do
|
100
|
+
it "loads the existing metadata" do
|
101
|
+
subject.include?(42)
|
102
|
+
|
103
|
+
expect(File).to have_received(:read).with(pathname)
|
104
|
+
end
|
105
|
+
|
106
|
+
context "when there is a matching UID" do
|
107
|
+
it "is true" do
|
108
|
+
expect(subject.include?(42)).to be true
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
context "when there isn't a matching UID" do
|
113
|
+
it "is false" do
|
114
|
+
expect(subject.include?(99)).to be false
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
describe "#index" do
|
120
|
+
it "loads the existing metadata" do
|
121
|
+
subject.include?(42)
|
122
|
+
|
123
|
+
expect(File).to have_received(:read).with(pathname)
|
124
|
+
end
|
125
|
+
|
126
|
+
context "when there is a matching UID" do
|
127
|
+
it "returns the index of the matcing UID" do
|
128
|
+
expect(subject.index(42)).to eq(0)
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
context "when there isn't a matching UID" do
|
133
|
+
it "is nil" do
|
134
|
+
expect(subject.index(99)).to be nil
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
describe "#rename" do
|
140
|
+
before do
|
141
|
+
allow(File).to receive(:rename)
|
142
|
+
|
143
|
+
subject.rename("new_path")
|
144
|
+
end
|
145
|
+
|
146
|
+
context "when the metadata file exists" do
|
147
|
+
it "sets the folder_path" do
|
148
|
+
expect(subject.folder_path).to eq("new_path")
|
149
|
+
end
|
150
|
+
|
151
|
+
it "renames the metadata file" do
|
152
|
+
expect(File).to have_received(:rename).with(pathname, "new_path.imap")
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
context "when the metadata file doesn't exist" do
|
157
|
+
let(:exists) { false }
|
158
|
+
|
159
|
+
it "sets the folder_path" do
|
160
|
+
expect(subject.folder_path).to eq("new_path")
|
161
|
+
end
|
162
|
+
|
163
|
+
it "doesn't try to rename the metadata file" do
|
164
|
+
expect(File).to_not have_received(:rename)
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
describe "#uid_validity" do
|
170
|
+
it "returns the uid_validity" do
|
171
|
+
expect(subject.uid_validity).to eq(99)
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
describe "#uid_validity=" do
|
176
|
+
before { subject.uid_validity = 567 }
|
177
|
+
|
178
|
+
it "updates the uid_validity" do
|
179
|
+
expect(subject.uid_validity).to eq(567)
|
180
|
+
end
|
181
|
+
|
182
|
+
it "saves the file" do
|
183
|
+
expect(file).to have_received(:write).
|
184
|
+
with(/"uid_validity":567/)
|
185
|
+
end
|
186
|
+
|
187
|
+
context "when no metadata file exists" do
|
188
|
+
let(:exists) { false }
|
189
|
+
|
190
|
+
it "saves an empty list of UIDs" do
|
191
|
+
expect(file).to have_received(:write).
|
192
|
+
with(/"uids":\[\]/)
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
describe "#update_uid" do
|
198
|
+
before { subject.update_uid(42, 57) }
|
199
|
+
|
200
|
+
it "sets the UID" do
|
201
|
+
expect(subject.uids).to eq([57])
|
202
|
+
end
|
203
|
+
|
204
|
+
it "saves the file" do
|
205
|
+
expect(file).to have_received(:write).
|
206
|
+
with(/"uids":\[57\]/)
|
207
|
+
end
|
208
|
+
|
209
|
+
context "when the UID is not present" do
|
210
|
+
let(:existing) { {uid_validity: 99, uids: [2]} }
|
211
|
+
|
212
|
+
it "doesn't save the file" do
|
213
|
+
expect(file).to_not have_received(:write)
|
214
|
+
end
|
215
|
+
end
|
216
|
+
end
|
217
|
+
end
|
218
|
+
end
|