imap-backup 4.0.5 → 4.1.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/bin/imap-backup +5 -2
- data/lib/imap/backup/account/connection.rb +70 -58
- data/lib/imap/backup/account/folder.rb +23 -3
- data/lib/imap/backup/account.rb +6 -7
- data/lib/imap/backup/cli/accounts.rb +43 -0
- data/lib/imap/backup/cli/folders.rb +3 -1
- data/lib/imap/backup/cli/helpers.rb +8 -9
- data/lib/imap/backup/cli/local.rb +4 -2
- data/lib/imap/backup/cli/setup.rb +1 -1
- data/lib/imap/backup/cli/status.rb +1 -1
- data/lib/imap/backup/cli/utils.rb +3 -2
- data/lib/imap/backup/{configuration/store.rb → configuration.rb} +49 -14
- data/lib/imap/backup/downloader.rb +26 -12
- data/lib/imap/backup/logger.rb +42 -0
- data/lib/imap/backup/sanitizer.rb +42 -0
- data/lib/imap/backup/serializer/mbox_store.rb +2 -2
- data/lib/imap/backup/{configuration → setup}/account.rb +59 -41
- data/lib/imap/backup/{configuration → setup}/asker.rb +5 -5
- data/lib/imap/backup/setup/connection_tester.rb +26 -0
- data/lib/imap/backup/{configuration → setup}/folder_chooser.rb +25 -17
- data/lib/imap/backup/setup/helpers.rb +15 -0
- data/lib/imap/backup/{configuration/setup.rb → setup.rb} +33 -25
- data/lib/imap/backup/uploader.rb +2 -2
- data/lib/imap/backup/version.rb +2 -2
- data/lib/imap/backup.rb +7 -33
- data/lib/retry_on_error.rb +1 -1
- data/spec/features/backup_spec.rb +1 -0
- data/spec/features/status_spec.rb +43 -0
- data/spec/features/support/email_server.rb +5 -2
- data/spec/features/support/shared/connection_context.rb +7 -5
- data/spec/support/higline_test_helpers.rb +1 -1
- data/spec/support/silence_logging.rb +1 -1
- data/spec/unit/email/provider/base_spec.rb +1 -1
- data/spec/unit/email/provider_spec.rb +2 -2
- data/spec/unit/imap/backup/account/connection_spec.rb +22 -26
- data/spec/unit/imap/backup/cli/accounts_spec.rb +47 -0
- data/spec/unit/imap/backup/cli/local_spec.rb +15 -4
- data/spec/unit/imap/backup/cli/utils_spec.rb +54 -42
- data/spec/unit/imap/backup/{configuration/store_spec.rb → configuration_spec.rb} +23 -24
- data/spec/unit/imap/backup/downloader_spec.rb +1 -1
- data/spec/unit/imap/backup/logger_spec.rb +48 -0
- data/spec/unit/imap/backup/{configuration → setup}/account_spec.rb +78 -70
- data/spec/unit/imap/backup/{configuration → setup}/asker_spec.rb +2 -2
- data/spec/unit/imap/backup/{configuration → setup}/connection_tester_spec.rb +10 -10
- data/spec/unit/imap/backup/{configuration → setup}/folder_chooser_spec.rb +25 -26
- data/spec/unit/imap/backup/{configuration/setup_spec.rb → setup_spec.rb} +81 -52
- metadata +51 -48
- data/lib/imap/backup/configuration/connection_tester.rb +0 -14
- data/lib/imap/backup/configuration/list.rb +0 -53
- data/spec/support/shared_examples/account_flagging.rb +0 -23
- data/spec/unit/imap/backup/configuration/list_spec.rb +0 -89
- data/spec/unit/imap/backup_spec.rb +0 -28
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 00a744249ff80a26be2c57655709be59d9678c9bb998dc1c9f3ea4bf3539d747
|
4
|
+
data.tar.gz: 4f44c050c66fe2ef2e136b46dec581a91f77ab4b650e9b3466a893540e481a24
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 66067924fda33810aff4b02aa047c20cd26ce3fc31d8e752fc43a8f78af3e1c952871fa0ba372c670a5ee399e7fc686f9df9601fc5cbb7caa6f2065b4ac95b90
|
7
|
+
data.tar.gz: 443a86c3bda9cc1652fc9d1c5619347c1d82498befed16851a6f8a227eaf3299813dd6a4084e1bb0d180170adb1b63ccf8e5d2a8bb19f0457611e7916d796c4d
|
data/bin/imap-backup
CHANGED
@@ -2,7 +2,10 @@
|
|
2
2
|
|
3
3
|
$LOAD_PATH.unshift(File.expand_path("../lib/", __dir__))
|
4
4
|
require "imap/backup/cli"
|
5
|
+
require "imap/backup/logger"
|
5
6
|
|
6
|
-
Imap::Backup::
|
7
|
+
Imap::Backup::Logger.setup_logging
|
7
8
|
|
8
|
-
Imap::Backup::
|
9
|
+
Imap::Backup::Logger.sanitize_stderr do
|
10
|
+
Imap::Backup::CLI.start(ARGV)
|
11
|
+
end
|
@@ -4,63 +4,74 @@ require "imap/backup/client/default"
|
|
4
4
|
require "retry_on_error"
|
5
5
|
|
6
6
|
module Imap::Backup
|
7
|
-
|
7
|
+
class Account; end
|
8
8
|
|
9
9
|
class Account::Connection
|
10
10
|
include RetryOnError
|
11
11
|
|
12
12
|
LOGIN_RETRY_CLASSES = [EOFError, Errno::ECONNRESET, SocketError].freeze
|
13
13
|
|
14
|
-
attr_reader :
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
def initialize(options)
|
20
|
-
@username = options[:username]
|
21
|
-
@password = options[:password]
|
22
|
-
@local_path = options[:local_path]
|
23
|
-
@config_folders = options[:folders]
|
24
|
-
@server = options[:server]
|
25
|
-
@connection_options = options[:connection_options] || {}
|
26
|
-
@folders = nil
|
14
|
+
attr_reader :account
|
15
|
+
|
16
|
+
def initialize(account)
|
17
|
+
@account = account
|
18
|
+
reset
|
27
19
|
create_account_folder
|
28
20
|
end
|
29
21
|
|
30
|
-
|
31
|
-
|
22
|
+
# TODO: Make this private once the 'folders' command
|
23
|
+
# has been removed.
|
24
|
+
def folder_names
|
25
|
+
@folder_names ||=
|
32
26
|
begin
|
33
|
-
|
27
|
+
folder_names = client.list
|
34
28
|
|
35
|
-
if
|
36
|
-
message = "Unable to get folder list for account #{username}"
|
37
|
-
Imap::Backup.logger.info message
|
29
|
+
if folder_names.empty?
|
30
|
+
message = "Unable to get folder list for account #{account.username}"
|
31
|
+
Imap::Backup::Logger.logger.info message
|
38
32
|
raise message
|
39
33
|
end
|
40
34
|
|
41
|
-
|
35
|
+
folder_names
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def backup_folders
|
40
|
+
@backup_folders ||=
|
41
|
+
begin
|
42
|
+
names =
|
43
|
+
if account.folders&.any?
|
44
|
+
account.folders.map { |af| af[:name] }
|
45
|
+
else
|
46
|
+
folder_names
|
47
|
+
end
|
48
|
+
|
49
|
+
names.map do |name|
|
50
|
+
Account::Folder.new(self, name)
|
51
|
+
end
|
42
52
|
end
|
43
53
|
end
|
44
54
|
|
45
55
|
def status
|
46
|
-
backup_folders.map do |
|
47
|
-
|
48
|
-
|
49
|
-
{name: backup_folder[:name], local: s.uids, remote: f.uids}
|
56
|
+
backup_folders.map do |folder|
|
57
|
+
s = Serializer::Mbox.new(account.local_path, folder.name)
|
58
|
+
{name: folder.name, local: s.uids, remote: folder.uids}
|
50
59
|
end
|
51
60
|
end
|
52
61
|
|
53
62
|
def run_backup
|
54
|
-
Imap::Backup.logger.debug "Running backup of account: #{username}"
|
63
|
+
Imap::Backup::Logger.logger.debug "Running backup of account: #{account.username}"
|
55
64
|
# start the connection so we get logging messages in the right order
|
56
65
|
client
|
57
66
|
each_folder do |folder, serializer|
|
58
67
|
next if !folder.exist?
|
59
68
|
|
60
|
-
Imap::Backup.logger.debug "[#{folder.name}] running backup"
|
69
|
+
Imap::Backup::Logger.logger.debug "[#{folder.name}] running backup"
|
61
70
|
serializer.apply_uid_validity(folder.uid_validity)
|
62
71
|
begin
|
63
|
-
Downloader.new(
|
72
|
+
Downloader.new(
|
73
|
+
folder, serializer, block_size: config.download_block_size
|
74
|
+
).run
|
64
75
|
rescue Net::IMAP::ByeResponseError
|
65
76
|
reconnect
|
66
77
|
retry
|
@@ -71,11 +82,11 @@ module Imap::Backup
|
|
71
82
|
def local_folders
|
72
83
|
return enum_for(:local_folders) if !block_given?
|
73
84
|
|
74
|
-
glob = File.join(local_path, "**", "*.imap")
|
75
|
-
base = Pathname.new(local_path)
|
85
|
+
glob = File.join(account.local_path, "**", "*.imap")
|
86
|
+
base = Pathname.new(account.local_path)
|
76
87
|
Pathname.glob(glob) do |path|
|
77
88
|
name = path.relative_path_from(base).to_s[0..-6]
|
78
|
-
serializer = Serializer::Mbox.new(local_path, name)
|
89
|
+
serializer = Serializer::Mbox.new(account.local_path, name)
|
79
90
|
folder = Account::Folder.new(self, name)
|
80
91
|
yield serializer, folder
|
81
92
|
end
|
@@ -89,18 +100,27 @@ module Imap::Backup
|
|
89
100
|
|
90
101
|
def disconnect
|
91
102
|
client.disconnect if @client
|
103
|
+
reset
|
92
104
|
end
|
93
105
|
|
94
106
|
def reconnect
|
95
107
|
disconnect
|
108
|
+
end
|
109
|
+
|
110
|
+
def reset
|
111
|
+
@backup_folders = nil
|
96
112
|
@client = nil
|
113
|
+
@config = nil
|
114
|
+
@folder_names = nil
|
115
|
+
@provider = nil
|
116
|
+
@server = nil
|
97
117
|
end
|
98
118
|
|
99
119
|
def client
|
100
120
|
@client ||=
|
101
121
|
retry_on_error(errors: LOGIN_RETRY_CLASSES) do
|
102
122
|
options = provider_options
|
103
|
-
Imap::Backup.logger.debug(
|
123
|
+
Imap::Backup::Logger.logger.debug(
|
104
124
|
"Creating IMAP instance: #{server}, options: #{options.inspect}"
|
105
125
|
)
|
106
126
|
client =
|
@@ -109,23 +129,22 @@ module Imap::Backup
|
|
109
129
|
else
|
110
130
|
Client::Default.new(server, options)
|
111
131
|
end
|
112
|
-
Imap::Backup.logger.debug "Logging in: #{username}/#{masked_password}"
|
113
|
-
client.login(username, password)
|
114
|
-
Imap::Backup.logger.debug "Login complete"
|
132
|
+
Imap::Backup::Logger.logger.debug "Logging in: #{account.username}/#{masked_password}"
|
133
|
+
client.login(account.username, account.password)
|
134
|
+
Imap::Backup::Logger.logger.debug "Login complete"
|
115
135
|
client
|
116
136
|
end
|
117
137
|
end
|
118
138
|
|
119
139
|
def server
|
120
|
-
@server ||= provider.host
|
140
|
+
@server ||= account.server || provider.host
|
121
141
|
end
|
122
142
|
|
123
143
|
private
|
124
144
|
|
125
145
|
def each_folder
|
126
|
-
backup_folders.each do |
|
127
|
-
|
128
|
-
serializer = Serializer::Mbox.new(local_path, backup_folder[:name])
|
146
|
+
backup_folders.each do |folder|
|
147
|
+
serializer = Serializer::Mbox.new(account.local_path, folder.name)
|
129
148
|
yield folder, serializer
|
130
149
|
end
|
131
150
|
end
|
@@ -133,16 +152,16 @@ module Imap::Backup
|
|
133
152
|
def restore_folder(serializer, folder)
|
134
153
|
existing_uids = folder.uids
|
135
154
|
if existing_uids.any?
|
136
|
-
Imap::Backup.logger.debug(
|
155
|
+
Imap::Backup::Logger.logger.debug(
|
137
156
|
"There's already a '#{folder.name}' folder with emails"
|
138
157
|
)
|
139
158
|
new_name = serializer.apply_uid_validity(folder.uid_validity)
|
140
159
|
old_name = serializer.folder
|
141
160
|
if new_name
|
142
|
-
Imap::Backup.logger.debug(
|
161
|
+
Imap::Backup::Logger.logger.debug(
|
143
162
|
"Backup '#{old_name}' renamed and restored to '#{new_name}'"
|
144
163
|
)
|
145
|
-
new_serializer = Serializer::Mbox.new(local_path, new_name)
|
164
|
+
new_serializer = Serializer::Mbox.new(account.local_path, new_name)
|
146
165
|
new_folder = Account::Folder.new(self, new_name)
|
147
166
|
new_folder.create
|
148
167
|
new_serializer.force_uid_validity(new_folder.uid_validity)
|
@@ -159,33 +178,26 @@ module Imap::Backup
|
|
159
178
|
|
160
179
|
def create_account_folder
|
161
180
|
Utils.make_folder(
|
162
|
-
File.dirname(local_path),
|
163
|
-
File.basename(local_path),
|
181
|
+
File.dirname(account.local_path),
|
182
|
+
File.basename(account.local_path),
|
164
183
|
Serializer::DIRECTORY_PERMISSIONS
|
165
184
|
)
|
166
185
|
end
|
167
186
|
|
168
187
|
def masked_password
|
169
|
-
password.gsub(/./, "x")
|
170
|
-
end
|
171
|
-
|
172
|
-
def backup_folders
|
173
|
-
@backup_folders ||=
|
174
|
-
begin
|
175
|
-
if @config_folders&.any?
|
176
|
-
@config_folders
|
177
|
-
else
|
178
|
-
folders.map { |name| {name: name} }
|
179
|
-
end
|
180
|
-
end
|
188
|
+
account.password.gsub(/./, "x")
|
181
189
|
end
|
182
190
|
|
183
191
|
def provider
|
184
|
-
@provider ||= Email::Provider.for_address(username)
|
192
|
+
@provider ||= Email::Provider.for_address(account.username)
|
185
193
|
end
|
186
194
|
|
187
195
|
def provider_options
|
188
|
-
provider.options.merge(connection_options)
|
196
|
+
provider.options.merge(account.connection_options || {})
|
197
|
+
end
|
198
|
+
|
199
|
+
def config
|
200
|
+
@config ||= Configuration.new
|
189
201
|
end
|
190
202
|
end
|
191
203
|
end
|
@@ -3,7 +3,7 @@ require "forwardable"
|
|
3
3
|
require "retry_on_error"
|
4
4
|
|
5
5
|
module Imap::Backup
|
6
|
-
|
6
|
+
class Account; end
|
7
7
|
|
8
8
|
class FolderNotFound < StandardError; end
|
9
9
|
|
@@ -64,7 +64,7 @@ module Imap::Backup
|
|
64
64
|
in `search_internal` in stdlib net/imap.rb.
|
65
65
|
This is caused by `@responses["SEARCH"] being unset/undefined
|
66
66
|
MESSAGE
|
67
|
-
Imap::Backup.logger.warn message
|
67
|
+
Imap::Backup::Logger.logger.warn message
|
68
68
|
[]
|
69
69
|
end
|
70
70
|
|
@@ -84,6 +84,26 @@ module Imap::Backup
|
|
84
84
|
nil
|
85
85
|
end
|
86
86
|
|
87
|
+
def fetch_multi(uids)
|
88
|
+
examine
|
89
|
+
fetch_data_items =
|
90
|
+
retry_on_error(errors: UID_FETCH_RETRY_CLASSES) do
|
91
|
+
client.uid_fetch(uids, [BODY_ATTRIBUTE])
|
92
|
+
end
|
93
|
+
return nil if fetch_data_items.nil?
|
94
|
+
|
95
|
+
fetch_data_items.map do |item|
|
96
|
+
attributes = item.attr
|
97
|
+
|
98
|
+
{
|
99
|
+
uid: attributes["UID"],
|
100
|
+
body: attributes[BODY_ATTRIBUTE]
|
101
|
+
}
|
102
|
+
end
|
103
|
+
rescue FolderNotFound
|
104
|
+
nil
|
105
|
+
end
|
106
|
+
|
87
107
|
def append(message)
|
88
108
|
body = message.imap_body
|
89
109
|
date = message.date&.to_time
|
@@ -96,7 +116,7 @@ module Imap::Backup
|
|
96
116
|
def examine
|
97
117
|
client.examine(utf7_encoded_name)
|
98
118
|
rescue Net::IMAP::NoResponseError
|
99
|
-
Imap::Backup.logger.warn "Folder '#{name}' does not exist on server"
|
119
|
+
Imap::Backup::Logger.logger.warn "Folder '#{name}' does not exist on server"
|
100
120
|
raise FolderNotFound, "Folder '#{name}' does not exist on server"
|
101
121
|
end
|
102
122
|
|
data/lib/imap/backup/account.rb
CHANGED
@@ -20,6 +20,10 @@ module Imap::Backup
|
|
20
20
|
@marked_for_deletion = false
|
21
21
|
end
|
22
22
|
|
23
|
+
def connection
|
24
|
+
Account::Connection.new(self)
|
25
|
+
end
|
26
|
+
|
23
27
|
def valid?
|
24
28
|
username && password
|
25
29
|
end
|
@@ -83,14 +87,9 @@ module Imap::Backup
|
|
83
87
|
def update(field, value)
|
84
88
|
if changes[field]
|
85
89
|
change = changes[field]
|
86
|
-
if change[:from] == value
|
87
|
-
changes.delete(field)
|
88
|
-
else
|
89
|
-
set_field!(field, value)
|
90
|
-
end
|
91
|
-
else
|
92
|
-
set_field!(field, value)
|
90
|
+
changes.delete(field) if change[:from] == value
|
93
91
|
end
|
92
|
+
set_field!(field, value)
|
94
93
|
end
|
95
94
|
|
96
95
|
def set_field!(field, value)
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module Imap::Backup
|
2
|
+
class CLI; end
|
3
|
+
|
4
|
+
class CLI::Accounts
|
5
|
+
include Enumerable
|
6
|
+
|
7
|
+
attr_reader :required_accounts
|
8
|
+
|
9
|
+
def initialize(required_accounts = [])
|
10
|
+
@required_accounts = required_accounts
|
11
|
+
end
|
12
|
+
|
13
|
+
def each(&block)
|
14
|
+
return enum_for(:each) if !block
|
15
|
+
|
16
|
+
accounts.each(&block)
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def accounts
|
22
|
+
@accounts ||=
|
23
|
+
if required_accounts.empty?
|
24
|
+
config.accounts
|
25
|
+
else
|
26
|
+
config.accounts.select do |account|
|
27
|
+
required_accounts.include?(account.username)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def config
|
33
|
+
@config ||= begin
|
34
|
+
exists = Configuration.exist?
|
35
|
+
if !exists
|
36
|
+
path = Configuration.default_pathname
|
37
|
+
raise ConfigurationNotFound, "Configuration file '#{path}' not found"
|
38
|
+
end
|
39
|
+
Configuration.new
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -14,7 +14,9 @@ module Imap::Backup
|
|
14
14
|
def run
|
15
15
|
each_connection(account_names) do |connection|
|
16
16
|
puts connection.username
|
17
|
-
|
17
|
+
# TODO: Make folder_names private once this command
|
18
|
+
# has been removed.
|
19
|
+
folders = connection.folder_names
|
18
20
|
if folders.nil?
|
19
21
|
warn "Unable to list account folders"
|
20
22
|
return false
|
@@ -1,4 +1,5 @@
|
|
1
1
|
require "imap/backup"
|
2
|
+
require "imap/backup/cli/accounts"
|
2
3
|
|
3
4
|
module Imap::Backup::CLI::Helpers
|
4
5
|
def symbolized(options)
|
@@ -6,8 +7,8 @@ module Imap::Backup::CLI::Helpers
|
|
6
7
|
end
|
7
8
|
|
8
9
|
def account(email)
|
9
|
-
|
10
|
-
account =
|
10
|
+
accounts = Imap::Backup::CLI::Accounts.new
|
11
|
+
account = accounts.find { |a| a.username == email }
|
11
12
|
raise "#{email} is not a configured account" if !account
|
12
13
|
|
13
14
|
account
|
@@ -20,14 +21,12 @@ module Imap::Backup::CLI::Helpers
|
|
20
21
|
end
|
21
22
|
|
22
23
|
def each_connection(names)
|
23
|
-
|
24
|
-
connections = Imap::Backup::Configuration::List.new(names)
|
25
|
-
rescue Imap::Backup::ConfigurationNotFound
|
26
|
-
raise "imap-backup is not configured. Run `imap-backup setup`"
|
27
|
-
end
|
24
|
+
accounts = Imap::Backup::CLI::Accounts.new(names)
|
28
25
|
|
29
|
-
|
30
|
-
yield connection
|
26
|
+
accounts.each do |account|
|
27
|
+
yield account.connection
|
31
28
|
end
|
29
|
+
rescue Imap::Backup::ConfigurationNotFound
|
30
|
+
raise "imap-backup is not configured. Run `imap-backup setup`"
|
32
31
|
end
|
33
32
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
require "imap/backup/cli/accounts"
|
2
|
+
|
1
3
|
module Imap::Backup
|
2
4
|
class CLI::Local < Thor
|
3
5
|
include Thor::Actions
|
@@ -5,8 +7,8 @@ module Imap::Backup
|
|
5
7
|
|
6
8
|
desc "accounts", "List locally backed-up accounts"
|
7
9
|
def accounts
|
8
|
-
|
9
|
-
|
10
|
+
accounts = CLI::Accounts.new
|
11
|
+
accounts.each { |a| Kernel.puts a.username }
|
10
12
|
end
|
11
13
|
|
12
14
|
desc "folders EMAIL", "List account folders"
|
@@ -13,7 +13,7 @@ module Imap::Backup
|
|
13
13
|
no_commands do
|
14
14
|
def run
|
15
15
|
each_connection(account_names) do |connection|
|
16
|
-
puts connection.username
|
16
|
+
puts connection.account.username
|
17
17
|
folders = connection.status
|
18
18
|
folders.each do |f|
|
19
19
|
missing_locally = f[:remote] - f[:local]
|
@@ -11,9 +11,10 @@ module Imap::Backup
|
|
11
11
|
def ignore_history(email)
|
12
12
|
connection = connection(email)
|
13
13
|
|
14
|
-
connection.
|
14
|
+
connection.backup_folders.each do |folder|
|
15
15
|
next if !folder.exist?
|
16
16
|
|
17
|
+
serializer = Serializer::Mbox.new(connection.account.local_path, folder.name)
|
17
18
|
do_ignore_folder_history(folder, serializer)
|
18
19
|
end
|
19
20
|
end
|
@@ -63,7 +64,7 @@ module Imap::Backup
|
|
63
64
|
no_commands do
|
64
65
|
def do_ignore_folder_history(folder, serializer)
|
65
66
|
uids = folder.uids - serializer.uids
|
66
|
-
Imap::Backup.logger.info "Folder '#{folder.name}' - #{uids.length} messages"
|
67
|
+
Imap::Backup::Logger.logger.info "Folder '#{folder.name}' - #{uids.length} messages"
|
67
68
|
|
68
69
|
serializer.apply_uid_validity(folder.uid_validity)
|
69
70
|
|
@@ -1,11 +1,13 @@
|
|
1
1
|
require "json"
|
2
2
|
require "os"
|
3
3
|
|
4
|
-
|
5
|
-
module Configuration; end
|
4
|
+
require "imap/backup/account"
|
6
5
|
|
7
|
-
|
6
|
+
module Imap::Backup
|
7
|
+
class Configuration
|
8
8
|
CONFIGURATION_DIRECTORY = File.expand_path("~/.imap-backup")
|
9
|
+
DEFAULT_DOWNLOAD_BLOCK_SIZE = 1
|
10
|
+
VERSION = "2.0"
|
9
11
|
|
10
12
|
attr_reader :pathname
|
11
13
|
|
@@ -19,6 +21,8 @@ module Imap::Backup
|
|
19
21
|
|
20
22
|
def initialize(pathname = self.class.default_pathname)
|
21
23
|
@pathname = pathname
|
24
|
+
@saved_debug = nil
|
25
|
+
@debug = nil
|
22
26
|
end
|
23
27
|
|
24
28
|
def path
|
@@ -26,53 +30,84 @@ module Imap::Backup
|
|
26
30
|
end
|
27
31
|
|
28
32
|
def save
|
33
|
+
ensure_loaded!
|
29
34
|
FileUtils.mkdir(path) if !File.directory?(path)
|
30
35
|
make_private(path) if !windows?
|
31
36
|
remove_modified_flags
|
32
37
|
remove_deleted_accounts
|
33
|
-
|
38
|
+
save_data = {
|
39
|
+
version: VERSION,
|
40
|
+
accounts: accounts.map(&:to_h),
|
41
|
+
debug: debug?
|
42
|
+
}
|
43
|
+
File.open(pathname, "w") { |f| f.write(JSON.pretty_generate(save_data)) }
|
34
44
|
FileUtils.chmod(0o600, pathname) if !windows?
|
45
|
+
@data = nil
|
35
46
|
end
|
36
47
|
|
37
48
|
def accounts
|
38
|
-
|
49
|
+
@accounts ||= begin
|
50
|
+
ensure_loaded!
|
51
|
+
data[:accounts].map { |data| Account.new(data) }
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def download_block_size
|
56
|
+
size = ENV["DOWNLOAD_BLOCK_SIZE"].to_i
|
57
|
+
if size > 0
|
58
|
+
size
|
59
|
+
else
|
60
|
+
DEFAULT_DOWNLOAD_BLOCK_SIZE
|
61
|
+
end
|
39
62
|
end
|
40
63
|
|
41
64
|
def modified?
|
42
|
-
|
65
|
+
ensure_loaded!
|
66
|
+
return true if @saved_debug != @debug
|
67
|
+
|
68
|
+
accounts.any? { |a| a.modified? || a.marked_for_deletion? }
|
43
69
|
end
|
44
70
|
|
45
71
|
def debug?
|
46
|
-
|
72
|
+
ensure_loaded!
|
73
|
+
@debug
|
47
74
|
end
|
48
75
|
|
49
76
|
def debug=(value)
|
50
|
-
|
77
|
+
ensure_loaded!
|
78
|
+
@debug = [true, false].include?(value) ? value : false
|
51
79
|
end
|
52
80
|
|
53
81
|
private
|
54
82
|
|
83
|
+
def ensure_loaded!
|
84
|
+
return true if @data
|
85
|
+
|
86
|
+
data
|
87
|
+
@debug = data.key?(:debug) ? data[:debug] == true : false
|
88
|
+
@saved_debug = @debug
|
89
|
+
true
|
90
|
+
end
|
91
|
+
|
55
92
|
def data
|
56
93
|
@data ||=
|
57
94
|
begin
|
58
95
|
if File.exist?(pathname)
|
59
96
|
Utils.check_permissions(pathname, 0o600) if !windows?
|
60
97
|
contents = File.read(pathname)
|
61
|
-
|
98
|
+
JSON.parse(contents, symbolize_names: true)
|
62
99
|
else
|
63
|
-
|
100
|
+
{accounts: []}
|
64
101
|
end
|
65
|
-
data[:debug] = data.key?(:debug) ? data[:debug] == true : false
|
66
|
-
data
|
67
102
|
end
|
68
103
|
end
|
69
104
|
|
70
105
|
def remove_modified_flags
|
71
|
-
accounts.each { |a| a.
|
106
|
+
accounts.each { |a| a.clear_changes! }
|
72
107
|
end
|
73
108
|
|
74
109
|
def remove_deleted_accounts
|
75
|
-
accounts.reject! { |a| a
|
110
|
+
accounts.reject! { |a| a.marked_for_deletion? }
|
76
111
|
end
|
77
112
|
|
78
113
|
def make_private(path)
|
@@ -2,27 +2,41 @@ module Imap::Backup
|
|
2
2
|
class Downloader
|
3
3
|
attr_reader :folder
|
4
4
|
attr_reader :serializer
|
5
|
+
attr_reader :block_size
|
5
6
|
|
6
|
-
def initialize(folder, serializer)
|
7
|
+
def initialize(folder, serializer, block_size: 1)
|
7
8
|
@folder = folder
|
8
9
|
@serializer = serializer
|
10
|
+
@block_size = block_size
|
9
11
|
end
|
10
12
|
|
11
13
|
def run
|
12
14
|
uids = folder.uids - serializer.uids
|
13
15
|
count = uids.count
|
14
|
-
Imap::Backup.logger.debug "[#{folder.name}] #{count} new messages"
|
15
|
-
uids.
|
16
|
-
|
17
|
-
|
18
|
-
if
|
19
|
-
|
20
|
-
|
16
|
+
Imap::Backup::Logger.logger.debug "[#{folder.name}] #{count} new messages"
|
17
|
+
uids.each_slice(block_size).with_index do |block, i|
|
18
|
+
offset = i * block_size + 1
|
19
|
+
uids_and_bodies = folder.fetch_multi(block)
|
20
|
+
if uids_and_bodies.nil?
|
21
|
+
if block_size > 1
|
22
|
+
Imap::Backup::Logger.logger.debug("[#{folder.name}] Multi fetch failed for UIDs #{block.join(", ")}, switching to single fetches")
|
23
|
+
@block_size = 1
|
24
|
+
redo
|
25
|
+
else
|
26
|
+
Imap::Backup::Logger.logger.debug("[#{folder.name}] Fetch failed for UID #{block[0]} - skipping")
|
27
|
+
next
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
uids_and_bodies.each.with_index do |uid_and_body, j|
|
32
|
+
uid = uid_and_body[:uid]
|
33
|
+
body = uid_and_body[:body]
|
34
|
+
Imap::Backup::Logger.logger.debug(
|
35
|
+
"[#{folder.name}] uid: #{uid} (#{offset +j}/#{count}) - " \
|
36
|
+
"#{body.size} bytes"
|
37
|
+
)
|
38
|
+
serializer.save(uid, body)
|
21
39
|
end
|
22
|
-
Imap::Backup.logger.debug(
|
23
|
-
"#{log_prefix} #{body.size} bytes"
|
24
|
-
)
|
25
|
-
serializer.save(uid, body)
|
26
40
|
end
|
27
41
|
end
|
28
42
|
end
|