gmail 0.5.0 → 0.6.0

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