imap-backup 16.4.1 → 16.4.2

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: f3ffe8d1bd32587d59e12a514b005d68175244e2cb73a9d138dd3a5d273c785f
4
- data.tar.gz: 62d6364641799f1ce20466affc34052f8614a2a2d5633f53a05b4cac31a4e0f3
3
+ metadata.gz: fc471fce2bd514e0eee4cec922e8f368df0b2fb99f8b89b2d852e07799a2be77
4
+ data.tar.gz: fd563c2ee648d5e384a10cdfc79a7d1d5be128bdce5908711396d414c801173f
5
5
  SHA512:
6
- metadata.gz: f4fdd4254948ef5dd7fba17e3683aa13bf84a19a77ffd935bad3f830355d4bf0d14ff48c731be2542ce56332ed8b4903b1b7971a16e7e947f4406ba4473b4a44
7
- data.tar.gz: 4ca3ca9bb9eda64a9b8b9a6cc0889e5815f446e2a798f762f1d55e228928e99988c186bdaad4dfd7d033cd21667de7d906ea3cbd1f11d947dc77d68f4bbd1090
6
+ metadata.gz: 36088caab383a7ff2528d8ad83813f58918abda4d80a56b0f2b2f1f45d5fc840934d1bc31e1907de11cc665f5aecb5b12152bb8dee686fb656294eb55440897e
7
+ data.tar.gz: 389a82b7755342dd591fd0e2d45d760eb8350f697d46414d8d90776624bd0ee7263acfe163ab1e8dd42c869d12a544c2d5faa4656d4ef5c802f4991b9f33c1b4
data/imap-backup.gemspec CHANGED
@@ -30,7 +30,7 @@ Gem::Specification.new do |gem|
30
30
  gem.add_dependency "rake"
31
31
  gem.add_dependency "sys-proctable"
32
32
  gem.add_dependency "thor", "~> 1.1"
33
- gem.add_dependency "thunderbird", "0.3.0"
33
+ gem.add_dependency "thunderbird", "~> 0.6.0"
34
34
 
35
35
  gem.metadata = {
36
36
  "rubygems_mfa_required" => "true"
@@ -1,6 +1,5 @@
1
1
  require "imap/backup/account/backup_folders"
2
2
  require "imap/backup/account/folder_backup"
3
- require "imap/backup/account/folder_ensurer"
4
3
  require "imap/backup/account/local_only_folder_deleter"
5
4
  require "imap/backup/account/locker"
6
5
 
@@ -25,7 +24,7 @@ module Imap::Backup
25
24
  # start the connection so we get logging messages in the right order
26
25
  account.client.login
27
26
 
28
- ensure_folder
27
+ ensure_directory
29
28
  delete_local_only_folders if account.mirror_mode
30
29
 
31
30
  if backup_folders.none?
@@ -53,8 +52,8 @@ module Imap::Backup
53
52
  Account::LocalOnlyFolderDeleter.new(account: account).run
54
53
  end
55
54
 
56
- def ensure_folder
57
- Account::FolderEnsurer.new(account: account).run
55
+ def ensure_directory
56
+ Serializer::DirectoryMaker.new(files_path: account.files_path).run
58
57
  end
59
58
 
60
59
  def locker
@@ -4,6 +4,7 @@ require "imap/backup/flag_refresher"
4
4
  require "imap/backup/local_only_message_deleter"
5
5
  require "imap/backup/logger"
6
6
  require "imap/backup/serializer"
7
+ require "imap/backup/serializer/files/path"
7
8
 
8
9
  module Imap; end
9
10
 
@@ -86,7 +87,12 @@ module Imap::Backup
86
87
  end
87
88
 
88
89
  def raw_serializer
89
- @raw_serializer ||= Serializer.new(account.local_path, folder.name)
90
+ @raw_serializer ||= begin
91
+ path = Serializer::Files::Path.new(
92
+ base_path: account.local_path, folder_name: folder.name
93
+ )
94
+ Serializer.new(files_path: path)
95
+ end
90
96
  end
91
97
  end
92
98
  end
@@ -3,6 +3,7 @@ require "pathname"
3
3
 
4
4
  require "imap/backup/account/folder"
5
5
  require "imap/backup/serializer"
6
+ require "imap/backup/serializer/files/path"
6
7
 
7
8
  module Imap; end
8
9
 
@@ -44,7 +45,10 @@ module Imap::Backup
44
45
  glob = File.join(source_local_path, "**", "*.imap")
45
46
  Pathname.glob(glob) do |path|
46
47
  name = source_folder_name(path)
47
- serializer = Serializer.new(source_local_path, name)
48
+ path = Serializer::Files::Path.new(
49
+ base_path: source_local_path, folder_name: name
50
+ )
51
+ serializer = Serializer.new(files_path: path)
48
52
  folder = destination_folder_for_path(name)
49
53
  block.call(serializer, folder)
50
54
  end
@@ -1,4 +1,3 @@
1
- require "imap/backup/account/folder_ensurer"
2
1
  require "imap/backup/lockfile"
3
2
 
4
3
  module Imap; end
@@ -25,7 +24,7 @@ module Imap::Backup
25
24
  Logger.logger.info("Stale lockfile '#{account.lockfile_path}' found. Removing it.")
26
25
  lockfile.remove
27
26
  else
28
- Account::FolderEnsurer.new(account: account).run
27
+ Serializer::DirectoryMaker.new(files_path: account.files_path).run
29
28
  end
30
29
 
31
30
  begin
@@ -1,8 +1,9 @@
1
1
  require "pathname"
2
2
 
3
3
  require "imap/backup/account/folder"
4
- require "imap/backup/account/folder_ensurer"
5
4
  require "imap/backup/serializer"
5
+ require "imap/backup/serializer/directory_maker"
6
+ require "imap/backup/serializer/files/path"
6
7
 
7
8
  module Imap; end
8
9
 
@@ -27,7 +28,10 @@ module Imap::Backup
27
28
 
28
29
  glob.each do |path|
29
30
  name = path.relative_path_from(base).to_s[0..-6]
30
- serializer = Serializer.new(account.local_path, name)
31
+ files_path = Serializer::Files::Path.new(
32
+ base_path: account.local_path, folder_name: name
33
+ )
34
+ serializer = Serializer.new(files_path: files_path)
31
35
  folder = Account::Folder.new(account.client, name)
32
36
  block.call(serializer, folder)
33
37
  end
@@ -41,7 +45,10 @@ module Imap::Backup
41
45
 
42
46
  glob.each do |path|
43
47
  name = path.relative_path_from(base).to_s[0..-6]
44
- serializer = Serializer.new(account.local_path, name)
48
+ files_path = Serializer::Files::Path.new(
49
+ base_path: account.local_path, folder_name: name
50
+ )
51
+ serializer = Serializer.new(files_path: files_path)
45
52
  block.call(serializer)
46
53
  end
47
54
  end
@@ -69,7 +76,10 @@ module Imap::Backup
69
76
 
70
77
  def glob
71
78
  @glob ||= begin
72
- Account::FolderEnsurer.new(account: account).run
79
+ files_path = Serializer::Files::Path.new(
80
+ base_path: account.local_path, folder_name: nil
81
+ )
82
+ Serializer::DirectoryMaker.new(files_path: files_path).run
73
83
 
74
84
  pattern = File.join(account.local_path, "**", "*.imap")
75
85
  Pathname.glob(pattern)
@@ -4,7 +4,6 @@ require "imap/backup/account/client_factory"
4
4
 
5
5
  module Imap; end
6
6
 
7
- # rubocop:disable Metrics/ClassLength
8
7
  module Imap::Backup
9
8
  # Contains the attributes relating to an email account.
10
9
  class Account
@@ -188,6 +187,11 @@ module Imap::Backup
188
187
  update(:local_path, value)
189
188
  end
190
189
 
190
+ # @return [Serializer::Files::Path] the base files path for the account
191
+ def files_path
192
+ Serializer::Files::Path.new(base_path: local_path, folder_name: nil)
193
+ end
194
+
191
195
  # @raise [RuntimeError] if the local_path is not set
192
196
  # @return [String] the path to the lockfile for the account
193
197
  def lockfile_path
@@ -3,6 +3,7 @@ require "thor"
3
3
  require "imap/backup/account/backup_folders"
4
4
  require "imap/backup/cli/helpers"
5
5
  require "imap/backup/serializer"
6
+ require "imap/backup/serializer/files/path"
6
7
 
7
8
  module Imap; end
8
9
 
@@ -63,7 +64,10 @@ module Imap::Backup
63
64
  backup_folders.map do |folder|
64
65
  next if !folder.exist?
65
66
 
66
- serializer = Serializer.new(account.local_path, folder.name)
67
+ path = Serializer::Files::Path.new(
68
+ base_path: account.local_path, folder_name: folder.name
69
+ )
70
+ serializer = Serializer.new(files_path: path)
67
71
  local_uids = serializer.uids
68
72
  Logger.logger.debug("[Stats] fetching email list for '#{folder.name}'")
69
73
  remote_uids = folder.uids
@@ -7,6 +7,7 @@ require "imap/backup/cli/helpers"
7
7
  require "imap/backup/logger"
8
8
  require "imap/backup/serializer"
9
9
  require "imap/backup/thunderbird/mailbox_exporter"
10
+ require "imap/backup/serializer/files/path"
10
11
 
11
12
  module Imap; end
12
13
 
@@ -103,7 +104,10 @@ module Imap::Backup
103
104
  backup_folders.each do |folder|
104
105
  next if !folder.exist?
105
106
 
106
- serializer = Serializer.new(account.local_path, folder.name)
107
+ path = Serializer::Files::Path.new(
108
+ base_path: account.local_path, folder_name: folder.name
109
+ )
110
+ serializer = Serializer.new(files_path: path)
107
111
  ignore_folder_history(folder, serializer)
108
112
  end
109
113
  end
@@ -126,18 +130,41 @@ module Imap::Backup
126
130
  end
127
131
 
128
132
  def thunderbird_profile(name = nil)
133
+ Logger.logger.info("[CLI::Utils] Fetching Thunderbird profile")
129
134
  profiles = ::Thunderbird::Profiles.new
135
+ Logger.logger.debug("[CLI::Utils] Found #{profiles.installs.count} Thunderbird install(s)")
130
136
  if name
137
+ Logger.logger.debug("[CLI::Utils] Using profile name '#{name}'")
131
138
  profiles.profile(name)
132
139
  else
133
- if profiles.installs.count > 1
134
- raise <<~MESSAGE
135
- Thunderbird has multiple installs, so no default profile exists.
136
- Please supply a profile name
137
- MESSAGE
138
- end
139
-
140
- profiles.installs[0].default
140
+ choose_default_profile(profiles)
141
+ end
142
+ end
143
+
144
+ def choose_default_profile(profiles)
145
+ Logger.logger.debug("[CLI::Utils] No profile name supplied, so looking for default profile")
146
+ case profiles.installs.count
147
+ when 0
148
+ raise "No Thunderbird installs found, so no default profile exists"
149
+ when 1
150
+ install = profiles.installs.first
151
+ Logger.logger.debug(
152
+ "[CLI::Utils] Only one Thunderbird install found '#{install.title}', " \
153
+ "so using its default profile"
154
+ )
155
+
156
+ profile = install.default_profile
157
+ raise "Thunderbird install '#{install.title}' does not have a default profile" if !profile
158
+
159
+ Logger.logger.debug(
160
+ "[CLI::Utils] Default profile '#{profile.title}' has path '#{profile.root}'"
161
+ )
162
+ profile
163
+ else
164
+ raise <<~MESSAGE
165
+ Thunderbird has multiple installs, so no default profile exists.
166
+ Please supply a profile name
167
+ MESSAGE
141
168
  end
142
169
  end
143
170
  end
@@ -106,7 +106,7 @@ module Imap::Backup
106
106
  end
107
107
 
108
108
  def map_pathname
109
- "#{serializer.folder_path}.mirror"
109
+ "#{serializer.files_path}.mirror"
110
110
  end
111
111
 
112
112
  def destination_email
@@ -7,13 +7,9 @@ module Imap::Backup
7
7
 
8
8
  # Appends messages to the local store
9
9
  class Serializer::Appender
10
- # @param folder [String] the name of the folder
11
- # @param imap [Serializer::Imap] the metadata serializer for the folder
12
- # @param mbox [Serializer::Mbox] the folder's mailbox
13
- def initialize(folder:, imap:, mbox:)
14
- @folder = folder
15
- @imap = imap
16
- @mbox = mbox
10
+ # @param files [Serializer::Files] the folder's files
11
+ def initialize(files:)
12
+ @files = files
17
13
  end
18
14
 
19
15
  # Adds a message to the metadata file and the mailbox.
@@ -31,7 +27,7 @@ module Imap::Backup
31
27
  existing = imap.get(uid)
32
28
  if existing
33
29
  Logger.logger.debug(
34
- "[#{folder}] message #{uid} already downloaded - skipping"
30
+ "[#{files.files_path}] message #{uid} already downloaded - skipping"
35
31
  )
36
32
  return
37
33
  end
@@ -42,7 +38,7 @@ module Imap::Backup
42
38
  raise wrap_error(
43
39
  error: e,
44
40
  note: "failed to serialize message",
45
- folder: folder,
41
+ files_path: files.files_path,
46
42
  uid: uid,
47
43
  message: message
48
44
  )
@@ -55,7 +51,7 @@ module Imap::Backup
55
51
  raise wrap_error(
56
52
  error: e,
57
53
  note: "failed to append message",
58
- folder: folder,
54
+ files_path: files.files_path,
59
55
  uid: uid,
60
56
  message: message
61
57
  )
@@ -64,13 +60,19 @@ module Imap::Backup
64
60
 
65
61
  private
66
62
 
67
- attr_reader :imap
68
- attr_reader :folder
69
- attr_reader :mbox
63
+ attr_reader :files
70
64
 
71
- def wrap_error(error:, note:, folder:, uid:, message:)
65
+ def imap
66
+ files.imap
67
+ end
68
+
69
+ def mbox
70
+ files.mbox
71
+ end
72
+
73
+ def wrap_error(error:, note:, files_path:, uid:, message:)
72
74
  <<-ERROR.gsub(/^\s*/m, "")
73
- [#{folder}] #{note} #{uid}: #{message}.
75
+ [#{files_path}] #{note} #{uid}: #{message}.
74
76
  #{error}:
75
77
  #{error.backtrace.join("\n")}"
76
78
  ERROR
@@ -104,11 +104,11 @@ module Imap::Backup
104
104
  end
105
105
 
106
106
  def mbox
107
- @mbox ||= Serializer::Mbox.new(serializer.folder_path)
107
+ @mbox ||= Serializer::Mbox.new(files_path: serializer.files_path)
108
108
  end
109
109
 
110
110
  def imap
111
- @imap ||= Serializer::Imap.new(serializer.folder_path)
111
+ @imap ||= Serializer::Imap.new(files_path: serializer.files_path)
112
112
  end
113
113
 
114
114
  def tsx
@@ -0,0 +1,43 @@
1
+ require "fileutils"
2
+ require "os"
3
+
4
+ require "imap/backup/serializer/files/path"
5
+
6
+ module Imap; end
7
+
8
+ module Imap::Backup
9
+ class Serializer; end
10
+
11
+ # Creates any directories needed to store backups
12
+ class Serializer::DirectoryMaker
13
+ # @param files_path [Serializer::Files::Path] the folder path components
14
+ # @param permissions [Integer] The permissions to set on the folder
15
+ def initialize(files_path:)
16
+ @files_path = files_path
17
+ end
18
+
19
+ # Creates the containing directory and any missing parent directories,
20
+ # ensuring the required permissions (except on Windows).
21
+ # @return [void]
22
+ def run
23
+ directory = files_path.directory
24
+ FileUtils.mkdir_p(directory)
25
+
26
+ return if windows?
27
+
28
+ FileUtils.chmod DIRECTORY_PERMISSIONS, directory
29
+ end
30
+
31
+ private
32
+
33
+ attr_reader :files_path
34
+
35
+ # The desired permissions for all directories that store backups
36
+ DIRECTORY_PERMISSIONS = 0o700
37
+ private_constant :DIRECTORY_PERMISSIONS
38
+
39
+ def windows?
40
+ OS.windows?
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,27 @@
1
+ module Imap; end
2
+
3
+ module Imap::Backup
4
+ class Serializer; end
5
+ class Serializer::Files; end
6
+
7
+ # A Data class representing the path to a folder's serialized data
8
+ # it contains two elements: the base path and the folder name.
9
+ # The base path is the root path for an account's backup,
10
+ # and the folder name is the name of the folder within that account.
11
+ # If the folder name is nil, the path represents the base path only.
12
+ Serializer::Files::Path = Data.define(:base_path, :folder_name) do
13
+ # @return [String] the full path to the folder's serialized data
14
+ def to_s
15
+ File.join(base_path, folder_name)
16
+ end
17
+
18
+ # @return [String] the directory containing the folder's serialized data
19
+ def directory
20
+ if folder_name
21
+ File.dirname(to_s)
22
+ else
23
+ base_path
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,167 @@
1
+ require "imap/backup/naming"
2
+ require "imap/backup/serializer/directory_maker"
3
+ require "imap/backup/serializer/imap"
4
+ require "imap/backup/serializer/integrity_checker"
5
+ require "imap/backup/serializer/mbox"
6
+ require "imap/backup/serializer/files/path"
7
+
8
+ module Imap; end
9
+
10
+ module Imap::Backup
11
+ # Provides memoized file helpers for Serializer
12
+ class Serializer::Files
13
+ extend Forwardable
14
+
15
+ def_delegator :imap, :update
16
+
17
+ attr_reader :files_path
18
+
19
+ # @param files_path [Serializer::Files::Path] the path of the folder
20
+ def initialize(files_path:)
21
+ @files_path = files_path
22
+ @directory_ensured = false
23
+ @imap = nil
24
+ @mbox = nil
25
+ @sanitized_files_path = nil
26
+ end
27
+
28
+ def imap
29
+ @imap ||= begin
30
+ ensure_directory
31
+ Serializer::Imap.new(files_path: sanitized_files_path)
32
+ end
33
+ end
34
+
35
+ def mbox
36
+ @mbox ||= begin
37
+ ensure_directory
38
+ Serializer::Mbox.new(files_path: sanitized_files_path)
39
+ end
40
+ end
41
+
42
+ # Checks that the folder's data is stored correctly
43
+ # @return [void]
44
+ def check_integrity!
45
+ Serializer::IntegrityChecker.new(imap: imap, mbox: mbox).run
46
+ end
47
+
48
+ # Deletes the serialized data
49
+ # @return [void]
50
+ def delete
51
+ imap.delete
52
+ mbox.delete
53
+ reload
54
+ end
55
+
56
+ # Enumerates over a series of messages.
57
+ # When called without a block, returns an Enumerator
58
+ # @param required_uids [Array<Integer>] the UIDs of the message to enumerate over
59
+ # @return [Enumerator, void]
60
+ def each_message(required_uids = nil, &block)
61
+ return enum_for(:each_message, required_uids) if !block
62
+
63
+ required_uids ||= uids
64
+
65
+ validate!
66
+
67
+ enumerator = Serializer::MessageEnumerator.new(imap: imap)
68
+ enumerator.run(uids: required_uids, &block)
69
+ end
70
+
71
+ # Get message metadata
72
+ # @param uid [Integer] a message UID
73
+ # @return [Serializer::Message]
74
+ def get(uid)
75
+ validate!
76
+ imap.get(uid)
77
+ end
78
+
79
+ # @return [Array<Hash>]
80
+ def messages
81
+ validate!
82
+ imap.messages
83
+ end
84
+
85
+ # Forces a reload of the serialized files
86
+ # @return [void]
87
+ def reload
88
+ @imap = nil
89
+ @mbox = nil
90
+ end
91
+
92
+ # Renames the serialized files
93
+ # @param new_files_path [Serializer::Files::Path] the new path of the folder
94
+ # @return [void]
95
+ def rename(new_files_path)
96
+ Serializer::DirectoryMaker.new(files_path: new_files_path).run
97
+ mbox.rename new_files_path
98
+ imap.rename new_files_path
99
+ end
100
+
101
+ # @return [Integer] the UID validity for the folder
102
+ def uid_validity
103
+ validate!
104
+ imap.uid_validity
105
+ end
106
+
107
+ def uid_validity=(value)
108
+ validate!
109
+ imap.uid_validity = value
110
+ mbox.touch
111
+ end
112
+
113
+ # @return [Array<Integer>] The uids of all messages
114
+ def uids
115
+ validate!
116
+ imap.uids
117
+ end
118
+
119
+ # Update a message's metadata, replacing its UID
120
+ # @param old [Integer] the existing message UID
121
+ # @param new [Integer] the new UID to apply to the message
122
+ # @return [void]
123
+ def update_uid(old, new)
124
+ validate!
125
+ imap.update_uid(old, new)
126
+ end
127
+
128
+ # Checks that the metadata files are valid,
129
+ # or deletes any existing files if the pair are not valid.
130
+ # @return [Boolean] indicates whether there are existing, valid files
131
+ def validate!
132
+ return true if @validated
133
+
134
+ imap_valid = imap.valid?
135
+ mbox_valid = mbox.valid?
136
+ if imap_valid && mbox_valid
137
+ @validated = true
138
+ return true
139
+ end
140
+ warn_imap = !imap_valid && imap.exist?
141
+ Logger.logger.info("Metadata file '#{imap.pathname}' is invalid") if warn_imap
142
+
143
+ delete
144
+
145
+ false
146
+ end
147
+
148
+ private
149
+
150
+ def ensure_directory
151
+ return if @directory_ensured
152
+
153
+ Serializer::DirectoryMaker.new(files_path: sanitized_files_path).run
154
+ @directory_ensured = true
155
+ end
156
+
157
+ def sanitized
158
+ @sanitized ||= Naming.to_local_path(files_path.folder_name)
159
+ end
160
+
161
+ def sanitized_files_path
162
+ @sanitized_files_path ||= Serializer::Files::Path.new(
163
+ base_path: files_path.base_path, folder_name: sanitized
164
+ )
165
+ end
166
+ end
167
+ end
@@ -14,12 +14,14 @@ module Imap::Backup
14
14
  # The version number to store in the metadata file
15
15
  CURRENT_VERSION = 3
16
16
 
17
- # @return [String] The path of the imap metadata file, without the '.imap' extension
18
- attr_reader :folder_path
19
-
20
- # @param folder_path [String] The path of the imap metadata file, without the '.imap' extension
21
- def initialize(folder_path)
22
- @folder_path = folder_path
17
+ # @return [Serializer::Files::Path] The path of the imap metadata file, without the '.imap'
18
+ # extension
19
+ attr_reader :files_path
20
+
21
+ # @param files_path [Serializer::Files::Path] The path of the imap metadata file, without
22
+ # the '.imap' extension
23
+ def initialize(files_path:)
24
+ @files_path = files_path
23
25
  @loaded = false
24
26
  @uid_validity = nil
25
27
  @messages = nil
@@ -61,7 +63,7 @@ module Imap::Backup
61
63
 
62
64
  # @return [String] The full path name of the metadata file
63
65
  def pathname
64
- "#{folder_path}.imap"
66
+ "#{files_path}.imap"
65
67
  end
66
68
 
67
69
  def exist?
@@ -136,15 +138,16 @@ module Imap::Backup
136
138
 
137
139
  # Renames the metadata file, if it exists,
138
140
  # otherwise, simply stores the new name
139
- # @param new_path [String] the new path (without extension)
141
+ # @param new_path [Serializer::Files::Path] the new path
140
142
  # @return [void]
141
143
  def rename(new_path)
142
144
  if exist?
143
145
  old_pathname = pathname
144
- @folder_path = new_path
145
- File.rename(old_pathname, pathname)
146
+ @files_path = new_path
147
+ new_pathname = pathname
148
+ File.rename(old_pathname, new_pathname)
146
149
  else
147
- @folder_path = new_path
150
+ @files_path = new_path
148
151
  end
149
152
  end
150
153
 
@@ -260,7 +263,7 @@ module Imap::Backup
260
263
  end
261
264
 
262
265
  def mbox
263
- @mbox ||= Serializer::Mbox.new(folder_path)
266
+ @mbox ||= Serializer::Mbox.new(files_path: files_path)
264
267
  end
265
268
 
266
269
  def tsx