imap-backup 7.0.2 → 8.0.0

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.
@@ -1,14 +1,106 @@
1
+ require "imap/backup/logger"
2
+
1
3
  module Imap::Backup
2
4
  class CLI::Remote < Thor
3
5
  include Thor::Actions
4
6
  include CLI::Helpers
5
7
 
6
8
  desc "folders EMAIL", "List account folders"
9
+ config_option
10
+ format_option
11
+ quiet_option
12
+ verbose_option
7
13
  def folders(email)
8
- connection = connection(email)
14
+ Imap::Backup::Logger.setup_logging options
15
+ names = names(email)
16
+ case options[:format]
17
+ when "json"
18
+ json_format_names names
19
+ else
20
+ list_names names
21
+ end
22
+ end
23
+
24
+ desc "namespaces EMAIL", "List account namespaces"
25
+ long_desc <<~DESC
26
+ Lists namespaces defined for an email account.
27
+
28
+ This command is useful in deciding the parameters for
29
+ the `imap-backup migrate` and `imap-backup mirror` commands.
30
+ DESC
31
+ config_option
32
+ format_option
33
+ quiet_option
34
+ verbose_option
35
+ def namespaces(email)
36
+ Imap::Backup::Logger.setup_logging options
37
+ config = load_config(**options)
38
+ connection = connection(config, email)
39
+ namespaces = connection.namespaces
40
+ case options[:format]
41
+ when "json"
42
+ json_format_namespaces namespaces
43
+ else
44
+ list_namespaces namespaces
45
+ end
46
+ end
47
+
48
+ no_commands do
49
+ def names(email)
50
+ config = load_config(**options)
51
+ connection = connection(config, email)
52
+
53
+ connection.folder_names
54
+ end
55
+
56
+ def json_format_names(names)
57
+ list = names.map do |name|
58
+ {name: name}
59
+ end
60
+ Kernel.puts list.to_json
61
+ end
62
+
63
+ def list_names(names)
64
+ names.each do |name|
65
+ Kernel.puts %("#{name}")
66
+ end
67
+ end
68
+
69
+ def json_format_namespaces(namespaces)
70
+ list = {
71
+ personal: namespace_info(namespaces.personal.first),
72
+ other: namespace_info(namespaces.other.first),
73
+ shared: namespace_info(namespaces.shared.first)
74
+ }
75
+ Kernel.puts list.to_json
76
+ end
77
+
78
+ def list_namespaces(namespaces)
79
+ Kernel.puts format(
80
+ "%-10<name>s %-10<prefix>s %<delim>s",
81
+ {name: "Name", prefix: "Prefix", delim: "Delimiter"}
82
+ )
83
+ list_namespace namespaces, :personal
84
+ list_namespace namespaces, :other
85
+ list_namespace namespaces, :shared
86
+ end
87
+
88
+ def list_namespace(namespaces, name)
89
+ info = namespace_info(namespaces.send(name).first, quote: true)
90
+ if info
91
+ Kernel.puts format("%-10<name>s %-10<prefix>s %<delim>s", name: name, **info)
92
+ else
93
+ Kernel.puts format("%-10<name>s (Not defined)", name: name)
94
+ end
95
+ end
96
+
97
+ def namespace_info(namespace, quote: false)
98
+ return nil if !namespace
9
99
 
10
- connection.folder_names.each do |name|
11
- Kernel.puts %("#{name}")
100
+ {
101
+ prefix: quote ? namespace.prefix.to_json : namespace.prefix,
102
+ delim: quote ? namespace.delim.to_json : namespace.delim
103
+ }
12
104
  end
13
105
  end
14
106
  end
@@ -4,33 +4,38 @@ module Imap::Backup
4
4
  include CLI::Helpers
5
5
 
6
6
  attr_reader :email
7
- attr_reader :account_names
7
+ attr_reader :options
8
8
 
9
9
  def initialize(email = nil, options)
10
10
  super([])
11
11
  @email = email
12
- @account_names = options[:accounts].split(",") if options.key?(:accounts)
12
+ @options = options
13
13
  end
14
14
 
15
15
  no_commands do
16
16
  def run
17
+ config = load_config(**options)
17
18
  case
18
- when email && !account_names
19
- connection = connection(email)
19
+ when email && !emails
20
+ connection = connection(config, email)
20
21
  connection.restore
21
- when !email && !account_names
22
+ when !email && !emails
22
23
  Logger.logger.info "Calling restore without an EMAIL parameter is deprecated"
23
- each_connection([], &:restore)
24
- when email && account_names.any?
24
+ each_connection(config, [], &:restore)
25
+ when email && emails.any?
25
26
  raise "Pass either an email or the --accounts option, not both"
26
- when account_names.any?
27
+ when emails.any?
27
28
  Logger.logger.info(
28
29
  "Calling restore with the --account option is deprected, " \
29
30
  "please pass a single EMAIL argument"
30
31
  )
31
- each_connection(account_names, &:restore)
32
+ each_connection(config, emails, &:restore)
32
33
  end
33
34
  end
35
+
36
+ def emails
37
+ @emails ||= options[:accounts].split(",") if options.key?(:accounts)
38
+ end
34
39
  end
35
40
  end
36
41
  end
@@ -1,14 +1,19 @@
1
1
  module Imap::Backup
2
2
  class CLI::Setup < Thor
3
3
  include Thor::Actions
4
+ include CLI::Helpers
4
5
 
5
- def initialize
6
+ attr_reader :options
7
+
8
+ def initialize(options)
6
9
  super([])
10
+ @options = options
7
11
  end
8
12
 
9
13
  no_commands do
10
14
  def run
11
- Setup.new.run
15
+ config = load_config(**options, require_exists: false)
16
+ Setup.new(config: config).run
12
17
  end
13
18
  end
14
19
  end
@@ -14,7 +14,7 @@ module Imap::Backup
14
14
  attr_reader :email
15
15
  attr_reader :options
16
16
 
17
- def initialize(email, options = {})
17
+ def initialize(email, options)
18
18
  super([])
19
19
  @email = email
20
20
  @options = options
@@ -31,7 +31,8 @@ module Imap::Backup
31
31
  end
32
32
 
33
33
  def stats
34
- connection = connection(email)
34
+ config = load_config(**options)
35
+ connection = connection(config, email)
35
36
 
36
37
  connection.backup_folders.map do |folder|
37
38
  next if !folder.exist?
@@ -8,11 +8,13 @@ module Imap::Backup
8
8
  FAKE_EMAIL = "fake@email.com".freeze
9
9
 
10
10
  desc "ignore-history EMAIL", "Skip downloading emails up to today for all configured folders"
11
- verbose_option
11
+ config_option
12
12
  quiet_option
13
+ verbose_option
13
14
  def ignore_history(email)
14
- Imap::Backup::Logger.setup_logging symbolized(options)
15
- connection = connection(email)
15
+ Imap::Backup::Logger.setup_logging options
16
+ config = load_config(**options)
17
+ connection = connection(config, email)
16
18
 
17
19
  connection.backup_folders.each do |folder|
18
20
  next if !folder.exist?
@@ -25,12 +27,13 @@ module Imap::Backup
25
27
  desc(
26
28
  "export-to-thunderbird EMAIL [OPTIONS]",
27
29
  <<~DOC
28
- [Experimental] Copy backed up emails to Thunderbird.
30
+ Copy backed up emails to Thunderbird.
29
31
  A folder called 'imap-backup/EMAIL' is created under 'Local Folders'.
30
32
  DOC
31
33
  )
32
- verbose_option
34
+ config_option
33
35
  quiet_option
36
+ verbose_option
34
37
  method_option(
35
38
  "force",
36
39
  type: :boolean,
@@ -44,12 +47,12 @@ module Imap::Backup
44
47
  aliases: ["-p"]
45
48
  )
46
49
  def export_to_thunderbird(email)
47
- opts = symbolized(options)
48
- Imap::Backup::Logger.setup_logging opts
49
- force = opts.key?(:force) ? opts[:force] : false
50
- profile_name = opts[:profile]
50
+ Imap::Backup::Logger.setup_logging options
51
+ force = options.key?(:force) ? options[:force] : false
52
+ profile_name = options[:profile]
51
53
 
52
- connection = connection(email)
54
+ config = load_config(**options)
55
+ connection = connection(config, email)
53
56
  profile = thunderbird_profile(profile_name)
54
57
 
55
58
  if !profile
@@ -16,7 +16,6 @@ module Imap::Backup
16
16
  autoload :Restore, "imap/backup/cli/restore"
17
17
  autoload :Setup, "imap/backup/cli/setup"
18
18
  autoload :Stats, "imap/backup/cli/stats"
19
- autoload :Status, "imap/backup/cli/status"
20
19
  autoload :Utils, "imap/backup/cli/utils"
21
20
 
22
21
  include Helpers
@@ -45,8 +44,9 @@ module Imap::Backup
45
44
  The setup tool can be used to choose a specific list of folders to back up.
46
45
  DESC
47
46
  accounts_option
48
- verbose_option
47
+ config_option
49
48
  quiet_option
49
+ verbose_option
50
50
  method_option(
51
51
  "refresh",
52
52
  type: :boolean,
@@ -54,20 +54,8 @@ module Imap::Backup
54
54
  aliases: ["-r"]
55
55
  )
56
56
  def backup
57
- Imap::Backup::Logger.setup_logging symbolized(options)
58
- Backup.new(symbolized(options)).run
59
- end
60
-
61
- desc "folders [OPTIONS]", "This command is deprecated, use `imap-backup remote folders ACCOUNT`"
62
- long_desc <<~DESC
63
- Lists all folders of all configured accounts.
64
- This command is deprecated.
65
- Instead, use a combination of `imap-backup local accounts` to get the list of accounts,
66
- and `imap-backup remote folders ACCOUNT` to get the folder list.
67
- DESC
68
- accounts_option
69
- def folders
70
- Folders.new(symbolized(options)).run
57
+ Imap::Backup::Logger.setup_logging options
58
+ Backup.new(options).run
71
59
  end
72
60
 
73
61
  desc "local SUBCOMMAND [OPTIONS]", "View local info"
@@ -75,7 +63,6 @@ module Imap::Backup
75
63
 
76
64
  desc(
77
65
  "migrate SOURCE_EMAIL DESTINATION_EMAIL [OPTIONS]",
78
- "[Experimental] " \
79
66
  "Uploads backed-up emails from account SOURCE_EMAIL to account DESTINATION_EMAIL"
80
67
  )
81
68
  long_desc <<~DESC
@@ -96,8 +83,9 @@ module Imap::Backup
96
83
  use the `--reset` option. In this case, all existing emails are
97
84
  deleted before uploading the migrated emails.
98
85
  DESC
99
- verbose_option
86
+ config_option
100
87
  quiet_option
88
+ verbose_option
101
89
  method_option(
102
90
  "destination-prefix",
103
91
  type: :string,
@@ -117,8 +105,8 @@ module Imap::Backup
117
105
  aliases: ["-s"]
118
106
  )
119
107
  def migrate(source_email, destination_email)
120
- Imap::Backup::Logger.setup_logging symbolized(options)
121
- Migrate.new(source_email, destination_email, **symbolized(options)).run
108
+ Imap::Backup::Logger.setup_logging options
109
+ Migrate.new(source_email, destination_email, **options).run
122
110
  end
123
111
 
124
112
  desc(
@@ -141,8 +129,9 @@ module Imap::Backup
141
129
  This file has a '.mirror' extension. This file contains a mapping of
142
130
  the known UIDs on the source account to those on the destination account.
143
131
  DESC
144
- verbose_option
132
+ config_option
145
133
  quiet_option
134
+ verbose_option
146
135
  method_option(
147
136
  "destination-prefix",
148
137
  type: :string,
@@ -156,8 +145,8 @@ module Imap::Backup
156
145
  aliases: ["-s"]
157
146
  )
158
147
  def mirror(source_email, destination_email)
159
- Imap::Backup::Logger.setup_logging symbolized(options)
160
- Mirror.new(source_email, destination_email, **symbolized(options)).run
148
+ Imap::Backup::Logger.setup_logging options
149
+ Mirror.new(source_email, destination_email, **options).run
161
150
  end
162
151
 
163
152
  desc "remote SUBCOMMAND [OPTIONS]", "View info about online accounts"
@@ -169,11 +158,12 @@ module Imap::Backup
169
158
  their original server.
170
159
  DESC
171
160
  accounts_option
172
- verbose_option
161
+ config_option
173
162
  quiet_option
163
+ verbose_option
174
164
  def restore(email = nil)
175
- Imap::Backup::Logger.setup_logging symbolized(options)
176
- Restore.new(email, symbolized(options)).run
165
+ Imap::Backup::Logger.setup_logging options
166
+ Restore.new(email, options).run
177
167
  end
178
168
 
179
169
  desc "setup", "Configure imap-backup"
@@ -181,11 +171,12 @@ module Imap::Backup
181
171
  A menu-driven command-line application used to configure imap-backup.
182
172
  Configure email accounts to back up.
183
173
  DESC
184
- verbose_option
174
+ config_option
185
175
  quiet_option
176
+ verbose_option
186
177
  def setup
187
- Imap::Backup::Logger.setup_logging symbolized(options)
188
- Setup.new.run
178
+ Imap::Backup::Logger.setup_logging options
179
+ Setup.new(options).run
189
180
  end
190
181
 
191
182
  desc "stats EMAIL [OPTIONS]", "Print stats for each account folder"
@@ -194,24 +185,13 @@ module Imap::Backup
194
185
  are downloaded (exist on server and locally) "both" and those which
195
186
  are only present in the backup (as they have been deleted on the server) "local".
196
187
  DESC
197
- method_option(
198
- "format",
199
- type: :string,
200
- desc: "the output type, text (plain text) or json",
201
- aliases: ["-f"]
202
- )
188
+ config_option
189
+ format_option
190
+ quiet_option
191
+ verbose_option
203
192
  def stats(email)
204
- Stats.new(email, symbolized(options)).run
205
- end
206
-
207
- desc "status", "This command is deprecated, use `imap-backup stats ACCOUNT`"
208
- long_desc <<~DESC
209
- For each configured account and folder, lists the number of emails yet to be downloaded.
210
- This command is deprecated.
211
- DESC
212
- accounts_option
213
- def status
214
- Status.new(symbolized(options)).run
193
+ Imap::Backup::Logger.setup_logging options
194
+ Stats.new(email, options).run
215
195
  end
216
196
 
217
197
  desc "utils SUBCOMMAND [OPTIONS]", "Various utilities"
@@ -7,7 +7,7 @@ module Imap::Backup
7
7
  class Client::Default
8
8
  extend Forwardable
9
9
  def_delegators :imap, *%i(
10
- append authenticate create expunge login
10
+ append authenticate create expunge login namespace
11
11
  responses uid_fetch uid_search uid_store
12
12
  )
13
13
 
@@ -14,12 +14,12 @@ module Imap::Backup
14
14
  File.join(CONFIGURATION_DIRECTORY, "config.json")
15
15
  end
16
16
 
17
- def self.exist?(pathname = default_pathname)
18
- File.exist?(pathname)
17
+ def self.exist?(path: nil)
18
+ File.exist?(path || default_pathname)
19
19
  end
20
20
 
21
- def initialize(pathname = self.class.default_pathname)
22
- @pathname = pathname
21
+ def initialize(path: nil)
22
+ @pathname = path || self.class.default_pathname
23
23
  end
24
24
 
25
25
  def path
@@ -51,7 +51,7 @@ module Imap::Backup
51
51
  if changed.any?
52
52
  ids = changed.join(", ")
53
53
  debug "Removing '\Seen' flag for the following messages: #{ids}"
54
- folder.unset_flags(changed, [:Seen])
54
+ folder.remove_flags(changed, [:Seen])
55
55
  end
56
56
  uids_and_bodies
57
57
  else
@@ -56,7 +56,7 @@ module Imap::Backup
56
56
  next if !source_uid
57
57
 
58
58
  message = serializer.get(source_uid)
59
- folder.apply_flags([destination_uid], message.flags) if flags.sort != message.flags.sort
59
+ folder.set_flags([destination_uid], message.flags) if flags.sort != message.flags.sort
60
60
  end
61
61
  end
62
62
 
@@ -14,14 +14,22 @@ module Imap::Backup
14
14
  end
15
15
 
16
16
  def run
17
+ rows = [
18
+ email,
19
+ password,
20
+ server,
21
+ connection_options,
22
+ mode,
23
+ path,
24
+ folders,
25
+ multi_fetch,
26
+ reset_seen_flags_after_fetch
27
+ ].compact
28
+
17
29
  menu.header = <<~HEADER.chomp
18
30
  #{helpers.title_prefix} Account#{modified_flag}
19
31
 
20
- email #{space}#{account.username}
21
- password#{space}#{masked_password}
22
- path #{space}#{local_path}
23
- folders #{space}#{folders.map { |f| f[:name] }.join(', ')}#{mirror_mode}#{multi_fetch_size}
24
- server #{space}#{account.server}#{connection_options}#{reset_seen_flags_after_fetch}
32
+ #{format_rows(rows)}
25
33
 
26
34
  Choose an action
27
35
  HEADER
@@ -29,28 +37,70 @@ module Imap::Backup
29
37
 
30
38
  private
31
39
 
32
- def folders
33
- account.folders || []
40
+ def modified_flag
41
+ account.modified? ? "*" : ""
34
42
  end
35
43
 
36
- def mirror_mode
37
- if account.mirror_mode
38
- "\nmode #{space}mirror emails"
39
- else
40
- "\nmode #{space}keep all emails"
41
- end
44
+ def email
45
+ ["email", account.username]
42
46
  end
43
47
 
44
- def helpers
45
- Setup::Helpers.new
48
+ def password
49
+ masked_password =
50
+ if (account.password == "") || account.password.nil?
51
+ "(unset)"
52
+ else
53
+ account.password.gsub(/./, "x")
54
+ end
55
+ ["password", masked_password]
46
56
  end
47
57
 
48
- def modified_flag
49
- account.modified? ? "*" : ""
58
+ def path
59
+ # In order to handle backslashes, as Highline effectively
60
+ # does an eval (!) on its templates, we need to doubly
61
+ # escape them
62
+ local_path = account.local_path.gsub("\\", "\\\\\\\\")
63
+ ["path", local_path]
50
64
  end
51
65
 
52
- def multi_fetch_size
53
- "\nmulti-fetch #{account.multi_fetch_size}" if account.multi_fetch_size > 1
66
+ def folders
67
+ label =
68
+ if account.folder_blacklist
69
+ "exclude"
70
+ else
71
+ "include"
72
+ end
73
+ items = account.folders || []
74
+ list =
75
+ case
76
+ when items.any?
77
+ items.map { |f| f[:name] }.join(", ")
78
+ when !account.folder_blacklist
79
+ "(all folders)"
80
+ else
81
+ "(all folders) <- you have opted to not backup any folders!"
82
+ end
83
+ [label, list]
84
+ end
85
+
86
+ def mode
87
+ value =
88
+ if account.mirror_mode
89
+ "mirror emails"
90
+ else
91
+ "keep all emails"
92
+ end
93
+ ["mode", value]
94
+ end
95
+
96
+ def multi_fetch
97
+ return nil if account.multi_fetch_size == 1
98
+
99
+ ["multi-fetch", account.multi_fetch_size]
100
+ end
101
+
102
+ def server
103
+ ["server", account.server]
54
104
  end
55
105
 
56
106
  def connection_options
@@ -58,32 +108,33 @@ module Imap::Backup
58
108
 
59
109
  escaped = JSON.generate(account.connection_options)
60
110
  escaped.gsub!('"', '\"')
61
- "\nconnection options '#{escaped}'"
111
+ ["connection options", "'#{escaped}'"]
62
112
  end
63
113
 
64
114
  def reset_seen_flags_after_fetch
65
115
  return nil if !account.reset_seen_flags_after_fetch
66
116
 
67
- "\nchanges to unread flags will be reset during download"
117
+ ["changes to unread flags will be reset during download"]
68
118
  end
69
119
 
70
- def space
71
- account.connection_options ? " " * 12 : " " * 4
72
- end
73
-
74
- def masked_password
75
- if (account.password == "") || account.password.nil?
76
- "(unset)"
77
- else
78
- account.password.gsub(/./, "x")
120
+ def format_rows(rows)
121
+ largest_label, _value = rows.max_by do |(label, value)|
122
+ if value
123
+ label.length
124
+ else
125
+ 0
126
+ end
79
127
  end
128
+ rows.map do |(label, value)|
129
+ format(
130
+ "%-#{largest_label.length}<label>s %<value>s",
131
+ {label: label, value: value}
132
+ )
133
+ end.join("\n")
80
134
  end
81
135
 
82
- def local_path
83
- # In order to handle backslashes, as Highline effectively
84
- # does an eval (!) on its templates, we need to doubly
85
- # escape them
86
- account.local_path.gsub("\\", "\\\\\\\\")
136
+ def helpers
137
+ Setup::Helpers.new
87
138
  end
88
139
  end
89
140
  end
@@ -36,14 +36,15 @@ module Imap::Backup
36
36
  header menu
37
37
  modify_email menu
38
38
  modify_password menu
39
+ modify_server menu
40
+ modify_connection_options menu
41
+ test_connection menu
42
+ toggle_mirror_mode menu
39
43
  modify_backup_path menu
44
+ toggle_folder_blacklist menu
40
45
  choose_folders menu
41
- toggle_mirror_mode menu
42
46
  modify_multi_fetch_size menu
43
- modify_server menu
44
- modify_connection_options menu
45
47
  toggle_reset_seen_flags_after_fetch menu
46
- test_connection menu
47
48
  delete_account menu
48
49
  menu.choice("(q) return to main menu") { throw :done }
49
50
  menu.hidden("quit") { throw :done }
@@ -74,8 +75,17 @@ module Imap::Backup
74
75
  end
75
76
  end
76
77
 
78
+ def toggle_folder_blacklist(menu)
79
+ menu_item = "toggle folder inclusion mode (whitelist/blacklist)"
80
+ new_value = account.folder_blacklist ? nil : true
81
+ menu.choice(menu_item) do
82
+ account.folder_blacklist = new_value
83
+ end
84
+ end
85
+
77
86
  def choose_folders(menu)
78
- menu.choice("choose backup folders") do
87
+ action = account.folder_blacklist ? "exclude from backups" : "include in backups"
88
+ menu.choice("choose folders to #{action}") do
79
89
  Setup::FolderChooser.new(account).run
80
90
  end
81
91
  end
@@ -105,15 +115,7 @@ module Imap::Backup
105
115
 
106
116
  def modify_connection_options(menu)
107
117
  menu.choice("modify connection options") do
108
- default =
109
- if account.connection_options
110
- account.connection_options.to_json
111
- else
112
- ""
113
- end
114
- connection_options = highline.ask("connections options (as JSON): ") do |q|
115
- q.default = default
116
- end
118
+ connection_options = highline.ask("connections options (as JSON): ")
117
119
  if !connection_options.nil?
118
120
  begin
119
121
  account.connection_options = connection_options