imap-backup 1.4.2 → 2.0.0.rc1

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.
@@ -3,7 +3,7 @@ module BackupDirectoryHelpers
3
3
  from = fixture("connection")[:username]
4
4
  subject = options[:subject]
5
5
  body = options[:body]
6
- body_and_headers = <<-EOT.gsub("\n", "\r\n")
6
+ body_and_headers = <<-EOT
7
7
  From: #{from}
8
8
  Subject: #{subject}
9
9
 
@@ -13,6 +13,14 @@ Subject: #{subject}
13
13
  "From #{from}\n#{body_and_headers}\n"
14
14
  end
15
15
 
16
+ def imap_data(uid_validity, uids)
17
+ {
18
+ version: 2,
19
+ uid_validity: uid_validity,
20
+ uids: uids
21
+ }
22
+ end
23
+
16
24
  def mbox_content(name)
17
25
  File.read(mbox_path(name))
18
26
  end
@@ -20,6 +28,18 @@ Subject: #{subject}
20
28
  def mbox_path(name)
21
29
  File.join(local_backup_path, name + ".mbox")
22
30
  end
31
+
32
+ def imap_path(name)
33
+ File.join(local_backup_path, name + ".imap")
34
+ end
35
+
36
+ def imap_content(name)
37
+ File.read(imap_path(name))
38
+ end
39
+
40
+ def imap_parsed(name)
41
+ JSON.parse(imap_content(name), symbolize_names: true)
42
+ end
23
43
  end
24
44
 
25
45
  RSpec.configure do |config|
@@ -1,18 +1,43 @@
1
1
  module EmailServerHelpers
2
2
  REQUESTED_ATTRIBUTES = ["RFC822", "FLAGS", "INTERNALDATE"]
3
+ DEFAULT_EMAIL = "address@example.org"
3
4
 
4
5
  def send_email(folder, options)
5
- from = options[:from] || "address@example.org"
6
+ message = message_as_server_message(options)
7
+ imap.append(folder, message, nil, nil)
8
+ end
9
+
10
+ def message_as_server_message(options)
11
+ from = options[:from] || DEFAULT_EMAIL
6
12
  subject = options[:subject]
7
13
  body = options[:body]
8
- message = <<-EOT
14
+ message = <<-EOT.gsub("\n", "\r\n")
9
15
  From: #{from}
10
16
  Subject: #{subject}
11
17
 
12
18
  #{body}
19
+
13
20
  EOT
21
+ end
14
22
 
15
- imap.append(folder, message, nil, nil)
23
+ def server_messages(folder)
24
+ server_uids(folder).map do |uid|
25
+ server_fetch_email(folder, uid)
26
+ end
27
+ end
28
+
29
+ def server_message_to_body(message)
30
+ message["RFC822"]
31
+ end
32
+
33
+ def server_fetch_email(folder, uid)
34
+ examine folder
35
+ fetch_data_items = imap.uid_fetch([uid.to_i], REQUESTED_ATTRIBUTES)
36
+ return nil if fetch_data_items.nil?
37
+ fetch_data_item = fetch_data_items[0]
38
+ attributes = fetch_data_item.attr
39
+ attributes["RFC822"].force_encoding("utf-8")
40
+ attributes
16
41
  end
17
42
 
18
43
  def delete_emails(folder)
@@ -22,6 +47,44 @@ Subject: #{subject}
22
47
  imap.expunge
23
48
  end
24
49
 
50
+ def examine(folder)
51
+ imap.examine(folder)
52
+ end
53
+
54
+ def server_uids(folder)
55
+ examine(folder)
56
+ imap.uid_search(["ALL"]).sort
57
+ end
58
+
59
+ def server_uid_validity(folder)
60
+ examine(folder)
61
+ imap.responses["UIDVALIDITY"][0]
62
+ end
63
+
64
+ def server_folders
65
+ root_info = imap.list("", "")[0]
66
+ root = root_info.name
67
+ imap.list(root, "*")
68
+ end
69
+
70
+ def server_create_folder(folder)
71
+ imap.create(folder)
72
+ imap.disconnect
73
+ @imap = nil
74
+ end
75
+
76
+ def server_rename_folder(from, to)
77
+ imap.rename(from, to)
78
+ imap.disconnect
79
+ @imap = nil
80
+ end
81
+
82
+ def server_delete_folder(folder)
83
+ imap.delete folder
84
+ imap.disconnect
85
+ @imap = nil
86
+ end
87
+
25
88
  def imap
26
89
  @imap ||=
27
90
  begin
@@ -1,6 +1,8 @@
1
1
  shared_context "message-fixtures" do
2
2
  let(:uid1) { 123 }
3
3
  let(:uid2) { 345 }
4
+ let(:uid3) { 567 }
4
5
  let(:msg1) { {uid: uid1, subject: "Test 1", body: "body 1\nHi"} }
5
6
  let(:msg2) { {uid: uid2, subject: "Test 2", body: "body 2"} }
7
+ let(:msg3) { {uid: uid3, subject: "Test 3", body: "body 3"} }
6
8
  end
@@ -2,6 +2,5 @@
2
2
  :username: 'address@example.org'
3
3
  :password: 'pass'
4
4
  :connection_options:
5
- :port: 8993
6
- :ssl:
7
- :verify_mode: 0
5
+ :port: 8143
6
+ :ssl: false
@@ -10,7 +10,7 @@ describe Imap::Backup::Account::Connection do
10
10
  end
11
11
 
12
12
  let(:imap) do
13
- double("Net::IMAP", login: nil, disconnect: nil)
13
+ instance_double(Net::IMAP, login: nil, disconnect: nil)
14
14
  end
15
15
  let(:imap_folders) { [] }
16
16
  let(:options) do
@@ -86,7 +86,9 @@ describe Imap::Backup::Account::Connection do
86
86
  end
87
87
 
88
88
  context "#status" do
89
- let(:folder) { double("folder", uids: [remote_uid]) }
89
+ let(:folder) do
90
+ instance_double(Imap::Backup::Account::Folder, uids: [remote_uid])
91
+ end
90
92
  let(:local_uid) { "local_uid" }
91
93
  let(:serializer) do
92
94
  instance_double(Imap::Backup::Serializer::Mbox, uids: [local_uid])
@@ -112,9 +114,21 @@ describe Imap::Backup::Account::Connection do
112
114
  end
113
115
 
114
116
  context "#run_backup" do
115
- let(:folder) { double("folder", name: "folder") }
116
- let(:serializer) { double("serializer") }
117
- let(:downloader) { double(Imap::Backup::Downloader, run: nil) }
117
+ let(:folder) do
118
+ instance_double(
119
+ Imap::Backup::Account::Folder,
120
+ name: "folder",
121
+ uid_validity: uid_validity
122
+ )
123
+ end
124
+ let(:uid_validity) { 123 }
125
+ let(:serializer) do
126
+ instance_double(
127
+ Imap::Backup::Serializer::Mbox,
128
+ set_uid_validity: nil
129
+ )
130
+ end
131
+ let(:downloader) { instance_double(Imap::Backup::Downloader, run: nil) }
118
132
 
119
133
  before do
120
134
  allow(Imap::Backup::Downloader).
@@ -143,7 +143,8 @@ describe Imap::Backup::Configuration::Account do
143
143
  context "if the server is blank" do
144
144
  [
145
145
  ["GMail", "foo@gmail.com", "imap.gmail.com"],
146
- ["Fastmail", "bar@fastmail.fm", "mail.messagingengine.com"]
146
+ ["Fastmail", "bar@fastmail.fm", "imap.fastmail.com"],
147
+ ["Fastmail", "bar@fastmail.com", "imap.fastmail.com"]
147
148
  ].each do |service, email, expected|
148
149
  context service do
149
150
  let(:new_email) { email }
@@ -26,11 +26,7 @@ describe Email::Provider do
26
26
 
27
27
  describe "#options" do
28
28
  it "returns options" do
29
- expect(subject.options).to be_a(Hash)
30
- end
31
-
32
- it "forces TLSv1_2" do
33
- expect(subject.options[:ssl][:ssl_version]).to eq(:TLSv1_2)
29
+ expect(subject.options).to eq(port: 993, ssl: true)
34
30
  end
35
31
  end
36
32
 
@@ -1,119 +1,82 @@
1
- require "spec_helper"
2
-
3
1
  describe Imap::Backup::Serializer::Mbox do
4
- let(:stat) { double("File::Stat", mode: 0o700) }
5
2
  let(:base_path) { "/base/path" }
6
- let(:mbox_pathname) { "/base/path/my/folder.mbox" }
7
- let(:mbox_exists) { true }
8
- let(:imap_pathname) { "/base/path/my/folder.imap" }
9
- let(:imap_exists) { true }
3
+ let(:store) do
4
+ instance_double(
5
+ Imap::Backup::Serializer::MboxStore,
6
+ add: nil,
7
+ uids: nil
8
+ )
9
+ end
10
+ let(:imap_folder) { "folder" }
11
+ let(:permissions) { 0o700 }
12
+ let(:dir_exists) { true }
10
13
 
11
14
  before do
12
15
  allow(Imap::Backup::Utils).to receive(:make_folder)
13
- allow(File).to receive(:exist?).with(base_path).and_return(true)
14
- allow(File).to receive(:stat).with(base_path).and_return(stat)
15
- allow(File).to receive(:exist?).with(mbox_pathname).and_return(mbox_exists)
16
- allow(File).to receive(:exist?).with(imap_pathname).and_return(imap_exists)
16
+ allow(Imap::Backup::Utils).to receive(:mode) { permissions }
17
+ allow(Imap::Backup::Utils).to receive(:check_permissions) { true }
18
+ allow(File).to receive(:directory?) { dir_exists }
17
19
  end
18
20
 
19
- context "#initialize" do
20
- it "creates the containing directory" do
21
- described_class.new(base_path, "my/folder")
21
+ subject { described_class.new(base_path, imap_folder) }
22
22
 
23
- expect(Imap::Backup::Utils).
24
- to have_received(:make_folder).with(base_path, "my", 0o700)
25
- end
23
+ before do
24
+ allow(FileUtils).to receive(:chmod)
25
+ allow(Imap::Backup::Serializer::MboxStore).to receive(:new) { store }
26
+ end
26
27
 
27
- context "mbox and imap files" do
28
- context "if mbox exists and imap doesn't" do
29
- let(:imap_exists) { false }
28
+ context "containing directory" do
29
+ before { subject.uids }
30
30
 
31
- it "fails" do
32
- expect {
33
- described_class.new(base_path, "my/folder")
34
- }.to raise_error(RuntimeError, ".imap file missing")
35
- end
36
- end
31
+ context "when the IMAP folder has multiple elements" do
32
+ let(:imap_folder) { "folder/path" }
37
33
 
38
- context "if imap exists and mbox doesn't" do
39
- let(:mbox_exists) { false }
34
+ context "when the containing directory is missing" do
35
+ let(:dir_exists) { false }
40
36
 
41
- it "fails" do
42
- expect {
43
- described_class.new(base_path, "my/folder")
44
- }.to raise_error(RuntimeError, ".mbox file missing")
37
+ it "is created" do
38
+ expect(Imap::Backup::Utils).to have_received(:make_folder).
39
+ with(base_path, File.dirname(imap_folder), 0o700)
45
40
  end
46
41
  end
47
42
  end
48
- end
49
43
 
50
- context "instance methods" do
51
- let(:ids) { %w(3 2 1) }
44
+ context "when the containing directory permissons are incorrect" do
45
+ let(:permissions) { 0o777 }
52
46
 
53
- before do
54
- allow(CSV).to receive(:foreach) { |&b| ids.each { |id| b.call [id] } }
55
- end
56
-
57
- subject { described_class.new(base_path, "my/folder") }
58
-
59
- context "#uids" do
60
- it "returns the backed-up uids as sorted integers" do
61
- expect(subject.uids).to eq(ids.map(&:to_i).sort)
62
- end
63
-
64
- context "if the mbox does not exist" do
65
- let(:mbox_exists) { false }
66
- let(:imap_exists) { false }
67
-
68
- it "returns an empty Array" do
69
- expect(subject.uids).to eq([])
70
- end
47
+ it "corrects them" do
48
+ path = File.expand_path(File.join(base_path, File.dirname(imap_folder)))
49
+ expect(FileUtils).to have_received(:chmod).with(0o700, path)
71
50
  end
72
51
  end
73
52
 
74
- context "#save" do
75
- let(:mbox_formatted_message) { "message in mbox format" }
76
- let(:message_uid) { "999" }
77
- let(:message) do
78
- double("Email::Mboxrd::Message", to_serialized: mbox_formatted_message)
53
+ context "when the containing directory permissons are correct" do
54
+ it "does nothing" do
55
+ expect(FileUtils).to_not have_received(:chmod)
79
56
  end
80
- let(:mbox_file) { double("File - mbox", write: nil, close: nil) }
81
- let(:imap_file) { double("File - imap", write: nil, close: nil) }
82
-
83
- before do
84
- allow(Email::Mboxrd::Message).to receive(:new).and_return(message)
85
- allow(File).to receive(:open).with(mbox_pathname, "ab") { mbox_file }
86
- allow(File).to receive(:open).with(imap_pathname, "ab") { imap_file }
87
- end
88
-
89
- it "saves the message to the mbox" do
90
- subject.save(message_uid, "The\nemail\n")
57
+ end
91
58
 
92
- expect(mbox_file).to have_received(:write).with(mbox_formatted_message)
59
+ context "when the containing directory exists" do
60
+ it "is not created" do
61
+ expect(Imap::Backup::Utils).to_not have_received(:make_folder).
62
+ with(base_path, File.dirname(imap_folder), 0o700)
93
63
  end
64
+ end
65
+ end
94
66
 
95
- it "saves the uid to the imap file" do
96
- subject.save(message_uid, "The\nemail\n")
97
-
98
- expect(imap_file).to have_received(:write).with(message_uid + "\n")
99
- end
67
+ context "#uids" do
68
+ it "calls the store" do
69
+ subject.uids
100
70
 
101
- context "when the message causes parsing errors" do
102
- before do
103
- allow(message).to receive(:to_serialized).and_raise(ArgumentError)
104
- end
71
+ expect(store).to have_received(:uids)
72
+ end
73
+ end
105
74
 
106
- it "skips the message" do
107
- subject.save(message_uid, "The\nemail\n")
108
- expect(mbox_file).to_not have_received(:write)
109
- end
75
+ context "#save" do
76
+ it "calls the store" do
77
+ subject.save("foo", "bar")
110
78
 
111
- it "does not fail" do
112
- expect do
113
- subject.save(message_uid, "The\nemail\n")
114
- end.to_not raise_error
115
- end
116
- end
79
+ expect(store).to have_received(:add)
117
80
  end
118
81
  end
119
82
  end
@@ -0,0 +1,117 @@
1
+ describe Imap::Backup::Serializer::MboxStore do
2
+ let(:base_path) { "/base/path" }
3
+ let(:folder) { "the/folder" }
4
+ let(:folder_path) { File.join(base_path, folder) }
5
+ let(:imap_pathname) { folder_path + ".imap" }
6
+ let(:imap_exists) { true }
7
+ let(:imap_file) { double("File - imap", write: nil, close: nil) }
8
+ let(:mbox_pathname) { folder_path + ".mbox" }
9
+ let(:mbox_exists) { true }
10
+ let(:mbox_file) { double("File - mbox", write: nil, close: nil) }
11
+ let(:uids) { [3, 2, 1] }
12
+ let(:imap_content) do
13
+ {
14
+ version: Imap::Backup::Serializer::MboxStore::CURRENT_VERSION,
15
+ uid_validity: 123,
16
+ uids: uids.sort
17
+ }.to_json
18
+ end
19
+
20
+ subject { described_class.new(base_path, folder) }
21
+
22
+ before do
23
+ allow(File).to receive(:exist?).and_call_original
24
+ allow(File).to receive(:exist?).with(imap_pathname) { imap_exists }
25
+ allow(File).to receive(:exist?).with(mbox_pathname) { mbox_exists }
26
+
27
+ allow(File).to receive(:open).and_call_original
28
+ allow(File).
29
+ to receive(:open).with("/base/path/my/folder.imap") { imap_content }
30
+ allow(File).to receive(:open).with(imap_pathname, "w").and_yield(imap_file)
31
+ allow(File).to receive(:open).with(mbox_pathname, "w").and_yield(mbox_file)
32
+
33
+ allow(File).to receive(:read).and_call_original
34
+ allow(File).to receive(:read).with(imap_pathname) { imap_content }
35
+
36
+ allow(File).to receive(:unlink).and_call_original
37
+ allow(File).to receive(:unlink).with(imap_pathname)
38
+ allow(File).to receive(:unlink).with(mbox_pathname)
39
+
40
+ allow(FileUtils).to receive(:chmod)
41
+ end
42
+
43
+ context "#uids" do
44
+ it "returns the backed-up uids as sorted integers" do
45
+ expect(subject.uids).to eq(uids.map(&:to_i).sort)
46
+ end
47
+
48
+ context "when the imap file does not exist" do
49
+ let(:imap_exists) { false }
50
+
51
+ it "returns an empty Array" do
52
+ expect(subject.uids).to eq([])
53
+ end
54
+ end
55
+
56
+ context "when the mbox does not exist" do
57
+ let(:mbox_exists) { false }
58
+
59
+ it "returns an empty Array" do
60
+ expect(subject.uids).to eq([])
61
+ end
62
+ end
63
+ end
64
+
65
+ context "#add" do
66
+ let(:mbox_formatted_message) { "message in mbox format" }
67
+ let(:message_uid) { "999" }
68
+ let(:message) do
69
+ instance_double(
70
+ Email::Mboxrd::Message,
71
+ to_serialized: mbox_formatted_message
72
+ )
73
+ end
74
+ let(:updated_imap_content) do
75
+ {
76
+ version: Imap::Backup::Serializer::MboxStore::CURRENT_VERSION,
77
+ uid_validity: 123,
78
+ uids: (uids + [999]).sort
79
+ }.to_json
80
+ end
81
+
82
+ before do
83
+ allow(Email::Mboxrd::Message).to receive(:new).and_return(message)
84
+ allow(File).to receive(:open).with(mbox_pathname, "ab") { mbox_file }
85
+ subject.uid_validity = 123
86
+ end
87
+
88
+ it "saves the message to the mbox" do
89
+ subject.add(message_uid, "The\nemail\n")
90
+
91
+ expect(mbox_file).to have_received(:write).with(mbox_formatted_message)
92
+ end
93
+
94
+ it "saves the uid to the imap file" do
95
+ subject.add(message_uid, "The\nemail\n")
96
+
97
+ expect(imap_file).to have_received(:write).with(updated_imap_content)
98
+ end
99
+
100
+ context "when the message causes parsing errors" do
101
+ before do
102
+ allow(message).to receive(:to_serialized).and_raise(ArgumentError)
103
+ end
104
+
105
+ it "skips the message" do
106
+ subject.add(message_uid, "The\nemail\n")
107
+ expect(mbox_file).to_not have_received(:write)
108
+ end
109
+
110
+ it "does not fail" do
111
+ expect do
112
+ subject.add(message_uid, "The\nemail\n")
113
+ end.to_not raise_error
114
+ end
115
+ end
116
+ end
117
+ end