imap-backup 4.0.3 → 4.0.7

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.
Files changed (32) hide show
  1. checksums.yaml +4 -4
  2. data/lib/email/provider/apple_mail.rb +2 -2
  3. data/lib/email/provider/base.rb +8 -0
  4. data/lib/email/provider/fastmail.rb +2 -2
  5. data/lib/email/provider/gmail.rb +2 -2
  6. data/lib/email/provider/{default.rb → unknown.rb} +2 -3
  7. data/lib/email/provider.rb +2 -2
  8. data/lib/imap/backup/account/connection.rb +23 -31
  9. data/lib/imap/backup/account/folder.rb +1 -1
  10. data/lib/imap/backup/account.rb +98 -0
  11. data/lib/imap/backup/cli/helpers.rb +1 -1
  12. data/lib/imap/backup/cli/local.rb +1 -1
  13. data/lib/imap/backup/configuration/account.rb +38 -30
  14. data/lib/imap/backup/configuration/folder_chooser.rb +7 -9
  15. data/lib/imap/backup/configuration/list.rb +1 -1
  16. data/lib/imap/backup/configuration/setup.rb +10 -8
  17. data/lib/imap/backup/configuration/store.rb +33 -11
  18. data/lib/imap/backup/version.rb +1 -1
  19. data/lib/imap/backup.rb +1 -0
  20. data/spec/features/support/shared/connection_context.rb +7 -5
  21. data/spec/unit/email/provider/{default_spec.rb → base_spec.rb} +1 -7
  22. data/spec/unit/email/provider_spec.rb +2 -2
  23. data/spec/unit/imap/backup/account/connection_spec.rb +8 -17
  24. data/spec/unit/imap/backup/cli/local_spec.rb +9 -2
  25. data/spec/unit/imap/backup/cli/utils_spec.rb +44 -42
  26. data/spec/unit/imap/backup/configuration/account_spec.rb +47 -46
  27. data/spec/unit/imap/backup/configuration/folder_chooser_spec.rb +17 -18
  28. data/spec/unit/imap/backup/configuration/list_spec.rb +12 -5
  29. data/spec/unit/imap/backup/configuration/setup_spec.rb +33 -12
  30. data/spec/unit/imap/backup/configuration/store_spec.rb +21 -22
  31. metadata +32 -32
  32. data/spec/support/shared_examples/account_flagging.rb +0 -23
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 139120639e68abb1555421c5c63f28a6ede222f95767f86a52e3b8d548ceaf99
4
- data.tar.gz: ef9ef718e09d4b812085c3e72f374f922ae760d318322fa2237d742aec826e37
3
+ metadata.gz: 5819f0b50ce3de10cfedb1cdb5381e51f2d94f00dc2bcdd99234f57855c9959d
4
+ data.tar.gz: a140267c179d6002a7186f8591743abbfea2dca7697b7f1761feaa359fd1521c
5
5
  SHA512:
6
- metadata.gz: 3d7447ca37c7f2134ea7254fe90640ce3f6db6599a287b5b07796236d614e887cd05614045a57d8fb2b729d9e2da357f4cb42b083303dbfe1c3dc4b92b3b3dbd
7
- data.tar.gz: 77ea7b8ba366c8416a6cef3c707b8942d4f3261f138c603b98128d2f43079880e12f241c8672a2f301c035204ccd68bc8a6bbbc6dce8d5d9590ecbd4c490844c
6
+ metadata.gz: 1b7d49f159df4d29582cf98f01ce105769301bcf94fd019486f1e0b43321eb90594e16861e24fe9d7993a76eeba0c115d6d991b64c1188020b09d04ea8db0714
7
+ data.tar.gz: 7dacdd3c5c3835e1925cd36a3beaaf7cdf6bf98d4bf7f02bbdd4abe3635512708c03654c3da9dfbfcf35f4e94a46dd43a3fc9e6e4693dd2b943d34d4a9ec8b02
@@ -1,6 +1,6 @@
1
- require "email/provider/default"
1
+ require "email/provider/base"
2
2
 
3
- class Email::Provider::AppleMail < Email::Provider::Default
3
+ class Email::Provider::AppleMail < Email::Provider::Base
4
4
  def host
5
5
  "imap.mail.me.com"
6
6
  end
@@ -0,0 +1,8 @@
1
+ module Email; end
2
+ class Email::Provider; end
3
+
4
+ class Email::Provider::Base
5
+ def options
6
+ {port: 993, ssl: {ssl_version: :TLSv1_2}}
7
+ end
8
+ end
@@ -1,6 +1,6 @@
1
- require "email/provider/default"
1
+ require "email/provider/base"
2
2
 
3
- class Email::Provider::Fastmail < Email::Provider::Default
3
+ class Email::Provider::Fastmail < Email::Provider::Base
4
4
  def host
5
5
  "imap.fastmail.com"
6
6
  end
@@ -1,6 +1,6 @@
1
- require "email/provider/default"
1
+ require "email/provider/base"
2
2
 
3
- class Email::Provider::GMail < Email::Provider::Default
3
+ class Email::Provider::GMail < Email::Provider::Base
4
4
  def host
5
5
  "imap.gmail.com"
6
6
  end
@@ -1,7 +1,6 @@
1
- module Email; end
2
- class Email::Provider; end
1
+ require "email/provider/base"
3
2
 
4
- class Email::Provider::Default
3
+ class Email::Provider::Unknown < Email::Provider::Base
5
4
  # We don't know how to guess the IMAP server
6
5
  def host
7
6
  end
@@ -1,7 +1,7 @@
1
- require "email/provider/default"
2
1
  require "email/provider/apple_mail"
3
2
  require "email/provider/fastmail"
4
3
  require "email/provider/gmail"
4
+ require "email/provider/unknown"
5
5
 
6
6
  module Email; end
7
7
 
@@ -21,7 +21,7 @@ class Email::Provider
21
21
  when address.end_with?("@me.com")
22
22
  Email::Provider::AppleMail.new
23
23
  else
24
- Email::Provider::Default.new
24
+ Email::Provider::Unknown.new
25
25
  end
26
26
  end
27
27
  end
@@ -4,25 +4,17 @@ require "imap/backup/client/default"
4
4
  require "retry_on_error"
5
5
 
6
6
  module Imap::Backup
7
- module Account; end
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 :connection_options
15
- attr_reader :local_path
16
- attr_reader :password
17
- attr_reader :username
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] || {}
14
+ attr_reader :account
15
+
16
+ def initialize(account)
17
+ @account = account
26
18
  @folders = nil
27
19
  create_account_folder
28
20
  end
@@ -33,7 +25,7 @@ module Imap::Backup
33
25
  folders = client.list
34
26
 
35
27
  if folders.empty?
36
- message = "Unable to get folder list for account #{username}"
28
+ message = "Unable to get folder list for account #{account.username}"
37
29
  Imap::Backup.logger.info message
38
30
  raise message
39
31
  end
@@ -45,13 +37,13 @@ module Imap::Backup
45
37
  def status
46
38
  backup_folders.map do |backup_folder|
47
39
  f = Account::Folder.new(self, backup_folder[:name])
48
- s = Serializer::Mbox.new(local_path, backup_folder[:name])
40
+ s = Serializer::Mbox.new(account.local_path, backup_folder[:name])
49
41
  {name: backup_folder[:name], local: s.uids, remote: f.uids}
50
42
  end
51
43
  end
52
44
 
53
45
  def run_backup
54
- Imap::Backup.logger.debug "Running backup of account: #{username}"
46
+ Imap::Backup.logger.debug "Running backup of account: #{account.username}"
55
47
  # start the connection so we get logging messages in the right order
56
48
  client
57
49
  each_folder do |folder, serializer|
@@ -71,11 +63,11 @@ module Imap::Backup
71
63
  def local_folders
72
64
  return enum_for(:local_folders) if !block_given?
73
65
 
74
- glob = File.join(local_path, "**", "*.imap")
75
- base = Pathname.new(local_path)
66
+ glob = File.join(account.local_path, "**", "*.imap")
67
+ base = Pathname.new(account.local_path)
76
68
  Pathname.glob(glob) do |path|
77
69
  name = path.relative_path_from(base).to_s[0..-6]
78
- serializer = Serializer::Mbox.new(local_path, name)
70
+ serializer = Serializer::Mbox.new(account.local_path, name)
79
71
  folder = Account::Folder.new(self, name)
80
72
  yield serializer, folder
81
73
  end
@@ -109,15 +101,15 @@ module Imap::Backup
109
101
  else
110
102
  Client::Default.new(server, options)
111
103
  end
112
- Imap::Backup.logger.debug "Logging in: #{username}/#{masked_password}"
113
- client.login(username, password)
104
+ Imap::Backup.logger.debug "Logging in: #{account.username}/#{masked_password}"
105
+ client.login(account.username, account.password)
114
106
  Imap::Backup.logger.debug "Login complete"
115
107
  client
116
108
  end
117
109
  end
118
110
 
119
111
  def server
120
- @server ||= provider.host
112
+ @server ||= account.server || provider.host
121
113
  end
122
114
 
123
115
  private
@@ -125,7 +117,7 @@ module Imap::Backup
125
117
  def each_folder
126
118
  backup_folders.each do |backup_folder|
127
119
  folder = Account::Folder.new(self, backup_folder[:name])
128
- serializer = Serializer::Mbox.new(local_path, backup_folder[:name])
120
+ serializer = Serializer::Mbox.new(account.local_path, backup_folder[:name])
129
121
  yield folder, serializer
130
122
  end
131
123
  end
@@ -142,7 +134,7 @@ module Imap::Backup
142
134
  Imap::Backup.logger.debug(
143
135
  "Backup '#{old_name}' renamed and restored to '#{new_name}'"
144
136
  )
145
- new_serializer = Serializer::Mbox.new(local_path, new_name)
137
+ new_serializer = Serializer::Mbox.new(account.local_path, new_name)
146
138
  new_folder = Account::Folder.new(self, new_name)
147
139
  new_folder.create
148
140
  new_serializer.force_uid_validity(new_folder.uid_validity)
@@ -159,21 +151,21 @@ module Imap::Backup
159
151
 
160
152
  def create_account_folder
161
153
  Utils.make_folder(
162
- File.dirname(local_path),
163
- File.basename(local_path),
154
+ File.dirname(account.local_path),
155
+ File.basename(account.local_path),
164
156
  Serializer::DIRECTORY_PERMISSIONS
165
157
  )
166
158
  end
167
159
 
168
160
  def masked_password
169
- password.gsub(/./, "x")
161
+ account.password.gsub(/./, "x")
170
162
  end
171
163
 
172
164
  def backup_folders
173
165
  @backup_folders ||=
174
166
  begin
175
- if @config_folders&.any?
176
- @config_folders
167
+ if account.folders&.any?
168
+ account.folders
177
169
  else
178
170
  folders.map { |name| {name: name} }
179
171
  end
@@ -181,11 +173,11 @@ module Imap::Backup
181
173
  end
182
174
 
183
175
  def provider
184
- @provider ||= Email::Provider.for_address(username)
176
+ @provider ||= Email::Provider.for_address(account.username)
185
177
  end
186
178
 
187
179
  def provider_options
188
- provider.options.merge(connection_options)
180
+ provider.options.merge(account.connection_options || {})
189
181
  end
190
182
  end
191
183
  end
@@ -3,7 +3,7 @@ require "forwardable"
3
3
  require "retry_on_error"
4
4
 
5
5
  module Imap::Backup
6
- module Account; end
6
+ class Account; end
7
7
 
8
8
  class FolderNotFound < StandardError; end
9
9
 
@@ -0,0 +1,98 @@
1
+ module Imap::Backup
2
+ class Account
3
+ attr_reader :username
4
+ attr_reader :password
5
+ attr_reader :local_path
6
+ attr_reader :folders
7
+ attr_reader :server
8
+ attr_reader :connection_options
9
+ attr_reader :changes
10
+ attr_reader :marked_for_deletion
11
+
12
+ def initialize(options)
13
+ @username = options[:username]
14
+ @password = options[:password]
15
+ @local_path = options[:local_path]
16
+ @folders = options[:folders]
17
+ @server = options[:server]
18
+ @connection_options = options[:connection_options]
19
+ @changes = {}
20
+ @marked_for_deletion = false
21
+ end
22
+
23
+ def valid?
24
+ username && password
25
+ end
26
+
27
+ def modified?
28
+ changes.any?
29
+ end
30
+
31
+ def clear_changes!
32
+ @changes = {}
33
+ end
34
+
35
+ def mark_for_deletion!
36
+ @marked_for_deletion = true
37
+ end
38
+
39
+ def marked_for_deletion?
40
+ @marked_for_deletion
41
+ end
42
+
43
+ def to_h
44
+ h = {
45
+ username: @username,
46
+ password: @password,
47
+ }
48
+ h[:local_path] = @local_path if @local_path
49
+ h[:folders] = @folders if @folders
50
+ h[:server] = @server if @server
51
+ h[:connection_options] = @connection_options if @connection_options
52
+ h
53
+ end
54
+
55
+ def username=(value)
56
+ update(:username, value)
57
+ end
58
+
59
+ def password=(value)
60
+ update(:password, value)
61
+ end
62
+
63
+ def local_path=(value)
64
+ update(:local_path, value)
65
+ end
66
+
67
+ def folders=(value)
68
+ raise "folders must be an Array" if !value.is_a?(Array)
69
+ update(:folders, value)
70
+ end
71
+
72
+ def server=(value)
73
+ update(:server, value)
74
+ end
75
+
76
+ def connection_options=(value)
77
+ parsed = JSON.parse(value)
78
+ update(:connection_options, parsed)
79
+ end
80
+
81
+ private
82
+
83
+ def update(field, value)
84
+ if changes[field]
85
+ change = changes[field]
86
+ changes.delete(field) if change[:from] == value
87
+ end
88
+ set_field!(field, value)
89
+ end
90
+
91
+ def set_field!(field, value)
92
+ key = :"@#{field}"
93
+ current = instance_variable_get(key)
94
+ changes[field] = {from: current, to: value}
95
+ instance_variable_set(key, value)
96
+ end
97
+ end
98
+ end
@@ -7,7 +7,7 @@ module Imap::Backup::CLI::Helpers
7
7
 
8
8
  def account(email)
9
9
  connections = Imap::Backup::Configuration::List.new
10
- account = connections.accounts.find { |a| a[:username] == email }
10
+ account = connections.accounts.find { |a| a.username == email }
11
11
  raise "#{email} is not a configured account" if !account
12
12
 
13
13
  account
@@ -6,7 +6,7 @@ module Imap::Backup
6
6
  desc "accounts", "List locally backed-up accounts"
7
7
  def accounts
8
8
  connections = Imap::Backup::Configuration::List.new
9
- connections.accounts.each { |a| Kernel.puts a[:username] }
9
+ connections.accounts.each { |a| Kernel.puts a.username }
10
10
  end
11
11
 
12
12
  desc "folders EMAIL", "List account folders"
@@ -22,9 +22,10 @@ module Imap::Backup
22
22
  header menu
23
23
  modify_email menu
24
24
  modify_password menu
25
- modify_server menu
26
25
  modify_backup_path menu
27
26
  choose_folders menu
27
+ modify_server menu
28
+ modify_connection_options menu
28
29
  test_connection menu
29
30
  delete_account menu
30
31
  menu.choice("return to main menu") { throw :done }
@@ -33,13 +34,20 @@ module Imap::Backup
33
34
  end
34
35
 
35
36
  def header(menu)
36
- menu.header = <<-HEADER.gsub(/^\s{8}/m, "")
37
+ connection_options =
38
+ if account.connection_options
39
+ escaped =
40
+ JSON.generate(account.connection_options).
41
+ gsub('"', '\"')
42
+ "\n connection options: #{escaped}"
43
+ end
44
+ menu.header = <<~HEADER
37
45
  Account:
38
- email: #{account[:username]}
39
- server: #{account[:server]}
40
- path: #{account[:local_path]}
41
- folders: #{folders.map { |f| f[:name] }.join(', ')}
46
+ email: #{account.username}
42
47
  password: #{masked_password}
48
+ path: #{account.local_path}
49
+ folders: #{folders.map { |f| f[:name] }.join(', ')}
50
+ server: #{account.server}#{connection_options}
43
51
  HEADER
44
52
  end
45
53
 
@@ -48,20 +56,20 @@ module Imap::Backup
48
56
  username = Configuration::Asker.email(username)
49
57
  Kernel.puts "username: #{username}"
50
58
  other_accounts = store.accounts.reject { |a| a == account }
51
- others = other_accounts.map { |a| a[:username] }
59
+ others = other_accounts.map { |a| a.username }
52
60
  Kernel.puts "others: #{others.inspect}"
53
61
  if others.include?(username)
54
62
  Kernel.puts(
55
63
  "There is already an account set up with that email address"
56
64
  )
57
65
  else
58
- account[:username] = username
66
+ account.username = username
59
67
  # rubocop:disable Style/IfUnlessModifier
60
- if account[:server].nil? || (account[:server] == "")
61
- account[:server] = default_server(username)
68
+ default = default_server(username)
69
+ if default && (account.server.nil? || (account.server == ""))
70
+ account.server = default
62
71
  end
63
72
  # rubocop:enable Style/IfUnlessModifier
64
- account[:modified] = true
65
73
  end
66
74
  end
67
75
  end
@@ -70,30 +78,31 @@ module Imap::Backup
70
78
  menu.choice("modify password") do
71
79
  password = Configuration::Asker.password
72
80
 
73
- if !password.nil?
74
- account[:password] = password
75
- account[:modified] = true
76
- end
81
+ account.password = password if !password.nil?
77
82
  end
78
83
  end
79
84
 
80
85
  def modify_server(menu)
81
86
  menu.choice("modify server") do
82
87
  server = highline.ask("server: ")
83
- if !server.nil?
84
- account[:server] = server
85
- account[:modified] = true
86
- end
88
+ account.server = server if !server.nil?
89
+ end
90
+ end
91
+
92
+ def modify_connection_options(menu)
93
+ menu.choice("modify connection options") do
94
+ connection_options = highline.ask("connections options (as JSON): ")
95
+ account.connection_options = connection_options if !connection_options.nil?
87
96
  end
88
97
  end
89
98
 
90
99
  def path_modification_validator(path)
91
100
  same = store.accounts.find do |a|
92
- a[:username] != account[:username] && a[:local_path] == path
101
+ a.username != account.username && a.local_path == path
93
102
  end
94
103
  if same
95
104
  Kernel.puts "The path '#{path}' is used to backup " \
96
- "the account '#{same[:username]}'"
105
+ "the account '#{same.username}'"
97
106
  false
98
107
  else
99
108
  true
@@ -102,11 +111,10 @@ module Imap::Backup
102
111
 
103
112
  def modify_backup_path(menu)
104
113
  menu.choice("modify backup path") do
105
- existing = account[:local_path].clone
106
- account[:local_path] = Configuration::Asker.backup_path(
107
- account[:local_path], ->(path) { path_modification_validator(path) }
114
+ existing = account.local_path.clone
115
+ account.local_path = Configuration::Asker.backup_path(
116
+ account.local_path, ->(path) { path_modification_validator(path) }
108
117
  )
109
- account[:modified] = true if existing != account[:local_path]
110
118
  end
111
119
  end
112
120
 
@@ -127,28 +135,28 @@ module Imap::Backup
127
135
  def delete_account(menu)
128
136
  menu.choice("delete") do
129
137
  if highline.agree("Are you sure? (y/n) ")
130
- account[:delete] = true
138
+ account.mark_for_deletion!
131
139
  throw :done
132
140
  end
133
141
  end
134
142
  end
135
143
 
136
144
  def folders
137
- account[:folders] || []
145
+ account.folders || []
138
146
  end
139
147
 
140
148
  def masked_password
141
- if (account[:password] == "") || account[:password].nil?
149
+ if (account.password == "") || account.password.nil?
142
150
  "(unset)"
143
151
  else
144
- account[:password].gsub(/./, "x")
152
+ account.password.gsub(/./, "x")
145
153
  end
146
154
  end
147
155
 
148
156
  def default_server(username)
149
157
  provider = Email::Provider.for_address(username)
150
158
 
151
- if provider.is_a?(Email::Provider::Default)
159
+ if provider.is_a?(Email::Provider::Unknown)
152
160
  Kernel.puts "Can't decide provider for email address '#{username}'"
153
161
  return nil
154
162
  end
@@ -53,7 +53,7 @@ module Imap::Backup
53
53
  end
54
54
 
55
55
  def selected?(folder_name)
56
- config_folders = account[:folders]
56
+ config_folders = account.folders
57
57
  return false if config_folders.nil?
58
58
 
59
59
  config_folders.find { |f| f[:name] == folder_name }
@@ -62,7 +62,7 @@ module Imap::Backup
62
62
  def remove_missing
63
63
  removed = []
64
64
  config_folders = []
65
- account[:folders].each do |f|
65
+ account.folders.each do |f|
66
66
  found = imap_folders.find { |folder| folder == f[:name] }
67
67
  if found
68
68
  config_folders << f
@@ -73,8 +73,7 @@ module Imap::Backup
73
73
 
74
74
  return if removed.empty?
75
75
 
76
- account[:folders] = config_folders
77
- account[:modified] = true
76
+ account.folders = config_folders
78
77
 
79
78
  Kernel.puts <<~MESSAGE
80
79
  The following folders have been removed: #{removed.join(', ')}
@@ -85,12 +84,11 @@ module Imap::Backup
85
84
 
86
85
  def toggle_selection(folder_name)
87
86
  if selected?(folder_name)
88
- changed = account[:folders].reject! { |f| f[:name] == folder_name }
89
- account[:modified] = true if changed
87
+ new_list = account.folders.select { |f| f[:name] != folder_name }
88
+ account.folders = new_list
90
89
  else
91
- account[:folders] ||= []
92
- account[:folders] << {name: folder_name}
93
- account[:modified] = true
90
+ existing = account.folders || []
91
+ account.folders = existing + [{name: folder_name}]
94
92
  end
95
93
  end
96
94
 
@@ -29,7 +29,7 @@ module Imap::Backup
29
29
  config.accounts
30
30
  else
31
31
  config.accounts.select do |account|
32
- required_accounts.include?(account[:username])
32
+ required_accounts.include?(account.username)
33
33
  end
34
34
  end
35
35
  end
@@ -1,5 +1,7 @@
1
1
  require "highline"
2
2
 
3
+ require "imap/backup/account"
4
+
3
5
  module Imap::Backup
4
6
  module Configuration; end
5
7
 
@@ -39,12 +41,12 @@ module Imap::Backup
39
41
 
40
42
  def account_items(menu)
41
43
  config.accounts.each do |account|
42
- next if account[:delete]
44
+ next if account.marked_for_deletion?
43
45
 
44
- item = account[:username].clone
45
- item << " *" if account[:modified]
46
+ item = account.username.clone
47
+ item << " *" if account.modified?
46
48
  menu.choice(item) do
47
- edit_account account[:username]
49
+ edit_account account.username
48
50
  end
49
51
  end
50
52
  end
@@ -70,19 +72,19 @@ module Imap::Backup
70
72
  end
71
73
 
72
74
  def default_account_config(username)
73
- {
75
+ ::Imap::Backup::Account.new(
74
76
  username: username,
75
77
  password: "",
76
78
  local_path: File.join(config.path, username.tr("@", "_")),
77
79
  folders: []
78
- }.tap do |c|
80
+ ).tap do |a|
79
81
  server = Email::Provider.for_address(username)
80
- c[:server] = server.host if server.host
82
+ a.server = server.host if server.host
81
83
  end
82
84
  end
83
85
 
84
86
  def edit_account(username)
85
- account = config.accounts.find { |a| a[:username] == username }
87
+ account = config.accounts.find { |a| a.username == username }
86
88
  if account.nil?
87
89
  account = default_account_config(username)
88
90
  config.accounts << account