imap-backup 2.0.0 → 2.2.2

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 (56) hide show
  1. checksums.yaml +4 -4
  2. data/.rspec +0 -1
  3. data/.rspec-all +2 -0
  4. data/.rubocop.yml +15 -2
  5. data/.rubocop_todo.yml +58 -0
  6. data/.travis.yml +15 -2
  7. data/README.md +14 -22
  8. data/Rakefile +6 -3
  9. data/bin/imap-backup +5 -11
  10. data/imap-backup.gemspec +10 -6
  11. data/lib/email/mboxrd/message.rb +16 -16
  12. data/lib/imap/backup/account/connection.rb +38 -22
  13. data/lib/imap/backup/account/folder.rb +23 -7
  14. data/lib/imap/backup/configuration/account.rb +25 -21
  15. data/lib/imap/backup/configuration/asker.rb +3 -2
  16. data/lib/imap/backup/configuration/connection_tester.rb +1 -1
  17. data/lib/imap/backup/configuration/folder_chooser.rb +32 -5
  18. data/lib/imap/backup/configuration/list.rb +2 -0
  19. data/lib/imap/backup/configuration/setup.rb +2 -1
  20. data/lib/imap/backup/configuration/store.rb +3 -6
  21. data/lib/imap/backup/downloader.rb +8 -7
  22. data/lib/imap/backup/serializer/mbox.rb +44 -25
  23. data/lib/imap/backup/serializer/mbox_enumerator.rb +31 -0
  24. data/lib/imap/backup/serializer/mbox_store.rb +35 -32
  25. data/lib/imap/backup/uploader.rb +11 -2
  26. data/lib/imap/backup/utils.rb +11 -9
  27. data/lib/imap/backup/version.rb +2 -2
  28. data/spec/features/backup_spec.rb +6 -5
  29. data/spec/features/helper.rb +1 -1
  30. data/spec/features/restore_spec.rb +75 -27
  31. data/spec/features/support/backup_directory.rb +7 -7
  32. data/spec/features/support/email_server.rb +15 -11
  33. data/spec/features/support/shared/connection_context.rb +2 -2
  34. data/spec/features/support/shared/message_fixtures.rb +8 -0
  35. data/spec/spec_helper.rb +1 -1
  36. data/spec/support/fixtures.rb +2 -2
  37. data/spec/support/higline_test_helpers.rb +1 -1
  38. data/spec/unit/email/mboxrd/message_spec.rb +73 -53
  39. data/spec/unit/email/provider_spec.rb +3 -5
  40. data/spec/unit/imap/backup/account/connection_spec.rb +82 -59
  41. data/spec/unit/imap/backup/account/folder_spec.rb +75 -37
  42. data/spec/unit/imap/backup/configuration/account_spec.rb +95 -61
  43. data/spec/unit/imap/backup/configuration/asker_spec.rb +43 -45
  44. data/spec/unit/imap/backup/configuration/connection_tester_spec.rb +21 -22
  45. data/spec/unit/imap/backup/configuration/folder_chooser_spec.rb +66 -33
  46. data/spec/unit/imap/backup/configuration/list_spec.rb +32 -11
  47. data/spec/unit/imap/backup/configuration/setup_spec.rb +97 -56
  48. data/spec/unit/imap/backup/configuration/store_spec.rb +30 -25
  49. data/spec/unit/imap/backup/downloader_spec.rb +28 -26
  50. data/spec/unit/imap/backup/serializer/mbox_enumerator_spec.rb +45 -0
  51. data/spec/unit/imap/backup/serializer/mbox_spec.rb +109 -51
  52. data/spec/unit/imap/backup/serializer/mbox_store_spec.rb +232 -20
  53. data/spec/unit/imap/backup/uploader_spec.rb +23 -9
  54. data/spec/unit/imap/backup/utils_spec.rb +14 -15
  55. data/spec/unit/imap/backup_spec.rb +28 -0
  56. metadata +13 -7
@@ -3,10 +3,12 @@ require "forwardable"
3
3
  module Imap::Backup
4
4
  module Account; end
5
5
 
6
+ class FolderNotFound < StandardError; end
7
+
6
8
  class Account::Folder
7
9
  extend Forwardable
8
10
 
9
- REQUESTED_ATTRIBUTES = ["RFC822", "FLAGS", "INTERNALDATE"].freeze
11
+ REQUESTED_ATTRIBUTES = %w[RFC822 FLAGS INTERNALDATE].freeze
10
12
 
11
13
  attr_reader :connection
12
14
  attr_reader :name
@@ -27,12 +29,13 @@ module Imap::Backup
27
29
  def exist?
28
30
  examine
29
31
  true
30
- rescue Net::IMAP::NoResponseError => e
32
+ rescue FolderNotFound
31
33
  false
32
34
  end
33
35
 
34
36
  def create
35
37
  return if exist?
38
+
36
39
  imap.create(name)
37
40
  end
38
41
 
@@ -47,8 +50,17 @@ module Imap::Backup
47
50
  def uids
48
51
  examine
49
52
  imap.uid_search(["ALL"]).sort
50
- rescue Net::IMAP::NoResponseError
51
- Imap::Backup.logger.warn "Folder '#{name}' does not exist"
53
+ rescue FolderNotFound
54
+ []
55
+ rescue NoMethodError
56
+ message = <<~MESSAGE
57
+ Folder '#{name}' caused NoMethodError
58
+ probably
59
+ `undefined method `[]' for nil:NilClass (NoMethodError)`
60
+ in `search_internal` in stdlib net/imap.rb.
61
+ This is caused by `@responses["SEARCH"] being unset/undefined
62
+ MESSAGE
63
+ Imap::Backup.logger.warn message
52
64
  []
53
65
  end
54
66
 
@@ -56,12 +68,13 @@ module Imap::Backup
56
68
  examine
57
69
  fetch_data_items = imap.uid_fetch([uid.to_i], REQUESTED_ATTRIBUTES)
58
70
  return nil if fetch_data_items.nil?
71
+
59
72
  fetch_data_item = fetch_data_items[0]
60
73
  attributes = fetch_data_item.attr
61
- attributes["RFC822"].force_encoding("utf-8")
74
+ return nil if !attributes.key?("RFC822")
75
+
62
76
  attributes
63
- rescue Net::IMAP::NoResponseError
64
- Imap::Backup.logger.warn "Folder '#{name}' does not exist"
77
+ rescue FolderNotFound
65
78
  nil
66
79
  end
67
80
 
@@ -76,6 +89,9 @@ module Imap::Backup
76
89
 
77
90
  def examine
78
91
  imap.examine(name)
92
+ rescue Net::IMAP::NoResponseError
93
+ Imap::Backup.logger.warn "Folder '#{name}' does not exist"
94
+ raise FolderNotFound, "Folder '#{name}' does not exist"
79
95
  end
80
96
 
81
97
  def extract_uid(response)
@@ -1,7 +1,7 @@
1
1
  module Imap::Backup
2
2
  module Configuration; end
3
3
 
4
- class Configuration::Account < Struct.new(:store, :account, :highline)
4
+ Configuration::Account = Struct.new(:store, :account, :highline) do
5
5
  def initialize(store, account, highline)
6
6
  super
7
7
  end
@@ -9,7 +9,7 @@ module Imap::Backup
9
9
  def run
10
10
  catch :done do
11
11
  loop do
12
- system("clear")
12
+ Kernel.system("clear")
13
13
  create_menu
14
14
  end
15
15
  end
@@ -46,12 +46,14 @@ module Imap::Backup
46
46
  def modify_email(menu)
47
47
  menu.choice("modify email") do
48
48
  username = Configuration::Asker.email(username)
49
- puts "username: #{username}"
49
+ Kernel.puts "username: #{username}"
50
50
  other_accounts = store.accounts.reject { |a| a == account }
51
51
  others = other_accounts.map { |a| a[:username] }
52
- puts "others: #{others.inspect}"
52
+ Kernel.puts "others: #{others.inspect}"
53
53
  if others.include?(username)
54
- puts "There is already an account set up with that email address"
54
+ Kernel.puts(
55
+ "There is already an account set up with that email address"
56
+ )
55
57
  else
56
58
  account[:username] = username
57
59
  if account[:server].nil? || (account[:server] == "")
@@ -82,23 +84,25 @@ module Imap::Backup
82
84
  end
83
85
  end
84
86
 
87
+ def path_modification_validator(path)
88
+ same = store.accounts.find do |a|
89
+ a[:username] != account[:username] && a[:local_path] == path
90
+ end
91
+ if same
92
+ Kernel.puts "The path '#{path}' is used to backup " \
93
+ "the account '#{same[:username]}'"
94
+ false
95
+ else
96
+ true
97
+ end
98
+ end
99
+
85
100
  def modify_backup_path(menu)
86
101
  menu.choice("modify backup path") do
87
- validator = ->(p) do
88
- same = store.accounts.find do |a|
89
- a[:username] != account[:username] && a[:local_path] == p
90
- end
91
- if same
92
- puts "The path '#{p}' is used to backup " \
93
- "the account '#{same[:username]}'"
94
- false
95
- else
96
- true
97
- end
98
- end
99
102
  existing = account[:local_path].clone
100
- account[:local_path] =
101
- Configuration::Asker.backup_path(account[:local_path], validator)
103
+ account[:local_path] = Configuration::Asker.backup_path(
104
+ account[:local_path], ->(path) { path_modification_validator(path) }
105
+ )
102
106
  account[:modified] = true if existing != account[:local_path]
103
107
  end
104
108
  end
@@ -112,7 +116,7 @@ module Imap::Backup
112
116
  def test_connection(menu)
113
117
  menu.choice("test connection") do
114
118
  result = Configuration::ConnectionTester.test(account)
115
- puts result
119
+ Kernel.puts result
116
120
  highline.ask "Press a key "
117
121
  end
118
122
  end
@@ -141,7 +145,7 @@ module Imap::Backup
141
145
  def default_server(username)
142
146
  provider = Email::Provider.for_address(username)
143
147
  if provider.provider == :default
144
- puts "Can't decide provider for email address '#{username}'"
148
+ Kernel.puts "Can't decide provider for email address '#{username}'"
145
149
  return nil
146
150
  end
147
151
  provider.host
@@ -1,8 +1,8 @@
1
1
  module Imap::Backup
2
2
  module Configuration; end
3
3
 
4
- class Configuration::Asker < Struct.new(:highline)
5
- EMAIL_MATCHER = /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]+$/i
4
+ Configuration::Asker = Struct.new(:highline) do
5
+ EMAIL_MATCHER = /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]+$/i.freeze
6
6
 
7
7
  def initialize(highline)
8
8
  super
@@ -24,6 +24,7 @@ module Imap::Backup
24
24
  return nil if !highline.agree(
25
25
  "the password and confirmation did not match.\nContinue? (y/n) "
26
26
  )
27
+
27
28
  return self.password
28
29
  end
29
30
  password
@@ -7,7 +7,7 @@ module Imap::Backup
7
7
  "Connection successful"
8
8
  rescue Net::IMAP::NoResponseError
9
9
  "No response"
10
- rescue Exception => e
10
+ rescue StandardError => e
11
11
  "Unexpected error: #{e}"
12
12
  end
13
13
  end
@@ -21,9 +21,11 @@ module Imap::Backup
21
21
  return
22
22
  end
23
23
 
24
+ remove_missing
25
+
24
26
  catch :done do
25
27
  loop do
26
- system("clear")
28
+ Kernel.system("clear")
27
29
  show_menu
28
30
  end
29
31
  end
@@ -44,21 +46,46 @@ module Imap::Backup
44
46
  def add_folders(menu)
45
47
  folders.each do |folder|
46
48
  name = folder.name
47
- mark = is_selected?(name) ? "+" : "-"
49
+ mark = selected?(name) ? "+" : "-"
48
50
  menu.choice("#{mark} #{name}") do
49
51
  toggle_selection name
50
52
  end
51
53
  end
52
54
  end
53
55
 
54
- def is_selected?(folder_name)
56
+ def selected?(folder_name)
55
57
  backup_folders = account[:folders]
56
58
  return false if backup_folders.nil?
59
+
57
60
  backup_folders.find { |f| f[:name] == folder_name }
58
61
  end
59
62
 
63
+ def remove_missing
64
+ removed = []
65
+ backup_folders = []
66
+ account[:folders].each do |f|
67
+ found = folders.find { |folder| folder.name == f[:name] }
68
+ if found
69
+ backup_folders << f
70
+ else
71
+ removed << f[:name]
72
+ end
73
+ end
74
+
75
+ return if removed.empty?
76
+
77
+ account[:folders] = backup_folders
78
+ account[:modified] = true
79
+
80
+ Kernel.puts <<~MESSAGE
81
+ The following folders have been removed: #{removed.join(', ')}
82
+ MESSAGE
83
+
84
+ highline.ask "Press a key "
85
+ end
86
+
60
87
  def toggle_selection(folder_name)
61
- if is_selected?(folder_name)
88
+ if selected?(folder_name)
62
89
  changed = account[:folders].reject! { |f| f[:name] == folder_name }
63
90
  account[:modified] = true if changed
64
91
  else
@@ -70,7 +97,7 @@ module Imap::Backup
70
97
 
71
98
  def connection
72
99
  @connection ||= Account::Connection.new(account)
73
- rescue
100
+ rescue StandardError
74
101
  nil
75
102
  end
76
103
 
@@ -10,6 +10,7 @@ module Imap::Backup
10
10
 
11
11
  def setup_logging
12
12
  return if !config_exists?
13
+
13
14
  Imap::Backup.setup_logging config
14
15
  end
15
16
 
@@ -25,6 +26,7 @@ module Imap::Backup
25
26
 
26
27
  def config
27
28
  return @config if @config
29
+
28
30
  if !config_exists?
29
31
  path = Configuration::Store.default_pathname
30
32
  raise ConfigurationNotFound, "Configuration file '#{path}' not found"
@@ -13,7 +13,7 @@ module Imap::Backup
13
13
  Imap::Backup.setup_logging config
14
14
  catch :done do
15
15
  loop do
16
- system("clear")
16
+ Kernel.system("clear")
17
17
  show_menu
18
18
  end
19
19
  end
@@ -40,6 +40,7 @@ module Imap::Backup
40
40
  def account_items(menu)
41
41
  config.accounts.each do |account|
42
42
  next if account[:delete]
43
+
43
44
  item = account[:username].clone
44
45
  item << " *" if account[:modified]
45
46
  menu.choice(item) do
@@ -52,6 +52,7 @@ module Imap::Backup
52
52
 
53
53
  def data
54
54
  return @data if @data
55
+
55
56
  if File.exist?(pathname)
56
57
  Utils.check_permissions pathname, 0o600
57
58
  contents = File.read(pathname)
@@ -73,12 +74,8 @@ module Imap::Backup
73
74
  end
74
75
 
75
76
  def mkdir_private(path)
76
- if !File.directory?(path)
77
- FileUtils.mkdir path
78
- end
79
- if Utils::mode(path) != 0o700
80
- FileUtils.chmod 0o700, path
81
- end
77
+ FileUtils.mkdir(path) if !File.directory?(path)
78
+ FileUtils.chmod(0o700, path) if Utils.mode(path) != 0o700
82
79
  end
83
80
  end
84
81
  end
@@ -4,22 +4,23 @@ module Imap::Backup
4
4
  attr_reader :serializer
5
5
 
6
6
  def initialize(folder, serializer)
7
- @folder, @serializer = folder, serializer
7
+ @folder = folder
8
+ @serializer = serializer
8
9
  end
9
10
 
10
11
  def run
11
12
  uids = folder.uids - serializer.uids
12
- Imap::Backup.logger.debug "[#{folder.name}] #{uids.count} new messages"
13
- uids.each do |uid|
13
+ count = uids.count
14
+ Imap::Backup.logger.debug "[#{folder.name}] #{count} new messages"
15
+ uids.each.with_index do |uid, i|
14
16
  message = folder.fetch(uid)
17
+ log_prefix = "[#{folder.name}] uid: #{uid} (#{i + 1}/#{count}) -"
15
18
  if message.nil?
16
- Imap::Backup.logger.debug(
17
- "[#{folder.name}] #{uid} - not available - skipped"
18
- )
19
+ Imap::Backup.logger.debug("#{log_prefix} not available - skipped")
19
20
  next
20
21
  end
21
22
  Imap::Backup.logger.debug(
22
- "[#{folder.name}] #{uid} - #{message['RFC822'].size} bytes"
23
+ "#{log_prefix} #{message['RFC822'].size} bytes"
23
24
  )
24
25
  serializer.save(uid, message)
25
26
  end
@@ -10,30 +10,16 @@ module Imap::Backup
10
10
  @folder = folder
11
11
  end
12
12
 
13
- def set_uid_validity(value)
14
- existing_uid_validity = store.uid_validity
13
+ def apply_uid_validity(value)
15
14
  case
16
- when existing_uid_validity.nil?
15
+ when store.uid_validity.nil?
17
16
  store.uid_validity = value
18
17
  nil
19
- when existing_uid_validity == value
18
+ when store.uid_validity == value
20
19
  # NOOP
21
20
  nil
22
21
  else
23
- digit = nil
24
- new_name = nil
25
- loop do
26
- extra = digit ? ".#{digit}" : ""
27
- new_name = "#{folder}.#{existing_uid_validity}#{extra}"
28
- test_store = Serializer::MboxStore.new(path, new_name)
29
- break if !test_store.exist?
30
- digit ||= 0
31
- digit += 1
32
- end
33
- store.rename new_name
34
- @store = nil
35
- store.uid_validity = value
36
- new_name
22
+ apply_new_uid_validity value
37
23
  end
38
24
  end
39
25
 
@@ -49,6 +35,10 @@ module Imap::Backup
49
35
  store.load(uid)
50
36
  end
51
37
 
38
+ def each_message(uids)
39
+ store.each_message(uids)
40
+ end
41
+
52
42
  def save(uid, message)
53
43
  store.add(uid, message)
54
44
  end
@@ -72,19 +62,48 @@ module Imap::Backup
72
62
  end
73
63
  end
74
64
 
75
- def create_containing_directory
76
- relative_path = File.dirname(folder)
77
- containing_directory = File.join(path, relative_path)
78
- full_path = File.expand_path(containing_directory)
65
+ def apply_new_uid_validity(value)
66
+ digit = 0
67
+ new_name = nil
68
+ loop do
69
+ extra = digit.zero? ? "" : ".#{digit}"
70
+ new_name = "#{folder}.#{store.uid_validity}#{extra}"
71
+ test_store = Serializer::MboxStore.new(path, new_name)
72
+ break if !test_store.exist?
79
73
 
74
+ digit += 1
75
+ end
76
+ rename_store new_name, value
77
+ end
78
+
79
+ def rename_store(new_name, value)
80
+ store.rename new_name
81
+ @store = nil
82
+ store.uid_validity = value
83
+ new_name
84
+ end
85
+
86
+ def relative_path
87
+ File.dirname(folder)
88
+ end
89
+
90
+ def containing_directory
91
+ File.join(path, relative_path)
92
+ end
93
+
94
+ def full_path
95
+ File.expand_path(containing_directory)
96
+ end
97
+
98
+ def create_containing_directory
80
99
  if !File.directory?(full_path)
81
- Imap::Backup::Utils.make_folder(
100
+ Utils.make_folder(
82
101
  path, relative_path, Serializer::DIRECTORY_PERMISSIONS
83
102
  )
84
103
  end
85
104
 
86
- if Imap::Backup::Utils.mode(full_path) !=
87
- Serializer::DIRECTORY_PERMISSIONS
105
+ if Utils.mode(full_path) !=
106
+ Serializer::DIRECTORY_PERMISSIONS
88
107
  FileUtils.chmod Serializer::DIRECTORY_PERMISSIONS, full_path
89
108
  end
90
109
  end