luggage 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,151 @@
1
+ module Luggage
2
+ class Message
3
+ class MessageNotFoundError < ArgumentError; end
4
+ class DuplicateMessageError < StandardError; end
5
+
6
+ attr_accessor :flags
7
+ attr_reader :connection, :mailbox, :template, :date, :message_id
8
+
9
+ # Creates a local Message instance
10
+ #
11
+ # `connection` should be an authenticated Imap connection
12
+ # `mailbox` should be either a Mailbox or a string describing a remote mailbox
13
+ # `args` will be passed to Message::new. Keys used by Message::new will be set, any
14
+ # other keys will be delegated to Mail::Message.
15
+ #
16
+ # Example Usage:
17
+ # Message.new_local(c, 'INBOX', :template => 'base.eml', :date => 4.days.ago)
18
+ # Message.new_local(c, m, :subject => "VIAGRA ROCKS!", :body => "<insert phishing here>", :cc => "yourmom@gmail.com")
19
+ #
20
+ def self.new_local(connection, mailbox, args = {}, &block)
21
+ message = new(connection, mailbox, args)
22
+ message.instance_eval do
23
+ @mail = @template ? Mail.read(@template) : Mail.new
24
+ args.each do |key, value|
25
+ mail[key] = value if mail.respond_to?(key)
26
+ end
27
+ mail[:message_id] = message_id
28
+ end
29
+
30
+ message.instance_eval &block if block_given?
31
+ message
32
+ end
33
+
34
+ # Creates a Message instance
35
+ #
36
+ # `connection` should be an authenticated Imap connection
37
+ # `mailbox` should be either a Mailbox or a string describing a remote mailbox
38
+ # `args[:date]` when this message is appended to the remote server, this is the date which will be used
39
+ # `args[:template]` use this file as the initial raw email content.
40
+ # `args[:message_id]` use this as for the Message-ID header. This header is used to identify messages across requests
41
+ #
42
+ def initialize(connection, mailbox, args = {}, &block)
43
+ raise ArgumentError, "Net::IMAP connection required" unless connection.kind_of?(Net::IMAP)
44
+
45
+ @connection = connection
46
+ @mailbox = mailbox.kind_of?(Mailbox) ? mailbox : Mailbox.new(connection, mailbox)
47
+ @flags = []
48
+ @date = args[:date] || Time.now
49
+ @template = args[:template]
50
+ @message_id = args[:message_id] || "<#{UUIDTools::UUID.random_create}@test.oib.com>"
51
+
52
+ raise ArgumentError, "mailbox requried" unless @mailbox.present?
53
+
54
+ instance_eval &block if block_given?
55
+ end
56
+
57
+ # Formatted to save to file
58
+ #
59
+ # Mail::Message.new( message.to_s ).raw_source = message.to_s
60
+ #
61
+ def to_s
62
+ mail.encoded
63
+ end
64
+
65
+ # Fetch this message from the server and update all its attributes
66
+ #
67
+ def reload
68
+ fields = fetch_fields
69
+ @mail = Mail.new(fields["BODY[]"])
70
+ @flags = fields["FLAGS"]
71
+ @date = Time.parse(fields["INTERNALDATE"])
72
+ self
73
+ end
74
+
75
+ # Append this message to the remote mailbox
76
+ #
77
+ def save!
78
+ mailbox.select!
79
+ connection.append(mailbox.name, raw_message, flags.map {|f| f.to_sym.upcase}, date)
80
+ end
81
+
82
+ # Add the 'Deleted' flag to this message on the remote server
83
+ #
84
+ def delete!
85
+ mailbox.select!
86
+ connection.uid_store([uid], "+FLAGS", [:Deleted])
87
+ @mail = nil
88
+ end
89
+
90
+ # Returns true if a message with the same message_id exists in the remote mailbox
91
+ #
92
+ def exists?
93
+ mailbox.select!
94
+ connection.uid_search("HEADER Message-ID #{message_id}").present?
95
+ end
96
+
97
+ # Proxy all other methods to this instance's Mail::Message
98
+ #
99
+ def method_missing(meth, *args, &block)
100
+ if mail.respond_to?(meth)
101
+ mail.send(meth, *args, &block)
102
+ else
103
+ super
104
+ end
105
+ end
106
+
107
+ def inspect
108
+ "#<Luggage::Message server: \"#{host}\", mailbox: \"#{mailbox.name}\", message_id: \"#{message_id}\">"
109
+ end
110
+
111
+ def host
112
+ connection.instance_variable_get(:@host)
113
+ end
114
+
115
+ private
116
+
117
+ # Formatted to upload to IMAP server
118
+ #
119
+ def raw_message
120
+ mail.to_s
121
+ end
122
+
123
+ def mail
124
+ reload unless @mail.present?
125
+ @mail
126
+ end
127
+
128
+ def uid
129
+ unless @uid.present?
130
+ mailbox.select!
131
+ @uid = fetch_uid
132
+ end
133
+ @uid
134
+ end
135
+
136
+ def fetch_uid
137
+ response = connection.uid_search("HEADER Message-ID #{message_id}")
138
+ raise MessageNotFoundError if response.empty?
139
+ raise DuplicateMessageError if response.length > 1
140
+ response.first
141
+ end
142
+
143
+ def fetch_fields
144
+ response = connection.uid_fetch([uid], ["FLAGS", "INTERNALDATE", "BODY.PEEK[]"])
145
+ raise MessageNotFoundError if response.empty?
146
+ raise DuplicateMessageError if response.length > 1
147
+ response.first[:attr]
148
+ end
149
+ end
150
+ end
151
+
@@ -0,0 +1,3 @@
1
+ module Luggage
2
+ VERSION = "1.0.0"
3
+ end
data/lib/luggage.rb ADDED
@@ -0,0 +1,16 @@
1
+ require 'net/imap'
2
+ require 'mail'
3
+ require 'uuidtools'
4
+
5
+ require 'luggage/factory'
6
+ require 'luggage/mailbox'
7
+ require 'luggage/mailbox_array'
8
+ require 'luggage/mailbox_query_builder'
9
+ require 'luggage/message'
10
+ require 'luggage/version'
11
+
12
+ module Luggage
13
+ def self.new(*args, &block)
14
+ Factory.new(*args, &block)
15
+ end
16
+ end
data/luggage.gemspec ADDED
@@ -0,0 +1,30 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ require File.expand_path('../lib/luggage/version', __FILE__)
4
+
5
+ Gem::Specification.new do |gem|
6
+ gem.name = "luggage"
7
+ gem.version = Luggage::VERSION
8
+ gem.summary = %q{Net::IMAP DSL}
9
+ gem.description = %q{Easily interact with IMAP servers}
10
+ gem.license = "MIT"
11
+ gem.authors = ["Ryan Michael", "Eric Pinzur"]
12
+ gem.email = "ryanmichael@otherinbox.com"
13
+ gem.homepage = "https://github.com/otherinbox/luggage"
14
+
15
+ gem.files = `git ls-files`.split($/)
16
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
17
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
18
+ gem.require_paths = ['lib']
19
+
20
+ gem.add_dependency 'mail', '~> 2.5.0'
21
+ gem.add_dependency 'uuidtools', '~> 2.1.3'
22
+
23
+ gem.add_development_dependency 'bundler', '~> 1.0'
24
+ gem.add_development_dependency 'rake', '~> 0.8'
25
+ gem.add_development_dependency 'rdoc', '~> 3.0'
26
+ gem.add_development_dependency 'rspec', '~> 2.4'
27
+ gem.add_development_dependency 'activesupport', '~> 3.2.9'
28
+ gem.add_development_dependency 'fuubar', '~> 1.1.0'
29
+ gem.add_development_dependency 'pry', '~> 0.9.10'
30
+ end
@@ -0,0 +1,83 @@
1
+ require 'spec_helper'
2
+
3
+ describe Luggage::Factory do
4
+ include_context "factories"
5
+
6
+ describe "::new" do
7
+ it "accepts :connection" do
8
+ expect(Luggage.new(:connection => connection).connection).to eq(connection)
9
+ end
10
+
11
+ it "accepts :server, :authenticate and creates a connection" do
12
+ Net::IMAP.any_instance.stub(:authenticate).and_return(true)
13
+ expect(Luggage.new(:server => :foo, :authenticate => "LOGIN username password").connection).to be_a(Net::IMAP)
14
+ end
15
+
16
+ it "accepts :server, :xoauth and creates a connection" do
17
+ Net::IMAP.any_instance.stub(:send_command).and_return(true)
18
+ expect(Luggage.new(:server => :foo, :xoauth => "token").connection).to be_a(Net::IMAP)
19
+ end
20
+
21
+ it "requires a way to build a connection" do
22
+ expect { Luggage.new }.to raise_error(ArgumentError)
23
+ end
24
+ end
25
+
26
+ describe '#message' do
27
+ it "returns a Luggage::Message" do
28
+ expect(factory.message(:mailbox)).to be_a(Luggage::Message)
29
+ end
30
+
31
+ it "passes arguments to initialize" do
32
+ Luggage::Message.should_receive(:new).with(connection, "mailbox_name", :key => :value).and_return( double('Message').as_null_object )
33
+ factory.message("mailbox_name", :key => :value)
34
+ end
35
+ end
36
+
37
+ describe "#mailboxes" do
38
+ before(:each) do
39
+ connection.stub(:list).
40
+ and_return([
41
+ OpenStruct.new(:name => "Mailbox_1"),
42
+ OpenStruct.new(:name => "Mailbox_2"),
43
+ OpenStruct.new(:name => "Mailbox_3"),
44
+ OpenStruct.new(:name => "Mailbox_4") ])
45
+ end
46
+
47
+ context "with no arguments" do
48
+ it "returns an array of mailboxes defined on the remote server" do
49
+ expect(factory.mailboxes.map(&:name)).to eq(["Mailbox_1", "Mailbox_2", "Mailbox_3", "Mailbox_4"])
50
+ end
51
+ end
52
+
53
+ context "with a string argument (function-call syntax)" do
54
+ it "returns a mailbox with the passed name" do
55
+ expect(factory.mailboxes("foo").name).to eq("foo")
56
+ end
57
+ end
58
+
59
+ context "with an iteger (function-call syntax)" do
60
+ it "returns the nth mailbox on the remote server" do
61
+ expect(factory.mailboxes(1).name).to eq("Mailbox_2")
62
+ end
63
+ end
64
+
65
+ context "with a string argument (hash-index syntax)" do
66
+ it "returns a mailbox with the passed name" do
67
+ expect(factory.mailboxes["foo"].name).to eq("foo")
68
+ end
69
+ end
70
+
71
+ context "with an integer (array-index syntax)" do
72
+ it "returns the nth mailbox on the remote server" do
73
+ expect(factory.mailboxes[1].name).to eq("Mailbox_2")
74
+ end
75
+ end
76
+
77
+ context "with a range (array-slice syntax)" do
78
+ it "returns a subset array of mailboxes defined on the remote server" do
79
+ expect(factory.mailboxes[1..2].map(&:name)).to eq(["Mailbox_2", "Mailbox_3"])
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,48 @@
1
+ require 'spec_helper'
2
+
3
+ describe Luggage::MailboxQueryBuilder do
4
+ include_context "factories"
5
+
6
+ describe "::new" do
7
+ it "sets the mailbox" do
8
+ expect(Luggage::MailboxQueryBuilder.new(mailbox).mailbox).to eq(mailbox)
9
+ end
10
+
11
+ it "requires a mailbox" do
12
+ expect { Luggage::MailboxQueryBuilder.new(:not_a_mailbox) }.to raise_error(ArgumentError)
13
+ end
14
+
15
+ it "sets the connection" do
16
+ expect(Luggage::MailboxQueryBuilder.new(mailbox).connection).to eq(connection)
17
+ end
18
+ end
19
+
20
+ describe "#each" do
21
+ it "yields to each member of #messages" do
22
+ block = Proc.new {}
23
+ query_builder.stub(:message_ids).and_return(['<one@example.com>', '<two@example.com>'])
24
+ query_builder.messages.should_receive(:each) #.with(block)
25
+
26
+ query_builder.each(&block)
27
+ end
28
+ end
29
+
30
+ describe "::where" do
31
+ it "returns self" do
32
+ expect(query_builder.where(:foo => :bar)).to be_a(Luggage::MailboxQueryBuilder)
33
+ end
34
+
35
+ it "appends to the query" do
36
+ expect(query_builder.where(:foo => :bar).where(:baz => :qux).query).to eq([:foo, :bar, :baz, :qux])
37
+ end
38
+ end
39
+
40
+ describe "::[]" do
41
+ it "returns messages[]" do
42
+ query_builder.stub(:message_ids).and_return(['<one@example.com>', '<two@example.com>'])
43
+ query_builder.messages.should_receive(:[]).with((1..2))
44
+
45
+ query_builder[1..2]
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,135 @@
1
+ require 'spec_helper'
2
+
3
+ describe Luggage::Mailbox do
4
+ include_context "factories"
5
+
6
+ describe "::new" do
7
+ it "executes a passed block" do
8
+ m = Luggage::Mailbox.new(connection, :mailbox) do
9
+ @name = "woot"
10
+ end
11
+ expect(m.name).to eq("woot")
12
+ end
13
+
14
+ it "sets the connection" do
15
+ expect(Luggage::Mailbox.new(connection, :mailbox).connection).to eq(connection)
16
+ end
17
+
18
+ it "requires a connection" do
19
+ expect { Luggage::Mailbox.new(:foo, :mailbox).connection }.to raise_error(ArgumentError)
20
+ end
21
+
22
+ it "sets the name" do
23
+ expect(Luggage::Mailbox.new(connection, :mailbox).name).to eq(:mailbox)
24
+ end
25
+ end
26
+
27
+ describe "message" do
28
+ it "executes a passed block" do
29
+ m = mailbox.message do
30
+ subject("new subject")
31
+ end
32
+ expect(m.subject).to eq("new subject")
33
+ end
34
+
35
+
36
+ it "returns an Luggage::Message" do
37
+ expect(mailbox.message).to be_a(Luggage::Message)
38
+ end
39
+
40
+ it "instantiates with expected arguments" do
41
+ message = Luggage::Message.new(connection, mailbox, :foo => :bar)
42
+ Luggage::Message.should_receive(:new).with(connection, mailbox, :foo => :bar).and_return(message)
43
+
44
+ mailbox.message(:foo => :bar)
45
+ end
46
+ end
47
+
48
+ describe "all" do
49
+ it "returns a QueryBuilder" do
50
+ expect(mailbox.all).to be_a(Luggage::MailboxQueryBuilder)
51
+ end
52
+ end
53
+
54
+ describe "each" do
55
+ it "delegates to QueryBuilder instance" do
56
+ block = Proc.new {}
57
+ Luggage::MailboxQueryBuilder.any_instance.should_receive(:each).with(&block)
58
+
59
+ mailbox.each(&block)
60
+ end
61
+ end
62
+
63
+ describe "first" do
64
+ it "delegates to QueryBuilder instance" do
65
+ Luggage::MailboxQueryBuilder.any_instance.should_receive(:first)
66
+
67
+ mailbox.first
68
+ end
69
+ end
70
+
71
+ describe "where" do
72
+ it "delegates to QueryBuilder instance" do
73
+ Luggage::MailboxQueryBuilder.any_instance.should_receive(:where).with(:foo => :bar)
74
+
75
+ mailbox.where(:foo => :bar)
76
+ end
77
+ end
78
+
79
+ describe "select!" do
80
+ it "selects the mailbox" do
81
+ connection.should_receive(:select).with(:mailbox)
82
+
83
+ mailbox.select!
84
+ end
85
+ end
86
+
87
+ describe "save!" do
88
+ it "creates the mailbox if it doesn't exist" do
89
+ connection.should_receive(:create).with(:mailbox)
90
+
91
+ mailbox.save!
92
+ end
93
+
94
+ it "doesn't create the mailbox if the mailbox exists" do
95
+ mailbox.stub(:exists?).and_return(true)
96
+ connection.should_not_receive(:create)
97
+
98
+ mailbox.save!
99
+ end
100
+ end
101
+
102
+ describe "expunge!" do
103
+ it "selects the mailbox" do
104
+ connection.should_receive(:select).with(:mailbox)
105
+
106
+ mailbox.expunge!
107
+ end
108
+
109
+ it "expunges the mailbox" do
110
+ connection.should_receive(:expunge)
111
+
112
+ mailbox.expunge!
113
+ end
114
+ end
115
+
116
+ describe "delete!" do
117
+ it "deletes the mailbox" do
118
+ connection.should_receive(:delete)
119
+
120
+ mailbox.delete!
121
+ end
122
+ end
123
+
124
+ describe "exists?" do
125
+ it "returns true if list includes the mailbox name" do
126
+ connection.stub(:list).with("", :mailbox).and_return([:results])
127
+
128
+ expect(mailbox.exists?).to be_true
129
+ end
130
+
131
+ it "returns false if the list doesn't include the mailbox name" do
132
+ expect(mailbox.exists?).to be_false
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,181 @@
1
+ require 'spec_helper'
2
+
3
+ describe Luggage::Message do
4
+ include_context "factories"
5
+
6
+ describe "::new_local" do
7
+ it "executes a passed block" do
8
+ m = Luggage::Message.new_local(connection, "Inbox") do
9
+ subject("new subject")
10
+ end
11
+ expect(m.subject).to eq("new subject")
12
+ end
13
+
14
+ it "returns an Luggage::Message" do
15
+ expect(Luggage::Message.new_local(connection, "Inbox")).to be_a(Luggage::Message)
16
+ end
17
+
18
+ it "raises ArgumentError if Luggage::Mailbox isn't passed" do
19
+ expect{ Luggage::Message.new_local() }.to raise_error(ArgumentError)
20
+ end
21
+
22
+ [:subject, :body, :to].each do |method|
23
+ context "with #{method} passed as argument" do
24
+ it "sets #{method} on Mail object" do
25
+ message = Luggage::Message.new_local(connection, :mailbox, method => "string")
26
+ expect(message.to_s).to match(/string/)
27
+ end
28
+ end
29
+ end
30
+
31
+ context "without template" do
32
+ it "instatiates a Mail object" do
33
+ mail = Mail.new
34
+ Mail.should_receive(:new).and_return(mail)
35
+ Luggage::Message.new_local(connection, :mailbox)
36
+ end
37
+ end
38
+
39
+ context "with a template" do
40
+ it "instatiates a Mail object" do
41
+ Mail.should_receive(:read).and_return({})
42
+
43
+ Luggage::Message.new_local(connection, :mailbox, :template =>"base")
44
+ end
45
+ end
46
+ end
47
+
48
+ describe "::new" do
49
+ it "executes a passed block" do
50
+ m = Luggage::Message.new(connection, "Inbox") do
51
+ subject("new subject")
52
+ end
53
+ expect(m.subject).to eq("new subject")
54
+ end
55
+
56
+ it "sets connection" do
57
+ expect(Luggage::Message.new(connection, :mailbox).connection).to eq(connection)
58
+ end
59
+
60
+ it "requires a connection" do
61
+ expect { Luggage::Message.new(:not_a_connection, :mailbox).connection }.to raise_error(ArgumentError)
62
+ end
63
+
64
+ it "sets mailbox if Mailbox passed" do
65
+ expect(Luggage::Message.new(connection, mailbox).mailbox).to eq(mailbox)
66
+ end
67
+
68
+ it "instantiates new mailbox if string passed" do
69
+ expect(Luggage::Message.new(connection, :mailbox).mailbox).to be_a(Luggage::Mailbox)
70
+ end
71
+
72
+ it "sets date if passed" do
73
+ date = 2.days.ago
74
+ expect(Luggage::Message.new(connection, :mailbox, :date => date).date).to eq(date)
75
+ end
76
+
77
+ it "sets date to now if not passed" do
78
+ expect(Luggage::Message.new(connection, :mailbox).date).to be_a(Time)
79
+ end
80
+
81
+ it "sets message_id if passed" do
82
+ message_id = "<foo@example.com>"
83
+ expect(Luggage::Message.new(connection, :mailbox, :message_id => message_id).message_id).to eq(message_id)
84
+ end
85
+
86
+ it "creates message_id if not passed" do
87
+ expect(Luggage::Message.new(connection, :mailbox).message_id).to match(/<\S*@\S*>/)
88
+ end
89
+ end
90
+
91
+ describe "#reload" do
92
+ it "selects mailbox" do
93
+ connection.should_receive(:select).with("Inbox")
94
+
95
+ message.reload
96
+ end
97
+
98
+ it "fetches raw email" do
99
+ connection.should_receive(:uid_fetch).
100
+ with([1], ["FLAGS", "INTERNALDATE", "BODY.PEEK[]"]).
101
+ and_return( [{:attr => {"BODY[]" => "raw_body", "FLAGS" => [], "INTERNALDATE" => 1.day.ago.to_s}}] )
102
+
103
+ message.reload
104
+ end
105
+
106
+ it "fetches flags" do
107
+ connection.should_receive(:uid_fetch).
108
+ with([1], ["FLAGS", "INTERNALDATE", "BODY.PEEK[]"]).
109
+ and_return( [{:attr => {"BODY[]" => "raw_body", "FLAGS" => [], "INTERNALDATE" => 1.day.ago.to_s}}] )
110
+
111
+ message.reload
112
+ end
113
+
114
+ it "creates new Mail instance" do
115
+ message # To instantiate the first one...
116
+
117
+ Mail.should_receive(:new).with("raw_body")
118
+
119
+ message.reload
120
+ end
121
+ end
122
+
123
+ describe "#save!" do
124
+ it "selects mailbox" do
125
+ connection.should_receive(:select).with("Inbox")
126
+
127
+ message.save!
128
+ end
129
+
130
+ it "appends message to mailbox" do
131
+ message_date = 2.days.ago
132
+ message.stub(:raw_message).and_return("Random Content")
133
+ message.stub(:flags).and_return([:Seen])
134
+ message.stub(:date).and_return(message_date)
135
+
136
+ connection.should_receive(:append).with("Inbox", "Random Content", [:SEEN], message_date)
137
+ message.save!
138
+ end
139
+ end
140
+
141
+ describe "#to_s" do
142
+ it "returns a string" do
143
+ expect(message.to_s).to be_a(String)
144
+ end
145
+ end
146
+
147
+
148
+ describe "#delete!" do
149
+ it "selects mailbox" do
150
+ connection.should_receive(:select).with("Inbox")
151
+
152
+ message.delete!
153
+ end
154
+
155
+ it "sets Deleted flag" do
156
+ connection.should_receive(:uid_store).with([1], "+FLAGS", [:Deleted])
157
+
158
+ message.delete!
159
+ end
160
+
161
+ it "resets cached mail instance" do
162
+ message.delete!
163
+ expect(message.instance_variable_get(:@mail)).to be_nil
164
+ end
165
+ end
166
+
167
+ describe "#exists?" do
168
+ it "selects mailbox" do
169
+ connection.should_receive(:select).with("Inbox")
170
+
171
+ message.exists?
172
+ end
173
+
174
+ it "searches for message with message_id" do
175
+ message.stub(:message_id).and_return("<foo@example.com>")
176
+ connection.should_receive(:uid_search).with("HEADER Message-ID <foo@example.com>").and_return([1])
177
+
178
+ message.exists?
179
+ end
180
+ end
181
+ end
@@ -0,0 +1,16 @@
1
+ require 'spec_helper'
2
+
3
+ describe Luggage do
4
+ it "should have a VERSION constant" do
5
+ subject.const_get('VERSION').should_not be_empty
6
+ end
7
+
8
+ include_context "factories"
9
+
10
+ describe "::new" do
11
+ it "proxies Luggage::Factory" do
12
+ Luggage::Factory.should_receive(:new).with(:server => :foo, :authenticate => :bar)
13
+ Luggage.new(:server => :foo, :authenticate => :bar)
14
+ end
15
+ end
16
+ end
data/spec/net_imap.rb ADDED
@@ -0,0 +1,15 @@
1
+ # mock Net::IMAP
2
+ class Net::IMAP
3
+ def initialize(*args, &block)
4
+ end
5
+
6
+ def login(user, password)
7
+ end
8
+
9
+ def disconnect
10
+ end
11
+
12
+ def disconnected?
13
+ end
14
+ end
15
+