LeakyBucket/google_apps

View on GitHub
lib/google_apps/client.rb

Summary

Maintainability
A
0 mins
Test Coverage
require 'cgi'
require 'openssl'

module GoogleApps
  module Client
    attr_reader :domain

    BOUNDARY = "=AaB03xDFHT8xgg"
    PAGE_SIZE = {
        user: 100,
        group: 200
    }
    FEEDS_ROOT = 'https://apps-apis.google.com/a/feeds'
    AUDIT_ROOT = "#{FEEDS_ROOT}/compliance/audit/mail"

    # request_export performs the GoogleApps API call to
    # generate a mailbox export.  It takes the username
    # and an GoogleApps::Atom::Export instance as
    # arguments
    #
    # request_export 'username', document
    #
    # request_export returns the request ID on success or
    # the HTTP response object on failure.
    def request_export(username, document)
      response = make_request(:post, export + "/#{username}", body: document, headers: {'content-type' => 'application/atom+xml'})
      export = create_doc(response.body, :export_response)

      export.find('//apps:property').inject(nil) do |request_id, node|
        node.attributes['name'] == 'requestId' ? node.attributes['value'].to_i : request_id
      end
    end

    # export_status checks the status of a mailbox export
    # request.  It takes the username and the request_id
    # as arguments
    #
    # export_status 'username', 847576
    #
    # export_status will return the body of the HTTP response
    # from Google
    def export_status(username, id)
      response = make_request(:get, URI(export + "/#{username}" + build_id(id)).to_s, headers: {'content-type' => 'application/atom+xml'})
      create_doc(response.body, :export_status)
    end

    def create_doc(response_body, type = nil)
      doc_handler.create_doc(response_body, type)
    end

    # export_ready? checks the export_status response for the
    # presence of an apps:property element with a fileUrl name
    # attribute.
    #
    # export_ready?(export_status('username', 847576))
    #
    # export_ready? returns true if there is a fileUrl present
    # in the response and false if there is no fileUrl present
    # in the response.
    def export_ready?(export_status_doc)
      export_file_urls(export_status_doc).any?
    end

    # fetch_export downloads the mailbox export from Google.
    # It takes a username, request id and a filename as
    # arguments.  If the export consists of more than one file
    # the file name will have numbers appended to indicate the
    # piece of the export.
    #
    # fetch_export 'lholcomb2', 838382, 'lholcomb2'
    #
    # fetch_export reutrns nil in the event that the export is
    # not yet ready.
    def fetch_export(username, req_id, filename)
      export_status_doc = export_status(username, req_id)
      if export_ready?(export_status_doc)
        download_export(export_status_doc, filename).each_with_index { |url, index| url.gsub!(/.*/, "#{filename}#{index}") }
      else
        nil
      end
    end

    # download makes a get request of the provided url
    # and writes the body to the provided filename.
    #
    # download 'url', 'save_file'
    def download(url, filename)
      File.open(filename, "w") do |file|
        file.puts(make_request(:get, url, headers: {'content-type' => 'application/atom+xml'}).body)
      end
    end

    # get_users retrieves as many users as specified from the
    # domain.  If no starting point is given it will grab all the
    # users in the domain.  If a starting point is specified all
    # users from that point on (alphabetically) will be returned.
    #
    # get_users start: 'lholcomb2'
    #
    # get_users returns the final response from google.
    def get_users(options = {})
      limit = options[:limit] || 1000000
      response = make_request(:get, user + "?startUsername=#{options[:start]}", headers: {'content-type' => 'application/atom+xml'})
      pages = fetch_pages(response, limit, :feed)

      return_all(pages)
    end

    # get_groups retrieves all the groups from the domain
    #
    # get_groups
    #
    # get_groups returns the final response from Google.
    def get_groups(options = {})
      limit = options[:limit] || 1000000
      response = make_request(:get, group + "#{options[:extra]}" + "?startGroup=#{options[:start]}", headers: {'content-type' => 'application/atom+xml'})
      pages = fetch_pages(response, limit, :feed)

      return_all(pages)
    end

    # Retrieves the members of the requested group.
    #
    # @param [String] group_id the Group ID in the Google Apps Environment
    #
    # @visibility public
    # @return
    def get_members_of(group_id, options = {})
      options[:extra] = "/#{group_id}/member"
      get_groups options
    end

    # add_member_to adds a member to a group in the domain.
    # It takes a group_id and a GoogleApps::Atom::GroupMember
    # document as arguments.
    #
    # add_member_to 'test', document
    #
    # add_member_to returns the response received from Google.
    def add_member_to(group_id, document)
      response = make_request(:post, group + "/#{group_id}/member", body: document, headers: {'content-type' => 'application/atom+xml'})
      create_doc(response.body)
    end

    # @param [String] group_id The ID for the group being modified
    # @param [GoogleApps::Atom::GroupOwner] document The XML document with the owner address
    #
    # @visibility public
    # @return
    def add_owner_to(group_id, document)
      make_request(:post, group + "/#{group_id}/owner", body: document, headers: {'content-type' => 'application/atom+xml'})
    end

    # delete_member_from removes a member from a group in the
    # domain.  It takes a group_id and member_id as arguments.
    #
    # delete_member_from 'test_group', 'member@cnm.edu'
    #
    # delete_member_from returns the respnse received from Google.
    def delete_member_from(group_id, member_id)
      make_request(:delete, group + "/#{group_id}/member/#{member_id}", headers: {'content-type' => 'application/atom+xml'})
    end

    # @param [String] group_id Email address of group
    # @param [String] owner_id Email address of owner to remove
    #
    # @visibility public
    # @return
    def delete_owner_from(group_id, owner_id)
      make_request(:delete, group + "/#{group_id}/owner/#{owner_id}", headers: {'content-type' => 'application/atom+xml'})
    end

    # get_nicknames_for retrieves all the nicknames associated
    # with the requested user.  It takes the username as a string.
    #
    # get_nickname_for 'lholcomb2'
    #
    # get_nickname_for returns the HTTP response from Google
    def get_nicknames_for(login)
      create_doc(make_request(:get, nickname + "?username=#{login}", headers: {'content-type' => 'application/atom+xml'}).body)
    end

    # migration performs mail migration from a local
    # mail environment to GoogleApps.  migrate takes a
    # username a GoogleApps::Atom::Properties dcoument
    # and the message as plain text (String) as arguments.
    #
    # migrate 'user', properties, message
    #
    # migrate returns the HTTP response received from Google.
    def migrate(username, properties, message)
      headers = { 'content-type' => "multipart/related; boundary=\"#{BOUNDARY}\"" }
      make_request(:post, migration + "/#{username}/mail", body: multi_part(properties.to_s, message), headers: headers)
    end

    def method_missing(name, options = {})
      super unless name.match /([a-z]*)_([a-z]*)/
      options[:headers] ||= {}
      options[:headers].merge!({'content-type' => 'application/atom+xml'})
      case $1
        when "new", "add"
          response = make_request(:post, send($2), options)
          create_doc(response.body, $2)
        when "delete"
          response = make_request(:delete, send($2), options)
          create_doc(response.body, $2)
        when "update"
          response = make_request(:put, send($2), options)
          create_doc(response.body, $2)
        when "get"
          response = make_request(:get, send($2), options)
          create_doc(response.body, $2)
        else
          super
      end
    end

    private

    def user
      "#{FEEDS_ROOT}/#{domain}/user/2.0"
    end

    def pubkey
      "#{FEEDS_ROOT}/compliance/audit/publickey/#{domain}"
    end

    def migration
      "#{FEEDS_ROOT}/migration/2.0/#{domain}"
    end

    def group
      "#{FEEDS_ROOT}/group/2.0/#{domain}"
    end

    def nickname
      "#{FEEDS_ROOT}/#{domain}/nickname/2.0"
    end

    def export
      "#{AUDIT_ROOT}/export/#{domain}"
    end

    def monitor
      "#{AUDIT_ROOT}/monitor/#{domain}"
    end

    def doc_handler
      @doc_handler ||= DocumentHandler.new
    end


    # build_id checks the id string.  If it is formatted
    # as a query string it is returned as is.  If not
    # a / is prepended to the id string.
    def build_id(id)
      id =~ /^\?/ ? id : "/#{id}"
    end

    # export_file_urls searches an export status doc for any apps:property elements with a
    # fileUrl name attribute and returns an array of the values.
    def export_file_urls(export_status_doc)
      export_status_doc.find("//apps:property[contains(@name, 'fileUrl')]").collect do |prop|
        prop.attributes['value']
      end
    end

    def download_export(export, filename)
      export_file_urls(export).each_with_index do |url, index|
        download(url, filename + "#{index}")
      end
    end

    # Takes all the items in each feed and puts them into one array.
    #
    # @visibility private
    # @return Array of Documents
    def return_all(pages)
      pages.inject([]) do |results, feed|
        results | feed.items
      end
    end

    # get_next_page retrieves the next page in the response.
    def get_next_page(next_page_url, type)
      response = make_request(:get, next_page_url, headers: {'content-type' => 'application/atom+xml'})
      GoogleApps::Atom.feed(response.body)
    end

    # fetch_feed retrieves the remaining pages in the request.
    # It takes a page and a limit as arguments.
    def fetch_pages(response, limit, type)
      pages = [GoogleApps::Atom.feed(response.body)]

      while (pages.last.next_page) and (pages.count * PAGE_SIZE[:user] < limit)
        pages << get_next_page(pages.last.next_page, type)
      end
      pages
    end

    def multi_part(properties, message)
      post_body = []
      post_body << "--#{BOUNDARY}\n"
      post_body << "Content-Type: application/atom+xml\n\n"
      post_body << properties.to_s
      post_body << "\n--#{BOUNDARY}\n"
      post_body << "Content-Type: message/rfc822\n\n"
      post_body << message.to_s
      post_body << "\n--#{BOUNDARY}--}"

      post_body.join
    end
  end
end