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,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
|