gmailgem/gmail

View on GitHub
lib/gmail/imap_extensions.rb

Summary

Maintainability
C
1 day
Test Coverage
module Gmail
  module ImapExtensions
    LABELS_FLAG_REGEXP = /\\([^\x80-\xff(){ \x00-\x1f\x7f%"\\]+)/n
    # Taken from https://github.com/oxos/gmail-oauth-thread-stats/blob/master/gmail_imap_extensions_compatibility.rb
    def self.patch_net_imap_response_parser(klass = Net::IMAP::ResponseParser)
      # https://github.com/ruby/ruby/blob/4d426fc2e03078d583d5d573d4863415c3e3eb8d/lib/net/imap.rb#L2258
      klass.class_eval do
        def msg_att(n = -1)
          match(Net::IMAP::ResponseParser::T_LPAR)
          attr = {}
          while true
            token = lookahead
            case token.symbol
            when Net::IMAP::ResponseParser::T_RPAR
              shift_token
              break
            when Net::IMAP::ResponseParser::T_SPACE
              shift_token
              next
            end
            case token.value
            when /\A(?:ENVELOPE)\z/ni
              name, val = envelope_data
            when /\A(?:FLAGS)\z/ni
              name, val = flags_data
            when /\A(?:INTERNALDATE)\z/ni
              name, val = internaldate_data
            when /\A(?:RFC822(?:\.HEADER|\.TEXT)?)\z/ni
              name, val = rfc822_text
            when /\A(?:RFC822\.SIZE)\z/ni
              name, val = rfc822_size
            when /\A(?:BODY(?:STRUCTURE)?)\z/ni
              name, val = body_data
            when /\A(?:UID)\z/ni
              name, val = uid_data

            # Gmail extension
            # Cargo-cult code warning: no idea why the regexp works - just copying a pattern
            when /\A(?:X-GM-LABELS)\z/ni
              name, val = x_gm_labels_data
            when /\A(?:X-GM-MSGID)\z/ni
              name, val = uid_data
            when /\A(?:X-GM-THRID)\z/ni
              name, val = uid_data
            # End Gmail extension

            else
              parse_error("unknown attribute `%s' for {%d}", token.value, n)
            end
            attr[name] = val
          end
          return attr
        end

        # Based on Net::IMAP#flags_data, but calling x_gm_labels_list to parse labels
        def x_gm_labels_data
          token = match(self.class::T_ATOM)
          name = token.value.upcase
          match(self.class::T_SPACE)
          return name, x_gm_label_list
        end

        # Based on Net::IMAP#flag_list with a modified Regexp
        # Labels are returned as escape-quoted strings
        # We extract the labels using a regexp which extracts any unescaped strings
        def x_gm_label_list
          if @str.index(/\(([^)]*)\)/ni, @pos)
            resp = extract_labels_response

            # We need to manually update the position of the regexp to prevent trip-ups
            @pos += resp.length - 1

            # `resp` will look something like this:
            # ("\\Inbox" "\\Sent" "one's and two's" "some new label" Awesome Ni&APE-os)
            result = resp.gsub(/^\s*\(|\)+\s*$/, '').scan(/"([^"]*)"|([^\s"]+)/ni).flatten.compact.collect(&:unescape)
            result.map do |x|
              flag = x.scan(LABELS_FLAG_REGEXP)
              if flag.empty?
                x
              else
                flag.first.first.capitalize.untaint.intern
              end
            end
          else
            parse_error("invalid label list")
          end
        end

        # The way Gmail return tokens can cause issues with Net::IMAP's reader,
        # so we need to extract this section manually
        def extract_labels_response
          special, quoted = false, false
          index, paren_count = 0, 0

          # Start parsing response string for the labels section, parentheses inclusive
          labels_header = "X-GM-LABELS ("
          start = @str.index(labels_header) + labels_header.length - 1
          substr = @str[start..-1]
          substr.each_char do |char|
            index += 1
            case char
            when '('
              paren_count += 1 unless quoted
            when ')'
              paren_count -= 1 unless quoted
              break if paren_count.zero?
            when '"'
              quoted = !quoted unless special
            end
            special = (char == '\\' && !special)
          end
          substr[0..index]
        end
      end # class_eval

      # Add String#unescape
      add_unescape
    end # PNIRP

    def self.add_unescape(klass = String)
      klass.class_eval do
        # Add a method to string which unescapes special characters
        # We use a simple state machine to ensure that specials are not
        # themselves escaped
        def unescape
          unesc = ''
          special = false
          escapes = { '\\' => '\\',
                      '"'  => '"',
                      'n' => "\n",
                      't' => "\t",
                      'r' => "\r",
                      'f' => "\f",
                      'v' => "\v",
                      '0' => "\0",
                      'a' => "\a" }

          self.each_char do |char|
            if special
              # If in special mode, add in the replaced special char if there's a match
              # Otherwise, add in the backslash and the current character
              unesc << (escapes.keys.include?(char) ? escapes[char] : "\\#{char}")
              special = false
            elsif char == '\\'
              # Toggle special mode if backslash is detected; otherwise just add character
              special = true
            else
              unesc << char
            end
          end
          unesc
        end
      end
    end
  end
end