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.
- checksums.yaml +4 -4
- data/.rspec +0 -1
- data/.rspec-all +2 -0
- data/.rubocop.yml +15 -2
- data/.rubocop_todo.yml +58 -0
- data/.travis.yml +15 -2
- data/README.md +14 -22
- data/Rakefile +6 -3
- data/bin/imap-backup +5 -11
- data/imap-backup.gemspec +10 -6
- data/lib/email/mboxrd/message.rb +16 -16
- data/lib/imap/backup/account/connection.rb +38 -22
- data/lib/imap/backup/account/folder.rb +23 -7
- data/lib/imap/backup/configuration/account.rb +25 -21
- data/lib/imap/backup/configuration/asker.rb +3 -2
- data/lib/imap/backup/configuration/connection_tester.rb +1 -1
- data/lib/imap/backup/configuration/folder_chooser.rb +32 -5
- data/lib/imap/backup/configuration/list.rb +2 -0
- data/lib/imap/backup/configuration/setup.rb +2 -1
- data/lib/imap/backup/configuration/store.rb +3 -6
- data/lib/imap/backup/downloader.rb +8 -7
- data/lib/imap/backup/serializer/mbox.rb +44 -25
- data/lib/imap/backup/serializer/mbox_enumerator.rb +31 -0
- data/lib/imap/backup/serializer/mbox_store.rb +35 -32
- data/lib/imap/backup/uploader.rb +11 -2
- data/lib/imap/backup/utils.rb +11 -9
- data/lib/imap/backup/version.rb +2 -2
- data/spec/features/backup_spec.rb +6 -5
- data/spec/features/helper.rb +1 -1
- data/spec/features/restore_spec.rb +75 -27
- data/spec/features/support/backup_directory.rb +7 -7
- data/spec/features/support/email_server.rb +15 -11
- data/spec/features/support/shared/connection_context.rb +2 -2
- data/spec/features/support/shared/message_fixtures.rb +8 -0
- data/spec/spec_helper.rb +1 -1
- data/spec/support/fixtures.rb +2 -2
- data/spec/support/higline_test_helpers.rb +1 -1
- data/spec/unit/email/mboxrd/message_spec.rb +73 -53
- data/spec/unit/email/provider_spec.rb +3 -5
- data/spec/unit/imap/backup/account/connection_spec.rb +82 -59
- data/spec/unit/imap/backup/account/folder_spec.rb +75 -37
- data/spec/unit/imap/backup/configuration/account_spec.rb +95 -61
- data/spec/unit/imap/backup/configuration/asker_spec.rb +43 -45
- data/spec/unit/imap/backup/configuration/connection_tester_spec.rb +21 -22
- data/spec/unit/imap/backup/configuration/folder_chooser_spec.rb +66 -33
- data/spec/unit/imap/backup/configuration/list_spec.rb +32 -11
- data/spec/unit/imap/backup/configuration/setup_spec.rb +97 -56
- data/spec/unit/imap/backup/configuration/store_spec.rb +30 -25
- data/spec/unit/imap/backup/downloader_spec.rb +28 -26
- data/spec/unit/imap/backup/serializer/mbox_enumerator_spec.rb +45 -0
- data/spec/unit/imap/backup/serializer/mbox_spec.rb +109 -51
- data/spec/unit/imap/backup/serializer/mbox_store_spec.rb +232 -20
- data/spec/unit/imap/backup/uploader_spec.rb +23 -9
- data/spec/unit/imap/backup/utils_spec.rb +14 -15
- data/spec/unit/imap/backup_spec.rb +28 -0
- 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 = [
|
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
|
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
|
51
|
-
|
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
|
74
|
+
return nil if !attributes.key?("RFC822")
|
75
|
+
|
62
76
|
attributes
|
63
|
-
rescue
|
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
|
-
|
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
|
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
|
-
|
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
|
-
|
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
|
@@ -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 =
|
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
|
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
|
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
|
-
|
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
|
7
|
+
@folder = folder
|
8
|
+
@serializer = serializer
|
8
9
|
end
|
9
10
|
|
10
11
|
def run
|
11
12
|
uids = folder.uids - serializer.uids
|
12
|
-
|
13
|
-
|
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
|
-
"
|
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
|
14
|
-
existing_uid_validity = store.uid_validity
|
13
|
+
def apply_uid_validity(value)
|
15
14
|
case
|
16
|
-
when
|
15
|
+
when store.uid_validity.nil?
|
17
16
|
store.uid_validity = value
|
18
17
|
nil
|
19
|
-
when
|
18
|
+
when store.uid_validity == value
|
20
19
|
# NOOP
|
21
20
|
nil
|
22
21
|
else
|
23
|
-
|
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
|
76
|
-
|
77
|
-
|
78
|
-
|
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
|
-
|
100
|
+
Utils.make_folder(
|
82
101
|
path, relative_path, Serializer::DIRECTORY_PERMISSIONS
|
83
102
|
)
|
84
103
|
end
|
85
104
|
|
86
|
-
if
|
87
|
-
|
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
|