imap-backup 4.0.1 → 4.0.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 +4 -4
- data/docs/development.md +4 -10
- data/imap-backup.gemspec +0 -4
- data/lib/email/mboxrd/message.rb +6 -2
- data/lib/imap/backup/cli/helpers.rb +13 -0
- data/lib/imap/backup/cli/local.rb +16 -25
- data/lib/imap/backup/cli/utils.rb +55 -9
- data/lib/imap/backup/serializer/mbox.rb +5 -0
- data/lib/imap/backup/serializer/mbox_store.rb +8 -8
- data/lib/imap/backup/thunderbird/mailbox_exporter.rb +71 -0
- data/lib/imap/backup/version.rb +1 -1
- data/lib/thunderbird/local_folder.rb +97 -0
- data/lib/thunderbird/local_folder_placeholder.rb +21 -0
- data/lib/thunderbird/mailbox.rb +25 -0
- data/lib/thunderbird/profile.rb +30 -0
- data/lib/thunderbird/profiles.rb +63 -0
- data/lib/thunderbird.rb +6 -0
- data/spec/unit/imap/backup/cli/local_spec.rb +70 -0
- data/spec/unit/imap/backup/cli/utils_spec.rb +50 -0
- metadata +35 -30
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 14c0bd789a2532461bbfbe92dd3bb2466228fe0d14b676c308f766a5c62941cf
|
4
|
+
data.tar.gz: 06203ea6c126b17b26f784f2f91f3dd72d1bfd431fe100428931d624de85c53e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a4de838badaaf992ae6ff46d590bb782810f4dae83aeeb9bd1495525bddfc396341374e18c51bec2e71a1cf66135335f2e21bdd6632ced9f64bc81f93f4eafe1
|
7
|
+
data.tar.gz: 20cc1c12289c753a3814205fd7cf125a2b234b567301a37f88da65b7f86b26a66274209fc8e2218975877f6568df3d991f1db03f76fe4262c8415adc3a59d6e6
|
data/docs/development.md
CHANGED
@@ -2,18 +2,12 @@
|
|
2
2
|
|
3
3
|
## Integration Tests
|
4
4
|
|
5
|
-
Integration tests (feature specs) are run against a
|
6
|
-
|
7
|
-
|
8
|
-
In one shell, run the Docker image:
|
5
|
+
Integration tests (feature specs) are run against a local IMAP server
|
6
|
+
controlled by Docker Compose, which needs to be started
|
7
|
+
before running the test suite.
|
9
8
|
|
10
9
|
```sh
|
11
|
-
$ docker
|
12
|
-
--env MAIL_ADDRESS=address@example.org \
|
13
|
-
--env MAIL_PASS=pass \
|
14
|
-
--env MAILNAME=example.org \
|
15
|
-
--publish 8993:993 \
|
16
|
-
antespi/docker-imap-devel:latest
|
10
|
+
$ docker-compose up -d
|
17
11
|
```
|
18
12
|
|
19
13
|
```sh
|
data/imap-backup.gemspec
CHANGED
@@ -17,10 +17,6 @@ Gem::Specification.new do |gem|
|
|
17
17
|
gem.files += %w[imap-backup.gemspec]
|
18
18
|
gem.files += %w[LICENSE README.md]
|
19
19
|
|
20
|
-
gem.extra_rdoc_files += Dir.glob("README.md")
|
21
|
-
gem.extra_rdoc_files += Dir.glob("docs/*.md")
|
22
|
-
gem.rdoc_options = ["--main", "README.md"]
|
23
|
-
|
24
20
|
gem.executables = gem.files.grep(%r{^bin/}).map { |f| File.basename(f) }
|
25
21
|
gem.test_files = Dir.glob("spec/**/*{.rb,.yml}")
|
26
22
|
gem.require_paths = ["lib"]
|
data/lib/email/mboxrd/message.rb
CHANGED
@@ -1,9 +1,13 @@
|
|
1
|
+
require "forwardable"
|
1
2
|
require "mail"
|
2
3
|
|
3
4
|
module Email; end
|
4
5
|
|
5
6
|
module Email::Mboxrd
|
6
7
|
class Message
|
8
|
+
extend Forwardable
|
9
|
+
def_delegators :@parsed, :subject
|
10
|
+
|
7
11
|
attr_reader :supplied_body
|
8
12
|
|
9
13
|
def self.from_serialized(serialized)
|
@@ -36,12 +40,12 @@ module Email::Mboxrd
|
|
36
40
|
supplied_body.gsub(/(?<!\r)\n/, "\r\n")
|
37
41
|
end
|
38
42
|
|
43
|
+
private
|
44
|
+
|
39
45
|
def parsed
|
40
46
|
@parsed ||= Mail.new(supplied_body)
|
41
47
|
end
|
42
48
|
|
43
|
-
private
|
44
|
-
|
45
49
|
def from
|
46
50
|
@from ||=
|
47
51
|
begin
|
@@ -5,6 +5,19 @@ module Imap::Backup::CLI::Helpers
|
|
5
5
|
options.each.with_object({}) { |(k, v), acc| acc[k.intern] = v }
|
6
6
|
end
|
7
7
|
|
8
|
+
def account(email)
|
9
|
+
connections = Imap::Backup::Configuration::List.new
|
10
|
+
account = connections.accounts.find { |a| a[:username] == email }
|
11
|
+
raise "#{email} is not a configured account" if !account
|
12
|
+
account
|
13
|
+
end
|
14
|
+
|
15
|
+
def connection(email)
|
16
|
+
account = account(email)
|
17
|
+
|
18
|
+
Imap::Backup::Account::Connection.new(account)
|
19
|
+
end
|
20
|
+
|
8
21
|
def each_connection(names)
|
9
22
|
begin
|
10
23
|
connections = Imap::Backup::Configuration::List.new(names)
|
@@ -6,49 +6,43 @@ module Imap::Backup
|
|
6
6
|
desc "accounts", "List locally backed-up accounts"
|
7
7
|
def accounts
|
8
8
|
connections = Imap::Backup::Configuration::List.new
|
9
|
-
connections.accounts.each { |a| puts a[:username] }
|
9
|
+
connections.accounts.each { |a| Kernel.puts a[:username] }
|
10
10
|
end
|
11
11
|
|
12
12
|
desc "folders EMAIL", "List account folders"
|
13
13
|
def folders(email)
|
14
|
-
|
15
|
-
account = connections.accounts.find { |a| a[:username] == email }
|
16
|
-
raise "#{email} is not a configured account" if !account
|
14
|
+
connection = connection(email)
|
17
15
|
|
18
|
-
|
19
|
-
|
20
|
-
puts %("#{f.name}")
|
16
|
+
connection.local_folders.each do |_s, f|
|
17
|
+
Kernel.puts %("#{f.name}")
|
21
18
|
end
|
22
19
|
end
|
23
20
|
|
24
21
|
desc "list EMAIL FOLDER", "List emails in a folder"
|
25
22
|
def list(email, folder_name)
|
26
|
-
|
27
|
-
account = connections.accounts.find { |a| a[:username] == email }
|
28
|
-
raise "#{email} is not a configured account" if !account
|
23
|
+
connection = connection(email)
|
29
24
|
|
30
|
-
|
31
|
-
folder_serializer, _folder = account_connection.local_folders.find do |(_s, f)|
|
25
|
+
folder_serializer, _folder = connection.local_folders.find do |(_s, f)|
|
32
26
|
f.name == folder_name
|
33
27
|
end
|
34
28
|
raise "Folder '#{folder_name}' not found" if !folder_serializer
|
35
29
|
|
36
30
|
max_subject = 60
|
37
|
-
puts format("%-10<uid>s %-#{max_subject}<subject>s - %<date>s", {uid: "UID", subject: "Subject", date: "Date"})
|
38
|
-
puts "-" * (12 + max_subject + 28)
|
31
|
+
Kernel.puts format("%-10<uid>s %-#{max_subject}<subject>s - %<date>s", {uid: "UID", subject: "Subject", date: "Date"})
|
32
|
+
Kernel.puts "-" * (12 + max_subject + 28)
|
39
33
|
|
40
34
|
uids = folder_serializer.uids
|
41
35
|
|
42
36
|
folder_serializer.each_message(uids).map do |uid, message|
|
43
37
|
m = {
|
44
38
|
uid: uid,
|
45
|
-
date: message.
|
46
|
-
subject: message.
|
39
|
+
date: message.date.to_s,
|
40
|
+
subject: message.subject || ""
|
47
41
|
}
|
48
42
|
if m[:subject].length > max_subject
|
49
|
-
puts format("% 10<uid>u: %.#{max_subject - 3}<subject>s... - %<date>s", m)
|
43
|
+
Kernel.puts format("% 10<uid>u: %.#{max_subject - 3}<subject>s... - %<date>s", m)
|
50
44
|
else
|
51
|
-
puts format("% 10<uid>u: %-#{max_subject}<subject>s - %<date>s", m)
|
45
|
+
Kernel.puts format("% 10<uid>u: %-#{max_subject}<subject>s - %<date>s", m)
|
52
46
|
end
|
53
47
|
end
|
54
48
|
end
|
@@ -60,12 +54,9 @@ module Imap::Backup
|
|
60
54
|
the UID.
|
61
55
|
DESC
|
62
56
|
def show(email, folder_name, uids)
|
63
|
-
|
64
|
-
account = connections.accounts.find { |a| a[:username] == email }
|
65
|
-
raise "#{email} is not a configured account" if !account
|
57
|
+
connection = connection(email)
|
66
58
|
|
67
|
-
|
68
|
-
folder_serializer, _folder = account_connection.local_folders.find do |(_s, f)|
|
59
|
+
folder_serializer, _folder = connection.local_folders.find do |(_s, f)|
|
69
60
|
f.name == folder_name
|
70
61
|
end
|
71
62
|
raise "Folder '#{folder_name}' not found" if !folder_serializer
|
@@ -73,13 +64,13 @@ module Imap::Backup
|
|
73
64
|
uid_list = uids.split(",")
|
74
65
|
folder_serializer.each_message(uid_list).each do |uid, message|
|
75
66
|
if uid_list.count > 1
|
76
|
-
puts <<~HEADER
|
67
|
+
Kernel.puts <<~HEADER
|
77
68
|
#{'-' * 80}
|
78
69
|
#{format('| UID: %-71s |', uid)}
|
79
70
|
#{'-' * 80}
|
80
71
|
HEADER
|
81
72
|
end
|
82
|
-
puts message.supplied_body
|
73
|
+
Kernel.puts message.supplied_body
|
83
74
|
end
|
84
75
|
end
|
85
76
|
end
|
@@ -1,26 +1,64 @@
|
|
1
|
+
require "imap/backup/thunderbird/mailbox_exporter"
|
2
|
+
|
1
3
|
module Imap::Backup
|
2
4
|
class CLI::Utils < Thor
|
3
5
|
include Thor::Actions
|
6
|
+
include CLI::Helpers
|
4
7
|
|
5
8
|
FAKE_EMAIL = "fake@email.com"
|
6
9
|
|
7
10
|
desc "ignore-history EMAIL", "Skip downloading emails up to today for all configured folders"
|
8
11
|
def ignore_history(email)
|
9
|
-
|
10
|
-
account = connections.accounts.find { |a| a[:username] == email }
|
11
|
-
raise "#{email} is not a configured account" if !account
|
12
|
-
|
13
|
-
connection = Imap::Backup::Account::Connection.new(account)
|
12
|
+
connection = connection(email)
|
14
13
|
|
15
|
-
connection.local_folders.each do |
|
14
|
+
connection.local_folders.each do |serializer, folder|
|
16
15
|
next if !folder.exist?
|
17
|
-
do_ignore_folder_history(
|
16
|
+
do_ignore_folder_history(folder, serializer)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
desc "export-to-thunderbird EMAIL [OPTIONS]",
|
21
|
+
<<~DOC
|
22
|
+
[Experimental] Copy backed up emails to Thunderbird.
|
23
|
+
A folder called 'imap-backup/EMAIL' is created under 'Local Folders'.
|
24
|
+
DOC
|
25
|
+
method_option(
|
26
|
+
"force",
|
27
|
+
type: :boolean,
|
28
|
+
banner: "overwrite existing mailboxes",
|
29
|
+
aliases: ["-f"]
|
30
|
+
)
|
31
|
+
method_option(
|
32
|
+
"profile",
|
33
|
+
type: :string,
|
34
|
+
banner: "the name of the Thunderbird profile to copy emails to",
|
35
|
+
aliases: ["-p"]
|
36
|
+
)
|
37
|
+
def export_to_thunderbird(email)
|
38
|
+
opts = symbolized(options)
|
39
|
+
force = opts.key?(:force) ? opts[:force] : false
|
40
|
+
profile_name = opts[:profile]
|
41
|
+
|
42
|
+
connection = connection(email)
|
43
|
+
profile = thunderbird_profile(profile_name)
|
44
|
+
|
45
|
+
if !profile
|
46
|
+
if profile_name
|
47
|
+
raise "Thunderbird profile '#{profile_name}' not found"
|
48
|
+
else
|
49
|
+
raise "Default Thunderbird profile not found"
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
connection.local_folders.each do |serializer, folder|
|
54
|
+
Thunderbird::MailboxExporter.new(
|
55
|
+
email, serializer, profile, force: force
|
56
|
+
).run
|
18
57
|
end
|
19
58
|
end
|
20
59
|
|
21
60
|
no_commands do
|
22
|
-
def do_ignore_folder_history(
|
23
|
-
serializer = Imap::Backup::Serializer::Mbox.new(connection.local_path, folder.name)
|
61
|
+
def do_ignore_folder_history(folder, serializer)
|
24
62
|
uids = folder.uids - serializer.uids
|
25
63
|
Imap::Backup.logger.info "Folder '#{folder.name}' - #{uids.length} messages"
|
26
64
|
|
@@ -36,6 +74,14 @@ module Imap::Backup
|
|
36
74
|
serializer.save(uid, message)
|
37
75
|
end
|
38
76
|
end
|
77
|
+
|
78
|
+
def thunderbird_profile(name = nil)
|
79
|
+
if name
|
80
|
+
Thunderbird::Profiles.new.profile(name)
|
81
|
+
else
|
82
|
+
Thunderbird::Profiles.new.default
|
83
|
+
end
|
84
|
+
end
|
39
85
|
end
|
40
86
|
end
|
41
87
|
end
|
@@ -121,6 +121,14 @@ module Imap::Backup
|
|
121
121
|
@folder = new_name
|
122
122
|
end
|
123
123
|
|
124
|
+
def mbox_pathname
|
125
|
+
absolute_path("#{folder}.mbox")
|
126
|
+
end
|
127
|
+
|
128
|
+
def imap_pathname
|
129
|
+
absolute_path("#{folder}.imap")
|
130
|
+
end
|
131
|
+
|
124
132
|
private
|
125
133
|
|
126
134
|
def do_load
|
@@ -205,13 +213,5 @@ module Imap::Backup
|
|
205
213
|
def absolute_path(relative_path)
|
206
214
|
File.join(path, relative_path)
|
207
215
|
end
|
208
|
-
|
209
|
-
def mbox_pathname
|
210
|
-
absolute_path("#{folder}.mbox")
|
211
|
-
end
|
212
|
-
|
213
|
-
def imap_pathname
|
214
|
-
absolute_path("#{folder}.imap")
|
215
|
-
end
|
216
216
|
end
|
217
217
|
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
require "thunderbird/local_folder"
|
2
|
+
require "thunderbird/mailbox"
|
3
|
+
require "thunderbird/profiles"
|
4
|
+
|
5
|
+
module Imap::Backup
|
6
|
+
class Thunderbird::MailboxExporter
|
7
|
+
EXPORT_PREFIX = "imap-backup"
|
8
|
+
|
9
|
+
attr_reader :email
|
10
|
+
attr_reader :serializer
|
11
|
+
attr_reader :profile
|
12
|
+
attr_reader :force
|
13
|
+
|
14
|
+
def initialize(email, serializer, profile, force: false)
|
15
|
+
@email = email
|
16
|
+
@serializer = serializer
|
17
|
+
@profile = profile
|
18
|
+
@force = force
|
19
|
+
end
|
20
|
+
|
21
|
+
def run
|
22
|
+
local_folder.set_up
|
23
|
+
|
24
|
+
if mailbox.msf_exists?
|
25
|
+
if force
|
26
|
+
Kernel.puts "Deleting '#{mailbox.msf_path}' as --force option was supplied"
|
27
|
+
File.unlink mailbox.msf_path
|
28
|
+
else
|
29
|
+
Kernel.puts "Skipping export of '#{folder.name}' as '#{mailbox.msf_path}' exists"
|
30
|
+
return false
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
if mailbox.exists?
|
35
|
+
if force
|
36
|
+
Kernel.puts "Overwriting '#{mailbox.path}' as --force option was supplied"
|
37
|
+
else
|
38
|
+
Kernel.puts "Skipping export of '#{folder.name}' as '#{mailbox.path}' exists"
|
39
|
+
return false
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
FileUtils.cp serializer.mbox_pathname, mailbox.path
|
44
|
+
|
45
|
+
true
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def local_folder
|
51
|
+
@local_folder ||= begin
|
52
|
+
folder_path = File.dirname(serializer.folder)
|
53
|
+
top_level_folders = [EXPORT_PREFIX, email]
|
54
|
+
prefixed_folder_path =
|
55
|
+
if folder_path == "."
|
56
|
+
File.join(top_level_folders)
|
57
|
+
else
|
58
|
+
File.join(top_level_folders, folder_path)
|
59
|
+
end
|
60
|
+
Thunderbird::LocalFolder.new(profile, prefixed_folder_path)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def mailbox
|
65
|
+
@mailbox ||= begin
|
66
|
+
mailbox_name = File.basename(serializer.folder)
|
67
|
+
Thunderbird::Mailbox.new(local_folder, mailbox_name)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
data/lib/imap/backup/version.rb
CHANGED
@@ -0,0 +1,97 @@
|
|
1
|
+
require "thunderbird/profile"
|
2
|
+
require "thunderbird/local_folder_placeholder"
|
3
|
+
|
4
|
+
class Thunderbird::LocalFolder
|
5
|
+
attr_reader :folder_path
|
6
|
+
attr_reader :profile
|
7
|
+
|
8
|
+
def initialize(profile, folder_path)
|
9
|
+
@profile = profile
|
10
|
+
@folder_path = folder_path
|
11
|
+
end
|
12
|
+
|
13
|
+
def local_folder_placeholder
|
14
|
+
if parent
|
15
|
+
path = File.join(parent.full_path, folder_path_elements[-1])
|
16
|
+
Thunderbird::LocalFolderPlaceholder.new(path)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def directory_is_directory?
|
21
|
+
File.directory?(full_path)
|
22
|
+
end
|
23
|
+
|
24
|
+
def full_path
|
25
|
+
File.join(profile.local_folders_path, relative_path)
|
26
|
+
end
|
27
|
+
|
28
|
+
def parent
|
29
|
+
if folder_path_elements.count > 0
|
30
|
+
self.class.new(profile, File.join(folder_path_elements[0..-2]))
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def folder_path_elements
|
35
|
+
folder_path.split(File::SEPARATOR)
|
36
|
+
end
|
37
|
+
|
38
|
+
def directory_exists?
|
39
|
+
File.exists?(full_path)
|
40
|
+
end
|
41
|
+
|
42
|
+
def is_directory?
|
43
|
+
File.directory?(full_path)
|
44
|
+
end
|
45
|
+
|
46
|
+
def subdirectories
|
47
|
+
folder_path_elements.map { |p| "#{p}.sbd" }
|
48
|
+
end
|
49
|
+
|
50
|
+
def relative_path
|
51
|
+
File.join(subdirectories)
|
52
|
+
end
|
53
|
+
|
54
|
+
def set_up
|
55
|
+
ok = check
|
56
|
+
return if !ok
|
57
|
+
|
58
|
+
ensure_initialized
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
def ensure_initialized
|
64
|
+
return true if !parent
|
65
|
+
|
66
|
+
parent.ensure_initialized
|
67
|
+
|
68
|
+
local_folder_placeholder.ensure_initialized
|
69
|
+
|
70
|
+
FileUtils.mkdir_p full_path
|
71
|
+
end
|
72
|
+
|
73
|
+
def check
|
74
|
+
return true if !parent
|
75
|
+
|
76
|
+
parent_ok = parent.check
|
77
|
+
|
78
|
+
return if !parent_ok
|
79
|
+
|
80
|
+
case
|
81
|
+
when local_folder_placeholder.exists? && !directory_exists?
|
82
|
+
Kernel.puts "Can't set up folder '#{folder_path}': '#{local_folder_placeholder.path}' exists, but '#{full_path}' is missing"
|
83
|
+
false
|
84
|
+
when directory_exists? && !local_folder_placeholder.exists?
|
85
|
+
Kernel.puts "Can't set up folder '#{folder_path}': '#{full_path}' exists, but '#{local_folder_placeholder.path}' is missing"
|
86
|
+
false
|
87
|
+
when local_folder_placeholder.exists? && !local_folder_placeholder.is_regular?
|
88
|
+
Kernel.puts "Can't set up folder '#{folder_path}': '#{local_folder_placeholder.path}' exists, but it is not a regular file"
|
89
|
+
false
|
90
|
+
when directory_exists? && !is_directory?
|
91
|
+
Kernel.puts "Can't set up folder '#{folder_path}': '#{full_path}' exists, but it is not a directory"
|
92
|
+
false
|
93
|
+
else
|
94
|
+
true
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# Each subdirectory is "accompanied" by a blank
|
2
|
+
# file of the same name (without the '.sbd' extension)
|
3
|
+
class Thunderbird::LocalFolderPlaceholder
|
4
|
+
attr_reader :path
|
5
|
+
|
6
|
+
def initialize(path)
|
7
|
+
@path = path
|
8
|
+
end
|
9
|
+
|
10
|
+
def exists?
|
11
|
+
File.exists?(path)
|
12
|
+
end
|
13
|
+
|
14
|
+
def is_regular?
|
15
|
+
File.file?(path)
|
16
|
+
end
|
17
|
+
|
18
|
+
def ensure_initialized
|
19
|
+
FileUtils.touch path
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
class Thunderbird::Mailbox
|
2
|
+
attr_reader :local_folder
|
3
|
+
attr_reader :name
|
4
|
+
|
5
|
+
def initialize(local_folder, name)
|
6
|
+
@local_folder = local_folder
|
7
|
+
@name = name
|
8
|
+
end
|
9
|
+
|
10
|
+
def path
|
11
|
+
File.join(local_folder.full_path, name)
|
12
|
+
end
|
13
|
+
|
14
|
+
def exists?
|
15
|
+
File.exists?(path)
|
16
|
+
end
|
17
|
+
|
18
|
+
def msf_path
|
19
|
+
path + ".msf"
|
20
|
+
end
|
21
|
+
|
22
|
+
def msf_exists?
|
23
|
+
File.exists?(msf_path)
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require "thunderbird"
|
2
|
+
|
3
|
+
class Thunderbird::Profile
|
4
|
+
attr_reader :title
|
5
|
+
attr_reader :entries
|
6
|
+
|
7
|
+
# entries are lines from profile.ini
|
8
|
+
def initialize(title, entries)
|
9
|
+
@title = title
|
10
|
+
@entries = entries
|
11
|
+
end
|
12
|
+
|
13
|
+
def root
|
14
|
+
if relative?
|
15
|
+
File.join(Thunderbird.new.data_path, entries[:Path])
|
16
|
+
else
|
17
|
+
entries[:Path]
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def local_folders_path
|
22
|
+
File.join(root, "Mail", "Local Folders")
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def relative?
|
28
|
+
entries[:IsRelative] == "1"
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
require "thunderbird"
|
2
|
+
require "thunderbird/profile"
|
3
|
+
|
4
|
+
# http://kb.mozillazine.org/Profiles.ini_file
|
5
|
+
class Thunderbird::Profiles
|
6
|
+
def default
|
7
|
+
title, entries = blocks.find { |_name, entries| entries[:Default] == "1" }
|
8
|
+
|
9
|
+
Thunderbird::Profile.new(title, entries) if title
|
10
|
+
end
|
11
|
+
|
12
|
+
def profile(name)
|
13
|
+
title, entries = blocks.find { |_name, entries| entries[:Name] == name }
|
14
|
+
|
15
|
+
return nil if !title
|
16
|
+
|
17
|
+
Thunderbird::Profile.new(title, entries) if title
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
# Parse profiles.ini.
|
23
|
+
# Blocks start with a title, e.g. '[Abc]'
|
24
|
+
# and are followed by a number of lines
|
25
|
+
def blocks
|
26
|
+
@blocks ||= begin
|
27
|
+
blocks = {}
|
28
|
+
File.open(profiles_ini_path, "rb") do |f|
|
29
|
+
title = nil
|
30
|
+
entries = nil
|
31
|
+
|
32
|
+
loop do
|
33
|
+
line = f.gets
|
34
|
+
break if !line
|
35
|
+
line.chomp!
|
36
|
+
|
37
|
+
# Is this line the start of a new block
|
38
|
+
match = line.match(/\A\[([A-Za-z0-9]+)\]\z/)
|
39
|
+
if match
|
40
|
+
# Store what we got before this title as a new block
|
41
|
+
blocks[title] = entries if title
|
42
|
+
|
43
|
+
# Start a new block
|
44
|
+
title = match[1]
|
45
|
+
entries = {}
|
46
|
+
else
|
47
|
+
# Collect entries until we get to the next title
|
48
|
+
if line != ""
|
49
|
+
key, value = line.split("=")
|
50
|
+
entries[key.to_sym] = value
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
blocks
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def profiles_ini_path
|
61
|
+
File.join(Thunderbird.new.data_path, "profiles.ini")
|
62
|
+
end
|
63
|
+
end
|
data/lib/thunderbird.rb
ADDED
@@ -0,0 +1,70 @@
|
|
1
|
+
describe Imap::Backup::CLI::Local do
|
2
|
+
let(:list) do
|
3
|
+
instance_double(Imap::Backup::Configuration::List, accounts: accounts)
|
4
|
+
end
|
5
|
+
let(:accounts) { [{username: email}] }
|
6
|
+
let(:connection) do
|
7
|
+
instance_double(
|
8
|
+
Imap::Backup::Account::Connection,
|
9
|
+
local_folders: local_folders
|
10
|
+
)
|
11
|
+
end
|
12
|
+
let(:local_folders) { [[serializer, folder]] }
|
13
|
+
let(:folder) { instance_double(Imap::Backup::Account::Folder, name: "bar") }
|
14
|
+
let(:serializer) do
|
15
|
+
instance_double(
|
16
|
+
Imap::Backup::Serializer::Mbox,
|
17
|
+
uids: uids,
|
18
|
+
each_message: [[123, message]]
|
19
|
+
)
|
20
|
+
end
|
21
|
+
let(:uids) { ["123"] }
|
22
|
+
let(:message) do
|
23
|
+
instance_double(
|
24
|
+
Email::Mboxrd::Message,
|
25
|
+
date: Date.today,
|
26
|
+
subject: "Ciao",
|
27
|
+
supplied_body: "Supplied"
|
28
|
+
)
|
29
|
+
end
|
30
|
+
let(:email) { "foo@example.com" }
|
31
|
+
|
32
|
+
before do
|
33
|
+
allow(Kernel).to receive(:puts)
|
34
|
+
allow(Imap::Backup::Configuration::List).to receive(:new) { list }
|
35
|
+
allow(Imap::Backup::Account::Connection).to receive(:new) { connection }
|
36
|
+
allow(Mail).to receive(:new) { mail }
|
37
|
+
end
|
38
|
+
|
39
|
+
describe "accounts" do
|
40
|
+
it "lists configured emails" do
|
41
|
+
subject.accounts
|
42
|
+
|
43
|
+
expect(Kernel).to have_received(:puts).with(email)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
describe "folders" do
|
48
|
+
it "lists downloaded folders in quotes" do
|
49
|
+
subject.folders(email)
|
50
|
+
|
51
|
+
expect(Kernel).to have_received(:puts).with(%("bar"))
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
describe "list" do
|
56
|
+
it "lists downloaded emails" do
|
57
|
+
subject.list(email, "bar")
|
58
|
+
|
59
|
+
expect(Kernel).to have_received(:puts).with(/Ciao/)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
describe "show" do
|
64
|
+
it "prints a downloaded email" do
|
65
|
+
subject.show(email, "bar", "123")
|
66
|
+
|
67
|
+
expect(Kernel).to have_received(:puts).with("Supplied")
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
describe Imap::Backup::CLI::Utils do
|
2
|
+
let(:list) do
|
3
|
+
instance_double(Imap::Backup::Configuration::List, accounts: accounts)
|
4
|
+
end
|
5
|
+
let(:accounts) { [{username: email}] }
|
6
|
+
let(:connection) do
|
7
|
+
instance_double(
|
8
|
+
Imap::Backup::Account::Connection,
|
9
|
+
local_folders: local_folders
|
10
|
+
)
|
11
|
+
end
|
12
|
+
let(:local_folders) { [[serializer, folder]] }
|
13
|
+
let(:folder) do
|
14
|
+
instance_double(
|
15
|
+
Imap::Backup::Account::Folder,
|
16
|
+
exist?: true,
|
17
|
+
name: "name",
|
18
|
+
uid_validity: "uid_validity",
|
19
|
+
uids: ["123", "456"]
|
20
|
+
)
|
21
|
+
end
|
22
|
+
let(:serializer) do
|
23
|
+
instance_double(
|
24
|
+
Imap::Backup::Serializer::Mbox,
|
25
|
+
uids: ["123", "789"],
|
26
|
+
apply_uid_validity: nil,
|
27
|
+
save: nil
|
28
|
+
)
|
29
|
+
end
|
30
|
+
let(:email) { "foo@example.com" }
|
31
|
+
|
32
|
+
before do
|
33
|
+
allow(Imap::Backup::Configuration::List).to receive(:new) { list }
|
34
|
+
allow(Imap::Backup::Account::Connection).to receive(:new) { connection }
|
35
|
+
end
|
36
|
+
|
37
|
+
describe "ignore_history" do
|
38
|
+
it "ensures the local UID validity matches the server" do
|
39
|
+
subject.ignore_history(email)
|
40
|
+
|
41
|
+
expect(serializer).to have_received(:apply_uid_validity).with("uid_validity")
|
42
|
+
end
|
43
|
+
|
44
|
+
it "fills the local folder with fake emails" do
|
45
|
+
subject.ignore_history(email)
|
46
|
+
|
47
|
+
expect(serializer).to have_received(:save).with("456", /From: fake@email.com/)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
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: 4.0.
|
4
|
+
version: 4.0.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Joe Yates
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-12-
|
11
|
+
date: 2021-12-13 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: highline
|
@@ -156,11 +156,7 @@ email:
|
|
156
156
|
executables:
|
157
157
|
- imap-backup
|
158
158
|
extensions: []
|
159
|
-
extra_rdoc_files:
|
160
|
-
- README.md
|
161
|
-
- docs/setup.md
|
162
|
-
- docs/restore.md
|
163
|
-
- docs/development.md
|
159
|
+
extra_rdoc_files: []
|
164
160
|
files:
|
165
161
|
- LICENSE
|
166
162
|
- README.md
|
@@ -195,10 +191,17 @@ files:
|
|
195
191
|
- lib/imap/backup/serializer/mbox.rb
|
196
192
|
- lib/imap/backup/serializer/mbox_enumerator.rb
|
197
193
|
- lib/imap/backup/serializer/mbox_store.rb
|
194
|
+
- lib/imap/backup/thunderbird/mailbox_exporter.rb
|
198
195
|
- lib/imap/backup/uploader.rb
|
199
196
|
- lib/imap/backup/utils.rb
|
200
197
|
- lib/imap/backup/version.rb
|
201
198
|
- lib/retry_on_error.rb
|
199
|
+
- lib/thunderbird.rb
|
200
|
+
- lib/thunderbird/local_folder.rb
|
201
|
+
- lib/thunderbird/local_folder_placeholder.rb
|
202
|
+
- lib/thunderbird/mailbox.rb
|
203
|
+
- lib/thunderbird/profile.rb
|
204
|
+
- lib/thunderbird/profiles.rb
|
202
205
|
- spec/features/backup_spec.rb
|
203
206
|
- spec/features/helper.rb
|
204
207
|
- spec/features/restore_spec.rb
|
@@ -217,6 +220,8 @@ files:
|
|
217
220
|
- spec/unit/email/provider_spec.rb
|
218
221
|
- spec/unit/imap/backup/account/connection_spec.rb
|
219
222
|
- spec/unit/imap/backup/account/folder_spec.rb
|
223
|
+
- spec/unit/imap/backup/cli/local_spec.rb
|
224
|
+
- spec/unit/imap/backup/cli/utils_spec.rb
|
220
225
|
- spec/unit/imap/backup/configuration/account_spec.rb
|
221
226
|
- spec/unit/imap/backup/configuration/asker_spec.rb
|
222
227
|
- spec/unit/imap/backup/configuration/connection_tester_spec.rb
|
@@ -236,9 +241,7 @@ licenses:
|
|
236
241
|
- MIT
|
237
242
|
metadata: {}
|
238
243
|
post_install_message:
|
239
|
-
rdoc_options:
|
240
|
-
- "--main"
|
241
|
-
- README.md
|
244
|
+
rdoc_options: []
|
242
245
|
require_paths:
|
243
246
|
- lib
|
244
247
|
required_ruby_version: !ruby/object:Gem::Requirement
|
@@ -252,40 +255,42 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
252
255
|
- !ruby/object:Gem::Version
|
253
256
|
version: '0'
|
254
257
|
requirements: []
|
255
|
-
rubygems_version: 3.1.
|
258
|
+
rubygems_version: 3.1.2
|
256
259
|
signing_key:
|
257
260
|
specification_version: 4
|
258
261
|
summary: Backup GMail (or other IMAP) accounts to disk
|
259
262
|
test_files:
|
260
263
|
- spec/spec_helper.rb
|
264
|
+
- spec/fixtures/connection.yml
|
265
|
+
- spec/unit/email/provider_spec.rb
|
266
|
+
- spec/unit/email/mboxrd/message_spec.rb
|
261
267
|
- spec/unit/imap/backup_spec.rb
|
262
|
-
- spec/unit/imap/backup/
|
263
|
-
- spec/unit/imap/backup/
|
268
|
+
- spec/unit/imap/backup/cli/utils_spec.rb
|
269
|
+
- spec/unit/imap/backup/cli/local_spec.rb
|
270
|
+
- spec/unit/imap/backup/utils_spec.rb
|
271
|
+
- spec/unit/imap/backup/downloader_spec.rb
|
272
|
+
- spec/unit/imap/backup/configuration/store_spec.rb
|
264
273
|
- spec/unit/imap/backup/configuration/connection_tester_spec.rb
|
265
|
-
- spec/unit/imap/backup/configuration/setup_spec.rb
|
266
274
|
- spec/unit/imap/backup/configuration/asker_spec.rb
|
275
|
+
- spec/unit/imap/backup/configuration/setup_spec.rb
|
267
276
|
- spec/unit/imap/backup/configuration/folder_chooser_spec.rb
|
268
|
-
- spec/unit/imap/backup/configuration/
|
277
|
+
- spec/unit/imap/backup/configuration/account_spec.rb
|
278
|
+
- spec/unit/imap/backup/configuration/list_spec.rb
|
279
|
+
- spec/unit/imap/backup/account/folder_spec.rb
|
280
|
+
- spec/unit/imap/backup/account/connection_spec.rb
|
269
281
|
- spec/unit/imap/backup/serializer/mbox_spec.rb
|
270
282
|
- spec/unit/imap/backup/serializer/mbox_store_spec.rb
|
271
283
|
- spec/unit/imap/backup/serializer/mbox_enumerator_spec.rb
|
272
|
-
- spec/unit/imap/backup/downloader_spec.rb
|
273
284
|
- spec/unit/imap/backup/uploader_spec.rb
|
274
|
-
- spec/
|
275
|
-
- spec/
|
276
|
-
- spec/
|
277
|
-
- spec/
|
278
|
-
- spec/
|
285
|
+
- spec/support/fixtures.rb
|
286
|
+
- spec/support/shared_examples/account_flagging.rb
|
287
|
+
- spec/support/higline_test_helpers.rb
|
288
|
+
- spec/support/silence_logging.rb
|
289
|
+
- spec/gather_rspec_coverage.rb
|
279
290
|
- spec/features/backup_spec.rb
|
280
291
|
- spec/features/helper.rb
|
281
|
-
- spec/features/restore_spec.rb
|
282
|
-
- spec/features/support/email_server.rb
|
283
292
|
- spec/features/support/backup_directory.rb
|
284
|
-
- spec/features/support/shared/connection_context.rb
|
285
293
|
- spec/features/support/shared/message_fixtures.rb
|
286
|
-
- spec/support/
|
287
|
-
- spec/support/
|
288
|
-
- spec/
|
289
|
-
- spec/support/shared_examples/account_flagging.rb
|
290
|
-
- spec/fixtures/connection.yml
|
291
|
-
- spec/gather_rspec_coverage.rb
|
294
|
+
- spec/features/support/shared/connection_context.rb
|
295
|
+
- spec/features/support/email_server.rb
|
296
|
+
- spec/features/restore_spec.rb
|