diff --git a/lib/model/generic_folder.rb b/lib/model/generic_folder.rb index 569d3f6d..2138129b 100644 --- a/lib/model/generic_folder.rb +++ b/lib/model/generic_folder.rb @@ -69,7 +69,7 @@ def self.find_folders(root = :msgfolderroot, traversal = 'Shallow', shape = 'Def if( folder_type.nil? ) resp = (Viewpoint::EWS::EWS.instance).ews.find_folder( [normalize_id(root)], traversal, {:base_shape => shape} ) else - restr = {:restriction => + restr = {:restriction => {:is_equal_to => [{:field_uRI => {:field_uRI=>'folder:FolderClass'}}, {:field_uRI_or_constant=>{:constant => {:value => folder_type}}}]} } resp = (Viewpoint::EWS::EWS.instance).ews.find_folder( [normalize_id(root)], traversal, {:base_shape => shape}, restr) @@ -109,7 +109,7 @@ def self.get_folder_by_name(name, shape = 'Default') # For now the :field_uRI and :field_uRI_or_constant must be in an Array for Ruby 1.8.7 because Hashes # are not positional at insertion until 1.9 restr = {:restriction => - {:is_equal_to => + {:is_equal_to => [{:field_uRI => {:field_uRI=>'folder:DisplayName'}}, {:field_uRI_or_constant =>{:constant => {:value=>name}}}]}} resp = (Viewpoint::EWS::EWS.instance).ews.find_folder([:msgfolderroot], 'Deep', {:base_shape => shape}, restr) if(resp.status == 'Success') @@ -127,12 +127,12 @@ def self.get_folder_by_name(name, shape = 'Default') def initialize(ews_item) super() # Calls initialize in Model (creates @ews_methods Array) - @ews_item = ews_item - @folder_id = ews_item[:folder_id][:id] - @ews_methods << :folder_id - @ews_methods << :id - @change_key = ews_item[:folder_id][:change_key] - @ews_methods << :change_key + @ews_item = ews_item + @folder_id = ews_item[:folder_id][:id] + @ews_methods << :folder_id + @ews_methods << :id + @change_key = ews_item[:folder_id][:change_key] + @ews_methods << :change_key unless ews_item[:parent_folder_id].nil? @parent_id = ews_item[:parent_folder_id] @ews_methods << :parent_id @@ -142,11 +142,12 @@ def initialize(ews_item) # @todo Handle: # , , , - @sync_state = nil # Base-64 encoded sync data - @synced = false # Whether or not the synchronization process is complete - @subscription_id = nil - @watermark = nil - @shallow = true + @tagspace = (Viewpoint::EWS::EWS.instance).tagspace + @sync_state = nil # Base-64 encoded sync data + @synced = false # Whether or not the synchronization process is complete + @subscription_id = nil + @watermark = nil + @shallow = true end # Subscribe this folder to events. This method initiates an Exchange pull @@ -217,7 +218,26 @@ def get_events def find_items(opts = {}) opts = opts.clone # clone the passed in object so we don't modify it in case it's being used in a loop item_shape = opts.has_key?(:item_shape) ? opts.delete(:item_shape) : {:base_shape => 'Default'} - item_shape[:additional_properties] = {:field_uRI => ['item:ParentFolderId']} + tagspace = opts.delete(:tagspace) || @tagspace + if item_shape.has_key?(:additional_properties) + aprops = item_shape[:additional_properties] + if aprops.has_key?(:field_uRI) + raise EwsBadArgumentError, ":field_uRI val should be an Array instead of #{aprops[:field_uRI].class.name}" unless aprops[:field_uRI].is_a?(Array) + aprops[:field_uRI] << ['item:ParentFolderId'] + else + aprops[:field_uRI] = ['item:ParentFolderId'] + end + if aprops.has_key?(:extended_field_uRI) + raise EwsBadArgumentError, ":extended_field_uRI val should be an Array instead of #{aprops[:extended_field_uRI].class.name}" unless aprops[:extended_field_uRI].is_a?(Array) + aprops[:extended_field_uRI] << [{:distinguished_property_set_id=>"PublicStrings", :property_name=>tagspace, :property_type=>"StringArray"}] + else + aprops[:extended_field_uRI] = [{:distinguished_property_set_id=>"PublicStrings", :property_name=>tagspace, :property_type=>"StringArray"}] + end + else + item_shape[:additional_properties] = {} + item_shape[:additional_properties][:field_uRI] = ['item:ParentFolderId'] + item_shape[:additional_properties][:extended_field_uRI] = [{:distinguished_property_set_id=>"PublicStrings", :property_name=>tagspace, :property_type=>"StringArray"}] + end resp = (Viewpoint::EWS::EWS.instance).ews.find_item([@folder_id], 'Shallow', item_shape, opts) if(resp.status == 'Success') parms = resp.items.shift @@ -232,6 +252,18 @@ def find_items(opts = {}) end end + # Find Items with a specific tag + def find_items_with_tag(tag, opts = {}) + tagspace = opts[:tagspace] || @tagspace + + restrict = { :restriction => { + :is_equal_to => [ {:extended_field_uRI=>{:distinguished_property_set_id=>"PublicStrings", :property_name=>tagspace, :property_type=>"StringArray"}}, + {:field_uRI_or_constant => {:constant => {:value=>tag}}} ] + } } + + find_items(opts.merge(restrict)) + end + # Fetch only items from today (since midnight) def todays_items(opts = {}) #opts = {:query_string => ["Received:today"]} @@ -243,7 +275,7 @@ def todays_items(opts = {}) # @param [DateTime] date_time the time to fetch Items since. def items_since(date_time, opts = {}) restr = {:restriction => - {:is_greater_than_or_equal_to => + {:is_greater_than_or_equal_to => [{:field_uRI => {:field_uRI=>'item:DateTimeReceived'}}, {:field_uRI_or_constant =>{:constant => {:value=>date_time}}}] }} @@ -255,7 +287,7 @@ def items_since(date_time, opts = {}) # @param [DateTime] end_date the time to stop fetching Items from def items_between(start_date, end_date, opts={}) restr = {:restriction => {:and => [ - {:is_greater_than_or_equal_to => + {:is_greater_than_or_equal_to => [{:field_uRI => {:field_uRI=>'item:DateTimeReceived'}}, {:field_uRI_or_constant=>{:constant => {:value =>start_date}}}]}, {:is_less_than_or_equal_to => diff --git a/lib/model/item.rb b/lib/model/item.rb index ce3e9846..2af23194 100644 --- a/lib/model/item.rb +++ b/lib/model/item.rb @@ -57,12 +57,12 @@ def self.add_attachments(parent_id, attachments) attachments.each do |a| b64attach << {:name => {:text =>(File.basename a.path)}, :content => {:text => Base64.encode64(a.read)}} end - resp = conn.ews.create_attachment(parent_id, b64attach) + resp = conn.ews.create_attachment(parent_id, b64attach) (resp.status == 'Success') || (raise EwsError, "Could not create attachments. #{resp.code}: #{resp.message}") {:id => resp.items.first[:attachment_id][:root_item_id], :change_key => resp.items.first[:attachment_id][:root_item_change_key]} end - attr_reader :item_id, :change_key + attr_reader :item_id, :change_key, :tags alias :id :item_id # Initialize an Exchange Web Services item @@ -70,12 +70,14 @@ def self.add_attachments(parent_id, attachments) # @param [Boolean] shallow Whether or not we have retrieved all the elements for this object def initialize(ews_item, shallow = true) super() # Calls initialize in Model (creates @ews_methods Array) - @ews_item = ews_item - @shallow = shallow - @item_id = ews_item[:item_id][:id] + @ews_item = ews_item + @shallow = shallow + @item_id = ews_item[:item_id][:id] @change_key = ews_item[:item_id][:change_key] - @text_only = false - @updates = {} + @text_only = false + @updates = {} + @tags = parse_tags + @tagspace = (Viewpoint::EWS::EWS.instance).tagspace init_methods end @@ -182,7 +184,7 @@ def deepen! return true unless @shallow conn = Viewpoint::EWS::EWS.instance shape = {:base_shape => 'AllProperties', :body_type => (@text_only ? 'Text' : 'Best')} - resp = conn.ews.get_item([@item_id], shape) + resp = conn.ews.get_item([@item_id], shape) resp = resp.items.shift @ews_item = resp[resp.keys.first] @shallow = false @@ -249,7 +251,7 @@ def attachments end # Delete this item - # @param [Boolean] soft Whether or not to do a soft delete. By default EWS will do a + # @param [Boolean] soft Whether or not to do a soft delete. By default EWS will do a # hard delete of this item. See the MSDN docs for more info: # http://msdn.microsoft.com/en-us/library/aa562961.aspx # @return [Boolean] Whether or not the item was deleted @@ -278,7 +280,69 @@ def parent_folder GenericFolder.get_folder @parent_folder_id end + # Use ExtendedProperties to create tags + # @param [String] tag a tag to add to this item + # @param [Hash] opts options to pass to add_tag! + # @option opts [String] :tagspace the namespace to add the tag to. (default: 'viewpoint_tags') + def add_tag!(tag, opts={}) + @tags |= [tag.downcase] + set_tags!(@tags, opts) + end + + # @param [String] tag a tag to delete from this item + # @param [Hash] opts options to pass to remove_tag! + # @option opts [String] :tagspace the namespace to add the tag to. (default: 'viewpoint_tags') + def remove_tag!(tag, opts={}) + @tags -= [tag.downcase] + if(@tags.blank?) + clear_all_tags!(opts) + else + set_tags!(@tags, opts) + end + end + + # @param [Hash] opts options to pass to clear_all_tags! + # @option opts [String] :tagspace the namespace to add the tag to. (default: 'viewpoint_tags') + def clear_all_tags!(opts={}) + tagspace = opts[:tagspace] || @tagspace + vtag = {:preformatted => []} + vtag[:preformatted] << {:delete_item_field => + {:extended_field_uRI=>{:distinguished_property_set_id=>"PublicStrings", :property_name=>tagspace, :property_type=>"StringArray"}} + } + + if(self.update_attribs!(vtag)) + @tags = [] + true + else + false + end + end + # @param [Array] tags viewpoint_tags to set on this item + # @param [Hash] opts options to pass to set_tags! + # @option opts [String] :tagspace the namespace to add the tag to. (default: 'viewpoint_tags') + def set_tags!(tags, opts={}) + tagspace = opts[:tagspace] || @tagspace + i_type = self.class.name.split(/::/).last.ruby_case.to_sym + + tag_vals = [] + tags.each do |t| + tag_vals << {:value => {:text => t}} + end + + vtag = {:preformatted => []} + vtag[:preformatted] << {:set_item_field => [ + {:extended_field_uRI=>{:distinguished_property_set_id=>"PublicStrings", :property_name=>tagspace, :property_type=>"StringArray"}}, + {i_type => [ + {:extended_property => [ + {:extended_field_uRI=>{:distinguished_property_set_id=>"PublicStrings", :property_name=>tagspace, :property_type=>"StringArray"}}, + {:values => tag_vals} + ]} + ]} + ]} + + self.update_attribs!(vtag) + end private @@ -310,6 +374,26 @@ def method_missing(m, *args, &block) end end + def parse_tags(opts={}) + tagspace = opts[:tagspace] || @tagspace + + return [] unless(@ews_item.has_key?(:extended_property) && + @ews_item[:extended_property].has_key?(:extended_field_u_r_i) && + @ews_item[:extended_property][:extended_field_u_r_i].has_key?(:property_name) && + @ews_item[:extended_property][:extended_field_u_r_i][:property_name] == tagspace) + + tags = [] + vals = @ews_item[:extended_property][:values][:value] + if vals.is_a?(Array) + vals.each do |v| + tags << v[:text] + end + else + tags << vals[:text] + end + tags + end + end # Item end # EWS end # Viewpoint diff --git a/lib/soap/handsoap/builders/ews_build_helpers.rb b/lib/soap/handsoap/builders/ews_build_helpers.rb index 810232d5..bf9e1efb 100644 --- a/lib/soap/handsoap/builders/ews_build_helpers.rb +++ b/lib/soap/handsoap/builders/ews_build_helpers.rb @@ -148,6 +148,11 @@ def folder_shape!(node, folder_shape) end end + # This isn't exactly pretty for AdditionalProperties, but it works. The incoming Hash should be formulated like so: + # @example + # :additional_properties => { + # :extended_field_uRI => [{:distinguished_property_set_id=>"PublicStrings", :property_name=>"viewpoint_tags", :property_type=>"StringArray"}] + # } # @todo Finish AdditionalProperties implementation def item_shape!(node, item_shape) node.add("#{NS_EWS_MESSAGES}:ItemShape") do |is| @@ -157,11 +162,22 @@ def item_shape!(node, item_shape) is.add("#{NS_EWS_TYPES}:FilterHtmlContent", item_shape[:filter_html_content]) if item_shape.has_key?(:filter_html_content) is.add("#{NS_EWS_TYPES}:ConvertHtmlCodePageToUTF8", item_shape[:convert_html_code_page_to_utf8]) if item_shape.has_key?(:convert_html_code_page_to_utf8) unless( item_shape[:additional_properties].nil? ) - unless( item_shape[:additional_properties][:field_uRI].nil? ) - is.add("#{NS_EWS_TYPES}:AdditionalProperties") do |addprops| - item_shape[:additional_properties][:field_uRI].each do |uri| - addprops.add("#{NS_EWS_TYPES}:FieldURI") { |furi| furi.set_attr('FieldURI', uri) } - end + is.add("#{NS_EWS_TYPES}:AdditionalProperties") do |addprops| + item_shape[:additional_properties].each_pair do |prop_t,prop_v| + case prop_t + when :field_uRI + prop_v.each do |uri| + addprops.add("#{NS_EWS_TYPES}:FieldURI") { |furi| furi.set_attr('FieldURI', uri) } + end + when :extended_field_uRI + prop_v.each do |uri| + addprops.add("#{NS_EWS_TYPES}:ExtendedFieldURI") do |furi| + uri.each_pair do |attr,val| + furi.set_attr(attr.to_s.camel_case, val) + end + end + end + end #when end end end diff --git a/lib/viewpoint.rb b/lib/viewpoint.rb index 00d55f28..878ff77c 100644 --- a/lib/viewpoint.rb +++ b/lib/viewpoint.rb @@ -77,11 +77,15 @@ module EWS # @attr_reader [Viewpoint::EWS::SOAP::ExchangeWebService] :ews The EWS object used # to make SOAP calls. You typically don't need to use this, but if you want to # play around with the SOAP back-end it's available. + # @attr_accessor [String] :tagspace the name of the tagspace to collect tags in (default: viewpoint_tags) + # Set the tagspace used for creating tags on the Model objects. Under the covers this uses an Exchange + # StringArray and Exchange extended properties. class EWS include Singleton include Viewpoint attr_reader :ews + attr_accessor :tagspace # Set the endpoint for Exchange Web Services. # @param [String] endpoint The URL of the endpoint. This should end in @@ -122,6 +126,7 @@ def self.set_trust_ca(ca_path) end def initialize + @tagspace ||= 'viewpoint_tags' @ews = SOAP::ExchangeWebService.new end