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.
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