exchanger 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. data/LICENSE +20 -0
  2. data/README.md +53 -0
  3. data/lib/exchanger.rb +79 -0
  4. data/lib/exchanger/attributes.rb +61 -0
  5. data/lib/exchanger/boolean.rb +4 -0
  6. data/lib/exchanger/client.rb +19 -0
  7. data/lib/exchanger/config.rb +32 -0
  8. data/lib/exchanger/dirty.rb +239 -0
  9. data/lib/exchanger/element.rb +161 -0
  10. data/lib/exchanger/elements/attendee.rb +10 -0
  11. data/lib/exchanger/elements/base_folder.rb +61 -0
  12. data/lib/exchanger/elements/calendar_folder.rb +11 -0
  13. data/lib/exchanger/elements/calendar_item.rb +59 -0
  14. data/lib/exchanger/elements/complete_name.rb +16 -0
  15. data/lib/exchanger/elements/contact.rb +49 -0
  16. data/lib/exchanger/elements/contacts_folder.rb +17 -0
  17. data/lib/exchanger/elements/distribution_list.rb +8 -0
  18. data/lib/exchanger/elements/email_address.rb +16 -0
  19. data/lib/exchanger/elements/entry.rb +11 -0
  20. data/lib/exchanger/elements/folder.rb +9 -0
  21. data/lib/exchanger/elements/identifier.rb +7 -0
  22. data/lib/exchanger/elements/im_address.rb +20 -0
  23. data/lib/exchanger/elements/item.rb +86 -0
  24. data/lib/exchanger/elements/mailbox.rb +34 -0
  25. data/lib/exchanger/elements/meeting_cancellation.rb +4 -0
  26. data/lib/exchanger/elements/meeting_message.rb +16 -0
  27. data/lib/exchanger/elements/meeting_request.rb +6 -0
  28. data/lib/exchanger/elements/meeting_response.rb +4 -0
  29. data/lib/exchanger/elements/message.rb +24 -0
  30. data/lib/exchanger/elements/phone_number.rb +13 -0
  31. data/lib/exchanger/elements/physical_address.rb +15 -0
  32. data/lib/exchanger/elements/search_folder.rb +5 -0
  33. data/lib/exchanger/elements/single_recipient.rb +6 -0
  34. data/lib/exchanger/elements/task.rb +5 -0
  35. data/lib/exchanger/elements/tasks_folder.rb +4 -0
  36. data/lib/exchanger/field.rb +139 -0
  37. data/lib/exchanger/operation.rb +110 -0
  38. data/lib/exchanger/operations/create_item.rb +64 -0
  39. data/lib/exchanger/operations/expand_dl.rb +42 -0
  40. data/lib/exchanger/operations/find_folder.rb +55 -0
  41. data/lib/exchanger/operations/find_item.rb +54 -0
  42. data/lib/exchanger/operations/get_folder.rb +55 -0
  43. data/lib/exchanger/operations/get_item.rb +44 -0
  44. data/lib/exchanger/operations/resolve_names.rb +43 -0
  45. data/lib/exchanger/operations/update_item.rb +43 -0
  46. data/lib/exchanger/persistence.rb +27 -0
  47. data/spec/calendar_item_spec.rb +26 -0
  48. data/spec/client_spec.rb +11 -0
  49. data/spec/contact_spec.rb +68 -0
  50. data/spec/element_spec.rb +4 -0
  51. data/spec/field_spec.rb +118 -0
  52. data/spec/folder_spec.rb +13 -0
  53. data/spec/mailbox_spec.rb +9 -0
  54. data/spec/spec_helper.rb +6 -0
  55. metadata +208 -0
@@ -0,0 +1,161 @@
1
+ module Exchanger
2
+ # General purpose element.
3
+ class Element
4
+ include Exchanger::Attributes
5
+ include Exchanger::Dirty
6
+ include Exchanger::Persistence
7
+
8
+ class_inheritable_accessor :elements
9
+ class_inheritable_accessor :keys
10
+ class_inheritable_accessor :field_uri_namespace
11
+ class_inheritable_accessor :identifier_name
12
+
13
+ # Exchanger expects elements to be in the same order as defined in types.xsd
14
+ self.elements = ActiveSupport::OrderedHash.new
15
+ self.keys = []
16
+
17
+ # Define a new child element.
18
+ def self.element(name, options = {})
19
+ options[:field_uri_namespace] ||= self.field_uri_namespace
20
+ elements[name] = Field.new(name, options)
21
+ create_element_accessors(name)
22
+ add_dirty_methods(name)
23
+ end
24
+
25
+ # Defina a new element attribute.
26
+ def self.key(name, options = {})
27
+ keys << name
28
+ create_element_accessors(name)
29
+ add_dirty_methods(name)
30
+ end
31
+
32
+ # Create child element accessors.
33
+ def self.create_element_accessors(name)
34
+ define_method(name) { read_attribute(name) }
35
+ define_method("#{name}=") { |value| write_attribute(name, value) }
36
+ define_method("#{name}?") { read_attribute(name).present? }
37
+ end
38
+
39
+ attr_writer :tag_name
40
+
41
+ def tag_name
42
+ @tag_name || self.class.name.demodulize
43
+ end
44
+
45
+ def initialize(attributes = {})
46
+ @attributes = {}
47
+ self.attributes = attributes
48
+ setup_modifications
49
+ end
50
+
51
+ # Performs equality checking on attributes
52
+ def ==(other)
53
+ self.class == other.class &&
54
+ self.attributes == other.attributes
55
+ end
56
+
57
+ def errors
58
+ @errors ||= []
59
+ end
60
+
61
+ def inspect
62
+ keys = self.class.elements.keys | attributes.keys.map(&:to_sym)
63
+ keys -= [:id, :change_key, self.class.identifier_name]
64
+ attrs = keys.map { |name, field| "#{name}: #{attributes[name.to_s].inspect}" }
65
+ str = "#<#{self.class.name}"
66
+ str << " id: #{id.inspect}, change_key: #{change_key.inspect}" if self.class.identifier_name || self.class.keys.include?(:id)
67
+ str << " #{attrs * ', '}" unless attrs.empty?
68
+ str << ">"
69
+ str
70
+ end
71
+
72
+ def self.new_from_xml(xml)
73
+ object = new
74
+ # Keys
75
+ xml.attributes.values.each do |attr|
76
+ name = attr.name.underscore.to_sym
77
+ value = attr.value
78
+ object.write_attribute(name, value)
79
+ end
80
+ # Fields
81
+ xml.children.each do |node|
82
+ name = node.name.underscore.to_sym
83
+ field = elements[name] || Field.new(name)
84
+ value = field.value_from_xml(node)
85
+ object.write_attribute(name, value)
86
+ end
87
+ object.send(:reset_modifications)
88
+ object
89
+ end
90
+
91
+ # Builds XML from elements and attributes
92
+ def to_xml(options = {})
93
+ doc = Nokogiri::XML::Document.new
94
+ root = doc.create_element(tag_name)
95
+ self.class.keys.each do |name|
96
+ value = read_attribute(name)
97
+ next if value.blank?
98
+ root[name.to_s.camelize] = value
99
+ end
100
+ self.class.elements.each do |name, field|
101
+ next if options[:only] && !options[:only].include?(name)
102
+ next if field.options[:readonly]
103
+ value = read_attribute(name)
104
+ next if field.type.is_a?(Array) && value.blank?
105
+ next if new_record? && field.type == Identifier
106
+ next if new_record? && value.blank?
107
+ if name == :text
108
+ root << value.to_s
109
+ else
110
+ root << field.to_xml(value)
111
+ end
112
+ end
113
+ root
114
+ end
115
+
116
+ # Builds XML Item/Folder change for update operations.
117
+ def to_xml_change
118
+ doc = Nokogiri::XML::Document.new
119
+ root = doc.create_element("#{identifier.tag_name.gsub(/Id$/, '')}Change")
120
+ root << identifier.to_xml
121
+ root << to_xml_updates
122
+ root
123
+ end
124
+
125
+ # Builds XML Set/Delete fields for update operations.
126
+ def to_xml_updates
127
+ doc = Nokogiri::XML::Document.new
128
+ root = doc.create_element("Updates")
129
+ self.class.elements.each do |name, field|
130
+ value = read_attribute(name)
131
+ # Create or update existing fields
132
+ if changes.include?(name.to_s)
133
+ field.to_xml_updates(value) do |field_uri_xml, element_xml|
134
+ # Exchanger does not like updating to nil, so delete those.
135
+ if element_xml.text.present?
136
+ set_item_field = doc.create_element("SetItemField")
137
+ set_item_field << field_uri_xml
138
+ set_item_field << doc.create_element(tag_name) << element_xml
139
+ root << set_item_field
140
+ else
141
+ delete_item_field = doc.create_element("DeleteItemField")
142
+ delete_item_field << field_uri_xml
143
+ root << delete_item_field
144
+ end
145
+ end
146
+ end
147
+ # Delete removed phone numbers, etc
148
+ if changes.include?(name.to_s) && value.is_a?(Array)
149
+ old_values, new_values = changes[name.to_s]
150
+ deleted_values = old_values - new_values
151
+ field.to_xml_updates(deleted_values) do |field_uri_xml, _|
152
+ delete_item_field = doc.create_element("DeleteItemField")
153
+ delete_item_field << field_uri_xml
154
+ root << delete_item_field
155
+ end
156
+ end
157
+ end
158
+ root
159
+ end
160
+ end
161
+ end
@@ -0,0 +1,10 @@
1
+ module Exchanger
2
+ # The Attendee element represents attendees and resources for a meeting.
3
+ #
4
+ # http://msdn.microsoft.com/en-us/library/aa580339.aspx
5
+ class Attendee < Element
6
+ element :mailbox, :type => Mailbox
7
+ element :response_type # Unknown Organizer Tentative Accept Decline NoResponseReceived
8
+ element :last_response_time, :type => Time
9
+ end
10
+ end
@@ -0,0 +1,61 @@
1
+ module Exchanger
2
+ # Abstract folder class
3
+ class BaseFolder < Element
4
+ self.identifier_name = :folder_id
5
+ self.field_uri_namespace = :folder
6
+
7
+ element :folder_id, :type => Identifier
8
+ element :parent_folder_id, :type => Identifier
9
+ element :folder_class
10
+ element :display_name
11
+ element :total_count, :type => Integer
12
+ element :child_folder_count, :type => Integer
13
+ element :extended_property, :type => Element
14
+ element :managed_folder_information, :type => Element
15
+ element :effective_rights, :type => Element
16
+
17
+ # Folders that can be referenced by name.
18
+ # http://msdn.microsoft.com/en-us/library/aa580808.aspx
19
+ DISTINGUISHED_NAMES = [
20
+ :inbox, :outbox, :drafts, :deleteditems, :sentitems, :junkemail,
21
+ :calendar, :contacts, :tasks, :notes, :journal,
22
+ :msgfolderroot, :publicfoldersroot, :root,
23
+ :searchfolders, :voicemail
24
+ ]
25
+
26
+ def self.find(id, email_address = nil)
27
+ find_all([id], email_address).first
28
+ end
29
+
30
+ def self.find_all(ids, email_address = nil)
31
+ response = GetFolder.run(:folder_ids => ids, :email_address => email_address)
32
+ response.folders
33
+ end
34
+
35
+ def self.find_all_by_parent_id(parent_id)
36
+ response = FindFolder.run(:parent_folder_id => parent_id)
37
+ response.folders
38
+ end
39
+
40
+ attr_writer :parent_folder
41
+
42
+ def parent_folder
43
+ @parent_folder ||= if parent_folder_id
44
+ Folder.find(parent_folder_id.id)
45
+ end
46
+ end
47
+
48
+ # Sub folders
49
+ def folders
50
+ self.class.find_all_by_parent_id(id).each do |folder|
51
+ folder.parent_folder = self
52
+ end
53
+ end
54
+
55
+ def items
56
+ Item.find_all_by_folder_id(id).each do |item|
57
+ item.parent_folder = self
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,11 @@
1
+ module Exchanger
2
+ class CalendarFolder < BaseFolder
3
+ element :permission_set
4
+
5
+ def new_calendar_item(attributes = {})
6
+ contact = CalendarItem.new(attributes)
7
+ contact.parent_folder_id = folder_id
8
+ contact
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,59 @@
1
+ module Exchanger
2
+ # The CalendarItem element represents an Exchanger calendar item.
3
+ #
4
+ # http://msdn.microsoft.com/en-us/library/aa564765.aspx
5
+ class CalendarItem < Item
6
+ self.field_uri_namespace = :calendar
7
+
8
+ # iCalendar properties
9
+ element :uid, :name => "UID"
10
+ element :recurrence_id, :type => Time
11
+ element :date_time_stamp, :type => Time
12
+ # Single and Occurrence only
13
+ element :start, :type => Time
14
+ element :end, :type => Time
15
+ # Occurrence only
16
+ element :original_start, :type => Time
17
+ # Other properties
18
+ element :is_all_day_event, :type => Boolean
19
+ element :legacy_free_busy_status
20
+ element :location
21
+ element :when
22
+ element :is_meeting, :type => Boolean
23
+ element :is_cancelled, :type => Boolean
24
+ element :is_recurring, :type => Boolean
25
+ element :meeting_request_was_sent, :type => Boolean
26
+ element :is_response_requested, :type => Boolean
27
+ element :calendar_item_type
28
+ element :my_response_type
29
+ element :organizer, :type => SingleRecipient
30
+ element :required_attendees, :type => [Attendee]
31
+ element :optional_attendees, :type => [Attendee]
32
+ element :resources, :type => [Attendee]
33
+ # Conflicting and adjacent meetings
34
+ element :conflicting_meeting_count, :type => Integer
35
+ element :adjacent_meeting_count, :type => Integer
36
+ element :conflicting_meetings, :type => [CalendarItem]
37
+ element :adjacent_meetings, :type => [CalendarItem]
38
+ # Duration
39
+ element :duration
40
+ element :time_zone
41
+ # Appointment
42
+ element :appointment_reply_time, :type => Time
43
+ element :appointment_sequence_number, :type => Integer
44
+ element :appointment_state, :type => Integer
45
+ # Recurrence specific data, only valid if CalendarItemType is RecurringMaster
46
+ element :recurrence #, :type => Recurrence
47
+ element :first_occurrence #, :type => OccurrenceInfo
48
+ element :last_occurrence #, :type => OccurrenceInfo
49
+ element :modified_occurrences #, :type => [OccurrenceInfo]
50
+ element :deleted_occurrences #, :type => [DeletedOccurrenceInfo]
51
+ element :meeting_time_zone #, :type => TimeZone
52
+ # Other properties
53
+ element :conference_type, :type => Integer
54
+ element :allow_new_time_proposal, :type => Boolean
55
+ element :is_online_meeting, :type => Boolean
56
+ element :meeting_workspace_url
57
+ element :net_show_url
58
+ end
59
+ end
@@ -0,0 +1,16 @@
1
+ module Exchanger
2
+ # The CompleteName element represents the complete name of a contact.
3
+ # This element is read only and will not be sent on create/update.
4
+ #
5
+ # http://msdn.microsoft.com/en-us/library/aa494294.aspx
6
+ class CompleteName < Element
7
+ element :title
8
+ element :first_name
9
+ element :middle_name
10
+ element :last_name
11
+ element :suffix
12
+ element :initials
13
+ element :full_name
14
+ element :nickname
15
+ end
16
+ end
@@ -0,0 +1,49 @@
1
+ module Exchanger
2
+ # The Contact element represents a contact item in the Exchanger store.
3
+ # http://msdn.microsoft.com/en-us/library/aa581315.aspx
4
+ class Contact < Item
5
+ # All the uri namespaces are singular (item, task, etc) except contacts.
6
+ self.field_uri_namespace = :contacts
7
+
8
+ element :file_as
9
+ element :file_as_mapping
10
+ element :display_name
11
+ element :given_name
12
+ element :initials
13
+ element :middle_name
14
+ element :nickname
15
+ element :complete_name, :type => CompleteName, :readonly => true
16
+ element :company_name
17
+ element :email_addresses, :type => [EmailAddress]
18
+ element :physical_addresses, :type => [PhysicalAddress]
19
+ element :phone_numbers, :type => [PhoneNumber]
20
+ element :assistant_name
21
+ element :birthday, :type => Time
22
+ element :business_home_page
23
+ element :children, :type => [String]
24
+ element :companies, :type => [String]
25
+ element :contact_source # ActiveDirectory, Store
26
+ element :department
27
+ element :generation
28
+ element :im_addresses, :type => [ImAddress]
29
+ element :job_title
30
+ element :manager
31
+ element :mileage
32
+ element :office_location
33
+ element :postal_address_index
34
+ element :profession
35
+ element :spouse_name
36
+ element :surname
37
+ element :wedding_anniversary, :type => Time
38
+
39
+ # Marked as private in Outlook?
40
+ def private?
41
+ sensitivity == "Private"
42
+ end
43
+
44
+ def self.search(name)
45
+ response = Exchanger::ResolveNames.run(:name => name)
46
+ response.contacts
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,17 @@
1
+ module Exchanger
2
+ class ContactsFolder < BaseFolder
3
+ element :permission_set
4
+
5
+ def contacts
6
+ items.select do |item|
7
+ item.is_a?(Contact)
8
+ end
9
+ end
10
+
11
+ def new_contact(attributes = {})
12
+ contact = Contact.new(attributes)
13
+ contact.parent_folder_id = folder_id
14
+ contact
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,8 @@
1
+ module Exchanger
2
+ # http://msdn.microsoft.com/en-us/library/aa566353.aspx
3
+ class DistributionList < Item
4
+ element :display_name
5
+ element :file_as
6
+ element :contact_source # ActiveDirectory, Store
7
+ end
8
+ end
@@ -0,0 +1,16 @@
1
+ module Exchanger
2
+ # The Entry element represents a single e-mail address for a contact.
3
+ #
4
+ # http://msdn.microsoft.com/en-us/library/aa564757.aspx
5
+ class EmailAddress < Entry
6
+ self.field_uri_namespace = :"contacts:EmailAddress"
7
+
8
+ key :key # EmailAddress1, EmailAddress2 or EmailAddress3
9
+ key :name
10
+ key :routing_type
11
+ key :mailbox_type
12
+
13
+ # A text value that represents the entry is required if this element is used.
14
+ element :text
15
+ end
16
+ end
@@ -0,0 +1,11 @@
1
+ module Exchanger
2
+ # Entry (EmailAddress),
3
+ # Entry (IMAddress),
4
+ # Entry (PhoneNumber),
5
+ # Entry (PhysicalAddress)
6
+ class Entry < Element
7
+ def tag_name
8
+ "Entry"
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,9 @@
1
+ module Exchanger
2
+ # The Folder element defines a folder to create, get, find, synchronize, or update.
3
+ #
4
+ # http://msdn.microsoft.com/en-us/library/aa494294.aspx
5
+ class Folder < BaseFolder
6
+ element :permission_set
7
+ element :unread_count, :type => Integer
8
+ end
9
+ end
@@ -0,0 +1,7 @@
1
+ module Exchanger
2
+ # ItemId, FolderId, etc
3
+ class Identifier < Element
4
+ key :id
5
+ key :change_key
6
+ end
7
+ end