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
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2010 Edgars Beigarts
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,53 @@
1
+ Exchanger
2
+ =========
3
+
4
+ Ruby library for accessing Microsoft Exchange using Exchange Web Services.
5
+ This library tries to make creating and updating items as easy as possible.
6
+ It will keep track of changed properties and will update only them.
7
+
8
+ Supported operations
9
+ ====================
10
+
11
+ * FindItem, GetItem, CreateItem, UpdateItem
12
+ * FindFolder, GetFolder
13
+
14
+
15
+ Installing
16
+ ==========
17
+
18
+ gem install exchanger
19
+
20
+ Configuration
21
+ =============
22
+
23
+ Exchanger.configure do |config|
24
+ config.endpoint = "https://domain.com/EWS/Exchanger.asmx"
25
+ config.username = "username"
26
+ config.password = "password"
27
+ end
28
+
29
+ or configure from YAML
30
+
31
+ Exchanger::Config.instance.from_hash(YAML.load_file("#{Rails.root}/config/exchanger.yml")[Rails.env])
32
+
33
+ Examples
34
+ ========
35
+
36
+ Creating and updating contacts
37
+ ------------------------------
38
+
39
+ folder = Exchanger::Folder.find(:contacts)
40
+ contact = folder.new_contact
41
+ contact.given_name = "Edgars"
42
+ contact.surname = "Beigarts"
43
+ contact.email_addresses = [ Exchanger::EmailAddress.new(:key => "EmailAddress1", :text => "me@domain.com") ]
44
+ contact.phone_numbers = [ Exchanger::PhoneNumber.new(:key => "MobilePhone", :text => "+371 80000000") ]
45
+ contact.save # CreateItem operation
46
+ contact.company_name = "Tieto"
47
+ contact.save # UpdateItem operation
48
+
49
+ Alternatives
50
+ ============
51
+
52
+ * [github.com/jrun/ews-api](http://github.com/jrun/ews-api)
53
+ * [github.com/zenchild/Viewpoint](http://github.com/zenchild/Viewpoint)
@@ -0,0 +1,79 @@
1
+ require "singleton"
2
+ require "delegate"
3
+ require "base64"
4
+
5
+ require "active_support/core_ext"
6
+ require "nokogiri"
7
+ require "httpclient"
8
+ require "net/ntlm"
9
+
10
+ require "exchanger/config"
11
+ require "exchanger/client"
12
+ require "exchanger/boolean"
13
+ require "exchanger/field"
14
+ require "exchanger/dirty"
15
+ require "exchanger/attributes"
16
+ require "exchanger/persistence"
17
+
18
+ # Elements
19
+ require "exchanger/element"
20
+ require "exchanger/elements/identifier"
21
+ require "exchanger/elements/mailbox"
22
+ require "exchanger/elements/single_recipient"
23
+ require "exchanger/elements/attendee"
24
+ require "exchanger/elements/complete_name"
25
+ # Entry elements
26
+ require "exchanger/elements/entry"
27
+ require "exchanger/elements/email_address"
28
+ require "exchanger/elements/phone_number"
29
+ require "exchanger/elements/physical_address"
30
+ require "exchanger/elements/im_address"
31
+ # Folder elements
32
+ require "exchanger/elements/base_folder"
33
+ require "exchanger/elements/folder"
34
+ require "exchanger/elements/calendar_folder"
35
+ require "exchanger/elements/contacts_folder"
36
+ require "exchanger/elements/tasks_folder"
37
+ require "exchanger/elements/search_folder"
38
+ # Item elements
39
+ require "exchanger/elements/item"
40
+ require "exchanger/elements/message"
41
+ require "exchanger/elements/calendar_item"
42
+ require "exchanger/elements/contact"
43
+ require "exchanger/elements/meeting_message"
44
+ require "exchanger/elements/meeting_request"
45
+ require "exchanger/elements/meeting_response"
46
+ require "exchanger/elements/meeting_cancellation"
47
+ require "exchanger/elements/task"
48
+ require "exchanger/elements/distribution_list"
49
+
50
+ # Operations
51
+ require "exchanger/operation"
52
+ require "exchanger/operations/get_folder"
53
+ require "exchanger/operations/find_folder"
54
+ require "exchanger/operations/get_item"
55
+ require "exchanger/operations/find_item"
56
+ require "exchanger/operations/create_item"
57
+ require "exchanger/operations/update_item"
58
+ require "exchanger/operations/resolve_names"
59
+ require "exchanger/operations/expand_dl"
60
+
61
+ module Exchanger
62
+ NS = {
63
+ "xsi" => "http://www.w3.org/2001/XMLSchema-instance",
64
+ "xsd" => "http://www.w3.org/2001/XMLSchema",
65
+ "soap" => "http://schemas.xmlsoap.org/soap/envelope/",
66
+ "m" => "http://schemas.microsoft.com/exchange/services/2006/messages",
67
+ "t" => "http://schemas.microsoft.com/exchange/services/2006/types"
68
+ }
69
+
70
+ class << self
71
+ # The Exchanger +Config+ singleton instance.
72
+ def configure
73
+ config = Config.instance
74
+ block_given? ? yield(config) : config
75
+ end
76
+
77
+ alias :config :configure
78
+ end
79
+ end
@@ -0,0 +1,61 @@
1
+ module Exchanger
2
+ module Attributes
3
+ def attributes=(values = {})
4
+ values.each do |name, value|
5
+ write_attribute(name, value)
6
+ end
7
+ end
8
+
9
+ # Return the attributes hash with indifferent access.
10
+ def attributes
11
+ @attributes.with_indifferent_access
12
+ end
13
+
14
+ # TODO: Add typecasting
15
+ # Read a value from the +Document+ attributes. If the value does not exist
16
+ # it will return nil.
17
+ def read_attribute(name)
18
+ name = name.to_s
19
+ value = @attributes[name]
20
+ accessed(name, value)
21
+ end
22
+
23
+ # TODO: Add typecasting
24
+ def write_attribute(name, value)
25
+ name = name.to_s
26
+ modify(name, @attributes[name], value)
27
+ end
28
+
29
+ def identifier
30
+ if self.class.identifier_name
31
+ @identifier ||= self.send(self.class.identifier_name)
32
+ @identifier.tag_name = self.class.identifier_name.to_s.camelize if @identifier
33
+ @identifier
34
+ end
35
+ end
36
+
37
+ def id
38
+ identifier && identifier.id
39
+ end
40
+
41
+ def change_key
42
+ identifier && identifier.change_key
43
+ end
44
+
45
+ # Override respond_to? so it responds properly for dynamic attributes
46
+ def respond_to?(name)
47
+ (@attributes && @attributes.has_key?(name.to_s)) || super
48
+ end
49
+
50
+ # Used for allowing accessor methods for dynamic attributes
51
+ def method_missing(name, *args)
52
+ attr = name.to_s.sub("=", "")
53
+ return super unless attributes.has_key?(attr)
54
+ if name.to_s.ends_with?("=")
55
+ write_attribute(attr, *args)
56
+ else
57
+ read_attribute(attr)
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,4 @@
1
+ module Exchanger
2
+ class Boolean
3
+ end
4
+ end
@@ -0,0 +1,19 @@
1
+ module Exchanger
2
+ # SOAP Client for Exhange Web Services
3
+ class Client
4
+ delegate :endpoint, :timeout, :username, :password, :debug, :to => "Exchanger.config"
5
+
6
+ def initialize
7
+ @client = HTTPClient.new
8
+ @client.debug_dev = STDERR if debug
9
+ @client.set_auth nil, username, password if username
10
+ end
11
+
12
+ # Does the actual HTTP level interaction.
13
+ def request(post_body, headers)
14
+ # @client.ssl_config.verify_mode = OpenSSL::SSL::VERIFY_NONE
15
+ response = @client.post(endpoint, post_body, headers)
16
+ return { :status => response.status, :body => response.content, :content_type => response.contenttype }
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,32 @@
1
+ module Exchanger
2
+ class Config
3
+ include Singleton
4
+
5
+ attr_accessor :endpoint, :timeout, :username, :password, :debug
6
+
7
+ def initialize
8
+ reset
9
+ end
10
+
11
+ # Reset the configuration options to the defaults.
12
+ def reset
13
+ @endpoint = nil
14
+ @timeout = 5
15
+ @username = nil
16
+ @password = nil
17
+ @debug = false
18
+ end
19
+
20
+ # Configure Exchanger client from a hash. This is usually called after parsing a
21
+ # yaml config file such as exchanger.yml.
22
+ #
23
+ # Example:
24
+ #
25
+ # <tt>Exchanger::Config.instance.from_hash({})</tt>
26
+ def from_hash(settings)
27
+ settings.each do |name, value|
28
+ send("#{name}=", value) if respond_to?("#{name}=")
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,239 @@
1
+ module Exchanger
2
+ module Dirty
3
+ def self.included(base)
4
+ base.extend(ClassMethods)
5
+ end
6
+
7
+ # Gets the changes for a specific field.
8
+ #
9
+ # Example:
10
+ #
11
+ # person = Person.new(:title => "Sir")
12
+ # person.title = "Madam"
13
+ # person.attribute_change("title") # [ "Sir", "Madam" ]
14
+ #
15
+ # Returns:
16
+ #
17
+ # An +Array+ containing the old and new values.
18
+ def attribute_change(name)
19
+ modifications[name]
20
+ end
21
+
22
+ # Determines if a specific field has chaged.
23
+ #
24
+ # Example:
25
+ #
26
+ # person = Person.new(:title => "Sir")
27
+ # person.title = "Madam"
28
+ # person.attribute_changed?("title") # true
29
+ #
30
+ # Returns:
31
+ #
32
+ # +true+ if changed, +false+ if not.
33
+ def attribute_changed?(name)
34
+ modifications.include?(name)
35
+ end
36
+
37
+ # Gets the old value for a specific field.
38
+ #
39
+ # Example:
40
+ #
41
+ # person = Person.new(:title => "Sir")
42
+ # person.title = "Madam"
43
+ # person.attribute_was("title") # "Sir"
44
+ #
45
+ # Returns:
46
+ #
47
+ # The old field value.
48
+ def attribute_was(name)
49
+ change = modifications[name]
50
+ change ? change[0] : nil
51
+ end
52
+
53
+ # Gets the names of all the fields that have changed in the document.
54
+ #
55
+ # Example:
56
+ #
57
+ # person = Person.new(:title => "Sir")
58
+ # person.title = "Madam"
59
+ # person.changed # returns [ "title" ]
60
+ #
61
+ # Returns:
62
+ #
63
+ # An +Array+ of changed field names.
64
+ def changed
65
+ modifications.keys
66
+ end
67
+
68
+ # Alerts to whether the document has been modified or not.
69
+ #
70
+ # Example:
71
+ #
72
+ # person = Person.new(:title => "Sir")
73
+ # person.title = "Madam"
74
+ # person.changed? # returns true
75
+ #
76
+ # Returns:
77
+ #
78
+ # +true+ if changed, +false+ if not.
79
+ def changed?
80
+ !modifications.empty?
81
+ end
82
+
83
+ # Gets all the modifications that have happened to the object as a +Hash+
84
+ # with the keys being the names of the fields, and the values being an
85
+ # +Array+ with the old value and new value.
86
+ #
87
+ # Example:
88
+ #
89
+ # person = Person.new(:title => "Sir")
90
+ # person.title = "Madam"
91
+ # person.changes # returns { "title" => [ "Sir", "Madam" ] }
92
+ #
93
+ # Returns:
94
+ #
95
+ # A +Hash+ of changes.
96
+ def changes
97
+ modifications
98
+ end
99
+
100
+ # Call this method after save, so the changes can be properly switched.
101
+ #
102
+ # Example:
103
+ #
104
+ # <tt>person.move_changes</tt>
105
+ def move_changes
106
+ @previous_modifications = modifications.dup
107
+ @modifications = {}
108
+ end
109
+
110
+ # Gets all the modifications that have happened to the object before the
111
+ # object was saved.
112
+ #
113
+ # Example:
114
+ #
115
+ # person = Person.new(:title => "Sir")
116
+ # person.title = "Madam"
117
+ # person.save!
118
+ # person.previous_changes # returns { "title" => [ "Sir", "Madam" ] }
119
+ #
120
+ # Returns:
121
+ #
122
+ # A +Hash+ of changes before save.
123
+ def previous_changes
124
+ @previous_modifications
125
+ end
126
+
127
+ # Resets a changed field back to its old value.
128
+ #
129
+ # Example:
130
+ #
131
+ # person = Person.new(:title => "Sir")
132
+ # person.title = "Madam"
133
+ # person.reset_attribute!("title")
134
+ # person.title # "Sir"
135
+ #
136
+ # Returns:
137
+ #
138
+ # The old field value.
139
+ def reset_attribute!(name)
140
+ value = attribute_was(name)
141
+ if value
142
+ @attributes[name] = value
143
+ modifications.delete(name)
144
+ end
145
+ end
146
+
147
+ # Sets up the modifications hash. This occurs just after the document is
148
+ # instantiated.
149
+ #
150
+ # Example:
151
+ #
152
+ # <tt>document.setup_notifications</tt>
153
+ def setup_modifications
154
+ @accessed ||= {}
155
+ @modifications ||= {}
156
+ @previous_modifications ||= {}
157
+ end
158
+
159
+ # Reset all modifications for the document. This will wipe all the marked
160
+ # changes, but not reset the values.
161
+ #
162
+ # Example:
163
+ #
164
+ # <tt>document.reset_modifications</tt>
165
+ def reset_modifications
166
+ @accessed = {}
167
+ @modifications = {}
168
+ end
169
+
170
+ protected
171
+
172
+ # Audit the original value for a field that can be modified in place.
173
+ #
174
+ # Example:
175
+ #
176
+ # <tt>person.accessed("aliases", [ "007" ])</tt>
177
+ def accessed(name, value)
178
+ @accessed ||= {}
179
+ @accessed[name] = value.dup if (value.is_a?(Array) || value.is_a?(Hash)) && !@accessed.has_key?(name)
180
+ value
181
+ end
182
+
183
+ # Get all normal modifications plus in place potential changes.
184
+ # Also checks changes in array attributes with +Element+ objects.
185
+ #
186
+ # Example:
187
+ #
188
+ # <tt>person.modifications</tt>
189
+ #
190
+ # Returns:
191
+ #
192
+ # All changes to the document.
193
+ def modifications
194
+ @accessed.each_pair do |field, value|
195
+ current = @attributes[field]
196
+ if current != value || (current.is_a?(Array) &&
197
+ current.any? { |v| v.respond_to?(:changed?) && v.changed? })
198
+ @modifications[field] = [ value, current ]
199
+ end
200
+ end
201
+ @accessed.clear
202
+ @modifications
203
+ end
204
+
205
+ # Audit the change of a field's value.
206
+ #
207
+ # Example:
208
+ #
209
+ # <tt>person.modify("name", "Jack", "John")</tt>
210
+ def modify(name, old_value, new_value)
211
+ @attributes[name] = new_value
212
+ if @modifications && (old_value != new_value)
213
+ original = @modifications[name].first if @modifications[name]
214
+ @modifications[name] = [ (original || old_value), new_value ]
215
+ end
216
+ end
217
+
218
+ module ClassMethods #:nodoc:
219
+ # Add the dynamic dirty methods. These are custom methods defined on a
220
+ # field by field basis that wrap the dirty attribute methods.
221
+ #
222
+ # Example:
223
+ #
224
+ # person = Person.new(:title => "Sir")
225
+ # person.title = "Madam"
226
+ # person.title_change # [ "Sir", "Madam" ]
227
+ # person.title_changed? # true
228
+ # person.title_was # "Sir"
229
+ # person.reset_title!
230
+ def add_dirty_methods(name)
231
+ name = name.to_s
232
+ define_method("#{name}_change") { attribute_change(name) } unless instance_methods.include?("#{name}_change")
233
+ define_method("#{name}_changed?") { attribute_changed?(name) } unless instance_methods.include?("#{name}_changed?")
234
+ define_method("#{name}_was") { attribute_was(name) } unless instance_methods.include?("#{name}_was")
235
+ define_method("reset_#{name}!") { reset_attribute!(name) } unless instance_methods.include?("reset_#{name}!")
236
+ end
237
+ end
238
+ end
239
+ end