gmail 0.4.2 → 0.5.0
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.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/.rubocop.yml +13 -0
- data/.rubocop_todo.yml +239 -0
- data/.travis.yml +19 -0
- data/CHANGELOG.md +75 -18
- data/Gemfile +1 -1
- data/README.md +214 -131
- data/Rakefile +11 -0
- data/gmail.gemspec +8 -8
- data/lib/gmail.rb +31 -18
- data/lib/gmail/client.rb +11 -7
- data/lib/gmail/client/base.rb +52 -48
- data/lib/gmail/client/plain.rb +9 -5
- data/lib/gmail/client/xoauth.rb +23 -6
- data/lib/gmail/client/xoauth2.rb +39 -0
- data/lib/gmail/imap_extensions.rb +150 -0
- data/lib/gmail/labels.rb +33 -16
- data/lib/gmail/mailbox.rb +60 -42
- data/lib/gmail/message.rb +142 -101
- data/lib/gmail/version.rb +2 -2
- data/spec/account.yml.obfus +2 -0
- data/spec/gmail/client/base_spec.rb +5 -0
- data/spec/gmail/client/plain_spec.rb +168 -0
- data/spec/gmail/client/xoauth2_spec.rb +186 -0
- data/spec/gmail/client/xoauth_spec.rb +5 -0
- data/spec/gmail/client_spec.rb +5 -0
- data/spec/gmail/imap_extensions_spec.rb +12 -0
- data/spec/gmail/labels_spec.rb +18 -0
- data/spec/{mailbox_spec.rb → gmail/mailbox_spec.rb} +14 -14
- data/spec/gmail/message_spec.rb +181 -0
- data/spec/gmail_spec.rb +21 -21
- data/spec/recordings/gmail/_new_connects_with_client_and_give_it_context_when_block_given.yml +28 -0
- data/spec/recordings/gmail/_new_connects_with_gmail_service_and_return_valid_connection_object.yml +28 -0
- data/spec/recordings/gmail/_new_does_not_raise_error_when_couldn_t_connect_with_given_account.yml +13 -0
- data/spec/recordings/gmail/_new_raises_error_when_couldn_t_connect_with_given_account.yml +13 -0
- data/spec/recordings/gmail_client_plain/instance/_connection_automatically_logs_in_to_gmail_account_when_it_s_called.yml +42 -0
- data/spec/recordings/gmail_client_plain/instance/delivers_inline_composed_email.yml +42 -0
- data/spec/recordings/gmail_client_plain/instance/does_not_log_in_when_given_gmail_account_is_invalid.yml +13 -0
- data/spec/recordings/gmail_client_plain/instance/does_not_raise_error_even_though_gmail_account_is_invalid.yml +13 -0
- data/spec/recordings/gmail_client_plain/instance/labels/checks_if_there_is_given_label_defined.yml +196 -0
- data/spec/recordings/gmail_client_plain/instance/labels/creates_given_label.yml +151 -0
- data/spec/recordings/gmail_client_plain/instance/labels/removes_existing_label.yml +146 -0
- data/spec/recordings/gmail_client_plain/instance/labels/returns_list_of_all_available_labels.yml +113 -0
- data/spec/recordings/gmail_client_plain/instance/properly_logs_in_to_valid_gmail_account.yml +42 -0
- data/spec/recordings/gmail_client_plain/instance/properly_logs_out_from_gmail.yml +42 -0
- data/spec/recordings/gmail_client_plain/instance/properly_switches_to_given_mailbox.yml +109 -0
- data/spec/recordings/gmail_client_plain/instance/properly_switches_to_given_mailbox_using_block_style.yml +109 -0
- data/spec/recordings/gmail_client_plain/instance/raises_error_when_given_gmail_account_is_invalid_and_errors_enabled.yml +13 -0
- data/spec/recordings/gmail_client_xo_auth2/instance/does_not_log_in_when_given_gmail_account_is_invalid.yml +13 -0
- data/spec/recordings/gmail_client_xo_auth2/instance/labels/checks_if_there_is_given_label_defined.yml +27 -0
- data/spec/recordings/gmail_client_xo_auth2/instance/labels/creates_given_label.yml +39 -0
- data/spec/recordings/gmail_client_xo_auth2/instance/labels/removes_existing_label.yml +39 -0
- data/spec/recordings/gmail_client_xo_auth2/instance/labels/returns_list_of_all_available_labels.yml +27 -0
- data/spec/recordings/gmail_client_xo_auth2/instance/properly_logs_in_to_valid_gmail_account.yml +15 -0
- data/spec/recordings/gmail_client_xo_auth2/instance/properly_logs_out_from_gmail.yml +15 -0
- data/spec/recordings/gmail_client_xo_auth2/instance/properly_switches_to_given_mailbox.yml +40 -0
- data/spec/recordings/gmail_client_xo_auth2/instance/properly_switches_to_given_mailbox_using_block_style.yml +40 -0
- data/spec/recordings/gmail_client_xo_auth2/instance/raises_error_when_given_gmail_account_is_invalid_and_errors_enabled.yml +13 -0
- data/spec/recordings/gmail_labels/localize/when_given_the_xl_is_tflag/all/localizes_into_the_appropriate_label.yml +116 -0
- data/spec/recordings/gmail_labels/localize/when_given_the_xl_is_tflag/drafts/localizes_into_the_appropriate_label.yml +116 -0
- data/spec/recordings/gmail_labels/localize/when_given_the_xl_is_tflag/flagged/localizes_into_the_appropriate_label.yml +116 -0
- data/spec/recordings/gmail_labels/localize/when_given_the_xl_is_tflag/important/localizes_into_the_appropriate_label.yml +116 -0
- data/spec/recordings/gmail_labels/localize/when_given_the_xl_is_tflag/inbox/localizes_into_the_appropriate_label.yml +42 -0
- data/spec/recordings/gmail_labels/localize/when_given_the_xl_is_tflag/junk/localizes_into_the_appropriate_label.yml +116 -0
- data/spec/recordings/gmail_labels/localize/when_given_the_xl_is_tflag/sent/localizes_into_the_appropriate_label.yml +116 -0
- data/spec/recordings/gmail_labels/localize/when_given_the_xl_is_tflag/trash/localizes_into_the_appropriate_label.yml +116 -0
- data/spec/recordings/gmail_mailbox/instance/counts_all_emails.yml +277 -0
- data/spec/recordings/gmail_mailbox/instance/finds_messages.yml +586 -0
- data/spec/recordings/gmail_mailbox/on_initialize/sets_client_and_name.yml +42 -0
- data/spec/recordings/gmail_mailbox/on_initialize/works_in_inbox_by_default.yml +42 -0
- data/spec/recordings/gmail_message/initialize/sets_prefetch_attrs.yml +578 -0
- data/spec/recordings/gmail_message/initialize/sets_uid_and_mailbox.yml +580 -0
- data/spec/recordings/gmail_message/instance_methods/deletes_itself.yml +637 -0
- data/spec/recordings/gmail_message/instance_methods/marks_itself_read.yml +682 -0
- data/spec/recordings/gmail_message/instance_methods/marks_itself_unread.yml +686 -0
- data/spec/recordings/gmail_message/instance_methods/moves_from_one_tag_to_other.yml +862 -0
- data/spec/recordings/gmail_message/instance_methods/removes_a_given_label.yml +776 -0
- data/spec/recordings/gmail_message/instance_methods/removes_a_given_label_with_old_method.yml +776 -0
- data/spec/recordings/gmail_message/instance_methods/sets_given_label.yml +690 -0
- data/spec/recordings/gmail_message/instance_methods/sets_given_label_with_old_method.yml +691 -0
- data/spec/spec_helper.rb +34 -10
- data/spec/support/imap_mock.rb +129 -0
- data/spec/support/obfuscation.rb +52 -0
- metadata +79 -30
- data/TODO.md +0 -12
- data/lib/gmail/client/imap_extensions.rb +0 -54
- data/spec/client_spec.rb +0 -178
- data/spec/message_spec.rb +0 -51
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'gmail_xoauth'
|
2
|
+
|
3
|
+
module Gmail
|
4
|
+
module Client
|
5
|
+
class XOAuth2 < Base
|
6
|
+
attr_reader :token
|
7
|
+
|
8
|
+
def initialize(username, token)
|
9
|
+
@token = token
|
10
|
+
|
11
|
+
super(username, {})
|
12
|
+
end
|
13
|
+
|
14
|
+
def login(raise_errors = false)
|
15
|
+
@imap and @logged_in = (login = @imap.authenticate('XOAUTH2', username, token)) && login.name == 'OK'
|
16
|
+
rescue Net::IMAP::NoResponseError => e
|
17
|
+
if raise_errors
|
18
|
+
message = "Couldn't login to given Gmail account: #{username}"
|
19
|
+
message += " (#{e.response.data.text.strip})"
|
20
|
+
raise(AuthorizationError.new(e.response), message, e.backtrace)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def smtp_settings
|
25
|
+
[:smtp, {
|
26
|
+
:address => GMAIL_SMTP_HOST,
|
27
|
+
:port => GMAIL_SMTP_PORT,
|
28
|
+
:domain => mail_domain,
|
29
|
+
:user_name => username,
|
30
|
+
:password => token,
|
31
|
+
:authentication => :xoauth2,
|
32
|
+
:enable_starttls_auto => true
|
33
|
+
}]
|
34
|
+
end
|
35
|
+
end # XOAuth
|
36
|
+
|
37
|
+
register :xoauth2, XOAuth2
|
38
|
+
end # Client
|
39
|
+
end # Gmail
|
@@ -0,0 +1,150 @@
|
|
1
|
+
module Gmail
|
2
|
+
module ImapExtensions
|
3
|
+
# Taken from https://github.com/oxos/gmail-oauth-thread-stats/blob/master/gmail_imap_extensions_compatibility.rb
|
4
|
+
def self.patch_net_imap_response_parser(klass = Net::IMAP::ResponseParser)
|
5
|
+
# https://github.com/ruby/ruby/blob/4d426fc2e03078d583d5d573d4863415c3e3eb8d/lib/net/imap.rb#L2258
|
6
|
+
klass.class_eval do
|
7
|
+
def msg_att(n = -1)
|
8
|
+
match(Net::IMAP::ResponseParser::T_LPAR)
|
9
|
+
attr = {}
|
10
|
+
while true
|
11
|
+
token = lookahead
|
12
|
+
case token.symbol
|
13
|
+
when Net::IMAP::ResponseParser::T_RPAR
|
14
|
+
shift_token
|
15
|
+
break
|
16
|
+
when Net::IMAP::ResponseParser::T_SPACE
|
17
|
+
shift_token
|
18
|
+
next
|
19
|
+
end
|
20
|
+
case token.value
|
21
|
+
when /\A(?:ENVELOPE)\z/ni
|
22
|
+
name, val = envelope_data
|
23
|
+
when /\A(?:FLAGS)\z/ni
|
24
|
+
name, val = flags_data
|
25
|
+
when /\A(?:INTERNALDATE)\z/ni
|
26
|
+
name, val = internaldate_data
|
27
|
+
when /\A(?:RFC822(?:\.HEADER|\.TEXT)?)\z/ni
|
28
|
+
name, val = rfc822_text
|
29
|
+
when /\A(?:RFC822\.SIZE)\z/ni
|
30
|
+
name, val = rfc822_size
|
31
|
+
when /\A(?:BODY(?:STRUCTURE)?)\z/ni
|
32
|
+
name, val = body_data
|
33
|
+
when /\A(?:UID)\z/ni
|
34
|
+
name, val = uid_data
|
35
|
+
|
36
|
+
# Gmail extension
|
37
|
+
# Cargo-cult code warning: no idea why the regexp works - just copying a pattern
|
38
|
+
when /\A(?:X-GM-LABELS)\z/ni
|
39
|
+
name, val = x_gm_labels_data
|
40
|
+
when /\A(?:X-GM-MSGID)\z/ni
|
41
|
+
name, val = uid_data
|
42
|
+
when /\A(?:X-GM-THRID)\z/ni
|
43
|
+
name, val = uid_data
|
44
|
+
# End Gmail extension
|
45
|
+
|
46
|
+
else
|
47
|
+
parse_error("unknown attribute `%s' for {%d}", token.value, n)
|
48
|
+
end
|
49
|
+
attr[name] = val
|
50
|
+
end
|
51
|
+
return attr
|
52
|
+
end
|
53
|
+
|
54
|
+
# Based on Net::IMAP#flags_data, but calling x_gm_labels_list to parse labels
|
55
|
+
def x_gm_labels_data
|
56
|
+
token = match(self.class::T_ATOM)
|
57
|
+
name = token.value.upcase
|
58
|
+
match(self.class::T_SPACE)
|
59
|
+
return name, x_gm_label_list
|
60
|
+
end
|
61
|
+
|
62
|
+
# Based on Net::IMAP#flag_list with a modified Regexp
|
63
|
+
# Labels are returned as escape-quoted strings
|
64
|
+
# We extract the labels using a regexp which extracts any unescaped strings
|
65
|
+
def x_gm_label_list
|
66
|
+
if @str.index(/\(([^)]*)\)/ni, @pos)
|
67
|
+
resp = extract_labels_response
|
68
|
+
|
69
|
+
# We need to manually update the position of the regexp to prevent trip-ups
|
70
|
+
@pos += resp.length
|
71
|
+
|
72
|
+
# `resp` will look something like this:
|
73
|
+
# ("\\Inbox" "\\Sent" "one's and two's" "some new label" Awesome Ni&APE-os)
|
74
|
+
return resp.gsub(/^\s*\(|\)\s*$/, '').scan(/"([^"]*)"|([^\s"]+)/ni).flatten.compact.collect(&:unescape)
|
75
|
+
else
|
76
|
+
parse_error("invalid label list")
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
# The way Gmail return tokens can cause issues with Net::IMAP's reader,
|
81
|
+
# so we need to extract this section manually
|
82
|
+
def extract_labels_response
|
83
|
+
special, quoted = false, false
|
84
|
+
index, paren_count = 0, 0
|
85
|
+
|
86
|
+
# Start parsing response string for the labels section, parentheses inclusive
|
87
|
+
labels_header = "X-GM-LABELS ("
|
88
|
+
start = @str.index(labels_header) + labels_header.length - 1
|
89
|
+
substr = @str[start..-1]
|
90
|
+
substr.each_char do |char|
|
91
|
+
index += 1
|
92
|
+
case char
|
93
|
+
when '('
|
94
|
+
paren_count += 1 unless quoted
|
95
|
+
when ')'
|
96
|
+
paren_count -= 1 unless quoted
|
97
|
+
break if paren_count == 0
|
98
|
+
when '"'
|
99
|
+
quoted = !quoted unless special
|
100
|
+
end
|
101
|
+
special = (char == '\\' && !special)
|
102
|
+
end
|
103
|
+
substr[0..index]
|
104
|
+
end
|
105
|
+
end # class_eval
|
106
|
+
|
107
|
+
# Add String#unescape
|
108
|
+
add_unescape
|
109
|
+
end # PNIRP
|
110
|
+
|
111
|
+
def self.add_unescape(klass = String)
|
112
|
+
klass.class_eval do
|
113
|
+
# Add a method to string which unescapes special characters
|
114
|
+
# We use a simple state machine to ensure that specials are not
|
115
|
+
# themselves escaped
|
116
|
+
def unescape
|
117
|
+
unesc = ''
|
118
|
+
special = false
|
119
|
+
escapes = { '\\' => '\\',
|
120
|
+
'"' => '"',
|
121
|
+
'n' => "\n",
|
122
|
+
't' => "\t",
|
123
|
+
'r' => "\r",
|
124
|
+
'f' => "\f",
|
125
|
+
'v' => "\v",
|
126
|
+
'0' => "\0",
|
127
|
+
'a' => "\a"
|
128
|
+
}
|
129
|
+
|
130
|
+
self.each_char do |char|
|
131
|
+
if special
|
132
|
+
# If in special mode, add in the replaced special char if there's a match
|
133
|
+
# Otherwise, add in the backslash and the current character
|
134
|
+
unesc << (escapes.keys.include?(char) ? escapes[char] : "\\#{char}")
|
135
|
+
special = false
|
136
|
+
else
|
137
|
+
# Toggle special mode if backslash is detected; otherwise just add character
|
138
|
+
if char == '\\'
|
139
|
+
special = true
|
140
|
+
else
|
141
|
+
unesc << char
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
unesc
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
data/lib/gmail/labels.rb
CHANGED
@@ -3,21 +3,21 @@ module Gmail
|
|
3
3
|
include Enumerable
|
4
4
|
attr_reader :connection
|
5
5
|
alias :conn :connection
|
6
|
-
|
6
|
+
|
7
7
|
def initialize(connection)
|
8
8
|
@connection = connection
|
9
9
|
end
|
10
|
-
|
10
|
+
|
11
11
|
# Get list of all defined labels.
|
12
12
|
def all
|
13
13
|
@list = []
|
14
|
-
|
14
|
+
|
15
15
|
## check each item in list for subfolders
|
16
|
-
conn.list("", "%").each {|l| sublabels_or_label(l)}
|
17
|
-
|
18
|
-
@list.inject([]) do |labels,label|
|
19
|
-
label[:name].each_line {|l| labels << Net::IMAP.decode_utf7(l) }
|
20
|
-
labels
|
16
|
+
conn.list("", "%").each { |l| sublabels_or_label(l) }
|
17
|
+
|
18
|
+
@list.inject([]) do |labels, label|
|
19
|
+
label[:name].each_line { |l| labels << Net::IMAP.decode_utf7(l) }
|
20
|
+
labels
|
21
21
|
end
|
22
22
|
end
|
23
23
|
alias :list :all
|
@@ -28,35 +28,52 @@ module Gmail
|
|
28
28
|
@list << label
|
29
29
|
else
|
30
30
|
@list << label
|
31
|
-
conn.list("#{label.name}/", "%").each {|l| sublabels_or_label(l)}
|
31
|
+
conn.list("#{label.name}/", "%").each { |l| sublabels_or_label(l) }
|
32
32
|
end
|
33
33
|
end
|
34
|
-
|
34
|
+
|
35
35
|
def each(*args, &block)
|
36
36
|
all.each(*args, &block)
|
37
37
|
end
|
38
|
-
|
39
|
-
# Returns +true+ when given label defined.
|
38
|
+
|
39
|
+
# Returns +true+ when given label defined.
|
40
40
|
def exists?(label)
|
41
41
|
all.include?(label)
|
42
42
|
end
|
43
43
|
alias :exist? :exists?
|
44
|
-
|
44
|
+
|
45
45
|
# Creates given label in your account.
|
46
46
|
def create(label)
|
47
47
|
!!conn.create(Net::IMAP.encode_utf7(label)) rescue false
|
48
48
|
end
|
49
49
|
alias :new :create
|
50
50
|
alias :add :create
|
51
|
-
|
52
|
-
# Deletes given label from your account.
|
51
|
+
|
52
|
+
# Deletes given label from your account.
|
53
53
|
def delete(label)
|
54
54
|
!!conn.delete(Net::IMAP.encode_utf7(label)) rescue false
|
55
55
|
end
|
56
56
|
alias :remove :delete
|
57
|
-
|
57
|
+
|
58
58
|
def inspect
|
59
59
|
"#<Gmail::Labels#{'0x%04x' % (object_id << 1)}>"
|
60
60
|
end
|
61
|
+
|
62
|
+
# Localizes a specific label flag into a label name
|
63
|
+
|
64
|
+
# Accepts standard mailbox flags returned by LIST's special-use extension:
|
65
|
+
# :Inbox, :All, :Drafts, :Sent, :Trash, :Important, :Junk, :Flagged
|
66
|
+
# and their string equivalents. Capitalization agnostic.
|
67
|
+
def localize(label)
|
68
|
+
type = label.to_sym.capitalize
|
69
|
+
if [:All, :Drafts, :Sent, :Trash, :Important, :Junk, :Flagged].include? type
|
70
|
+
@mailboxes ||= connection.list("", "*")
|
71
|
+
@mailboxes.select { |box| box.attr.include? type }.collect(&:name).compact.uniq.first
|
72
|
+
elsif type == :Inbox
|
73
|
+
'INBOX'
|
74
|
+
else
|
75
|
+
label
|
76
|
+
end
|
77
|
+
end
|
61
78
|
end # Labels
|
62
79
|
end # Gmail
|
data/lib/gmail/mailbox.rb
CHANGED
@@ -9,44 +9,29 @@ module Gmail
|
|
9
9
|
:flagged => ['FLAGGED'],
|
10
10
|
:unflagged => ['UNFLAGGED'],
|
11
11
|
:starred => ['FLAGGED'],
|
12
|
-
:unstarred => ['UNFLAGGED'],
|
12
|
+
:unstarred => ['UNFLAGGED'],
|
13
13
|
:deleted => ['DELETED'],
|
14
14
|
:undeleted => ['UNDELETED'],
|
15
15
|
:draft => ['DRAFT'],
|
16
16
|
:undrafted => ['UNDRAFT']
|
17
17
|
}
|
18
|
-
|
18
|
+
|
19
19
|
attr_reader :name
|
20
|
-
attr_reader :
|
20
|
+
attr_reader :encoded_name
|
21
21
|
|
22
|
-
def initialize(gmail, name="INBOX")
|
23
|
-
@name
|
24
|
-
@
|
22
|
+
def initialize(gmail, name = "INBOX")
|
23
|
+
@name = Net::IMAP.decode_utf7(name)
|
24
|
+
@encoded_name = Net::IMAP.encode_utf7(name)
|
25
25
|
@gmail = gmail
|
26
26
|
end
|
27
27
|
|
28
|
-
|
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)
|
28
|
+
def fetch_uids(*args)
|
44
29
|
args << :all if args.size == 0
|
45
30
|
|
46
|
-
if args.first.is_a?(Symbol)
|
31
|
+
if args.first.is_a?(Symbol)
|
47
32
|
search = MAILBOX_ALIASES[args.shift].dup
|
48
33
|
opts = args.first.is_a?(Hash) ? args.first : {}
|
49
|
-
|
34
|
+
|
50
35
|
opts[:after] and search.concat ['SINCE', opts[:after].to_imap_date]
|
51
36
|
opts[:before] and search.concat ['BEFORE', opts[:before].to_imap_date]
|
52
37
|
opts[:on] and search.concat ['ON', opts[:on].to_imap_date]
|
@@ -57,28 +42,66 @@ module Gmail
|
|
57
42
|
opts[:attachment] and search.concat ['HAS', 'attachment']
|
58
43
|
opts[:search] and search.concat ['BODY', opts[:search]]
|
59
44
|
opts[:body] and search.concat ['BODY', opts[:body]]
|
45
|
+
opts[:uid] and search.concat ['UID', opts[:uid]]
|
46
|
+
opts[:gm] and search.concat ['X-GM-RAW', opts[:gm]]
|
60
47
|
opts[:query] and search.concat opts[:query]
|
61
48
|
|
62
|
-
@gmail.mailbox(name)
|
63
|
-
@gmail.conn.uid_search(search)
|
64
|
-
|
65
|
-
block.call(message) if block_given?
|
66
|
-
message
|
67
|
-
end
|
68
|
-
end
|
49
|
+
@gmail.mailbox(name) {
|
50
|
+
@gmail.conn.uid_search(search)
|
51
|
+
}
|
69
52
|
elsif args.first.is_a?(Hash)
|
70
|
-
|
53
|
+
fetch_uids(:all, args.first)
|
71
54
|
else
|
72
55
|
raise ArgumentError, "Invalid search criteria"
|
73
56
|
end
|
74
57
|
end
|
75
|
-
|
58
|
+
|
59
|
+
# Returns list of emails which meets given criteria.
|
60
|
+
#
|
61
|
+
# ==== Examples
|
62
|
+
#
|
63
|
+
# gmail.inbox.emails(:all)
|
64
|
+
# gmail.inbox.emails(:unread, :from => "friend@gmail.com")
|
65
|
+
# gmail.inbox.emails(:all, :after => Time.now-(20*24*3600))
|
66
|
+
# gmail.mailbox("Test").emails(:read)
|
67
|
+
#
|
68
|
+
# gmail.mailbox("Test") do |box|
|
69
|
+
# box.emails(:read)
|
70
|
+
# box.emails(:unread) do |email|
|
71
|
+
# ... do something with each email...
|
72
|
+
# end
|
73
|
+
# end
|
74
|
+
def emails(*args, &block)
|
75
|
+
fetch_uids(*args).collect do |uid|
|
76
|
+
message = Message.new(self, uid)
|
77
|
+
yield(message) if block_given?
|
78
|
+
message
|
79
|
+
end
|
80
|
+
end
|
76
81
|
alias :search :emails
|
77
82
|
alias :find :emails
|
78
|
-
alias :filter :emails
|
79
83
|
|
80
|
-
|
81
|
-
|
84
|
+
def emails_in_batches(*args, &block)
|
85
|
+
messages = Array.new
|
86
|
+
|
87
|
+
uids = fetch_uids(*args)
|
88
|
+
if uids && uids.any?
|
89
|
+
uids.each_slice(100) do |slice|
|
90
|
+
@gmail.conn.uid_fetch(slice, Message::PREFETCH_ATTRS).each do |data|
|
91
|
+
message = Message.new(self, nil, data)
|
92
|
+
yield(message) if block_given?
|
93
|
+
messages << message
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
messages
|
99
|
+
end
|
100
|
+
alias :search_in_batches :emails_in_batches
|
101
|
+
alias :find_in_batches :emails_in_batches
|
102
|
+
|
103
|
+
# This is a convenience method that really probably shouldn't need to exist,
|
104
|
+
# but it does make code more readable, if seriously all you want is the count
|
82
105
|
# of messages.
|
83
106
|
#
|
84
107
|
# ==== Examples
|
@@ -95,13 +118,8 @@ module Gmail
|
|
95
118
|
@gmail.mailbox(name) { @gmail.conn.expunge }
|
96
119
|
end
|
97
120
|
|
98
|
-
# Cached messages.
|
99
|
-
def messages
|
100
|
-
@messages ||= {}
|
101
|
-
end
|
102
|
-
|
103
121
|
def inspect
|
104
|
-
"#<Gmail::Mailbox#{'0x%04x' % (object_id << 1)} name=#{
|
122
|
+
"#<Gmail::Mailbox#{'0x%04x' % (object_id << 1)} name=#{name}>"
|
105
123
|
end
|
106
124
|
|
107
125
|
def to_s
|
data/lib/gmail/message.rb
CHANGED
@@ -1,133 +1,167 @@
|
|
1
1
|
module Gmail
|
2
2
|
class Message
|
3
|
+
PREFETCH_ATTRS = ["UID", "ENVELOPE", "BODY.PEEK[]", "FLAGS", "X-GM-LABELS", "X-GM-MSGID", "X-GM-THRID"]
|
4
|
+
|
3
5
|
# Raised when given label doesn't exists.
|
4
|
-
class NoLabelError < Exception; end
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
def initialize(mailbox, uid)
|
6
|
+
class NoLabelError < Exception; end
|
7
|
+
|
8
|
+
def initialize(mailbox, uid, _attrs = nil)
|
9
9
|
@uid = uid
|
10
10
|
@mailbox = mailbox
|
11
|
-
@gmail = mailbox.instance_variable_get("@gmail") if mailbox
|
12
|
-
|
13
|
-
|
14
|
-
def labels
|
15
|
-
@gmail.conn.uid_fetch(uid, "X-GM-LABELS")[0].attr["X-GM-LABELS"]
|
11
|
+
@gmail = mailbox.instance_variable_get("@gmail") if mailbox # UGLY
|
12
|
+
@_attrs = _attrs
|
16
13
|
end
|
17
|
-
|
14
|
+
|
18
15
|
def uid
|
19
|
-
@uid ||=
|
16
|
+
@uid ||= fetch("UID")
|
17
|
+
end
|
18
|
+
|
19
|
+
def msg_id
|
20
|
+
@msg_id ||= fetch("X-GM-MSGID")
|
21
|
+
end
|
22
|
+
alias_method :message_id, :msg_id
|
23
|
+
|
24
|
+
def thr_id
|
25
|
+
@thr_id ||= fetch("X-GM-THRID")
|
26
|
+
end
|
27
|
+
alias_method :thread_id, :thr_id
|
28
|
+
|
29
|
+
def envelope
|
30
|
+
@envelope ||= fetch("ENVELOPE")
|
31
|
+
end
|
32
|
+
|
33
|
+
def message
|
34
|
+
@message ||= Mail.new(fetch("BODY[]"))
|
20
35
|
end
|
21
|
-
|
36
|
+
alias_method :raw_message, :message
|
37
|
+
|
38
|
+
def flags
|
39
|
+
@flags ||= fetch("FLAGS")
|
40
|
+
end
|
41
|
+
|
42
|
+
def labels
|
43
|
+
@labels ||= fetch("X-GM-LABELS")
|
44
|
+
end
|
45
|
+
|
22
46
|
# Mark message with given flag.
|
23
47
|
def flag(name)
|
24
|
-
!!@gmail.mailbox(@mailbox.name)
|
48
|
+
!!@gmail.mailbox(@mailbox.name) do
|
49
|
+
@gmail.conn.uid_store(uid, "+FLAGS", [name])
|
50
|
+
clear_cached_attributes
|
51
|
+
end
|
25
52
|
end
|
26
|
-
|
27
|
-
# Unmark message.
|
53
|
+
|
54
|
+
# Unmark message.
|
28
55
|
def unflag(name)
|
29
|
-
!!@gmail.mailbox(@mailbox.name)
|
56
|
+
!!@gmail.mailbox(@mailbox.name) do
|
57
|
+
@gmail.conn.uid_store(uid, "-FLAGS", [name])
|
58
|
+
clear_cached_attributes
|
59
|
+
end
|
30
60
|
end
|
31
|
-
|
32
|
-
# Do commonly used operations on message.
|
61
|
+
|
62
|
+
# Do commonly used operations on message.
|
33
63
|
def mark(flag)
|
34
64
|
case flag
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
65
|
+
when :read then read!
|
66
|
+
when :unread then unread!
|
67
|
+
when :deleted then delete!
|
68
|
+
when :spam then spam!
|
39
69
|
else
|
40
70
|
flag(flag)
|
41
71
|
end
|
42
72
|
end
|
43
|
-
|
44
|
-
#
|
45
|
-
def
|
46
|
-
|
73
|
+
|
74
|
+
# Check whether message is read
|
75
|
+
def read?
|
76
|
+
flags.include?(:Seen)
|
47
77
|
end
|
48
|
-
|
78
|
+
|
49
79
|
# Mark as read.
|
50
80
|
def read!
|
51
81
|
flag(:Seen)
|
52
82
|
end
|
53
|
-
|
83
|
+
|
54
84
|
# Mark as unread.
|
55
85
|
def unread!
|
56
86
|
unflag(:Seen)
|
57
87
|
end
|
58
|
-
|
88
|
+
|
89
|
+
# Check whether message is starred
|
90
|
+
def starred?
|
91
|
+
flags.include?(:Flagged)
|
92
|
+
end
|
93
|
+
|
59
94
|
# Mark message with star.
|
60
95
|
def star!
|
61
|
-
flag(
|
96
|
+
flag(:Flagged)
|
62
97
|
end
|
63
|
-
|
98
|
+
|
64
99
|
# Remove message from list of starred.
|
65
100
|
def unstar!
|
66
|
-
unflag(
|
101
|
+
unflag(:Flagged)
|
67
102
|
end
|
68
|
-
|
69
|
-
# Move to trash / bin.
|
70
|
-
def delete!
|
71
|
-
@mailbox.messages.delete(uid)
|
72
|
-
flag(:deleted)
|
73
103
|
|
74
|
-
|
75
|
-
|
76
|
-
|
104
|
+
# Marking as spam is done by adding the `\Spam` label. To undo this,
|
105
|
+
# you just re-apply the `\Inbox` label (see `#unspam!`)
|
106
|
+
def spam!
|
107
|
+
add_label("\\Spam")
|
77
108
|
end
|
78
109
|
|
79
|
-
#
|
110
|
+
# Deleting is done by adding the `\Trash` label. To undo this,
|
111
|
+
# you just re-apply the `\Inbox` label (see `#undelete!`)
|
112
|
+
def delete!
|
113
|
+
add_label("\\Trash")
|
114
|
+
end
|
115
|
+
|
116
|
+
# Archiving is done by adding the `\Trash` label. To undo this,
|
117
|
+
# you just re-apply the `\Inbox` label (see `#unarchive!`)
|
80
118
|
def archive!
|
81
|
-
|
82
|
-
end
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
#
|
92
|
-
#
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
move_to('[Gmail]/All Mail', name)
|
124
|
-
end
|
125
|
-
alias :delete_label! :remove_label!
|
126
|
-
|
119
|
+
remove_label("\\Inbox")
|
120
|
+
end
|
121
|
+
|
122
|
+
def unarchive!
|
123
|
+
add_label("\\Inbox")
|
124
|
+
end
|
125
|
+
alias_method :unspam!, :unarchive!
|
126
|
+
alias_method :undelete!, :unarchive!
|
127
|
+
|
128
|
+
# Move to given box and delete from others.
|
129
|
+
# Apply a given label and optionally remove one.
|
130
|
+
# TODO: We should probably deprecate this method. It doesn't really add a lot
|
131
|
+
# of value, especially since the concept of "moving" a message from one
|
132
|
+
# label to another doesn't totally make sense in the Gmail world.
|
133
|
+
def move_to(name, from = nil)
|
134
|
+
add_label(name)
|
135
|
+
remove_label(from) if from
|
136
|
+
end
|
137
|
+
alias_method :move, :move_to
|
138
|
+
alias_method :move!, :move_to
|
139
|
+
alias_method :move_to!, :move_to
|
140
|
+
|
141
|
+
# Use Gmail IMAP Extensions to add a Label to an email
|
142
|
+
def add_label(name)
|
143
|
+
@gmail.mailbox(@mailbox.name) do
|
144
|
+
@gmail.conn.uid_store(uid, "+X-GM-LABELS", [Net::IMAP.encode_utf7(name.to_s)])
|
145
|
+
clear_cached_attributes
|
146
|
+
end
|
147
|
+
end
|
148
|
+
alias_method :label, :add_label
|
149
|
+
alias_method :label!, :add_label
|
150
|
+
alias_method :add_label!, :add_label
|
151
|
+
|
152
|
+
# Use Gmail IMAP Extensions to remove a Label from an email
|
153
|
+
def remove_label(name)
|
154
|
+
@gmail.mailbox(@mailbox.name) do
|
155
|
+
@gmail.conn.uid_store(uid, "-X-GM-LABELS", [Net::IMAP.encode_utf7(name.to_s)])
|
156
|
+
clear_cached_attributes
|
157
|
+
end
|
158
|
+
end
|
159
|
+
alias_method :remove_label!, :remove_label
|
160
|
+
|
127
161
|
def inspect
|
128
|
-
"#<Gmail::Message#{'0x%04x' % (object_id << 1)} mailbox=#{@mailbox.
|
162
|
+
"#<Gmail::Message#{'0x%04x' % (object_id << 1)} mailbox=#{@mailbox.name}#{' uid=' + @uid.to_s if @uid}#{' message_id=' + @msg_id.to_s if @msg_id}>"
|
129
163
|
end
|
130
|
-
|
164
|
+
|
131
165
|
def method_missing(meth, *args, &block)
|
132
166
|
# Delegate rest directly to the message.
|
133
167
|
if envelope.respond_to?(meth)
|
@@ -149,18 +183,25 @@ module Gmail
|
|
149
183
|
end
|
150
184
|
end
|
151
185
|
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
@message
|
160
|
-
|
161
|
-
|
186
|
+
private
|
187
|
+
|
188
|
+
def clear_cached_attributes
|
189
|
+
@_attrs = nil
|
190
|
+
@msg_id = nil
|
191
|
+
@thr_id = nil
|
192
|
+
@envelope = nil
|
193
|
+
@message = nil
|
194
|
+
@flags = nil
|
195
|
+
@labels = nil
|
162
196
|
end
|
163
|
-
alias_method :raw_message, :message
|
164
197
|
|
198
|
+
def fetch(value)
|
199
|
+
@_attrs ||= begin
|
200
|
+
@gmail.mailbox(@mailbox.name) do
|
201
|
+
@gmail.conn.uid_fetch(uid, PREFETCH_ATTRS)[0]
|
202
|
+
end
|
203
|
+
end
|
204
|
+
@_attrs.attr[value]
|
205
|
+
end
|
165
206
|
end # Message
|
166
207
|
end # Gmail
|