gmail 0.6.0 → 0.7.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/.coveralls.yml +1 -0
- data/.gitignore +27 -27
- data/.rspec +1 -1
- data/.rubocop.yml +53 -13
- data/.rubocop_todo.yml +299 -239
- data/.travis.yml +19 -19
- data/CHANGELOG.md +152 -145
- data/Gemfile +5 -3
- data/LICENSE +21 -21
- data/README.md +380 -355
- data/Rakefile +44 -46
- data/gmail.gemspec +32 -34
- data/lib/gmail.rb +72 -78
- data/lib/gmail/client.rb +35 -34
- data/lib/gmail/client/base.rb +244 -229
- data/lib/gmail/client/plain.rb +24 -24
- data/lib/gmail/client/xoauth.rb +67 -68
- data/lib/gmail/client/xoauth2.rb +39 -39
- data/lib/gmail/imap_extensions.rb +156 -159
- data/lib/gmail/labels.rb +80 -79
- data/lib/gmail/mailbox.rb +178 -175
- data/lib/gmail/message.rb +212 -207
- data/lib/gmail/version.rb +3 -3
- data/spec/account.yml.example +1 -1
- data/spec/account.yml.obfus +2 -2
- data/spec/gmail/client/base_spec.rb +5 -5
- data/spec/gmail/client/plain_spec.rb +169 -169
- data/spec/gmail/client/xoauth2_spec.rb +186 -186
- data/spec/gmail/client/xoauth_spec.rb +5 -5
- data/spec/gmail/client_spec.rb +5 -5
- data/spec/gmail/imap_extensions_spec.rb +47 -47
- data/spec/gmail/labels_spec.rb +27 -27
- data/spec/gmail/mailbox_spec.rb +84 -84
- data/spec/gmail/message_spec.rb +181 -181
- data/spec/gmail_spec.rb +40 -39
- data/spec/recordings/gmail/_new_connects_with_client_and_give_it_context_when_block_given.yml +61 -28
- data/spec/recordings/gmail/_new_connects_with_gmail_service_and_return_valid_connection_object.yml +43 -28
- data/spec/recordings/gmail/_new_does_not_raise_error_when_couldn_t_connect_with_given_account.yml +21 -13
- data/spec/recordings/gmail/_new_raises_error_when_couldn_t_connect_with_given_account.yml +21 -13
- data/spec/recordings/gmail_client_plain/instance/delivers_inline_composed_email.yml +61 -42
- data/spec/recordings/gmail_client_plain/instance/does_not_log_in_when_given_gmail_account_is_invalid.yml +21 -13
- data/spec/recordings/gmail_client_plain/instance/does_not_raise_error_even_though_gmail_account_is_invalid.yml +21 -13
- data/spec/recordings/gmail_client_plain/instance/labels/checks_if_there_is_given_label_defined.yml +409 -196
- data/spec/recordings/gmail_client_plain/instance/labels/creates_given_label.yml +280 -151
- data/spec/recordings/gmail_client_plain/instance/labels/removes_existing_label.yml +271 -146
- data/spec/recordings/gmail_client_plain/instance/labels/returns_list_of_all_available_labels.yml +227 -113
- data/spec/recordings/gmail_client_plain/instance/properly_logs_in_to_valid_gmail_account.yml +61 -42
- data/spec/recordings/gmail_client_plain/instance/properly_logs_out_from_gmail.yml +61 -42
- data/spec/recordings/gmail_client_plain/instance/properly_switches_to_given_mailbox.yml +164 -109
- data/spec/recordings/gmail_client_plain/instance/properly_switches_to_given_mailbox_using_block_style.yml +164 -109
- data/spec/recordings/gmail_client_plain/instance/raises_error_when_given_gmail_account_is_invalid_and_errors_enabled.yml +21 -13
- data/spec/recordings/gmail_client_xo_auth2/instance/does_not_log_in_when_given_gmail_account_is_invalid.yml +24 -13
- data/spec/recordings/gmail_client_xo_auth2/instance/labels/checks_if_there_is_given_label_defined.yml +39 -27
- data/spec/recordings/gmail_client_xo_auth2/instance/labels/creates_given_label.yml +52 -39
- data/spec/recordings/gmail_client_xo_auth2/instance/labels/removes_existing_label.yml +52 -39
- data/spec/recordings/gmail_client_xo_auth2/instance/labels/returns_list_of_all_available_labels.yml +39 -27
- data/spec/recordings/gmail_client_xo_auth2/instance/properly_logs_in_to_valid_gmail_account.yml +26 -15
- data/spec/recordings/gmail_client_xo_auth2/instance/properly_logs_out_from_gmail.yml +26 -15
- data/spec/recordings/gmail_client_xo_auth2/instance/properly_switches_to_given_mailbox.yml +63 -40
- data/spec/recordings/gmail_client_xo_auth2/instance/properly_switches_to_given_mailbox_using_block_style.yml +63 -40
- data/spec/recordings/gmail_client_xo_auth2/instance/raises_error_when_given_gmail_account_is_invalid_and_errors_enabled.yml +24 -13
- data/spec/recordings/gmail_labels/localize/when_given_the_xl_is_tflag/all/localizes_into_the_appropriate_label.yml +229 -116
- data/spec/recordings/gmail_labels/localize/when_given_the_xl_is_tflag/and_the_mailbox_does_not_exist/returns_the_mailbox_name_as_a_string.yml +229 -110
- data/spec/recordings/gmail_labels/localize/when_given_the_xl_is_tflag/drafts/localizes_into_the_appropriate_label.yml +229 -116
- data/spec/recordings/gmail_labels/localize/when_given_the_xl_is_tflag/flagged/localizes_into_the_appropriate_label.yml +229 -116
- data/spec/recordings/gmail_labels/localize/when_given_the_xl_is_tflag/important/localizes_into_the_appropriate_label.yml +229 -116
- data/spec/recordings/gmail_labels/localize/when_given_the_xl_is_tflag/inbox/localizes_into_the_appropriate_label.yml +61 -42
- data/spec/recordings/gmail_labels/localize/when_given_the_xl_is_tflag/junk/localizes_into_the_appropriate_label.yml +229 -116
- data/spec/recordings/gmail_labels/localize/when_given_the_xl_is_tflag/sent/localizes_into_the_appropriate_label.yml +229 -116
- data/spec/recordings/gmail_labels/localize/when_given_the_xl_is_tflag/trash/localizes_into_the_appropriate_label.yml +229 -116
- data/spec/recordings/gmail_mailbox/instance/counts_all_emails.yml +595 -277
- data/spec/recordings/gmail_mailbox/instance/finds_messages.yml +1049 -586
- data/spec/recordings/gmail_mailbox/instance/waits_once.yml +191 -136
- data/spec/recordings/gmail_mailbox/instance/waits_repeatedly.yml +217 -141
- data/spec/recordings/gmail_mailbox/instance/waits_with_29-minute_re-issue.yml +188 -136
- data/spec/recordings/gmail_mailbox/instance/waits_with_an_unblocked_connection.yml +644 -207
- data/spec/recordings/gmail_mailbox/on_initialize/sets_client_and_name.yml +61 -42
- data/spec/recordings/gmail_mailbox/on_initialize/works_in_inbox_by_default.yml +61 -42
- data/spec/recordings/gmail_message/initialize/sets_prefetch_attrs.yml +1068 -578
- data/spec/recordings/gmail_message/initialize/sets_uid_and_mailbox.yml +1068 -580
- data/spec/recordings/gmail_message/instance_methods/deletes_itself.yml +1084 -637
- data/spec/recordings/gmail_message/instance_methods/marks_itself_read.yml +1137 -682
- data/spec/recordings/gmail_message/instance_methods/marks_itself_unread.yml +1153 -686
- data/spec/recordings/gmail_message/instance_methods/moves_from_one_tag_to_other.yml +1447 -862
- data/spec/recordings/gmail_message/instance_methods/removes_a_given_label.yml +1288 -776
- data/spec/recordings/gmail_message/instance_methods/removes_a_given_label_with_old_method.yml +1288 -776
- data/spec/recordings/gmail_message/instance_methods/sets_given_label.yml +1165 -690
- data/spec/recordings/gmail_message/instance_methods/sets_given_label_with_old_method.yml +1157 -691
- data/spec/spec_helper.rb +56 -53
- data/spec/support/imap_mock.rb +181 -181
- data/spec/support/obfuscation.rb +49 -52
- metadata +21 -90
- data/spec/recordings/gmail_client_plain/instance/_connection_automatically_logs_in_to_gmail_account_when_it_s_called.yml +0 -42
data/lib/gmail/client/plain.rb
CHANGED
@@ -1,24 +1,24 @@
|
|
1
|
-
module Gmail
|
2
|
-
module Client
|
3
|
-
class Plain < Base
|
4
|
-
attr_reader :password
|
5
|
-
|
6
|
-
def initialize(username, password, options = {})
|
7
|
-
@password = password
|
8
|
-
super(username, options)
|
9
|
-
end
|
10
|
-
|
11
|
-
def login(raise_errors = false)
|
12
|
-
@imap and @logged_in = (login = @imap.login(username, password)) && login.name == 'OK'
|
13
|
-
rescue Net::IMAP::NoResponseError => e
|
14
|
-
if raise_errors
|
15
|
-
message = "Couldn't login to given Gmail account: #{username}"
|
16
|
-
message += " (#{e.response.data.text.strip})"
|
17
|
-
raise(AuthorizationError.new(e.response), message, e.backtrace)
|
18
|
-
end
|
19
|
-
end
|
20
|
-
end # Plain
|
21
|
-
|
22
|
-
register :plain, Plain
|
23
|
-
end # Client
|
24
|
-
end # Gmail
|
1
|
+
module Gmail
|
2
|
+
module Client
|
3
|
+
class Plain < Base
|
4
|
+
attr_reader :password
|
5
|
+
|
6
|
+
def initialize(username, password, options = {})
|
7
|
+
@password = password
|
8
|
+
super(username, options)
|
9
|
+
end
|
10
|
+
|
11
|
+
def login(raise_errors = false)
|
12
|
+
@imap and @logged_in = (login = @imap.login(username, password)) && login.name == 'OK'
|
13
|
+
rescue Net::IMAP::NoResponseError => e
|
14
|
+
if raise_errors
|
15
|
+
message = "Couldn't login to given Gmail account: #{username}"
|
16
|
+
message += " (#{e.response.data.text.strip})"
|
17
|
+
raise(AuthorizationError.new(e.response), message, e.backtrace)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end # Plain
|
21
|
+
|
22
|
+
register :plain, Plain
|
23
|
+
end # Client
|
24
|
+
end # Gmail
|
data/lib/gmail/client/xoauth.rb
CHANGED
@@ -1,68 +1,67 @@
|
|
1
|
-
require 'gmail_xoauth'
|
2
|
-
|
3
|
-
module Gmail
|
4
|
-
module Client
|
5
|
-
class XOAuth < Base
|
6
|
-
attr_reader :token
|
7
|
-
attr_reader :secret
|
8
|
-
attr_reader :consumer_key
|
9
|
-
attr_reader :consumer_secret
|
10
|
-
|
11
|
-
def initialize(username, options = {})
|
12
|
-
@token = options.delete(:token)
|
13
|
-
@secret = options.delete(:secret)
|
14
|
-
@consumer_key = options.delete(:consumer_key)
|
15
|
-
@consumer_secret = options.delete(:consumer_secret)
|
16
|
-
|
17
|
-
super(username, options)
|
18
|
-
end
|
19
|
-
|
20
|
-
def login(raise_errors = false)
|
21
|
-
@imap and @logged_in = (login = @imap.authenticate('XOAUTH', username,
|
22
|
-
:consumer_key => consumer_key,
|
23
|
-
:consumer_secret => consumer_secret,
|
24
|
-
:token => token,
|
25
|
-
:token_secret => secret
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
message
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
:
|
38
|
-
:
|
39
|
-
:
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
@access_token
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
:
|
50
|
-
:
|
51
|
-
:
|
52
|
-
:
|
53
|
-
|
54
|
-
:
|
55
|
-
:
|
56
|
-
:
|
57
|
-
:
|
58
|
-
|
59
|
-
|
60
|
-
:
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
end # Gmail
|
1
|
+
require 'gmail_xoauth'
|
2
|
+
|
3
|
+
module Gmail
|
4
|
+
module Client
|
5
|
+
class XOAuth < Base
|
6
|
+
attr_reader :token
|
7
|
+
attr_reader :secret
|
8
|
+
attr_reader :consumer_key
|
9
|
+
attr_reader :consumer_secret
|
10
|
+
|
11
|
+
def initialize(username, options = {})
|
12
|
+
@token = options.delete(:token)
|
13
|
+
@secret = options.delete(:secret)
|
14
|
+
@consumer_key = options.delete(:consumer_key)
|
15
|
+
@consumer_secret = options.delete(:consumer_secret)
|
16
|
+
|
17
|
+
super(username, options)
|
18
|
+
end
|
19
|
+
|
20
|
+
def login(raise_errors = false)
|
21
|
+
@imap and @logged_in = (login = @imap.authenticate('XOAUTH', username,
|
22
|
+
:consumer_key => consumer_key,
|
23
|
+
:consumer_secret => consumer_secret,
|
24
|
+
:token => token,
|
25
|
+
:token_secret => secret)) && login.name == 'OK'
|
26
|
+
rescue Net::IMAP::NoResponseError => e
|
27
|
+
if raise_errors
|
28
|
+
message = "Couldn't login to given Gmail account: #{username}"
|
29
|
+
message += " (#{e.response.data.text.strip})"
|
30
|
+
raise(AuthorizationError.new(e.response), message, e.backtrace)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def access_token
|
35
|
+
consumer_options = {
|
36
|
+
:site => "https://www.google.com",
|
37
|
+
:request_token_path => "/accounts/OAuthGetRequestToken",
|
38
|
+
:authorize_path => "/accounts/OAuthAuthorizeToken",
|
39
|
+
:access_token_path => "/accounts/OAuthGetAccessToken"
|
40
|
+
}
|
41
|
+
consumer = OAuth::Consumer.new(consumer_key, consumer_secret, consumer_options)
|
42
|
+
@access_token ||= OAuth::AccessToken.new(consumer, token, secret)
|
43
|
+
@access_token
|
44
|
+
end
|
45
|
+
|
46
|
+
def smtp_settings
|
47
|
+
[:smtp, {
|
48
|
+
:address => GMAIL_SMTP_HOST,
|
49
|
+
:port => GMAIL_SMTP_PORT,
|
50
|
+
:domain => mail_domain,
|
51
|
+
:user_name => username,
|
52
|
+
:password => {
|
53
|
+
:consumer_key => consumer_key,
|
54
|
+
:consumer_secret => consumer_secret,
|
55
|
+
:token => token,
|
56
|
+
:token_secret => secret,
|
57
|
+
:access_token => access_token
|
58
|
+
},
|
59
|
+
:authentication => :xoauth,
|
60
|
+
:enable_starttls_auto => true
|
61
|
+
}]
|
62
|
+
end
|
63
|
+
end # XOAuth
|
64
|
+
|
65
|
+
register :xoauth, XOAuth
|
66
|
+
end # Client
|
67
|
+
end # Gmail
|
data/lib/gmail/client/xoauth2.rb
CHANGED
@@ -1,39 +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
|
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
|
@@ -1,159 +1,156 @@
|
|
1
|
-
module Gmail
|
2
|
-
module ImapExtensions
|
3
|
-
LABELS_FLAG_REGEXP = /\\([^\x80-\xff(){ \x00-\x1f\x7f%"\\]+)/n
|
4
|
-
# Taken from https://github.com/oxos/gmail-oauth-thread-stats/blob/master/gmail_imap_extensions_compatibility.rb
|
5
|
-
def self.patch_net_imap_response_parser(klass = Net::IMAP::ResponseParser)
|
6
|
-
# https://github.com/ruby/ruby/blob/4d426fc2e03078d583d5d573d4863415c3e3eb8d/lib/net/imap.rb#L2258
|
7
|
-
klass.class_eval do
|
8
|
-
def msg_att(n = -1)
|
9
|
-
match(Net::IMAP::ResponseParser::T_LPAR)
|
10
|
-
attr = {}
|
11
|
-
while true
|
12
|
-
token = lookahead
|
13
|
-
case token.symbol
|
14
|
-
when Net::IMAP::ResponseParser::T_RPAR
|
15
|
-
shift_token
|
16
|
-
break
|
17
|
-
when Net::IMAP::ResponseParser::T_SPACE
|
18
|
-
shift_token
|
19
|
-
next
|
20
|
-
end
|
21
|
-
case token.value
|
22
|
-
when /\A(?:ENVELOPE)\z/ni
|
23
|
-
name, val = envelope_data
|
24
|
-
when /\A(?:FLAGS)\z/ni
|
25
|
-
name, val = flags_data
|
26
|
-
when /\A(?:INTERNALDATE)\z/ni
|
27
|
-
name, val = internaldate_data
|
28
|
-
when /\A(?:RFC822(?:\.HEADER|\.TEXT)?)\z/ni
|
29
|
-
name, val = rfc822_text
|
30
|
-
when /\A(?:RFC822\.SIZE)\z/ni
|
31
|
-
name, val = rfc822_size
|
32
|
-
when /\A(?:BODY(?:STRUCTURE)?)\z/ni
|
33
|
-
name, val = body_data
|
34
|
-
when /\A(?:UID)\z/ni
|
35
|
-
name, val = uid_data
|
36
|
-
|
37
|
-
# Gmail extension
|
38
|
-
# Cargo-cult code warning: no idea why the regexp works - just copying a pattern
|
39
|
-
when /\A(?:X-GM-LABELS)\z/ni
|
40
|
-
name, val = x_gm_labels_data
|
41
|
-
when /\A(?:X-GM-MSGID)\z/ni
|
42
|
-
name, val = uid_data
|
43
|
-
when /\A(?:X-GM-THRID)\z/ni
|
44
|
-
name, val = uid_data
|
45
|
-
# End Gmail extension
|
46
|
-
|
47
|
-
else
|
48
|
-
parse_error("unknown attribute `%s' for {%d}", token.value, n)
|
49
|
-
end
|
50
|
-
attr[name] = val
|
51
|
-
end
|
52
|
-
return attr
|
53
|
-
end
|
54
|
-
|
55
|
-
# Based on Net::IMAP#flags_data, but calling x_gm_labels_list to parse labels
|
56
|
-
def x_gm_labels_data
|
57
|
-
token = match(self.class::T_ATOM)
|
58
|
-
name = token.value.upcase
|
59
|
-
match(self.class::T_SPACE)
|
60
|
-
return name, x_gm_label_list
|
61
|
-
end
|
62
|
-
|
63
|
-
# Based on Net::IMAP#flag_list with a modified Regexp
|
64
|
-
# Labels are returned as escape-quoted strings
|
65
|
-
# We extract the labels using a regexp which extracts any unescaped strings
|
66
|
-
def x_gm_label_list
|
67
|
-
if @str.index(/\(([^)]*)\)/ni, @pos)
|
68
|
-
resp = extract_labels_response
|
69
|
-
|
70
|
-
# We need to manually update the position of the regexp to prevent trip-ups
|
71
|
-
@pos += resp.length - 1
|
72
|
-
|
73
|
-
# `resp` will look something like this:
|
74
|
-
# ("\\Inbox" "\\Sent" "one's and two's" "some new label" Awesome Ni&APE-os)
|
75
|
-
result = resp.gsub(/^\s*\(|\)+\s*$/, '').scan(/"([^"]*)"|([^\s"]+)/ni).flatten.compact.collect(&:unescape)
|
76
|
-
result.map do |x|
|
77
|
-
flag = x.scan(LABELS_FLAG_REGEXP)
|
78
|
-
if flag.empty?
|
79
|
-
x
|
80
|
-
else
|
81
|
-
flag.first.first.capitalize.untaint.intern
|
82
|
-
end
|
83
|
-
end
|
84
|
-
else
|
85
|
-
parse_error("invalid label list")
|
86
|
-
end
|
87
|
-
end
|
88
|
-
|
89
|
-
# The way Gmail return tokens can cause issues with Net::IMAP's reader,
|
90
|
-
# so we need to extract this section manually
|
91
|
-
def extract_labels_response
|
92
|
-
special, quoted = false, false
|
93
|
-
index, paren_count = 0, 0
|
94
|
-
|
95
|
-
# Start parsing response string for the labels section, parentheses inclusive
|
96
|
-
labels_header = "X-GM-LABELS ("
|
97
|
-
start = @str.index(labels_header) + labels_header.length - 1
|
98
|
-
substr = @str[start..-1]
|
99
|
-
substr.each_char do |char|
|
100
|
-
index += 1
|
101
|
-
case char
|
102
|
-
when '('
|
103
|
-
paren_count += 1 unless quoted
|
104
|
-
when ')'
|
105
|
-
paren_count -= 1 unless quoted
|
106
|
-
break if paren_count
|
107
|
-
when '"'
|
108
|
-
quoted = !quoted unless special
|
109
|
-
end
|
110
|
-
special = (char == '\\' && !special)
|
111
|
-
end
|
112
|
-
substr[0..index]
|
113
|
-
end
|
114
|
-
end # class_eval
|
115
|
-
|
116
|
-
# Add String#unescape
|
117
|
-
add_unescape
|
118
|
-
end # PNIRP
|
119
|
-
|
120
|
-
def self.add_unescape(klass = String)
|
121
|
-
klass.class_eval do
|
122
|
-
# Add a method to string which unescapes special characters
|
123
|
-
# We use a simple state machine to ensure that specials are not
|
124
|
-
# themselves escaped
|
125
|
-
def unescape
|
126
|
-
unesc = ''
|
127
|
-
special = false
|
128
|
-
escapes = { '\\' => '\\',
|
129
|
-
'"' => '"',
|
130
|
-
'n' => "\n",
|
131
|
-
't' => "\t",
|
132
|
-
'r' => "\r",
|
133
|
-
'f' => "\f",
|
134
|
-
'v' => "\v",
|
135
|
-
'0' => "\0",
|
136
|
-
'a' => "\a"
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
#
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
end
|
158
|
-
end
|
159
|
-
end
|
1
|
+
module Gmail
|
2
|
+
module ImapExtensions
|
3
|
+
LABELS_FLAG_REGEXP = /\\([^\x80-\xff(){ \x00-\x1f\x7f%"\\]+)/n
|
4
|
+
# Taken from https://github.com/oxos/gmail-oauth-thread-stats/blob/master/gmail_imap_extensions_compatibility.rb
|
5
|
+
def self.patch_net_imap_response_parser(klass = Net::IMAP::ResponseParser)
|
6
|
+
# https://github.com/ruby/ruby/blob/4d426fc2e03078d583d5d573d4863415c3e3eb8d/lib/net/imap.rb#L2258
|
7
|
+
klass.class_eval do
|
8
|
+
def msg_att(n = -1)
|
9
|
+
match(Net::IMAP::ResponseParser::T_LPAR)
|
10
|
+
attr = {}
|
11
|
+
while true
|
12
|
+
token = lookahead
|
13
|
+
case token.symbol
|
14
|
+
when Net::IMAP::ResponseParser::T_RPAR
|
15
|
+
shift_token
|
16
|
+
break
|
17
|
+
when Net::IMAP::ResponseParser::T_SPACE
|
18
|
+
shift_token
|
19
|
+
next
|
20
|
+
end
|
21
|
+
case token.value
|
22
|
+
when /\A(?:ENVELOPE)\z/ni
|
23
|
+
name, val = envelope_data
|
24
|
+
when /\A(?:FLAGS)\z/ni
|
25
|
+
name, val = flags_data
|
26
|
+
when /\A(?:INTERNALDATE)\z/ni
|
27
|
+
name, val = internaldate_data
|
28
|
+
when /\A(?:RFC822(?:\.HEADER|\.TEXT)?)\z/ni
|
29
|
+
name, val = rfc822_text
|
30
|
+
when /\A(?:RFC822\.SIZE)\z/ni
|
31
|
+
name, val = rfc822_size
|
32
|
+
when /\A(?:BODY(?:STRUCTURE)?)\z/ni
|
33
|
+
name, val = body_data
|
34
|
+
when /\A(?:UID)\z/ni
|
35
|
+
name, val = uid_data
|
36
|
+
|
37
|
+
# Gmail extension
|
38
|
+
# Cargo-cult code warning: no idea why the regexp works - just copying a pattern
|
39
|
+
when /\A(?:X-GM-LABELS)\z/ni
|
40
|
+
name, val = x_gm_labels_data
|
41
|
+
when /\A(?:X-GM-MSGID)\z/ni
|
42
|
+
name, val = uid_data
|
43
|
+
when /\A(?:X-GM-THRID)\z/ni
|
44
|
+
name, val = uid_data
|
45
|
+
# End Gmail extension
|
46
|
+
|
47
|
+
else
|
48
|
+
parse_error("unknown attribute `%s' for {%d}", token.value, n)
|
49
|
+
end
|
50
|
+
attr[name] = val
|
51
|
+
end
|
52
|
+
return attr
|
53
|
+
end
|
54
|
+
|
55
|
+
# Based on Net::IMAP#flags_data, but calling x_gm_labels_list to parse labels
|
56
|
+
def x_gm_labels_data
|
57
|
+
token = match(self.class::T_ATOM)
|
58
|
+
name = token.value.upcase
|
59
|
+
match(self.class::T_SPACE)
|
60
|
+
return name, x_gm_label_list
|
61
|
+
end
|
62
|
+
|
63
|
+
# Based on Net::IMAP#flag_list with a modified Regexp
|
64
|
+
# Labels are returned as escape-quoted strings
|
65
|
+
# We extract the labels using a regexp which extracts any unescaped strings
|
66
|
+
def x_gm_label_list
|
67
|
+
if @str.index(/\(([^)]*)\)/ni, @pos)
|
68
|
+
resp = extract_labels_response
|
69
|
+
|
70
|
+
# We need to manually update the position of the regexp to prevent trip-ups
|
71
|
+
@pos += resp.length - 1
|
72
|
+
|
73
|
+
# `resp` will look something like this:
|
74
|
+
# ("\\Inbox" "\\Sent" "one's and two's" "some new label" Awesome Ni&APE-os)
|
75
|
+
result = resp.gsub(/^\s*\(|\)+\s*$/, '').scan(/"([^"]*)"|([^\s"]+)/ni).flatten.compact.collect(&:unescape)
|
76
|
+
result.map do |x|
|
77
|
+
flag = x.scan(LABELS_FLAG_REGEXP)
|
78
|
+
if flag.empty?
|
79
|
+
x
|
80
|
+
else
|
81
|
+
flag.first.first.capitalize.untaint.intern
|
82
|
+
end
|
83
|
+
end
|
84
|
+
else
|
85
|
+
parse_error("invalid label list")
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
# The way Gmail return tokens can cause issues with Net::IMAP's reader,
|
90
|
+
# so we need to extract this section manually
|
91
|
+
def extract_labels_response
|
92
|
+
special, quoted = false, false
|
93
|
+
index, paren_count = 0, 0
|
94
|
+
|
95
|
+
# Start parsing response string for the labels section, parentheses inclusive
|
96
|
+
labels_header = "X-GM-LABELS ("
|
97
|
+
start = @str.index(labels_header) + labels_header.length - 1
|
98
|
+
substr = @str[start..-1]
|
99
|
+
substr.each_char do |char|
|
100
|
+
index += 1
|
101
|
+
case char
|
102
|
+
when '('
|
103
|
+
paren_count += 1 unless quoted
|
104
|
+
when ')'
|
105
|
+
paren_count -= 1 unless quoted
|
106
|
+
break if paren_count.zero?
|
107
|
+
when '"'
|
108
|
+
quoted = !quoted unless special
|
109
|
+
end
|
110
|
+
special = (char == '\\' && !special)
|
111
|
+
end
|
112
|
+
substr[0..index]
|
113
|
+
end
|
114
|
+
end # class_eval
|
115
|
+
|
116
|
+
# Add String#unescape
|
117
|
+
add_unescape
|
118
|
+
end # PNIRP
|
119
|
+
|
120
|
+
def self.add_unescape(klass = String)
|
121
|
+
klass.class_eval do
|
122
|
+
# Add a method to string which unescapes special characters
|
123
|
+
# We use a simple state machine to ensure that specials are not
|
124
|
+
# themselves escaped
|
125
|
+
def unescape
|
126
|
+
unesc = ''
|
127
|
+
special = false
|
128
|
+
escapes = { '\\' => '\\',
|
129
|
+
'"' => '"',
|
130
|
+
'n' => "\n",
|
131
|
+
't' => "\t",
|
132
|
+
'r' => "\r",
|
133
|
+
'f' => "\f",
|
134
|
+
'v' => "\v",
|
135
|
+
'0' => "\0",
|
136
|
+
'a' => "\a" }
|
137
|
+
|
138
|
+
self.each_char do |char|
|
139
|
+
if special
|
140
|
+
# If in special mode, add in the replaced special char if there's a match
|
141
|
+
# Otherwise, add in the backslash and the current character
|
142
|
+
unesc << (escapes.keys.include?(char) ? escapes[char] : "\\#{char}")
|
143
|
+
special = false
|
144
|
+
elsif char == '\\'
|
145
|
+
# Toggle special mode if backslash is detected; otherwise just add character
|
146
|
+
special = true
|
147
|
+
else
|
148
|
+
unesc << char
|
149
|
+
end
|
150
|
+
end
|
151
|
+
unesc
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|