imap-backup 5.0.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.
Files changed (70) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +8 -7
  3. data/bin/imap-backup +4 -0
  4. data/docs/development.md +10 -4
  5. data/imap-backup.gemspec +2 -7
  6. data/lib/cli_coverage.rb +18 -0
  7. data/lib/imap/backup/account/connection.rb +7 -11
  8. data/lib/imap/backup/account/folder.rb +0 -16
  9. data/lib/imap/backup/account.rb +31 -11
  10. data/lib/imap/backup/cli/folders.rb +3 -3
  11. data/lib/imap/backup/cli/migrate.rb +3 -3
  12. data/lib/imap/backup/cli/restore.rb +20 -4
  13. data/lib/imap/backup/cli/utils.rb +2 -2
  14. data/lib/imap/backup/cli.rb +6 -7
  15. data/lib/imap/backup/configuration.rb +1 -11
  16. data/lib/imap/backup/downloader.rb +13 -9
  17. data/lib/imap/backup/serializer/directory.rb +37 -0
  18. data/lib/imap/backup/serializer/imap.rb +120 -0
  19. data/lib/imap/backup/serializer/mbox.rb +23 -94
  20. data/lib/imap/backup/serializer/mbox_enumerator.rb +2 -0
  21. data/lib/imap/backup/serializer.rb +180 -3
  22. data/lib/imap/backup/setup/account.rb +52 -29
  23. data/lib/imap/backup/setup/helpers.rb +1 -1
  24. data/lib/imap/backup/thunderbird/mailbox_exporter.rb +1 -1
  25. data/lib/imap/backup/version.rb +2 -2
  26. data/lib/imap/backup.rb +0 -1
  27. data/spec/features/backup_spec.rb +22 -29
  28. data/spec/features/restore_spec.rb +8 -6
  29. data/spec/features/support/aruba.rb +12 -3
  30. data/spec/features/support/backup_directory.rb +0 -4
  31. data/spec/features/support/email_server.rb +0 -1
  32. data/spec/spec_helper.rb +4 -9
  33. data/spec/unit/imap/backup/account/connection_spec.rb +36 -8
  34. data/spec/unit/imap/backup/account/folder_spec.rb +18 -16
  35. data/spec/unit/imap/backup/account_spec.rb +246 -0
  36. data/spec/unit/imap/backup/cli/accounts_spec.rb +12 -1
  37. data/spec/unit/imap/backup/cli/backup_spec.rb +19 -0
  38. data/spec/unit/imap/backup/cli/folders_spec.rb +39 -0
  39. data/spec/unit/imap/backup/cli/local_spec.rb +26 -7
  40. data/spec/unit/imap/backup/cli/migrate_spec.rb +80 -0
  41. data/spec/unit/imap/backup/cli/restore_spec.rb +67 -0
  42. data/spec/unit/imap/backup/cli/setup_spec.rb +17 -0
  43. data/spec/unit/imap/backup/cli/utils_spec.rb +68 -5
  44. data/spec/unit/imap/backup/cli_spec.rb +93 -0
  45. data/spec/unit/imap/backup/client/apple_mail_spec.rb +9 -0
  46. data/spec/unit/imap/backup/configuration_spec.rb +2 -2
  47. data/spec/unit/imap/backup/downloader_spec.rb +60 -8
  48. data/spec/unit/imap/backup/logger_spec.rb +1 -1
  49. data/spec/unit/imap/backup/migrator_spec.rb +1 -1
  50. data/spec/unit/imap/backup/sanitizer_spec.rb +42 -0
  51. data/spec/unit/imap/backup/serializer/directory_spec.rb +37 -0
  52. data/spec/unit/imap/backup/serializer/imap_spec.rb +218 -0
  53. data/spec/unit/imap/backup/serializer/mbox_spec.rb +62 -183
  54. data/spec/unit/imap/backup/serializer_spec.rb +296 -0
  55. data/spec/unit/imap/backup/setup/account_spec.rb +120 -25
  56. data/spec/unit/imap/backup/setup/helpers_spec.rb +15 -0
  57. data/spec/unit/imap/backup/thunderbird/mailbox_exporter_spec.rb +116 -0
  58. data/spec/unit/imap/backup/uploader_spec.rb +1 -1
  59. data/spec/unit/retry_on_error_spec.rb +34 -0
  60. metadata +44 -37
  61. data/lib/imap/backup/serializer/mbox_store.rb +0 -217
  62. data/lib/thunderbird/install.rb +0 -16
  63. data/lib/thunderbird/local_folder.rb +0 -65
  64. data/lib/thunderbird/profile.rb +0 -30
  65. data/lib/thunderbird/profiles.rb +0 -71
  66. data/lib/thunderbird/subdirectory.rb +0 -93
  67. data/lib/thunderbird/subdirectory_placeholder.rb +0 -21
  68. data/lib/thunderbird.rb +0 -14
  69. data/spec/gather_rspec_coverage.rb +0 -1
  70. data/spec/unit/imap/backup/serializer/mbox_store_spec.rb +0 -329
@@ -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
@@ -1,222 +1,101 @@
1
- describe Imap::Backup::Serializer::Mbox do
2
- subject { described_class.new(base_path, imap_folder) }
3
-
4
- let(:base_path) { "/base/path" }
5
- let(:store) do
6
- instance_double(
7
- Imap::Backup::Serializer::MboxStore,
8
- add: nil,
9
- rename: nil,
10
- uids: nil,
11
- uid_validity: existing_uid_validity,
12
- "uid_validity=": nil,
13
- update_uid: nil
14
- )
15
- end
16
- let(:imap_folder) { "folder" }
17
- let(:permissions) { 0o700 }
18
- let(:dir_exists) { true }
19
- let(:existing_uid_validity) { nil }
20
-
21
- before do
22
- allow(Imap::Backup::Utils).to receive(:make_folder)
23
- allow(Imap::Backup::Utils).to receive(:mode) { permissions }
24
- allow(Imap::Backup::Utils).to receive(:check_permissions) { true }
25
- allow(File).to receive(:directory?) { dir_exists }
26
- allow(FileUtils).to receive(:chmod)
27
- allow(Imap::Backup::Serializer::MboxStore).to receive(:new) { store }
28
- end
29
-
30
- describe "folder path" do
31
- context "when it has multiple elements" do
32
- let(:imap_folder) { "folder/path" }
33
-
34
- context "when the containing directory is missing" do
35
- let(:dir_exists) { false }
36
-
37
- it "is created" do
38
- expect(Imap::Backup::Utils).to receive(:make_folder).
39
- with(base_path, File.dirname(imap_folder), 0o700)
40
-
41
- subject.uids
42
- end
43
- end
1
+ module Imap::Backup
2
+ describe Serializer::Mbox do
3
+ subject { described_class.new(folder_path) }
4
+
5
+ let(:folder_path) { "folder_path" }
6
+ let(:pathname) { "folder_path.mbox" }
7
+ let(:exists) { true }
8
+ let(:file) { instance_double(File, truncate: nil, write: nil) }
9
+
10
+ before do
11
+ allow(File).to receive(:exist?).and_call_original
12
+ allow(File).to receive(:exist?).with(pathname) { exists }
13
+ allow(File).to receive(:open).with(pathname, "ab").and_yield(file)
14
+ allow(File).to receive(:read).with(pathname) { existing.to_json }
44
15
  end
45
16
 
46
- context "when permissions are incorrect" do
47
- let(:permissions) { 0o777 }
48
-
49
- it "corrects them" do
50
- path = File.expand_path(File.join(base_path, File.dirname(imap_folder)))
51
- expect(FileUtils).to receive(:chmod).with(0o700, path)
17
+ describe "#append" do
18
+ it "appends the message" do
19
+ subject.append("message")
52
20
 
53
- subject.uids
21
+ expect(file).to have_received(:write).with("message")
54
22
  end
55
23
  end
56
24
 
57
- context "when permissons are correct" do
58
- it "does nothing" do
59
- expect(FileUtils).to_not receive(:chmod)
60
-
61
- subject.uids
25
+ describe "#exist?" do
26
+ context "when the mailbox exists" do
27
+ it "is true" do
28
+ expect(subject.exist?).to be true
29
+ end
62
30
  end
63
- end
64
31
 
65
- context "when it exists" do
66
- it "is not created" do
67
- expect(Imap::Backup::Utils).to_not receive(:make_folder).
68
- with(base_path, File.dirname(imap_folder), 0o700)
32
+ context "when the mailbox doesn't exist" do
33
+ let(:exists) { false }
69
34
 
70
- subject.uids
35
+ it "is false" do
36
+ expect(subject.exist?).to be false
37
+ end
71
38
  end
72
39
  end
73
- end
74
-
75
- describe "#apply_uid_validity" do
76
- context "when the existing uid validity is unset" do
77
- it "sets uid validity" do
78
- expect(store).to receive(:uid_validity=).with("aaa")
79
40
 
80
- subject.apply_uid_validity("aaa")
81
- end
41
+ describe "#length" do
42
+ let(:stat) { instance_double(File::Stat, size: 99) }
82
43
 
83
- it "does not rename the store" do
84
- expect(store).to_not receive(:rename)
44
+ before { allow(File).to receive(:stat) { stat } }
85
45
 
86
- subject.apply_uid_validity("aaa")
87
- end
88
-
89
- it "returns nil" do
90
- expect(subject.apply_uid_validity("aaa")).to be_nil
46
+ it "returns the length of the mailbox file" do
47
+ expect(subject.length).to eq(99)
91
48
  end
92
49
  end
93
50
 
94
- context "when the uid validity is unchanged" do
95
- let(:existing_uid_validity) { "aaa" }
96
-
97
- it "does not set uid validity" do
98
- expect(store).to_not receive(:uid_validity=)
99
-
100
- subject.apply_uid_validity("aaa")
101
- end
102
-
103
- it "does not rename the store" do
104
- expect(store).to_not receive(:rename)
105
-
106
- subject.apply_uid_validity("aaa")
107
- end
108
-
109
- it "returns nil" do
110
- expect(subject.apply_uid_validity("aaa")).to be_nil
51
+ describe "#pathname" do
52
+ it "is the folder_path plus .mbox" do
53
+ expect(subject.pathname).to eq("folder_path.mbox")
111
54
  end
112
55
  end
113
56
 
114
- context "when the uid validity is changed" do
115
- let(:existing_uid_validity) { "bbb" }
116
- let(:existing_store) do
117
- instance_double(Imap::Backup::Serializer::MboxStore)
118
- end
119
- let(:exists) { false }
120
-
121
- before do
122
- allow(Imap::Backup::Serializer::MboxStore).
123
- to receive(:new).with(anything, /bbb/) { existing_store }
124
- allow(existing_store).to receive(:exist?).and_return(exists, false)
125
- end
126
-
127
- it "sets uid validity" do
128
- expect(store).to receive(:uid_validity=).with("aaa")
57
+ describe "#rename" do
58
+ context "when the mailbox exists" do
59
+ let(:exists) { true }
129
60
 
130
- subject.apply_uid_validity("aaa")
131
- end
61
+ before do
62
+ allow(File).to receive(:rename)
132
63
 
133
- context "when adding the uid validity does not cause a name clash" do
134
- it "renames the store, adding the existing uid validity" do
135
- expect(store).to receive(:rename).with("folder.bbb")
64
+ subject.rename("new_name")
65
+ end
136
66
 
137
- subject.apply_uid_validity("aaa")
67
+ it "renames the mailbox" do
68
+ expect(File).to have_received(:rename)
138
69
  end
139
70
 
140
- it "returns the new name" do
141
- expect(subject.apply_uid_validity("aaa")).to eq("folder.bbb")
71
+ it "sets the folder_path" do
72
+ expect(subject.folder_path).to eq("new_name")
142
73
  end
143
74
  end
144
75
 
145
- context "when adding the uid validity causes a name clash" do
146
- let(:exists) { true }
147
-
148
- it "renames the store, adding the existing uid validity and a digit" do
149
- expect(store).to receive(:rename).with("folder.bbb.1")
76
+ context "when the mailbox doesn't exist" do
77
+ let(:exists) { false }
150
78
 
151
- subject.apply_uid_validity("aaa")
152
- end
79
+ it "sets the folder_path" do
80
+ subject.rename("new_name")
153
81
 
154
- it "returns the new name" do
155
- expect(subject.apply_uid_validity("aaa")).to eq("folder.bbb.1")
82
+ expect(subject.folder_path).to eq("new_name")
156
83
  end
157
84
  end
158
85
  end
159
- end
160
-
161
- describe "#force_uid_validity" do
162
- it "sets the uid_validity" do
163
- expect(store).to receive(:uid_validity=).with("66")
164
-
165
- subject.force_uid_validity("66")
166
- end
167
- end
168
-
169
- describe "#uids" do
170
- it "calls the store" do
171
- expect(store).to receive(:uids)
172
-
173
- subject.uids
174
- end
175
- end
176
-
177
- describe "#load" do
178
- before { allow(store).to receive(:load).with("66") { "xxx" } }
179
-
180
- it "returns the value loaded by the store" do
181
- expect(subject.load("66")).to eq("xxx")
182
- end
183
- end
184
-
185
- describe "#each_message" do
186
- it "calls the store" do
187
- expect(store).to receive(:each_message).with([1])
188
86
 
189
- subject.each_message([1])
190
- end
191
- end
192
-
193
- describe "#save" do
194
- it "calls the store" do
195
- expect(store).to receive(:add).with("foo", "bar")
196
-
197
- subject.save("foo", "bar")
198
- end
199
- end
200
-
201
- describe "#rename" do
202
- it "calls the store" do
203
- expect(store).to receive(:rename).with("foo")
204
-
205
- subject.rename("foo")
206
- end
207
-
208
- it "updates the folder name" do
209
- subject.rename("foo")
210
-
211
- expect(subject.folder).to eq("foo")
212
- end
213
- end
87
+ describe "#rewind" do
88
+ before do
89
+ allow(File).to receive(:open).
90
+ with(pathname, File::RDWR | File::CREAT, 0o644).
91
+ and_yield(file)
92
+ end
214
93
 
215
- describe "#update_uid" do
216
- it "calls the store" do
217
- expect(store).to receive(:update_uid).with("foo", "bar")
94
+ it "truncates the mailbox" do
95
+ subject.rewind(123)
218
96
 
219
- subject.update_uid("foo", "bar")
97
+ expect(file).to have_received(:truncate).with(123)
98
+ end
220
99
  end
221
100
  end
222
101
  end