gmail 0.6.0 → 0.7.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|