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,20 @@
|
|
1
|
+
module Exchanger
|
2
|
+
# The Entry element represents an instant messaging (IM) address for a contact.
|
3
|
+
#
|
4
|
+
# http://msdn.microsoft.com/en-us/library/aa566142.aspx
|
5
|
+
class ImAddress < Entry
|
6
|
+
self.field_uri_namespace = :"contacts:ImAddress"
|
7
|
+
|
8
|
+
element :title
|
9
|
+
element :first_name
|
10
|
+
element :middle_name
|
11
|
+
element :last_name
|
12
|
+
element :suffix
|
13
|
+
element :initials
|
14
|
+
element :full_name
|
15
|
+
element :nickname
|
16
|
+
|
17
|
+
# A text value that represents the entry is required if this element is used.
|
18
|
+
element :text
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
module Exchanger
|
2
|
+
class Item < Element
|
3
|
+
self.field_uri_namespace = :item
|
4
|
+
self.identifier_name = :item_id
|
5
|
+
|
6
|
+
element :mime_content
|
7
|
+
element :item_id, :type => Identifier
|
8
|
+
element :parent_folder_id, :type => Identifier
|
9
|
+
element :item_class
|
10
|
+
element :subject
|
11
|
+
element :sensitivity
|
12
|
+
element :body
|
13
|
+
element :attachments, :type => [String]
|
14
|
+
element :date_time_received, :type => Time
|
15
|
+
element :size, :type => Integer
|
16
|
+
element :categories, :type => [String]
|
17
|
+
element :importance
|
18
|
+
element :in_reply_to
|
19
|
+
element :is_submitted, :type => Boolean
|
20
|
+
element :is_draft, :type => Boolean
|
21
|
+
element :is_from_me, :type => Boolean
|
22
|
+
element :is_resend, :type => Boolean
|
23
|
+
element :is_unmodified, :type => Boolean
|
24
|
+
element :internet_message_headers, :type => [String]
|
25
|
+
element :date_time_sent, :type => Time
|
26
|
+
element :date_time_created, :type => Time
|
27
|
+
element :response_objects, :type => [String]
|
28
|
+
element :reminder_due_by, :type => Time
|
29
|
+
element :reminder_is_set, :type => Boolean
|
30
|
+
element :reminder_minutes_before_start
|
31
|
+
element :display_cc
|
32
|
+
element :display_to
|
33
|
+
element :has_attachments, :type => Boolean
|
34
|
+
element :extended_property
|
35
|
+
element :culture
|
36
|
+
element :effective_rights
|
37
|
+
element :last_modified_name
|
38
|
+
element :last_modified_time, :type => Time
|
39
|
+
|
40
|
+
def self.find(id)
|
41
|
+
find_all([id]).first
|
42
|
+
end
|
43
|
+
|
44
|
+
def self.find_all(ids)
|
45
|
+
response = GetItem.run(:item_ids => ids)
|
46
|
+
response.items
|
47
|
+
end
|
48
|
+
|
49
|
+
def self.find_all_by_folder_id(folder_id, email_address = nil)
|
50
|
+
response = FindItem.run(:folder_id => folder_id, :email_address => email_address)
|
51
|
+
response.items
|
52
|
+
end
|
53
|
+
|
54
|
+
attr_writer :parent_folder
|
55
|
+
|
56
|
+
def parent_folder
|
57
|
+
@parent_folder ||= if parent_folder_id
|
58
|
+
Folder.find(parent_folder_id.id)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
def create
|
65
|
+
if parent_folder_id
|
66
|
+
response = CreateItem.run(:folder_id => parent_folder_id.id, :items => [self])
|
67
|
+
self.item_id = response.item_ids[0]
|
68
|
+
move_changes
|
69
|
+
true
|
70
|
+
else
|
71
|
+
errors << "Parent folder can't be blank"
|
72
|
+
false
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def update
|
77
|
+
if changed?
|
78
|
+
response = UpdateItem.run(:items => [self])
|
79
|
+
move_changes
|
80
|
+
true
|
81
|
+
else
|
82
|
+
true
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module Exchanger
|
2
|
+
# TODO It looks like this is the same as Address.
|
3
|
+
#
|
4
|
+
# The Mailbox element identifies a mail-enabled Active Directory object.
|
5
|
+
#
|
6
|
+
# The EmailAddress and ItemId elements identify a mailbox or distribution list.
|
7
|
+
# The EmailAddress element identifies a mailbox or distribution list by SMTP address.
|
8
|
+
# The ItemId element identifies a mailbox by an item identifier, which is associated with a particular mailbox.
|
9
|
+
# The ItemId element cannot be used for sending a message to a distribution list or a contact in a public contacts folder.
|
10
|
+
# An error will be thrown if this is used in a CreateItem, UpdateItem, or SendItem operation
|
11
|
+
# when an attempt is made to send a message to a distribution list or contact in a contacts public folder.
|
12
|
+
# Use the ExpandDL operation to get the SMTP address and then send the message by
|
13
|
+
# using the EmailAddress element instead of the ItemId element.
|
14
|
+
#
|
15
|
+
# http://msdn.microsoft.com/en-us/library/aa565036.aspx
|
16
|
+
class Mailbox < Element
|
17
|
+
element :name
|
18
|
+
element :email_address
|
19
|
+
element :routing_type
|
20
|
+
element :mailbox_type # Mailbox, PublicDL, PrivateDL, Contact, PublicFolder
|
21
|
+
element :item_id, :type => Identifier
|
22
|
+
|
23
|
+
def self.search(name)
|
24
|
+
response = Exchanger::ResolveNames.run(:name => name)
|
25
|
+
response.mailboxes
|
26
|
+
end
|
27
|
+
|
28
|
+
def members
|
29
|
+
return [] unless mailbox_type == "PublicDL"
|
30
|
+
response = Exchanger::ExpandDL.run(:mailbox => self)
|
31
|
+
response.mailboxes
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Exchanger
|
2
|
+
class MeetingMessage < Item
|
3
|
+
self.field_uri_namespace = :meeting
|
4
|
+
|
5
|
+
element :associated_calendar_item_id, :type => Identifier
|
6
|
+
element :is_delegated, :type => Boolean
|
7
|
+
element :is_out_of_date, :type => Boolean
|
8
|
+
element :has_been_processed, :type => Boolean
|
9
|
+
# Meeting response related properties
|
10
|
+
element :response_type # Unknown, Organizer, Tentative, Accept, Decline, NoResponseReceived
|
11
|
+
# iCalendar properties
|
12
|
+
element :uid, :name => "UID"
|
13
|
+
element :recurrence_id, :type => Time
|
14
|
+
element :date_time_stamp, :type => Time
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Exchanger
|
2
|
+
# The Message element represents a Microsoft Exchanger e-mail message.
|
3
|
+
#
|
4
|
+
# http://msdn.microsoft.com/en-us/library/aa494306.aspx
|
5
|
+
class Message < Item
|
6
|
+
self.field_uri_namespace = :message
|
7
|
+
element :sender, :type => SingleRecipient
|
8
|
+
element :to_recipients, :type => [Mailbox]
|
9
|
+
element :cc_recipients, :type => [Mailbox]
|
10
|
+
element :bcc_recipients, :type => [Mailbox]
|
11
|
+
element :is_read_receipt_requested, :type => Boolean
|
12
|
+
element :is_delivery_receipt_requested, :type => Boolean
|
13
|
+
element :conversation_index # base64Binary
|
14
|
+
element :conversation_topic
|
15
|
+
element :from, :type => SingleRecipient
|
16
|
+
element :internet_message_id
|
17
|
+
element :is_read, :type => Boolean
|
18
|
+
element :is_response_requested, :type => Boolean
|
19
|
+
element :references
|
20
|
+
element :reply_to, :type => [Mailbox]
|
21
|
+
element :received_by, :type => SingleRecipient
|
22
|
+
element :received_representing, :type => SingleRecipient
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module Exchanger
|
2
|
+
# The Entry element represents a telephone number for a contact.
|
3
|
+
#
|
4
|
+
# http://msdn.microsoft.com/en-us/library/aa565941.aspx
|
5
|
+
class PhoneNumber < Entry
|
6
|
+
self.field_uri_namespace = :"contacts:PhoneNumber"
|
7
|
+
|
8
|
+
key :key # BusinessFax, BusinessPhone, BusinessPhone2, MobilePhone ...
|
9
|
+
|
10
|
+
# A text value that represents the entry is required if this element is used.
|
11
|
+
element :text
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Exchanger
|
2
|
+
# The Entry element describes a single physical address for a contact item.
|
3
|
+
# http://msdn.microsoft.com/en-us/library/aa564323.aspx
|
4
|
+
class PhysicalAddress < Entry
|
5
|
+
self.field_uri_namespace = :"contacts:PhysicalAddress"
|
6
|
+
|
7
|
+
key :key # Business, Home, Other
|
8
|
+
|
9
|
+
element :street
|
10
|
+
element :city
|
11
|
+
element :state
|
12
|
+
element :country_or_region
|
13
|
+
element :postal_code
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,139 @@
|
|
1
|
+
module Exchanger
|
2
|
+
class Field
|
3
|
+
attr_accessor :name, :options
|
4
|
+
|
5
|
+
def initialize(name, options = {})
|
6
|
+
@name = name
|
7
|
+
@options = options
|
8
|
+
end
|
9
|
+
|
10
|
+
def type
|
11
|
+
options[:type] || String
|
12
|
+
end
|
13
|
+
|
14
|
+
def tag_name
|
15
|
+
(options[:name] || name).to_s.camelize
|
16
|
+
end
|
17
|
+
|
18
|
+
# Only for arrays
|
19
|
+
def sub_field
|
20
|
+
if type.is_a?(Array)
|
21
|
+
Field.new(name, {
|
22
|
+
:type => type[0],
|
23
|
+
:field_uri_namespace => field_uri
|
24
|
+
})
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def field_uri_namespace
|
29
|
+
options[:field_uri_namespace].to_s
|
30
|
+
end
|
31
|
+
|
32
|
+
def field_uri
|
33
|
+
if name == :text
|
34
|
+
field_uri_namespace
|
35
|
+
else
|
36
|
+
"#{field_uri_namespace}:#{tag_name}"
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# FieldURI or IndexedFieldURI
|
41
|
+
# <t:FieldURI FieldURI="item:Sensitivity"/>
|
42
|
+
# <t:IndexedFieldURI FieldURI="contacts:EmailAddress" FieldIndex="EmailAddress1"/>
|
43
|
+
#
|
44
|
+
# http://msdn.microsoft.com/en-us/library/aa494315.aspx
|
45
|
+
# http://msdn.microsoft.com/en-us/library/aa581079.aspx
|
46
|
+
def to_xml_field_uri(value)
|
47
|
+
doc = Nokogiri::XML::Document.new
|
48
|
+
if value.is_a?(Entry)
|
49
|
+
doc.create_element("IndexedFieldURI", "FieldURI" => field_uri, "FieldIndex" => value.key)
|
50
|
+
else
|
51
|
+
doc.create_element("FieldURI", "FieldURI" => field_uri)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
# See Element#to_xml_updates.
|
56
|
+
# Yields blocks with FieldURI and Item/Folder/etc changes.
|
57
|
+
def to_xml_updates(value)
|
58
|
+
return if options[:readonly]
|
59
|
+
doc = Nokogiri::XML::Document.new
|
60
|
+
if value.is_a?(Array)
|
61
|
+
value.each do |sub_value|
|
62
|
+
sub_field.to_xml_updates(sub_value) do |field_uri_xml, element_xml|
|
63
|
+
element_wrapper = doc.create_element(sub_field.tag_name)
|
64
|
+
element_wrapper << element_xml
|
65
|
+
yield [
|
66
|
+
field_uri_xml,
|
67
|
+
element_wrapper
|
68
|
+
]
|
69
|
+
end
|
70
|
+
end
|
71
|
+
elsif value.is_a?(Exchanger::Element)
|
72
|
+
value.tag_name = tag_name
|
73
|
+
if value.class.elements.keys.include?(:text)
|
74
|
+
field = value.class.elements[:text]
|
75
|
+
yield [
|
76
|
+
field.to_xml_field_uri(value),
|
77
|
+
field.to_xml(value)
|
78
|
+
]
|
79
|
+
else
|
80
|
+
# PhysicalAddress ?
|
81
|
+
value.class.elements.each do |name, field|
|
82
|
+
yield [
|
83
|
+
field.to_xml_field_uri(value),
|
84
|
+
field.to_xml(value, :only => [name])
|
85
|
+
]
|
86
|
+
end
|
87
|
+
end
|
88
|
+
else # String, Integer, Boolean, ...
|
89
|
+
yield [
|
90
|
+
self.to_xml_field_uri(value),
|
91
|
+
self.to_xml(value)
|
92
|
+
]
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
# Convert Ruby value to XML
|
97
|
+
def to_xml(value, options = {})
|
98
|
+
if value.is_a?(Exchanger::Element)
|
99
|
+
value.tag_name = tag_name
|
100
|
+
value.to_xml(options)
|
101
|
+
else
|
102
|
+
doc = Nokogiri::XML::Document.new
|
103
|
+
root = doc.create_element(tag_name)
|
104
|
+
case value
|
105
|
+
when Array
|
106
|
+
value.each do |sub_value|
|
107
|
+
root << sub_field.to_xml(sub_value, options)
|
108
|
+
end
|
109
|
+
when Boolean
|
110
|
+
root << doc.create_text_node(value == true)
|
111
|
+
when Time
|
112
|
+
root << doc.create_text_node(value.xmlschema)
|
113
|
+
else # String, Integer, etc
|
114
|
+
root << doc.create_text_node(value.to_s)
|
115
|
+
end
|
116
|
+
root
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
# Convert XML to Ruby value
|
121
|
+
def value_from_xml(node)
|
122
|
+
if type.respond_to?(:new_from_xml)
|
123
|
+
type.new_from_xml(node)
|
124
|
+
elsif type.is_a?(Array)
|
125
|
+
node.children.map do |sub_node|
|
126
|
+
sub_field.value_from_xml(sub_node)
|
127
|
+
end
|
128
|
+
elsif type == Boolean
|
129
|
+
node.text == "true"
|
130
|
+
elsif type == Integer
|
131
|
+
node.text.to_i unless node.text.empty?
|
132
|
+
elsif type == Time
|
133
|
+
Time.xmlschema(node.text) unless node.text.empty?
|
134
|
+
else
|
135
|
+
node.text
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
module Exchanger
|
2
|
+
# Abstract class for operations.
|
3
|
+
#
|
4
|
+
# Exchanger Web Services provides many operations that enable you to access
|
5
|
+
# information from the Exchanger store.
|
6
|
+
#
|
7
|
+
# http://msdn.microsoft.com/en-us/library/bb409286.aspx
|
8
|
+
class Operation
|
9
|
+
attr_reader :options, :request, :response
|
10
|
+
|
11
|
+
def initialize(options = {})
|
12
|
+
@options = options
|
13
|
+
end
|
14
|
+
|
15
|
+
# Shortcut for initialize and run.
|
16
|
+
def self.run(options = {})
|
17
|
+
operation = self.new(options)
|
18
|
+
operation.run
|
19
|
+
end
|
20
|
+
|
21
|
+
# Returns response
|
22
|
+
def run
|
23
|
+
@request = self.class::Request.new(options)
|
24
|
+
@response = self.class::Response.new(
|
25
|
+
Exchanger::Client.new.request(@request.body, @request.headers)
|
26
|
+
)
|
27
|
+
end
|
28
|
+
|
29
|
+
class Request
|
30
|
+
attr_accessor :body, :response
|
31
|
+
|
32
|
+
def initialize(options = {})
|
33
|
+
reset
|
34
|
+
options.each do |name, value|
|
35
|
+
send("#{name}=", value) if respond_to?("#{name}=")
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def reset
|
40
|
+
end
|
41
|
+
|
42
|
+
# For a request with class Exchanger::FindItem::Request it will return "FindItem".
|
43
|
+
def action
|
44
|
+
self.class.name.split("::")[1]
|
45
|
+
end
|
46
|
+
|
47
|
+
def headers
|
48
|
+
{ "SOAPAction" => "http://schemas.microsoft.com/exchange/services/2006/messages/#{action}",
|
49
|
+
"Content-Type" => "text/xml; charset=utf-8" }
|
50
|
+
end
|
51
|
+
|
52
|
+
def body
|
53
|
+
to_xml.to_xml
|
54
|
+
end
|
55
|
+
|
56
|
+
def to_xml
|
57
|
+
raise "NotImplemented"
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
class ResponseError < StandardError
|
62
|
+
attr_reader :response_code
|
63
|
+
|
64
|
+
def initialize(message, response_code)
|
65
|
+
super message
|
66
|
+
@response_code = response_code
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
class Response
|
71
|
+
attr_reader :status, :body
|
72
|
+
|
73
|
+
def initialize(options = {})
|
74
|
+
@status = options[:status]
|
75
|
+
@body = options[:body]
|
76
|
+
parse_soap_errors
|
77
|
+
parse_response_message
|
78
|
+
end
|
79
|
+
|
80
|
+
def to_xml
|
81
|
+
@xml ||= Nokogiri::XML(@body)
|
82
|
+
end
|
83
|
+
|
84
|
+
def parse_soap_errors
|
85
|
+
fault_node = to_xml.xpath(".//faultstring")
|
86
|
+
unless fault_node.empty?
|
87
|
+
error_msg = fault_node.text
|
88
|
+
raise ResponseError.new(error_msg, 0)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
# Checks the ResponseMessage for errors.
|
93
|
+
#
|
94
|
+
# http://msdn.microsoft.com/en-us/library/aa494164%28EXCHG.80%29.aspx
|
95
|
+
# Exhange 2007 Valid Response Messages
|
96
|
+
def parse_response_message
|
97
|
+
error_node = to_xml.xpath('//m:ResponseMessages/child::*[@ResponseClass="Error"]', NS)
|
98
|
+
unless error_node.empty?
|
99
|
+
error_msg = error_node.xpath('m:MessageText/text()', NS).to_s
|
100
|
+
response_code = error_node.xpath('m:ResponseCode/text()', NS).to_s
|
101
|
+
raise ResponseError.new(error_msg, response_code)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def code
|
106
|
+
raise NotImplemented
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|