imap-backup 15.1.3 → 16.1.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: 9a391a879278c1f286d5756ebfc3e77c39acda7e5ec9441776e30fb720251cee
4
- data.tar.gz: bcd9c851be504e9c775a241a21e1ecc708baf4f05ff7fe42160e7468c10ca3f3
3
+ metadata.gz: bf6997d289ca5babbecd35e683ede921273300779f3f7dac95e247c0478f1814
4
+ data.tar.gz: '009892c47699a0cc94ff28c2d3ea7346915732b7dc86c7231b7e606302219888'
5
5
  SHA512:
6
- metadata.gz: 289206eb94864c60e82b35db46e9f5d7531f73692bd10c02d407e800c77aa00597a149cfc7d05f749abe8d8189745a4b9b5a184fd8df472ea13d646fe8065105
7
- data.tar.gz: b38e2740e7862fc92175a684dfd93a3b176ed3b8e56ae2ee6438490577ab54002238ecdf4d6f1449d0c557ca5996eea1b81f6aabbb6aaaa47ad455796b431903
6
+ metadata.gz: 46bdbccf9be3efade25e500f6c67f37de4fc0a0aa8514b55f8eabf7e1171042b5be87d029f37d9cb449c23d1fcf0d5fd2a20c2e8595c6149eff0c20f89e6b3d3
7
+ data.tar.gz: d507a668897c325e2cc771d69b083f9929247b839b55d1fe86bfaeac3ee8be40df43b9f634d4f97c0188fe3bcbfba666456a9131236103455020fb45ecec9d61
data/imap-backup.gemspec CHANGED
@@ -21,10 +21,12 @@ Gem::Specification.new do |gem|
21
21
  gem.required_ruby_version = ">= 3.0"
22
22
 
23
23
  gem.add_runtime_dependency "highline"
24
+ gem.add_runtime_dependency "logger"
24
25
  gem.add_runtime_dependency "mail", "2.7.1"
25
26
  gem.add_runtime_dependency "net-imap", ">= 0.3.2"
26
27
  gem.add_runtime_dependency "net-smtp"
27
28
  gem.add_runtime_dependency "os"
29
+ gem.add_runtime_dependency "ostruct"
28
30
  gem.add_runtime_dependency "rake"
29
31
  gem.add_runtime_dependency "thor", "~> 1.1"
30
32
  gem.add_runtime_dependency "thunderbird", "0.3.0"
@@ -1,9 +1,7 @@
1
1
  require "socket"
2
2
 
3
- require "imap/backup/client/apple_mail"
4
3
  require "imap/backup/client/automatic_login_wrapper"
5
4
  require "imap/backup/client/default"
6
- require "imap/backup/email/provider"
7
5
 
8
6
  module Imap; end
9
7
 
@@ -14,39 +12,17 @@ module Imap::Backup
14
12
  class Account::ClientFactory
15
13
  def initialize(account:)
16
14
  @account = account
17
- @provider = nil
18
- @server = nil
19
15
  end
20
16
 
21
17
  # @return [Client::AutomaticLoginWrapper] a client for the account
22
18
  def run
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
19
+ Logger.logger.debug("Creating IMAP instance")
20
+ client = Client::Default.new(account)
33
21
  Client::AutomaticLoginWrapper.new(client: client)
34
22
  end
35
23
 
36
24
  private
37
25
 
38
26
  attr_reader :account
39
-
40
- def provider
41
- @provider ||= Email::Provider.for_address(account.username)
42
- end
43
-
44
- def provider_options
45
- provider.options.merge(account.connection_options || {})
46
- end
47
-
48
- def server
49
- @server ||= account.server || provider.host
50
- end
51
27
  end
52
28
  end
@@ -38,7 +38,6 @@ module Imap::Backup
38
38
  Logger.logger.debug "Folder '#{name}' exists"
39
39
  true
40
40
  rescue FolderNotFound
41
- Logger.logger.debug "Folder '#{name}' does not exist"
42
41
  false
43
42
  end
44
43
 
@@ -67,7 +66,10 @@ module Imap::Backup
67
66
  def uids
68
67
  Logger.logger.debug "Fetching UIDs for folder '#{name}'"
69
68
  examine
70
- result = client.uid_search(["ALL"]).sort
69
+ result =
70
+ retry_on_error(errors: UID_SEARCH_RETRY_CLASSES) do
71
+ client.uid_search(["ALL"]).sort
72
+ end
71
73
  Logger.logger.debug "#{result.count} UIDs found for folder '#{name}'"
72
74
  result
73
75
  rescue FolderNotFound
@@ -177,6 +179,7 @@ module Imap::Backup
177
179
 
178
180
  BODY_ATTRIBUTE = "BODY[]".freeze
179
181
  UID_FETCH_RETRY_CLASSES = [::EOFError, ::Errno::ECONNRESET, ::IOError].freeze
182
+ UID_SEARCH_RETRY_CLASSES = [::EOFError, ::Errno::ECONNRESET, ::IOError].freeze
180
183
  APPEND_RETRY_CLASSES = [::Net::IMAP::BadResponseError].freeze
181
184
  CREATE_RETRY_CLASSES = [::Net::IMAP::BadResponseError].freeze
182
185
  EXAMINE_RETRY_CLASSES = [::Net::IMAP::BadResponseError].freeze
@@ -185,9 +188,9 @@ module Imap::Backup
185
188
  def examine
186
189
  client.examine(utf7_encoded_name)
187
190
  rescue Net::IMAP::NoResponseError
188
- Imap::Backup::Logger.logger.warn "Folder '#{name}' does not exist on server"
189
- Imap::Backup::Logger.logger.warn caller.join("\n")
190
- raise FolderNotFound, "Folder '#{name}' does not exist on server"
191
+ message = "Folder '#{name}' does not exist on server"
192
+ Imap::Backup::Logger.logger.warn message
193
+ raise FolderNotFound, message
191
194
  end
192
195
 
193
196
  def extract_uid(response)
@@ -211,11 +211,7 @@ module Imap::Backup
211
211
  def multi_fetch_size
212
212
  @multi_fetch_size ||= begin
213
213
  int = @multi_fetch_size_orignal.to_i
214
- if int.positive?
215
- int
216
- else
217
- DEFAULT_MULTI_FETCH_SIZE
218
- end
214
+ int.positive? ? int : DEFAULT_MULTI_FETCH_SIZE
219
215
  end
220
216
  end
221
217
 
@@ -16,6 +16,28 @@ module Imap::Backup
16
16
  options.define_options
17
17
  end
18
18
 
19
+ # @return [String] a description of the namespace configuration
20
+ NAMESPACE_CONFIGURATION_DESCRIPTION = <<~DESC.freeze
21
+ Some IMAP servers use namespaces (i.e. prefixes like "INBOX"),
22
+ while others, while others concatenate the names of subfolders
23
+ with a charater ("delimiter") other than "/".
24
+
25
+ In these cases there are two choices.
26
+
27
+ You can use the `--automatic-namespaces` option.
28
+ This will query the source and detination servers for their
29
+ namespace configuration and will adapt paths accordingly.
30
+ This option requires that both the source and destination
31
+ servers are available and work with the provided parameters
32
+ and authentication.
33
+
34
+ If automatic configuration does not work as desired, there are the
35
+ `--source-prefix=`, `--source-delimiter=`,
36
+ `--destination-prefix=` and `--destination-delimiter=` parameters.
37
+ To check what values you should use, check the output of the
38
+ `imap-backup remote namespaces EMAIL` command.
39
+ DESC
40
+
19
41
  # Processes command-line parameters
20
42
  # @return [Hash] the supplied command-line parameters with
21
43
  # with hyphens in keys replaced by underscores
@@ -0,0 +1,79 @@
1
+ module Imap; end
2
+
3
+ module Imap::Backup
4
+ # Processes parameters to run the `migrate` command via command-line parameters
5
+ module CLI::Migrate
6
+ include Thor::Actions
7
+ include CLI::Helpers
8
+
9
+ LONG_DESCRIPTION = <<~DESC.freeze
10
+ This command is deprecated and will be removed in a future version.
11
+ Use 'copy' instead.
12
+
13
+ All emails which have been backed up for the "source account" (SOURCE_EMAIL) are
14
+ uploaded to the "destination account" (DESTINATION_EMAIL).
15
+
16
+ Some configuration may be necessary, as follows:
17
+
18
+ #{CLI::Helpers::NAMESPACE_CONFIGURATION_DESCRIPTION}
19
+
20
+ Finally, if you want to delete existing emails in destination folders,
21
+ use the `--reset` option. In this case, all existing emails are
22
+ deleted before uploading the migrated emails.
23
+ DESC
24
+
25
+ def self.included(base)
26
+ base.class_eval do
27
+ desc(
28
+ "migrate SOURCE_EMAIL DESTINATION_EMAIL [OPTIONS]",
29
+ "(Deprecated) Uploads backed-up emails from account SOURCE_EMAIL " \
30
+ "to account DESTINATION_EMAIL"
31
+ )
32
+ long_desc LONG_DESCRIPTION
33
+ config_option
34
+ quiet_option
35
+ verbose_option
36
+ method_option(
37
+ "automatic-namespaces",
38
+ type: :boolean,
39
+ desc: "automatically choose delimiters and prefixes"
40
+ )
41
+ method_option(
42
+ "destination-delimiter",
43
+ type: :string,
44
+ desc: "the delimiter for destination folder names"
45
+ )
46
+ method_option(
47
+ "destination-prefix",
48
+ type: :string,
49
+ desc: "the prefix (namespace) to add to destination folder names",
50
+ aliases: ["-d"]
51
+ )
52
+ method_option(
53
+ "reset",
54
+ type: :boolean,
55
+ desc: "DANGER! This option deletes all messages from destination " \
56
+ "folders before uploading",
57
+ aliases: ["-r"]
58
+ )
59
+ method_option(
60
+ "source-delimiter",
61
+ type: :string,
62
+ desc: "the delimiter for source folder names"
63
+ )
64
+ method_option(
65
+ "source-prefix",
66
+ type: :string,
67
+ desc: "the prefix (namespace) to strip from source folder names",
68
+ aliases: ["-s"]
69
+ )
70
+ # Migrates emails from one account to another
71
+ # @return [void]
72
+ def migrate(source_email, destination_email)
73
+ non_logging_options = Imap::Backup::Logger.setup_logging(options)
74
+ CLI::Transfer.new(:migrate, source_email, destination_email, non_logging_options).run
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,79 @@
1
+ module Imap; end
2
+
3
+ module Imap::Backup
4
+ # Processes parameters to run the `mirror` command via command-line parameters
5
+ module CLI::Mirror
6
+ include Thor::Actions
7
+ include CLI::Helpers
8
+
9
+ LONG_DESCRIPTION = <<~DESC.freeze
10
+ This command is deprecated and will be removed in a future version.
11
+ Use 'copy' instead.
12
+
13
+ This command updates the DESTINATION_EMAIL account's folders to have the same contents
14
+ as those on the SOURCE_EMAIL account.
15
+
16
+ If a folder list is configured for the SOURCE_EMAIL account,
17
+ only the folders indicated by the setting are copied.
18
+
19
+ First, it runs the download of the SOURCE_EMAIL account.
20
+ If the SOURCE_EMAIL account is **not** configured to be in 'mirror' mode,
21
+ a warning is printed.
22
+
23
+ When the mirror command is used, for each folder that is processed,
24
+ a new file is created alongside the normal backup files (.imap and .mbox)
25
+ This file has a '.mirror' extension. This file contains a mapping of
26
+ the known UIDs on the source account to those on the destination account.
27
+
28
+ Some configuration may be necessary, as follows:
29
+
30
+ #{CLI::Helpers::NAMESPACE_CONFIGURATION_DESCRIPTION}
31
+ DESC
32
+
33
+ def self.included(base)
34
+ base.class_eval do
35
+ desc(
36
+ "mirror SOURCE_EMAIL DESTINATION_EMAIL [OPTIONS]",
37
+ "(Deprecated) Keeps the DESTINATION_EMAIL account aligned with the SOURCE_EMAIL account"
38
+ )
39
+ long_desc LONG_DESCRIPTION
40
+ config_option
41
+ quiet_option
42
+ verbose_option
43
+ method_option(
44
+ "automatic-namespaces",
45
+ type: :boolean,
46
+ desc: "automatically choose delimiters and prefixes"
47
+ )
48
+ method_option(
49
+ "destination-delimiter",
50
+ type: :string,
51
+ desc: "the delimiter for destination folder names"
52
+ )
53
+ method_option(
54
+ "destination-prefix",
55
+ type: :string,
56
+ desc: "the prefix (namespace) to add to destination folder names",
57
+ aliases: ["-d"]
58
+ )
59
+ method_option(
60
+ "source-delimiter",
61
+ type: :string,
62
+ desc: "the delimiter for source folder names"
63
+ )
64
+ method_option(
65
+ "source-prefix",
66
+ type: :string,
67
+ desc: "the prefix (namespace) to strip from source folder names",
68
+ aliases: ["-s"]
69
+ )
70
+ # Keeps one email account in line with another
71
+ # @return [void]
72
+ def mirror(source_email, destination_email)
73
+ non_logging_options = Imap::Backup::Logger.setup_logging(options)
74
+ CLI::Transfer.new(:mirror, source_email, destination_email, non_logging_options).run
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
@@ -13,7 +13,7 @@ module Imap::Backup
13
13
  include CLI::Helpers
14
14
 
15
15
  # The possible values for the action parameter
16
- ACTIONS = %i(migrate mirror).freeze
16
+ ACTIONS = %i(copy migrate mirror).freeze
17
17
 
18
18
  def initialize(action, source_email, destination_email, options)
19
19
  @action = action
@@ -39,14 +39,17 @@ module Imap::Backup
39
39
  raise "Unknown action '#{action}'" if !ACTIONS.include?(action)
40
40
 
41
41
  process_options!
42
- prepare_mirror if action == :mirror
42
+ warn_if_source_account_is_not_in_mirror_mode if action == :mirror
43
+ run_backup if %i(copy mirror).include?(action)
43
44
 
44
45
  folders.each do |serializer, folder|
45
46
  case action
47
+ when :copy
48
+ Mirror.new(serializer, folder, reset: false).run
46
49
  when :migrate
47
50
  Migrator.new(serializer, folder, reset: reset).run
48
51
  when :mirror
49
- Mirror.new(serializer, folder).run
52
+ Mirror.new(serializer, folder, reset: true).run
50
53
  end
51
54
  end
52
55
  end
@@ -123,9 +126,7 @@ module Imap::Backup
123
126
  self.source_prefix ||= ""
124
127
  end
125
128
 
126
- def prepare_mirror
127
- warn_if_source_account_is_not_in_mirror_mode
128
-
129
+ def run_backup
129
130
  CLI::Backup.new(config: config_path, accounts: source_email).run
130
131
  end
131
132
 
@@ -12,6 +12,8 @@ module Imap::Backup
12
12
 
13
13
  autoload :Backup, "imap/backup/cli/backup"
14
14
  autoload :Local, "imap/backup/cli/local"
15
+ autoload :Migrate, "imap/backup/cli/migrate"
16
+ autoload :Mirror, "imap/backup/cli/mirror"
15
17
  autoload :Remote, "imap/backup/cli/remote"
16
18
  autoload :Restore, "imap/backup/cli/restore"
17
19
  autoload :Setup, "imap/backup/cli/setup"
@@ -22,28 +24,6 @@ module Imap::Backup
22
24
 
23
25
  include Helpers
24
26
 
25
- NAMESPACE_CONFIGURATION_DESCRIPTION = <<~DESC.freeze
26
- Some IMAP servers use namespaces (i.e. prefixes like "INBOX"),
27
- while others, while others concatenate the names of subfolders
28
- with a charater ("delimiter") other than "/".
29
-
30
- In these cases there are two choices.
31
-
32
- You can use the `--automatic-namespaces` option.
33
- This will query the source and detination servers for their
34
- namespace configuration and will adapt paths accordingly.
35
- This option requires that both the source and destination
36
- servers are available and work with the provided parameters
37
- and authentication.
38
-
39
- If automatic configuration does not work as desired, there are the
40
- `--source-prefix=`, `--source-delimiter=`,
41
- `--destination-prefix=` and `--destination-delimiter=` parameters.
42
- To check what values you should use, check the output of the
43
- `imap-backup remote namespaces EMAIL` command.
44
- DESC
45
- private_constant :NAMESPACE_CONFIGURATION_DESCRIPTION
46
-
47
27
  default_task :backup
48
28
 
49
29
  # Overrides {https://www.rubydoc.info/gems/thor/Thor%2FBase%2FClassMethods:start Thor's method}
@@ -94,91 +74,31 @@ module Imap::Backup
94
74
  Backup.new(non_logging_options).run
95
75
  end
96
76
 
97
- desc "local SUBCOMMAND [OPTIONS]", "View local info"
98
- subcommand "local", Local
99
-
100
77
  desc(
101
- "migrate SOURCE_EMAIL DESTINATION_EMAIL [OPTIONS]",
102
- "Uploads backed-up emails from account SOURCE_EMAIL to account DESTINATION_EMAIL"
78
+ "copy SOURCE_EMAIL DESTINATION_EMAIL [OPTIONS]",
79
+ "Copies emails from the SOURCE account to the DESTINATION account, avoiding duplicates"
103
80
  )
104
81
  long_desc <<~DESC
105
- All emails which have been backed up for the "source account" (SOURCE_EMAIL) are
106
- uploaded to the "destination account" (DESTINATION_EMAIL).
107
-
108
- Some configuration may be necessary, as follows:
109
-
110
- #{NAMESPACE_CONFIGURATION_DESCRIPTION}
111
-
112
- Finally, if you want to delete existing emails in destination folders,
113
- use the `--reset` option. In this case, all existing emails are
114
- deleted before uploading the migrated emails.
115
- DESC
116
- config_option
117
- quiet_option
118
- verbose_option
119
- method_option(
120
- "automatic-namespaces",
121
- type: :boolean,
122
- desc: "automatically choose delimiters and prefixes"
123
- )
124
- method_option(
125
- "destination-delimiter",
126
- type: :string,
127
- desc: "the delimiter for destination folder names"
128
- )
129
- method_option(
130
- "destination-prefix",
131
- type: :string,
132
- desc: "the prefix (namespace) to add to destination folder names",
133
- aliases: ["-d"]
134
- )
135
- method_option(
136
- "reset",
137
- type: :boolean,
138
- desc: "DANGER! This option deletes all messages from destination folders before uploading",
139
- aliases: ["-r"]
140
- )
141
- method_option(
142
- "source-delimiter",
143
- type: :string,
144
- desc: "the delimiter for source folder names"
145
- )
146
- method_option(
147
- "source-prefix",
148
- type: :string,
149
- desc: "the prefix (namespace) to strip from source folder names",
150
- aliases: ["-s"]
151
- )
152
- # Migrates emails from one account to another
153
- # @return [void]
154
- def migrate(source_email, destination_email)
155
- non_logging_options = Imap::Backup::Logger.setup_logging(options)
156
- Transfer.new(:migrate, source_email, destination_email, non_logging_options).run
157
- end
82
+ This command copies messages from the SOURCE_EMAIL account
83
+ to the DESTINATION_EMAIL account. It keeps track of copied
84
+ messages and avoids duplicate copies.
158
85
 
159
- desc(
160
- "mirror SOURCE_EMAIL DESTINATION_EMAIL [OPTIONS]",
161
- "Keeps the DESTINATION_EMAIL account aligned with the SOURCE_EMAIL account"
162
- )
163
- long_desc <<~DESC
164
- This command updates the DESTINATION_EMAIL account's folders to have the same contents
165
- as those on the SOURCE_EMAIL account.
86
+ Any other messages that are present on the DESTINATION_EMAIL account
87
+ are not affected.
166
88
 
167
89
  If a folder list is configured for the SOURCE_EMAIL account,
168
90
  only the folders indicated by the setting are copied.
169
91
 
170
92
  First, it runs the download of the SOURCE_EMAIL account.
171
- If the SOURCE_EMAIL account is **not** configured to be in 'mirror' mode,
172
- a warning is printed.
173
93
 
174
- When the mirror command is used, for each folder that is processed,
94
+ When the copy command is used, for each folder that is processed,
175
95
  a new file is created alongside the normal backup files (.imap and .mbox)
176
96
  This file has a '.mirror' extension. This file contains a mapping of
177
97
  the known UIDs on the source account to those on the destination account.
178
98
 
179
99
  Some configuration may be necessary, as follows:
180
100
 
181
- #{NAMESPACE_CONFIGURATION_DESCRIPTION}
101
+ #{Helpers::NAMESPACE_CONFIGURATION_DESCRIPTION}
182
102
  DESC
183
103
  config_option
184
104
  quiet_option
@@ -210,13 +130,19 @@ module Imap::Backup
210
130
  desc: "the prefix (namespace) to strip from source folder names",
211
131
  aliases: ["-s"]
212
132
  )
213
- # Keeps one email account in line with another
133
+ # Copies messages from one email account to another
214
134
  # @return [void]
215
- def mirror(source_email, destination_email)
135
+ def copy(source_email, destination_email)
216
136
  non_logging_options = Imap::Backup::Logger.setup_logging(options)
217
- Transfer.new(:mirror, source_email, destination_email, non_logging_options).run
137
+ Transfer.new(:copy, source_email, destination_email, non_logging_options).run
218
138
  end
219
139
 
140
+ desc "local SUBCOMMAND [OPTIONS]", "View local info"
141
+ subcommand "local", Local
142
+
143
+ include Migrate
144
+ include Mirror
145
+
220
146
  desc "remote SUBCOMMAND [OPTIONS]", "View info about online accounts"
221
147
  subcommand "remote", Remote
222
148
 
@@ -1,6 +1,7 @@
1
1
  require "forwardable"
2
2
  require "net/imap"
3
3
 
4
+ require "imap/backup/email/provider"
4
5
  require "imap/backup/logger"
5
6
 
6
7
  module Imap; end
@@ -17,10 +18,8 @@ module Imap::Backup
17
18
  responses uid_fetch uid_search uid_store
18
19
  )
19
20
 
20
- def initialize(server, account, options)
21
+ def initialize(account)
21
22
  @account = account
22
- @options = options
23
- @server = server
24
23
  @state = nil
25
24
  end
26
25
 
@@ -32,7 +31,10 @@ module Imap::Backup
32
31
 
33
32
  return [] if mailbox_lists.nil?
34
33
 
35
- mailbox_lists.map { |ml| extract_name(ml) }
34
+ ignored_tags = provider.folder_ignore_tags
35
+ mailbox_lists.
36
+ select { |ml| ml.attr & ignored_tags == [] }.
37
+ map { |ml| extract_name(ml) }
36
38
  end
37
39
 
38
40
  # Logs in to the account on the IMAP server
@@ -83,8 +85,6 @@ module Imap::Backup
83
85
  private
84
86
 
85
87
  attr_reader :account
86
- attr_reader :options
87
- attr_reader :server
88
88
  attr_accessor :state
89
89
 
90
90
  def imap
@@ -100,17 +100,36 @@ module Imap::Backup
100
100
  account.password.gsub(/./, "x")
101
101
  end
102
102
 
103
+ def provider
104
+ @provider ||= Email::Provider.for_address(account.username)
105
+ end
106
+
107
+ def options
108
+ @options ||= provider.options.merge(account.connection_options || {})
109
+ end
110
+
111
+ def server
112
+ @server ||= account.server || provider.host
113
+ end
114
+
103
115
  # 6.3.8. LIST Command
104
116
  # An empty ("" string) mailbox name argument is a special request to
105
117
  # return the hierarchy delimiter and the root name of the name given
106
118
  # in the reference.
107
119
  def provider_root
108
- @provider_root ||= begin
109
- Logger.logger.debug "Fetching provider root"
110
- root_info = imap.list("", "")[0]
111
- Logger.logger.debug "Provider root is '#{root_info.name}'"
112
- root_info.name
113
- end
120
+ @provider_root ||=
121
+ if provider.root
122
+ Logger.logger.debug "Using fixed provider root '#{provider.root}'"
123
+ provider.root
124
+ else
125
+ Logger.logger.debug "Fetching provider root"
126
+ result = imap.list("", "")
127
+ raise "IMAP server did not return root folder for #{account.username}" if result.empty?
128
+
129
+ root_info = result[0]
130
+ Logger.logger.debug "Provider root is '#{root_info.name}'"
131
+ root_info.name
132
+ end
114
133
  end
115
134
  end
116
135
  end
@@ -13,13 +13,15 @@ module Imap::Backup
13
13
  @serializer = serializer
14
14
  @multi_fetch_size = multi_fetch_size
15
15
  @reset_seen_flags_after_fetch = reset_seen_flags_after_fetch
16
+ @folder_uids = nil
17
+ @serializer_uids = nil
16
18
  @uids = nil
17
19
  end
18
20
 
19
21
  # Runs the downloader
20
22
  # @return [void]
21
23
  def run
22
- debug("#{serializer_uids.count} already messages already downloaded")
24
+ debug("#{serializer_uids.count} messages already downloaded")
23
25
  debug("#{folder_uids.count} messages on server")
24
26
  local_only_count = (serializer_uids - folder_uids).count
25
27
  if local_only_count.positive?
@@ -33,15 +35,7 @@ module Imap::Backup
33
35
 
34
36
  info("#{uids.count} new messages")
35
37
 
36
- uids.each_slice(multi_fetch_size).with_index do |block, i|
37
- multifetch_failed = download_block(block, i)
38
- raise MultiFetchFailedError if multifetch_failed
39
- end
40
- rescue MultiFetchFailedError
41
- @count = nil
42
- @multi_fetch_size = 1
43
- @uids = nil
44
- retry
38
+ download
45
39
  rescue Net::IMAP::ByeResponseError
46
40
  folder.client.reconnect
47
41
  retry
@@ -54,6 +48,21 @@ module Imap::Backup
54
48
  attr_reader :multi_fetch_size
55
49
  attr_reader :reset_seen_flags_after_fetch
56
50
 
51
+ def download
52
+ block_count = (uids.count / multi_fetch_size.to_f).ceil
53
+ uids.each_slice(multi_fetch_size).with_index do |block, i|
54
+ debug("Downloading #{block.count} messages (block #{i + 1}/#{block_count})")
55
+ multifetch_failed = download_block(block, i)
56
+ raise MultiFetchFailedError if multifetch_failed
57
+ end
58
+ rescue MultiFetchFailedError
59
+ @multi_fetch_size = 1
60
+ @uids = nil
61
+ @folder_uids = nil
62
+ @serializer_uids = nil
63
+ retry
64
+ end
65
+
57
66
  def download_block(block, index)
58
67
  uids_and_bodies =
59
68
  if reset_seen_flags_after_fetch
@@ -10,6 +10,11 @@ module Imap::Backup
10
10
  "imap.mail.me.com"
11
11
  end
12
12
 
13
+ # With Apple Mails's IMAP, passing "/" to list results in an empty list
14
+ def root
15
+ ""
16
+ end
17
+
13
18
  def sets_seen_flags_on_fetch?
14
19
  true
15
20
  end
@@ -6,11 +6,22 @@ module Imap::Backup
6
6
 
7
7
  # Supplies defaults for email provider behaviour
8
8
  class Email::Provider::Base
9
+ # @return [Array<Symbol>] tags to ignore when listing folders
10
+ def folder_ignore_tags
11
+ []
12
+ end
13
+
9
14
  # @return [Hash] defaults for the Net::IMAP connection
10
15
  def options
11
16
  {port: 993, ssl: {min_version: OpenSSL::SSL::TLS1_2_VERSION}}
12
17
  end
13
18
 
19
+ # By default, we query the server for this value.
20
+ # It is only fixed for Apple Mail accounts.
21
+ # @return [String, nil] any fixed value to use when requesting the list of account folders
22
+ def root
23
+ end
24
+
14
25
  def sets_seen_flags_on_fetch?
15
26
  false
16
27
  end
@@ -5,6 +5,11 @@ module Imap; end
5
5
  module Imap::Backup
6
6
  # Provides overrides for GMail accounts
7
7
  class Email::Provider::GMail < Email::Provider::Base
8
+ # https://imap-use.u.washington.narkive.com/RYMsOHTN/imap-protocol-status-on-a-noselect-mailbox
9
+ def folder_ignore_tags
10
+ [:Noselect]
11
+ end
12
+
8
13
  # @return [String] the GMail IMAP server host name
9
14
  def host
10
15
  "imap.gmail.com"
@@ -5,9 +5,10 @@ module Imap; end
5
5
  module Imap::Backup
6
6
  # Synchronises a folder between a source and destination
7
7
  class Mirror
8
- def initialize(serializer, folder)
8
+ def initialize(serializer, folder, reset: false)
9
9
  @serializer = serializer
10
10
  @folder = folder
11
+ @reset = reset
11
12
  end
12
13
 
13
14
  # If necessary, reates the destination folder,
@@ -19,7 +20,7 @@ module Imap::Backup
19
20
  # @return [void]
20
21
  def run
21
22
  ensure_destination_folder
22
- delete_destination_only_emails
23
+ delete_destination_only_emails if reset
23
24
  update_flags
24
25
  append_emails
25
26
  map.save
@@ -31,6 +32,7 @@ module Imap::Backup
31
32
 
32
33
  attr_reader :serializer
33
34
  attr_reader :folder
35
+ attr_reader :reset
34
36
 
35
37
  def ensure_destination_folder
36
38
  return if folder.exist?
@@ -90,7 +92,7 @@ module Imap::Backup
90
92
  destination: folder.uid_validity
91
93
  )
92
94
  if !map_ok
93
- folder.clear
95
+ folder.clear if reset
94
96
  map.reset(
95
97
  source_uid_validity: serializer.uid_validity,
96
98
  destination_uid_validity: folder.uid_validity
@@ -2,11 +2,11 @@ module Imap; end
2
2
 
3
3
  module Imap::Backup
4
4
  # @private
5
- MAJOR = 15
5
+ MAJOR = 16
6
6
  # @private
7
7
  MINOR = 1
8
8
  # @private
9
- REVISION = 3
9
+ REVISION = 0
10
10
  # @private
11
11
  PRE = nil
12
12
  # The application version
metadata CHANGED
@@ -1,13 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: imap-backup
3
3
  version: !ruby/object:Gem::Version
4
- version: 15.1.3
4
+ version: 16.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Joe Yates
8
+ autorequire:
8
9
  bindir: bin
9
10
  cert_chain: []
10
- date: 1980-01-02 00:00:00.000000000 Z
11
+ date: 2025-08-02 00:00:00.000000000 Z
11
12
  dependencies:
12
13
  - !ruby/object:Gem::Dependency
13
14
  name: highline
@@ -23,6 +24,20 @@ dependencies:
23
24
  - - ">="
24
25
  - !ruby/object:Gem::Version
25
26
  version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: logger
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
26
41
  - !ruby/object:Gem::Dependency
27
42
  name: mail
28
43
  requirement: !ruby/object:Gem::Requirement
@@ -79,6 +94,20 @@ dependencies:
79
94
  - - ">="
80
95
  - !ruby/object:Gem::Version
81
96
  version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: ostruct
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :runtime
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
82
111
  - !ruby/object:Gem::Dependency
83
112
  name: rake
84
113
  requirement: !ruby/object:Gem::Requirement
@@ -152,6 +181,8 @@ files:
152
181
  - lib/imap/backup/cli/helpers.rb
153
182
  - lib/imap/backup/cli/local.rb
154
183
  - lib/imap/backup/cli/local/check.rb
184
+ - lib/imap/backup/cli/migrate.rb
185
+ - lib/imap/backup/cli/mirror.rb
155
186
  - lib/imap/backup/cli/options.rb
156
187
  - lib/imap/backup/cli/remote.rb
157
188
  - lib/imap/backup/cli/restore.rb
@@ -161,7 +192,6 @@ files:
161
192
  - lib/imap/backup/cli/stats.rb
162
193
  - lib/imap/backup/cli/transfer.rb
163
194
  - lib/imap/backup/cli/utils.rb
164
- - lib/imap/backup/client/apple_mail.rb
165
195
  - lib/imap/backup/client/automatic_login_wrapper.rb
166
196
  - lib/imap/backup/client/default.rb
167
197
  - lib/imap/backup/configuration.rb
@@ -218,6 +248,7 @@ licenses:
218
248
  - MIT
219
249
  metadata:
220
250
  rubygems_mfa_required: 'true'
251
+ post_install_message:
221
252
  rdoc_options: []
222
253
  require_paths:
223
254
  - lib
@@ -232,7 +263,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
232
263
  - !ruby/object:Gem::Version
233
264
  version: '0'
234
265
  requirements: []
235
- rubygems_version: 3.6.7
266
+ rubygems_version: 3.2.33
267
+ signing_key:
236
268
  specification_version: 4
237
269
  summary: Backup GMail (or other IMAP) accounts to disk
238
270
  test_files: []
@@ -1,15 +0,0 @@
1
- require "imap/backup/client/default"
2
-
3
- module Imap; end
4
-
5
- module Imap::Backup
6
- # Overrides default IMAP client behaviour for Apple Mail accounts
7
- class Client::AppleMail < Client::Default
8
- # With Apple Mails's IMAP, passing "/" to list
9
- # results in an empty list
10
- # @return [String] the value to use when requesting the list of account folders
11
- def provider_root
12
- ""
13
- end
14
- end
15
- end