imap-backup 3.3.0 → 4.0.0.rc1

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: 78b548972cde69ce86be3d23e21dd973730f414b90e5fd0526fd94c2852dc031
4
- data.tar.gz: d83b2679e25bea24837b3474ce53c5dede5e90e3611b4c2e7374930dc531aa20
3
+ metadata.gz: ba3f202308a586f7ec54b28d66ea2f9422cf30f706c118ab02f362346351f268
4
+ data.tar.gz: 585bd29f2d2f836aad27c6ac58855649b7621eff770d5e2b40bfdd3f6aa774f0
5
5
  SHA512:
6
- metadata.gz: 419aa2779f25fdb7d34d50b8931a7c77118042082bad3e8d4dcc94a0b5b0a9084d20a684bfe8b8e8b6820f134e3e4f6a3ec0e1d185ad53eb6ccb6ec718f97371
7
- data.tar.gz: 90a2a552949b4977acd5bbe920de73ac2f39a20bd3c7656dbac1000176d7457c7969305334d9be7e8d3ec8075f292dbbff2901246022dd5e51d65104e507c8df
6
+ metadata.gz: 6270660f55ab52e3a3d8242bdfbb2d044ce7428ccc75daabf25b6753a7c91f9c3f4d8b2be4db517f44fb887663a40e946f128c2bf06f4d1006cd02182d9f477e
7
+ data.tar.gz: f94af41fee60be97e15e0d2aa3da5a0696b9d8bab5cb03cb42c805881b13e8a38fc5be42ee30efd26576d5cadb10d2f53b0c46c00fc94ca72cf23f94e05d602b
data/.circleci/config.yml CHANGED
@@ -27,7 +27,7 @@ jobs:
27
27
  type: string
28
28
  environment:
29
29
  BUNDLE_PATH: ./vendor/bundle
30
- DOCKER_IMAP_PORT: 993
30
+ DOCKER_IMAP_SERVER: 993
31
31
  docker:
32
32
  - image: "cimg/ruby:<< parameters.ruby_version >>"
33
33
  - image: antespi/docker-imap-devel:latest
@@ -48,4 +48,4 @@ workflows:
48
48
  - test:
49
49
  matrix:
50
50
  parameters:
51
- ruby_version: ["2.4", "2.5", "2.6", "2.7"]
51
+ ruby_version: ["2.5", "2.6", "2.7"]
data/.rubocop_todo.yml CHANGED
@@ -1,42 +1,129 @@
1
1
  # This configuration was generated by
2
2
  # `rubocop --auto-gen-config`
3
- # on 2021-01-09 09:21:34 UTC using RuboCop version 0.89.1.
3
+ # on 2021-09-21 15:30:34 UTC using RuboCop version 1.21.0.
4
4
  # The point is for the user to remove these configuration records
5
5
  # one by one as the offenses are removed from the code base.
6
6
  # Note that changes in the inspected code, or installation of new
7
7
  # versions of RuboCop, may require this file to be generated again.
8
8
 
9
- # Offense count: 11
10
- # Configuration parameters: IgnoredMethods.
11
- Metrics/AbcSize:
12
- Max: 33
9
+ # Offense count: 1
10
+ # Cop supports --auto-correct.
11
+ Layout/ElseAlignment:
12
+ Exclude:
13
+ - 'lib/imap/backup/configuration/gmail_oauth2.rb'
13
14
 
14
15
  # Offense count: 2
15
- # Configuration parameters: CountComments, CountAsOne, ExcludedMethods.
16
- # ExcludedMethods: refine
17
- Metrics/BlockLength:
18
- Max: 138
16
+ # Cop supports --auto-correct.
17
+ # Configuration parameters: EmptyLineBetweenMethodDefs, EmptyLineBetweenClassDefs, EmptyLineBetweenModuleDefs, AllowAdjacentOneLineDefs, NumberOfEmptyLines.
18
+ Layout/EmptyLineBetweenDefs:
19
+ Exclude:
20
+ - 'lib/google/auth/stores/in_memory_token_store.rb'
21
+
22
+ # Offense count: 1
23
+ # Cop supports --auto-correct.
24
+ # Configuration parameters: EnforcedStyleAlignWith, Severity.
25
+ # SupportedStylesAlignWith: keyword, variable, start_of_line
26
+ Layout/EndAlignment:
27
+ Exclude:
28
+ - 'lib/imap/backup/configuration/gmail_oauth2.rb'
29
+
30
+ # Offense count: 1
31
+ # Cop supports --auto-correct.
32
+ # Configuration parameters: Width, IgnoredPatterns.
33
+ Layout/IndentationWidth:
34
+ Exclude:
35
+ - 'lib/imap/backup/configuration/gmail_oauth2.rb'
36
+
37
+ # Offense count: 34
38
+ # Configuration parameters: AllowedMethods.
39
+ # AllowedMethods: enums
40
+ Lint/ConstantDefinitionInBlock:
41
+ Exclude:
42
+ - 'lib/imap/backup/configuration/asker.rb'
43
+ - 'spec/unit/gmail/authenticator_spec.rb'
44
+ - 'spec/unit/google/auth/stores/in_memory_token_store_spec.rb'
45
+ - 'spec/unit/imap/backup/account/connection_spec.rb'
46
+ - 'spec/unit/imap/backup/account/folder_spec.rb'
47
+ - 'spec/unit/imap/backup/configuration/account_spec.rb'
48
+ - 'spec/unit/imap/backup/configuration/gmail_oauth2_spec.rb'
19
49
 
20
50
  # Offense count: 2
51
+ # Cop supports --auto-correct.
52
+ # Configuration parameters: AllowComments.
53
+ Lint/UselessMethodDefinition:
54
+ Exclude:
55
+ - 'lib/imap/backup/configuration/account.rb'
56
+ - 'lib/imap/backup/configuration/asker.rb'
57
+
58
+ # Offense count: 11
59
+ # Configuration parameters: IgnoredMethods, CountRepeatedAttributes.
60
+ Metrics/AbcSize:
61
+ Max: 33
62
+
63
+ # Offense count: 3
21
64
  # Configuration parameters: CountComments, CountAsOne.
22
65
  Metrics/ClassLength:
23
- Max: 167
66
+ Max: 172
24
67
 
25
- # Offense count: 17
26
- # Configuration parameters: CountComments, CountAsOne, ExcludedMethods.
68
+ # Offense count: 19
69
+ # Configuration parameters: CountComments, CountAsOne, ExcludedMethods, IgnoredMethods.
27
70
  Metrics/MethodLength:
28
- Max: 25
71
+ Max: 26
29
72
 
30
73
  # Offense count: 2
31
74
  # Configuration parameters: CountComments, CountAsOne.
32
75
  Metrics/ModuleLength:
33
- Max: 141
76
+ Max: 145
77
+
78
+ # Offense count: 2
79
+ # Configuration parameters: EnforcedStyle, CheckMethodNames, CheckSymbols, AllowedIdentifiers.
80
+ # SupportedStyles: snake_case, normalcase, non_integer
81
+ # AllowedIdentifiers: capture3, iso8601, rfc1123_date, rfc822, rfc2822, rfc3339
82
+ Naming/VariableNumber:
83
+ Exclude:
84
+ - 'lib/email/provider.rb'
85
+ - 'spec/unit/email/provider_spec.rb'
34
86
 
35
- # Offense count: 200
87
+ # Offense count: 209
36
88
  # Configuration parameters: AllowSubject.
37
89
  RSpec/MultipleMemoizedHelpers:
38
90
  Max: 16
39
91
 
40
- # Offense count: 1
92
+ # Offense count: 49
41
93
  RSpec/NestedGroups:
42
94
  Max: 6
95
+
96
+ # Offense count: 8
97
+ # Cop supports --auto-correct.
98
+ # Configuration parameters: EnforcedStyle.
99
+ # SupportedStyles: nested, compact
100
+ Style/ClassAndModuleChildren:
101
+ Exclude:
102
+ - 'lib/email/mboxrd/message.rb'
103
+ - 'lib/imap/backup/downloader.rb'
104
+ - 'lib/imap/backup/serializer.rb'
105
+ - 'lib/imap/backup/serializer/mbox.rb'
106
+ - 'lib/imap/backup/serializer/mbox_enumerator.rb'
107
+ - 'lib/imap/backup/serializer/mbox_store.rb'
108
+ - 'lib/imap/backup/uploader.rb'
109
+ - 'lib/imap/backup/utils.rb'
110
+
111
+ # Offense count: 1
112
+ # Cop supports --auto-correct.
113
+ Style/RedundantBegin:
114
+ Exclude:
115
+ - 'lib/imap/backup/account/connection.rb'
116
+
117
+ # Offense count: 1
118
+ # Cop supports --auto-correct.
119
+ # Configuration parameters: MinSize, WordRegex.
120
+ # SupportedStyles: percent, brackets
121
+ Style/WordArray:
122
+ EnforcedStyle: percent
123
+
124
+ # Offense count: 1
125
+ # Cop supports --auto-correct.
126
+ # Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns.
127
+ # URISchemes: http, https
128
+ Layout/LineLength:
129
+ Max: 133
data/CHANGELOG.md ADDED
@@ -0,0 +1,11 @@
1
+ # Change Log
2
+ All notable changes to this project will be documented in this file.
3
+
4
+ The format is based on [Keep a Changelog](http://keepachangelog.com/)
5
+ and this project adheres to [Semantic Versioning](http://semver.org/).
6
+
7
+ ## [4.0.0.rc1] - 2021-11-17
8
+
9
+ ### Added
10
+ * `local` commands to list accounts, folders and emails and to view single
11
+ emails.
data/README.md CHANGED
@@ -16,11 +16,19 @@
16
16
  [Rubygem]: http://rubygems.org/gems/imap-backup "Ruby gem at rubygems.org"
17
17
  [Continuous Integration]: https://circleci.com/gh/joeyates/imap-backup "Build status by CirceCI"
18
18
 
19
- ## GMail
19
+ # GMail
20
20
 
21
- GMail OAuth2 authentication is supported.
21
+ To use imap-backup with GMail, you will need to enable 'App passwords' on your account.
22
22
 
23
- To set it up, [follow the HOWTO](docs/setting-up-gmail.md).
23
+ ## GMail OAuth2
24
+
25
+ GMail OAuth2 authentication is supported, but as GMail's policy requires
26
+ users to set up an application specific to their account, the feature
27
+ is disabled by default.
28
+
29
+ You will need to set the environment variable IMAP_BACKUP_ENABLE_GMAIL_OAUTH2.
30
+
31
+ To set it up, [follow the HOWTO](docs/setting-up-gmail-with-oauth2.md).
24
32
 
25
33
  # Installation
26
34
 
data/bin/imap-backup CHANGED
@@ -1,98 +1,8 @@
1
1
  #!/usr/bin/env ruby
2
- require "optparse"
3
2
 
4
3
  $LOAD_PATH.unshift(File.expand_path("../lib/", __dir__))
5
- require "imap/backup"
4
+ require "imap/backup/cli"
6
5
 
7
- KNOWN_COMMANDS = [
8
- {name: "setup", help: "Create/edit the configuration file"},
9
- {name: "backup", help: "Do the backup (default)"},
10
- {name: "folders", help: "List folders for all (or selected) accounts"},
11
- {name: "restore", help: "Restore emails to server"},
12
- {name: "status", help: "List count of non backed-up emails per folder"},
13
- {name: "help", help: "Show usage"}
14
- ].freeze
6
+ Imap::Backup::Configuration::List.new.setup_logging
15
7
 
16
- options = {command: "backup"}
17
- parser = OptionParser.new do |opts|
18
- opts.banner = "Usage: #{$PROGRAM_NAME} [options] COMMAND"
19
-
20
- opts.separator ""
21
- opts.separator "Commands:"
22
- KNOWN_COMMANDS.each do |command|
23
- opts.separator format("\t%- 20<name>s %<help>s", command)
24
- end
25
- opts.separator ""
26
- opts.separator "Common options:"
27
-
28
- opts.on(
29
- "-a",
30
- "--accounts ACCOUNT1[,ACCOUNT2,...]",
31
- Array,
32
- "only these accounts"
33
- ) do |account|
34
- options[:accounts] = account
35
- end
36
-
37
- opts.on_tail("-h", "--help", "Show usage") do
38
- puts opts
39
- exit
40
- end
41
-
42
- opts.on_tail("--version", "Show version") do
43
- puts Imap::Backup::VERSION
44
- exit
45
- end
46
- end
47
- parser.parse!
48
-
49
- options[:command] = ARGV.shift if !ARGV.empty?
50
-
51
- # rubocop:disable Style/IfUnlessModifier
52
- if KNOWN_COMMANDS.find { |c| c[:name] == options[:command] }.nil?
53
- raise "Unknown command '#{options[:command]}'"
54
- end
55
-
56
- # rubocop:enable Style/IfUnlessModifier
57
-
58
- if options[:command] == "help"
59
- puts parser
60
- exit
61
- end
62
-
63
- begin
64
- configuration = Imap::Backup::Configuration::List.new(options[:accounts])
65
- rescue Imap::Backup::ConfigurationNotFound
66
- Imap::Backup::Configuration::Setup.new.run
67
- exit
68
- end
69
-
70
- configuration.setup_logging
71
-
72
- case options[:command]
73
- when "setup"
74
- Imap::Backup::Configuration::Setup.new.run
75
- when "backup"
76
- configuration.each_connection(&:run_backup)
77
- when "folders"
78
- configuration.each_connection do |connection|
79
- puts connection.username
80
- folders = connection.folders
81
- if folders.nil?
82
- warn "Unable to list account folders"
83
- exit 1
84
- end
85
- folders.each { |f| puts "\t#{f}" }
86
- end
87
- when "restore"
88
- configuration.each_connection(&:restore)
89
- when "status"
90
- configuration.each_connection do |connection|
91
- puts connection.username
92
- folders = connection.status
93
- folders.each do |f|
94
- missing_locally = f[:remote] - f[:local]
95
- puts "#{f[:name]}: #{missing_locally.size}"
96
- end
97
- end
98
- end
8
+ Imap::Backup::CLI.start(ARGV)
data/imap-backup ADDED
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $LOAD_PATH.unshift(File.expand_path("../lib/", __dir__))
4
+ require "imap/backup/cli"
5
+
6
+ Imap::Backup::CLI.start(ARGV)
7
+
8
+ __END__
9
+
data/imap-backup.gemspec CHANGED
@@ -13,7 +13,7 @@ Gem::Specification.new do |gem|
13
13
  gem.executables = gem.files.grep(%r{^bin/}).map { |f| File.basename(f) }
14
14
  gem.test_files = gem.files.grep(%r{^spec/})
15
15
  gem.require_paths = ["lib"]
16
- gem.required_ruby_version = [">= 2.4.0"]
16
+ gem.required_ruby_version = ">= 2.5"
17
17
  gem.version = Imap::Backup::VERSION
18
18
 
19
19
  gem.add_runtime_dependency "gmail_xoauth"
@@ -21,6 +21,7 @@ Gem::Specification.new do |gem|
21
21
  gem.add_runtime_dependency "highline"
22
22
  gem.add_runtime_dependency "mail"
23
23
  gem.add_runtime_dependency "rake"
24
+ gem.add_runtime_dependency "thor", "~> 1.1"
24
25
 
25
26
  gem.add_development_dependency "codeclimate-test-reporter", "~> 0.4.8"
26
27
  if RUBY_ENGINE == "jruby"
@@ -36,12 +36,12 @@ module Email::Mboxrd
36
36
  supplied_body.gsub(/(?<!\r)\n/, "\r\n")
37
37
  end
38
38
 
39
- private
40
-
41
39
  def parsed
42
40
  @parsed ||= Mail.new(supplied_body)
43
41
  end
44
42
 
43
+ private
44
+
45
45
  def from
46
46
  @from ||=
47
47
  begin
@@ -64,7 +64,25 @@ module Imap::Backup
64
64
 
65
65
  Imap::Backup.logger.debug "[#{folder.name}] running backup"
66
66
  serializer.apply_uid_validity(folder.uid_validity)
67
- Downloader.new(folder, serializer).run
67
+ begin
68
+ Downloader.new(folder, serializer).run
69
+ rescue Net::IMAP::ByeResponseError
70
+ reconnect
71
+ retry
72
+ end
73
+ end
74
+ end
75
+
76
+ def local_folders
77
+ return enum_for(:local_folders) if !block_given?
78
+
79
+ glob = File.join(local_path, "**", "*.imap")
80
+ base = Pathname.new(local_path)
81
+ Pathname.glob(glob) do |path|
82
+ name = path.relative_path_from(base).to_s[0..-6]
83
+ serializer = Serializer::Mbox.new(local_path, name)
84
+ folder = Account::Folder.new(self, name)
85
+ yield serializer, folder
68
86
  end
69
87
  end
70
88
 
@@ -75,7 +93,7 @@ module Imap::Backup
75
93
  end
76
94
 
77
95
  def disconnect
78
- imap.disconnect
96
+ imap.disconnect if @imap
79
97
  end
80
98
 
81
99
  def reconnect
@@ -91,7 +109,7 @@ module Imap::Backup
91
109
  "Creating IMAP instance: #{server}, options: #{options.inspect}"
92
110
  )
93
111
  imap = Net::IMAP.new(server, options)
94
- if gmail? && Gmail::Authenticator.refresh_token?(password)
112
+ if use_gmail_oauth2? && Gmail::Authenticator.refresh_token?(password)
95
113
  authenticator = Gmail::Authenticator.new(email: username, token: password)
96
114
  credentials = authenticator.credentials
97
115
  raise InvalidGmailOauth2RefreshToken if !credentials
@@ -163,19 +181,10 @@ module Imap::Backup
163
181
  password.gsub(/./, "x")
164
182
  end
165
183
 
166
- def gmail?
167
- server == Email::Provider::GMAIL_IMAP_SERVER
168
- end
169
-
170
- def local_folders
171
- glob = File.join(local_path, "**", "*.imap")
172
- base = Pathname.new(local_path)
173
- Pathname.glob(glob) do |path|
174
- name = path.relative_path_from(base).to_s[0..-6]
175
- serializer = Serializer::Mbox.new(local_path, name)
176
- folder = Account::Folder.new(self, name)
177
- yield serializer, folder
178
- end
184
+ def use_gmail_oauth2?
185
+ # TODO: test use of ENV
186
+ server == Email::Provider::GMAIL_IMAP_SERVER &&
187
+ ENV["IMAP_BACKUP_ENABLE_GMAIL_OAUTH2"]
179
188
  end
180
189
 
181
190
  def backup_folders
@@ -0,0 +1,21 @@
1
+ module Imap::Backup
2
+ class CLI::Backup < Thor
3
+ include Thor::Actions
4
+ include CLI::Helpers
5
+
6
+ attr_reader :account_names
7
+
8
+ def initialize(options)
9
+ super([])
10
+ @account_names = (options[:accounts] || "").split(",")
11
+ end
12
+
13
+ no_commands do
14
+ def run
15
+ each_connection(account_names) do |connection|
16
+ connection.run_backup
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,27 @@
1
+ module Imap::Backup
2
+ class CLI::Folders < Thor
3
+ include Thor::Actions
4
+ include CLI::Helpers
5
+
6
+ attr_reader :account_names
7
+
8
+ def initialize(options)
9
+ super([])
10
+ @account_names = (options[:accounts] || "").split(",")
11
+ end
12
+
13
+ no_commands do
14
+ def run
15
+ each_connection(account_names) do |connection|
16
+ puts connection.username
17
+ folders = connection.folders
18
+ if folders.nil?
19
+ warn "Unable to list account folders"
20
+ return false
21
+ end
22
+ folders.each { |f| puts "\t#{f}" }
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,20 @@
1
+ require "imap/backup"
2
+
3
+ module Imap::Backup::CLI::Helpers
4
+ def symbolized(options)
5
+ options.each.with_object({}) { |(k, v), acc| acc[k.intern] = v }
6
+ end
7
+
8
+ def each_connection(names)
9
+ begin
10
+ connections = Imap::Backup::Configuration::List.new(names)
11
+ rescue Imap::Backup::ConfigurationNotFound
12
+ raise "imap-backup is not configured. Run `imap-backup setup`"
13
+ return
14
+ end
15
+
16
+ connections.each_connection do |connection|
17
+ yield connection
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,70 @@
1
+ module Imap::Backup
2
+ class CLI::Local < Thor
3
+ include Thor::Actions
4
+ include CLI::Helpers
5
+
6
+ desc "accounts", "list locally backed-up accounts"
7
+ def accounts
8
+ connections = Imap::Backup::Configuration::List.new
9
+ connections.accounts.each { |a| puts a[:username] }
10
+ end
11
+
12
+ desc "folders EMAIL", "list account folders"
13
+ def folders(email)
14
+ connections = Imap::Backup::Configuration::List.new
15
+ account = connections.accounts.find { |a| a[:username] == email }
16
+ raise "#{email} is not a configured account" if !account
17
+
18
+ account_connection = Imap::Backup::Account::Connection.new(account)
19
+ account_connection.local_folders.each do |_s, f|
20
+ puts %("#{f.name}")
21
+ end
22
+ end
23
+
24
+ desc "emails EMAIL FOLDER", "list emails in a folder"
25
+ def emails(email, folder_name)
26
+ connections = Imap::Backup::Configuration::List.new
27
+ account = connections.accounts.find { |a| a[:username] == email }
28
+ raise "#{email} is not a configured account" if !account
29
+
30
+ account_connection = Imap::Backup::Account::Connection.new(account)
31
+ folder_serializer, folder = account_connection.local_folders.find do |(_s, f)|
32
+ f.name == folder_name
33
+ end
34
+ raise "Folder '#{folder_name}' not found" if !folder_serializer
35
+
36
+ max_subject = 60
37
+ puts format("%-10<uid>s %-#{max_subject}<subject>s - %<date>s", {uid: "UID", subject: "Subject", date: "Date"})
38
+ puts "-" * (12 + max_subject + 28)
39
+
40
+ uids = folder_serializer.uids
41
+
42
+ folder_serializer.each_message(uids).map do |uid, message|
43
+ m = {uid: uid, date: message.parsed.date.to_s, subject: message.parsed.subject}
44
+ if m[:subject].length > max_subject
45
+ puts format("% 10<uid>u: %.#{max_subject - 3}<subject>s... - %<date>s", m)
46
+ else
47
+ puts format("% 10<uid>u: %-#{max_subject}<subject>s - %<date>s", m)
48
+ end
49
+ end
50
+ end
51
+
52
+ desc "email EMAIL FOLDER UID", "show an email"
53
+ def email(email, folder_name, uid)
54
+ connections = Imap::Backup::Configuration::List.new
55
+ account = connections.accounts.find { |a| a[:username] == email }
56
+ raise "#{email} is not a configured account" if !account
57
+
58
+ account_connection = Imap::Backup::Account::Connection.new(account)
59
+ folder_serializer, _folder = account_connection.local_folders.find do |(_s, f)|
60
+ f.name == folder_name
61
+ end
62
+ raise "Folder '#{folder_name}' not found" if !folder_serializer
63
+
64
+ loaded_message = folder_serializer.load(uid)
65
+ raise "Message #{uid} not found in folder '#{folder_name}'" if !loaded_message
66
+
67
+ puts loaded_message.supplied_body
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,19 @@
1
+ module Imap::Backup
2
+ class CLI::Restore < Thor
3
+ include Thor::Actions
4
+ include CLI::Helpers
5
+
6
+ attr_reader :account_names
7
+
8
+ def initialize(options)
9
+ super([])
10
+ @account_names = (options[:accounts] || "").split(",")
11
+ end
12
+
13
+ no_commands do
14
+ def run
15
+ each_connection(account_names) { |connection| connection.restore }
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,13 @@
1
+ class Imap::Backup::CLI::Setup < Thor
2
+ include Thor::Actions
3
+
4
+ def initialize()
5
+ super([])
6
+ end
7
+
8
+ no_commands do
9
+ def run
10
+ Imap::Backup::Configuration::Setup.new.run
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,26 @@
1
+ module Imap::Backup
2
+ class CLI::Status < Thor
3
+ include Thor::Actions
4
+ include CLI::Helpers
5
+
6
+ attr_reader :account_names
7
+
8
+ def initialize(options)
9
+ super([])
10
+ @account_names = (options[:accounts] || "").split(",")
11
+ end
12
+
13
+ no_commands do
14
+ def run
15
+ each_connection(account_names) do |connection|
16
+ puts connection.username
17
+ folders = connection.status
18
+ folders.each do |f|
19
+ missing_locally = f[:remote] - f[:local]
20
+ puts "#{f[:name]}: #{missing_locally.size}"
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,87 @@
1
+ require "thor"
2
+
3
+ class Imap::Backup::CLI < Thor
4
+ require "imap/backup/cli/helpers"
5
+
6
+ autoload :Backup, "imap/backup/cli/backup"
7
+ autoload :Folders, "imap/backup/cli/folders"
8
+ autoload :Local, "imap/backup/cli/local"
9
+ autoload :Remote, "imap/backup/cli/remote"
10
+ autoload :Restore, "imap/backup/cli/restore"
11
+ autoload :Setup, "imap/backup/cli/setup"
12
+ autoload :Status, "imap/backup/cli/status"
13
+
14
+ include Helpers
15
+
16
+ default_task :backup
17
+
18
+ def self.exit_on_failure?
19
+ true
20
+ end
21
+
22
+ def self.accounts_option
23
+ method_option(
24
+ "accounts",
25
+ type: :string,
26
+ banner: "a comma-separated list of accounts (defaults to all configured accounts)",
27
+ aliases: ["-a"]
28
+ )
29
+ end
30
+
31
+ desc "backup [OPTIONS]", "Run the backup"
32
+ long_desc <<~DESC
33
+ Downloads any emails not yet present locally.
34
+ Runs the backup for each configured account,
35
+ or for those requested via the --accounts option.
36
+ By default all folders, are backed up.
37
+ The setup tool can be used to choose a specific list of folders to back up.
38
+ DESC
39
+ accounts_option
40
+ def backup
41
+ Backup.new(symbolized(options)).run
42
+ end
43
+
44
+ desc "folders [OPTIONS]", "This command is deprecated, use `imap-backup remote folders ACCOUNT`"
45
+ long_desc <<~DESC
46
+ Lists all folders of all configured accounts.
47
+ This command is deprecated.
48
+ Instead, use a combination of `imap-backup local accounts` to get the list of accounts,
49
+ and `imap-backup remote folders ACCOUNT` to get the folder list.
50
+ DESC
51
+ accounts_option
52
+ def folders
53
+ Folders.new(symbolized(options)).run
54
+ end
55
+
56
+ desc "restore [OPTIONS]", "This command is deprecated, use `imap-backup restore ACCOUNT`"
57
+ long_desc <<~DESC
58
+ By default, restores all local emails to their respective servers.
59
+ This command is deprecated.
60
+ Instead, use `imap-backup restore ACCOUNT` to restore a single account.
61
+ DESC
62
+ accounts_option
63
+ def restore
64
+ Restore.new(symbolized(options)).run
65
+ end
66
+
67
+ desc "setup", "Configure imap-backup"
68
+ long_desc <<~DESC
69
+ A menu-driven command-line application used to configure imap-backup.
70
+ Configure email accounts to back up.
71
+ DESC
72
+ def setup
73
+ Setup.new().run
74
+ end
75
+
76
+ desc "status", "Show backup status"
77
+ long_desc <<~DESC
78
+ For each configured account and folder, lists the number of emails yet to be downloaded.
79
+ DESC
80
+ accounts_option
81
+ def status
82
+ Status.new(symbolized(options)).run
83
+ end
84
+
85
+ desc "local subcommand ...ARGS", "View local info"
86
+ subcommand "local", Local
87
+ end
@@ -69,7 +69,7 @@ module Imap::Backup
69
69
  def modify_password(menu)
70
70
  menu.choice("modify password") do
71
71
  password =
72
- if account[:server] == Email::Provider::GMAIL_IMAP_SERVER
72
+ if use_gmail_oauth2?(account)
73
73
  Configuration::GmailOauth2.new(account).run
74
74
  else
75
75
  Configuration::Asker.password
@@ -82,6 +82,11 @@ module Imap::Backup
82
82
  end
83
83
  end
84
84
 
85
+ def use_gmail_oauth2?(account)
86
+ account[:server] == Email::Provider::GMAIL_IMAP_SERVER &&
87
+ ENV["IMAP_BACKUP_ENABLE_GMAIL_OAUTH2"]
88
+ end
89
+
85
90
  def modify_server(menu)
86
91
  menu.choice("modify server") do
87
92
  server = highline.ask("server: ")
@@ -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/blob/main/docs/setting-up-gmail.md
11
+ https://github.com/joeyates/imap-backup/blob/main/docs/setting-up-gmail-with-oauth2.md
12
12
 
13
13
  BANNER
14
14
 
@@ -4,7 +4,7 @@ module Imap::Backup
4
4
  class Configuration::List
5
5
  attr_reader :required_accounts
6
6
 
7
- def initialize(required_accounts = nil)
7
+ def initialize(required_accounts = [])
8
8
  @required_accounts = required_accounts
9
9
  end
10
10
 
@@ -12,6 +12,7 @@ module Imap::Backup
12
12
  return if !config_exists?
13
13
 
14
14
  Imap::Backup.setup_logging config
15
+ Net::IMAP.debug = config.debug?
15
16
  end
16
17
 
17
18
  def each_connection
@@ -22,6 +23,17 @@ module Imap::Backup
22
23
  end
23
24
  end
24
25
 
26
+ def accounts
27
+ @accounts ||=
28
+ if required_accounts.empty?
29
+ config.accounts
30
+ else
31
+ config.accounts.select do |account|
32
+ required_accounts.include?(account[:username])
33
+ end
34
+ end
35
+ end
36
+
25
37
  private
26
38
 
27
39
  def config
@@ -37,16 +49,5 @@ module Imap::Backup
37
49
  def config_exists?
38
50
  Configuration::Store.exist?
39
51
  end
40
-
41
- def accounts
42
- @accounts ||=
43
- if required_accounts.nil?
44
- config.accounts
45
- else
46
- config.accounts.select do |account|
47
- required_accounts.include?(account[:username])
48
- end
49
- end
50
- end
51
52
  end
52
53
  end
@@ -1,9 +1,9 @@
1
1
  module Imap; end
2
2
 
3
3
  module Imap::Backup
4
- MAJOR = 3
5
- MINOR = 3
4
+ MAJOR = 4
5
+ MINOR = 0
6
6
  REVISION = 0
7
- PRE = nil
7
+ PRE = "rc1"
8
8
  VERSION = [MAJOR, MINOR, REVISION, PRE].compact.map(&:to_s).join(".")
9
9
  end
@@ -2,6 +2,6 @@
2
2
  :username: 'address@example.org'
3
3
  :password: 'pass'
4
4
  :connection_options:
5
- :port: <%= ENV.fetch("DOCKER_IMAP_SERVER", 993) %>
5
+ :port: <%= ENV.fetch("DOCKER_IMAP_SERVER", 8993) %>
6
6
  :ssl:
7
7
  :verify_mode: 0
@@ -111,7 +111,15 @@ describe Imap::Backup::Account::Connection do
111
111
  with(email: USERNAME, token: PASSWORD) { authenticator }
112
112
  end
113
113
 
114
- context "when the password is our copy of a GMail refresh token" do
114
+ context "when the password is our copy of a GMail refresh token and the environment IMAP_BACKUP_ENABLE_GMAIL_OAUTH2 is set" do
115
+ before do
116
+ ENV["IMAP_BACKUP_ENABLE_GMAIL_OAUTH2"] = "1"
117
+ end
118
+
119
+ after do
120
+ ENV.delete("IMAP_BACKUP_ENABLE_GMAIL_OAUTH2")
121
+ end
122
+
115
123
  it "uses the OAuth2 access_token to authenticate" do
116
124
  subject.imap
117
125
 
@@ -288,6 +296,24 @@ describe Imap::Backup::Account::Connection do
288
296
  end
289
297
  end
290
298
 
299
+ context "when the IMAP session expires" do
300
+ before do
301
+ data = OpenStruct.new(data: "Session expired")
302
+ response = OpenStruct.new(data: data)
303
+ outcomes = [
304
+ -> { raise Net::IMAP::ByeResponseError, response },
305
+ -> { nil }
306
+ ]
307
+ allow(downloader).to receive(:run) { outcomes.shift.call }
308
+ end
309
+
310
+ it "reconnects" do
311
+ expect(downloader).to receive(:run).exactly(:twice)
312
+
313
+ subject.run_backup
314
+ end
315
+ end
316
+
291
317
  context "when run" do
292
318
  before { subject.run_backup }
293
319
 
@@ -407,24 +433,49 @@ describe Imap::Backup::Account::Connection do
407
433
  end
408
434
 
409
435
  describe "#reconnect" do
410
- it "disconnects from the server" do
411
- expect(imap).to receive(:disconnect)
436
+ context "when the IMAP connection has been used" do
437
+ before { subject.imap }
412
438
 
413
- subject.reconnect
439
+ it "disconnects from the server" do
440
+ expect(imap).to receive(:disconnect)
441
+
442
+ subject.reconnect
443
+ end
444
+ end
445
+
446
+ context "when the IMAP connection has not been used" do
447
+ it "does not disconnect from the server" do
448
+ expect(imap).to_not receive(:disconnect)
449
+
450
+ subject.reconnect
451
+ end
414
452
  end
415
453
 
416
454
  it "causes reconnection on future access" do
417
455
  expect(Net::IMAP).to receive(:new)
418
456
 
419
457
  subject.reconnect
458
+ subject.imap
420
459
  end
421
460
  end
422
461
 
423
462
  describe "#disconnect" do
424
- it "disconnects from the server" do
425
- expect(imap).to receive(:disconnect)
463
+ context "when the IMAP connection has been used" do
464
+ it "disconnects from the server" do
465
+ subject.imap
426
466
 
427
- subject.disconnect
467
+ expect(imap).to receive(:disconnect)
468
+
469
+ subject.disconnect
470
+ end
471
+ end
472
+
473
+ context "when the IMAP connection has not been used" do
474
+ it "does not disconnect from the server" do
475
+ expect(imap).to_not receive(:disconnect)
476
+
477
+ subject.disconnect
478
+ end
428
479
  end
429
480
  end
430
481
  end
@@ -214,16 +214,10 @@ describe Imap::Backup::Configuration::Account do
214
214
 
215
215
  describe "choosing 'modify password'" do
216
216
  let(:new_password) { "new_password" }
217
- let(:gmail_oauth2) do
218
- instance_double(Imap::Backup::Configuration::GmailOauth2, run: nil)
219
- end
220
217
 
221
218
  before do
222
219
  allow(Imap::Backup::Configuration::Asker).
223
220
  to receive(:password) { new_password }
224
- allow(Imap::Backup::Configuration::GmailOauth2).
225
- to receive(:new).
226
- with(account) { gmail_oauth2 }
227
221
  subject.run
228
222
  menu.choices["modify password"].call
229
223
  end
@@ -245,14 +239,49 @@ describe Imap::Backup::Configuration::Account do
245
239
 
246
240
  include_examples "it doesn't flag the account as modified"
247
241
  end
242
+ end
248
243
 
249
- context "when the server is for GMail" do
250
- let(:current_server) { GMAIL_IMAP_SERVER }
244
+ describe "choosing 'modify password' when the server is for GMail" do
245
+ let(:new_password) { "new_password" }
246
+ let(:current_server) { GMAIL_IMAP_SERVER }
247
+ let(:gmail_oauth2) do
248
+ instance_double(Imap::Backup::Configuration::GmailOauth2, run: nil)
249
+ end
250
+
251
+ before do
252
+ allow(Imap::Backup::Configuration::Asker).
253
+ to receive(:password) { new_password }
254
+ allow(Imap::Backup::Configuration::GmailOauth2).
255
+ to receive(:new).
256
+ with(account) { gmail_oauth2 }
257
+ end
258
+
259
+ context "when the environment IMAP_BACKUP_ENABLE_GMAIL_OAUTH2 is set" do
260
+ before do
261
+ ENV["IMAP_BACKUP_ENABLE_GMAIL_OAUTH2"] = "1"
262
+ subject.run
263
+ menu.choices["modify password"].call
264
+ end
265
+
266
+ after do
267
+ ENV.delete("IMAP_BACKUP_ENABLE_GMAIL_OAUTH2")
268
+ end
251
269
 
252
270
  it "sets up GMail OAuth2" do
253
271
  expect(gmail_oauth2).to have_received(:run)
254
272
  end
255
273
  end
274
+
275
+ context "when the environment IMAP_BACKUP_ENABLE_GMAIL_OAUTH2 is not set" do
276
+ before do
277
+ subject.run
278
+ menu.choices["modify password"].call
279
+ end
280
+
281
+ it "sets up GMail OAuth2" do
282
+ expect(gmail_oauth2).to_not have_received(:run)
283
+ end
284
+ end
256
285
  end
257
286
 
258
287
  describe "choosing 'modify server'" do
@@ -35,6 +35,7 @@ describe Imap::Backup::Configuration::List do
35
35
  allow(Imap::Backup::Configuration::Store).
36
36
  to receive(:exist?) { config_exists }
37
37
  allow(Imap::Backup).to receive(:setup_logging)
38
+ allow(store).to receive(:debug?)
38
39
  end
39
40
 
40
41
  it "sets global logging level" do
@@ -43,7 +43,7 @@ describe Imap::Backup::Configuration::Setup do
43
43
  describe "main menu" do
44
44
  before { subject.run }
45
45
 
46
- %w(add\ account save\ and\ exit exit\ without\ saving).each do |choice|
46
+ ["add account", "save and exit", "exit without saving"].each do |choice|
47
47
  it "includes #{choice}" do
48
48
  expect(output.string).to include(choice)
49
49
  end
@@ -12,7 +12,6 @@ describe Imap::Backup::Utils do
12
12
  describe ".check_permissions" do
13
13
  let(:requested) { 0o345 }
14
14
 
15
- # rubocop:disable RSpec/EmptyExampleGroup
16
15
  context "with existing files" do
17
16
  [
18
17
  [0o100, "less than the limit", true],
@@ -37,7 +36,6 @@ describe Imap::Backup::Utils do
37
36
  end
38
37
  end
39
38
  end
40
- # rubocop:enable RSpec/EmptyExampleGroup
41
39
 
42
40
  context "with non-existent files" do
43
41
  let(:exists) { false }
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.3.0
4
+ version: 4.0.0.rc1
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-21 00:00:00.000000000 Z
11
+ date: 2021-11-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: gmail_xoauth
@@ -80,6 +80,20 @@ dependencies:
80
80
  - - ">="
81
81
  - !ruby/object:Gem::Version
82
82
  version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: thor
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '1.1'
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '1.1'
83
97
  - !ruby/object:Gem::Dependency
84
98
  name: codeclimate-test-reporter
85
99
  requirement: !ruby/object:Gem::Requirement
@@ -178,6 +192,7 @@ files:
178
192
  - ".rspec-all"
179
193
  - ".rubocop.yml"
180
194
  - ".rubocop_todo.yml"
195
+ - CHANGELOG.md
181
196
  - Gemfile
182
197
  - LICENSE
183
198
  - README.md
@@ -210,7 +225,8 @@ files:
210
225
  - docs/26-type-code-into-imap-backup.png
211
226
  - docs/27-success.png
212
227
  - docs/docker-imap.md
213
- - docs/setting-up-gmail.md
228
+ - docs/setting-up-gmail-with-oauth2.md
229
+ - imap-backup
214
230
  - imap-backup.gemspec
215
231
  - lib/email/mboxrd/message.rb
216
232
  - lib/email/provider.rb
@@ -219,6 +235,14 @@ files:
219
235
  - lib/imap/backup.rb
220
236
  - lib/imap/backup/account/connection.rb
221
237
  - lib/imap/backup/account/folder.rb
238
+ - lib/imap/backup/cli.rb
239
+ - lib/imap/backup/cli/backup.rb
240
+ - lib/imap/backup/cli/folders.rb
241
+ - lib/imap/backup/cli/helpers.rb
242
+ - lib/imap/backup/cli/local.rb
243
+ - lib/imap/backup/cli/restore.rb
244
+ - lib/imap/backup/cli/setup.rb
245
+ - lib/imap/backup/cli/status.rb
222
246
  - lib/imap/backup/configuration/account.rb
223
247
  - lib/imap/backup/configuration/asker.rb
224
248
  - lib/imap/backup/configuration/connection_tester.rb
@@ -283,14 +307,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
283
307
  requirements:
284
308
  - - ">="
285
309
  - !ruby/object:Gem::Version
286
- version: 2.4.0
310
+ version: '2.5'
287
311
  required_rubygems_version: !ruby/object:Gem::Requirement
288
312
  requirements:
289
- - - ">="
313
+ - - ">"
290
314
  - !ruby/object:Gem::Version
291
- version: '0'
315
+ version: 1.3.1
292
316
  requirements: []
293
- rubygems_version: 3.0.3
317
+ rubygems_version: 3.1.4
294
318
  signing_key:
295
319
  specification_version: 4
296
320
  summary: Backup GMail (or other IMAP) accounts to disk