exchanger 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|