Class: Viewpoint::EWS::GenericFolder

Inherits:
Object
  • Object
show all
Includes:
Viewpoint, Model
Defined in:
lib/model/generic_folder.rb

Overview

This class is a generic folder that should typically not be instantiated on it’s own. It represents all the commonalities among folders found in the Exchange Data Store of which there are many.

Constant Summary collapse

@@distinguished_folder_ids =
%w{calendar contacts deleteditems drafts inbox journal
notes outbox sentitems tasks msgfolderroot root junkemail searchfolders voicemail
recoverableitemsroot recoverableitemsdeletions recoverableitemsversions
recoverableitemspurges archiveroot archivemsgfolderroot archivedeleteditems
archiverecoverableitemsroot archiverecoverableitemsdeletions
archiverecoverableitemsversions archiverecoverableitemspurges}
@@event_types =
%w{CopiedEvent CreatedEvent DeletedEvent ModifiedEvent MovedEvent NewMailEvent}

Instance Attribute Summary collapse

Attributes included from Model

#ews_methods, #ews_methods_undef

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(ews_item) ⇒ GenericFolder

Returns a new instance of GenericFolder.



130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
# File 'lib/model/generic_folder.rb', line 130

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
  unless ews_item[:parent_folder_id].nil?
    @parent_id = ews_item[:parent_folder_id]
    @ews_methods << :parent_id
  end
  define_str_var :display_name, :folder_class
  define_int_var :child_folder_count, :total_count
  # @todo Handle:
  #   <EffectiveRights/>, <ExtendedProperty/>, <ManagedFolderInformation/>, <PermissionSet/>

  @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

Instance Attribute Details

#change_keyObject

Returns the value of attribute change_key.



126
127
128
# File 'lib/model/generic_folder.rb', line 126

def change_key
  @change_key
end

#folder_idObject Also known as: id

Returns the value of attribute folder_id.



126
127
128
# File 'lib/model/generic_folder.rb', line 126

def folder_id
  @folder_id
end

#parent_idObject

Returns the value of attribute parent_id.



126
127
128
# File 'lib/model/generic_folder.rb', line 126

def parent_id
  @parent_id
end

#subscription_idObject (readonly)

Returns the value of attribute subscription_id.



127
128
129
# File 'lib/model/generic_folder.rb', line 127

def subscription_id
  @subscription_id
end

#sync_stateObject

Returns the value of attribute sync_state.



126
127
128
# File 'lib/model/generic_folder.rb', line 126

def sync_state
  @sync_state
end

#watermarkObject (readonly)

Returns the value of attribute watermark.



127
128
129
# File 'lib/model/generic_folder.rb', line 127

def watermark
  @watermark
end

Class Method Details

.find_folders(root = :msgfolderroot, traversal = 'Shallow', shape = 'Default', folder_type = nil) ⇒ Array

Find subfolders of the passed root folder. If no parameters are passed this method will search from the Root folder.

Parameters:

  • root (String, Symbol) (defaults to: :msgfolderroot)

    An folder id, either a DistinguishedFolderId (must me a Symbol) or a FolderId (String)

  • traversal (String) (defaults to: 'Shallow')

    Shallow/Deep/SoftDeleted

  • shape (String) (defaults to: 'Default')

    the shape to return IdOnly/Default/AllProperties

  • folder_type (optional, String) (defaults to: nil)

    an optional folder type to limit the search to like ‘IPF.Task’

Returns:

  • (Array)

    Returns an Array of Folder or subclasses of Folder



68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
# File 'lib/model/generic_folder.rb', line 68

def self.find_folders(root = :msgfolderroot, traversal = 'Shallow', shape = 'Default', folder_type = nil)
  if( folder_type.nil? )
    resp = (Viewpoint::EWS::EWS.instance).ews.find_folder( [normalize_id(root)], traversal, {:base_shape => shape} )
  else
    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)
  end

  if(resp.status == 'Success')
    folders = []
    resp.items.each do |f|
      f_type = f.keys.first.to_s.camel_case
      folders << (eval "#{f_type}.new(f[f.keys.first])")
    end
    return folders
  else
    raise EwsError, "Could not retrieve folders. #{resp.code}: #{resp.message}"
  end
end

.folder_namesArray<String>

Return a list of folder names

Returns:

  • (Array<String>)

    Return an Array of folder names.



92
93
94
95
96
97
98
99
100
101
102
103
# File 'lib/model/generic_folder.rb', line 92

def self.folder_names
  resp = (Viewpoint::EWS::EWS.instance).ews.find_folder([:msgfolderroot], 'Shallow')
  if(resp.status == 'Success')
    flds = []
    resp.items.each do |f|
      flds << f[f.keys.first][:display_name][:text]
    end
    flds
  else
    raise EwsError, "Could not retrieve folders. #{resp.code}: #{resp.message}"
  end
end

.get_folder(folder_id, act_as = nil, folder_shape = {:base_shape => 'Default'}) ⇒ Object

Get a specific folder by its ID.

Parameters:

  • folder_id (String, Symbol)

    either a DistinguishedFolderID or simply a FolderID

  • act_as (String, nil) (defaults to: nil)

    User to act on behalf as. This user must have been given delegate access to this folder or else this operation will fail.

  • folder_shape (Hash) (defaults to: {:base_shape => 'Default'})

Options Hash (folder_shape):

  • :base_shape (String)

    IdOnly/Default/AllProperties



49
50
51
52
53
54
55
56
57
58
# File 'lib/model/generic_folder.rb', line 49

def self.get_folder(folder_id, act_as = nil, folder_shape = {:base_shape => 'Default'})
  resp = (Viewpoint::EWS::EWS.instance).ews.get_folder( [normalize_id(folder_id)], folder_shape, act_as )
  if(resp.status == 'Success')
    folder = resp.items.first
    f_type = folder.keys.first.to_s.camel_case
    return(eval "#{f_type}.new(folder[folder.keys.first])")
  else
    raise EwsError, "Could not retrieve folder. #{resp.code}: #{resp.message}"
  end
end

.get_folder_by_name(name, shape = 'Default') ⇒ GenericFolder?

Gets a folder by name. This name must match the folder name exactly.

Parameters:

  • name (String)

    The name of the folder to fetch.

  • shape (String) (defaults to: 'Default')

    the shape of the object to return IdOnly/Default/AllProperties

Returns:

  • (GenericFolder, nil)

    will return the folder by the given name of nil if not found.



109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
# File 'lib/model/generic_folder.rb', line 109

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 => 
      [{:field_uRI => {:field_uRI=>'folder:DisplayName'}}, {:field_uRI_or_constant =>{:constant => {:value=>name}}}]}}
  resp = (Viewpoint::EWS::EWS.instance).ews.find_folder([:root], 'Deep', {:base_shape => shape}, restr)
  if(resp.status == 'Success')
    return nil if resp.items.empty?
    f = resp.items.first
    f_type = f.keys.first.to_s.camel_case
    eval "#{f_type}.new(f[f.keys.first])"
  else
    raise EwsError, "Could not retrieve folder. #{resp.code}: #{resp.message}"
  end
end

Instance Method Details

#add_subfolder(name) ⇒ Object

Create a subfolder of this folder

Parameters:

  • name (String)

    The name of the new folder



401
402
403
404
405
406
# File 'lib/model/generic_folder.rb', line 401

def add_subfolder(name)
  resp = (Viewpoint::EWS::EWS.instance).ews.create_folder(@folder_id, name)
  folder = resp.items.first
  ftype = folder.keys.first
  GenericFolder.get_folder(folder[ftype][:folder_id][:id])
end

#clear_sync_state!Object

Clears out the @sync_state so you can freshly synchronize this folder if needed



394
395
396
# File 'lib/model/generic_folder.rb', line 394

def clear_sync_state!
  @sync_state = nil
end

#delete!(recycle_bin = false) ⇒ TrueClass

Deletes this folder from the Exchange Data Store

Parameters:

  • recycle_bin (Boolean) (defaults to: false)

    Send to the recycle bin instead of deleting (default: false)

Returns:

  • (TrueClass)

    This will return true because if an issue occurs it will be thrown in the SOAP Parser



412
413
414
415
416
# File 'lib/model/generic_folder.rb', line 412

def delete!(recycle_bin = false)
  deltype = recycle_bin ? 'MoveToDeletedItems' : 'HardDelete'
  resp = (Viewpoint::EWS::EWS.instance).ews.delete_folder(@folder_id, deltype)
  true
end

#find_items(opts = {}) ⇒ Object

Find Items



219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
# File 'lib/model/generic_folder.rb', line 219

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'}
  unless item_shape.has_key?(:additional_properties) # Don't overwrite if specified by caller
    item_shape[:additional_properties] = {:field_uRI => ['item:ParentFolderId']}
  end
  resp = (Viewpoint::EWS::EWS.instance).ews.find_item([@folder_id], 'Shallow', item_shape, opts)
  if(resp.status == 'Success')
    parms = resp.items.shift
    items = []
    resp.items.each do |i|
      i_type = i.keys.first
      items << (eval "#{i_type.to_s.camel_case}.new(i[i_type])")
    end
    return items
  else
    raise EwsError, "Could not find items. #{resp.code}: #{resp.message}"
  end
end

#get_eventsObject

TODO:

check for subscription expiry

Checks a subscribed folder for events



201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
# File 'lib/model/generic_folder.rb', line 201

def get_events
  begin
    if(subscribed?)
      resp = (Viewpoint::EWS::EWS.instance).ews.get_events(@subscription_id, @watermark)
      parms = resp.items.shift
      @watermark = parms[:watermark]
      # @todo if parms[:more_events] # get more events
      return resp.items
    else
      raise StandardError, "Folder <#{self.display_name}> not subscribed to. Issue a Folder#subscribe before checking events."
    end
  rescue EwsSubscriptionTimeout => e
    @subscription_id, @watermark = nil, nil
    raise e
  end
end

#get_item(item_id, change_key = nil) ⇒ Object

Get Item

Parameters:

  • item_id (String)

    the ID of the item to fetch

  • change_key (String) (defaults to: nil)

    specify an optional change_key if you want to make sure you are fetching a specific version of the object.



307
308
309
310
311
312
313
314
315
316
317
# File 'lib/model/generic_folder.rb', line 307

def get_item(item_id, change_key = nil)
  item_shape = {:base_shape => 'Default', :additional_properties => {:field_uRI => ['item:ParentFolderId']}}
  resp = (Viewpoint::EWS::EWS.instance).ews.get_item([item_id], item_shape)
  if(resp.status == 'Success')
    item = resp.items.shift
    type = item.keys.first
    eval "#{type.to_s.camel_case}.new(item[type])"
  else
    raise EwsError, "Could not retrieve item. #{resp.code}: #{resp.message}"
  end
end

#get_items(item_ids, change_key = nil, options = {}) ⇒ Object

Get Items

Parameters:

  • item_ids (String)

    is an array of Item IDs to fetch

  • change_key (String) (defaults to: nil)

    specify an optional change_key if you want to make sure you are fetching a specific version of the object.

  • options (String) (defaults to: {})

    specify an optional options hash. Supports the key :item_shape that expects a hash value with :base_shape and other optional parameters that specify the desired fields to return.



326
327
328
329
330
331
332
333
334
335
336
337
338
339
# File 'lib/model/generic_folder.rb', line 326

def get_items(item_ids, change_key = nil, options={})
  item_shape = options[:item_shape] ||
    {:base_shape => 'Default', :additional_properties => {:field_uRI => ['item:ParentFolderId']}}
  shallow = item_shape[:base_shape] != 'AllProperties'
  resp = (Viewpoint::EWS::EWS.instance).ews.get_item(item_ids, item_shape)
  if(resp.status == 'Success')
    resp.items.map do |item|
      type = item.keys.first
      eval "#{type.to_s.camel_case}.new(item[type], :shallow => #{shallow})"
    end
  else
    raise EwsError, "Could not retrieve items. #{resp.code}: #{resp.message}"
  end
end

#items_between(start_date, end_date, opts = {}) ⇒ Object

Fetch items between a given time period

Parameters:

  • start_date (DateTime)

    the time to start fetching Items from

  • end_date (DateTime)

    the time to stop fetching Items from



260
261
262
263
264
265
266
267
268
269
270
# File 'lib/model/generic_folder.rb', line 260

def items_between(start_date, end_date, opts={})
  restr = {:restriction =>  {:and => [
    {: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 =>
      [{:field_uRI => {:field_uRI=>'item:DateTimeReceived'}},
      {:field_uRI_or_constant=>{:constant => {:value =>end_date}}}]}
  ]}}
  find_items(opts.merge(restr))
end

#items_since(date_time, opts = {}) ⇒ Object

Fetch items since a give DateTime

Parameters:

  • date_time (DateTime)

    the time to fetch Items since.



248
249
250
251
252
253
254
255
# File 'lib/model/generic_folder.rb', line 248

def items_since(date_time, opts = {})
  restr = {:restriction =>
    {:is_greater_than_or_equal_to => 
      [{:field_uRI => {:field_uRI=>'item:DateTimeReceived'}},
      {:field_uRI_or_constant =>{:constant => {:value=>date_time}}}]
    }}
  find_items(opts.merge(restr))
end

#search_by_subject(match_str, exclude_str = nil) ⇒ Object

Search on the item subject

Parameters:

  • match_str (String)

    A simple string paramater to match against the subject. The search ignores case and does not accept regexes… only strings.

  • exclude_str (String, nil) (defaults to: nil)

    A string to exclude from matches against the subject. This is optional.



276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
# File 'lib/model/generic_folder.rb', line 276

def search_by_subject(match_str, exclude_str = nil)
  match = {:contains =>
    {
      :containment_mode => 'Substring',
      :containment_comparison => 'IgnoreCase',
      :field_uRI => {:field_uRI=>'item:Subject'},
      :constant => {:value =>match_str}
    }
  }
  unless exclude_str.nil?
    excl = {:not =>
      {:contains =>
        {
          :containment_mode => 'Substring',
          :containment_comparison => 'IgnoreCase',
          :field_uRI => {:field_uRI=>'item:Subject'},
          :constant => {:value =>exclude_str}
        }
      }
    }

    match[:and] = [{:contains => match.delete(:contains)}, excl]
  end

  find_items({:restriction => match})
end

#subscribe(event_types = @@event_types) ⇒ Boolean

TODO:

Add custom Exception for EWS

Subscribe this folder to events. This method initiates an Exchange pull type subscription.

Parameters:

  • event_types (Array) (defaults to: @@event_types)

    Which event types to subscribe to. By default we subscribe to all Exchange event types: CopiedEvent, CreatedEvent, DeletedEvent, ModifiedEvent, MovedEvent, NewMailEvent, FreeBusyChangedEvent

Returns:

  • (Boolean)

    Did the subscription happen successfully?



162
163
164
165
166
167
168
169
170
171
172
173
174
# File 'lib/model/generic_folder.rb', line 162

def subscribe(event_types = @@event_types)
  # Refresh the subscription if already subscribed
  unsubscribe if subscribed?

  resp = (Viewpoint::EWS::EWS.instance).ews.subscribe([folder_id],event_types)
  if(resp.status == 'Success')
    @subscription_id = resp.items.first[:subscription_id][:text]
    @watermark = resp.items.first[:watermark][:text]
    return true
  else
    raise StandardError, "Error: #{resp.message}"
  end
end

#subscribed?Boolean

Check if there is a subscription for this folder.

Returns:

  • (Boolean)

    Are we subscribed to this folder?



178
179
180
# File 'lib/model/generic_folder.rb', line 178

def subscribed?
  ( @subscription_id.nil? or @watermark.nil? )? false : true
end

#sync_items!(sync_amount = 256, sync_all = false, opts = {}) ⇒ Hash

Syncronize Items in this folder. If this method is issued multiple times it will continue where the last sync completed.

Parameters:

  • sync_amount (Integer) (defaults to: 256)

    The number of items to synchronize per sync

  • sync_all (Boolean) (defaults to: false)

    Whether to sync all the data by looping through. The default is to just sync the first set. You can manually loop through with multiple calls to #sync_items!

Returns:

  • (Hash)

    Returns a hash with keys for each change type that ocurred. Possible key values are (:create/:udpate/:delete). For create and update changes the values are Arrays of Item or a subclass of Item. For deletes an array of ItemIds are returned wich is a Hash in the form: id”, :change_key=>“change key” See: msdn.microsoft.com/en-us/library/aa565609.aspx



354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
# File 'lib/model/generic_folder.rb', line 354

def sync_items!(sync_amount = 256, sync_all = false, opts = {})
  item_shape = opts.has_key?(:item_shape) ? opts.delete(:item_shape) : {:base_shape => 'Default'}
  resp = (Viewpoint::EWS::EWS.instance).ews.sync_folder_items(@folder_id, @sync_state, sync_amount, item_shape)
  parms = resp.items.shift
  @sync_state = parms[:sync_state]
  @synced = parms[:includes_last_item_in_range]
  items = {}
  resp.items.each do |i|
    key = i.keys.first
    items[key] = [] unless items[key].is_a?(Array)
    if(key == :delete || key == :read_flag_change)
      items[key] << i[key][:item_id]
    else
      i_type = i[key].keys.first
      items[key] << (eval "#{i_type.to_s.camel_case}.new(i[key][i_type])")
    end
  end
  items
end

#sync_items_since!(datetime, opts = {}) ⇒ Array<Item>

This is basically a work-around for Microsoft’s BPOS hosted Exchange, which does not support subscriptions at the time of this writing. This is the best way I could think of to get items from a specific period of time and track changes. !! Before using this method I would suggest trying a GenericFolder#items_since then using a subscription to track changes. This method should be followed by subsequent calls to GenericFolder#sync_items! to fetch additional items. Calling this method again will clear the sync_state and synchronize everything again.

Returns:

  • (Array<Item>)

    returns an array of Items



383
384
385
386
387
388
389
390
391
# File 'lib/model/generic_folder.rb', line 383

def sync_items_since!(datetime, opts={})
  clear_sync_state!

  begin
    items = sync_items!
  end until items.empty?

  items_since(datetime, opts)
end

#todays_items(opts = {}) ⇒ Object

Fetch only items from today (since midnight)



240
241
242
243
244
# File 'lib/model/generic_folder.rb', line 240

def todays_items(opts = {})
  #opts = {:query_string => ["Received:today"]}
  #This is a bit convoluted for pre-1.9.x ruby versions that don't support to_datetime
  items_since(DateTime.parse(Date.today.to_s), opts)
end

#unsubscribeBoolean

TODO:

Add custom Exception for EWS

Unsubscribe this folder from further Exchange events.

Returns:

  • (Boolean)

    Did we unsubscribe successfully?



186
187
188
189
190
191
192
193
194
195
196
# File 'lib/model/generic_folder.rb', line 186

def unsubscribe
  return true if @subscription_id.nil?

  resp = (Viewpoint::EWS::EWS.instance).ews.unsubscribe(@subscription_id)
  if(resp.status == 'Success')
    @subscription_id, @watermark = nil, nil
    return true
  else
    raise StandardError, "Error: #{resp.message}"
  end
end