imap-backup 1.3.0 → 1.4.0
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/.gitignore +1 -0
- data/.rspec +3 -0
- data/.rubocop.yml +2 -1
- data/.travis.yml +1 -1
- data/README.md +12 -0
- data/bin/imap-backup +11 -6
- data/docker-compose.yml +15 -0
- data/imap-backup.gemspec +3 -2
- data/lib/email/mboxrd/message.rb +30 -4
- data/lib/email/provider.rb +2 -2
- data/lib/imap/backup.rb +0 -1
- data/lib/imap/backup/account/connection.rb +29 -15
- data/lib/imap/backup/account/folder.rb +3 -3
- data/lib/imap/backup/configuration/account.rb +15 -12
- data/lib/imap/backup/configuration/asker.rb +3 -1
- data/lib/imap/backup/configuration/folder_chooser.rb +5 -2
- data/lib/imap/backup/configuration/list.rb +12 -9
- data/lib/imap/backup/configuration/setup.rb +5 -3
- data/lib/imap/backup/configuration/store.rb +4 -4
- data/lib/imap/backup/downloader.rb +6 -2
- data/lib/imap/backup/serializer/base.rb +2 -2
- data/lib/imap/backup/serializer/mbox.rb +16 -7
- data/lib/imap/backup/utils.rb +8 -3
- data/lib/imap/backup/version.rb +1 -1
- data/spec/features/backup_spec.rb +27 -0
- data/spec/features/helper.rb +2 -0
- data/spec/features/support/backup_directory.rb +27 -0
- data/spec/features/support/email_server.rb +40 -0
- data/spec/features/support/shared/connection_context.rb +12 -0
- data/spec/features/support/shared/message_fixtures.rb +6 -0
- data/spec/fixtures/connection.yml +7 -0
- data/spec/support/fixtures.rb +6 -0
- data/spec/unit/account/connection_spec.rb +19 -7
- data/spec/unit/account/folder_spec.rb +15 -5
- data/spec/unit/configuration/account_spec.rb +18 -8
- data/spec/unit/configuration/asker_spec.rb +6 -3
- data/spec/unit/configuration/connection_tester_spec.rb +1 -1
- data/spec/unit/configuration/folder_chooser_spec.rb +21 -11
- data/spec/unit/configuration/list_spec.rb +13 -6
- data/spec/unit/configuration/setup_spec.rb +5 -3
- data/spec/unit/configuration/store_spec.rb +13 -8
- data/spec/unit/downloader_spec.rb +3 -1
- data/spec/unit/email/mboxrd/message_spec.rb +32 -13
- data/spec/unit/email/provider_spec.rb +7 -3
- data/spec/unit/serializer/base_spec.rb +3 -2
- data/spec/unit/serializer/mbox_spec.rb +9 -6
- data/spec/unit/utils_spec.rb +15 -13
- metadata +35 -5
- data/lib/imap/backup/serializer/directory.rb +0 -43
- data/spec/unit/serializer/directory_spec.rb +0 -69
@@ -9,7 +9,7 @@ module Imap::Backup
|
|
9
9
|
end
|
10
10
|
|
11
11
|
def setup_logging
|
12
|
-
return if
|
12
|
+
return if !config_exists?
|
13
13
|
Imap::Backup.setup_logging config
|
14
14
|
end
|
15
15
|
|
@@ -25,8 +25,9 @@ module Imap::Backup
|
|
25
25
|
|
26
26
|
def config
|
27
27
|
return @config if @config
|
28
|
-
if
|
29
|
-
|
28
|
+
if !config_exists?
|
29
|
+
path = Configuration::Store.default_pathname
|
30
|
+
raise ConfigurationNotFound, "Configuration file '#{path}' not found"
|
30
31
|
end
|
31
32
|
@config = Configuration::Store.new
|
32
33
|
end
|
@@ -36,12 +37,14 @@ module Imap::Backup
|
|
36
37
|
end
|
37
38
|
|
38
39
|
def accounts
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
40
|
+
@accounts ||=
|
41
|
+
if required_accounts.nil?
|
42
|
+
config.accounts
|
43
|
+
else
|
44
|
+
config.accounts.select do |account|
|
45
|
+
required_accounts.include?(account[:username])
|
46
|
+
end
|
47
|
+
end
|
45
48
|
end
|
46
49
|
end
|
47
50
|
end
|
@@ -69,10 +69,10 @@ module Imap::Backup
|
|
69
69
|
end
|
70
70
|
|
71
71
|
def default_account_config(username)
|
72
|
-
|
72
|
+
{
|
73
73
|
username: username,
|
74
74
|
password: "",
|
75
|
-
local_path: File.join(config.path, username.
|
75
|
+
local_path: File.join(config.path, username.tr("@", "_")),
|
76
76
|
folders: []
|
77
77
|
}
|
78
78
|
end
|
@@ -83,7 +83,9 @@ module Imap::Backup
|
|
83
83
|
account = default_account_config(username)
|
84
84
|
config.accounts << account
|
85
85
|
end
|
86
|
-
Configuration::Account.new(
|
86
|
+
Configuration::Account.new(
|
87
|
+
config, account, Configuration::Setup.highline
|
88
|
+
).run
|
87
89
|
end
|
88
90
|
end
|
89
91
|
end
|
@@ -29,7 +29,7 @@ module Imap::Backup
|
|
29
29
|
remove_modified_flags
|
30
30
|
remove_deleted_accounts
|
31
31
|
File.open(pathname, "w") { |f| f.write(JSON.pretty_generate(data)) }
|
32
|
-
FileUtils.chmod
|
32
|
+
FileUtils.chmod 0o600, pathname
|
33
33
|
end
|
34
34
|
|
35
35
|
def accounts
|
@@ -53,7 +53,7 @@ module Imap::Backup
|
|
53
53
|
def data
|
54
54
|
return @data if @data
|
55
55
|
if File.exist?(pathname)
|
56
|
-
Utils.check_permissions pathname,
|
56
|
+
Utils.check_permissions pathname, 0o600
|
57
57
|
contents = File.read(pathname)
|
58
58
|
@data = JSON.parse(contents, symbolize_names: true)
|
59
59
|
else
|
@@ -76,8 +76,8 @@ module Imap::Backup
|
|
76
76
|
if !File.directory?(path)
|
77
77
|
FileUtils.mkdir path
|
78
78
|
end
|
79
|
-
if Utils::stat(path) !=
|
80
|
-
FileUtils.chmod
|
79
|
+
if Utils::stat(path) != 0o700
|
80
|
+
FileUtils.chmod 0o700, path
|
81
81
|
end
|
82
82
|
end
|
83
83
|
end
|
@@ -13,10 +13,14 @@ module Imap::Backup
|
|
13
13
|
uids.each do |uid|
|
14
14
|
message = folder.fetch(uid)
|
15
15
|
if message.nil?
|
16
|
-
Imap::Backup.logger.debug
|
16
|
+
Imap::Backup.logger.debug(
|
17
|
+
"[#{folder.name}] #{uid} - not available - skipped"
|
18
|
+
)
|
17
19
|
next
|
18
20
|
end
|
19
|
-
Imap::Backup.logger.debug
|
21
|
+
Imap::Backup.logger.debug(
|
22
|
+
"[#{folder.name}] #{uid} - #{message['RFC822'].size} bytes"
|
23
|
+
)
|
20
24
|
serializer.save(uid, message)
|
21
25
|
end
|
22
26
|
end
|
@@ -17,7 +17,7 @@ module Imap::Backup
|
|
17
17
|
return @uids if @uids
|
18
18
|
|
19
19
|
@uids = []
|
20
|
-
return @uids if
|
20
|
+
return @uids if !exist?
|
21
21
|
|
22
22
|
CSV.foreach(imap_pathname) do |row|
|
23
23
|
@uids << row[0]
|
@@ -29,7 +29,9 @@ module Imap::Backup
|
|
29
29
|
def save(uid, message)
|
30
30
|
uid = uid.to_s
|
31
31
|
if uids.include?(uid)
|
32
|
-
Imap::Backup.logger.debug
|
32
|
+
Imap::Backup.logger.debug(
|
33
|
+
"[#{folder}] message #{uid} already downloaded - skipping"
|
34
|
+
)
|
33
35
|
return
|
34
36
|
end
|
35
37
|
|
@@ -42,10 +44,15 @@ module Imap::Backup
|
|
42
44
|
begin
|
43
45
|
mbox = File.open(mbox_pathname, "ab")
|
44
46
|
imap = File.open(imap_pathname, "ab")
|
45
|
-
mbox.write mboxrd_message.
|
47
|
+
mbox.write mboxrd_message.to_serialized
|
46
48
|
imap.write uid + "\n"
|
47
49
|
rescue => e
|
48
|
-
|
50
|
+
message = <<-ERROR.gsub(/^\s*/m, "")
|
51
|
+
[#{folder}] failed to save message #{uid}:
|
52
|
+
#{body}. #{e}:
|
53
|
+
#{e.backtrace.join("\n")}"
|
54
|
+
ERROR
|
55
|
+
Imap::Backup.logger.warn message
|
49
56
|
ensure
|
50
57
|
mbox.close if mbox
|
51
58
|
imap.close if imap
|
@@ -57,14 +64,16 @@ module Imap::Backup
|
|
57
64
|
def assert_files
|
58
65
|
mbox = mbox_exist?
|
59
66
|
imap = imap_exist?
|
60
|
-
raise ".imap file missing" if mbox && (
|
61
|
-
raise ".mbox file missing" if imap && (
|
67
|
+
raise ".imap file missing" if mbox && (!imap)
|
68
|
+
raise ".mbox file missing" if imap && (!mbox)
|
62
69
|
end
|
63
70
|
|
64
71
|
def create_containing_directory
|
65
72
|
mbox_relative_path = File.dirname(mbox_relative_pathname)
|
66
73
|
return if mbox_relative_path == "."
|
67
|
-
Utils.make_folder(
|
74
|
+
Utils.make_folder(
|
75
|
+
path, mbox_relative_path, Serializer::DIRECTORY_PERMISSIONS
|
76
|
+
)
|
68
77
|
end
|
69
78
|
|
70
79
|
def exist?
|
data/lib/imap/backup/utils.rb
CHANGED
@@ -5,9 +5,14 @@ module Imap::Backup
|
|
5
5
|
def self.check_permissions(filename, limit)
|
6
6
|
actual = stat(filename)
|
7
7
|
return nil if actual.nil?
|
8
|
-
mask = ~limit &
|
8
|
+
mask = ~limit & 0o777
|
9
9
|
if actual & mask != 0
|
10
|
-
|
10
|
+
message = format(
|
11
|
+
"Permissions on '%<filename>s' " \
|
12
|
+
"should be 0%<limit>o, not 0%<actual>o",
|
13
|
+
filename: filename, limit: limit, actual: actual
|
14
|
+
)
|
15
|
+
raise message
|
11
16
|
end
|
12
17
|
end
|
13
18
|
|
@@ -15,7 +20,7 @@ module Imap::Backup
|
|
15
20
|
return nil unless File.exist?(filename)
|
16
21
|
|
17
22
|
stat = File.stat(filename)
|
18
|
-
stat.mode &
|
23
|
+
stat.mode & 0o777
|
19
24
|
end
|
20
25
|
|
21
26
|
def self.make_folder(base_path, path, permissions)
|
data/lib/imap/backup/version.rb
CHANGED
@@ -0,0 +1,27 @@
|
|
1
|
+
require "features/helper"
|
2
|
+
|
3
|
+
RSpec.describe "backup", type: :feature, docker: true do
|
4
|
+
include_context "imap-backup connection"
|
5
|
+
include_context "message-fixtures"
|
6
|
+
|
7
|
+
let(:messages_as_mbox) do
|
8
|
+
message_as_mbox_entry(msg1) + message_as_mbox_entry(msg2)
|
9
|
+
end
|
10
|
+
let(:folder) { "INBOX" }
|
11
|
+
|
12
|
+
before do
|
13
|
+
send_email folder, msg1
|
14
|
+
send_email folder, msg2
|
15
|
+
end
|
16
|
+
|
17
|
+
after do
|
18
|
+
FileUtils.rm_rf local_backup_path
|
19
|
+
delete_emails folder
|
20
|
+
end
|
21
|
+
|
22
|
+
it "downloads messages" do
|
23
|
+
connection.run_backup
|
24
|
+
|
25
|
+
expect(mbox_content(folder)).to eq(messages_as_mbox)
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module BackupDirectoryHelpers
|
2
|
+
def message_as_mbox_entry(options)
|
3
|
+
from = fixture("connection")[:username]
|
4
|
+
subject = options[:subject]
|
5
|
+
body = options[:body]
|
6
|
+
body_and_headers = <<-EOT.gsub("\n", "\r\n")
|
7
|
+
From: #{from}
|
8
|
+
Subject: #{subject}
|
9
|
+
|
10
|
+
#{body}
|
11
|
+
EOT
|
12
|
+
|
13
|
+
"From #{from}\n#{body_and_headers}\n"
|
14
|
+
end
|
15
|
+
|
16
|
+
def mbox_content(name)
|
17
|
+
File.read(mbox_path(name))
|
18
|
+
end
|
19
|
+
|
20
|
+
def mbox_path(name)
|
21
|
+
File.join(local_backup_path, name + ".mbox")
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
RSpec.configure do |config|
|
26
|
+
config.include BackupDirectoryHelpers, type: :feature
|
27
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module EmailServerHelpers
|
2
|
+
REQUESTED_ATTRIBUTES = ["RFC822", "FLAGS", "INTERNALDATE"]
|
3
|
+
|
4
|
+
def send_email(folder, options)
|
5
|
+
from = options[:from] || "address@example.org"
|
6
|
+
subject = options[:subject]
|
7
|
+
body = options[:body]
|
8
|
+
message = <<-EOT
|
9
|
+
From: #{from}
|
10
|
+
Subject: #{subject}
|
11
|
+
|
12
|
+
#{body}
|
13
|
+
EOT
|
14
|
+
|
15
|
+
imap.append(folder, message, nil, nil)
|
16
|
+
end
|
17
|
+
|
18
|
+
def delete_emails(folder)
|
19
|
+
imap.select(folder)
|
20
|
+
uids = imap.uid_search(["ALL"]).sort
|
21
|
+
imap.store(1 .. uids.size, "+FLAGS", [:Deleted])
|
22
|
+
imap.expunge
|
23
|
+
end
|
24
|
+
|
25
|
+
def imap
|
26
|
+
@imap ||=
|
27
|
+
begin
|
28
|
+
connection = fixture("connection")
|
29
|
+
imap = Net::IMAP.new(
|
30
|
+
connection[:server], connection[:connection_options]
|
31
|
+
)
|
32
|
+
imap.login(connection[:username], connection[:password])
|
33
|
+
imap
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
RSpec.configure do |config|
|
39
|
+
config.include EmailServerHelpers, type: :feature
|
40
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
shared_context "imap-backup connection" do
|
2
|
+
let(:local_backup_path) { Dir.mktmpdir(nil, "tmp") }
|
3
|
+
let(:default_connection) { fixture("connection") }
|
4
|
+
let(:backup_folders) { nil }
|
5
|
+
let(:connection_options) do
|
6
|
+
default_connection.merge({
|
7
|
+
local_path: local_backup_path,
|
8
|
+
folders: backup_folders
|
9
|
+
})
|
10
|
+
end
|
11
|
+
let(:connection) { Imap::Backup::Account::Connection.new(connection_options) }
|
12
|
+
end
|
@@ -9,7 +9,9 @@ describe Imap::Backup::Account::Connection do
|
|
9
9
|
{name: backup_folder}
|
10
10
|
end
|
11
11
|
|
12
|
-
let(:imap)
|
12
|
+
let(:imap) do
|
13
|
+
double("Net::IMAP", login: nil, disconnect: nil)
|
14
|
+
end
|
13
15
|
let(:imap_folders) { [] }
|
14
16
|
let(:options) do
|
15
17
|
{
|
@@ -22,9 +24,15 @@ describe Imap::Backup::Account::Connection do
|
|
22
24
|
let(:local_path) { "local_path" }
|
23
25
|
let(:backup_folders) { [self.class.folder_config] }
|
24
26
|
let(:username) { "username@gmail.com" }
|
27
|
+
let(:root_info) do
|
28
|
+
instance_double(Net::IMAP::MailboxList, name: root_name)
|
29
|
+
end
|
30
|
+
let(:root_name) { "foo" }
|
25
31
|
|
26
32
|
before do
|
27
33
|
allow(Net::IMAP).to receive(:new).and_return(imap)
|
34
|
+
allow(imap).to receive(:list).with("", "") { [root_info] }
|
35
|
+
allow(imap).to receive(:list).with(root_name, "*") { imap_folders }
|
28
36
|
allow(Imap::Backup::Utils).to receive(:make_folder)
|
29
37
|
end
|
30
38
|
|
@@ -68,9 +76,9 @@ describe Imap::Backup::Account::Connection do
|
|
68
76
|
end
|
69
77
|
|
70
78
|
context "#folders" do
|
71
|
-
let(:imap_folders)
|
72
|
-
|
73
|
-
|
79
|
+
let(:imap_folders) do
|
80
|
+
[instance_double(Net::IMAP::MailboxList)]
|
81
|
+
end
|
74
82
|
|
75
83
|
it "returns the list of folders" do
|
76
84
|
expect(subject.folders).to eq(imap_folders)
|
@@ -80,12 +88,14 @@ describe Imap::Backup::Account::Connection do
|
|
80
88
|
context "#status" do
|
81
89
|
let(:folder) { double("folder", uids: [remote_uid]) }
|
82
90
|
let(:local_uid) { "local_uid" }
|
83
|
-
let(:serializer)
|
91
|
+
let(:serializer) do
|
92
|
+
instance_double(Imap::Backup::Serializer::Mbox, uids: [local_uid])
|
93
|
+
end
|
84
94
|
let(:remote_uid) { "remote_uid" }
|
85
95
|
|
86
96
|
before do
|
87
97
|
allow(Imap::Backup::Account::Folder).to receive(:new).and_return(folder)
|
88
|
-
allow(Imap::Backup::Serializer::
|
98
|
+
allow(Imap::Backup::Serializer::Mbox).to receive(:new) { serializer }
|
89
99
|
end
|
90
100
|
|
91
101
|
it "should return the names of folders" do
|
@@ -127,7 +137,9 @@ describe Imap::Backup::Account::Connection do
|
|
127
137
|
end
|
128
138
|
|
129
139
|
context "without supplied backup_folders" do
|
130
|
-
let(:imap_folders)
|
140
|
+
let(:imap_folders) do
|
141
|
+
[instance_double(Net::IMAP::MailboxList, name: "foo")]
|
142
|
+
end
|
131
143
|
|
132
144
|
before do
|
133
145
|
allow(Imap::Backup::Account::Folder).to receive(:new).
|
@@ -3,9 +3,15 @@ require "spec_helper"
|
|
3
3
|
describe Imap::Backup::Account::Folder do
|
4
4
|
let(:imap) { double("Net::IMAP", examine: nil) }
|
5
5
|
let(:connection) { double("Imap::Backup::Account::Connection", imap: imap) }
|
6
|
-
let(:missing_mailbox_data)
|
7
|
-
|
8
|
-
|
6
|
+
let(:missing_mailbox_data) do
|
7
|
+
double("Data", text: "Unknown Mailbox: my_folder")
|
8
|
+
end
|
9
|
+
let(:missing_mailbox_response) do
|
10
|
+
double("Response", data: missing_mailbox_data)
|
11
|
+
end
|
12
|
+
let(:missing_mailbox_error) do
|
13
|
+
Net::IMAP::NoResponseError.new(missing_mailbox_response)
|
14
|
+
end
|
9
15
|
|
10
16
|
subject { described_class.new(connection, "my_folder") }
|
11
17
|
|
@@ -19,7 +25,9 @@ describe Imap::Backup::Account::Folder do
|
|
19
25
|
end
|
20
26
|
|
21
27
|
context "with missing mailboxes" do
|
22
|
-
before
|
28
|
+
before do
|
29
|
+
allow(imap).to receive(:examine).and_raise(missing_mailbox_error)
|
30
|
+
end
|
23
31
|
|
24
32
|
it "returns an empty array" do
|
25
33
|
expect(subject.uids).to eq([])
|
@@ -49,7 +57,9 @@ describe Imap::Backup::Account::Folder do
|
|
49
57
|
end
|
50
58
|
|
51
59
|
context "if the mailbox doesn't exist" do
|
52
|
-
before
|
60
|
+
before do
|
61
|
+
allow(imap).to receive(:examine).and_raise(missing_mailbox_error)
|
62
|
+
end
|
53
63
|
|
54
64
|
it "is nil" do
|
55
65
|
expect(subject.fetch(123)).to be_nil
|