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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cb709e6431c47afc24c55d39ef7d9213fc0c27b42929c0692843a254cfd83c1d
4
- data.tar.gz: 702d93af210afa826e64c0b5b1a4d1e16780c6078201e4a8db13dd4703ac62d3
3
+ metadata.gz: ccb9808337c50af0fe59f2cdc85dedcbab50b5b0980a60e2ccf10fc3734b9f37
4
+ data.tar.gz: e0b67bd5bfb040220f54c9a662c90ad545fce9c84a5fadaf6d93a485ac865ffc
5
5
  SHA512:
6
- metadata.gz: 04a0c173fea0dd80e87443db040383402eb9eacbc7f557b4db8339cfd16c20153b068da8f4ade84d82643e3e9d3b9a76f04d654cbbc931620ef197546b24492a
7
- data.tar.gz: e3cb1c12f73c421a66e41016b826ecdf3d199fd5920f9e5cbb04e0ef41619984791c8ce436283a91e6264707b04db7ecef94703f5df9af4c64cbcbb75c0fd7d7
6
+ metadata.gz: 4070e85e86a627a0791924cba209d044dd71fd54a7d0d79a7c34b9aade4d4827ae020ed3a78d5e4169d0d8aa188ce34741f5a6a44d4f0ae1b035e6bbead30df8
7
+ data.tar.gz: b20ac83f5bffea68ab4dd3ae12fcef6df78cb20aab136864ba97e23ddaa414fe344e122d430bf1de242e393e50ccef916ee4e1fba77353836aee9e148c5a3da8
data/README.md CHANGED
@@ -70,7 +70,6 @@ Alternatively, add it to your crontab.
70
70
  # Commands
71
71
 
72
72
  * [backup](docs/commands/backup.md)
73
- * [folders](docs/commands/folders.md)
74
73
  * [local accounts](docs/commands/local-accounts.md)
75
74
  * [local folders](docs/commands/local-folders.md)
76
75
  * [local list](docs/commands/local-list.md)
@@ -80,7 +79,6 @@ Alternatively, add it to your crontab.
80
79
  * [remote folders](docs/commands/remote-folders.md)
81
80
  * [restore](docs/commands/restore.md)
82
81
  * [setup](docs/commands/setup.md)
83
- * [status](docs/commands/status.md)
84
82
  * [utils export-to-thunderbird](docs/commands/utils-export-to-thunderbird.md)
85
83
  * [utils ignore-history](docs/commands/utils-ignore-history.md)
86
84
 
@@ -105,4 +103,8 @@ If you have problems:
105
103
 
106
104
  # Development
107
105
 
108
- See the [Development documentation](./docs/development.md)
106
+ See the [Development documentation](./docs/development.md) for notes
107
+ on development and testing.
108
+
109
+ See [the CHANGELOG](./CHANGELOG.md) to a list of changes that have been
110
+ made in each release.
@@ -1,6 +1,6 @@
1
1
  Configuration is stored in a JSON file.
2
2
 
3
- The format is documented [here](./docs/files/config.json).
3
+ The format is documented [here](files/config.md).
4
4
 
5
5
  # Folders
6
6
 
@@ -11,6 +11,8 @@ specific folders.
11
11
 
12
12
  You can override the parameters passed to `Net::IMAP` with `connection_options`.
13
13
 
14
+ See the Ruby Standard Library documentation for `Net::IMAP` of details of [supported parameters](https://ruby-doc.org/stdlib-3.1.2/libdoc/net-imap/rdoc/Net/IMAP.html#method-c-new).
15
+
14
16
  Specifically, if you are using a self-signed certificate and get SSL errors, e.g.
15
17
  `certificate verify failed`, you can choose to not verify the TLS connection.
16
18
 
@@ -12,16 +12,26 @@ module Imap::Backup
12
12
  end
13
13
 
14
14
  def run
15
+ all_names = Account::Connection::FolderNames.new(client: client, account: account).run
16
+
15
17
  names =
16
18
  if account.folders&.any?
17
19
  account.folders.map { |af| af[:name] }
18
20
  else
19
- Account::Connection::FolderNames.new(client: client, account: account).run
21
+ all_names
20
22
  end
21
23
 
22
- names.map do |name|
24
+ all_names.map do |name|
25
+ backup =
26
+ if account.folder_blacklist
27
+ !names.include?(name)
28
+ else
29
+ names.include?(name)
30
+ end
31
+ next if !backup
32
+
23
33
  Account::Folder.new(account.connection, name)
24
- end
34
+ end.compact
25
35
  end
26
36
  end
27
37
  end
@@ -27,12 +27,8 @@ module Imap::Backup
27
27
  Account::Connection::BackupFolders.new(client: client, account: account).run
28
28
  end
29
29
 
30
- def status
31
- ensure_account_folder
32
- backup_folders.map do |folder|
33
- s = Serializer.new(account.local_path, folder.name)
34
- {name: folder.name, local: s.uids, remote: folder.uids}
35
- end
30
+ def namespaces
31
+ client.namespace
36
32
  end
37
33
 
38
34
  def run_backup(refresh: false)
@@ -100,24 +100,24 @@ module Imap::Backup
100
100
  end
101
101
 
102
102
  def delete_multi(uids)
103
- set_flags(uids, [:Deleted])
103
+ add_flags(uids, [:Deleted])
104
104
  client.expunge
105
105
  end
106
106
 
107
- def apply_flags(uids, flags)
107
+ def set_flags(uids, flags)
108
108
  client.select(utf7_encoded_name)
109
109
  flags.reject! { |f| f == :Recent }
110
110
  client.uid_store(uids, "FLAGS", flags)
111
111
  end
112
112
 
113
- def set_flags(uids, flags)
113
+ def add_flags(uids, flags)
114
114
  # Use read-write access, via `select`
115
115
  client.select(utf7_encoded_name)
116
116
  flags.reject! { |f| f == :Recent }
117
117
  client.uid_store(uids, "+FLAGS", flags)
118
118
  end
119
119
 
120
- def unset_flags(uids, flags)
120
+ def remove_flags(uids, flags)
121
121
  client.select(utf7_encoded_name)
122
122
  client.uid_store(uids, "-FLAGS", flags)
123
123
  end
@@ -126,7 +126,7 @@ module Imap::Backup
126
126
  existing = uids
127
127
  return if existing.empty?
128
128
 
129
- set_flags(existing, [:Deleted])
129
+ add_flags(existing, [:Deleted])
130
130
  client.expunge
131
131
  end
132
132
 
@@ -8,6 +8,7 @@ module Imap::Backup
8
8
  attr_reader :password
9
9
  attr_reader :local_path
10
10
  attr_reader :folders
11
+ attr_reader :folder_blacklist
11
12
  attr_reader :mirror_mode
12
13
  attr_reader :server
13
14
  attr_reader :connection_options
@@ -19,6 +20,7 @@ module Imap::Backup
19
20
  @password = options[:password]
20
21
  @local_path = options[:local_path]
21
22
  @folders = options[:folders]
23
+ @folder_blacklist = options[:folder_blacklist]
22
24
  @mirror_mode = options[:mirror_mode]
23
25
  @server = options[:server]
24
26
  @connection_options = options[:connection_options]
@@ -57,6 +59,7 @@ module Imap::Backup
57
59
  h = {username: @username, password: @password}
58
60
  h[:local_path] = @local_path if @local_path
59
61
  h[:folders] = @folders if @folders
62
+ h[:folder_blacklist] = true if @folder_blacklist
60
63
  h[:mirror_mode] = true if @mirror_mode
61
64
  h[:server] = @server if @server
62
65
  h[:connection_options] = @connection_options if @connection_options
@@ -85,6 +88,10 @@ module Imap::Backup
85
88
  update(:folders, value)
86
89
  end
87
90
 
91
+ def folder_blacklist=(value)
92
+ update(:folder_blacklist, value)
93
+ end
94
+
88
95
  def mirror_mode=(value)
89
96
  update(:mirror_mode, value)
90
97
  end
@@ -94,7 +101,12 @@ module Imap::Backup
94
101
  end
95
102
 
96
103
  def connection_options=(value)
97
- parsed = JSON.parse(value, symbolize_names: true)
104
+ parsed =
105
+ if value == ""
106
+ nil
107
+ else
108
+ JSON.parse(value, symbolize_names: true)
109
+ end
98
110
  update(:connection_options, parsed)
99
111
  end
100
112
 
@@ -3,21 +3,28 @@ module Imap::Backup
3
3
  include Thor::Actions
4
4
  include CLI::Helpers
5
5
 
6
- attr_reader :account_names
7
- attr_reader :refresh
6
+ attr_reader :options
8
7
 
9
8
  def initialize(options)
10
9
  super([])
11
- @account_names = (options[:accounts] || "").split(",")
12
- @refresh = options.key?(:refresh) ? !!options[:refresh] : false
10
+ @options = options
13
11
  end
14
12
 
15
13
  no_commands do
16
14
  def run
17
- each_connection(account_names) do |connection|
15
+ config = load_config(**options)
16
+ each_connection(config, emails) do |connection|
18
17
  connection.run_backup(refresh: refresh)
19
18
  end
20
19
  end
20
+
21
+ def emails
22
+ (options[:accounts] || "").split(",")
23
+ end
24
+
25
+ def refresh
26
+ options.key?(:refresh) ? !!options[:refresh] : false
27
+ end
21
28
  end
22
29
  end
23
30
  end
@@ -1,16 +1,24 @@
1
1
  require "imap/backup"
2
- require "imap/backup/cli/accounts"
3
2
 
4
3
  module Imap::Backup
5
4
  module CLI::Helpers
6
5
  def self.included(base)
7
6
  base.class_eval do
8
- def self.verbose_option
7
+ def self.config_option
9
8
  method_option(
10
- "verbose",
11
- type: :boolean,
12
- desc: "increase the amount of logging",
13
- aliases: ["-v"]
9
+ "config",
10
+ type: :string,
11
+ desc: "supply the configuration file path (default: ~/.imap-backup/config.json)",
12
+ aliases: ["-c"]
13
+ )
14
+ end
15
+
16
+ def self.format_option
17
+ method_option(
18
+ "format",
19
+ type: :string,
20
+ desc: "the output type, 'text' for plain text or 'json'",
21
+ aliases: ["-f"]
14
22
  )
15
23
  end
16
24
 
@@ -22,34 +30,64 @@ module Imap::Backup
22
30
  aliases: ["-q"]
23
31
  )
24
32
  end
33
+
34
+ def self.verbose_option
35
+ method_option(
36
+ "verbose",
37
+ type: :boolean,
38
+ desc: "increase the amount of logging",
39
+ aliases: ["-v"]
40
+ )
41
+ end
25
42
  end
26
43
  end
27
44
 
28
- def symbolized(options)
29
- options.each.with_object({}) do |(k, v), acc|
30
- key = k.gsub("-", "_").intern
31
- acc[key] = v
45
+ def options
46
+ @symbolized_options ||= # rubocop:disable Naming/MemoizedInstanceVariableName
47
+ begin
48
+ options = super()
49
+ options.each.with_object({}) do |(k, v), acc|
50
+ key =
51
+ if k.is_a?(String)
52
+ k.gsub("-", "_").intern
53
+ else
54
+ k
55
+ end
56
+ acc[key] = v
57
+ end
58
+ end
59
+ end
60
+
61
+ def load_config(**options)
62
+ path = options[:config]
63
+ require_exists = options.key?(:require_exists) ? options[:require_exists] : true
64
+ if require_exists
65
+ exists = Configuration.exist?(path: path)
66
+ if !exists
67
+ expected = path || Configuration.default_pathname
68
+ raise ConfigurationNotFound, "Configuration file '#{expected}' not found"
69
+ end
32
70
  end
71
+ Configuration.new(path: path)
33
72
  end
34
73
 
35
- def account(email)
36
- accounts = CLI::Accounts.new
37
- account = accounts.find { |a| a.username == email }
74
+ def account(config, email)
75
+ account = config.accounts.find { |a| a.username == email }
38
76
  raise "#{email} is not a configured account" if !account
39
77
 
40
78
  account
41
79
  end
42
80
 
43
- def connection(email)
44
- account = account(email)
81
+ def connection(config, email)
82
+ account = account(config, email)
45
83
 
46
84
  Account::Connection.new(account)
47
85
  end
48
86
 
49
- def each_connection(names)
50
- accounts = CLI::Accounts.new(names)
87
+ def each_connection(config, names)
88
+ config.accounts.each do |account|
89
+ next if names.any? && !names.include?(account.username)
51
90
 
52
- accounts.each do |account|
53
91
  yield account.connection
54
92
  end
55
93
  rescue ConfigurationNotFound
@@ -1,5 +1,3 @@
1
- require "imap/backup/cli/accounts"
2
-
3
1
  module Imap::Backup
4
2
  class CLI::Local < Thor
5
3
  include Thor::Actions
@@ -8,39 +6,56 @@ module Imap::Backup
8
6
  MAX_SUBJECT = 60
9
7
 
10
8
  desc "accounts", "List locally backed-up accounts"
9
+ config_option
10
+ format_option
11
11
  def accounts
12
- accounts = CLI::Accounts.new
13
- accounts.each { |a| Kernel.puts a.username }
12
+ config = load_config(**options)
13
+ names = config.accounts.map(&:username)
14
+ case options[:format]
15
+ when "json"
16
+ list = names.map { |n| {username: n} }
17
+ Kernel.puts list.to_json
18
+ else
19
+ names.each { |n| Kernel.puts n }
20
+ end
14
21
  end
15
22
 
16
23
  desc "folders EMAIL", "List backed up folders"
24
+ config_option
25
+ format_option
17
26
  def folders(email)
18
- connection = connection(email)
27
+ config = load_config(**options)
28
+ connection = connection(config, email)
19
29
 
20
- connection.local_folders.each do |_s, f|
21
- Kernel.puts %("#{f.name}")
30
+ folders = connection.local_folders
31
+ case options[:format]
32
+ when "json"
33
+ list = folders.map { |_s, f| {name: f.name} }
34
+ Kernel.puts list.to_json
35
+ else
36
+ folders.each do |_s, f|
37
+ Kernel.puts %("#{f.name}")
38
+ end
22
39
  end
23
40
  end
24
41
 
25
42
  desc "list EMAIL FOLDER", "List emails in a folder"
43
+ config_option
44
+ format_option
26
45
  def list(email, folder_name)
27
- connection = connection(email)
46
+ config = load_config(**options)
47
+ connection = connection(config, email)
28
48
 
29
- folder_serializer, _folder = connection.local_folders.find do |(_s, f)|
49
+ serializer, _folder = connection.local_folders.find do |(_s, f)|
30
50
  f.name == folder_name
31
51
  end
32
- raise "Folder '#{folder_name}' not found" if !folder_serializer
33
-
34
- Kernel.puts format(
35
- "%-10<uid>s %-#{MAX_SUBJECT}<subject>s - %<date>s",
36
- {uid: "UID", subject: "Subject", date: "Date"}
37
- )
38
- Kernel.puts "-" * (12 + MAX_SUBJECT + 28)
39
-
40
- uids = folder_serializer.uids
52
+ raise "Folder '#{folder_name}' not found" if !serializer
41
53
 
42
- folder_serializer.each_message(uids).map do |message|
43
- list_message message
54
+ case options[:format]
55
+ when "json"
56
+ list_emails_as_json serializer
57
+ else
58
+ list_emails_as_text serializer
44
59
  end
45
60
  end
46
61
 
@@ -50,29 +65,52 @@ module Imap::Backup
50
65
  If more than one UID is given, they are separated by a header indicating
51
66
  the UID.
52
67
  DESC
68
+ config_option
69
+ format_option
53
70
  def show(email, folder_name, uids)
54
- connection = connection(email)
71
+ config = load_config(**options)
72
+ connection = connection(config, email)
55
73
 
56
- folder_serializer, _folder = connection.local_folders.find do |(_s, f)|
74
+ serializer, _folder = connection.local_folders.find do |(_s, f)|
57
75
  f.name == folder_name
58
76
  end
59
- raise "Folder '#{folder_name}' not found" if !folder_serializer
77
+ raise "Folder '#{folder_name}' not found" if !serializer
60
78
 
61
79
  uid_list = uids.split(",")
62
- folder_serializer.each_message(uid_list).each do |message|
63
- if uid_list.count > 1
64
- Kernel.puts <<~HEADER
65
- #{'-' * 80}
66
- #{format('| UID: %-71s |', message.uid)}
67
- #{'-' * 80}
68
- HEADER
69
- end
70
- Kernel.puts message.body
80
+
81
+ case options[:format]
82
+ when "json"
83
+ show_emails_as_json serializer, uid_list
84
+ else
85
+ show_emails_as_text serializer, uid_list
71
86
  end
72
87
  end
73
88
 
74
89
  no_commands do
75
- def list_message(message)
90
+ def list_emails_as_json(serializer)
91
+ emails = serializer.each_message.map do |message|
92
+ {
93
+ uid: message.uid,
94
+ date: message.date.to_s,
95
+ subject: message.subject || ""
96
+ }
97
+ end
98
+ Kernel.puts emails.to_json
99
+ end
100
+
101
+ def list_emails_as_text(serializer)
102
+ Kernel.puts format(
103
+ "%-10<uid>s %-#{MAX_SUBJECT}<subject>s - %<date>s",
104
+ {uid: "UID", subject: "Subject", date: "Date"}
105
+ )
106
+ Kernel.puts "-" * (12 + MAX_SUBJECT + 28)
107
+
108
+ serializer.each_message.map do |message|
109
+ list_message_as_text message
110
+ end
111
+ end
112
+
113
+ def list_message_as_text(message)
76
114
  m = {
77
115
  uid: message.uid,
78
116
  date: message.date.to_s,
@@ -84,6 +122,26 @@ module Imap::Backup
84
122
  Kernel.puts format("% 10<uid>u: %-#{MAX_SUBJECT}<subject>s - %<date>s", m)
85
123
  end
86
124
  end
125
+
126
+ def show_emails_as_json(serializer, uids)
127
+ emails = serializer.each_message(uids).map do |m|
128
+ m.to_h.tap { |h| h[:body] = m.body }
129
+ end
130
+ Kernel.puts emails.to_json
131
+ end
132
+
133
+ def show_emails_as_text(serializer, uids)
134
+ serializer.each_message(uids).each do |message|
135
+ if uids.count > 1
136
+ Kernel.puts <<~HEADER
137
+ #{'-' * 80}
138
+ #{format('| UID: %-71s |', message.uid)}
139
+ #{'-' * 80}
140
+ HEADER
141
+ end
142
+ Kernel.puts message.body
143
+ end
144
+ end
87
145
  end
88
146
  end
89
147
  end
@@ -3,9 +3,11 @@ require "imap/backup/migrator"
3
3
  module Imap::Backup
4
4
  class CLI::Migrate < Thor
5
5
  include Thor::Actions
6
+ include CLI::Helpers
6
7
 
7
8
  attr_reader :destination_email
8
9
  attr_reader :destination_prefix
10
+ attr_reader :config_path
9
11
  attr_reader :reset
10
12
  attr_reader :source_email
11
13
  attr_reader :source_prefix
@@ -13,6 +15,7 @@ module Imap::Backup
13
15
  def initialize(
14
16
  source_email,
15
17
  destination_email,
18
+ config: nil,
16
19
  destination_prefix: "",
17
20
  reset: false,
18
21
  source_prefix: ""
@@ -20,6 +23,7 @@ module Imap::Backup
20
23
  super([])
21
24
  @destination_email = destination_email
22
25
  @destination_prefix = destination_prefix
26
+ @config_path = config
23
27
  @reset = reset
24
28
  @source_email = source_email
25
29
  @source_prefix = source_prefix
@@ -44,7 +48,7 @@ module Imap::Backup
44
48
  end
45
49
 
46
50
  def config
47
- Configuration.new
51
+ @config ||= load_config(config: config_path)
48
52
  end
49
53
 
50
54
  def destination_account
@@ -3,21 +3,25 @@ require "imap/backup/mirror"
3
3
  module Imap::Backup
4
4
  class CLI::Mirror < Thor
5
5
  include Thor::Actions
6
+ include CLI::Helpers
6
7
 
7
8
  attr_reader :destination_email
8
9
  attr_reader :destination_prefix
10
+ attr_reader :config_path
9
11
  attr_reader :source_email
10
12
  attr_reader :source_prefix
11
13
 
12
14
  def initialize(
13
15
  source_email,
14
16
  destination_email,
17
+ config: nil,
15
18
  destination_prefix: "",
16
19
  source_prefix: ""
17
20
  )
18
21
  super([])
19
22
  @destination_email = destination_email
20
23
  @destination_prefix = destination_prefix
24
+ @config_path = config
21
25
  @source_email = source_email
22
26
  @source_prefix = source_prefix
23
27
  end
@@ -27,7 +31,7 @@ module Imap::Backup
27
31
  check_accounts!
28
32
  warn_if_source_account_is_not_in_mirror_mode
29
33
 
30
- CLI::Backup.new(accounts: source_email).run
34
+ CLI::Backup.new(config: config_path, accounts: source_email).run
31
35
 
32
36
  folders.each do |serializer, folder|
33
37
  Mirror.new(serializer, folder).run
@@ -54,7 +58,7 @@ module Imap::Backup
54
58
  end
55
59
 
56
60
  def config
57
- Configuration.new
61
+ @config = load_config(config: config_path)
58
62
  end
59
63
 
60
64
  def destination_account