gmail 0.5.0 → 0.6.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 (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