imap-backup 4.0.1 → 4.0.2
Sign up to get free protection for your applications and to get access to all the features.
- 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
|