imap-backup 4.0.3 → 4.0.7

Sign up to get free protection for your applications and to get access to all the features.
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