gmail 0.6.0 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (94) hide show
  1. checksums.yaml +4 -4
  2. data/.coveralls.yml +1 -0
  3. data/.gitignore +27 -27
  4. data/.rspec +1 -1
  5. data/.rubocop.yml +53 -13
  6. data/.rubocop_todo.yml +299 -239
  7. data/.travis.yml +19 -19
  8. data/CHANGELOG.md +152 -145
  9. data/Gemfile +5 -3
  10. data/LICENSE +21 -21
  11. data/README.md +380 -355
  12. data/Rakefile +44 -46
  13. data/gmail.gemspec +32 -34
  14. data/lib/gmail.rb +72 -78
  15. data/lib/gmail/client.rb +35 -34
  16. data/lib/gmail/client/base.rb +244 -229
  17. data/lib/gmail/client/plain.rb +24 -24
  18. data/lib/gmail/client/xoauth.rb +67 -68
  19. data/lib/gmail/client/xoauth2.rb +39 -39
  20. data/lib/gmail/imap_extensions.rb +156 -159
  21. data/lib/gmail/labels.rb +80 -79
  22. data/lib/gmail/mailbox.rb +178 -175
  23. data/lib/gmail/message.rb +212 -207
  24. data/lib/gmail/version.rb +3 -3
  25. data/spec/account.yml.example +1 -1
  26. data/spec/account.yml.obfus +2 -2
  27. data/spec/gmail/client/base_spec.rb +5 -5
  28. data/spec/gmail/client/plain_spec.rb +169 -169
  29. data/spec/gmail/client/xoauth2_spec.rb +186 -186
  30. data/spec/gmail/client/xoauth_spec.rb +5 -5
  31. data/spec/gmail/client_spec.rb +5 -5
  32. data/spec/gmail/imap_extensions_spec.rb +47 -47
  33. data/spec/gmail/labels_spec.rb +27 -27
  34. data/spec/gmail/mailbox_spec.rb +84 -84
  35. data/spec/gmail/message_spec.rb +181 -181
  36. data/spec/gmail_spec.rb +40 -39
  37. data/spec/recordings/gmail/_new_connects_with_client_and_give_it_context_when_block_given.yml +61 -28
  38. data/spec/recordings/gmail/_new_connects_with_gmail_service_and_return_valid_connection_object.yml +43 -28
  39. data/spec/recordings/gmail/_new_does_not_raise_error_when_couldn_t_connect_with_given_account.yml +21 -13
  40. data/spec/recordings/gmail/_new_raises_error_when_couldn_t_connect_with_given_account.yml +21 -13
  41. data/spec/recordings/gmail_client_plain/instance/delivers_inline_composed_email.yml +61 -42
  42. data/spec/recordings/gmail_client_plain/instance/does_not_log_in_when_given_gmail_account_is_invalid.yml +21 -13
  43. data/spec/recordings/gmail_client_plain/instance/does_not_raise_error_even_though_gmail_account_is_invalid.yml +21 -13
  44. data/spec/recordings/gmail_client_plain/instance/labels/checks_if_there_is_given_label_defined.yml +409 -196
  45. data/spec/recordings/gmail_client_plain/instance/labels/creates_given_label.yml +280 -151
  46. data/spec/recordings/gmail_client_plain/instance/labels/removes_existing_label.yml +271 -146
  47. data/spec/recordings/gmail_client_plain/instance/labels/returns_list_of_all_available_labels.yml +227 -113
  48. data/spec/recordings/gmail_client_plain/instance/properly_logs_in_to_valid_gmail_account.yml +61 -42
  49. data/spec/recordings/gmail_client_plain/instance/properly_logs_out_from_gmail.yml +61 -42
  50. data/spec/recordings/gmail_client_plain/instance/properly_switches_to_given_mailbox.yml +164 -109
  51. data/spec/recordings/gmail_client_plain/instance/properly_switches_to_given_mailbox_using_block_style.yml +164 -109
  52. data/spec/recordings/gmail_client_plain/instance/raises_error_when_given_gmail_account_is_invalid_and_errors_enabled.yml +21 -13
  53. data/spec/recordings/gmail_client_xo_auth2/instance/does_not_log_in_when_given_gmail_account_is_invalid.yml +24 -13
  54. data/spec/recordings/gmail_client_xo_auth2/instance/labels/checks_if_there_is_given_label_defined.yml +39 -27
  55. data/spec/recordings/gmail_client_xo_auth2/instance/labels/creates_given_label.yml +52 -39
  56. data/spec/recordings/gmail_client_xo_auth2/instance/labels/removes_existing_label.yml +52 -39
  57. data/spec/recordings/gmail_client_xo_auth2/instance/labels/returns_list_of_all_available_labels.yml +39 -27
  58. data/spec/recordings/gmail_client_xo_auth2/instance/properly_logs_in_to_valid_gmail_account.yml +26 -15
  59. data/spec/recordings/gmail_client_xo_auth2/instance/properly_logs_out_from_gmail.yml +26 -15
  60. data/spec/recordings/gmail_client_xo_auth2/instance/properly_switches_to_given_mailbox.yml +63 -40
  61. data/spec/recordings/gmail_client_xo_auth2/instance/properly_switches_to_given_mailbox_using_block_style.yml +63 -40
  62. data/spec/recordings/gmail_client_xo_auth2/instance/raises_error_when_given_gmail_account_is_invalid_and_errors_enabled.yml +24 -13
  63. data/spec/recordings/gmail_labels/localize/when_given_the_xl_is_tflag/all/localizes_into_the_appropriate_label.yml +229 -116
  64. 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
  65. data/spec/recordings/gmail_labels/localize/when_given_the_xl_is_tflag/drafts/localizes_into_the_appropriate_label.yml +229 -116
  66. data/spec/recordings/gmail_labels/localize/when_given_the_xl_is_tflag/flagged/localizes_into_the_appropriate_label.yml +229 -116
  67. data/spec/recordings/gmail_labels/localize/when_given_the_xl_is_tflag/important/localizes_into_the_appropriate_label.yml +229 -116
  68. data/spec/recordings/gmail_labels/localize/when_given_the_xl_is_tflag/inbox/localizes_into_the_appropriate_label.yml +61 -42
  69. data/spec/recordings/gmail_labels/localize/when_given_the_xl_is_tflag/junk/localizes_into_the_appropriate_label.yml +229 -116
  70. data/spec/recordings/gmail_labels/localize/when_given_the_xl_is_tflag/sent/localizes_into_the_appropriate_label.yml +229 -116
  71. data/spec/recordings/gmail_labels/localize/when_given_the_xl_is_tflag/trash/localizes_into_the_appropriate_label.yml +229 -116
  72. data/spec/recordings/gmail_mailbox/instance/counts_all_emails.yml +595 -277
  73. data/spec/recordings/gmail_mailbox/instance/finds_messages.yml +1049 -586
  74. data/spec/recordings/gmail_mailbox/instance/waits_once.yml +191 -136
  75. data/spec/recordings/gmail_mailbox/instance/waits_repeatedly.yml +217 -141
  76. data/spec/recordings/gmail_mailbox/instance/waits_with_29-minute_re-issue.yml +188 -136
  77. data/spec/recordings/gmail_mailbox/instance/waits_with_an_unblocked_connection.yml +644 -207
  78. data/spec/recordings/gmail_mailbox/on_initialize/sets_client_and_name.yml +61 -42
  79. data/spec/recordings/gmail_mailbox/on_initialize/works_in_inbox_by_default.yml +61 -42
  80. data/spec/recordings/gmail_message/initialize/sets_prefetch_attrs.yml +1068 -578
  81. data/spec/recordings/gmail_message/initialize/sets_uid_and_mailbox.yml +1068 -580
  82. data/spec/recordings/gmail_message/instance_methods/deletes_itself.yml +1084 -637
  83. data/spec/recordings/gmail_message/instance_methods/marks_itself_read.yml +1137 -682
  84. data/spec/recordings/gmail_message/instance_methods/marks_itself_unread.yml +1153 -686
  85. data/spec/recordings/gmail_message/instance_methods/moves_from_one_tag_to_other.yml +1447 -862
  86. data/spec/recordings/gmail_message/instance_methods/removes_a_given_label.yml +1288 -776
  87. data/spec/recordings/gmail_message/instance_methods/removes_a_given_label_with_old_method.yml +1288 -776
  88. data/spec/recordings/gmail_message/instance_methods/sets_given_label.yml +1165 -690
  89. data/spec/recordings/gmail_message/instance_methods/sets_given_label_with_old_method.yml +1157 -691
  90. data/spec/spec_helper.rb +56 -53
  91. data/spec/support/imap_mock.rb +181 -181
  92. data/spec/support/obfuscation.rb +49 -52
  93. metadata +21 -90
  94. data/spec/recordings/gmail_client_plain/instance/_connection_automatically_logs_in_to_gmail_account_when_it_s_called.yml +0 -42
@@ -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
@@ -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
- )) && login.name == 'OK'
27
- rescue Net::IMAP::NoResponseError => e
28
- if raise_errors
29
- message = "Couldn't login to given Gmail account: #{username}"
30
- message += " (#{e.response.data.text.strip})"
31
- raise(AuthorizationError.new(e.response), message, e.backtrace)
32
- end
33
- end
34
-
35
- def access_token
36
- consumer_options = {
37
- :site => "https://www.google.com",
38
- :request_token_path => "/accounts/OAuthGetRequestToken",
39
- :authorize_path => "/accounts/OAuthAuthorizeToken",
40
- :access_token_path => "/accounts/OAuthGetAccessToken"
41
- }
42
- consumer = OAuth::Consumer.new(consumer_key, consumer_secret, consumer_options)
43
- @access_token ||= OAuth::AccessToken.new(consumer, token, secret)
44
- @access_token
45
- end
46
-
47
- def smtp_settings
48
- [:smtp, {
49
- :address => GMAIL_SMTP_HOST,
50
- :port => GMAIL_SMTP_PORT,
51
- :domain => mail_domain,
52
- :user_name => username,
53
- :password => {
54
- :consumer_key => consumer_key,
55
- :consumer_secret => consumer_secret,
56
- :token => token,
57
- :token_secret => secret,
58
- :access_token => access_token
59
- },
60
- :authentication => :xoauth,
61
- :enable_starttls_auto => true
62
- }]
63
- end
64
- end # XOAuth
65
-
66
- register :xoauth, XOAuth
67
- end # Client
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
@@ -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 == 0
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
- self.each_char do |char|
140
- if special
141
- # If in special mode, add in the replaced special char if there's a match
142
- # Otherwise, add in the backslash and the current character
143
- unesc << (escapes.keys.include?(char) ? escapes[char] : "\\#{char}")
144
- special = false
145
- else
146
- # Toggle special mode if backslash is detected; otherwise just add character
147
- if char == '\\'
148
- special = true
149
- else
150
- unesc << char
151
- end
152
- end
153
- end
154
- unesc
155
- end
156
- end
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