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 +31 -22
- data/bin/imap-backup +60 -9
- data/imap-backup.gemspec +5 -0
- data/lib/imap/backup.rb +3 -1
- data/lib/imap/backup/account/connection.rb +47 -0
- data/lib/imap/backup/account/folder.rb +30 -0
- data/lib/imap/backup/downloader.rb +6 -16
- data/lib/imap/backup/serializer/directory.rb +51 -0
- data/lib/imap/backup/settings.rb +14 -7
- data/lib/imap/backup/version.rb +1 -1
- data/spec/unit/account/connection_spec.rb +117 -0
- data/spec/unit/account/folder_spec.rb +60 -0
- data/spec/unit/downloader_spec.rb +46 -97
- data/spec/unit/serializer/directory_spec.rb +100 -0
- data/spec/unit/settings_spec.rb +54 -29
- metadata +29 -7
- data/lib/imap/backup/account.rb +0 -42
- data/spec/unit/account_spec.rb +0 -95
data/README.md
CHANGED
@@ -1,5 +1,4 @@
|
|
1
|
-
imap-backup [][Continuous Integration]
|
2
|
-
===========
|
1
|
+
# imap-backup [][Continuous Integration]
|
3
2
|
|
4
3
|
*Backup GMail (or other IMAP) accounts to disk*
|
5
4
|
|
@@ -13,31 +12,35 @@ imap-backup [
|
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
|
-
|
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
|
-
|
10
|
-
|
11
|
-
|
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
|
-
|
14
|
-
settings.
|
15
|
-
|
16
|
-
|
17
|
-
|
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
|
-
|
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
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
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
|
+
|
data/lib/imap/backup/settings.rb
CHANGED
@@ -7,18 +7,25 @@ module Imap
|
|
7
7
|
|
8
8
|
include Imap::Backup::Utils
|
9
9
|
|
10
|
-
|
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.
|
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
|
18
|
-
@
|
19
|
-
|
20
|
-
yield
|
21
|
-
|
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
|
|
data/lib/imap/backup/version.rb
CHANGED
@@ -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 '
|
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
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
@
|
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
|
-
|
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
|
-
|
36
|
-
end
|
26
|
+
context '#run' do
|
37
27
|
|
38
|
-
|
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
|
60
|
-
|
30
|
+
it 'should list messages' do
|
31
|
+
@folder.should_receive(:uids).and_return([])
|
61
32
|
|
62
|
-
|
33
|
+
subject.run
|
63
34
|
end
|
64
35
|
|
65
|
-
|
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
|
-
|
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
|
94
|
-
|
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
|
-
@
|
44
|
+
@serializer.should_not_receive(:fetch)
|
45
|
+
|
46
|
+
subject.run
|
98
47
|
end
|
99
48
|
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
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
|
-
|
108
|
-
|
60
|
+
it 'should request messages' do
|
61
|
+
@folder.should_receive(:fetch).with('999')
|
62
|
+
@folder.should_receive(:fetch).with('1234')
|
109
63
|
|
110
|
-
|
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
|
-
|
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
|
-
|
119
|
-
|
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
|
+
|
data/spec/unit/settings_spec.rb
CHANGED
@@ -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
|
-
|
34
|
-
File.should_receive(:
|
35
|
-
JSON.should_receive(:
|
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 '
|
68
|
+
context 'instance methods' do
|
69
|
+
|
43
70
|
before :each do
|
44
|
-
@
|
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
|
-
|
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
|
-
|
63
|
-
|
64
|
-
|
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
|
-
|
67
|
-
calls += 1
|
68
|
-
a.should == @account
|
82
|
+
subject.each_connection{}
|
69
83
|
end
|
70
|
-
calls.should == 1
|
71
|
-
end
|
72
84
|
|
73
|
-
|
74
|
-
|
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
|
-
|
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.
|
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-
|
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/
|
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:
|
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:
|
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/
|
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:
|
data/lib/imap/backup/account.rb
DELETED
@@ -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
|
-
|
data/spec/unit/account_spec.rb
DELETED
@@ -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
|
-
|