imap-backup 2.0.0 → 2.2.2

Sign up to get free protection for your applications and to get access to all the features.
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