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