imap-backup 11.1.0 → 12.1.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: fdcbbac2458c28626fdf8be328db6f949be5cf981fde3438df03356b28849ebf
4
- data.tar.gz: 1ed5fc6db436f57c30e03a5036a61c2fa21853cd2d7ae08b861b491b04b6d12b
3
+ metadata.gz: ed7a9fd869f7b1ea66f5dc0816a4ce35d77c150bcf8170e2d778e893fda43e9d
4
+ data.tar.gz: e75c535f849eb326906c1965a1e86507676209cf41cae5b98ad62e894912d9b8
5
5
  SHA512:
6
- metadata.gz: a60997a5fdee3274d726e54c413c7b0e38df97246f4619358128a6786f5a148414d7050a6d498b805cb5376fca3d555f232e90c10d15d092ea49dce6007272e7
7
- data.tar.gz: 41b3038e096c34486670e423e6231cb4dd6eb8c39b035f8ce7580fb406bc6210ba8e436a51d51c61e67f9b5c3af3a971ad6ce016e1904850bdb14bc52e150f9c
6
+ metadata.gz: 7ced0838fae828c771e831441fc3caf71b0465f4da630c3580e1ff842c687e51ad92d7e1913e7f8d48e9bb2c2db2eeee60eea3e2dcb464fda2b51ffc9252d8ea
7
+ data.tar.gz: d751a6fb0e2eebc107f539cb80609d353bb0c07126fb09e5001bea69476ca9eaa5f1ffce7e6bab27f7efdc149be213997d20c741cc2b60bf3ccf6a3fd0bd810c
data/README.md CHANGED
@@ -113,7 +113,7 @@ These are activated via two settings:
113
113
  As with all performance tweaks, there are trade-offs.
114
114
  If you are using a small virtual server or Raspberry Pi
115
115
  to run your backups, you will probably want to leave
116
- the deafult settings.
116
+ the default settings.
117
117
  If, on the other hand, you are using a computer with a
118
118
  fair bit of RAM, and you are dealing with a *lot* of email,
119
119
  then changing these settings may be worthwhile.
@@ -122,16 +122,17 @@ then changing these settings may be worthwhile.
122
122
 
123
123
  This setting affects all account backups.
124
124
 
125
- When not set, each message is written to disk, one at a time.
126
- Doing so means the message itself is appended to the MBox file,
127
- but more importantly, the JSON metadata is rewritten to disk
128
- from scratch.
125
+ By default, `imap-backup` uses the "delay metadata" strategy.
126
+ As messages are being backed-up, the message *text*
127
+ is written to disk, while the related metadata is stored in memory.
129
128
 
130
- When in use, all of a mailboxes unbackupped messages are
131
- downloaded first, and then written to disk just once.
129
+ While this uses a little more memory, it avoids rewiting a growing JSON
130
+ file for every message, speeding things up and reducing disk wear.
132
131
 
133
- This speeds up backup as the metadata file is not rewritten
134
- after each message is added, but it potentially uses much more memory.
132
+ The alternative strategy, called "direct", writes everything to disk
133
+ as it is received. This method is slower, but has the advantage
134
+ of using slightly less memory, which may be important on very
135
+ resource-limited systems, like Raspberry Pis.
135
136
 
136
137
  ## Multi-fetch Size
137
138
 
@@ -140,12 +141,11 @@ By default, during backup, each message is downloaded one-by-one.
140
141
  Using this setting, you can download chunks of emails at a time,
141
142
  potentially speeding up the process.
142
143
 
143
- If you're not using "Delayed downlaod writes",
144
- using multi-fetch *will* mean that the backup process will use
145
- more memory - equivalent to the size of the greater number
146
- of messages downloaded at a time.
144
+ Using multi-fetch *will* mean that the backup process will use
145
+ more memory - equivalent to the size of the groups of messages
146
+ that are downloaded.
147
147
 
148
- This behaviour may also exceed limits on your email provider,
148
+ This behaviour may also exceed the rate limits on your email provider,
149
149
  so it's best to check before cranking it up!
150
150
 
151
151
  # Troubleshooting
data/docs/development.md CHANGED
@@ -6,25 +6,26 @@
6
6
 
7
7
  # Development
8
8
 
9
- A setup for developing under any available Ruby version is
10
- available in the container directory.
9
+ A Dockerfile is available to allow testing with all available Ruby versions,
10
+ see the `dev/container` directory.
11
11
 
12
12
  # Testing
13
13
 
14
14
  ## Feature Specs
15
15
 
16
- Specs under `specs/features` are integration specs run against a local IMAP server
17
- controlled by Docker Compose.
18
- Before running the test suite, it needs to be started:
16
+ Specs under `specs/features` are integration specs run against
17
+ two local IMAP servers controlled by Docker Compose.
18
+
19
+ Start them before running the test suite
19
20
 
20
21
  ```sh
21
- $ docker-compose up -d
22
+ $ docker-compose -f dev/docker-compose.yml up -d
22
23
  ```
23
24
 
24
25
  or, with Podman
25
26
 
26
27
  ```sh
27
- $ podman-compose -f docker-compose.yml up -d
28
+ $ podman-compose -f dev/docker-compose.yml up -d
28
29
  ```
29
30
 
30
31
  ```sh
@@ -62,24 +63,23 @@ use `last_command_started.output`.
62
63
 
63
64
  ```ruby
64
65
  require "net/imap"
66
+ require_relative "spec/features/support/30_email_server_helpers"
65
67
 
66
- imap = Net::IMAP.new("localhost", {port: 8993, ssl: {verify_mode: 0}})
67
- username = "address@example.com"
68
- imap.login(username, "pass")
68
+ include EmailServerHelpers
69
69
 
70
- message = "From: #{username}\nSubject: Some Subject\n\nHello!\n"
71
- response = imap.append("INBOX", message, nil, nil)
70
+ test_connection = test_server_connection_parameters
72
71
 
73
- imap.examine("INBOX")
74
- uids = imap.uid_search(["ALL"]).sort
72
+ test_imap = Net::IMAP.new(test_connection[:server], test_connection[:connection_options])
73
+ test_imap.login(test_connection[:username], test_connection[:password])
75
74
 
76
- fetch_data_items = imap.uid_fetch(uids, ["BODY[]"])
77
- ```
75
+ message = "From: #{test_connection[:username]}\nSubject: Some Subject\n\nHello!\n"
76
+ response = test_imap.append("INBOX", message, nil, nil)
78
77
 
79
- # Older Ruby Versions
78
+ test_imap.examine("INBOX")
79
+ uids = test_imap.uid_search(["ALL"]).sort
80
80
 
81
- Dockerfiles are available for all the supported Ruby versions,
82
- see the `container` directory.
81
+ fetch_data_items = test_imap.uid_fetch(uids, ["BODY[]"])
82
+ ```
83
83
 
84
84
  # Contributing
85
85
 
@@ -1,29 +1,47 @@
1
1
  # Migrate to a new e-mail server while keeping your existing address
2
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!
3
+ While switching e-mail provider (from provider `A` to `B`),
4
+ you might want to keep the same address (`mymail@domain.com`),
5
+ and copy all your existing e-mails to your new server `B`.
6
+ `imap-backup` can do that too!
4
7
 
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).
8
+ It is best to use [`imap-backup migrate`](/docs/commands/migrate.md)
9
+ and not [`imap-backup restore`](/docs/commands/restore.md) here because
10
+ `migrate` simply copies emails to folders with the same name as the ones
11
+ they were downloaded from, while `restore` changes the names of restored
12
+ folders if folders with the same name already exist on the destination server.
13
+
14
+ 1. Backup your e-mails: use [`imap-backup setup`](/docs/commands/setup.md)
15
+ to setup connection to your old provider `A`,
16
+ then launch [`imap-backup backup`](/docs/commands/backup.md).
6
17
  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:
18
+ 1. As both the source and the destination have the same address,
19
+ you need to manually rename your old account first:
8
20
 
9
- 1. Modify your configuration file manually (i.e. not via `imap-backup setup`) and rename your account to `mymail-old@domain.com`:
21
+ 1. Modify your configuration file manually
22
+ (i.e. not via `imap-backup setup`) and
23
+ rename your account to `mymail-old@domain.com`:
10
24
 
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
- ```
25
+ ```diff
26
+ "accounts": [
27
+ {
28
+ - "username": "mymail@domain.com",
29
+ + "username": "mymail-old@domain.com",
30
+ "password": "...",
31
+ - "local_path": "/some/path/.imap-backup/mymail_domain.com",
32
+ + "local_path": "/some/path/.imap-backup/mymail-old_domain.com",
33
+ "folders": [...],
34
+ "server": "..."
35
+ }
36
+ ```
23
37
 
24
- 1. Rename the backup directory from `mymail_domain.com` to `mymail-old_domain.com`.
38
+ 1. Rename the backup directory from `mymail_domain.com`
39
+ to `mymail-old_domain.com`.
25
40
 
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:
41
+ 1. Set up a new account giving access to the new provider `B`
42
+ using `imap-backup setup`.
43
+ 1. Now you can use `imap-backup migrate`, optionally adapting
44
+ [delimiters and prefixes configuration](/docs/delimiters-and-prefixes.md)
45
+ if need be:
28
46
 
29
- imap-backup migrate mymail-old@domain.com mymail@domain.com [options]
47
+ imap-backup migrate mymail-old@domain.com mymail@domain.com [options]
@@ -20,7 +20,7 @@ module Imap::Backup
20
20
  def run
21
21
  Logger.logger.info "Running backup of account: #{account.username}"
22
22
  # start the connection so we get logging messages in the right order
23
- account.client
23
+ account.client.login
24
24
 
25
25
  Account::FolderEnsurer.new(account: account).run
26
26
  Account::LocalOnlyFolderDeleter.new(account: account).run if account.mirror_mode
@@ -2,8 +2,8 @@ require "socket"
2
2
 
3
3
  require "email/provider"
4
4
  require "imap/backup/client/apple_mail"
5
+ require "imap/backup/client/automatic_login_wrapper"
5
6
  require "imap/backup/client/default"
6
- require "retry_on_error"
7
7
 
8
8
  module Imap; end
9
9
 
@@ -11,10 +11,6 @@ module Imap::Backup
11
11
  class Account; end
12
12
 
13
13
  class Account::ClientFactory
14
- include RetryOnError
15
-
16
- LOGIN_RETRY_CLASSES = [::EOFError, ::Errno::ECONNRESET, ::SocketError].freeze
17
-
18
14
  attr_reader :account
19
15
 
20
16
  def initialize(account:)
@@ -24,20 +20,17 @@ module Imap::Backup
24
20
  end
25
21
 
26
22
  def run
27
- retry_on_error(errors: LOGIN_RETRY_CLASSES) do
28
- options = provider_options
29
- Logger.logger.debug(
30
- "Creating IMAP instance: #{server}, options: #{options.inspect}"
31
- )
32
- client =
33
- if provider.is_a?(Email::Provider::AppleMail)
34
- Client::AppleMail.new(server, account, options)
35
- else
36
- Client::Default.new(server, account, options)
37
- end
38
- client.login
39
- client
40
- end
23
+ options = provider_options
24
+ Logger.logger.debug(
25
+ "Creating IMAP instance: #{server}, options: #{options.inspect}"
26
+ )
27
+ client =
28
+ if provider.is_a?(Email::Provider::AppleMail)
29
+ Client::AppleMail.new(server, account, options)
30
+ else
31
+ Client::Default.new(server, account, options)
32
+ end
33
+ Client::AutomaticLoginWrapper.new(client: client)
41
34
  end
42
35
 
43
36
  private
@@ -30,7 +30,7 @@ module Imap::Backup
30
30
  Pathname.glob(glob) do |path|
31
31
  name = source_folder_name(path)
32
32
  serializer = Serializer.new(source_local_path, name)
33
- folder = destination_folder_for(name)
33
+ folder = destination_folder_for_path(name)
34
34
  yield serializer, folder
35
35
  end
36
36
  end
@@ -55,8 +55,8 @@ module Imap::Backup
55
55
  end
56
56
  end
57
57
 
58
- def destination_folder_for(name)
59
- parts = name.split(source_delimiter)
58
+ def destination_folder_for_path(name)
59
+ parts = name.split("/")
60
60
  no_source_prefix =
61
61
  if source_prefix_clipped != "" && parts.first == source_prefix_clipped
62
62
  parts[1..]
@@ -35,6 +35,7 @@ module Imap::Backup
35
35
  end
36
36
 
37
37
  def stats
38
+ Logger.logger.debug("[Stats] loading configuration")
38
39
  config = load_config(**options)
39
40
  account = account(config, email)
40
41
 
@@ -46,6 +47,7 @@ module Imap::Backup
46
47
 
47
48
  serializer = Serializer.new(account.local_path, folder.name)
48
49
  local_uids = serializer.uids
50
+ Logger.logger.debug("[Stats] fetching email list for '#{folder.name}'")
49
51
  remote_uids = folder.uids
50
52
  {
51
53
  folder: folder.name,
@@ -0,0 +1,163 @@
1
+ require "imap/backup/cli/backup"
2
+ require "imap/backup/cli/helpers"
3
+ require "imap/backup/cli/folder_enumerator"
4
+ require "imap/backup/migrator"
5
+ require "imap/backup/mirror"
6
+
7
+ module Imap; end
8
+
9
+ module Imap::Backup
10
+ class CLI::Transfer < Thor
11
+ include Thor::Actions
12
+ include CLI::Helpers
13
+
14
+ ACTIONS = %i(migrate mirror).freeze
15
+
16
+ attr_reader :action
17
+ attr_accessor :automatic_namespaces
18
+ attr_accessor :config_path
19
+ attr_accessor :destination_delimiter
20
+ attr_reader :destination_email
21
+ attr_accessor :destination_prefix
22
+ attr_reader :options
23
+ attr_accessor :reset
24
+ attr_accessor :source_delimiter
25
+ attr_reader :source_email
26
+ attr_accessor :source_prefix
27
+
28
+ def initialize(action, source_email, destination_email, options)
29
+ super([])
30
+ @action = action
31
+ @source_email = source_email
32
+ @destination_email = destination_email
33
+ @options = options
34
+ @automatic_namespaces = nil
35
+ @config_path = nil
36
+ @destination_delimiter = nil
37
+ @destination_prefix = nil
38
+ @reset = nil
39
+ @source_delimiter = nil
40
+ @source_prefix = nil
41
+ end
42
+
43
+ no_commands do
44
+ def run
45
+ raise "Unknown action '#{action}'" if !ACTIONS.include?(action)
46
+
47
+ process_options!
48
+ prepare_mirror if action == :mirror
49
+
50
+ folders.each do |serializer, folder|
51
+ case action
52
+ when :migrate
53
+ Migrator.new(serializer, folder, reset: reset).run
54
+ when :mirror
55
+ Mirror.new(serializer, folder).run
56
+ end
57
+ end
58
+ end
59
+
60
+ def process_options!
61
+ self.automatic_namespaces = options[:automatic_namespaces] || false
62
+ self.config_path = options[:config]
63
+ self.destination_delimiter = options[:destination_delimiter]
64
+ self.destination_prefix = options[:destination_prefix]
65
+ self.source_delimiter = options[:source_delimiter]
66
+ self.source_prefix = options[:source_prefix]
67
+ self.reset = options[:reset] || false
68
+ check_accounts!
69
+ choose_prefixes_and_delimiters!
70
+ end
71
+
72
+ def check_accounts!
73
+ if destination_email == source_email
74
+ raise "Source and destination accounts cannot be the same!"
75
+ end
76
+
77
+ raise "Account '#{destination_email}' does not exist" if !destination_account
78
+
79
+ raise "Account '#{source_email}' does not exist" if !source_account
80
+ end
81
+
82
+ def choose_prefixes_and_delimiters!
83
+ if automatic_namespaces
84
+ ensure_no_prefix_or_delimiter_parameters!
85
+ query_servers_for_settings
86
+ else
87
+ add_prefix_and_delimiter_defaults
88
+ end
89
+ end
90
+
91
+ def ensure_no_prefix_or_delimiter_parameters!
92
+ if destination_delimiter
93
+ raise "--automatic-namespaces is incompatible with --destination-delimiter"
94
+ end
95
+ if destination_prefix
96
+ raise "--automatic-namespaces is incompatible with --destination-prefix"
97
+ end
98
+ raise "--automatic-namespaces is incompatible with --source-delimiter" if source_delimiter
99
+ raise "--automatic-namespaces is incompatible with --source-prefix" if source_prefix
100
+ end
101
+
102
+ def query_servers_for_settings
103
+ self.destination_prefix, self.destination_delimiter = account_settings(destination_account)
104
+ self.source_prefix, self.source_delimiter = account_settings(source_account)
105
+ end
106
+
107
+ def account_settings(account)
108
+ namespaces = account.client.namespace
109
+ personal = namespaces.personal.first
110
+ [personal.prefix, personal.delim]
111
+ end
112
+
113
+ def add_prefix_and_delimiter_defaults
114
+ self.destination_delimiter ||= "/"
115
+ self.destination_prefix ||= ""
116
+ self.source_delimiter ||= "/"
117
+ self.source_prefix ||= ""
118
+ end
119
+
120
+ def prepare_mirror
121
+ warn_if_source_account_is_not_in_mirror_mode
122
+
123
+ CLI::Backup.new(config: config_path, accounts: source_email).run
124
+ end
125
+
126
+ def warn_if_source_account_is_not_in_mirror_mode
127
+ return if source_account.mirror_mode
128
+
129
+ message =
130
+ "The account '#{source_account.username}' " \
131
+ "is not set up to make mirror backups"
132
+ Logger.logger.warn message
133
+ end
134
+
135
+ def config
136
+ @config ||= load_config(config: config_path)
137
+ end
138
+
139
+ def enumerator_options
140
+ {
141
+ destination: destination_account,
142
+ destination_delimiter: destination_delimiter,
143
+ destination_prefix: destination_prefix,
144
+ source: source_account,
145
+ source_delimiter: source_delimiter,
146
+ source_prefix: source_prefix
147
+ }
148
+ end
149
+
150
+ def folders
151
+ CLI::FolderEnumerator.new(**enumerator_options)
152
+ end
153
+
154
+ def destination_account
155
+ config.accounts.find { |a| a.username == destination_email }
156
+ end
157
+
158
+ def source_account
159
+ config.accounts.find { |a| a.username == source_email }
160
+ end
161
+ end
162
+ end
163
+ end
@@ -10,18 +10,38 @@ module Imap::Backup
10
10
  autoload :Backup, "imap/backup/cli/backup"
11
11
  autoload :Folders, "imap/backup/cli/folders"
12
12
  autoload :Local, "imap/backup/cli/local"
13
- autoload :Migrate, "imap/backup/cli/migrate"
14
- autoload :Mirror, "imap/backup/cli/mirror"
15
13
  autoload :Remote, "imap/backup/cli/remote"
16
14
  autoload :Restore, "imap/backup/cli/restore"
17
15
  autoload :Setup, "imap/backup/cli/setup"
18
16
  autoload :Stats, "imap/backup/cli/stats"
17
+ autoload :Transfer, "imap/backup/cli/transfer"
19
18
  autoload :Utils, "imap/backup/cli/utils"
20
19
 
21
20
  include Helpers
22
21
 
23
22
  VERSION_ARGUMENTS = %w(-v --version).freeze
24
23
 
24
+ NAMESPACE_CONFIGURATION_DESCRIPTION = <<~DESC.freeze
25
+ Some IMAP servers use namespaces (i.e. prefixes like "INBOX"),
26
+ while others, while others concatenate the names of subfolders
27
+ with a charater ("delimiter") other than "/".
28
+
29
+ In these cases there are two choices.
30
+
31
+ You can use the `--automatic-namespaces` option.
32
+ This wil query the source and detination servers for their
33
+ namespace configuration and will adapt paths accordingly.
34
+ This option requires that both the source and destination
35
+ servers are available and work with the provided parameters
36
+ and authentication.
37
+
38
+ If automatic configuration does not work as desired, there are the
39
+ `--source-prefix=`, `--source-delimiter=`,
40
+ `--destination-prefix=` and `--destination-delimiter=` parameters.
41
+ To check what values you should use, check the output of the
42
+ `imap-backup remote namespaces EMAIL` command.
43
+ DESC
44
+
25
45
  default_task :backup
26
46
 
27
47
  def self.start(*args)
@@ -83,19 +103,22 @@ module Imap::Backup
83
103
  All emails which have been backed up for the "source account" (SOURCE_EMAIL) are
84
104
  uploaded to the "destination account" (DESTINATION_EMAIL).
85
105
 
86
- When one or other account has namespaces (i.e. prefixes like "INBOX"),
87
- use the `--source-prefix=` and/or `--destination-prefix=` options.
106
+ Some configuration may be necessary, as follows:
88
107
 
89
- When one or other account uses a delimiter other than `/` (i.e. `.`),
90
- use the `--source-delimiter=` and/or `--destination-delimiter=` options.
108
+ #{NAMESPACE_CONFIGURATION_DESCRIPTION}
91
109
 
92
- If you you want to delete existing emails in destination folders,
110
+ Finally, if you want to delete existing emails in destination folders,
93
111
  use the `--reset` option. In this case, all existing emails are
94
112
  deleted before uploading the migrated emails.
95
113
  DESC
96
114
  config_option
97
115
  quiet_option
98
116
  verbose_option
117
+ method_option(
118
+ "automatic-namespaces",
119
+ type: :boolean,
120
+ desc: "automatically choose delimiters and prefixes"
121
+ )
99
122
  method_option(
100
123
  "destination-delimiter",
101
124
  type: :string,
@@ -126,7 +149,7 @@ module Imap::Backup
126
149
  )
127
150
  def migrate(source_email, destination_email)
128
151
  non_logging_options = Imap::Backup::Logger.setup_logging(options)
129
- Migrate.new(source_email, destination_email, **non_logging_options).run
152
+ Transfer.new(:migrate, source_email, destination_email, non_logging_options).run
130
153
  end
131
154
 
132
155
  desc(
@@ -140,7 +163,7 @@ module Imap::Backup
140
163
  If a folder list is configured for the SOURCE_EMAIL account,
141
164
  only the folders indicated by the setting are copied.
142
165
 
143
- First, runs the download of the SOURCE_EMAIL account.
166
+ First, it runs the download of the SOURCE_EMAIL account.
144
167
  If the SOURCE_EMAIL account is **not** configured to be in 'mirror' mode,
145
168
  a warning is printed.
146
169
 
@@ -152,6 +175,11 @@ module Imap::Backup
152
175
  config_option
153
176
  quiet_option
154
177
  verbose_option
178
+ method_option(
179
+ "automatic-namespaces",
180
+ type: :boolean,
181
+ desc: "automatically choose delimiters and prefixes"
182
+ )
155
183
  method_option(
156
184
  "destination-delimiter",
157
185
  type: :string,
@@ -176,7 +204,7 @@ module Imap::Backup
176
204
  )
177
205
  def mirror(source_email, destination_email)
178
206
  non_logging_options = Imap::Backup::Logger.setup_logging(options)
179
- Mirror.new(source_email, destination_email, **non_logging_options).run
207
+ Transfer.new(:mirror, source_email, destination_email, non_logging_options).run
180
208
  end
181
209
 
182
210
  desc "remote SUBCOMMAND [OPTIONS]", "View info about online accounts"
@@ -211,9 +239,14 @@ module Imap::Backup
211
239
 
212
240
  desc "stats EMAIL [OPTIONS]", "Print stats for each account folder"
213
241
  long_desc <<~DESC
214
- For each account folder, lists emails that are yet to be downloaded "server",
215
- are downloaded (exist on server and locally) "both" and those which
216
- are only present in the backup (as they have been deleted on the server) "local".
242
+ For each account folder, lists three counts of emails:
243
+
244
+ 1. "server" - those yet to be downloaded,
245
+
246
+ 2. "both" - those that exist on server and are backed up,
247
+
248
+ 3. "local" - those which are only present in the backup (as they have been deleted
249
+ on the server).
217
250
  DESC
218
251
  config_option
219
252
  format_option
@@ -0,0 +1,43 @@
1
+ require "retry_on_error"
2
+
3
+ module Imap; end
4
+
5
+ module Imap::Backup
6
+ module Client; end
7
+
8
+ class Client::AutomaticLoginWrapper
9
+ include RetryOnError
10
+
11
+ LOGIN_RETRY_CLASSES = [::EOFError, ::Errno::ECONNRESET, ::SocketError].freeze
12
+
13
+ attr_reader :client
14
+ attr_reader :login_called
15
+
16
+ def initialize(client:)
17
+ @client = client
18
+ @login_called = false
19
+ end
20
+
21
+ def method_missing(method_name, *arguments, &block)
22
+ if login_called
23
+ client.send(method_name, *arguments, &block)
24
+ else
25
+ do_first_login
26
+ client.send(method_name, *arguments, &block) if method_name != :login
27
+ end
28
+ end
29
+
30
+ def respond_to_missing?(method_name, _include_private = false)
31
+ client.respond_to?(method_name)
32
+ end
33
+
34
+ private
35
+
36
+ def do_first_login
37
+ retry_on_error(errors: LOGIN_RETRY_CLASSES) do
38
+ client.login
39
+ @login_called = true
40
+ end
41
+ end
42
+ end
43
+ end
@@ -29,7 +29,7 @@ module Imap::Backup
29
29
  rollback_on_error do
30
30
  serialized = to_serialized(message)
31
31
  mbox.append serialized
32
- imap.append uid, serialized.length, flags: flags
32
+ imap.append uid, serialized.bytesize, flags: flags
33
33
  rescue StandardError => e
34
34
  raise <<-ERROR.gsub(/^\s*/m, "")
35
35
  [#{folder}] failed to append message #{uid}: #{message}.
@@ -33,7 +33,7 @@ module Imap::Backup
33
33
  tsx.fail_outside_transaction!(:append)
34
34
  mboxrd_message = Email::Mboxrd::Message.new(message)
35
35
  serialized = mboxrd_message.to_serialized
36
- tsx.data[:metadata] << {uid: uid, length: serialized.length, flags: flags}
36
+ tsx.data[:metadata] << {uid: uid, length: serialized.bytesize, flags: flags}
37
37
  mbox.append(serialized)
38
38
  end
39
39
 
@@ -15,6 +15,9 @@ module Imap::Backup
15
15
  end
16
16
 
17
17
  def run
18
+ Logger.logger.debug(
19
+ "[IntegrityChecker] checking '#{imap.pathname}' against '#{mbox.pathname}'"
20
+ )
18
21
  if !imap.valid?
19
22
  message = ".imap file '#{imap.pathname}' is corrupt"
20
23
  raise Serializer::FolderIntegrityError, message
@@ -56,14 +59,18 @@ module Imap::Backup
56
59
  def check_mbox_length!
57
60
  last = imap.messages[-1]
58
61
 
59
- if mbox.length < last.offset + last.length
62
+ expected = last.offset + last.length
63
+ Logger.logger.debug(
64
+ "[IntegrityChecker] mbox length is #{mbox.length}, expected length is #{expected}"
65
+ )
66
+ if mbox.length < expected
60
67
  message =
61
68
  ".mbox file '#{mbox.pathname}' is shorter than indicated by " \
62
69
  ".imap file '#{imap.pathname}'"
63
70
  raise Serializer::FolderIntegrityError, message
64
71
  end
65
72
 
66
- if mbox.length > last.offset + last.length
73
+ if mbox.length > expected
67
74
  message =
68
75
  ".mbox file '#{mbox.pathname}' is longer than indicated by " \
69
76
  ".imap file '#{imap.pathname}'"
@@ -77,6 +84,11 @@ module Imap::Backup
77
84
 
78
85
  next if text.start_with?("From ")
79
86
 
87
+ Logger.logger.debug(
88
+ "[IntegrityChecker] looking for message with UID #{m.uid} " \
89
+ "at offset #{m.offset}, " \
90
+ "mbox starts with '#{text[0..200]}', expecting 'From '"
91
+ )
80
92
  message =
81
93
  "Message #{m.uid} not found at expected offset #{m.offset} " \
82
94
  "in file '#{mbox.pathname}'"
@@ -7,11 +7,9 @@ class Imap::Backup::Setup; end
7
7
  class Imap::Backup::Setup::GlobalOptions
8
8
  class DownloadStrategyChooser
9
9
  attr_reader :config
10
- attr_reader :highline
11
10
 
12
- def initialize(config:, highline:)
11
+ def initialize(config:)
13
12
  @config = config
14
- @highline = highline
15
13
  end
16
14
 
17
15
  def run
@@ -81,5 +79,9 @@ class Imap::Backup::Setup::GlobalOptions
81
79
  highline.ask "Press a key "
82
80
  end
83
81
  end
82
+
83
+ def highline
84
+ Imap::Backup::Setup.highline
85
+ end
84
86
  end
85
87
  end
@@ -8,11 +8,9 @@ module Imap::Backup
8
8
 
9
9
  class Setup::GlobalOptions
10
10
  attr_reader :config
11
- attr_reader :highline
12
11
 
13
- def initialize(config:, highline:)
12
+ def initialize(config:)
14
13
  @config = config
15
- @highline = highline
16
14
  end
17
15
 
18
16
  def run
@@ -46,8 +44,12 @@ module Imap::Backup
46
44
  current = strategies.find { |s| s[:key] == config.download_strategy }
47
45
  changed = config.download_strategy_modified ? " *" : ""
48
46
  menu.choice("change download strategy (currently: '#{current[:description]}')#{changed}") do
49
- DownloadStrategyChooser.new(config: config, highline: Setup.highline).run
47
+ DownloadStrategyChooser.new(config: config).run
50
48
  end
51
49
  end
50
+
51
+ def highline
52
+ Imap::Backup::Setup.highline
53
+ end
52
54
  end
53
55
  end
@@ -76,7 +76,7 @@ module Imap::Backup
76
76
  def modify_global_options(menu)
77
77
  changed = config.modified? ? " *" : ""
78
78
  menu.choice("modify global options#{changed}") do
79
- GlobalOptions.new(config: config, highline: Setup.highline).run
79
+ GlobalOptions.new(config: config).run
80
80
  end
81
81
  end
82
82
 
@@ -1,7 +1,7 @@
1
1
  module Imap; end
2
2
 
3
3
  module Imap::Backup
4
- MAJOR = 11
4
+ MAJOR = 12
5
5
  MINOR = 1
6
6
  REVISION = 0
7
7
  PRE = nil
@@ -1,11 +1,12 @@
1
1
  module RetryOnError
2
- def retry_on_error(errors:, limit: 10)
2
+ def retry_on_error(errors:, limit: 10, on_error: nil)
3
3
  tries ||= 1
4
4
  yield
5
5
  rescue *errors => e
6
6
  if tries < limit
7
7
  message = "#{e}, attempt #{tries} of #{limit}"
8
8
  Imap::Backup::Logger.logger.debug message
9
+ on_error&.call
9
10
  tries += 1
10
11
  retry
11
12
  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: 11.1.0
4
+ version: 12.1.0
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-08-31 00:00:00.000000000 Z
11
+ date: 2023-09-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: highline
@@ -247,14 +247,14 @@ files:
247
247
  - lib/imap/backup/cli/helpers.rb
248
248
  - lib/imap/backup/cli/local.rb
249
249
  - lib/imap/backup/cli/local/check.rb
250
- - lib/imap/backup/cli/migrate.rb
251
- - lib/imap/backup/cli/mirror.rb
252
250
  - lib/imap/backup/cli/remote.rb
253
251
  - lib/imap/backup/cli/restore.rb
254
252
  - lib/imap/backup/cli/setup.rb
255
253
  - lib/imap/backup/cli/stats.rb
254
+ - lib/imap/backup/cli/transfer.rb
256
255
  - lib/imap/backup/cli/utils.rb
257
256
  - lib/imap/backup/client/apple_mail.rb
257
+ - lib/imap/backup/client/automatic_login_wrapper.rb
258
258
  - lib/imap/backup/client/default.rb
259
259
  - lib/imap/backup/configuration.rb
260
260
  - lib/imap/backup/downloader.rb
@@ -1,87 +0,0 @@
1
- require "imap/backup/cli/folder_enumerator"
2
- require "imap/backup/migrator"
3
-
4
- module Imap; end
5
-
6
- module Imap::Backup
7
- class CLI::Migrate < Thor
8
- include Thor::Actions
9
- include CLI::Helpers
10
-
11
- attr_reader :destination_delimiter
12
- attr_reader :destination_email
13
- attr_reader :destination_prefix
14
- attr_reader :config_path
15
- attr_reader :reset
16
- attr_reader :source_delimiter
17
- attr_reader :source_email
18
- attr_reader :source_prefix
19
-
20
- def initialize(
21
- source_email,
22
- destination_email,
23
- config: nil,
24
- destination_delimiter: "/",
25
- destination_prefix: "",
26
- reset: false,
27
- source_delimiter: "/",
28
- source_prefix: ""
29
- )
30
- super([])
31
- @destination_delimiter = destination_delimiter
32
- @destination_email = destination_email
33
- @destination_prefix = destination_prefix
34
- @config_path = config
35
- @reset = reset
36
- @source_delimiter = source_delimiter
37
- @source_email = source_email
38
- @source_prefix = source_prefix
39
- end
40
-
41
- no_commands do
42
- def run
43
- check_accounts!
44
- folders.each do |serializer, folder|
45
- Migrator.new(serializer, folder, reset: reset).run
46
- end
47
- end
48
-
49
- def check_accounts!
50
- if destination_email == source_email
51
- raise "Source and destination accounts cannot be the same!"
52
- end
53
-
54
- raise "Account '#{destination_email}' does not exist" if !destination_account
55
-
56
- raise "Account '#{source_email}' does not exist" if !source_account
57
- end
58
-
59
- def config
60
- @config ||= load_config(config: config_path)
61
- end
62
-
63
- def enumerator_options
64
- {
65
- destination: destination_account,
66
- destination_delimiter: destination_delimiter,
67
- destination_prefix: destination_prefix,
68
- source: source_account,
69
- source_delimiter: source_delimiter,
70
- source_prefix: source_prefix
71
- }
72
- end
73
-
74
- def folders
75
- CLI::FolderEnumerator.new(**enumerator_options)
76
- end
77
-
78
- def destination_account
79
- config.accounts.find { |a| a.username == destination_email }
80
- end
81
-
82
- def source_account
83
- config.accounts.find { |a| a.username == source_email }
84
- end
85
- end
86
- end
87
- end
@@ -1,97 +0,0 @@
1
- require "imap/backup/cli/folder_enumerator"
2
- require "imap/backup/mirror"
3
-
4
- module Imap; end
5
-
6
- module Imap::Backup
7
- class CLI::Mirror < Thor
8
- include Thor::Actions
9
- include CLI::Helpers
10
-
11
- attr_reader :destination_delimiter
12
- attr_reader :destination_email
13
- attr_reader :destination_prefix
14
- attr_reader :config_path
15
- attr_reader :source_delimiter
16
- attr_reader :source_email
17
- attr_reader :source_prefix
18
-
19
- def initialize(
20
- source_email,
21
- destination_email,
22
- config: nil,
23
- destination_delimiter: "/",
24
- destination_prefix: "",
25
- source_delimiter: "/",
26
- source_prefix: ""
27
- )
28
- super([])
29
- @destination_delimiter = destination_delimiter
30
- @destination_email = destination_email
31
- @destination_prefix = destination_prefix
32
- @config_path = config
33
- @source_delimiter = source_delimiter
34
- @source_email = source_email
35
- @source_prefix = source_prefix
36
- end
37
-
38
- no_commands do
39
- def run
40
- check_accounts!
41
- warn_if_source_account_is_not_in_mirror_mode
42
-
43
- CLI::Backup.new(config: config_path, accounts: source_email).run
44
-
45
- folders.each do |serializer, folder|
46
- Mirror.new(serializer, folder).run
47
- end
48
- end
49
-
50
- def check_accounts!
51
- if destination_email == source_email
52
- raise "Source and destination accounts cannot be the same!"
53
- end
54
-
55
- raise "Account '#{destination_email}' does not exist" if !destination_account
56
-
57
- raise "Account '#{source_email}' does not exist" if !source_account
58
- end
59
-
60
- def warn_if_source_account_is_not_in_mirror_mode
61
- return if source_account.mirror_mode
62
-
63
- message =
64
- "The account '#{source_account.username}' " \
65
- "is not set up to make mirror backups"
66
- Logger.logger.warn message
67
- end
68
-
69
- def config
70
- @config = load_config(config: config_path)
71
- end
72
-
73
- def enumerator_options
74
- {
75
- destination: destination_account,
76
- destination_delimiter: destination_delimiter,
77
- destination_prefix: destination_prefix,
78
- source: source_account,
79
- source_delimiter: source_delimiter,
80
- source_prefix: source_prefix
81
- }
82
- end
83
-
84
- def folders
85
- CLI::FolderEnumerator.new(**enumerator_options)
86
- end
87
-
88
- def destination_account
89
- config.accounts.find { |a| a.username == destination_email }
90
- end
91
-
92
- def source_account
93
- config.accounts.find { |a| a.username == source_email }
94
- end
95
- end
96
- end
97
- end