imap-backup 3.3.0 → 4.0.0.rc1

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.
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