imap-backup 3.0.0 → 3.3.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.circleci/config.yml +51 -0
- data/README.md +8 -2
- data/Rakefile +6 -0
- data/bin/imap-backup +1 -1
- data/imap-backup.gemspec +1 -0
- data/lib/imap/backup/account/connection.rb +60 -41
- data/lib/imap/backup/account/folder.rb +18 -6
- data/lib/imap/backup/configuration/folder_chooser.rb +14 -15
- data/lib/imap/backup/configuration/gmail_oauth2.rb +26 -6
- data/lib/imap/backup/configuration/store.rb +12 -12
- data/lib/imap/backup/serializer/mbox_store.rb +2 -2
- data/lib/imap/backup/version.rb +2 -2
- data/lib/retry_on_error.rb +14 -0
- data/spec/fixtures/connection.yml +1 -1
- data/spec/support/fixtures.rb +7 -2
- data/spec/unit/gmail/authenticator_spec.rb +1 -1
- data/spec/unit/imap/backup/account/connection_spec.rb +64 -16
- data/spec/unit/imap/backup/account/folder_spec.rb +42 -10
- data/spec/unit/imap/backup/configuration/folder_chooser_spec.rb +7 -13
- data/spec/unit/imap/backup/configuration/gmail_oauth2_spec.rb +42 -5
- metadata +19 -4
- data/.travis.yml +0 -25
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c3446b53604f5895cb09b109cbe663bc4ac131daeb63989b09ae8985c480464b
|
4
|
+
data.tar.gz: 8c3b6a7176dbda814f208505c49ed63f27d650f683dad7941734e8dbd4018601
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6926f54cb421029116f0e63533e1a3c3151937c9572b2c5b2143cab08e3c1aef81cba6ae3dcc2c799f77e37f3b983c4adee73f6fa90f585cddf030cff8fffd9d
|
7
|
+
data.tar.gz: 1ed5e647c1ef08049f928293295cb564d7e87b592dd499dc903bab74717892fffc2aa19d4f214824757dcb80f46356c12864a7cf8332b3f25512c1af637f1aff
|
@@ -0,0 +1,51 @@
|
|
1
|
+
version: 2.1
|
2
|
+
|
3
|
+
orbs:
|
4
|
+
ruby: circleci/ruby@1.1.2
|
5
|
+
|
6
|
+
references:
|
7
|
+
restore: &restore
|
8
|
+
restore_cache:
|
9
|
+
keys:
|
10
|
+
- 'imap.backup.<< parameters.ruby_version >>.{{checksum "imap-backup.gemspec"}}'
|
11
|
+
bundle: &bundle
|
12
|
+
run:
|
13
|
+
name: Install Ruby dependencies
|
14
|
+
command: |
|
15
|
+
bundle install
|
16
|
+
bundle clean
|
17
|
+
save: &save
|
18
|
+
save_cache:
|
19
|
+
key: 'imap.backup.<< parameters.ruby_version >>.{{checksum "imap-backup.gemspec"}}'
|
20
|
+
paths:
|
21
|
+
- vendor/bundle
|
22
|
+
|
23
|
+
jobs:
|
24
|
+
test:
|
25
|
+
parameters:
|
26
|
+
ruby_version:
|
27
|
+
type: string
|
28
|
+
environment:
|
29
|
+
BUNDLE_PATH: ./vendor/bundle
|
30
|
+
DOCKER_IMAP_PORT: 993
|
31
|
+
docker:
|
32
|
+
- image: "cimg/ruby:<< parameters.ruby_version >>"
|
33
|
+
- image: antespi/docker-imap-devel:latest
|
34
|
+
environment:
|
35
|
+
MAIL_ADDRESS: address@example.org
|
36
|
+
MAIL_PASS: pass
|
37
|
+
MAILNAME: example.org
|
38
|
+
steps:
|
39
|
+
- checkout
|
40
|
+
- <<: *restore
|
41
|
+
- <<: *bundle
|
42
|
+
- <<: *save
|
43
|
+
- ruby/rspec-test
|
44
|
+
|
45
|
+
workflows:
|
46
|
+
all-tests:
|
47
|
+
jobs:
|
48
|
+
- test:
|
49
|
+
matrix:
|
50
|
+
parameters:
|
51
|
+
ruby_version: ["2.4", "2.5", "2.6", "2.7"]
|
data/README.md
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
[![Build Status](https://
|
1
|
+
[![Build Status](https://circleci.com/gh/joeyates/imap-backup.svg?style=svg)][Continuous Integration]
|
2
2
|
[![Source Analysis](https://codeclimate.com/github/joeyates/imap-backup/badges/gpa.svg)](https://codeclimate.com/github/joeyates/imap-backup)
|
3
3
|
[![Test Coverage](https://codeclimate.com/github/joeyates/imap-backup/badges/coverage.svg)](https://codeclimate.com/github/joeyates/imap-backup/coverage)
|
4
4
|
|
@@ -14,7 +14,7 @@
|
|
14
14
|
[Source Code]: https://github.com/joeyates/imap-backup "Source code at GitHub"
|
15
15
|
[API documentation]: http://rubydoc.info/gems/imap-backup/frames "RDoc API Documentation at Rubydoc.info"
|
16
16
|
[Rubygem]: http://rubygems.org/gems/imap-backup "Ruby gem at rubygems.org"
|
17
|
-
[Continuous Integration]:
|
17
|
+
[Continuous Integration]: https://circleci.com/gh/joeyates/imap-backup "Build status by CirceCI"
|
18
18
|
|
19
19
|
## GMail
|
20
20
|
|
@@ -212,6 +212,12 @@ $ rake
|
|
212
212
|
|
213
213
|
To exclude Docker-based tests:
|
214
214
|
|
215
|
+
```sh
|
216
|
+
rake no-docker
|
217
|
+
```
|
218
|
+
|
219
|
+
or
|
220
|
+
|
215
221
|
```sh
|
216
222
|
$ rspec --tag ~docker
|
217
223
|
```
|
data/Rakefile
CHANGED
@@ -6,6 +6,12 @@ RSpec::Core::RakeTask.new do |t|
|
|
6
6
|
t.pattern = "spec/**/*_spec.rb"
|
7
7
|
end
|
8
8
|
|
9
|
+
desc "Run RSpec examples, excluding ones relying on Docker IMAP"
|
10
|
+
RSpec::Core::RakeTask.new("no-docker") do |t|
|
11
|
+
t.pattern = "spec/**/*_spec.rb"
|
12
|
+
t.rspec_opts = "--tag ~docker"
|
13
|
+
end
|
14
|
+
|
9
15
|
RuboCop::RakeTask.new
|
10
16
|
|
11
17
|
task default: :spec
|
data/bin/imap-backup
CHANGED
data/imap-backup.gemspec
CHANGED
@@ -29,6 +29,7 @@ Gem::Specification.new do |gem|
|
|
29
29
|
gem.add_development_dependency "pry-byebug"
|
30
30
|
end
|
31
31
|
gem.add_development_dependency "rspec", ">= 3.0.0"
|
32
|
+
gem.add_development_dependency "rspec_junit_formatter"
|
32
33
|
gem.add_development_dependency "rubocop-rspec"
|
33
34
|
gem.add_development_dependency "simplecov"
|
34
35
|
end
|
@@ -2,6 +2,7 @@ require "net/imap"
|
|
2
2
|
require "gmail_xoauth"
|
3
3
|
|
4
4
|
require "gmail/authenticator"
|
5
|
+
require "retry_on_error"
|
5
6
|
|
6
7
|
module Imap::Backup
|
7
8
|
module Account; end
|
@@ -9,6 +10,10 @@ module Imap::Backup
|
|
9
10
|
class Account::Connection
|
10
11
|
class InvalidGmailOauth2RefreshToken < StandardError; end
|
11
12
|
|
13
|
+
include RetryOnError
|
14
|
+
|
15
|
+
LOGIN_RETRY_CLASSES = [EOFError, Errno::ECONNRESET, SocketError].freeze
|
16
|
+
|
12
17
|
attr_reader :connection_options
|
13
18
|
attr_reader :local_path
|
14
19
|
attr_reader :password
|
@@ -18,7 +23,7 @@ module Imap::Backup
|
|
18
23
|
@username = options[:username]
|
19
24
|
@password = options[:password]
|
20
25
|
@local_path = options[:local_path]
|
21
|
-
@
|
26
|
+
@config_folders = options[:folders]
|
22
27
|
@server = options[:server]
|
23
28
|
@connection_options = options[:connection_options] || {}
|
24
29
|
@folders = nil
|
@@ -29,21 +34,24 @@ module Imap::Backup
|
|
29
34
|
@folders ||=
|
30
35
|
begin
|
31
36
|
root = provider_root
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
+
mailbox_lists = imap.list(root, "*")
|
38
|
+
|
39
|
+
if mailbox_lists.nil?
|
40
|
+
message = "Unable to get folder list for account #{username}"
|
41
|
+
Imap::Backup.logger.info message
|
42
|
+
raise message
|
37
43
|
end
|
38
|
-
|
44
|
+
|
45
|
+
utf7_encoded = mailbox_lists.map(&:name)
|
46
|
+
utf7_encoded.map { |n| Net::IMAP.decode_utf7(n) }
|
39
47
|
end
|
40
48
|
end
|
41
49
|
|
42
50
|
def status
|
43
|
-
backup_folders.map do |
|
44
|
-
f = Account::Folder.new(self,
|
45
|
-
s = Serializer::Mbox.new(local_path,
|
46
|
-
{name:
|
51
|
+
backup_folders.map do |backup_folder|
|
52
|
+
f = Account::Folder.new(self, backup_folder[:name])
|
53
|
+
s = Serializer::Mbox.new(local_path, backup_folder[:name])
|
54
|
+
{name: backup_folder[:name], local: s.uids, remote: f.uids}
|
47
55
|
end
|
48
56
|
end
|
49
57
|
|
@@ -56,7 +64,12 @@ module Imap::Backup
|
|
56
64
|
|
57
65
|
Imap::Backup.logger.debug "[#{folder.name}] running backup"
|
58
66
|
serializer.apply_uid_validity(folder.uid_validity)
|
59
|
-
|
67
|
+
begin
|
68
|
+
Downloader.new(folder, serializer).run
|
69
|
+
rescue Net::IMAP::ByeResponseError
|
70
|
+
reconnect
|
71
|
+
retry
|
72
|
+
end
|
60
73
|
end
|
61
74
|
end
|
62
75
|
|
@@ -76,26 +89,27 @@ module Imap::Backup
|
|
76
89
|
end
|
77
90
|
|
78
91
|
def imap
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
92
|
+
@imap ||=
|
93
|
+
retry_on_error(errors: LOGIN_RETRY_CLASSES) do
|
94
|
+
options = provider_options
|
95
|
+
Imap::Backup.logger.debug(
|
96
|
+
"Creating IMAP instance: #{server}, options: #{options.inspect}"
|
97
|
+
)
|
98
|
+
imap = Net::IMAP.new(server, options)
|
99
|
+
if gmail? && Gmail::Authenticator.refresh_token?(password)
|
100
|
+
authenticator = Gmail::Authenticator.new(email: username, token: password)
|
101
|
+
credentials = authenticator.credentials
|
102
|
+
raise InvalidGmailOauth2RefreshToken if !credentials
|
103
|
+
|
104
|
+
Imap::Backup.logger.debug "Logging in with OAuth2 token: #{username}"
|
105
|
+
imap.authenticate("XOAUTH2", username, credentials.access_token)
|
106
|
+
else
|
107
|
+
Imap::Backup.logger.debug "Logging in: #{username}/#{masked_password}"
|
108
|
+
imap.login(username, password)
|
109
|
+
end
|
110
|
+
Imap::Backup.logger.debug "Login complete"
|
111
|
+
imap
|
112
|
+
end
|
99
113
|
end
|
100
114
|
|
101
115
|
def server
|
@@ -105,18 +119,12 @@ module Imap::Backup
|
|
105
119
|
@server = provider.host
|
106
120
|
end
|
107
121
|
|
108
|
-
def backup_folders
|
109
|
-
return @backup_folders if @backup_folders && !@backup_folders.empty?
|
110
|
-
|
111
|
-
(folders || []).map { |f| {name: f.name} }
|
112
|
-
end
|
113
|
-
|
114
122
|
private
|
115
123
|
|
116
124
|
def each_folder
|
117
|
-
backup_folders.each do |
|
118
|
-
folder = Account::Folder.new(self,
|
119
|
-
serializer = Serializer::Mbox.new(local_path,
|
125
|
+
backup_folders.each do |backup_folder|
|
126
|
+
folder = Account::Folder.new(self, backup_folder[:name])
|
127
|
+
serializer = Serializer::Mbox.new(local_path, backup_folder[:name])
|
120
128
|
yield folder, serializer
|
121
129
|
end
|
122
130
|
end
|
@@ -175,6 +183,17 @@ module Imap::Backup
|
|
175
183
|
end
|
176
184
|
end
|
177
185
|
|
186
|
+
def backup_folders
|
187
|
+
@backup_folders ||=
|
188
|
+
begin
|
189
|
+
if @config_folders&.any?
|
190
|
+
@config_folders
|
191
|
+
else
|
192
|
+
folders.map { |name| {name: name} }
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
178
197
|
def provider
|
179
198
|
@provider ||= Email::Provider.for_address(username)
|
180
199
|
end
|
@@ -1,5 +1,7 @@
|
|
1
1
|
require "forwardable"
|
2
2
|
|
3
|
+
require "retry_on_error"
|
4
|
+
|
3
5
|
module Imap::Backup
|
4
6
|
module Account; end
|
5
7
|
|
@@ -7,8 +9,10 @@ module Imap::Backup
|
|
7
9
|
|
8
10
|
class Account::Folder
|
9
11
|
extend Forwardable
|
12
|
+
include RetryOnError
|
10
13
|
|
11
14
|
REQUESTED_ATTRIBUTES = %w[RFC822 FLAGS INTERNALDATE].freeze
|
15
|
+
UID_FETCH_RETRY_CLASSES = [EOFError].freeze
|
12
16
|
|
13
17
|
attr_reader :connection
|
14
18
|
attr_reader :name
|
@@ -36,7 +40,7 @@ module Imap::Backup
|
|
36
40
|
def create
|
37
41
|
return if exist?
|
38
42
|
|
39
|
-
imap.create(
|
43
|
+
imap.create(utf7_encoded_name)
|
40
44
|
end
|
41
45
|
|
42
46
|
def uid_validity
|
@@ -66,7 +70,10 @@ module Imap::Backup
|
|
66
70
|
|
67
71
|
def fetch(uid)
|
68
72
|
examine
|
69
|
-
fetch_data_items =
|
73
|
+
fetch_data_items =
|
74
|
+
retry_on_error(errors: UID_FETCH_RETRY_CLASSES) do
|
75
|
+
imap.uid_fetch([uid.to_i], REQUESTED_ATTRIBUTES)
|
76
|
+
end
|
70
77
|
return nil if fetch_data_items.nil?
|
71
78
|
|
72
79
|
fetch_data_item = fetch_data_items[0]
|
@@ -81,22 +88,27 @@ module Imap::Backup
|
|
81
88
|
def append(message)
|
82
89
|
body = message.imap_body
|
83
90
|
date = message.date&.to_time
|
84
|
-
response = imap.append(
|
91
|
+
response = imap.append(utf7_encoded_name, body, nil, date)
|
85
92
|
extract_uid(response)
|
86
93
|
end
|
87
94
|
|
88
95
|
private
|
89
96
|
|
90
97
|
def examine
|
91
|
-
imap.examine(
|
98
|
+
imap.examine(utf7_encoded_name)
|
92
99
|
rescue Net::IMAP::NoResponseError
|
93
|
-
Imap::Backup.logger.warn "Folder '#{name}' does not exist"
|
94
|
-
raise FolderNotFound, "Folder '#{name}' does not exist"
|
100
|
+
Imap::Backup.logger.warn "Folder '#{name}' does not exist on server"
|
101
|
+
raise FolderNotFound, "Folder '#{name}' does not exist on server"
|
95
102
|
end
|
96
103
|
|
97
104
|
def extract_uid(response)
|
98
105
|
@uid_validity, uid = response.data.code.data.split(" ").map(&:to_i)
|
99
106
|
uid
|
100
107
|
end
|
108
|
+
|
109
|
+
def utf7_encoded_name
|
110
|
+
@utf7_encoded_name ||=
|
111
|
+
Net::IMAP.encode_utf7(name).force_encoding("ASCII-8BIT")
|
112
|
+
end
|
101
113
|
end
|
102
114
|
end
|
@@ -15,7 +15,7 @@ module Imap::Backup
|
|
15
15
|
return
|
16
16
|
end
|
17
17
|
|
18
|
-
if
|
18
|
+
if imap_folders.nil?
|
19
19
|
Imap::Backup.logger.warn "Unable to get folder list"
|
20
20
|
highline.ask "Press a key "
|
21
21
|
return
|
@@ -44,29 +44,28 @@ module Imap::Backup
|
|
44
44
|
end
|
45
45
|
|
46
46
|
def add_folders(menu)
|
47
|
-
|
48
|
-
|
49
|
-
mark
|
50
|
-
|
51
|
-
toggle_selection name
|
47
|
+
imap_folders.each do |folder|
|
48
|
+
mark = selected?(folder) ? "+" : "-"
|
49
|
+
menu.choice("#{mark} #{folder}") do
|
50
|
+
toggle_selection folder
|
52
51
|
end
|
53
52
|
end
|
54
53
|
end
|
55
54
|
|
56
55
|
def selected?(folder_name)
|
57
|
-
|
58
|
-
return false if
|
56
|
+
config_folders = account[:folders]
|
57
|
+
return false if config_folders.nil?
|
59
58
|
|
60
|
-
|
59
|
+
config_folders.find { |f| f[:name] == folder_name }
|
61
60
|
end
|
62
61
|
|
63
62
|
def remove_missing
|
64
63
|
removed = []
|
65
|
-
|
64
|
+
config_folders = []
|
66
65
|
account[:folders].each do |f|
|
67
|
-
found =
|
66
|
+
found = imap_folders.find { |folder| folder == f[:name] }
|
68
67
|
if found
|
69
|
-
|
68
|
+
config_folders << f
|
70
69
|
else
|
71
70
|
removed << f[:name]
|
72
71
|
end
|
@@ -74,7 +73,7 @@ module Imap::Backup
|
|
74
73
|
|
75
74
|
return if removed.empty?
|
76
75
|
|
77
|
-
account[:folders] =
|
76
|
+
account[:folders] = config_folders
|
78
77
|
account[:modified] = true
|
79
78
|
|
80
79
|
Kernel.puts <<~MESSAGE
|
@@ -101,8 +100,8 @@ module Imap::Backup
|
|
101
100
|
nil
|
102
101
|
end
|
103
102
|
|
104
|
-
def
|
105
|
-
@
|
103
|
+
def imap_folders
|
104
|
+
@imap_folders ||= connection.folders
|
106
105
|
end
|
107
106
|
|
108
107
|
def highline
|
@@ -8,7 +8,7 @@ module Imap::Backup
|
|
8
8
|
You need to authorize imap_backup to get access to your email.
|
9
9
|
To do so, please follow the instructions here:
|
10
10
|
|
11
|
-
https://github.com/joeyates/imap-backup/docs/setting-up-gmail.md
|
11
|
+
https://github.com/joeyates/imap-backup/blob/main/docs/setting-up-gmail.md
|
12
12
|
|
13
13
|
BANNER
|
14
14
|
|
@@ -26,8 +26,20 @@ module Imap::Backup
|
|
26
26
|
def run
|
27
27
|
Kernel.system("clear")
|
28
28
|
Kernel.puts BANNER
|
29
|
-
|
30
|
-
|
29
|
+
|
30
|
+
keep = if token.valid?
|
31
|
+
highline.agree("Use existing client info?")
|
32
|
+
else
|
33
|
+
false
|
34
|
+
end
|
35
|
+
|
36
|
+
if keep
|
37
|
+
@client_id = token.client_id
|
38
|
+
@client_secret = token.client_secret
|
39
|
+
else
|
40
|
+
@client_id = highline.ask("client_id: ")
|
41
|
+
@client_secret = highline.ask("client_secret: ")
|
42
|
+
end
|
31
43
|
|
32
44
|
Kernel.puts <<~MESSAGE
|
33
45
|
|
@@ -46,9 +58,9 @@ module Imap::Backup
|
|
46
58
|
|
47
59
|
raise "Failed" if !@credentials
|
48
60
|
|
49
|
-
|
50
|
-
|
51
|
-
|
61
|
+
new_token = JSON.parse(token_store.load(email))
|
62
|
+
new_token["client_secret"] = client_secret
|
63
|
+
new_token.to_json
|
52
64
|
end
|
53
65
|
|
54
66
|
private
|
@@ -57,6 +69,14 @@ module Imap::Backup
|
|
57
69
|
account[:username]
|
58
70
|
end
|
59
71
|
|
72
|
+
def password
|
73
|
+
account[:password]
|
74
|
+
end
|
75
|
+
|
76
|
+
def token
|
77
|
+
@token ||= Gmail::Authenticator::ImapBackupToken.new(password)
|
78
|
+
end
|
79
|
+
|
60
80
|
def highline
|
61
81
|
Configuration::Setup.highline
|
62
82
|
end
|
@@ -51,18 +51,18 @@ module Imap::Backup
|
|
51
51
|
private
|
52
52
|
|
53
53
|
def data
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
54
|
+
@data ||=
|
55
|
+
begin
|
56
|
+
if File.exist?(pathname)
|
57
|
+
Utils.check_permissions pathname, 0o600
|
58
|
+
contents = File.read(pathname)
|
59
|
+
data = JSON.parse(contents, symbolize_names: true)
|
60
|
+
else
|
61
|
+
data = {accounts: []}
|
62
|
+
end
|
63
|
+
data[:debug] = data.key?(:debug) ? data[:debug] == true : false
|
64
|
+
data
|
65
|
+
end
|
66
66
|
end
|
67
67
|
|
68
68
|
def remove_modified_flags
|
@@ -161,7 +161,7 @@ module Imap::Backup
|
|
161
161
|
def load_nth(index)
|
162
162
|
enumerator = Serializer::MboxEnumerator.new(mbox_pathname)
|
163
163
|
enumerator.each.with_index do |raw, i|
|
164
|
-
next
|
164
|
+
next if i != index
|
165
165
|
|
166
166
|
return Email::Mboxrd::Message.from_serialized(raw)
|
167
167
|
end
|
@@ -169,7 +169,7 @@ module Imap::Backup
|
|
169
169
|
end
|
170
170
|
|
171
171
|
def imap_looks_like_json?
|
172
|
-
return false
|
172
|
+
return false if !imap_exist?
|
173
173
|
|
174
174
|
content = File.read(imap_pathname)
|
175
175
|
content.start_with?("{")
|
data/lib/imap/backup/version.rb
CHANGED
@@ -0,0 +1,14 @@
|
|
1
|
+
module RetryOnError
|
2
|
+
def retry_on_error(errors:, limit: 10)
|
3
|
+
tries ||= 1
|
4
|
+
yield
|
5
|
+
rescue *errors => e
|
6
|
+
if tries < limit
|
7
|
+
message = "#{e}, attempt #{tries} of #{limit}"
|
8
|
+
Imap::Backup.logger.debug message
|
9
|
+
tries += 1
|
10
|
+
retry
|
11
|
+
end
|
12
|
+
raise e
|
13
|
+
end
|
14
|
+
end
|
data/spec/support/fixtures.rb
CHANGED
@@ -1,6 +1,11 @@
|
|
1
|
+
require "erb"
|
2
|
+
require "yaml"
|
3
|
+
|
1
4
|
def fixture(name)
|
2
5
|
spec_root = File.expand_path("..", File.dirname(__FILE__))
|
3
6
|
fixture_path = File.join(spec_root, "fixtures", "#{name}.yml")
|
4
|
-
|
5
|
-
|
7
|
+
content = File.read(fixture_path)
|
8
|
+
template = ERB.new(content)
|
9
|
+
yaml = template.result(binding)
|
10
|
+
YAML.safe_load(yaml, [Symbol])
|
6
11
|
end
|
@@ -23,11 +23,11 @@ describe Imap::Backup::Account::Connection do
|
|
23
23
|
username: USERNAME,
|
24
24
|
password: PASSWORD,
|
25
25
|
local_path: LOCAL_PATH,
|
26
|
-
folders:
|
26
|
+
folders: config_folders,
|
27
27
|
server: server
|
28
28
|
}
|
29
29
|
end
|
30
|
-
let(:
|
30
|
+
let(:config_folders) { [FOLDER_CONFIG] }
|
31
31
|
let(:root_info) do
|
32
32
|
instance_double(Net::IMAP::MailboxList, name: ROOT_NAME)
|
33
33
|
end
|
@@ -62,7 +62,6 @@ describe Imap::Backup::Account::Connection do
|
|
62
62
|
[:username, USERNAME],
|
63
63
|
[:password, PASSWORD],
|
64
64
|
[:local_path, LOCAL_PATH],
|
65
|
-
[:backup_folders, [FOLDER_CONFIG]],
|
66
65
|
[:server, SERVER]
|
67
66
|
].each do |attr, expected|
|
68
67
|
it "expects #{attr}" do
|
@@ -78,13 +77,15 @@ describe Imap::Backup::Account::Connection do
|
|
78
77
|
end
|
79
78
|
|
80
79
|
describe "#imap" do
|
81
|
-
let
|
80
|
+
let(:result) { subject.imap }
|
82
81
|
|
83
82
|
it "returns the IMAP connection" do
|
84
83
|
expect(result).to eq(imap)
|
85
84
|
end
|
86
85
|
|
87
86
|
it "uses the password" do
|
87
|
+
result
|
88
|
+
|
88
89
|
expect(imap).to have_received(:login).with(USERNAME, PASSWORD)
|
89
90
|
end
|
90
91
|
|
@@ -139,16 +140,43 @@ describe Imap::Backup::Account::Connection do
|
|
139
140
|
end
|
140
141
|
end
|
141
142
|
|
142
|
-
|
143
|
+
context "when the first login attempt fails" do
|
144
|
+
before do
|
145
|
+
outcomes = [-> { raise EOFError }, -> { true }]
|
146
|
+
allow(imap).to receive(:login) { outcomes.shift.call }
|
147
|
+
end
|
148
|
+
|
149
|
+
it "retries" do
|
150
|
+
subject.imap
|
151
|
+
|
152
|
+
expect(imap).to have_received(:login).twice
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
context "when run" do
|
157
|
+
before { subject.imap }
|
158
|
+
|
159
|
+
include_examples "connects to IMAP"
|
160
|
+
end
|
143
161
|
end
|
144
162
|
|
145
163
|
describe "#folders" do
|
146
164
|
let(:imap_folders) do
|
147
|
-
[instance_double(Net::IMAP::MailboxList)]
|
165
|
+
[instance_double(Net::IMAP::MailboxList, name: BACKUP_FOLDER)]
|
148
166
|
end
|
149
167
|
|
150
168
|
it "returns the list of folders" do
|
151
|
-
expect(subject.folders).to eq(
|
169
|
+
expect(subject.folders).to eq([BACKUP_FOLDER])
|
170
|
+
end
|
171
|
+
|
172
|
+
context "with non-ASCII folder names" do
|
173
|
+
let(:imap_folders) do
|
174
|
+
[instance_double(Net::IMAP::MailboxList, name: "Gel&APY-scht")]
|
175
|
+
end
|
176
|
+
|
177
|
+
it "converts them to UTF-8" do
|
178
|
+
expect(subject.folders).to eq(["Gelöscht"])
|
179
|
+
end
|
152
180
|
end
|
153
181
|
end
|
154
182
|
|
@@ -198,7 +226,7 @@ describe Imap::Backup::Account::Connection do
|
|
198
226
|
with(LOCAL_PATH, BACKUP_FOLDER) { serializer }
|
199
227
|
end
|
200
228
|
|
201
|
-
context "with supplied
|
229
|
+
context "with supplied config_folders" do
|
202
230
|
it "runs the downloader" do
|
203
231
|
expect(downloader).to receive(:run)
|
204
232
|
|
@@ -216,7 +244,7 @@ describe Imap::Backup::Account::Connection do
|
|
216
244
|
end
|
217
245
|
end
|
218
246
|
|
219
|
-
context "without supplied
|
247
|
+
context "without supplied config_folders" do
|
220
248
|
let(:imap_folders) do
|
221
249
|
[instance_double(Net::IMAP::MailboxList, name: ROOT_NAME)]
|
222
250
|
end
|
@@ -228,8 +256,8 @@ describe Imap::Backup::Account::Connection do
|
|
228
256
|
with(LOCAL_PATH, ROOT_NAME) { serializer }
|
229
257
|
end
|
230
258
|
|
231
|
-
context "when supplied
|
232
|
-
let(:
|
259
|
+
context "when supplied config_folders is nil" do
|
260
|
+
let(:config_folders) { nil }
|
233
261
|
|
234
262
|
it "runs the downloader for each folder" do
|
235
263
|
expect(downloader).to receive(:run).exactly(:once)
|
@@ -238,8 +266,8 @@ describe Imap::Backup::Account::Connection do
|
|
238
266
|
end
|
239
267
|
end
|
240
268
|
|
241
|
-
context "when supplied
|
242
|
-
let(:
|
269
|
+
context "when supplied config_folders is an empty list" do
|
270
|
+
let(:config_folders) { [] }
|
243
271
|
|
244
272
|
it "runs the downloader for each folder" do
|
245
273
|
expect(downloader).to receive(:run).exactly(:once)
|
@@ -249,15 +277,35 @@ describe Imap::Backup::Account::Connection do
|
|
249
277
|
end
|
250
278
|
|
251
279
|
context "when the imap server doesn't return folders" do
|
252
|
-
let(:
|
280
|
+
let(:config_folders) { nil }
|
253
281
|
let(:imap_folders) { nil }
|
254
282
|
|
255
|
-
it "
|
256
|
-
expect
|
283
|
+
it "fails" do
|
284
|
+
expect do
|
285
|
+
subject.run_backup
|
286
|
+
end.to raise_error(RuntimeError, /Unable to get folder list/)
|
257
287
|
end
|
258
288
|
end
|
259
289
|
end
|
260
290
|
|
291
|
+
context "when the IMAP session expires" do
|
292
|
+
before do
|
293
|
+
data = OpenStruct.new(data: "Session expired")
|
294
|
+
response = OpenStruct.new(data: data)
|
295
|
+
outcomes = [
|
296
|
+
-> { raise Net::IMAP::ByeResponseError, response },
|
297
|
+
-> { nil }
|
298
|
+
]
|
299
|
+
allow(downloader).to receive(:run) { outcomes.shift.call }
|
300
|
+
end
|
301
|
+
|
302
|
+
it "reconnects" do
|
303
|
+
expect(downloader).to receive(:run).exactly(:twice)
|
304
|
+
|
305
|
+
subject.run_backup
|
306
|
+
end
|
307
|
+
end
|
308
|
+
|
261
309
|
context "when run" do
|
262
310
|
before { subject.run_backup }
|
263
311
|
|
@@ -1,7 +1,10 @@
|
|
1
1
|
# rubocop:disable RSpec/PredicateMatcher
|
2
2
|
|
3
3
|
describe Imap::Backup::Account::Folder do
|
4
|
-
|
4
|
+
FOLDER_NAME = "Gelöscht".freeze
|
5
|
+
ENCODED_FOLDER_NAME = "Gel&APY-scht".freeze
|
6
|
+
|
7
|
+
subject { described_class.new(connection, FOLDER_NAME) }
|
5
8
|
|
6
9
|
let(:imap) do
|
7
10
|
instance_double(
|
@@ -16,7 +19,7 @@ describe Imap::Backup::Account::Folder do
|
|
16
19
|
instance_double(Imap::Backup::Account::Connection, imap: imap)
|
17
20
|
end
|
18
21
|
let(:missing_mailbox_data) do
|
19
|
-
OpenStruct.new(text: "Unknown Mailbox:
|
22
|
+
OpenStruct.new(text: "Unknown Mailbox: #{FOLDER_NAME}")
|
20
23
|
end
|
21
24
|
let(:missing_mailbox_response) { OpenStruct.new(data: missing_mailbox_data) }
|
22
25
|
let(:missing_mailbox_error) do
|
@@ -36,7 +39,8 @@ describe Imap::Backup::Account::Folder do
|
|
36
39
|
|
37
40
|
context "with missing mailboxes" do
|
38
41
|
before do
|
39
|
-
allow(imap).to receive(:examine).
|
42
|
+
allow(imap).to receive(:examine).
|
43
|
+
with(ENCODED_FOLDER_NAME).and_raise(missing_mailbox_error)
|
40
44
|
end
|
41
45
|
|
42
46
|
it "returns an empty array" do
|
@@ -50,7 +54,8 @@ describe Imap::Backup::Account::Folder do
|
|
50
54
|
end
|
51
55
|
|
52
56
|
before do
|
53
|
-
allow(imap).to receive(:examine).
|
57
|
+
allow(imap).to receive(:examine).
|
58
|
+
with(ENCODED_FOLDER_NAME).and_raise(missing_mailbox_error)
|
54
59
|
end
|
55
60
|
|
56
61
|
it "returns an empty array" do
|
@@ -82,7 +87,8 @@ describe Imap::Backup::Account::Folder do
|
|
82
87
|
|
83
88
|
context "when the mailbox doesn't exist" do
|
84
89
|
before do
|
85
|
-
allow(imap).to receive(:examine).
|
90
|
+
allow(imap).to receive(:examine).
|
91
|
+
with(ENCODED_FOLDER_NAME).and_raise(missing_mailbox_error)
|
86
92
|
end
|
87
93
|
|
88
94
|
it "is nil" do
|
@@ -97,11 +103,28 @@ describe Imap::Backup::Account::Folder do
|
|
97
103
|
expect(subject.fetch(123)).to be_nil
|
98
104
|
end
|
99
105
|
end
|
106
|
+
|
107
|
+
context "when the first fetch_uid attempts fail" do
|
108
|
+
before do
|
109
|
+
outcomes = [-> { raise EOFError }, -> { [fetch_data_item] }]
|
110
|
+
allow(imap).to receive(:uid_fetch) { outcomes.shift.call }
|
111
|
+
end
|
112
|
+
|
113
|
+
it "retries" do
|
114
|
+
subject.fetch(123)
|
115
|
+
|
116
|
+
expect(imap).to have_received(:uid_fetch).twice
|
117
|
+
end
|
118
|
+
|
119
|
+
it "succeeds" do
|
120
|
+
subject.fetch(123)
|
121
|
+
end
|
122
|
+
end
|
100
123
|
end
|
101
124
|
|
102
125
|
describe "#folder" do
|
103
126
|
it "is the name" do
|
104
|
-
expect(subject.folder).to eq(
|
127
|
+
expect(subject.folder).to eq(FOLDER_NAME)
|
105
128
|
end
|
106
129
|
end
|
107
130
|
|
@@ -114,7 +137,8 @@ describe Imap::Backup::Account::Folder do
|
|
114
137
|
|
115
138
|
context "when the folder doesn't exist" do
|
116
139
|
before do
|
117
|
-
allow(imap).to receive(:examine).
|
140
|
+
allow(imap).to receive(:examine).
|
141
|
+
with(ENCODED_FOLDER_NAME).and_raise(missing_mailbox_error)
|
118
142
|
end
|
119
143
|
|
120
144
|
it "is false" do
|
@@ -134,14 +158,21 @@ describe Imap::Backup::Account::Folder do
|
|
134
158
|
|
135
159
|
context "when the folder doesn't exist" do
|
136
160
|
before do
|
137
|
-
allow(imap).to receive(:examine).
|
161
|
+
allow(imap).to receive(:examine).
|
162
|
+
with(ENCODED_FOLDER_NAME).and_raise(missing_mailbox_error)
|
138
163
|
end
|
139
164
|
|
140
|
-
it "
|
165
|
+
it "creates the folder" do
|
141
166
|
expect(imap).to receive(:create)
|
142
167
|
|
143
168
|
subject.create
|
144
169
|
end
|
170
|
+
|
171
|
+
it "encodes the folder name" do
|
172
|
+
expect(imap).to receive(:create).with(ENCODED_FOLDER_NAME)
|
173
|
+
|
174
|
+
subject.create
|
175
|
+
end
|
145
176
|
end
|
146
177
|
end
|
147
178
|
|
@@ -154,7 +185,8 @@ describe Imap::Backup::Account::Folder do
|
|
154
185
|
|
155
186
|
context "when the folder doesn't exist" do
|
156
187
|
before do
|
157
|
-
allow(imap).to receive(:examine).
|
188
|
+
allow(imap).to receive(:examine).
|
189
|
+
with(ENCODED_FOLDER_NAME).and_raise(missing_mailbox_error)
|
158
190
|
end
|
159
191
|
|
160
192
|
it "raises an error" do
|
@@ -6,11 +6,11 @@ describe Imap::Backup::Configuration::FolderChooser do
|
|
6
6
|
|
7
7
|
let(:connection) do
|
8
8
|
instance_double(
|
9
|
-
Imap::Backup::Account::Connection, folders:
|
9
|
+
Imap::Backup::Account::Connection, folders: connection_folders
|
10
10
|
)
|
11
11
|
end
|
12
12
|
let(:account) { {folders: []} }
|
13
|
-
let(:
|
13
|
+
let(:connection_folders) { [] }
|
14
14
|
let!(:highline_streams) { prepare_highline }
|
15
15
|
let(:input) { highline_streams[0] }
|
16
16
|
let(:output) { highline_streams[1] }
|
@@ -37,15 +37,9 @@ describe Imap::Backup::Configuration::FolderChooser do
|
|
37
37
|
|
38
38
|
describe "folder listing" do
|
39
39
|
let(:account) { {folders: [{name: "my_folder"}]} }
|
40
|
-
let(:
|
41
|
-
#
|
42
|
-
|
43
|
-
Imap::Backup::Account::Folder, name: "my_folder"
|
44
|
-
)
|
45
|
-
folder2 = instance_double(
|
46
|
-
Imap::Backup::Account::Folder, name: "another_folder"
|
47
|
-
)
|
48
|
-
[folder1, folder2]
|
40
|
+
let(:connection_folders) do
|
41
|
+
# N.B. my_folder is already backed up
|
42
|
+
%w(my_folder another_folder)
|
49
43
|
end
|
50
44
|
|
51
45
|
describe "display" do
|
@@ -93,7 +87,7 @@ describe Imap::Backup::Configuration::FolderChooser do
|
|
93
87
|
let(:account) do
|
94
88
|
{folders: [{name: "on_server"}, {name: "not_on_server"}]}
|
95
89
|
end
|
96
|
-
let(:
|
90
|
+
let(:connection_folders) do
|
97
91
|
[
|
98
92
|
instance_double(Imap::Backup::Account::Folder, name: "on_server")
|
99
93
|
]
|
@@ -112,7 +106,7 @@ describe Imap::Backup::Configuration::FolderChooser do
|
|
112
106
|
end
|
113
107
|
|
114
108
|
context "when folders are not available" do
|
115
|
-
let(:
|
109
|
+
let(:connection_folders) { nil }
|
116
110
|
|
117
111
|
before do
|
118
112
|
allow(Imap::Backup::Configuration::Setup.highline).
|
@@ -1,6 +1,9 @@
|
|
1
1
|
describe Imap::Backup::Configuration::GmailOauth2 do
|
2
2
|
include HighLineTestHelpers
|
3
3
|
|
4
|
+
CLIENT_ID = "my_client_id".freeze
|
5
|
+
CLIENT_SECRET = "my_client_secret".freeze
|
6
|
+
|
4
7
|
subject { described_class.new(account) }
|
5
8
|
|
6
9
|
let(:authorization_url) { "some long authorization_url" }
|
@@ -11,6 +14,7 @@ describe Imap::Backup::Configuration::GmailOauth2 do
|
|
11
14
|
let(:input) { highline_streams[0] }
|
12
15
|
let(:output) { highline_streams[1] }
|
13
16
|
let(:account) { {} }
|
17
|
+
let(:user_input) { %W(my_client_id\n my_secret\n my_code\n) }
|
14
18
|
|
15
19
|
let(:authorizer) do
|
16
20
|
instance_double(
|
@@ -25,23 +29,31 @@ describe Imap::Backup::Configuration::GmailOauth2 do
|
|
25
29
|
load: json_token
|
26
30
|
)
|
27
31
|
end
|
32
|
+
let(:token) do
|
33
|
+
instance_double(
|
34
|
+
Gmail::Authenticator::ImapBackupToken,
|
35
|
+
valid?: valid,
|
36
|
+
client_id: CLIENT_ID,
|
37
|
+
client_secret: CLIENT_SECRET
|
38
|
+
)
|
39
|
+
end
|
40
|
+
let(:valid) { false }
|
28
41
|
|
29
42
|
before do
|
30
43
|
allow(Google::Auth::UserAuthorizer).
|
31
44
|
to receive(:new) { authorizer }
|
32
45
|
allow(Google::Auth::Stores::InMemoryTokenStore).
|
33
46
|
to receive(:new) { token_store }
|
47
|
+
allow(Gmail::Authenticator::ImapBackupToken).
|
48
|
+
to receive(:new) { token }
|
34
49
|
|
35
50
|
allow(highline).to receive(:ask).and_call_original
|
51
|
+
allow(highline).to receive(:agree).and_call_original
|
36
52
|
|
37
53
|
allow(Kernel).to receive(:system)
|
38
54
|
allow(Kernel).to receive(:puts)
|
39
55
|
|
40
|
-
allow(input).to receive(:gets).and_return(
|
41
|
-
"my_client_id\n",
|
42
|
-
"my_secret\n",
|
43
|
-
"my_code\n"
|
44
|
-
)
|
56
|
+
allow(input).to receive(:gets).and_return(*user_input)
|
45
57
|
end
|
46
58
|
|
47
59
|
describe "#run" do
|
@@ -80,5 +92,30 @@ describe Imap::Backup::Configuration::GmailOauth2 do
|
|
80
92
|
it "includes the client_secret in the credentials" do
|
81
93
|
expect(result).to match('"client_secret":"my_secret"')
|
82
94
|
end
|
95
|
+
|
96
|
+
context "when the account already has client info" do
|
97
|
+
let(:valid) { true }
|
98
|
+
let(:user_input) { %W(yes\n) }
|
99
|
+
|
100
|
+
it "requests confirmation of client info" do
|
101
|
+
expect(highline).to have_received(:agree).with("Use existing client info?")
|
102
|
+
end
|
103
|
+
|
104
|
+
context "when yhe user says 'no'" do
|
105
|
+
let(:user_input) { %W(no\n) }
|
106
|
+
|
107
|
+
it "requests client_id" do
|
108
|
+
expect(highline).to have_received(:ask).with("client_id: ")
|
109
|
+
end
|
110
|
+
|
111
|
+
it "requests client_secret" do
|
112
|
+
expect(highline).to have_received(:ask).with("client_secret: ")
|
113
|
+
end
|
114
|
+
|
115
|
+
it "requests the success code" do
|
116
|
+
expect(highline).to have_received(:ask).with("success code: ")
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
83
120
|
end
|
84
121
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: imap-backup
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 3.
|
4
|
+
version: 3.3.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Joe Yates
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-02-
|
11
|
+
date: 2021-02-21 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: gmail_xoauth
|
@@ -122,6 +122,20 @@ dependencies:
|
|
122
122
|
- - ">="
|
123
123
|
- !ruby/object:Gem::Version
|
124
124
|
version: 3.0.0
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: rspec_junit_formatter
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - ">="
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '0'
|
132
|
+
type: :development
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - ">="
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '0'
|
125
139
|
- !ruby/object:Gem::Dependency
|
126
140
|
name: rubocop-rspec
|
127
141
|
requirement: !ruby/object:Gem::Requirement
|
@@ -158,12 +172,12 @@ executables:
|
|
158
172
|
extensions: []
|
159
173
|
extra_rdoc_files: []
|
160
174
|
files:
|
175
|
+
- ".circleci/config.yml"
|
161
176
|
- ".gitignore"
|
162
177
|
- ".rspec"
|
163
178
|
- ".rspec-all"
|
164
179
|
- ".rubocop.yml"
|
165
180
|
- ".rubocop_todo.yml"
|
166
|
-
- ".travis.yml"
|
167
181
|
- Gemfile
|
168
182
|
- LICENSE
|
169
183
|
- README.md
|
@@ -221,6 +235,7 @@ files:
|
|
221
235
|
- lib/imap/backup/uploader.rb
|
222
236
|
- lib/imap/backup/utils.rb
|
223
237
|
- lib/imap/backup/version.rb
|
238
|
+
- lib/retry_on_error.rb
|
224
239
|
- spec/features/backup_spec.rb
|
225
240
|
- spec/features/helper.rb
|
226
241
|
- spec/features/restore_spec.rb
|
@@ -275,7 +290,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
275
290
|
- !ruby/object:Gem::Version
|
276
291
|
version: '0'
|
277
292
|
requirements: []
|
278
|
-
rubygems_version: 3.
|
293
|
+
rubygems_version: 3.0.3
|
279
294
|
signing_key:
|
280
295
|
specification_version: 4
|
281
296
|
summary: Backup GMail (or other IMAP) accounts to disk
|
data/.travis.yml
DELETED
@@ -1,25 +0,0 @@
|
|
1
|
-
language: ruby
|
2
|
-
|
3
|
-
services:
|
4
|
-
- docker
|
5
|
-
|
6
|
-
rvm:
|
7
|
-
- 2.4
|
8
|
-
- 2.5
|
9
|
-
- 2.6
|
10
|
-
- 2.7
|
11
|
-
- jruby-19mode
|
12
|
-
|
13
|
-
branches:
|
14
|
-
only:
|
15
|
-
- master
|
16
|
-
|
17
|
-
before_install:
|
18
|
-
- gem update --system
|
19
|
-
- gem update bundler
|
20
|
-
|
21
|
-
script:
|
22
|
-
- docker pull antespi/docker-imap-devel:latest
|
23
|
-
- docker run -d --env MAIL_ADDRESS=address@example.org --env MAIL_PASS=pass --env MAILNAME=example.org --publish 8993:993 antespi/docker-imap-devel:latest
|
24
|
-
- sleep 10
|
25
|
-
- bundle exec rake
|