imap-backup 4.0.1 → 4.0.5

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.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +9 -1
  3. data/docs/development.md +5 -12
  4. data/imap-backup.gemspec +1 -4
  5. data/lib/email/mboxrd/message.rb +6 -2
  6. data/lib/email/provider/apple_mail.rb +7 -0
  7. data/lib/email/provider/base.rb +8 -0
  8. data/lib/email/provider/fastmail.rb +7 -0
  9. data/lib/email/provider/gmail.rb +7 -0
  10. data/lib/email/provider/unknown.rb +11 -0
  11. data/lib/email/provider.rb +16 -26
  12. data/lib/imap/backup/account/connection.rb +19 -29
  13. data/lib/imap/backup/account/folder.rb +9 -10
  14. data/lib/imap/backup/account.rb +103 -0
  15. data/lib/imap/backup/cli/helpers.rb +14 -0
  16. data/lib/imap/backup/cli/local.rb +19 -25
  17. data/lib/imap/backup/cli/utils.rb +67 -10
  18. data/lib/imap/backup/client/apple_mail.rb +11 -0
  19. data/lib/imap/backup/client/default.rb +51 -0
  20. data/lib/imap/backup/configuration/account.rb +3 -1
  21. data/lib/imap/backup/configuration/connection_tester.rb +1 -1
  22. data/lib/imap/backup/configuration/store.rb +10 -5
  23. data/lib/imap/backup/downloader.rb +3 -4
  24. data/lib/imap/backup/serializer/mbox.rb +5 -0
  25. data/lib/imap/backup/serializer/mbox_store.rb +8 -8
  26. data/lib/imap/backup/thunderbird/mailbox_exporter.rb +58 -0
  27. data/lib/imap/backup/version.rb +1 -1
  28. data/lib/imap/backup.rb +1 -0
  29. data/lib/thunderbird/install.rb +16 -0
  30. data/lib/thunderbird/local_folder.rb +65 -0
  31. data/lib/thunderbird/profile.rb +30 -0
  32. data/lib/thunderbird/profiles.rb +71 -0
  33. data/lib/thunderbird/subdirectory.rb +96 -0
  34. data/lib/thunderbird/subdirectory_placeholder.rb +21 -0
  35. data/lib/thunderbird.rb +14 -0
  36. data/spec/features/restore_spec.rb +1 -1
  37. data/spec/features/support/email_server.rb +2 -2
  38. data/spec/unit/email/provider/apple_mail_spec.rb +7 -0
  39. data/spec/unit/email/provider/base_spec.rb +11 -0
  40. data/spec/unit/email/provider/fastmail_spec.rb +7 -0
  41. data/spec/unit/email/provider/gmail_spec.rb +7 -0
  42. data/spec/unit/email/provider_spec.rb +12 -25
  43. data/spec/unit/imap/backup/account/connection_spec.rb +26 -51
  44. data/spec/unit/imap/backup/account/folder_spec.rb +22 -22
  45. data/spec/unit/imap/backup/cli/local_spec.rb +70 -0
  46. data/spec/unit/imap/backup/cli/utils_spec.rb +50 -0
  47. data/spec/unit/imap/backup/client/default_spec.rb +22 -0
  48. data/spec/unit/imap/backup/configuration/connection_tester_spec.rb +3 -3
  49. data/spec/unit/imap/backup/configuration/store_spec.rb +25 -12
  50. data/spec/unit/imap/backup/downloader_spec.rb +1 -2
  51. metadata +68 -30
@@ -1,26 +1,67 @@
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
- FAKE_EMAIL = "fake@email.com"
8
+ FAKE_EMAIL = "fake@email.com".freeze
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
- 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
-
13
- connection = Imap::Backup::Account::Connection.new(account)
12
+ connection = connection(email)
14
13
 
15
- connection.local_folders.each do |_s, f|
14
+ connection.local_folders.each do |serializer, folder|
16
15
  next if !folder.exist?
17
- do_ignore_folder_history(connection, folder)
16
+
17
+ do_ignore_folder_history(folder, serializer)
18
+ end
19
+ end
20
+
21
+ desc(
22
+ "export-to-thunderbird EMAIL [OPTIONS]",
23
+ <<~DOC
24
+ [Experimental] Copy backed up emails to Thunderbird.
25
+ A folder called 'imap-backup/EMAIL' is created under 'Local Folders'.
26
+ DOC
27
+ )
28
+ method_option(
29
+ "force",
30
+ type: :boolean,
31
+ banner: "overwrite existing mailboxes",
32
+ aliases: ["-f"]
33
+ )
34
+ method_option(
35
+ "profile",
36
+ type: :string,
37
+ banner: "the name of the Thunderbird profile to copy emails to",
38
+ aliases: ["-p"]
39
+ )
40
+ def export_to_thunderbird(email)
41
+ opts = symbolized(options)
42
+ force = opts.key?(:force) ? opts[:force] : false
43
+ profile_name = opts[:profile]
44
+
45
+ connection = connection(email)
46
+ profile = thunderbird_profile(profile_name)
47
+
48
+ if !profile
49
+ if profile_name
50
+ raise "Thunderbird profile '#{profile_name}' not found"
51
+ else
52
+ raise "Default Thunderbird profile not found"
53
+ end
54
+ end
55
+
56
+ connection.local_folders.each do |serializer, _folder|
57
+ Thunderbird::MailboxExporter.new(
58
+ email, serializer, profile, force: force
59
+ ).run
18
60
  end
19
61
  end
20
62
 
21
63
  no_commands do
22
- def do_ignore_folder_history(connection, folder)
23
- serializer = Imap::Backup::Serializer::Mbox.new(connection.local_path, folder.name)
64
+ def do_ignore_folder_history(folder, serializer)
24
65
  uids = folder.uids - serializer.uids
25
66
  Imap::Backup.logger.info "Folder '#{folder.name}' - #{uids.length} messages"
26
67
 
@@ -36,6 +77,22 @@ module Imap::Backup
36
77
  serializer.save(uid, message)
37
78
  end
38
79
  end
80
+
81
+ def thunderbird_profile(name = nil)
82
+ profiles = Thunderbird::Profiles.new
83
+ if name
84
+ profiles.profile(name)
85
+ else
86
+ if profiles.installs.count > 1
87
+ raise <<~MESSAGE
88
+ Thunderbird has multiple installs, so no default profile exists.
89
+ Please supply a profile name
90
+ MESSAGE
91
+ end
92
+
93
+ profiles.installs[0].default
94
+ end
95
+ end
39
96
  end
40
97
  end
41
98
  end
@@ -0,0 +1,11 @@
1
+ require "imap/backup/client/default"
2
+
3
+ module Imap::Backup
4
+ class Client::AppleMail < Client::Default
5
+ # With Apple Mails's IMAP, passing "/" to list
6
+ # results in an empty list
7
+ def provider_root
8
+ ""
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,51 @@
1
+ require "forwardable"
2
+ require "net/imap"
3
+
4
+ module Imap::Backup
5
+ module Client; end
6
+
7
+ class Client::Default
8
+ extend Forwardable
9
+ def_delegators :imap, *%i(
10
+ append authenticate create disconnect examine
11
+ login responses uid_fetch uid_search
12
+ )
13
+
14
+ attr_reader :args
15
+
16
+ def initialize(*args)
17
+ @args = args
18
+ end
19
+
20
+ def list
21
+ root = provider_root
22
+ mailbox_lists = imap.list(root, "*")
23
+
24
+ return [] if mailbox_lists.nil?
25
+
26
+ mailbox_lists.map { |ml| extract_name(ml) }
27
+ end
28
+
29
+ private
30
+
31
+ def imap
32
+ @imap ||= Net::IMAP.new(*args)
33
+ end
34
+
35
+ def extract_name(mailbox_list)
36
+ utf7_encoded = mailbox_list.name
37
+ Net::IMAP.decode_utf7(utf7_encoded)
38
+ end
39
+
40
+ # 6.3.8. LIST Command
41
+ # An empty ("" string) mailbox name argument is a special request to
42
+ # return the hierarchy delimiter and the root name of the name given
43
+ # in the reference.
44
+ def provider_root
45
+ @provider_root ||= begin
46
+ root_info = imap.list("", "")[0]
47
+ root_info.name
48
+ end
49
+ end
50
+ end
51
+ end
@@ -147,10 +147,12 @@ module Imap::Backup
147
147
 
148
148
  def default_server(username)
149
149
  provider = Email::Provider.for_address(username)
150
- if provider.provider == :default
150
+
151
+ if provider.is_a?(Email::Provider::Unknown)
151
152
  Kernel.puts "Can't decide provider for email address '#{username}'"
152
153
  return nil
153
154
  end
155
+
154
156
  provider.host
155
157
  end
156
158
  end
@@ -3,7 +3,7 @@ module Imap::Backup
3
3
 
4
4
  module Configuration::ConnectionTester
5
5
  def self.test(account)
6
- Account::Connection.new(account).imap
6
+ Account::Connection.new(account).client
7
7
  "Connection successful"
8
8
  rescue Net::IMAP::NoResponseError
9
9
  "No response"
@@ -1,4 +1,5 @@
1
1
  require "json"
2
+ require "os"
2
3
 
3
4
  module Imap::Backup
4
5
  module Configuration; end
@@ -25,11 +26,12 @@ module Imap::Backup
25
26
  end
26
27
 
27
28
  def save
28
- mkdir_private path
29
+ FileUtils.mkdir(path) if !File.directory?(path)
30
+ make_private(path) if !windows?
29
31
  remove_modified_flags
30
32
  remove_deleted_accounts
31
33
  File.open(pathname, "w") { |f| f.write(JSON.pretty_generate(data)) }
32
- FileUtils.chmod 0o600, pathname
34
+ FileUtils.chmod(0o600, pathname) if !windows?
33
35
  end
34
36
 
35
37
  def accounts
@@ -54,7 +56,7 @@ module Imap::Backup
54
56
  @data ||=
55
57
  begin
56
58
  if File.exist?(pathname)
57
- Utils.check_permissions pathname, 0o600
59
+ Utils.check_permissions(pathname, 0o600) if !windows?
58
60
  contents = File.read(pathname)
59
61
  data = JSON.parse(contents, symbolize_names: true)
60
62
  else
@@ -73,9 +75,12 @@ module Imap::Backup
73
75
  accounts.reject! { |a| a[:delete] }
74
76
  end
75
77
 
76
- def mkdir_private(path)
77
- FileUtils.mkdir(path) if !File.directory?(path)
78
+ def make_private(path)
78
79
  FileUtils.chmod(0o700, path) if Utils.mode(path) != 0o700
79
80
  end
81
+
82
+ def windows?
83
+ OS.windows?
84
+ end
80
85
  end
81
86
  end
@@ -13,16 +13,15 @@ module Imap::Backup
13
13
  count = uids.count
14
14
  Imap::Backup.logger.debug "[#{folder.name}] #{count} new messages"
15
15
  uids.each.with_index do |uid, i|
16
- message = folder.fetch(uid)
16
+ body = folder.fetch(uid)
17
17
  log_prefix = "[#{folder.name}] uid: #{uid} (#{i + 1}/#{count}) -"
18
- if message.nil?
18
+ if body.nil?
19
19
  Imap::Backup.logger.debug("#{log_prefix} not available - skipped")
20
20
  next
21
21
  end
22
22
  Imap::Backup.logger.debug(
23
- "#{log_prefix} #{message['RFC822'].size} bytes"
23
+ "#{log_prefix} #{body.size} bytes"
24
24
  )
25
- body = message["RFC822"]
26
25
  serializer.save(uid, body)
27
26
  end
28
27
  end
@@ -1,7 +1,12 @@
1
+ require "forwardable"
2
+
1
3
  require "imap/backup/serializer/mbox_store"
2
4
 
3
5
  module Imap::Backup
4
6
  class Serializer::Mbox
7
+ extend Forwardable
8
+ def_delegators :store, :mbox_pathname
9
+
5
10
  attr_reader :path
6
11
  attr_reader :folder
7
12
 
@@ -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,58 @@
1
+ require "thunderbird/local_folder"
2
+ require "thunderbird/profiles"
3
+
4
+ module Imap::Backup
5
+ class Thunderbird::MailboxExporter
6
+ EXPORT_PREFIX = "imap-backup".freeze
7
+
8
+ attr_reader :email
9
+ attr_reader :serializer
10
+ attr_reader :profile
11
+ attr_reader :force
12
+
13
+ def initialize(email, serializer, profile, force: false)
14
+ @email = email
15
+ @serializer = serializer
16
+ @profile = profile
17
+ @force = force
18
+ end
19
+
20
+ def run
21
+ local_folder_ok = local_folder.set_up
22
+ return if !local_folder_ok
23
+
24
+ if local_folder.msf_exists?
25
+ if force
26
+ Kernel.puts "Deleting '#{local_folder.msf_path}' as --force option was supplied"
27
+ File.unlink local_folder.msf_path
28
+ else
29
+ Kernel.puts "Skipping export of '#{serializer.folder}' as '#{local_folder.msf_path}' exists"
30
+ return false
31
+ end
32
+ end
33
+
34
+ if local_folder.exists?
35
+ if force
36
+ Kernel.puts "Overwriting '#{local_folder.path}' as --force option was supplied"
37
+ else
38
+ Kernel.puts "Skipping export of '#{serializer.folder}' as '#{local_folder.path}' exists"
39
+ return false
40
+ end
41
+ end
42
+
43
+ FileUtils.cp serializer.mbox_pathname, local_folder.full_path
44
+
45
+ true
46
+ end
47
+
48
+ private
49
+
50
+ def local_folder
51
+ @local_folder ||= begin
52
+ top_level_folders = [EXPORT_PREFIX, email]
53
+ prefixed_folder_path = File.join(top_level_folders, serializer.folder)
54
+ Thunderbird::LocalFolder.new(profile, prefixed_folder_path)
55
+ end
56
+ end
57
+ end
58
+ end
@@ -3,7 +3,7 @@ module Imap; end
3
3
  module Imap::Backup
4
4
  MAJOR = 4
5
5
  MINOR = 0
6
- REVISION = 1
6
+ REVISION = 5
7
7
  PRE = nil
8
8
  VERSION = [MAJOR, MINOR, REVISION, PRE].compact.map(&:to_s).join(".")
9
9
  end
data/lib/imap/backup.rb CHANGED
@@ -29,6 +29,7 @@ module Imap::Backup
29
29
 
30
30
  def initialize
31
31
  @logger = ::Logger.new($stdout)
32
+ $stdout.sync = true
32
33
  end
33
34
  end
34
35
 
@@ -0,0 +1,16 @@
1
+ require "thunderbird/profiles"
2
+
3
+ class Thunderbird::Install
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 default
14
+ Thunderbird::Profiles.new.profile_for_path(entries[:Default])
15
+ end
16
+ end
@@ -0,0 +1,65 @@
1
+ require "thunderbird/profile"
2
+ require "thunderbird/subdirectory"
3
+
4
+ # A local folder is a file containing emails
5
+ class Thunderbird::LocalFolder
6
+ attr_reader :path
7
+ attr_reader :profile
8
+
9
+ def initialize(profile, path)
10
+ @profile = profile
11
+ @path = path
12
+ end
13
+
14
+ def set_up
15
+ return if path_elements.empty?
16
+
17
+ return true if !in_subdirectory?
18
+
19
+ subdirectory.set_up
20
+ end
21
+
22
+ def full_path
23
+ if in_subdirectory?
24
+ File.join(subdirectory.full_path, folder_name)
25
+ else
26
+ folder_name
27
+ end
28
+ end
29
+
30
+ def exists?
31
+ File.exist?(full_path)
32
+ end
33
+
34
+ def msf_path
35
+ "#{path}.msf"
36
+ end
37
+
38
+ def msf_exists?
39
+ File.exist?(msf_path)
40
+ end
41
+
42
+ private
43
+
44
+ def in_subdirectory?
45
+ path_elements.count > 1
46
+ end
47
+
48
+ def subdirectory
49
+ return nil if !in_subdirectory?
50
+
51
+ Thunderbird::Subdirectory.new(profile, subdirectory_path)
52
+ end
53
+
54
+ def path_elements
55
+ path.split(File::SEPARATOR)
56
+ end
57
+
58
+ def subdirectory_path
59
+ File.join(path_elements[0..-2])
60
+ end
61
+
62
+ def folder_name
63
+ path_elements[-1]
64
+ end
65
+ 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,71 @@
1
+ require "thunderbird"
2
+ require "thunderbird/install"
3
+ require "thunderbird/profile"
4
+
5
+ # http://kb.mozillazine.org/Profiles.ini_file
6
+ class Thunderbird::Profiles
7
+ def profile_for_path(path)
8
+ title, entries = blocks.find { |_name, entries| entries[:Path] == path }
9
+
10
+ Thunderbird::Profile.new(title, entries) if title
11
+ end
12
+
13
+ def profile(name)
14
+ title, entries = blocks.find { |_name, entries| entries[:Name] == name }
15
+
16
+ Thunderbird::Profile.new(title, entries) if title
17
+ end
18
+
19
+ def installs
20
+ @installs ||= begin
21
+ pairs = blocks.filter { |name, _entries| name.start_with?("Install") }
22
+ pairs.map { |title, entries| Thunderbird::Install.new(title, entries) }
23
+ end
24
+ end
25
+
26
+ private
27
+
28
+ # Parse profiles.ini.
29
+ # Blocks start with a title, e.g. '[Abc]'
30
+ # and are followed by a number of lines
31
+ def blocks
32
+ @blocks ||= begin
33
+ blocks = {}
34
+ File.open(profiles_ini_path, "rb") do |f|
35
+ title = nil
36
+ entries = nil
37
+
38
+ loop do
39
+ line = f.gets
40
+ if !line
41
+ blocks[title] = entries if title
42
+ break
43
+ end
44
+
45
+ line.chomp!
46
+
47
+ # Is this line the start of a new block
48
+ match = line.match(/\A\[([A-Za-z0-9]+)\]\z/)
49
+ if match
50
+ # Store what we got before this title as a new block
51
+ blocks[title] = entries if title
52
+
53
+ # Start a new block
54
+ title = match[1]
55
+ entries = {}
56
+ elsif line != ""
57
+ # Collect entries until we get to the next title
58
+ key, value = line.split("=")
59
+ entries[key.to_sym] = value
60
+ end
61
+ end
62
+ end
63
+
64
+ blocks
65
+ end
66
+ end
67
+
68
+ def profiles_ini_path
69
+ File.join(Thunderbird.new.data_path, "profiles.ini")
70
+ end
71
+ end
@@ -0,0 +1,96 @@
1
+ require "thunderbird/subdirectory_placeholder"
2
+
3
+ class Thunderbird::Subdirectory
4
+ # `path` is the UI path, it doesn't have the '.sbd' extensions
5
+ # that are present in the real, file system path
6
+ attr_reader :path
7
+ attr_reader :profile
8
+
9
+ def initialize(profile, path)
10
+ @profile = profile
11
+ @path = path
12
+ end
13
+
14
+ def set_up
15
+ raise "Cannot create a subdirectory without a path" if !sub_directory?
16
+
17
+ if sub_sub_directory?
18
+ parent_ok = parent.set_up
19
+ return false if !parent_ok
20
+ end
21
+
22
+ ok = check
23
+ return false if !ok
24
+
25
+ FileUtils.mkdir_p full_path
26
+ placeholder.touch
27
+
28
+ true
29
+ end
30
+
31
+ # subdirectory relative path is 'Foo.sbd/Bar.sbd/Baz.sbd'
32
+ def full_path
33
+ relative_path = File.join(subdirectories)
34
+ File.join(profile.local_folders_path, relative_path)
35
+ end
36
+
37
+ private
38
+
39
+ def sub_directory?
40
+ path_elements.any?
41
+ end
42
+
43
+ def sub_sub_directory?
44
+ path_elements.count > 1
45
+ end
46
+
47
+ def parent
48
+ return nil if !sub_sub_directory?
49
+
50
+ self.class.new(profile, File.join(path_elements[0..-2]))
51
+ end
52
+
53
+ # placeholder relative path is 'Foo.sbd/Bar.sbd/Baz'
54
+ def placeholder
55
+ @placeholder = begin
56
+ relative_path = File.join(subdirectories[0..-2], path_elements[-1])
57
+ path = File.join(profile.local_folders_path, relative_path)
58
+ Thunderbird::SubdirectoryPlaceholder.new(path)
59
+ end
60
+ end
61
+
62
+ def path_elements
63
+ path.split(File::SEPARATOR)
64
+ end
65
+
66
+ def exists?
67
+ File.exist?(full_path)
68
+ end
69
+
70
+ def directory?
71
+ File.directory?(full_path)
72
+ end
73
+
74
+ def subdirectories
75
+ path_elements.map { |p| "#{p}.sbd" }
76
+ end
77
+
78
+ def check
79
+ case
80
+ when placeholder.exists? && !exists?
81
+ Kernel.puts "Can't set up folder '#{folder_path}': '#{placeholder.path}' exists, but '#{full_path}' is missing"
82
+ false
83
+ when exists? && !placeholder.exists?
84
+ Kernel.puts "Can't set up folder '#{folder_path}': '#{full_path}' exists, but '#{placeholder.path}' is missing"
85
+ false
86
+ when placeholder.exists? && !placeholder.regular?
87
+ Kernel.puts "Can't set up folder '#{folder_path}': '#{placeholder.path}' exists, but it is not a regular file"
88
+ false
89
+ when exists? && !directory?
90
+ Kernel.puts "Can't set up folder '#{folder_path}': '#{full_path}' exists, but it is not a directory"
91
+ false
92
+ else
93
+ true
94
+ end
95
+ end
96
+ 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::SubdirectoryPlaceholder
4
+ attr_reader :path
5
+
6
+ def initialize(path)
7
+ @path = path
8
+ end
9
+
10
+ def exists?
11
+ File.exist?(path)
12
+ end
13
+
14
+ def regular?
15
+ File.file?(path)
16
+ end
17
+
18
+ def touch
19
+ FileUtils.touch path
20
+ end
21
+ end
@@ -0,0 +1,14 @@
1
+ require "os"
2
+
3
+ class Thunderbird
4
+ def data_path
5
+ case
6
+ when OS.linux?
7
+ File.join(Dir.home, ".thunderbird")
8
+ when OS.mac?
9
+ File.join(Dir.home, "Library", "Thunderbird")
10
+ when OS.windows?
11
+ File.join(ENV["APPDATA"].gsub("\\", "/"), "Thunderbird")
12
+ end
13
+ end
14
+ end