exchanger 0.1.0

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