imap-backup 3.4.0 → 4.0.0.rc3

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 (58) hide show
  1. checksums.yaml +4 -4
  2. data/.circleci/config.yml +2 -2
  3. data/.rubocop_todo.yml +1 -1
  4. data/CHANGELOG.md +19 -0
  5. data/README.md +22 -8
  6. data/bin/imap-backup +3 -93
  7. data/imap-backup +9 -0
  8. data/imap-backup.gemspec +2 -3
  9. data/lib/email/mboxrd/message.rb +2 -2
  10. data/lib/imap/backup/account/connection.rb +16 -33
  11. data/lib/imap/backup/cli/backup.rb +21 -0
  12. data/lib/imap/backup/cli/folders.rb +27 -0
  13. data/lib/imap/backup/cli/helpers.rb +20 -0
  14. data/lib/imap/backup/cli/local.rb +70 -0
  15. data/lib/imap/backup/cli/restore.rb +19 -0
  16. data/lib/imap/backup/cli/setup.rb +13 -0
  17. data/lib/imap/backup/cli/status.rb +26 -0
  18. data/lib/imap/backup/cli.rb +89 -0
  19. data/lib/imap/backup/configuration/account.rb +1 -11
  20. data/lib/imap/backup/configuration/list.rb +13 -12
  21. data/lib/imap/backup/version.rb +3 -3
  22. data/lib/imap/backup.rb +0 -1
  23. data/spec/unit/imap/backup/account/connection_spec.rb +31 -51
  24. data/spec/unit/imap/backup/configuration/account_spec.rb +0 -43
  25. data/spec/unit/imap/backup/configuration/list_spec.rb +1 -0
  26. metadata +23 -62
  27. data/docs/01-credentials-screen.png +0 -0
  28. data/docs/02-new-project.png +0 -0
  29. data/docs/03-initial-credentials-for-project.png +0 -0
  30. data/docs/04-credential-type-selection.png +0 -0
  31. data/docs/05-cant-create-without-consent-setup.png +0 -0
  32. data/docs/06-user-type-selection.png +0 -0
  33. data/docs/07-consent-screen-form.png +0 -0
  34. data/docs/08-app-scopes.png +0 -0
  35. data/docs/09-scope-selection.png +0 -0
  36. data/docs/10-updated-app-scopes.png +0 -0
  37. data/docs/11-test-users.png +0 -0
  38. data/docs/12-add-users.png +0 -0
  39. data/docs/13-create-oauth-client.png +0 -0
  40. data/docs/14-application-details.png +0 -0
  41. data/docs/16-initial-menu.png +0 -0
  42. data/docs/17-inputting-the-email-address.png +0 -0
  43. data/docs/18-choose-password.png +0 -0
  44. data/docs/19-supply-client-info.png +0 -0
  45. data/docs/20-choose-gmail-account.png +0 -0
  46. data/docs/21-accept-warnings.png +0 -0
  47. data/docs/22-grant-access.png +0 -0
  48. data/docs/24-confirm-choices.png +0 -0
  49. data/docs/25-success-code.png +0 -0
  50. data/docs/26-type-code-into-imap-backup.png +0 -0
  51. data/docs/27-success.png +0 -0
  52. data/docs/setting-up-gmail-with-oauth2.md +0 -166
  53. data/lib/gmail/authenticator.rb +0 -160
  54. data/lib/google/auth/stores/in_memory_token_store.rb +0 -9
  55. data/lib/imap/backup/configuration/gmail_oauth2.rb +0 -102
  56. data/spec/unit/gmail/authenticator_spec.rb +0 -138
  57. data/spec/unit/google/auth/stores/in_memory_token_store_spec.rb +0 -15
  58. data/spec/unit/imap/backup/configuration/gmail_oauth2_spec.rb +0 -121
@@ -1,160 +0,0 @@
1
- require "googleauth"
2
- require "google/auth/stores/in_memory_token_store"
3
-
4
- module Gmail; end
5
-
6
- class Gmail::Authenticator
7
- class MalformedImapBackupToken < StandardError; end
8
-
9
- class ImapBackupToken
10
- attr_reader :token
11
-
12
- def self.from(
13
- access_token:,
14
- client_id:,
15
- client_secret:,
16
- expiration_time_millis:,
17
- refresh_token:
18
- )
19
- {
20
- access_token: access_token,
21
- client_id: client_id,
22
- client_secret: client_secret,
23
- expiration_time_millis: expiration_time_millis,
24
- refresh_token: refresh_token
25
- }.to_json
26
- end
27
-
28
- def initialize(token)
29
- @token = token
30
- end
31
-
32
- def valid?
33
- return false if !body
34
- return false if !access_token
35
- return false if !client_id
36
- return false if !client_secret
37
- return false if !expiration_time_millis
38
- return false if !refresh_token
39
-
40
- true
41
- end
42
-
43
- def access_token
44
- body["access_token"]
45
- end
46
-
47
- def client_id
48
- body["client_id"]
49
- end
50
-
51
- def client_secret
52
- body["client_secret"]
53
- end
54
-
55
- def expiration_time_millis
56
- body["expiration_time_millis"]
57
- end
58
-
59
- def refresh_token
60
- body["refresh_token"]
61
- end
62
-
63
- private
64
-
65
- def body
66
- @body ||= JSON.parse(token)
67
- rescue JSON::ParserError
68
- nil
69
- end
70
- end
71
-
72
- GMAIL_READ_SCOPE = "https://mail.google.com/".freeze
73
- OOB_URI = "urn:ietf:wg:oauth:2.0:oob".freeze
74
-
75
- attr_reader :email
76
- attr_reader :token
77
-
78
- def self.refresh_token?(text)
79
- ImapBackupToken.new(text).valid?
80
- end
81
-
82
- def initialize(email:, token:)
83
- @email = email
84
- @token = token
85
- end
86
-
87
- def authorization_url
88
- authorizer.get_authorization_url(base_url: OOB_URI)
89
- end
90
-
91
- def credentials
92
- authorizer.get_credentials(email).tap do |c|
93
- c.refresh! if c.expired?
94
- end
95
- end
96
-
97
- def credentials_from_code(code)
98
- authorizer.get_credentials_from_code(
99
- user_id: email,
100
- code: code,
101
- base_url: OOB_URI
102
- )
103
- end
104
-
105
- private
106
-
107
- def auth_client_id
108
- @auth_client_id = Google::Auth::ClientId.new(client_id, client_secret)
109
- end
110
-
111
- def authorizer
112
- @authorizer ||= Google::Auth::UserAuthorizer.new(
113
- auth_client_id, GMAIL_READ_SCOPE, token_store
114
- )
115
- end
116
-
117
- def access_token
118
- imap_backup_token.access_token
119
- end
120
-
121
- def client_id
122
- imap_backup_token.client_id
123
- end
124
-
125
- def client_secret
126
- imap_backup_token.client_secret
127
- end
128
-
129
- def expiration_time_millis
130
- imap_backup_token.expiration_time_millis
131
- end
132
-
133
- def refresh_token
134
- imap_backup_token.refresh_token
135
- end
136
-
137
- def imap_backup_token
138
- @imap_backup_token ||=
139
- ImapBackupToken.new(token).tap do |t|
140
- raise MalformedImapBackupToken if !t.valid?
141
- end
142
- end
143
-
144
- def store_token
145
- {
146
- "client_id" => client_id,
147
- "access_token" => access_token,
148
- "refresh_token" => refresh_token,
149
- "scope": [GMAIL_READ_SCOPE],
150
- "expiration_time_millis": expiration_time_millis
151
- }.to_json
152
- end
153
-
154
- def token_store
155
- @token_store ||=
156
- Google::Auth::Stores::InMemoryTokenStore.new.tap do |t|
157
- t.store(email, store_token)
158
- end
159
- end
160
- end
@@ -1,9 +0,0 @@
1
- module Google; end
2
- module Google::Auth; end
3
- module Google::Auth::Stores; end
4
-
5
- class Google::Auth::Stores::InMemoryTokenStore < Hash
6
- def load(id)
7
- self[id]
8
- end
9
- end
@@ -1,102 +0,0 @@
1
- module Imap::Backup
2
- module Configuration; end
3
-
4
- class Configuration::GmailOauth2
5
- BANNER = <<~BANNER.freeze
6
- GMail OAuth2 Setup
7
-
8
- You need to authorize imap_backup to get access to your email.
9
- To do so, please follow the instructions here:
10
-
11
- https://github.com/joeyates/imap-backup/blob/main/docs/setting-up-gmail-with-oauth2.md
12
-
13
- BANNER
14
-
15
- GMAIL_READ_SCOPE = "https://mail.google.com/".freeze
16
- OOB_URI = "urn:ietf:wg:oauth:2.0:oob".freeze
17
-
18
- attr_reader :account
19
- attr_reader :client_id
20
- attr_reader :client_secret
21
-
22
- def initialize(account)
23
- @account = account
24
- end
25
-
26
- def run
27
- Kernel.system("clear")
28
- Kernel.puts BANNER
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
43
-
44
- Kernel.puts <<~MESSAGE
45
-
46
- Open the following URL in your browser
47
-
48
- #{authorization_url}
49
-
50
- Then copy the success code
51
-
52
- MESSAGE
53
-
54
- @code = highline.ask("success code: ")
55
- @credentials = authorizer.get_and_store_credentials_from_code(
56
- user_id: email, code: @code, base_url: OOB_URI
57
- )
58
-
59
- raise "Failed" if !@credentials
60
-
61
- new_token = JSON.parse(token_store.load(email))
62
- new_token["client_secret"] = client_secret
63
- new_token.to_json
64
- end
65
-
66
- private
67
-
68
- def email
69
- account[:username]
70
- end
71
-
72
- def password
73
- account[:password]
74
- end
75
-
76
- def token
77
- @token ||= Gmail::Authenticator::ImapBackupToken.new(password)
78
- end
79
-
80
- def highline
81
- Configuration::Setup.highline
82
- end
83
-
84
- def auth_client_id
85
- @auth_client_id = Google::Auth::ClientId.new(client_id, client_secret)
86
- end
87
-
88
- def authorizer
89
- @authorizer ||= Google::Auth::UserAuthorizer.new(
90
- auth_client_id, GMAIL_READ_SCOPE, token_store
91
- )
92
- end
93
-
94
- def token_store
95
- @token_store ||= Google::Auth::Stores::InMemoryTokenStore.new
96
- end
97
-
98
- def authorization_url
99
- authorizer.get_authorization_url(base_url: OOB_URI)
100
- end
101
- end
102
- end
@@ -1,138 +0,0 @@
1
- require "gmail/authenticator"
2
- require "googleauth"
3
-
4
- describe Gmail::Authenticator do
5
- ACCESS_TOKEN = "access_token".freeze
6
- AUTHORIZATION_URL = "authorization_url".freeze
7
- CLIENT_ID = "client_id".freeze
8
- CLIENT_SECRET = "client_secret".freeze
9
- CODE = "code".freeze
10
- CREDENTIALS = "credentials".freeze
11
- EMAIL = "email".freeze
12
- EXPIRATION_TIME_MILLIS = "expiration_time_millis".freeze
13
- GMAIL_READ_SCOPE = "https://mail.google.com/".freeze
14
- IMAP_BACKUP_TOKEN = "imap_backup_token".freeze
15
- OOB_URI = "urn:ietf:wg:oauth:2.0:oob".freeze
16
- REFRESH_TOKEN = "refresh_token".freeze
17
-
18
- subject { described_class.new(**params) }
19
-
20
- let(:params) do
21
- {
22
- email: EMAIL,
23
- token: IMAP_BACKUP_TOKEN
24
- }
25
- end
26
-
27
- let(:authorizer) do
28
- instance_double(Google::Auth::UserAuthorizer)
29
- end
30
-
31
- let(:imap_backup_token) do
32
- instance_double(
33
- Gmail::Authenticator::ImapBackupToken,
34
- access_token: ACCESS_TOKEN,
35
- client_id: CLIENT_ID,
36
- client_secret: CLIENT_SECRET,
37
- expiration_time_millis: EXPIRATION_TIME_MILLIS,
38
- refresh_token: REFRESH_TOKEN,
39
- valid?: true
40
- )
41
- end
42
-
43
- let(:token_store) do
44
- instance_double(Google::Auth::Stores::InMemoryTokenStore)
45
- end
46
-
47
- let(:credentials) do
48
- instance_double(Google::Auth::UserRefreshCredentials, refresh!: true)
49
- end
50
-
51
- let(:expired) { false }
52
-
53
- before do
54
- allow(Google::Auth::UserAuthorizer).
55
- to receive(:new).
56
- with(
57
- instance_of(Google::Auth::ClientId),
58
- GMAIL_READ_SCOPE,
59
- token_store
60
- ) { authorizer }
61
- allow(authorizer).to receive(:get_authorization_url).
62
- with(base_url: OOB_URI) { AUTHORIZATION_URL }
63
- allow(authorizer).to receive(:get_credentials).
64
- with(EMAIL) { credentials }
65
- allow(authorizer).to receive(:get_credentials_from_code).
66
- with(user_id: EMAIL, code: CODE, base_url: OOB_URI) { CREDENTIALS }
67
-
68
- allow(Google::Auth::UserRefreshCredentials).
69
- to receive(:new) { credentials }
70
- allow(credentials).to receive(:expired?) { expired }
71
-
72
- allow(Google::Auth::Stores::InMemoryTokenStore).
73
- to receive(:new) { token_store }
74
- allow(token_store).to receive(:store).
75
- with(EMAIL, anything) # TODO: use a JSON matcher
76
- allow(Gmail::Authenticator::ImapBackupToken).
77
- to receive(:new).
78
- with(IMAP_BACKUP_TOKEN) { imap_backup_token }
79
- end
80
-
81
- describe "#initialize" do
82
- [:email, :token].each do |param|
83
- context "parameter #{param}" do
84
- let(:params) { super().dup.reject { |k| k == param } }
85
-
86
- it "is expected" do
87
- expect { subject }.to raise_error(
88
- ArgumentError, /missing keyword: :?#{param}/
89
- )
90
- end
91
- end
92
- end
93
- end
94
-
95
- describe "#credentials" do
96
- let!(:result) { subject.credentials }
97
-
98
- it "attempts to get credentials" do
99
- expect(authorizer).to have_received(:get_credentials)
100
- end
101
-
102
- it "returns the result" do
103
- expect(result).to eq(credentials)
104
- end
105
-
106
- context "when the access_token has expired" do
107
- let(:expired) { true }
108
-
109
- it "refreshes it" do
110
- expect(credentials).to have_received(:refresh!)
111
- end
112
- end
113
- end
114
-
115
- describe "#authorization_url" do
116
- let!(:result) { subject.authorization_url }
117
-
118
- it "requests an authorization URL" do
119
- expect(authorizer).to have_received(:get_authorization_url)
120
- end
121
-
122
- it "returns the result" do
123
- expect(result).to eq(AUTHORIZATION_URL)
124
- end
125
- end
126
-
127
- describe "#credentials_from_code" do
128
- let!(:result) { subject.credentials_from_code(CODE) }
129
-
130
- it "requests credentials" do
131
- expect(authorizer).to have_received(:get_credentials_from_code)
132
- end
133
-
134
- it "returns credentials" do
135
- expect(result).to eq(CREDENTIALS)
136
- end
137
- end
138
- end
@@ -1,15 +0,0 @@
1
- require "google/auth/stores/in_memory_token_store"
2
-
3
- describe Google::Auth::Stores::InMemoryTokenStore do
4
- KEY = "key".freeze
5
- VALUE = "value".freeze
6
-
7
- subject { described_class.new }
8
-
9
- describe "#load" do
10
- it "returns an item's value" do
11
- subject[KEY] = VALUE
12
- expect(subject.load(KEY)).to eq(VALUE)
13
- end
14
- end
15
- end
@@ -1,121 +0,0 @@
1
- describe Imap::Backup::Configuration::GmailOauth2 do
2
- include HighLineTestHelpers
3
-
4
- CLIENT_ID = "my_client_id".freeze
5
- CLIENT_SECRET = "my_client_secret".freeze
6
-
7
- subject { described_class.new(account) }
8
-
9
- let(:authorization_url) { "some long authorization_url" }
10
- let(:credentials) { "credentials" }
11
- let(:json_token) { '{"sentinel":"foo"}' }
12
- let!(:highline_streams) { prepare_highline }
13
- let(:highline) { Imap::Backup::Configuration::Setup.highline }
14
- let(:input) { highline_streams[0] }
15
- let(:output) { highline_streams[1] }
16
- let(:account) { {} }
17
- let(:user_input) { %W(my_client_id\n my_secret\n my_code\n) }
18
-
19
- let(:authorizer) do
20
- instance_double(
21
- Google::Auth::UserAuthorizer,
22
- get_authorization_url: authorization_url,
23
- get_and_store_credentials_from_code: credentials
24
- )
25
- end
26
- let(:token_store) do
27
- instance_double(
28
- Google::Auth::Stores::InMemoryTokenStore,
29
- load: json_token
30
- )
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 }
41
-
42
- before do
43
- allow(Google::Auth::UserAuthorizer).
44
- to receive(:new) { authorizer }
45
- allow(Google::Auth::Stores::InMemoryTokenStore).
46
- to receive(:new) { token_store }
47
- allow(Gmail::Authenticator::ImapBackupToken).
48
- to receive(:new) { token }
49
-
50
- allow(highline).to receive(:ask).and_call_original
51
- allow(highline).to receive(:agree).and_call_original
52
-
53
- allow(Kernel).to receive(:system)
54
- allow(Kernel).to receive(:puts)
55
-
56
- allow(input).to receive(:gets).and_return(*user_input)
57
- end
58
-
59
- describe "#run" do
60
- let!(:result) { subject.run }
61
-
62
- it "clears the screen" do
63
- expect(Kernel).to have_received(:system).with("clear")
64
- end
65
-
66
- it "requests client_id" do
67
- expect(highline).to have_received(:ask).with("client_id: ")
68
- end
69
-
70
- it "requests client_secret" do
71
- expect(highline).to have_received(:ask).with("client_secret: ")
72
- end
73
-
74
- it "displays the authorization URL" do
75
- expect(Kernel).
76
- to have_received(:puts).
77
- with(/#{authorization_url}/)
78
- end
79
-
80
- it "requests the success code" do
81
- expect(highline).to have_received(:ask).with("success code: ")
82
- end
83
-
84
- it "requests an access_token via the code" do
85
- expect(authorizer).to have_received(:get_and_store_credentials_from_code)
86
- end
87
-
88
- it "returns the credentials" do
89
- expect(result).to match('"sentinel":"foo"')
90
- end
91
-
92
- it "includes the client_secret in the credentials" do
93
- expect(result).to match('"client_secret":"my_secret"')
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
120
- end
121
- end