imap-backup 7.0.2 → 8.0.0

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