gmail-afurmanov 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.rspec +2 -0
- data/CHANGELOG.md +82 -0
- data/LICENSE +21 -0
- data/README.md +268 -0
- data/Rakefile +39 -0
- data/TODO.md +6 -0
- data/VERSION +1 -0
- data/gemspec.yml +22 -0
- data/gmail.gemspec +18 -0
- data/lib/gmail.rb +65 -0
- data/lib/gmail/client.rb +29 -0
- data/lib/gmail/client/base.rb +223 -0
- data/lib/gmail/client/plain.rb +20 -0
- data/lib/gmail/client/xoauth.rb +51 -0
- data/lib/gmail/labels.rb +48 -0
- data/lib/gmail/mailbox.rb +117 -0
- data/lib/gmail/message.rb +164 -0
- data/lib/gmail/version.rb +12 -0
- data/spec/account.yml.example +2 -0
- data/spec/client_spec.rb +173 -0
- data/spec/gmail_spec.rb +38 -0
- data/spec/mailbox_spec.rb +47 -0
- data/spec/message_spec.rb +51 -0
- data/spec/spec_helper.rb +29 -0
- metadata +185 -0
@@ -0,0 +1,117 @@
|
|
1
|
+
module Gmail
|
2
|
+
class Mailbox
|
3
|
+
MAILBOX_ALIASES = {
|
4
|
+
:all => ['ALL'],
|
5
|
+
:seen => ['SEEN'],
|
6
|
+
:unseen => ['UNSEEN'],
|
7
|
+
:read => ['SEEN'],
|
8
|
+
:unread => ['UNSEEN'],
|
9
|
+
:flagged => ['FLAGGED'],
|
10
|
+
:unflagged => ['UNFLAGGED'],
|
11
|
+
:starred => ['FLAGGED'],
|
12
|
+
:unstarred => ['UNFLAGGED'],
|
13
|
+
:deleted => ['DELETED'],
|
14
|
+
:undeleted => ['UNDELETED'],
|
15
|
+
:draft => ['DRAFT'],
|
16
|
+
:undrafted => ['UNDRAFT']
|
17
|
+
}
|
18
|
+
|
19
|
+
attr_reader :name
|
20
|
+
attr_reader :external_name
|
21
|
+
|
22
|
+
def initialize(gmail, name="INBOX")
|
23
|
+
@name = name
|
24
|
+
@external_name = Net::IMAP.decode_utf7(name)
|
25
|
+
@gmail = gmail
|
26
|
+
end
|
27
|
+
|
28
|
+
# Returns list of emails which meets given criteria.
|
29
|
+
#
|
30
|
+
# ==== Examples
|
31
|
+
#
|
32
|
+
# gmail.inbox.emails(:all)
|
33
|
+
# gmail.inbox.emails(:unread, :from => "friend@gmail.com")
|
34
|
+
# gmail.inbox.emails(:all, :after => Time.now-(20*24*3600))
|
35
|
+
# gmail.mailbox("Test").emails(:read)
|
36
|
+
#
|
37
|
+
# gmail.mailbox("Test") do |box|
|
38
|
+
# box.emails(:read)
|
39
|
+
# box.emails(:unread) do |email|
|
40
|
+
# ... do something with each email...
|
41
|
+
# end
|
42
|
+
# end
|
43
|
+
def emails(*args, &block)
|
44
|
+
args << :all if args.size == 0
|
45
|
+
|
46
|
+
if args.first.is_a?(Symbol)
|
47
|
+
search = MAILBOX_ALIASES[args.shift].dup
|
48
|
+
opts = args.first.is_a?(Hash) ? args.first : {}
|
49
|
+
|
50
|
+
opts[:after] and search.concat ['SINCE', opts[:after].to_imap_date]
|
51
|
+
opts[:before] and search.concat ['BEFORE', opts[:before].to_imap_date]
|
52
|
+
opts[:on] and search.concat ['ON', opts[:on].to_imap_date]
|
53
|
+
opts[:from] and search.concat ['FROM', opts[:from]]
|
54
|
+
opts[:to] and search.concat ['TO', opts[:to]]
|
55
|
+
opts[:subject] and search.concat ['SUBJECT', opts[:subject]]
|
56
|
+
opts[:label] and search.concat ['LABEL', opts[:label]]
|
57
|
+
opts[:attachment] and search.concat ['HAS', 'attachment']
|
58
|
+
opts[:search] and search.concat ['BODY', opts[:search]]
|
59
|
+
opts[:body] and search.concat ['BODY', opts[:body]]
|
60
|
+
opts[:query] and search.concat opts[:query]
|
61
|
+
|
62
|
+
@gmail.mailbox(name) do
|
63
|
+
@gmail.conn.uid_search(search).collect do |uid|
|
64
|
+
message = (messages[uid] ||= Message.new(self, uid))
|
65
|
+
block.call(message) if block_given?
|
66
|
+
message
|
67
|
+
end
|
68
|
+
end
|
69
|
+
elsif args.first.is_a?(Hash)
|
70
|
+
emails(:all, args.first)
|
71
|
+
else
|
72
|
+
raise ArgumentError, "Invalid search criteria"
|
73
|
+
end
|
74
|
+
end
|
75
|
+
alias :mails :emails
|
76
|
+
alias :search :emails
|
77
|
+
alias :find :emails
|
78
|
+
alias :filter :emails
|
79
|
+
|
80
|
+
# This is a convenience method that really probably shouldn't need to exist,
|
81
|
+
# but it does make code more readable, if seriously all you want is the count
|
82
|
+
# of messages.
|
83
|
+
#
|
84
|
+
# ==== Examples
|
85
|
+
#
|
86
|
+
# gmail.inbox.count(:all)
|
87
|
+
# gmail.inbox.count(:unread, :from => "friend@gmail.com")
|
88
|
+
# gmail.mailbox("Test").count(:all, :after => Time.now-(20*24*3600))
|
89
|
+
def count(*args)
|
90
|
+
emails(*args).size
|
91
|
+
end
|
92
|
+
|
93
|
+
# This permanently removes messages which are marked as deleted
|
94
|
+
def expunge
|
95
|
+
@gmail.mailbox(name) { @gmail.conn.expunge }
|
96
|
+
end
|
97
|
+
|
98
|
+
# Cached messages.
|
99
|
+
def messages
|
100
|
+
@messages ||= {}
|
101
|
+
end
|
102
|
+
|
103
|
+
def inspect
|
104
|
+
"#<Gmail::Mailbox#{'0x%04x' % (object_id << 1)} name=#{external_name}>"
|
105
|
+
end
|
106
|
+
|
107
|
+
def to_s
|
108
|
+
name
|
109
|
+
end
|
110
|
+
|
111
|
+
MAILBOX_ALIASES.each_key { |mailbox|
|
112
|
+
define_method(mailbox) do |*args, &block|
|
113
|
+
emails(mailbox, *args, &block)
|
114
|
+
end
|
115
|
+
}
|
116
|
+
end # Message
|
117
|
+
end # Gmail
|
@@ -0,0 +1,164 @@
|
|
1
|
+
require 'mime/message'
|
2
|
+
|
3
|
+
module Gmail
|
4
|
+
class Message
|
5
|
+
# Raised when given label doesn't exists.
|
6
|
+
class NoLabelError < Exception; end
|
7
|
+
|
8
|
+
attr_reader :uid
|
9
|
+
|
10
|
+
def initialize(mailbox, uid)
|
11
|
+
@uid = uid
|
12
|
+
@mailbox = mailbox
|
13
|
+
@gmail = mailbox.instance_variable_get("@gmail") if mailbox
|
14
|
+
end
|
15
|
+
|
16
|
+
def uid
|
17
|
+
@uid ||= @gmail.conn.uid_search(['HEADER', 'Message-ID', message_id])[0]
|
18
|
+
end
|
19
|
+
|
20
|
+
# Mark message with given flag.
|
21
|
+
def flag(name)
|
22
|
+
!!@gmail.mailbox(@mailbox.name) { @gmail.conn.uid_store(uid, "+FLAGS", [name]) }
|
23
|
+
end
|
24
|
+
|
25
|
+
# Unmark message.
|
26
|
+
def unflag(name)
|
27
|
+
!!@gmail.mailbox(@mailbox.name) { @gmail.conn.uid_store(uid, "-FLAGS", [name]) }
|
28
|
+
end
|
29
|
+
|
30
|
+
# Do commonly used operations on message.
|
31
|
+
def mark(flag)
|
32
|
+
case flag
|
33
|
+
when :read then read!
|
34
|
+
when :unread then unread!
|
35
|
+
when :deleted then delete!
|
36
|
+
when :spam then spam!
|
37
|
+
else
|
38
|
+
flag(flag)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# Mark this message as a spam.
|
43
|
+
def spam!
|
44
|
+
move_to('[Gmail]/Spam')
|
45
|
+
end
|
46
|
+
|
47
|
+
# Mark as read.
|
48
|
+
def read!
|
49
|
+
flag(:Seen)
|
50
|
+
end
|
51
|
+
|
52
|
+
# Mark as unread.
|
53
|
+
def unread!
|
54
|
+
unflag(:Seen)
|
55
|
+
end
|
56
|
+
|
57
|
+
# Mark message with star.
|
58
|
+
def star!
|
59
|
+
flag('[Gmail]/Starred')
|
60
|
+
end
|
61
|
+
|
62
|
+
# Remove message from list of starred.
|
63
|
+
def unstar!
|
64
|
+
unflag('[Gmail]/Starred')
|
65
|
+
end
|
66
|
+
|
67
|
+
# Move to trash / bin.
|
68
|
+
def delete!
|
69
|
+
@mailbox.messages.delete(uid)
|
70
|
+
flag(:deleted)
|
71
|
+
|
72
|
+
# For some, it's called "Trash", for others, it's called "Bin". Support both.
|
73
|
+
trash = @gmail.labels.exist?('[Gmail]/Bin') ? '[Gmail]/Bin' : '[Gmail]/Trash'
|
74
|
+
move_to(trash) unless %w[[Gmail]/Spam [Gmail]/Bin [Gmail]/Trash].include?(@mailbox.name)
|
75
|
+
end
|
76
|
+
|
77
|
+
# Archive this message.
|
78
|
+
def archive!
|
79
|
+
move_to('[Gmail]/All Mail')
|
80
|
+
end
|
81
|
+
|
82
|
+
# Move to given box and delete from others.
|
83
|
+
def move_to(name, from=nil)
|
84
|
+
label(name, from)
|
85
|
+
delete! if !%w[[Gmail]/Bin [Gmail]/Trash].include?(name)
|
86
|
+
end
|
87
|
+
alias :move :move_to
|
88
|
+
|
89
|
+
# Move message to given and delete from others. When given mailbox doesn't
|
90
|
+
# exist then it will be automaticaly created.
|
91
|
+
def move_to!(name, from=nil)
|
92
|
+
label!(name, from) && delete!
|
93
|
+
end
|
94
|
+
alias :move! :move_to!
|
95
|
+
|
96
|
+
# Mark this message with given label. When given label doesn't exist then
|
97
|
+
# it will raise <tt>NoLabelError</tt>.
|
98
|
+
#
|
99
|
+
# See also <tt>Gmail::Message#label!</tt>.
|
100
|
+
def label(name, from=nil)
|
101
|
+
@gmail.mailbox(Net::IMAP.encode_utf7(from || @mailbox.external_name)) { @gmail.conn.uid_copy(uid, Net::IMAP.encode_utf7(name)) }
|
102
|
+
rescue Net::IMAP::NoResponseError
|
103
|
+
raise NoLabelError, "Label '#{name}' doesn't exist!"
|
104
|
+
end
|
105
|
+
|
106
|
+
# Mark this message with given label. When given label doesn't exist then
|
107
|
+
# it will be automaticaly created.
|
108
|
+
#
|
109
|
+
# See also <tt>Gmail::Message#label</tt>.
|
110
|
+
def label!(name, from=nil)
|
111
|
+
label(name, from)
|
112
|
+
rescue NoLabelError
|
113
|
+
@gmail.labels.add(Net::IMAP.encode_utf7(name))
|
114
|
+
label(name, from)
|
115
|
+
end
|
116
|
+
alias :add_label :label!
|
117
|
+
alias :add_label! :label!
|
118
|
+
|
119
|
+
# Remove given label from this message.
|
120
|
+
def remove_label!(name)
|
121
|
+
move_to('[Gmail]/All Mail', name)
|
122
|
+
end
|
123
|
+
alias :delete_label! :remove_label!
|
124
|
+
|
125
|
+
def inspect
|
126
|
+
"#<Gmail::Message#{'0x%04x' % (object_id << 1)} mailbox=#{@mailbox.external_name}#{' uid='+@uid.to_s if @uid}#{' message_id='+@message_id.to_s if @message_id}>"
|
127
|
+
end
|
128
|
+
|
129
|
+
def method_missing(meth, *args, &block)
|
130
|
+
# Delegate rest directly to the message.
|
131
|
+
if envelope.respond_to?(meth)
|
132
|
+
envelope.send(meth, *args, &block)
|
133
|
+
elsif message.respond_to?(meth)
|
134
|
+
message.send(meth, *args, &block)
|
135
|
+
else
|
136
|
+
super(meth, *args, &block)
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
def respond_to?(meth, *args, &block)
|
141
|
+
if envelope.respond_to?(meth)
|
142
|
+
return true
|
143
|
+
elsif message.respond_to?(meth)
|
144
|
+
return true
|
145
|
+
else
|
146
|
+
super(meth, *args, &block)
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
def envelope
|
151
|
+
@envelope ||= @gmail.mailbox(@mailbox.name) {
|
152
|
+
@gmail.conn.uid_fetch(uid, "ENVELOPE")[0].attr["ENVELOPE"]
|
153
|
+
}
|
154
|
+
end
|
155
|
+
|
156
|
+
def message
|
157
|
+
@message ||= Mail.new(@gmail.mailbox(@mailbox.name) {
|
158
|
+
@gmail.conn.uid_fetch(uid, "RFC822")[0].attr["RFC822"] # RFC822
|
159
|
+
})
|
160
|
+
end
|
161
|
+
alias_method :raw_message, :message
|
162
|
+
|
163
|
+
end # Message
|
164
|
+
end # Gmail
|
data/spec/client_spec.rb
ADDED
@@ -0,0 +1,173 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe "Gmail client (Plain)" do
|
4
|
+
subject { Gmail::Client::Plain }
|
5
|
+
|
6
|
+
context "on initialize" do
|
7
|
+
it "should set username, password and options" do
|
8
|
+
client = subject.new("test@gmail.com", "pass", :foo => :bar)
|
9
|
+
client.username.should == "test@gmail.com"
|
10
|
+
client.password.should == "pass"
|
11
|
+
client.options[:foo].should == :bar
|
12
|
+
end
|
13
|
+
|
14
|
+
it "should convert simple name to gmail email" do
|
15
|
+
client = subject.new("test", "pass")
|
16
|
+
client.username.should == "test@gmail.com"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
context "instance" do
|
21
|
+
def mock_client(&block)
|
22
|
+
client = Gmail::Client::Plain.new(*TEST_ACCOUNT)
|
23
|
+
if block_given?
|
24
|
+
client.connect
|
25
|
+
yield client
|
26
|
+
client.logout
|
27
|
+
end
|
28
|
+
client
|
29
|
+
end
|
30
|
+
|
31
|
+
it "should connect to GMail IMAP service" do
|
32
|
+
lambda {
|
33
|
+
client = mock_client
|
34
|
+
client.connect!.should be_true
|
35
|
+
}.should_not raise_error(Gmail::Client::ConnectionError)
|
36
|
+
end
|
37
|
+
|
38
|
+
it "should properly login to valid GMail account" do
|
39
|
+
client = mock_client
|
40
|
+
client.connect.should be_true
|
41
|
+
client.login.should be_true
|
42
|
+
client.should be_logged_in
|
43
|
+
client.logout
|
44
|
+
end
|
45
|
+
|
46
|
+
it "should raise error when given GMail account is invalid and errors enabled" do
|
47
|
+
lambda {
|
48
|
+
client = Gmail::Client::Plain.new("foo", "bar")
|
49
|
+
client.connect.should be_true
|
50
|
+
client.login!.should_not be_true
|
51
|
+
}.should raise_error(Gmail::Client::AuthorizationError)
|
52
|
+
end
|
53
|
+
|
54
|
+
it "shouldn't login when given GMail account is invalid" do
|
55
|
+
lambda {
|
56
|
+
client = Gmail::Client::Plain.new("foo", "bar")
|
57
|
+
client.connect.should be_true
|
58
|
+
client.login.should_not be_true
|
59
|
+
}.should_not raise_error(Gmail::Client::AuthorizationError)
|
60
|
+
end
|
61
|
+
|
62
|
+
it "should properly logout from GMail" do
|
63
|
+
client = mock_client
|
64
|
+
client.connect
|
65
|
+
client.login.should be_true
|
66
|
+
client.logout.should be_true
|
67
|
+
client.should_not be_logged_in
|
68
|
+
end
|
69
|
+
|
70
|
+
it "#connection should automatically log in to GMail account when it's called" do
|
71
|
+
mock_client do |client|
|
72
|
+
client.expects(:login).once.returns(false)
|
73
|
+
client.connection.should_not be_nil
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
it "should properly compose message" do
|
78
|
+
mail = mock_client.compose do
|
79
|
+
from "test@gmail.com"
|
80
|
+
to "friend@gmail.com"
|
81
|
+
subject "Hello world!"
|
82
|
+
end
|
83
|
+
mail.from.should == ["test@gmail.com"]
|
84
|
+
mail.to.should == ["friend@gmail.com"]
|
85
|
+
mail.subject.should == "Hello world!"
|
86
|
+
end
|
87
|
+
|
88
|
+
it "#compose should automatically add `from` header when it is not specified" do
|
89
|
+
mail = mock_client.compose
|
90
|
+
mail.from.should == [TEST_ACCOUNT[0]]
|
91
|
+
mail = mock_client.compose(Mail.new)
|
92
|
+
mail.from.should == [TEST_ACCOUNT[0]]
|
93
|
+
mail = mock_client.compose {}
|
94
|
+
mail.from.should == [TEST_ACCOUNT[0]]
|
95
|
+
end
|
96
|
+
|
97
|
+
it "should deliver inline composed email" do
|
98
|
+
mock_client do |client|
|
99
|
+
client.deliver do
|
100
|
+
to TEST_ACCOUNT[0]
|
101
|
+
subject "Hello world!"
|
102
|
+
body "Yeah, hello there!"
|
103
|
+
end.should be_true
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
it "should not raise error when mail can't be delivered and errors are disabled" do
|
108
|
+
lambda {
|
109
|
+
client = mock_client
|
110
|
+
client.deliver(Mail.new {}).should be_false
|
111
|
+
}.should_not raise_error(Gmail::Client::DeliveryError)
|
112
|
+
end
|
113
|
+
|
114
|
+
it "should raise error when mail can't be delivered and errors are disabled" do
|
115
|
+
lambda {
|
116
|
+
client = mock_client
|
117
|
+
client.deliver!(Mail.new {})
|
118
|
+
}.should raise_error(Gmail::Client::DeliveryError)
|
119
|
+
end
|
120
|
+
|
121
|
+
it "should properly switch to given mailbox" do
|
122
|
+
mock_client do |client|
|
123
|
+
mailbox = client.mailbox("TEST")
|
124
|
+
mailbox.should be_kind_of(Gmail::Mailbox)
|
125
|
+
mailbox.name.should == "TEST"
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
it "should properly switch to given mailbox using block style" do
|
130
|
+
mock_client do |client|
|
131
|
+
client.mailbox("TEST") do |mailbox|
|
132
|
+
mailbox.should be_kind_of(Gmail::Mailbox)
|
133
|
+
mailbox.name.should == "TEST"
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
context "labels" do
|
139
|
+
subject {
|
140
|
+
client = Gmail::Client::Plain.new(*TEST_ACCOUNT)
|
141
|
+
client.connect
|
142
|
+
client.labels
|
143
|
+
}
|
144
|
+
|
145
|
+
it "should get list of all available labels" do
|
146
|
+
labels = subject
|
147
|
+
labels.all.should include("TEST", "INBOX")
|
148
|
+
end
|
149
|
+
|
150
|
+
it "should be able to check if there is given label defined" do
|
151
|
+
labels = subject
|
152
|
+
labels.exists?("TEST").should be_true
|
153
|
+
labels.exists?("FOOBAR").should be_false
|
154
|
+
end
|
155
|
+
|
156
|
+
it "should be able to create given label" do
|
157
|
+
labels = subject
|
158
|
+
labels.create("MYLABEL")
|
159
|
+
labels.exists?("MYLABEL").should be_true
|
160
|
+
labels.create("MYLABEL").should be_false
|
161
|
+
labels.delete("MYLABEL")
|
162
|
+
end
|
163
|
+
|
164
|
+
it "should be able to remove existing label" do
|
165
|
+
labels = subject
|
166
|
+
labels.create("MYLABEL")
|
167
|
+
labels.delete("MYLABEL").should be_true
|
168
|
+
labels.exists?("MYLABEL").should be_false
|
169
|
+
labels.delete("MYLABEL").should be_false
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|