imap-backup 0.0.5 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. data/README.md +43 -6
  2. data/bin/imap-backup +0 -1
  3. data/imap-backup.gemspec +1 -1
  4. data/lib/email/mboxrd/message.rb +37 -0
  5. data/lib/imap/backup.rb +2 -0
  6. data/lib/imap/backup/account/connection.rb +2 -4
  7. data/lib/imap/backup/account/folder.rb +0 -2
  8. data/lib/imap/backup/configuration/account.rb +0 -2
  9. data/lib/imap/backup/configuration/asker.rb +0 -2
  10. data/lib/imap/backup/configuration/folder_chooser.rb +0 -2
  11. data/lib/imap/backup/configuration/list.rb +17 -16
  12. data/lib/imap/backup/configuration/setup.rb +1 -3
  13. data/lib/imap/backup/configuration/store.rb +19 -13
  14. data/lib/imap/backup/downloader.rb +0 -2
  15. data/lib/imap/backup/serializer/base.rb +17 -0
  16. data/lib/imap/backup/serializer/directory.rb +3 -7
  17. data/lib/imap/backup/serializer/mbox.rb +96 -0
  18. data/lib/imap/backup/utils.rb +6 -3
  19. data/lib/imap/backup/version.rb +2 -2
  20. data/spec/unit/account/connection_spec.rb +12 -13
  21. data/spec/unit/account/folder_spec.rb +1 -9
  22. data/spec/unit/configuration/account_spec.rb +1 -16
  23. data/spec/unit/configuration/asker_spec.rb +1 -9
  24. data/spec/unit/configuration/connection_tester_spec.rb +1 -5
  25. data/spec/unit/configuration/folder_chooser_spec.rb +1 -7
  26. data/spec/unit/configuration/list_spec.rb +24 -20
  27. data/spec/unit/configuration/setup_spec.rb +3 -9
  28. data/spec/unit/configuration/store_spec.rb +11 -20
  29. data/spec/unit/downloader_spec.rb +1 -11
  30. data/spec/unit/email/mboxrd/message_spec.rb +51 -0
  31. data/spec/unit/serializer/base_spec.rb +19 -0
  32. data/spec/unit/serializer/directory_spec.rb +51 -74
  33. data/spec/unit/serializer/mbox_spec.rb +113 -0
  34. data/spec/unit/utils_spec.rb +3 -9
  35. metadata +29 -4
data/README.md CHANGED
@@ -14,19 +14,23 @@
14
14
 
15
15
  # Installation
16
16
 
17
- $ gem install 'imap-backup'
17
+ ```shell
18
+ $ gem install 'imap-backup'
19
+ ```
18
20
 
19
21
  # Setup
20
22
 
21
23
  Run:
22
24
 
23
- $ imap-backup setup
25
+ ```shell
26
+ $ imap-backup setup
27
+ ```
24
28
 
25
29
  The setup system is a menu-driven command line application.
26
30
 
27
31
  It creates ~/.imap-backup directory and configuration file. E.g.:
28
32
 
29
- ```
33
+ ```json
30
34
  {
31
35
  "accounts":
32
36
  [
@@ -44,6 +48,27 @@ It creates ~/.imap-backup directory and configuration file. E.g.:
44
48
  }
45
49
  ```
46
50
 
51
+ It connects to GMail by default, but you can also specify a server:
52
+
53
+ ```json
54
+ {
55
+ "accounts":
56
+ [
57
+ {
58
+ "username": "my.user@gmail.com",
59
+ "password": "secret",
60
+ "server": "my.imap.example.com",
61
+ "local_path": "/path/to/backup/root",
62
+ "folders":
63
+ [
64
+ {"name": "[Gmail]/All Mail"},
65
+ {"name": "my_folder"}
66
+ ]
67
+ }
68
+ ]
69
+ }
70
+ ```
71
+
47
72
  # Security
48
73
 
49
74
  Note that email usernames and passwords are held in plain text
@@ -57,19 +82,31 @@ by your user.
57
82
 
58
83
  Manually, from the command line:
59
84
 
60
- $ imap-backup
85
+ ```shell
86
+ $ imap-backup
87
+ ```
61
88
 
62
89
  Altertatively, add it to your crontab.
63
90
 
91
+ # Result
92
+
93
+ Each folder is saved to an mbox file.
94
+ Alongside each mbox is a fine *.imap which lists the source IMAP UIDs to allow
95
+ a full restore.
96
+
64
97
  # Other Usage
65
98
 
66
99
  List IMAP folders:
67
100
 
68
- imap-backup folders
101
+ ```shell
102
+ $ imap-backup folders
103
+ ```
69
104
 
70
105
  Get statistics of emails to download per folder:
71
106
 
72
- imap-backup status
107
+ ```shell
108
+ $ imap-backup status
109
+ ```
73
110
 
74
111
  # Design Goals
75
112
 
data/bin/imap-backup CHANGED
@@ -34,7 +34,6 @@ opts = OptionParser.new do |opts|
34
34
  puts opts
35
35
  exit
36
36
  end
37
-
38
37
  end
39
38
  opts.parse!
40
39
 
data/imap-backup.gemspec CHANGED
@@ -18,6 +18,7 @@ Gem::Specification.new do |gem|
18
18
 
19
19
  gem.add_runtime_dependency 'rake'
20
20
  gem.add_runtime_dependency 'highline'
21
+ gem.add_runtime_dependency 'mail'
21
22
  if RUBY_VERSION < '1.9'
22
23
  gem.add_runtime_dependency 'json'
23
24
  end
@@ -30,6 +31,5 @@ Gem::Specification.new do |gem|
30
31
  else
31
32
  gem.add_development_dependency 'simplecov'
32
33
  end
33
-
34
34
  end
35
35
 
@@ -0,0 +1,37 @@
1
+ require 'mail'
2
+
3
+ module Email
4
+ module Mboxrd
5
+ class Message
6
+ def initialize(body)
7
+ @body = body.clone
8
+ @body.force_encoding('binary') if RUBY_VERSION >= '1.9.0'
9
+ end
10
+
11
+ def to_s
12
+ 'From ' + from + "\n" + body + "\n"
13
+ end
14
+
15
+ private
16
+
17
+ def parsed
18
+ @parsed ||= Mail.new(@body)
19
+ end
20
+
21
+ def from
22
+ parsed.from[0] + ' ' + asctime
23
+ end
24
+
25
+ def body
26
+ mbox = @body.gsub(/\n(>*From)/, "\n>\\1")
27
+ mbox + "\n" unless mbox.end_with?("\n")
28
+ mbox
29
+ end
30
+
31
+ def asctime
32
+ parsed.date.asctime
33
+ end
34
+ end
35
+ end
36
+ end
37
+
data/lib/imap/backup.rb CHANGED
@@ -9,7 +9,9 @@ require 'imap/backup/configuration/list'
9
9
  require 'imap/backup/configuration/setup'
10
10
  require 'imap/backup/configuration/store'
11
11
  require 'imap/backup/downloader'
12
+ require 'imap/backup/serializer/base'
12
13
  require 'imap/backup/serializer/directory'
14
+ require 'imap/backup/serializer/mbox'
13
15
  require 'imap/backup/version'
14
16
 
15
17
  module Imap
@@ -4,14 +4,13 @@ module Imap
4
4
  module Backup
5
5
  module Account
6
6
  class Connection
7
-
8
7
  attr_reader :username
9
8
  attr_reader :imap
10
9
 
11
10
  def initialize(options)
12
11
  @username = options[:username]
13
12
  @local_path, @backup_folders = options[:local_path], options[:folders]
14
- @imap = Net::IMAP.new('imap.gmail.com', 993, true)
13
+ @imap = Net::IMAP.new(options[:server] || 'imap.gmail.com', 993, true)
15
14
  @imap.login(@username, options[:password])
16
15
  end
17
16
 
@@ -34,12 +33,11 @@ module Imap
34
33
  def run_backup
35
34
  @backup_folders.each do |folder|
36
35
  f = Imap::Backup::Account::Folder.new(self, folder[:name])
37
- s = Imap::Backup::Serializer::Directory.new(@local_path, folder[:name])
36
+ s = Imap::Backup::Serializer::Mbox.new(@local_path, folder[:name])
38
37
  d = Imap::Backup::Downloader.new(f, s)
39
38
  d.run
40
39
  end
41
40
  end
42
-
43
41
  end
44
42
  end
45
43
  end
@@ -4,7 +4,6 @@ module Imap
4
4
  module Backup
5
5
  module Account
6
6
  class Folder
7
-
8
7
  REQUESTED_ATTRIBUTES = ['RFC822', 'FLAGS', 'INTERNALDATE']
9
8
 
10
9
  def initialize(connection, folder)
@@ -22,7 +21,6 @@ module Imap
22
21
  message['RFC822'].force_encoding('utf-8') if RUBY_VERSION > '1.9'
23
22
  message
24
23
  end
25
-
26
24
  end
27
25
  end
28
26
  end
@@ -4,7 +4,6 @@ module Imap
4
4
  module Backup
5
5
  module Configuration
6
6
  class Account
7
-
8
7
  def initialize(store, account)
9
8
  @store, @account = store, account
10
9
  end
@@ -77,7 +76,6 @@ EOT
77
76
  end
78
77
  end
79
78
  end
80
-
81
79
  end
82
80
  end
83
81
  end
@@ -4,7 +4,6 @@ module Imap
4
4
  module Backup
5
5
  module Configuration
6
6
  module Asker
7
-
8
7
  EMAIL_MATCHER = /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i
9
8
 
10
9
  def self.email(default = '')
@@ -34,7 +33,6 @@ module Imap
34
33
  q.responses[:not_valid] = 'Choose a different directory '
35
34
  end
36
35
  end
37
-
38
36
  end
39
37
  end
40
38
  end
@@ -4,7 +4,6 @@ module Imap
4
4
  module Backup
5
5
  module Configuration
6
6
  class FolderChooser
7
-
8
7
  def initialize(account)
9
8
  @account = account
10
9
  end
@@ -48,7 +47,6 @@ module Imap
48
47
  end
49
48
  end
50
49
  end
51
-
52
50
  end
53
51
  end
54
52
  end
@@ -4,28 +4,29 @@ module Imap
4
4
  module Backup
5
5
  module Configuration
6
6
  class List
7
+ attr_reader :accounts
7
8
 
8
- attr_reader :accounts
9
+ def initialize(accounts = nil)
10
+ if not Imap::Backup::Configuration::Store.exist?
11
+ raise ConfigurationNotFound.new("Configuration file '#{Imap::Backup::Configuration::Store.default_pathname}' not found")
12
+ end
13
+ @config = Imap::Backup::Configuration::Store.new
9
14
 
10
- def initialize(accounts = nil)
11
- @config = Imap::Backup::Configuration::Store.new
12
-
13
- if accounts.nil?
14
- @accounts = @config.data[:accounts]
15
- else
16
- @accounts = @config.data[:accounts].select{ |account| accounts.include?(account[:username]) }
15
+ if accounts.nil?
16
+ @accounts = @config.data[:accounts]
17
+ else
18
+ @accounts = @config.data[:accounts].select{ |account| accounts.include?(account[:username]) }
19
+ end
17
20
  end
18
- end
19
21
 
20
- def each_connection
21
- @accounts.each do |account|
22
- connection = Imap::Backup::Account::Connection.new(account)
23
- yield connection
24
- connection.disconnect
22
+ def each_connection
23
+ @accounts.each do |account|
24
+ connection = Imap::Backup::Account::Connection.new(account)
25
+ yield connection
26
+ connection.disconnect
27
+ end
25
28
  end
26
29
  end
27
-
28
- end
29
30
  end
30
31
  end
31
32
  end
@@ -6,14 +6,13 @@ module Imap
6
6
  module Backup
7
7
  module Configuration
8
8
  class Setup
9
-
10
9
  class << self
11
10
  attr_accessor :highline
12
11
  end
13
12
  self.highline = HighLine.new
14
13
 
15
14
  def initialize
16
- @config = Imap::Backup::Configuration::Store.new(false)
15
+ @config = Imap::Backup::Configuration::Store.new
17
16
  end
18
17
 
19
18
  def run
@@ -60,7 +59,6 @@ module Imap
60
59
  end
61
60
  Account.new(@config, account).run
62
61
  end
63
-
64
62
  end
65
63
  end
66
64
  end
@@ -6,41 +6,44 @@ module Imap
6
6
  module Backup
7
7
  module Configuration
8
8
  class Store
9
-
10
9
  CONFIGURATION_DIRECTORY = File.expand_path('~/.imap-backup')
11
10
 
12
11
  attr_reader :data
13
12
  attr_reader :path
14
13
 
15
- def initialize(fail_if_missing = true)
16
- @path = CONFIGURATION_DIRECTORY
17
- @pathname = File.join(@path, 'config.json')
18
- if File.directory?(@path)
19
- Imap::Backup::Utils.check_permissions @path, 0700
14
+ def self.default_pathname
15
+ File.join(CONFIGURATION_DIRECTORY, 'config.json')
16
+ end
17
+
18
+ def self.exist?(pathname = default_pathname)
19
+ File.exist?(pathname)
20
+ end
21
+
22
+ def initialize(pathname = self.class.default_pathname)
23
+ @pathname = pathname
24
+ if File.directory?(path)
25
+ Imap::Backup::Utils.check_permissions path, 0700
20
26
  end
21
27
  if File.exist?(@pathname)
22
28
  Imap::Backup::Utils.check_permissions @pathname, 0600
23
29
  @data = JSON.parse(File.read(@pathname), :symbolize_names => true)
24
30
  else
25
- if fail_if_missing
26
- raise ConfigurationNotFound.new("Configuration file '#{@pathname}' not found")
27
- end
28
31
  @data = {:accounts => []}
29
32
  end
30
33
  end
31
34
 
32
35
  def save
33
- mkdir_private @path
36
+ mkdir_private path
34
37
  File.open(@pathname, 'w') { |f| f.write(JSON.pretty_generate(@data)) }
35
38
  FileUtils.chmod 0600, @pathname
36
39
  @data[:accounts].each do |account|
37
40
  mkdir_private account[:local_path]
38
41
  account[:folders].each do |f|
39
42
  parts = f[:name].split('/')
40
- path = account[:local_path].clone
43
+ p = account[:local_path].clone
41
44
  parts.each do |part|
42
- path = File.join(path, part)
43
- mkdir_private path
45
+ p = File.join(p, part)
46
+ mkdir_private p
44
47
  end
45
48
  end
46
49
  end
@@ -57,6 +60,9 @@ module Imap
57
60
  end
58
61
  end
59
62
 
63
+ def path
64
+ File.dirname(@pathname)
65
+ end
60
66
  end
61
67
  end
62
68
  end
@@ -5,7 +5,6 @@ require 'json'
5
5
  module Imap
6
6
  module Backup
7
7
  class Downloader
8
-
9
8
  def initialize(folder, serializer)
10
9
  @folder, @serializer = folder, serializer
11
10
  end
@@ -17,7 +16,6 @@ module Imap
17
16
  @serializer.save(uid, message)
18
17
  end
19
18
  end
20
-
21
19
  end
22
20
  end
23
21
  end
@@ -0,0 +1,17 @@
1
+ # encoding: utf-8
2
+
3
+ module Imap
4
+ module Backup
5
+ module Serializer
6
+ DIRECTORY_PERMISSIONS = 0700
7
+ FILE_PERMISSIONS = 0600
8
+ class Base
9
+ def initialize(path, folder)
10
+ @path, @folder = path, folder
11
+ Imap::Backup::Utils.check_permissions(@path, DIRECTORY_PERMISSIONS)
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
17
+
@@ -4,13 +4,10 @@ require 'fileutils'
4
4
  module Imap
5
5
  module Backup
6
6
  module Serializer
7
- class Directory
8
-
7
+ class Directory < Base
9
8
  def initialize(path, folder)
10
- @path, @folder = path, folder
11
- permissions = 0700
12
- Imap::Backup::Utils.check_permissions(@path, permissions)
13
- Imap::Backup::Utils.make_folder(@path, @folder, permissions)
9
+ super
10
+ Imap::Backup::Utils.make_folder(@path, @folder, DIRECTORY_PERMISSIONS)
14
11
  end
15
12
 
16
13
  def uids
@@ -43,7 +40,6 @@ module Imap
43
40
  def filename(uid)
44
41
  "#{directory}/%012u.json" % uid.to_i
45
42
  end
46
-
47
43
  end
48
44
  end
49
45
  end