imap-backup 9.2.0 → 9.3.1

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: 2817e61358b848f7dbef08e1a16562def8cd364e66d317e4dac56c1f5e62f958
4
- data.tar.gz: ec0e2d2963e863b204b0529ca3746492703486e8f77a9e7c08bc301b203249de
3
+ metadata.gz: 0f01e9469da5b4990bdcc90f5e019b0cf5e9cf06203f29980503edabdec4abb8
4
+ data.tar.gz: 93501685a083a9b3edc8f1bb606decf7bcac965773063eb9e59863f3afa43113
5
5
  SHA512:
6
- metadata.gz: 66871fce3faebbcbd87737fef230803b46f32025b28753fe4f83813e652ef14817fce302da0570000f0d0ecb582862589a84f535a94aa1fa7cc1578eaf30591c
7
- data.tar.gz: 274cfc47767322e31eb1e7206c3d1d449b2098ba1a65073c80aa5010c06924abfc7a952aeace01436715a7f95599503d86bcecbbdf210f7c8b5529bf7bee4fdf
6
+ metadata.gz: 494089b738a8ca9080c1057b4bdc5a8ebe8d2900e756fbd2c16445d856e3939da759bd8bc644585f8d6591055a79656b92a44b60cb0a522a87023d30f6b49ca9
7
+ data.tar.gz: fa3e100e307103808360797853953cfec677bbd6ee3b9b36330f2449ef065a5e128660154e6d3593a34652f6753a553bf4697e145c60a3e728645acb67477de3
data/README.md CHANGED
@@ -76,6 +76,7 @@ and exported via [utils export-to-thunderbird](docs/commands/utils-export-to-thu
76
76
 
77
77
  * [backup](docs/commands/backup.md)
78
78
  * [local accounts](docs/commands/local-accounts.md)
79
+ * [local check](docs/commands/local-check.md)
79
80
  * [local folders](docs/commands/local-folders.md)
80
81
  * [local list](docs/commands/local-list.md)
81
82
  * [local show](docs/commands/local-show.md)
@@ -0,0 +1,29 @@
1
+ # Migrate to a new e-mail server while keeping your existing address
2
+
3
+ While switching e-mail provider (from provider `A` to `B`), you might want to keep the same address (`mymail@domain.com`), and restrieve all your existing e-mails on your new server `B`. `imap-backup` can do that too!
4
+
5
+ 1. Backup your e-mails: use [`imap-backup setup`](/docs/commands/setup.md) to setup connection to your old provider `A`, then launch [`imap-backup backup`](/docs/commands/backup.md).
6
+ 1. Actually switch your e-mail service provider (update your DNS MX and all that...).
7
+ 1. It is best to use [`imap-backup migrate`](/docs/commands/migrate.md) and not [`imap-backup restore`](/docs/commands/restore.md) here, but both the source and the destination have the same address... You need to manually rename your old account first:
8
+
9
+ 1. Modify your configuration file manually (i.e. not via `imap-backup setup`) and rename your account to `mymail-old@domain.com`:
10
+
11
+ ```diff
12
+ "accounts": [
13
+ {
14
+ - "username": "mymail@domain.com",
15
+ + "username": "mymail-old@domain.com",
16
+ "password": "...",
17
+ - "local_path": "/some/path/.imap-backup/mymail_domain.com",
18
+ + "local_path": "/some/path/.imap-backup/mymail-old_domain.com",
19
+ "folders": [...],
20
+ "server": "..."
21
+ }
22
+ ```
23
+
24
+ 1. Rename the backup directory from `mymail_domain.com` to `mymail-old_domain.com`.
25
+
26
+ 1. Set up a new account giving access to the new provider `B` using `imap-backup setup`.
27
+ 1. Now you can use `imap-backup migrate`, optionnally adapting [delimiters and prefixes configuration](/docs/delimiters-and-prefixes.md) if need be:
28
+
29
+ imap-backup migrate mymail-old@domain.com mymail@domain.com [options]
@@ -14,6 +14,8 @@ module Imap::Backup
14
14
  BODY_ATTRIBUTE = "BODY[]".freeze
15
15
  UID_FETCH_RETRY_CLASSES = [EOFError, Errno::ECONNRESET, IOError].freeze
16
16
  APPEND_RETRY_CLASSES = [Net::IMAP::BadResponseError].freeze
17
+ CREATE_RETRY_CLASSES = [Net::IMAP::BadResponseError].freeze
18
+ EXAMINE_RETRY_CLASSES = [Net::IMAP::BadResponseError].freeze
17
19
  PERMITTED_FLAGS = %i(Answered Draft Flagged Seen).freeze
18
20
 
19
21
  attr_reader :connection
@@ -33,7 +35,9 @@ module Imap::Backup
33
35
  end
34
36
 
35
37
  def exist?
36
- examine
38
+ retry_on_error(errors: EXAMINE_RETRY_CLASSES) do
39
+ examine
40
+ end
37
41
  true
38
42
  rescue FolderNotFound
39
43
  false
@@ -42,7 +46,9 @@ module Imap::Backup
42
46
  def create
43
47
  return if exist?
44
48
 
45
- client.create(utf7_encoded_name)
49
+ retry_on_error(errors: CREATE_RETRY_CLASSES) do
50
+ client.create(utf7_encoded_name)
51
+ end
46
52
  end
47
53
 
48
54
  def uid_validity
@@ -85,6 +85,8 @@ module Imap::Backup
85
85
  end
86
86
 
87
87
  def each_connection(config, names)
88
+ return enum_for(:each_connection, config, names) if !block_given?
89
+
88
90
  config.accounts.each do |account|
89
91
  next if names.any? && !names.include?(account.username)
90
92
 
@@ -20,6 +20,47 @@ module Imap::Backup
20
20
  end
21
21
  end
22
22
 
23
+ desc(
24
+ "check",
25
+ "Check the integrity of backups for all accounts (or the selected account(s))"
26
+ )
27
+ method_option(
28
+ "delete_corrupt",
29
+ type: :boolean,
30
+ desc: "deletes any corrupted folders - USE WITH CAUTION!"
31
+ )
32
+ config_option
33
+ format_option
34
+ def check
35
+ config = load_config(**options)
36
+ results = each_connection(config, emails).map do |connection|
37
+ folders = connection.local_folders
38
+ folder_results = folders.map do |serializer|
39
+ serializer.check_integrity!
40
+ {name: serializer.folder, result: "OK"}
41
+ rescue Serializer::FolderIntegrityError => e
42
+ message = e.to_s
43
+ if options[:delete_corrupt]
44
+ serializer.delete
45
+ message << " and has been deleted"
46
+ end
47
+
48
+ {
49
+ name: serializer.folder,
50
+ result: message
51
+ }
52
+ end
53
+ {account: connection.account.username, folders: folder_results}
54
+ end
55
+
56
+ case options[:format]
57
+ when "json"
58
+ print_check_results_as_json(results)
59
+ else
60
+ print_check_results_as_text(results)
61
+ end
62
+ end
63
+
23
64
  desc "folders EMAIL", "List backed up folders"
24
65
  config_option
25
66
  format_option
@@ -87,6 +128,19 @@ module Imap::Backup
87
128
  end
88
129
 
89
130
  no_commands do
131
+ def print_check_results_as_json(results)
132
+ Kernel.puts results.to_json
133
+ end
134
+
135
+ def print_check_results_as_text(results)
136
+ results.each do |account_results|
137
+ Kernel.puts "Account: #{account_results[:account]}"
138
+ account_results[:folders].each do |folder_results|
139
+ Kernel.puts "\t#{folder_results[:name]}: #{folder_results[:result]}"
140
+ end
141
+ end
142
+ end
143
+
90
144
  def list_emails_as_json(serializer)
91
145
  emails = serializer.each_message.map do |message|
92
146
  {
@@ -142,6 +196,10 @@ module Imap::Backup
142
196
  Kernel.puts message.body
143
197
  end
144
198
  end
199
+
200
+ def emails
201
+ (options[:accounts] || "").split(",")
202
+ end
145
203
  end
146
204
  end
147
205
  end
@@ -12,7 +12,7 @@ module Imap::Backup
12
12
  quiet_option
13
13
  verbose_option
14
14
  def ignore_history(email)
15
- Imap::Backup::Logger.setup_logging options
15
+ Logger.setup_logging options
16
16
  config = load_config(**options)
17
17
  connection = connection(config, email)
18
18
 
@@ -22,6 +22,21 @@ module Imap::Backup
22
22
 
23
23
  default_task :backup
24
24
 
25
+ def self.start(*args)
26
+ # By default, commands like `imap-backup help foo bar`
27
+ # are handled by listing all `foo` methods, whereas the user
28
+ # probably wants the detailed help for the `bar` method.
29
+ # Move initial "help" argument to after any subcommand,
30
+ # so we get help for the requested subcommand method.
31
+ first_argument_is_help = ARGV[0] == "help"
32
+ second_argument_is_subcommand = subcommands.include?(ARGV[1])
33
+ if first_argument_is_help && second_argument_is_subcommand
34
+ help, subcommand = ARGV.shift(2)
35
+ ARGV.unshift(subcommand, help)
36
+ end
37
+ super
38
+ end
39
+
25
40
  def self.exit_on_failure?
26
41
  true
27
42
  end
@@ -15,6 +15,14 @@ module Imap::Backup
15
15
  @version = nil
16
16
  end
17
17
 
18
+ def pathname
19
+ "#{folder_path}.imap"
20
+ end
21
+
22
+ def exist?
23
+ File.exist?(pathname)
24
+ end
25
+
18
26
  def valid?
19
27
  return false if !exist?
20
28
  return false if version != CURRENT_VERSION
@@ -114,14 +122,6 @@ module Imap::Backup
114
122
 
115
123
  private
116
124
 
117
- def pathname
118
- "#{folder_path}.imap"
119
- end
120
-
121
- def exist?
122
- File.exist?(pathname)
123
- end
124
-
125
125
  def ensure_loaded
126
126
  return if loaded
127
127
 
@@ -59,8 +59,6 @@ module Imap::Backup
59
59
  File.open(pathname, "a") {}
60
60
  end
61
61
 
62
- private
63
-
64
62
  def exist?
65
63
  File.exist?(pathname)
66
64
  end
@@ -19,6 +19,8 @@ module Imap::Backup
19
19
 
20
20
  extend Forwardable
21
21
 
22
+ class FolderIntegrityError < StandardError; end
23
+
22
24
  def_delegator :mbox, :pathname, :mbox_pathname
23
25
  def_delegators :imap, :get, :messages, :uid_validity, :uids, :update_uid
24
26
 
@@ -42,6 +44,57 @@ module Imap::Backup
42
44
  false
43
45
  end
44
46
 
47
+ def check_integrity!
48
+ if !imap.valid?
49
+ message = ".imap file '#{imap.pathname}' is corrupt"
50
+ raise FolderIntegrityError, message
51
+ end
52
+
53
+ if !mbox.exist?
54
+ message = ".mbox file '#{mbox.pathname}' is missing"
55
+ raise FolderIntegrityError, message
56
+ end
57
+
58
+ return if imap.messages.empty?
59
+
60
+ offsets = imap.messages.map(&:offset)
61
+
62
+ if offsets != offsets.sort
63
+ message = ".imap file '#{imap.pathname}' has offset data which is out of order"
64
+ raise FolderIntegrityError, message
65
+ end
66
+
67
+ if mbox.length < offsets[-1]
68
+ message =
69
+ ".imap file '#{imap.pathname}' has offsets past the end " \
70
+ "of .mbox file '#{mbox.pathname}'"
71
+ raise FolderIntegrityError, message
72
+ end
73
+
74
+ imap.messages.each do |m|
75
+ text = mbox.read(m.offset, m.length)
76
+ if text.length < m.length
77
+ message = "Message #{m.uid} is incomplete in file '#{mbox.pathname}'"
78
+ raise FolderIntegrityError, message
79
+ end
80
+
81
+ next if text.start_with?("From ")
82
+
83
+ message =
84
+ "Message #{m.uid} not found at expected offset #{m.offset} " \
85
+ "in file '#{mbox.pathname}'"
86
+ raise FolderIntegrityError, message
87
+ end
88
+
89
+ last = imap.messages.last
90
+ expected_length = last.offset + last.length
91
+ actual_length = mbox.length
92
+ return if actual_length == expected_length
93
+
94
+ message = "Mbox file '#{mbox.pathname}' contains unexpected trailing data"
95
+ raise FolderIntegrityError, message
96
+ end
97
+
45
98
  def delete
46
99
  imap.delete
47
100
  @imap = nil
@@ -2,8 +2,8 @@ module Imap; end
2
2
 
3
3
  module Imap::Backup
4
4
  MAJOR = 9
5
- MINOR = 2
6
- REVISION = 0
5
+ MINOR = 3
6
+ REVISION = 1
7
7
  PRE = nil
8
8
  VERSION = [MAJOR, MINOR, REVISION, PRE].compact.map(&:to_s).join(".")
9
9
  end
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: 9.2.0
4
+ version: 9.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Joe Yates
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-03-01 00:00:00.000000000 Z
11
+ date: 2023-05-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: highline
@@ -219,6 +219,7 @@ files:
219
219
  - bin/imap-backup
220
220
  - docs/delimiters-and-prefixes.md
221
221
  - docs/development.md
222
+ - docs/migrate-server-keep-address.md
222
223
  - imap-backup.gemspec
223
224
  - lib/cli_coverage.rb
224
225
  - lib/email/mboxrd/message.rb