imap-backup 1.3.0 → 1.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/.rspec +3 -0
  4. data/.rubocop.yml +2 -1
  5. data/.travis.yml +1 -1
  6. data/README.md +12 -0
  7. data/bin/imap-backup +11 -6
  8. data/docker-compose.yml +15 -0
  9. data/imap-backup.gemspec +3 -2
  10. data/lib/email/mboxrd/message.rb +30 -4
  11. data/lib/email/provider.rb +2 -2
  12. data/lib/imap/backup.rb +0 -1
  13. data/lib/imap/backup/account/connection.rb +29 -15
  14. data/lib/imap/backup/account/folder.rb +3 -3
  15. data/lib/imap/backup/configuration/account.rb +15 -12
  16. data/lib/imap/backup/configuration/asker.rb +3 -1
  17. data/lib/imap/backup/configuration/folder_chooser.rb +5 -2
  18. data/lib/imap/backup/configuration/list.rb +12 -9
  19. data/lib/imap/backup/configuration/setup.rb +5 -3
  20. data/lib/imap/backup/configuration/store.rb +4 -4
  21. data/lib/imap/backup/downloader.rb +6 -2
  22. data/lib/imap/backup/serializer/base.rb +2 -2
  23. data/lib/imap/backup/serializer/mbox.rb +16 -7
  24. data/lib/imap/backup/utils.rb +8 -3
  25. data/lib/imap/backup/version.rb +1 -1
  26. data/spec/features/backup_spec.rb +27 -0
  27. data/spec/features/helper.rb +2 -0
  28. data/spec/features/support/backup_directory.rb +27 -0
  29. data/spec/features/support/email_server.rb +40 -0
  30. data/spec/features/support/shared/connection_context.rb +12 -0
  31. data/spec/features/support/shared/message_fixtures.rb +6 -0
  32. data/spec/fixtures/connection.yml +7 -0
  33. data/spec/support/fixtures.rb +6 -0
  34. data/spec/unit/account/connection_spec.rb +19 -7
  35. data/spec/unit/account/folder_spec.rb +15 -5
  36. data/spec/unit/configuration/account_spec.rb +18 -8
  37. data/spec/unit/configuration/asker_spec.rb +6 -3
  38. data/spec/unit/configuration/connection_tester_spec.rb +1 -1
  39. data/spec/unit/configuration/folder_chooser_spec.rb +21 -11
  40. data/spec/unit/configuration/list_spec.rb +13 -6
  41. data/spec/unit/configuration/setup_spec.rb +5 -3
  42. data/spec/unit/configuration/store_spec.rb +13 -8
  43. data/spec/unit/downloader_spec.rb +3 -1
  44. data/spec/unit/email/mboxrd/message_spec.rb +32 -13
  45. data/spec/unit/email/provider_spec.rb +7 -3
  46. data/spec/unit/serializer/base_spec.rb +3 -2
  47. data/spec/unit/serializer/mbox_spec.rb +9 -6
  48. data/spec/unit/utils_spec.rb +15 -13
  49. metadata +35 -5
  50. data/lib/imap/backup/serializer/directory.rb +0 -43
  51. 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 not config_exists?
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 not config_exists?
29
- raise ConfigurationNotFound.new("Configuration file '#{Configuration::Store.default_pathname}' not found")
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
- return @accounts if @accounts
40
- if required_accounts.nil?
41
- @accounts = config.accounts
42
- else
43
- @accounts = config.accounts.select { |account| required_accounts.include?(account[:username]) }
44
- end
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
- account = {
72
+ {
73
73
  username: username,
74
74
  password: "",
75
- local_path: File.join(config.path, username.gsub("@", "_")),
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(config, account, Configuration::Setup.highline).run
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 0600, pathname
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, 0600
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) != 0700
80
- FileUtils.chmod 0700, path
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 "[#{folder.name}] #{uid} - not available - skipped"
16
+ Imap::Backup.logger.debug(
17
+ "[#{folder.name}] #{uid} - not available - skipped"
18
+ )
17
19
  next
18
20
  end
19
- Imap::Backup.logger.debug "[#{folder.name}] #{uid} - #{message["RFC822"].size} bytes"
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
@@ -1,7 +1,7 @@
1
1
  module Imap::Backup
2
2
  module Serializer
3
- DIRECTORY_PERMISSIONS = 0700
4
- FILE_PERMISSIONS = 0600
3
+ DIRECTORY_PERMISSIONS = 0o700
4
+ FILE_PERMISSIONS = 0o600
5
5
 
6
6
  class Base
7
7
  attr_reader :path
@@ -17,7 +17,7 @@ module Imap::Backup
17
17
  return @uids if @uids
18
18
 
19
19
  @uids = []
20
- return @uids if not exist?
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 "[#{folder}] message #{uid} already downloaded - skipping"
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.to_s
47
+ mbox.write mboxrd_message.to_serialized
46
48
  imap.write uid + "\n"
47
49
  rescue => e
48
- Imap::Backup.logger.warn "[#{folder}] failed to save message #{uid}:\n#{body}. #{e}:\n#{e.backtrace.join("\n")}"
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 && (not imap)
61
- raise ".mbox file missing" if imap && (not mbox)
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(path, mbox_relative_path, Serializer::DIRECTORY_PERMISSIONS)
74
+ Utils.make_folder(
75
+ path, mbox_relative_path, Serializer::DIRECTORY_PERMISSIONS
76
+ )
68
77
  end
69
78
 
70
79
  def exist?
@@ -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 & 0777
8
+ mask = ~limit & 0o777
9
9
  if actual & mask != 0
10
- raise format("Permissions on '%s' should be 0%o, not 0%o", filename, limit, actual)
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 & 0777
23
+ stat.mode & 0o777
19
24
  end
20
25
 
21
26
  def self.make_folder(base_path, path, permissions)
@@ -2,7 +2,7 @@ module Imap; end
2
2
 
3
3
  module Imap::Backup
4
4
  MAJOR = 1
5
- MINOR = 3
5
+ MINOR = 4
6
6
  REVISION = 0
7
7
  VERSION = [MAJOR, MINOR, REVISION].map(&:to_s).join(".")
8
8
  end
@@ -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,2 @@
1
+ support_glob = File.expand_path("support/**/*.rb", __dir__)
2
+ Dir[support_glob].each { |f| require f }
@@ -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
@@ -0,0 +1,6 @@
1
+ shared_context "message-fixtures" do
2
+ let(:uid1) { 123 }
3
+ let(:uid2) { 345 }
4
+ let(:msg1) { {uid: uid1, subject: "Test 1", body: "body 1\nHi"} }
5
+ let(:msg2) { {uid: uid2, subject: "Test 2", body: "body 2"} }
6
+ end
@@ -0,0 +1,7 @@
1
+ :server: 'localhost'
2
+ :username: 'address@example.org'
3
+ :password: 'pass'
4
+ :connection_options:
5
+ :port: 8993
6
+ :ssl:
7
+ :verify_mode: 0
@@ -0,0 +1,6 @@
1
+ def fixture(name)
2
+ spec_root = File.expand_path("..", File.dirname(__FILE__))
3
+ fixture_path = File.join(spec_root, "fixtures", name + ".yml")
4
+ fixture = File.read(fixture_path)
5
+ YAML.load(fixture)
6
+ end
@@ -9,7 +9,9 @@ describe Imap::Backup::Account::Connection do
9
9
  {name: backup_folder}
10
10
  end
11
11
 
12
- let(:imap) { double("Net::IMAP", login: nil, list: imap_folders, disconnect: nil) }
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) { ["imap_folder"] }
72
-
73
- before { allow(imap).to receive(:list).and_return(imap_folders) }
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) { double("serializer", uids: [local_uid]) }
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::Directory).to receive(:new).and_return(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) { [double(name: "foo")] }
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) { double("Data", text: "Unknown Mailbox: my_folder") }
7
- let(:missing_mailbox_response) { double("Response", data: missing_mailbox_data) }
8
- let(:missing_mailbox_error) { Net::IMAP::NoResponseError.new(missing_mailbox_response) }
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 { allow(imap).to receive(:examine).and_raise(missing_mailbox_error) }
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 { allow(imap).to receive(:examine).and_raise(missing_mailbox_error) }
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