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.
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