imap-backup 5.2.0 → 6.0.0.rc2
Sign up to get free protection for your applications and to get access to all the features.
- 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
|