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.
- data/LICENSE +20 -0
- data/README.md +53 -0
- data/lib/exchanger.rb +79 -0
- data/lib/exchanger/attributes.rb +61 -0
- data/lib/exchanger/boolean.rb +4 -0
- data/lib/exchanger/client.rb +19 -0
- data/lib/exchanger/config.rb +32 -0
- data/lib/exchanger/dirty.rb +239 -0
- data/lib/exchanger/element.rb +161 -0
- data/lib/exchanger/elements/attendee.rb +10 -0
- data/lib/exchanger/elements/base_folder.rb +61 -0
- data/lib/exchanger/elements/calendar_folder.rb +11 -0
- data/lib/exchanger/elements/calendar_item.rb +59 -0
- data/lib/exchanger/elements/complete_name.rb +16 -0
- data/lib/exchanger/elements/contact.rb +49 -0
- data/lib/exchanger/elements/contacts_folder.rb +17 -0
- data/lib/exchanger/elements/distribution_list.rb +8 -0
- data/lib/exchanger/elements/email_address.rb +16 -0
- data/lib/exchanger/elements/entry.rb +11 -0
- data/lib/exchanger/elements/folder.rb +9 -0
- data/lib/exchanger/elements/identifier.rb +7 -0
- data/lib/exchanger/elements/im_address.rb +20 -0
- data/lib/exchanger/elements/item.rb +86 -0
- data/lib/exchanger/elements/mailbox.rb +34 -0
- data/lib/exchanger/elements/meeting_cancellation.rb +4 -0
- data/lib/exchanger/elements/meeting_message.rb +16 -0
- data/lib/exchanger/elements/meeting_request.rb +6 -0
- data/lib/exchanger/elements/meeting_response.rb +4 -0
- data/lib/exchanger/elements/message.rb +24 -0
- data/lib/exchanger/elements/phone_number.rb +13 -0
- data/lib/exchanger/elements/physical_address.rb +15 -0
- data/lib/exchanger/elements/search_folder.rb +5 -0
- data/lib/exchanger/elements/single_recipient.rb +6 -0
- data/lib/exchanger/elements/task.rb +5 -0
- data/lib/exchanger/elements/tasks_folder.rb +4 -0
- data/lib/exchanger/field.rb +139 -0
- data/lib/exchanger/operation.rb +110 -0
- data/lib/exchanger/operations/create_item.rb +64 -0
- data/lib/exchanger/operations/expand_dl.rb +42 -0
- data/lib/exchanger/operations/find_folder.rb +55 -0
- data/lib/exchanger/operations/find_item.rb +54 -0
- data/lib/exchanger/operations/get_folder.rb +55 -0
- data/lib/exchanger/operations/get_item.rb +44 -0
- data/lib/exchanger/operations/resolve_names.rb +43 -0
- data/lib/exchanger/operations/update_item.rb +43 -0
- data/lib/exchanger/persistence.rb +27 -0
- data/spec/calendar_item_spec.rb +26 -0
- data/spec/client_spec.rb +11 -0
- data/spec/contact_spec.rb +68 -0
- data/spec/element_spec.rb +4 -0
- data/spec/field_spec.rb +118 -0
- data/spec/folder_spec.rb +13 -0
- data/spec/mailbox_spec.rb +9 -0
- data/spec/spec_helper.rb +6 -0
- 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,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,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,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
|