imap-backup 0.0.5 → 1.0.0

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