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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1e4037b31fc56dcf88f312506ff2814b1c3fb516ae13cf9dbb4c8a772928b5fa
4
- data.tar.gz: 0465b9bf0b7a6a9dadc5c6c35b1a4dd65db058ab1ed37158d9fedf2dc093f777
3
+ metadata.gz: c3446b53604f5895cb09b109cbe663bc4ac131daeb63989b09ae8985c480464b
4
+ data.tar.gz: 8c3b6a7176dbda814f208505c49ed63f27d650f683dad7941734e8dbd4018601
5
5
  SHA512:
6
- metadata.gz: 459620b79228707291d0b3be5037830e7100da42ecc920a1b5e19797fd1cc9aa9bc8fa667f5a5a7f89cc5d2647f47df5ae7e384a575c62a9bf4a13c25c70dd4b
7
- data.tar.gz: 8ac9d67819df8f957ffbc6a93be59aa401bdec704d75fa637c186012d2f15e23c3efa5c68ca8dc8918c97de3d90623ee5627d3cb8107615ee108e76588afd054
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://secure.travis-ci.org/joeyates/imap-backup.svg)][Continuous Integration]
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]: http://travis-ci.org/joeyates/imap-backup "Build status by Travis-CI"
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
@@ -82,7 +82,7 @@ when "folders"
82
82
  warn "Unable to list account folders"
83
83
  exit 1
84
84
  end
85
- folders.each { |f| puts "\t#{f.name}" }
85
+ folders.each { |f| puts "\t#{f}" }
86
86
  end
87
87
  when "restore"
88
88
  configuration.each_connection(&:restore)
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
- @backup_folders = options[:folders]
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
- @folders = imap.list(root, "*")
33
- if @folders.nil?
34
- Imap::Backup.logger.warn(
35
- "Unable to get folder list for account #{username}"
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
- @folders
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 |folder|
44
- f = Account::Folder.new(self, folder[:name])
45
- s = Serializer::Mbox.new(local_path, folder[:name])
46
- {name: folder[:name], local: s.uids, remote: f.uids}
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
- Downloader.new(folder, serializer).run
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
- return @imap unless @imap.nil?
80
-
81
- options = provider_options
82
- Imap::Backup.logger.debug(
83
- "Creating IMAP instance: #{server}, options: #{options.inspect}"
84
- )
85
- @imap = Net::IMAP.new(server, options)
86
- if gmail? && Gmail::Authenticator.refresh_token?(password)
87
- authenticator = Gmail::Authenticator.new(email: username, token: password)
88
- credentials = authenticator.credentials
89
- raise InvalidGmailOauth2RefreshToken if !credentials
90
-
91
- Imap::Backup.logger.debug "Logging in with OAuth2 token: #{username}"
92
- @imap.authenticate("XOAUTH2", username, credentials.access_token)
93
- else
94
- Imap::Backup.logger.debug "Logging in: #{username}/#{masked_password}"
95
- @imap.login(username, password)
96
- end
97
- Imap::Backup.logger.debug "Login complete"
98
- @imap
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 |folder_info|
118
- folder = Account::Folder.new(self, folder_info[:name])
119
- serializer = Serializer::Mbox.new(local_path, folder_info[:name])
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(name)
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 = imap.uid_fetch([uid.to_i], REQUESTED_ATTRIBUTES)
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(name, body, nil, date)
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(name)
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 folders.nil?
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
- folders.each do |folder|
48
- name = folder.name
49
- mark = selected?(name) ? "+" : "-"
50
- menu.choice("#{mark} #{name}") do
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
- backup_folders = account[:folders]
58
- return false if backup_folders.nil?
56
+ config_folders = account[:folders]
57
+ return false if config_folders.nil?
59
58
 
60
- backup_folders.find { |f| f[:name] == folder_name }
59
+ config_folders.find { |f| f[:name] == folder_name }
61
60
  end
62
61
 
63
62
  def remove_missing
64
63
  removed = []
65
- backup_folders = []
64
+ config_folders = []
66
65
  account[:folders].each do |f|
67
- found = folders.find { |folder| folder.name == f[:name] }
66
+ found = imap_folders.find { |folder| folder == f[:name] }
68
67
  if found
69
- backup_folders << f
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] = backup_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 folders
105
- @folders ||= connection.folders
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
- @client_id = highline.ask("client_id: ")
30
- @client_secret = highline.ask("client_secret: ")
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
- token = JSON.parse(token_store.load(email))
50
- token["client_secret"] = client_secret
51
- token.to_json
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
- return @data if @data
55
-
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] = false unless @data.include?(:debug)
64
- @data[:debug] = false unless [true, false].include?(@data[:debug])
65
- @data
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 unless i == index
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 unless imap_exist?
172
+ return false if !imap_exist?
173
173
 
174
174
  content = File.read(imap_pathname)
175
175
  content.start_with?("{")
@@ -2,8 +2,8 @@ module Imap; end
2
2
 
3
3
  module Imap::Backup
4
4
  MAJOR = 3
5
- MINOR = 0
6
- REVISION = 0
5
+ MINOR = 3
6
+ REVISION = 1
7
7
  PRE = nil
8
8
  VERSION = [MAJOR, MINOR, REVISION, PRE].compact.map(&:to_s).join(".")
9
9
  end
@@ -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
@@ -2,6 +2,6 @@
2
2
  :username: 'address@example.org'
3
3
  :password: 'pass'
4
4
  :connection_options:
5
- :port: 8993
5
+ :port: <%= ENV.fetch("DOCKER_IMAP_SERVER", 993) %>
6
6
  :ssl:
7
7
  :verify_mode: 0
@@ -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
- fixture = File.read(fixture_path)
5
- YAML.safe_load(fixture, [Symbol])
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
@@ -85,7 +85,7 @@ describe Gmail::Authenticator do
85
85
 
86
86
  it "is expected" do
87
87
  expect { subject }.to raise_error(
88
- ArgumentError, /missing keyword: :#{param}/
88
+ ArgumentError, /missing keyword: :?#{param}/
89
89
  )
90
90
  end
91
91
  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: backup_folders,
26
+ folders: config_folders,
27
27
  server: server
28
28
  }
29
29
  end
30
- let(:backup_folders) { [FOLDER_CONFIG] }
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!(:result) { subject.imap }
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
- include_examples "connects to IMAP"
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(imap_folders)
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 backup_folders" do
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 backup_folders" do
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 backup_folders is nil" do
232
- let(:backup_folders) { nil }
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 backup_folders is an empty list" do
242
- let(:backup_folders) { [] }
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(:backup_folders) { nil }
280
+ let(:config_folders) { nil }
253
281
  let(:imap_folders) { nil }
254
282
 
255
- it "does not fail" do
256
- expect { subject.run_backup }.to_not raise_error
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
- subject { described_class.new(connection, "my_folder") }
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: my_folder")
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).and_raise(missing_mailbox_error)
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).and_raise(no_method_error)
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).and_raise(missing_mailbox_error)
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("my_folder")
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).and_raise(missing_mailbox_error)
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).and_raise(missing_mailbox_error)
161
+ allow(imap).to receive(:examine).
162
+ with(ENCODED_FOLDER_NAME).and_raise(missing_mailbox_error)
138
163
  end
139
164
 
140
- it "is does not create the folder" do
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).and_raise(missing_mailbox_error)
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: remote_folders
9
+ Imap::Backup::Account::Connection, folders: connection_folders
10
10
  )
11
11
  end
12
12
  let(:account) { {folders: []} }
13
- let(:remote_folders) { [] }
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(:remote_folders) do
41
- # this one is already backed up:
42
- folder1 = instance_double(
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(:remote_folders) do
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(:remote_folders) { nil }
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.0.0
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-07 00:00:00.000000000 Z
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.1.2
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