imap-backup 0.0.1 → 0.0.2

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