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,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,4 @@
1
+ module Exchanger
2
+ class MeetingCancellation < MeetingMessage
3
+ end
4
+ 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,6 @@
1
+ module Exchanger
2
+ # TODO Almost the same as calendar item
3
+ class MeetingRequest < MeetingMessage
4
+ self.field_uri_namespace = :meeting_request
5
+ end
6
+ end
@@ -0,0 +1,4 @@
1
+ module Exchanger
2
+ class MeetingResponse < MeetingMessage
3
+ end
4
+ 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,5 @@
1
+ module Exchanger
2
+ class SearchFolder < BaseFolder
3
+ element :search_parameters
4
+ end
5
+ end
@@ -0,0 +1,6 @@
1
+ module Exchanger
2
+ # Organizer, Sender, From, etc
3
+ class SingleRecipient < Element
4
+ element :mailbox, :type => Mailbox
5
+ end
6
+ end
@@ -0,0 +1,5 @@
1
+ module Exchanger
2
+ class Task < Item
3
+ self.field_uri_namespace = :task
4
+ end
5
+ end
@@ -0,0 +1,4 @@
1
+ module Exchanger
2
+ class TasksFolder < BaseFolder
3
+ end
4
+ 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