imap-backup 0.0.1 → 0.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.
data/README.md CHANGED
@@ -1,5 +1,4 @@
1
- imap-backup [![Build Status](https://secure.travis-ci.org/joeyates/imap-backup.png)][Continuous Integration]
2
- ===========
1
+ # imap-backup [![Build Status](https://secure.travis-ci.org/joeyates/imap-backup.png)][Continuous Integration]
3
2
 
4
3
  *Backup GMail (or other IMAP) accounts to disk*
5
4
 
@@ -13,31 +12,35 @@ imap-backup [![Build Status](https://secure.travis-ci.org/joeyates/imap-backup.p
13
12
  [Rubygem]: http://rubygems.org/gems/imap-backup "Ruby gem at rubygems.org"
14
13
  [Continuous Integration]: http://travis-ci.org/joeyates/imap-backup "Build status by Travis-CI"
15
14
 
16
- Installation
17
- ============
18
-
19
- Add this line to your application's Gemfile:
20
-
21
- gem 'imap/backup'
22
-
23
- And then execute:
24
-
25
- $ bundle
26
-
27
- Or install it yourself as:
15
+ # Installation
28
16
 
29
17
  gem install 'imap-backup'
30
18
 
31
- Basic Usage
32
- ===========
19
+ # Basic Usage
33
20
 
34
21
  * Create ~/.imap-backup
22
+
23
+ {
24
+ accounts:
25
+ [
26
+ {
27
+ username: "my.user@gmail.com",
28
+ password: "secret",
29
+ local_path: "/path/to/backup/root",
30
+ folders:
31
+ [
32
+ {name: "[Gmail]/All Mail"},
33
+ {name: "my_folder"}
34
+ ]
35
+ }
36
+ ]
37
+ }
38
+
35
39
  * Run
36
40
 
37
41
  imap-backup
38
42
 
39
- Usage
40
- =====
43
+ # Usage
41
44
 
42
45
  Check connection:
43
46
 
@@ -47,18 +50,24 @@ List IMAP folders:
47
50
 
48
51
  imap-backup --list
49
52
 
50
- Design Goals
51
- ============
53
+ # Design Goals
52
54
 
53
55
  * Secure - use a local file protected by permissions
54
56
  * Restartable - calculate start point based on alreadt downloaded messages
55
57
  * Standards compliant - save emails in a standard format
56
58
  * Standalone - does not rely on an email client or MTA
57
59
 
58
- Similar Software
59
- ================
60
+ # Similar Software
60
61
 
61
62
  * https://github.com/thefloweringash/gmail-imap-backup
62
63
  * https://github.com/mleonhard/imapbackup
63
64
  * https://github.com/rgrove/larch - copies between IMAP servers
64
65
 
66
+ ## Contributing
67
+
68
+ 1. Fork it
69
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
70
+ 3. Commit your changes (`git commit -am 'Added some feature'`)
71
+ 4. Push to the branch (`git push origin my-new-feature`)
72
+ 5. Create new Pull Request
73
+
data/bin/imap-backup CHANGED
@@ -1,20 +1,71 @@
1
1
  #!/usr/bin/env ruby
2
2
  # -*- encoding: utf-8 -*-
3
3
 
4
+ require 'optparse'
5
+
4
6
  $:.unshift(File.expand_path('../../lib/', __FILE__))
5
7
  require 'imap/backup'
6
8
 
7
- settings = Imap::Backup::Settings.new
9
+ KNOWN_COMMANDS = [
10
+ {:name => 'backup', :help => 'Do the backup (default)'},
11
+ {:name => 'folders', :help => 'List folders for all (or selected) accounts'},
12
+ {:name => 'status', :help => 'List count of non backed-up emails per folder'},
13
+ {:name => 'help', :help => 'Show usage'},
14
+ ]
15
+
16
+ options = {:command => 'backup'}
17
+ opts = OptionParser.new do |opts|
18
+ opts.banner = "Usage: #{$0} [options] COMMAND"
19
+
20
+ opts.separator ''
21
+ opts.separator 'Commands:'
22
+ KNOWN_COMMANDS.each do |command|
23
+ opts.separator "\t%- 20s %s" % [command[:name], command[:help]]
24
+ end
25
+ opts.separator ''
26
+ opts.separator 'Common options:'
27
+
28
+ opts.on('-a', '--accounts ACCOUNT1[,ACCOUNT2,...]', Array, 'only these accounts') do |account|
29
+ options[:account] = account
30
+ end
31
+
32
+ opts.on_tail("-h", "--help", "Show usage") do
33
+ puts opts
34
+ exit
35
+ end
36
+
37
+ end
38
+ opts.parse!
39
+
40
+ if ARGV.size > 0
41
+ options[:command] = ARGV.shift
42
+ end
43
+
44
+ if KNOWN_COMMANDS.find{|c| c[:name] == options[:command] }.nil?
45
+ raise "Unknown command '#{options[:command]}'"
46
+ end
47
+
48
+ settings = Imap::Backup::Settings.new(options[:accounts])
8
49
 
9
- if ARGV[0] == '--list'
10
- settings.each_account do |account|
11
- account.folders.each { |f| puts f.name }
50
+ case options[:command]
51
+ when 'backup'
52
+ settings.each_connection do |connection|
53
+ connection.run_backup
54
+ end
55
+ when 'help'
56
+ puts opts
57
+ when 'folders'
58
+ settings.each_connection do |connection|
59
+ puts connection.username
60
+ connection.folders.each { |f| puts "\t" + f.name }
12
61
  end
13
- else
14
- settings.each_account do |account|
15
- account.backup_folders.each do |folder|
16
- d = Imap::Backup::Downloader.new(account, folder['name'])
17
- d.run
62
+ when 'status'
63
+ settings.each_connection do |connection|
64
+ puts connection.username
65
+ folders = connection.status
66
+ folders.each do |f|
67
+ missing_locally = f[:remote] - f[:local]
68
+ puts "#{f[:name]}: #{missing_locally.size}"
18
69
  end
19
70
  end
20
71
  end
data/imap-backup.gemspec CHANGED
@@ -16,6 +16,11 @@ Gem::Specification.new do |gem|
16
16
  gem.require_paths = ['lib']
17
17
  gem.version = Imap::Backup::VERSION
18
18
 
19
+ gem.add_runtime_dependency 'rake'
20
+ if RUBY_VERSION < '1.9'
21
+ gem.add_runtime_dependency 'json'
22
+ end
23
+
19
24
  gem.add_development_dependency 'pry'
20
25
  gem.add_development_dependency 'pry-doc'
21
26
  gem.add_development_dependency 'rspec', '>= 2.3.0'
data/lib/imap/backup.rb CHANGED
@@ -1,6 +1,8 @@
1
1
  require 'imap/backup/utils'
2
- require 'imap/backup/account'
2
+ require 'imap/backup/account/connection'
3
+ require 'imap/backup/account/folder'
3
4
  require 'imap/backup/downloader'
5
+ require 'imap/backup/serializer/directory'
4
6
  require 'imap/backup/settings'
5
7
  require 'imap/backup/version'
6
8
 
@@ -0,0 +1,47 @@
1
+ require 'net/imap'
2
+
3
+ module Imap
4
+ module Backup
5
+ module Account
6
+ class Connection
7
+
8
+ attr_reader :username
9
+ attr_reader :imap
10
+
11
+ def initialize(options)
12
+ @username = options[:username]
13
+ @local_path, @backup_folders = options[:local_path], options[:folders]
14
+ @imap = Net::IMAP.new('imap.gmail.com', 993, true)
15
+ @imap.login(@username, options[:password])
16
+ end
17
+
18
+ def disconnect
19
+ @imap.disconnect
20
+ end
21
+
22
+ def folders
23
+ @imap.list('/', '*')
24
+ end
25
+
26
+ def status
27
+ @backup_folders.map do |folder|
28
+ f = Imap::Backup::Account::Folder.new(self, folder[:name])
29
+ s = Imap::Backup::Serializer::Directory.new(@local_path, folder[:name])
30
+ {:name => folder[:name], :local => s.uids, :remote => f.uids}
31
+ end
32
+ end
33
+
34
+ def run_backup
35
+ @backup_folders.each do |folder|
36
+ f = Imap::Backup::Account::Folder.new(self, folder[:name])
37
+ s = Imap::Backup::Serializer::Directory.new(@local_path, folder[:name])
38
+ d = Imap::Backup::Downloader.new(f, s)
39
+ d.run
40
+ end
41
+ end
42
+
43
+ end
44
+ end
45
+ end
46
+ end
47
+
@@ -0,0 +1,30 @@
1
+ # encoding: utf-8
2
+
3
+ module Imap
4
+ module Backup
5
+ module Account
6
+ class Folder
7
+
8
+ REQUESTED_ATTRIBUTES = ['RFC822', 'FLAGS', 'INTERNALDATE']
9
+
10
+ def initialize(connection, folder)
11
+ @connection, @folder = connection, folder
12
+ end
13
+
14
+ def uids
15
+ @connection.imap.examine(@folder)
16
+ @connection.imap.uid_search(['ALL']).sort
17
+ end
18
+
19
+ def fetch(uid)
20
+ @connection.imap.examine(@folder)
21
+ message = @connection.imap.uid_fetch([uid.to_i], REQUESTED_ATTRIBUTES)[0][1]
22
+ message['RFC822'].force_encoding('utf-8') if RUBY_VERSION > '1.9'
23
+ message
24
+ end
25
+
26
+ end
27
+ end
28
+ end
29
+ end
30
+
@@ -4,25 +4,15 @@ module Imap
4
4
  module Backup
5
5
  class Downloader
6
6
 
7
- include Imap::Backup::Utils
8
-
9
- def initialize(account, folder)
10
- @account, @folder = account, folder
11
-
12
- check_permissions(@account.local_path, 0700)
7
+ def initialize(folder, serializer)
8
+ @folder, @serializer = folder, serializer
13
9
  end
14
10
 
15
11
  def run
16
- make_folder(@account.local_path, @folder, 'g-wrx,o-wrx')
17
- destination_path = File.join(@account.local_path, @folder)
18
- @account.each_uid(@folder) do |uid|
19
- message_filename = "#{destination_path}/%012u.json" % uid.to_i
20
- next if File.exist?(message_filename)
21
-
22
- message = @account.fetch(uid)
23
-
24
- File.open(message_filename, 'w') { |f| f.write message.to_json }
25
- FileUtils.chmod 0600, message_filename
12
+ uids = @folder.uids - @serializer.uids
13
+ uids.each do |uid|
14
+ message = @folder.fetch(uid)
15
+ @serializer.save(uid, message)
26
16
  end
27
17
  end
28
18
 
@@ -0,0 +1,51 @@
1
+ # encoding: utf-8
2
+
3
+ module Imap
4
+ module Backup
5
+ module Serializer
6
+ class Directory
7
+
8
+ include Imap::Backup::Utils
9
+
10
+ def initialize(path, folder)
11
+ @path, @folder = path, folder
12
+ check_permissions(@path, 0700)
13
+ make_folder(@path, @folder, 'g-wrx,o-wrx')
14
+ end
15
+
16
+ def uids
17
+ return [] if ! File.exist?(directory)
18
+
19
+ d = Dir.open(directory)
20
+ as_strings = d.map do |file|
21
+ file[/^0*(\d+).json$/, 1]
22
+ end.compact
23
+ as_strings.map(&:to_i).sort
24
+ end
25
+
26
+ def exist?(uid)
27
+ message_filename = filename(uid)
28
+ File.exist?(message_filename)
29
+ end
30
+
31
+ def save(uid, message)
32
+ message_filename = filename(uid)
33
+ File.open(message_filename, 'w') { |f| f.write message.to_json }
34
+ FileUtils.chmod 0600, message_filename
35
+ end
36
+
37
+ private
38
+
39
+ def directory
40
+ File.join(@path, @folder)
41
+ end
42
+
43
+ def filename(uid)
44
+ "#{directory}/%012u.json" % uid.to_i
45
+ end
46
+
47
+ end
48
+ end
49
+ end
50
+ end
51
+
@@ -7,18 +7,25 @@ module Imap
7
7
 
8
8
  include Imap::Backup::Utils
9
9
 
10
- def initialize
10
+ attr_reader :accounts
11
+
12
+ def initialize(accounts = nil)
11
13
  config_pathname = File.expand_path('~/.imap-backup/config.json')
12
14
  raise "Configuration file '#{config_pathname}' not found" if ! File.exist?(config_pathname)
13
15
  check_permissions(config_pathname, 0600)
14
- @settings = JSON.load(File.open(config_pathname))
16
+ @settings = JSON.parse(File.read(config_pathname), :symbolize_names => true)
17
+ if accounts.nil?
18
+ @accounts = @settings[:accounts]
19
+ else
20
+ @accounts = @settings[:accounts].select{ |account| accounts.include?(account[:username]) }
21
+ end
15
22
  end
16
23
 
17
- def each_account
18
- @settings['accounts'].each do |account|
19
- a = Account.new(account)
20
- yield a
21
- a.disconnect
24
+ def each_connection
25
+ @accounts.each do |account|
26
+ connection = Imap::Backup::Account::Connection.new(account)
27
+ yield connection
28
+ connection.disconnect
22
29
  end
23
30
  end
24
31
 
@@ -2,7 +2,7 @@ module Imap
2
2
  module Backup
3
3
  MAJOR = 0
4
4
  MINOR = 0
5
- REVISION = 1
5
+ REVISION = 2
6
6
  VERSION = [ MAJOR, MINOR, REVISION ].map( &:to_s ).join( '.' )
7
7
  end
8
8
  end
@@ -0,0 +1,117 @@
1
+ # encoding: utf-8
2
+ load File.expand_path( '../../spec_helper.rb', File.dirname(__FILE__) )
3
+
4
+ describe Imap::Backup::Account::Connection do
5
+
6
+ context '#initialize' do
7
+
8
+ it 'should login to the imap server' do
9
+ imap = stub('Net::IMAP')
10
+ Net::IMAP.should_receive(:new).with('imap.gmail.com', 993, true).and_return(imap)
11
+ imap.should_receive('login').with('myuser', 'secret')
12
+
13
+ Imap::Backup::Account::Connection.new(:username => 'myuser', :password => 'secret')
14
+ end
15
+
16
+ end
17
+
18
+ context 'instance methods' do
19
+
20
+ before :each do
21
+ @imap = stub('Net::IMAP', :login => nil)
22
+ Net::IMAP.stub!(:new).and_return(@imap)
23
+ @account = {
24
+ :username => 'myuser',
25
+ :password => 'secret',
26
+ :folders => [{:name => 'my_folder'}],
27
+ :local_path => '/base/path',
28
+ }
29
+ end
30
+
31
+ subject { Imap::Backup::Account::Connection.new(@account) }
32
+
33
+ context '#disconnect' do
34
+ it 'should disconnect from the server' do
35
+ @imap.should_receive(:disconnect)
36
+
37
+ subject.disconnect
38
+ end
39
+ end
40
+
41
+ context '#folders' do
42
+ it 'should list all folders' do
43
+ @imap.should_receive(:list).with('/', '*')
44
+
45
+ subject.folders
46
+ end
47
+ end
48
+
49
+
50
+ context '#status' do
51
+
52
+ before :each do
53
+ @folder = stub('Imap::Backup::Account::Folder', :uids => [])
54
+ Imap::Backup::Account::Folder.stub!(:new).with(subject, 'my_folder').and_return(@folder)
55
+ @serializer = stub('Imap::Backup::Serializer', :uids => [])
56
+ Imap::Backup::Serializer::Directory.stub!(:new).with('/base/path', 'my_folder').and_return(@serializer)
57
+ end
58
+
59
+ it 'should return the names of folders' do
60
+ subject.status[0][:name].should == 'my_folder'
61
+ end
62
+
63
+ it 'should list local message uids' do
64
+ @serializer.should_receive(:uids).and_return([321, 456])
65
+
66
+ subject.status[0][:local].should == [321, 456]
67
+ end
68
+
69
+ it 'should retrieve the available uids' do
70
+ @folder.should_receive(:uids).and_return([101, 234])
71
+
72
+ subject.status[0][:remote].should == [101, 234]
73
+ end
74
+
75
+ end
76
+
77
+ context '#run_backup' do
78
+
79
+ before :each do
80
+ @folder = stub('Imap::Backup::Account::Folder', :uids => [])
81
+ Imap::Backup::Account::Folder.stub!(:new).with(subject, 'my_folder').and_return(@folder)
82
+ @serializer = stub('Imap::Backup::Serializer')
83
+ Imap::Backup::Serializer::Directory.stub!(:new).with('/base/path', 'my_folder').and_return(@serializer)
84
+ @downloader = stub('Imap::Backup::Downloader', :run => nil)
85
+ Imap::Backup::Downloader.stub!(:new).with(@folder, @serializer).and_return(@downloader)
86
+ end
87
+
88
+ it 'should instantiate folders' do
89
+ Imap::Backup::Account::Folder.should_receive(:new).with(subject, 'my_folder').and_return(@folder)
90
+
91
+ subject.run_backup
92
+ end
93
+
94
+ it 'should instantiate serializers' do
95
+ Imap::Backup::Serializer::Directory.should_receive(:new).with('/base/path', 'my_folder').and_return(@serializer)
96
+
97
+ subject.run_backup
98
+ end
99
+
100
+ it 'should instantiate downloaders' do
101
+ Imap::Backup::Downloader.should_receive(:new).with(@folder, @serializer).and_return(@downloader)
102
+
103
+ subject.run_backup
104
+ end
105
+
106
+ it 'should run downloaders' do
107
+ @downloader.should_receive(:run)
108
+
109
+ subject.run_backup
110
+ end
111
+
112
+ end
113
+
114
+ end
115
+
116
+ end
117
+
@@ -0,0 +1,60 @@
1
+ # encoding: utf-8
2
+
3
+ load File.expand_path('../../spec_helper.rb', File.dirname(__FILE__))
4
+
5
+ describe Imap::Backup::Account::Folder do
6
+
7
+ context 'with instance' do
8
+
9
+ before :each do
10
+ @imap = stub('Net::IMAP')
11
+ @connection = stub('Imap::Backup::Account::Connection', :imap => @imap)
12
+ end
13
+
14
+ subject { Imap::Backup::Account::Folder.new(@connection, 'my_folder') }
15
+
16
+ context '#uids' do
17
+
18
+ it 'should list available messages' do
19
+ @imap.should_receive(:examine).with('my_folder')
20
+ @imap.should_receive(:uid_search).with(['ALL']).and_return([5678, 123])
21
+
22
+ subject.uids.should == [123, 5678]
23
+ end
24
+
25
+ end
26
+
27
+ context '#fetch' do
28
+ before :each do
29
+ @message_body = 'the body'
30
+ @message = {
31
+ 'RFC822' => @message_body,
32
+ 'other' => 'xxx'
33
+ }
34
+ end
35
+
36
+ it 'should request the message, the flags and the date' do
37
+ @imap.should_receive(:examine).with('my_folder')
38
+ @imap.should_receive(:uid_fetch).
39
+ with([123], ['RFC822', 'FLAGS', 'INTERNALDATE']).
40
+ and_return([[nil, @message]])
41
+
42
+ subject.fetch(123)
43
+ end
44
+
45
+ if RUBY_VERSION > '1.9'
46
+ it 'should set the encoding on the message' do
47
+ @imap.stub!(:examine => nil, :uid_fetch => [[nil, @message]])
48
+
49
+ @message_body.should_receive(:force_encoding).with('utf-8')
50
+
51
+ subject.fetch(123)
52
+ end
53
+ end
54
+
55
+ end
56
+
57
+ end
58
+
59
+ end
60
+
@@ -3,129 +3,78 @@ load File.expand_path( '../spec_helper.rb', File.dirname(__FILE__) )
3
3
 
4
4
  describe Imap::Backup::Downloader do
5
5
 
6
- context '#initialize' do
7
-
8
- it 'should fail if download path file permissions are to lax' do
9
- account = stub('Imap::Backup::Account', :local_path => 'foobar')
10
- stat = stub('File::Stat', :mode => 0345)
11
- File.should_receive(:stat).with('foobar').and_return(stat)
12
-
13
- expect do
14
- Imap::Backup::Downloader.new(account, 'foo')
15
- end.to raise_error(RuntimeError, "Permissions on 'foobar' should be 0700, not 0345")
16
- end
17
-
18
- end
19
-
20
- context '#run' do
6
+ context 'with account and downloader' do
21
7
 
22
8
  before :each do
23
- stat = stub('File::Stat', :mode => 0700)
24
- File.stub!(:stat => stat)
25
-
26
- @account = stub('Imap::Backup::Account', :local_path => '/base/path')
27
- @d = Imap::Backup::Downloader.new(@account, 'my_folder')
9
+ local_path = '/base/path'
10
+ stat = stub('File::Stat', :mode => 0700)
11
+ File.stub!(:stat).with(local_path).and_return(stat)
12
+
13
+ @message = {
14
+ 'RFC822' => 'the body',
15
+ 'other' => 'xxx'
16
+ }
17
+ @folder = stub('Imap::Backup::Account::Folder', :fetch => @message)
18
+ @serializer = stub('Imap::Backup::Serializer', :prepare => nil,
19
+ :exist? => true,
20
+ :uids => [],
21
+ :save => nil)
28
22
  end
29
23
 
30
- it 'should create the folder and update permissions' do
31
- @account.stub!(:each_uid)
32
- FileUtils.should_receive(:mkdir_p).with('/base/path/my_folder')
33
- FileUtils.should_receive(:chmod_R).with('g-wrx,o-wrx', '/base/path/my_folder')
24
+ subject { Imap::Backup::Downloader.new(@folder, @serializer) }
34
25
 
35
- @d.run
36
- end
26
+ context '#run' do
37
27
 
38
- context 'with folder' do
39
- before :each do
40
- FileUtils.stub!(:mkdir_p)
41
- FileUtils.stub!(:chmod_R)
42
- end
43
-
44
- it 'should list messages' do
45
- @account.should_receive(:each_uid)
46
-
47
- @d.run
48
- end
49
-
50
- context 'with messages' do
51
- before :each do
52
- @account.should_receive(:each_uid) do |&block|
53
- block.call '123'
54
- block.call '999'
55
- block.call '1234'
56
- end
57
- end
28
+ context 'with folder' do
58
29
 
59
- it 'should check if messages exist' do
60
- File.should_receive(:exist?).with(%r{/base/path/my_folder/\d+.json}).exactly(3).times.and_return(true)
30
+ it 'should list messages' do
31
+ @folder.should_receive(:uids).and_return([])
61
32
 
62
- @d.run
33
+ subject.run
63
34
  end
64
35
 
65
- it 'should skip messages that are downloaded' do
66
- File.stub!(:exist?).and_return(true)
67
-
68
- @account.should_not_receive(:fetch)
69
-
70
- @d.run
71
- end
72
-
73
- context 'to download' do
36
+ context 'with messages' do
74
37
  before :each do
75
- # N.B. messages 999 and 1234 wil be 'fetched'
76
- File.stub!(:exist?) do |path|
77
- if path =~ %r{123.json$}
78
- true
79
- else
80
- false
81
- end
82
- end
83
-
84
- @message = {
85
- 'RFC822' => 'the body',
86
- 'other' => 'xxx'
87
- }
88
- @account.stub!(:fetch => @message)
89
- File.stub!(:open)
90
- FileUtils.stub!(:chmod)
38
+ @folder.stub!(:uids).and_return(['123', '999', '1234'])
91
39
  end
92
40
 
93
- it 'should request messages' do
94
- @account.should_receive(:fetch).with('999')
95
- @account.should_receive(:fetch).with('1234')
41
+ it 'should skip messages that are downloaded' do
42
+ File.stub!(:exist?).and_return(true)
96
43
 
97
- @d.run
44
+ @serializer.should_not_receive(:fetch)
45
+
46
+ subject.run
98
47
  end
99
48
 
100
- it 'should save messages' do
101
- file = stub('File')
102
- File.should_receive(:open) do |&block|
103
- block.call file
49
+ context 'to download' do
50
+ before :each do
51
+ @serializer.stub!(:exist?) do |uid|
52
+ if uid == '123'
53
+ true
54
+ else
55
+ false
56
+ end
57
+ end
104
58
  end
105
- file.should_receive(:write).with(/the body/)
106
59
 
107
- @d.run
108
- end
60
+ it 'should request messages' do
61
+ @folder.should_receive(:fetch).with('999')
62
+ @folder.should_receive(:fetch).with('1234')
109
63
 
110
- it 'should JSON encode messages' do
111
- file = stub('File', :write => nil)
112
- File.stub!(:open) do |&block|
113
- block.call file
64
+ subject.run
114
65
  end
115
66
 
116
- @message.should_receive(:to_json).twice
67
+ it 'should save messages' do
68
+ @serializer.should_receive(:save).with('999', @message)
69
+ @serializer.should_receive(:save).with('1234', @message)
117
70
 
118
- @d.run
119
- end
120
-
121
- it 'should set file permissions' do
122
- FileUtils.should_receive(:chmod).with(0600, /999.json$/)
123
- FileUtils.should_receive(:chmod).with(0600, /1234.json$/)
71
+ subject.run
72
+ end
124
73
 
125
- @d.run
126
74
  end
127
75
 
128
76
  end
77
+
129
78
  end
130
79
 
131
80
  end
@@ -0,0 +1,100 @@
1
+ # encoding: utf-8
2
+
3
+ load File.expand_path('../../spec_helper.rb', File.dirname(__FILE__))
4
+
5
+ describe Imap::Backup::Serializer::Directory do
6
+
7
+ context '#initialize' do
8
+
9
+ it 'should fail if download path file permissions are to lax' do
10
+ stat = stub('File::Stat', :mode => 0345)
11
+ File.should_receive(:stat).with('/base/path').and_return(stat)
12
+
13
+ expect do
14
+ Imap::Backup::Serializer::Directory.new('/base/path', 'my_folder')
15
+ end.to raise_error(RuntimeError, "Permissions on '/base/path' should be 0700, not 0345")
16
+ end
17
+
18
+ end
19
+
20
+ context 'with object' do
21
+
22
+ before :each do
23
+ stat = stub('File::Stat', :mode => 0700)
24
+ File.stub!(:stat).with('/base/path').and_return(stat)
25
+ FileUtils.stub!(:mkdir_p).with('/base/path/my_folder')
26
+ FileUtils.stub!(:chmod_R).with('g-wrx,o-wrx', '/base/path/my_folder')
27
+ end
28
+
29
+ subject { Imap::Backup::Serializer::Directory.new('/base/path', 'my_folder') }
30
+
31
+ context '#uids' do
32
+
33
+ it 'should return the backed-up uids' do
34
+ files = ['00000123.json', '000001.json']
35
+
36
+ File.should_receive(:exist?).with('/base/path/my_folder').and_return(true)
37
+ Dir.should_receive(:open).with('/base/path/my_folder').and_return(files)
38
+
39
+ subject.uids.should == [1, 123]
40
+ end
41
+
42
+ it 'should return an empty Array if the directory does not exist' do
43
+ File.should_receive(:exist?).with('/base/path/my_folder').and_return(false)
44
+
45
+ subject.uids.should == []
46
+ end
47
+
48
+ end
49
+
50
+ context '#exist?' do
51
+
52
+ it 'should check if the file exists' do
53
+ File.should_receive(:exist?).with(%r{/base/path/my_folder/0+123.json}).and_return(true)
54
+
55
+ subject.exist?(123).should be_true
56
+ end
57
+
58
+ end
59
+
60
+ context '#save' do
61
+
62
+ before :each do
63
+ File.stub!(:exist?).with(%r{/base/path/my_folder/0+1234.json}).and_return(true)
64
+ FileUtils.stub!(:chmod).with(0600, /0+1234.json$/)
65
+ @message = {
66
+ 'RFC822' => 'the body',
67
+ 'other' => 'xxx'
68
+ }
69
+ @file = stub('File', :write => nil)
70
+ File.stub!(:open) do |&block|
71
+ block.call @file
72
+ end
73
+ end
74
+
75
+ it 'should save messages' do
76
+ File.should_receive(:open) do |&block|
77
+ block.call @file
78
+ end
79
+ @file.should_receive(:write).with(/the body/)
80
+
81
+ subject.save('1234', @message)
82
+ end
83
+
84
+ it 'should JSON encode messages' do
85
+ @message.should_receive(:to_json)
86
+
87
+ subject.save('1234', @message)
88
+ end
89
+
90
+ it 'should set file permissions' do
91
+ FileUtils.should_receive(:chmod).with(0600, /0+1234.json$/)
92
+
93
+ subject.save(1234, @message)
94
+ end
95
+ end
96
+
97
+ end
98
+
99
+ end
100
+
@@ -3,6 +3,24 @@ load File.expand_path( '../spec_helper.rb', File.dirname(__FILE__) )
3
3
 
4
4
  describe Imap::Backup::Settings do
5
5
 
6
+ before :each do
7
+ @settings = {
8
+ :accounts => [
9
+ {
10
+ :username => 'a1@example.com'
11
+ },
12
+ {
13
+ :username => 'a2@example.com',
14
+ },
15
+ ]
16
+ }
17
+ File.stub!(:exist?).and_return(true)
18
+ stat = stub('File::Stat', :mode => 0600)
19
+ File.stub!(:stat).and_return(stat)
20
+ File.stub!(:read)
21
+ JSON.stub!(:parse).and_return(@settings)
22
+ end
23
+
6
24
  context '#initialize' do
7
25
 
8
26
  it 'should fail if the config file is missing' do
@@ -30,52 +48,59 @@ describe Imap::Backup::Settings do
30
48
  stat = stub('File::Stat', :mode => 0600)
31
49
  File.stub!(:stat).and_return(stat)
32
50
 
33
- file = stub('file')
34
- File.should_receive(:open).with(%r{/.imap-backup/config.json}).and_return(file)
35
- JSON.should_receive(:load).with(file)
51
+ configuration = 'JSON string'
52
+ File.should_receive(:read).with(%r{/.imap-backup/config.json}).and_return(configuration)
53
+ JSON.should_receive(:parse).with(configuration, :symbolize_names => true)
36
54
 
37
55
  Imap::Backup::Settings.new
38
56
  end
39
57
 
58
+ context 'with account parameter' do
59
+ it 'should only create requested accounts' do
60
+ settings = Imap::Backup::Settings.new(['a2@example.com'])
61
+
62
+ settings.accounts.should == @settings[:accounts][1..1]
63
+ end
64
+ end
65
+
40
66
  end
41
67
 
42
- context '#each_account' do
68
+ context 'instance methods' do
69
+
43
70
  before :each do
44
- @account1_settings = stub('account1 settings')
45
- settings = {
46
- 'accounts' => [
47
- @account1_settings
48
- ]
49
- }
50
- File.stub!(:open)
51
- JSON.stub!(:load).and_return(settings)
52
- @account = stub('Imap::Backup::Settings', :disconnect => nil)
71
+ @connection = stub('Imap::Backup::Account::Connection', :disconnect => nil)
53
72
  end
54
73
 
55
74
  subject { Imap::Backup::Settings.new }
56
75
 
57
- it 'should create accounts' do
58
- Imap::Backup::Account.should_receive(:new).with(@account1_settings).and_return(@account)
59
- subject.each_account {}
60
- end
76
+ context '#each_connection' do
61
77
 
62
- it 'should call the block' do
63
- Imap::Backup::Account.stub!(:new).and_return(@account)
64
- calls = 0
78
+ it 'should instantiate connections' do
79
+ Imap::Backup::Account::Connection.should_receive(:new).with(@settings[:accounts][0]).and_return(@connection)
80
+ Imap::Backup::Account::Connection.should_receive(:new).with(@settings[:accounts][1]).and_return(@connection)
65
81
 
66
- subject.each_account do |a|
67
- calls += 1
68
- a.should == @account
82
+ subject.each_connection{}
69
83
  end
70
- calls.should == 1
71
- end
72
84
 
73
- it 'should disconnect the account' do
74
- Imap::Backup::Account.stub!(:new).and_return(@account)
85
+ it 'should call the block' do
86
+ Imap::Backup::Account::Connection.stub!(:new).and_return(@connection)
87
+ calls = 0
88
+
89
+ subject.each_connection do |a|
90
+ calls += 1
91
+ a.should == @connection
92
+ end
93
+ calls.should == 2
94
+ end
75
95
 
76
- @account.should_receive(:disconnect)
96
+ it 'should disconnect connections' do
97
+ Imap::Backup::Account::Connection.stub!(:new).and_return(@connection)
98
+
99
+ @connection.should_receive(:disconnect)
100
+
101
+ subject.each_connection {}
102
+ end
77
103
 
78
- subject.each_account {}
79
104
  end
80
105
 
81
106
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: imap-backup
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.2
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,8 +9,24 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-06-10 00:00:00.000000000 Z
12
+ date: 2012-06-11 00:00:00.000000000 Z
13
13
  dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rake
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '0'
14
30
  - !ruby/object:Gem::Dependency
15
31
  name: pry
16
32
  requirement: !ruby/object:Gem::Requirement
@@ -92,15 +108,19 @@ files:
92
108
  - bin/imap-backup
93
109
  - imap-backup.gemspec
94
110
  - lib/imap/backup.rb
95
- - lib/imap/backup/account.rb
111
+ - lib/imap/backup/account/connection.rb
112
+ - lib/imap/backup/account/folder.rb
96
113
  - lib/imap/backup/downloader.rb
114
+ - lib/imap/backup/serializer/directory.rb
97
115
  - lib/imap/backup/settings.rb
98
116
  - lib/imap/backup/utils.rb
99
117
  - lib/imap/backup/version.rb
100
118
  - spec/gather_rspec_coverage.rb
101
119
  - spec/spec_helper.rb
102
- - spec/unit/account_spec.rb
120
+ - spec/unit/account/connection_spec.rb
121
+ - spec/unit/account/folder_spec.rb
103
122
  - spec/unit/downloader_spec.rb
123
+ - spec/unit/serializer/directory_spec.rb
104
124
  - spec/unit/settings_spec.rb
105
125
  - spec/unit/utils_spec.rb
106
126
  homepage: https://github.com/joeyates/imap-backup
@@ -117,7 +137,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
117
137
  version: '0'
118
138
  segments:
119
139
  - 0
120
- hash: 3027570741254356503
140
+ hash: 3223776400470663453
121
141
  required_rubygems_version: !ruby/object:Gem::Requirement
122
142
  none: false
123
143
  requirements:
@@ -126,7 +146,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
126
146
  version: '0'
127
147
  segments:
128
148
  - 0
129
- hash: 3027570741254356503
149
+ hash: 3223776400470663453
130
150
  requirements: []
131
151
  rubyforge_project:
132
152
  rubygems_version: 1.8.23
@@ -136,8 +156,10 @@ summary: Backup GMail (or other IMAP) accounts to disk
136
156
  test_files:
137
157
  - spec/gather_rspec_coverage.rb
138
158
  - spec/spec_helper.rb
139
- - spec/unit/account_spec.rb
159
+ - spec/unit/account/connection_spec.rb
160
+ - spec/unit/account/folder_spec.rb
140
161
  - spec/unit/downloader_spec.rb
162
+ - spec/unit/serializer/directory_spec.rb
141
163
  - spec/unit/settings_spec.rb
142
164
  - spec/unit/utils_spec.rb
143
165
  has_rdoc:
@@ -1,42 +0,0 @@
1
- require 'net/imap'
2
-
3
- module Imap
4
- module Backup
5
- class Account
6
-
7
- REQUESTED_ATTRIBUTES = ['RFC822', 'FLAGS', 'INTERNALDATE']
8
-
9
- attr_accessor :local_path
10
- attr_accessor :backup_folders
11
-
12
- def initialize(options)
13
- @local_path, @backup_folders = options['local_path'], options['folders']
14
- @imap = Net::IMAP.new('imap.gmail.com', 993, true)
15
- @imap.login(options['username'], options['password'])
16
- end
17
-
18
- def disconnect
19
- @imap.disconnect
20
- end
21
-
22
- def folders
23
- @imap.list('/', '*')
24
- end
25
-
26
- def each_uid(folder)
27
- @imap.examine(folder)
28
- @imap.uid_search(['ALL']).each do |uid|
29
- yield uid
30
- end
31
- end
32
-
33
- def fetch(uid)
34
- message = @imap.uid_fetch([uid], REQUESTED_ATTRIBUTES)[0][1]
35
- message['RFC822'].force_encoding('utf-8') if RUBY_VERSION > '1.9'
36
- message
37
- end
38
-
39
- end
40
- end
41
- end
42
-
@@ -1,95 +0,0 @@
1
- # encoding: utf-8
2
- load File.expand_path( '../spec_helper.rb', File.dirname(__FILE__) )
3
-
4
- describe Imap::Backup::Account do
5
-
6
- context '#initialize' do
7
-
8
- it 'should login to the imap server' do
9
- imap = stub('Net::IMAP')
10
- Net::IMAP.should_receive(:new).with('imap.gmail.com', 993, true).and_return(imap)
11
- imap.should_receive('login').with('myuser', 'secret')
12
-
13
- Imap::Backup::Account.new('username' => 'myuser', 'password' => 'secret')
14
- end
15
-
16
- end
17
-
18
- context 'with imap' do
19
- before :each do
20
- @imap = stub('Net::IMAP', :login => nil)
21
- Net::IMAP.stub!(:new).and_return(@imap)
22
- end
23
-
24
- subject { Imap::Backup::Account.new('username' => 'myuser', 'password' => 'secret') }
25
-
26
- context '#disconnect' do
27
- it 'should disconnect from the server' do
28
- @imap.should_receive(:disconnect)
29
-
30
- subject.disconnect
31
- end
32
- end
33
-
34
- context '#folders' do
35
- it 'should list all folders' do
36
- @imap.should_receive(:list).with('/', '*')
37
-
38
- subject.folders
39
- end
40
- end
41
-
42
- context '#each_uid' do
43
- it 'should examine the folder' do
44
- @imap.stub!(:uid_search => [])
45
- @imap.should_receive(:examine).with('my_folder')
46
-
47
- subject.each_uid('my_folder') {}
48
- end
49
-
50
- it 'should call the block with each message uid' do
51
- @imap.stub!(:examine).with('my_folder')
52
- @imap.should_receive(:uid_search).with(['ALL']).and_return(['123', '456'])
53
-
54
- uids = []
55
- subject.each_uid('my_folder') do |uid|
56
- uids << uid
57
- end
58
-
59
- uids.should == ['123', '456']
60
- end
61
- end
62
-
63
- context '#fetch' do
64
- before :each do
65
- @message_body = 'the body'
66
- @message = {
67
- 'RFC822' => @message_body,
68
- 'other' => 'xxx'
69
- }
70
- end
71
-
72
- it 'should request the message, the flags and the date' do
73
- @imap.should_receive(:uid_fetch).
74
- with(['123'], ['RFC822', 'FLAGS', 'INTERNALDATE']).
75
- and_return([[nil, @message]])
76
-
77
- subject.fetch('123')
78
- end
79
-
80
- if RUBY_VERSION > '1.9'
81
- it 'should set the encoding on the message' do
82
- @imap.stub!(:uid_fetch => [[nil, @message]])
83
-
84
- @message_body.should_receive(:force_encoding).with('utf-8')
85
-
86
- subject.fetch('123')
87
- end
88
- end
89
-
90
- end
91
-
92
- end
93
-
94
- end
95
-