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 publicfoldersroot}
@@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.



160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
# File 'lib/model/generic_folder.rb', line 160

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.



156
157
158
# File 'lib/model/generic_folder.rb', line 156

def change_key
  @change_key
end

#folder_idObject Also known as: id

Returns the value of attribute folder_id.



156
157
158
# File 'lib/model/generic_folder.rb', line 156

def folder_id
  @folder_id
end

#parent_idObject

Returns the value of attribute parent_id.



156
157
158
# File 'lib/model/generic_folder.rb', line 156

def parent_id
  @parent_id
end

#subscription_idObject (readonly)

Returns the value of attribute subscription_id.



157
158
159
# File 'lib/model/generic_folder.rb', line 157

def subscription_id
  @subscription_id
end

#sync_stateObject

Returns the value of attribute sync_state.



156
157
158
# File 'lib/model/generic_folder.rb', line 156

def sync_state
  @sync_state
end

#watermarkObject (readonly)

Returns the value of attribute watermark.



157
158
159
# File 'lib/model/generic_folder.rb', line 157

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). This is where to start the search from. Usually :root,:msgfolderroot,:publicfoldersroot

  • 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

Raises:

  • (EwsError)

    raised when the backend SOAP method returns an error.



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_names(root = :msgfolderroot) ⇒ Array<String>

Return a list of folder names

Parameters:

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

    An folder id, either a DistinguishedFolderId (must me a Symbol) or a FolderId (String). This is where to start the search from. Usually :root,:msgfolderroot,:publicfoldersroot

Returns:

  • (Array<String>)

    Return an Array of folder names.

Raises:

  • (EwsError)

    raised when the backend SOAP method returns an error.



95
96
97
98
99
100
101
102
103
104
105
106
# File 'lib/model/generic_folder.rb', line 95

def self.folder_names(root = :msgfolderroot)
  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

Raises:

  • (EwsError)

    raised when the backend SOAP method returns an error.



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

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, root = :msgfolderroot, shape = 'Default', opts = {}) ⇒ GenericFolder

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

Parameters:

  • name (String)

    The name of the folder to fetch.

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

    An folder id, either a DistinguishedFolderId (must me a Symbol) or a FolderId (String). This is where to start the search from. Usually :root,:msgfolderroot,:publicfoldersroot

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

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

Returns:

Raises:

  • (EwsFolderNotFound)

    raised when a folder requested is not found

  • (EwsError)

    raised when the backend SOAP method returns an error.



116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
# File 'lib/model/generic_folder.rb', line 116

def self.get_folder_by_name(name, root = :msgfolderroot, shape = 'Default', opts = {})
  opts[:traversal] = 'Deep' unless opts.has_key?(:traversal)
  # 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], opts[:traversal], {:base_shape => shape}, restr)
  if(resp.status == 'Success')
    raise EwsFolderNotFound, "The folder requested is invalid or unavailable" 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

.get_folder_by_path(path, root = :msgfolderroot, shape = 'Default') ⇒ GenericFolder

Gets a folder by the given path. The default search path is :msgfolderroot so if you want to specify a path at a different root change that parameter.

Parameters:

  • path (String)

    the fully qualified path to a folder at the given root @example “/myfolders/folder a/personal calendar”

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

    An folder id, either a DistinguishedFolderId (must me a Symbol) or a FolderId (String). This is where to start the search from. Usually :root,:msgfolderroot,:publicfoldersroot

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

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

Returns:

Raises:

  • (EwsFolderNotFound)

    raised when a folder requested is not found

  • (EwsError)

    raised when the backend SOAP method returns an error.



144
145
146
147
148
149
150
151
152
153
154
# File 'lib/model/generic_folder.rb', line 144

def self.get_folder_by_path(path, root = :msgfolderroot, shape = 'Default')
  parts = path.split(/\//)
  parts = parts.slice(1..(parts.length)) if parts.first.empty?
  retfld = nil
  parts.each do |p|
    fld = self.get_folder_by_name(p, root, shape, {:traversal => 'Shallow'})
    root = fld.id
    retfld = fld if(fld.display_name.downcase == p.downcase)
  end
  retfld
end

Instance Method Details

#add_subfolder(name) ⇒ Object

Create a subfolder of this folder

Parameters:

  • name (String)

    The name of the new folder



431
432
433
434
435
436
# File 'lib/model/generic_folder.rb', line 431

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



424
425
426
# File 'lib/model/generic_folder.rb', line 424

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



442
443
444
445
446
# File 'lib/model/generic_folder.rb', line 442

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



249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
# File 'lib/model/generic_folder.rb', line 249

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



231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
# File 'lib/model/generic_folder.rb', line 231

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.



337
338
339
340
341
342
343
344
345
346
347
# File 'lib/model/generic_folder.rb', line 337

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.



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

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



290
291
292
293
294
295
296
297
298
299
300
# File 'lib/model/generic_folder.rb', line 290

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.



278
279
280
281
282
283
284
285
# File 'lib/model/generic_folder.rb', line 278

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.



306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
# File 'lib/model/generic_folder.rb', line 306

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?



192
193
194
195
196
197
198
199
200
201
202
203
204
# File 'lib/model/generic_folder.rb', line 192

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?



208
209
210
# File 'lib/model/generic_folder.rb', line 208

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



384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
# File 'lib/model/generic_folder.rb', line 384

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



413
414
415
416
417
418
419
420
421
# File 'lib/model/generic_folder.rb', line 413

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)



270
271
272
273
274
# File 'lib/model/generic_folder.rb', line 270

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?



216
217
218
219
220
221
222
223
224
225
226
# File 'lib/model/generic_folder.rb', line 216

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