zimbra 0.0.4 → 0.0.5

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 (53) hide show
  1. data/.gitignore +4 -0
  2. data/.irbrc +6 -0
  3. data/Gemfile +6 -0
  4. data/README +3 -2
  5. data/Rakefile +1 -0
  6. data/lib/zimbra.rb +31 -6
  7. data/lib/zimbra/appointment.rb +274 -0
  8. data/lib/zimbra/appointment/alarm.rb +101 -0
  9. data/lib/zimbra/appointment/attendee.rb +97 -0
  10. data/lib/zimbra/appointment/invite.rb +360 -0
  11. data/lib/zimbra/appointment/recur_exception.rb +83 -0
  12. data/lib/zimbra/appointment/recur_rule.rb +184 -0
  13. data/lib/zimbra/appointment/reply.rb +91 -0
  14. data/lib/zimbra/calendar.rb +27 -0
  15. data/lib/zimbra/delegate_auth_token.rb +49 -0
  16. data/lib/zimbra/ext/hash.rb +72 -0
  17. data/lib/zimbra/extra/date_helpers.rb +111 -0
  18. data/lib/zimbra/folder.rb +100 -0
  19. data/lib/zimbra/handsoap_account_service.rb +44 -0
  20. data/lib/zimbra/handsoap_service.rb +1 -1
  21. data/lib/zimbra/version.rb +3 -0
  22. data/spec/fixtures/xml_api_requests/appointments/create.xml +84 -0
  23. data/spec/fixtures/xml_api_responses/alarms/15_minutes_before.xml +26 -0
  24. data/spec/fixtures/xml_api_responses/alarms/using_all_intervals.xml +26 -0
  25. data/spec/fixtures/xml_api_responses/appointments/appointment_response_1.xml +40 -0
  26. data/spec/fixtures/xml_api_responses/attendees/one_attendee_and_one_reply.xml +27 -0
  27. data/spec/fixtures/xml_api_responses/attendees/three_attendees_response_1.xml +30 -0
  28. data/spec/fixtures/xml_api_responses/multiple_invites/recurring_with_exceptions.xml +109 -0
  29. data/spec/fixtures/xml_api_responses/recur_rules/day_27_of_every_2_months.xml +27 -0
  30. data/spec/fixtures/xml_api_responses/recur_rules/every_2_days.xml +26 -0
  31. data/spec/fixtures/xml_api_responses/recur_rules/every_3_weeks_on_tuesday_and_friday.xml +30 -0
  32. data/spec/fixtures/xml_api_responses/recur_rules/every_day_50_instances.xml +27 -0
  33. data/spec/fixtures/xml_api_responses/recur_rules/every_monday_wednesday_friday.xml +31 -0
  34. data/spec/fixtures/xml_api_responses/recur_rules/every_tuesday.xml +29 -0
  35. data/spec/fixtures/xml_api_responses/recur_rules/every_weekday_with_end_date.xml +34 -0
  36. data/spec/fixtures/xml_api_responses/recur_rules/every_year_on_february_2.xml +28 -0
  37. data/spec/fixtures/xml_api_responses/recur_rules/first_day_of_every_month.xml +36 -0
  38. data/spec/fixtures/xml_api_responses/recur_rules/first_monday_of_every_february.xml +31 -0
  39. data/spec/fixtures/xml_api_responses/recur_rules/first_weekend_day_of_every_month.xml +31 -0
  40. data/spec/fixtures/xml_api_responses/recur_rules/last_day_of_every_month.xml +36 -0
  41. data/spec/fixtures/xml_api_responses/recur_rules/second_day_of_every_2_months.xml +36 -0
  42. data/spec/fixtures/xml_api_responses/recur_rules/second_wednesday_of_every_month.xml +29 -0
  43. data/spec/fixtures/xml_api_responses/recur_rules/weekly_with_an_exception.xml +44 -0
  44. data/spec/spec_helper.rb +32 -0
  45. data/spec/zimbra/acl_spec.rb +11 -0
  46. data/spec/zimbra/appointment/alarm_spec.rb +33 -0
  47. data/spec/zimbra/appointment/invite_spec.rb +62 -0
  48. data/spec/zimbra/appointment/recur_rule_spec.rb +307 -0
  49. data/spec/zimbra/appointment_spec.rb +175 -0
  50. data/spec/zimbra/common_elements_spec.rb +33 -0
  51. data/spec/zimbra/distribution_list_spec.rb +54 -0
  52. data/zimbra.gemspec +28 -0
  53. metadata +165 -68
@@ -0,0 +1,4 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
data/.irbrc ADDED
@@ -0,0 +1,6 @@
1
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), 'lib'))
2
+ require 'zimbra.rb'
3
+
4
+ puts "* Loaded Zimbra Gem"
5
+
6
+
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source "http://rubygems.org"
2
+
3
+ gemspec
4
+
5
+ group :development do
6
+ end
data/README CHANGED
@@ -1,7 +1,8 @@
1
1
  EXAMPLE USAGE
2
2
  =============
3
3
 
4
- Zimbra.url = 'http://example.com:7071/soap/service'
4
+ Zimbra.admin_api_url = 'https://mail.server.test:7071/service/admin/soap'
5
+ Zimbra.account_api_url = 'https://mail.server.test/service/soap'
5
6
  Zimbra.login('admin@example.com','secret')
6
7
 
7
8
  d = Zimbra::Domain.create('luser.com')
@@ -10,4 +11,4 @@ dl = Zimbra::DistributionList.create('info@luser.com')
10
11
  dl.admin_group = true
11
12
  dl.save
12
13
 
13
-
14
+ Zimbra.account_login('user@example.com')
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -1,5 +1,6 @@
1
1
  $:.unshift(File.join(File.dirname(__FILE__)))
2
2
  require 'zimbra/handsoap_service'
3
+ require 'zimbra/handsoap_account_service'
3
4
  require 'zimbra/auth'
4
5
  require 'zimbra/cos'
5
6
  require 'zimbra/domain'
@@ -7,20 +8,34 @@ require 'zimbra/distribution_list'
7
8
  require 'zimbra/account'
8
9
  require 'zimbra/acl'
9
10
  require 'zimbra/common_elements'
11
+ require 'zimbra/delegate_auth_token'
12
+ require 'zimbra/folder'
13
+ require 'zimbra/calendar'
14
+ require 'zimbra/appointment'
15
+ require 'zimbra/ext/hash'
16
+ require 'zimbra/extra/date_helpers'
10
17
 
11
18
  # Manages a Zimbra SOAP session. Offers ability to set the endpoint URL, log in, and enable debugging.
12
19
  module Zimbra
13
20
  class << self
14
21
 
15
22
  # The URL that will be used to contact the Zimbra SOAP service
16
- def url
17
- @@url
23
+ def admin_api_url
24
+ @@admin_api_url
18
25
  end
19
26
  # Sets the URL of the Zimbra SOAP service
20
- def url=(url)
21
- @@url = url
27
+ def admin_api_url=(url)
28
+ @@admin_api_url = url
22
29
  end
23
-
30
+
31
+ def account_api_url
32
+ @@account_api_url
33
+ end
34
+
35
+ def account_api_url=(url)
36
+ @@account_api_url = url
37
+ end
38
+
24
39
  # Turn debugging on/off. Outputs full SOAP conversations to stdout.
25
40
  # Zimbra.debug = true
26
41
  # Zimbra.debug = false
@@ -38,6 +53,10 @@ module Zimbra
38
53
  def auth_token
39
54
  @@auth_token
40
55
  end
56
+
57
+ def account_auth_token
58
+ @@account_auth_token
59
+ end
41
60
 
42
61
  # Log into the zimbra SOAP service. This is required before any other action is performed
43
62
  # If a login has already been performed, another login will not be attempted
@@ -48,8 +67,14 @@ module Zimbra
48
67
 
49
68
  # re-log into the zimbra SOAP service
50
69
  def reset_login(username, password)
51
- puts "Logging into zimbra as #{username}"
52
70
  @@auth_token = Auth.login(username, password)
53
71
  end
72
+
73
+ def account_login(username)
74
+ delegate_auth_token = DelegateAuthToken.for_account_name(username)
75
+ return false unless delegate_auth_token
76
+ @@account_auth_token = delegate_auth_token.token
77
+ true
78
+ end
54
79
  end
55
80
  end
@@ -0,0 +1,274 @@
1
+ # zmsoap -z -m mail03@greenviewdata.com SearchRequest @types="appointment" @query="inid:10"
2
+ # http://files.zimbra.com/docs/soap_api/8.0.4/soap-docs-804/api-reference/zimbraMail/Search.html
3
+ # GetRecurRequest
4
+
5
+ module Zimbra
6
+ class Appointment
7
+ autoload :RecurRule, 'zimbra/appointment/recur_rule'
8
+ autoload :Alarm, 'zimbra/appointment/alarm'
9
+ autoload :Attendee, 'zimbra/appointment/attendee'
10
+ autoload :Reply, 'zimbra/appointment/reply'
11
+ autoload :Invite, 'zimbra/appointment/invite'
12
+ autoload :RecurException, 'zimbra/appointment/recur_exception'
13
+
14
+ class << self
15
+ def find_all_by_calendar_id(calendar_id)
16
+ AppointmentService.find_all_by_calendar_id(calendar_id).collect { |attrs| new_from_zimbra_attributes(attrs.merge(:loaded_from_search => true)) }
17
+ end
18
+
19
+ def find_all_by_calendar_id_since(calendar_id, since_date)
20
+ AppointmentService.find_all_by_calendar_id_since(calendar_id, since_date).collect { |attrs| new_from_zimbra_attributes(attrs.merge(:loaded_from_search => true)) }
21
+ end
22
+
23
+ def find(appointment_id)
24
+ attrs = AppointmentService.find(appointment_id)
25
+ return nil unless attrs
26
+ new_from_zimbra_attributes(attrs)
27
+ end
28
+
29
+ def new_from_zimbra_attributes(zimbra_attributes)
30
+ new(parse_zimbra_attributes(zimbra_attributes))
31
+ end
32
+
33
+ def parse_zimbra_attributes(zimbra_attributes)
34
+ zimbra_attributes = Zimbra::Hash.symbolize_keys(zimbra_attributes.dup, true)
35
+
36
+ return {} unless zimbra_attributes.has_key?(:appt) && zimbra_attributes[:appt].has_key?(:attributes)
37
+
38
+ {
39
+ :id => zimbra_attributes[:appt][:attributes][:id],
40
+ :uid => zimbra_attributes[:appt][:attributes][:uid],
41
+ :revision => zimbra_attributes[:appt][:attributes][:rev],
42
+ :calendar_id => zimbra_attributes[:appt][:attributes][:l],
43
+ :size => zimbra_attributes[:appt][:attributes][:s],
44
+ :replies => zimbra_attributes[:appt][:replies],
45
+ :invites_zimbra_attributes => zimbra_attributes[:appt][:inv],
46
+ :date => zimbra_attributes[:appt][:attributes][:d],
47
+ :loaded_from_search => zimbra_attributes[:loaded_from_search]
48
+ }
49
+ end
50
+ end
51
+
52
+ ATTRS = [
53
+ :id, :uid, :date, :revision, :size, :calendar_id,
54
+ :replies, :invites, :invites_zimbra_attributes, :invites_attributes
55
+ ] unless const_defined?(:ATTRS)
56
+
57
+ attr_accessor *ATTRS
58
+ attr_reader :loaded_from_search
59
+
60
+ def initialize(args = {})
61
+ self.attributes = args
62
+ @loaded_from_search = args[:loaded_from_search] || false
63
+ end
64
+
65
+ def attributes=(args = {})
66
+ ATTRS.each do |attr_name|
67
+ self.send(:"#{attr_name}=", args[attr_name]) if args.has_key?(attr_name)
68
+ end
69
+ end
70
+
71
+ def reload
72
+ raw_attributes = AppointmentService.find(id)
73
+ self.attributes = Zimbra::Appointment.parse_zimbra_attributes(raw_attributes)
74
+ @loaded_from_search = false
75
+ end
76
+
77
+ def replies
78
+ reload if loaded_from_search
79
+ @replies
80
+ end
81
+
82
+ def replies=(replies_attributes)
83
+ return @replies = [] unless replies_attributes
84
+
85
+ replies_attributes = replies_attributes[:reply].is_a?(Array) ? replies_attributes[:reply] : [ replies_attributes[:reply] ]
86
+ @replies = replies_attributes.collect { |attrs| Zimbra::Appointment::Reply.new_from_zimbra_attributes(attrs[:attributes]) }
87
+ end
88
+
89
+ def invites
90
+ reload if loaded_from_search
91
+ @invites
92
+ end
93
+
94
+ def invites_attributes=(attributes)
95
+ return @invites = nil unless attributes
96
+
97
+ attributes = attributes.is_a?(Array) ? attributes : [ attributes ]
98
+ @invites = attributes.collect { |attrs| Zimbra::Appointment::Invite.new(attrs.merge( { :appointment => self } )) }
99
+ end
100
+
101
+ def invites_zimbra_attributes=(attributes)
102
+ return @invites = nil unless attributes
103
+
104
+ attributes = attributes.is_a?(Array) ? attributes : [ attributes ]
105
+ @invites = attributes.collect { |attrs| Zimbra::Appointment::Invite.new_from_zimbra_attributes(attrs.merge( { :appointment => self } )) }
106
+ end
107
+
108
+ def date=(val)
109
+ if val.is_a?(Integer)
110
+ @date = parse_date_in_seconds(val)
111
+ else
112
+ @date = val
113
+ end
114
+ end
115
+
116
+ def create_xml(document, invite_id = nil)
117
+ document.add "m" do |mime|
118
+ mime.set_attr "l", calendar_id
119
+
120
+ invites.each do |invite|
121
+ next unless invite_id.nil? || invite_id == invite.id
122
+
123
+ mime.add "inv" do |invite_element|
124
+ invite.create_xml(invite_element)
125
+ end
126
+ end
127
+ end
128
+
129
+ document
130
+ end
131
+
132
+ def destroy
133
+ invites.each do |invite|
134
+ AppointmentService.cancel(self, invite.id)
135
+ end
136
+ end
137
+
138
+ def save
139
+ if new_record?
140
+ response = Zimbra::AppointmentService.create(self)
141
+ invites.first.id = response[:invite_id]
142
+ @id = response[:id]
143
+ else
144
+ invites.each do |invite|
145
+ Zimbra::AppointmentService.update(self, invite.id)
146
+ end
147
+ end
148
+ end
149
+
150
+ def new_record?
151
+ id.nil?
152
+ end
153
+
154
+ def id_with_invite_id
155
+ "#{id}-#{invites.first.id}"
156
+ end
157
+
158
+ def last_instance_time
159
+ instance_times = Zimbra::AppointmentService.find_all_instances_of_an_appointment(self)
160
+ return nil unless instance_times && instance_times.count > 0
161
+ instance_times.max
162
+ end
163
+
164
+ private
165
+
166
+ def parse_date_in_seconds(seconds)
167
+ Time.at(seconds / 1000)
168
+ end
169
+
170
+ end
171
+
172
+ class AppointmentService < HandsoapAccountService
173
+ def find_all_by_calendar_id(calendar_id)
174
+ xml = invoke("n2:SearchRequest") do |message|
175
+ Builder.find_all_with_query(message, "inid:#{calendar_id}")
176
+ end
177
+ Parser.get_search_response(xml)
178
+ end
179
+
180
+ def find_all_instances_of_an_appointment(appointment)
181
+ xml = invoke("n2:SearchRequest") do |message|
182
+ message.set_attr 'query', "date:#{appointment.date.to_i * 1000}"
183
+ message.set_attr 'types', 'appointment'
184
+ message.set_attr 'calExpandInstStart', '1'
185
+ message.set_attr 'calExpandInstEnd', (Time.now + (86400 * 365 * 10)).to_i * 1000
186
+ end
187
+ response_hash = Zimbra::Hash.from_xml(xml.document.to_s)
188
+ response_hash = response_hash[:Envelope][:Body][:SearchResponse]
189
+ appointments = response_hash[:appt].is_a?(Array) ? response_hash[:appt] : [response_hash[:appt]]
190
+ appt_hash = appointments.find { |appt| appt[:attributes][:id] == appointment.id }
191
+ instances = appt_hash[:inst].is_a?(Array) ? appt_hash[:inst] : [appt_hash[:inst]]
192
+ instances.collect { |inst| Time.at(inst[:attributes][:s] / 1000) }
193
+ end
194
+
195
+ def find_all_by_calendar_id_since(calendar_id, since_date)
196
+ xml = invoke("n2:SearchRequest") do |message|
197
+ Builder.find_all_with_query(message, "inid:#{calendar_id} AND date:>#{since_date.to_i}")
198
+ end
199
+ Parser.get_search_response(xml)
200
+ end
201
+
202
+ def find(appointment_id)
203
+ xml = invoke("n2:GetAppointmentRequest") do |message|
204
+ Builder.find_by_id(message, appointment_id)
205
+ end
206
+ return nil unless xml
207
+ Parser.appointment_response(xml/"//n2:appt")
208
+ end
209
+
210
+ def create(appointment)
211
+ xml = invoke("n2:CreateAppointmentRequest") do |message|
212
+ Builder.create(message, appointment)
213
+ end
214
+ response_hash = Zimbra::Hash.from_xml(xml.document.to_s)
215
+ id = response_hash[:Envelope][:Body][:CreateAppointmentResponse][:attributes][:apptId] rescue nil
216
+ invite_id = response_hash[:Envelope][:Body][:CreateAppointmentResponse][:attributes][:invId].gsub(/#{id}\-/, '').to_i rescue nil
217
+ { :id => id, :invite_id => invite_id }
218
+ end
219
+
220
+ def update(appointment, invite_id)
221
+ xml = invoke("n2:ModifyAppointmentRequest") do |message|
222
+ Builder.update(message, appointment, invite_id)
223
+ end
224
+ end
225
+
226
+ def cancel(appointment, invite_id)
227
+ xml = invoke("n2:CancelAppointmentRequest") do |message|
228
+ Builder.cancel(message, appointment.id, invite_id)
229
+ end
230
+ end
231
+
232
+ class Builder
233
+ class << self
234
+ def find_all_with_query(message, query)
235
+ message.set_attr 'query', query
236
+ message.set_attr 'types', 'appointment'
237
+ end
238
+
239
+ def find_by_id(message, id)
240
+ message.set_attr 'id', id
241
+ end
242
+
243
+ def create(message, appointment)
244
+ appointment.create_xml(message)
245
+ end
246
+
247
+ def update(message, appointment, invite_id)
248
+ message.set_attr 'id', "#{appointment.id}-#{invite_id}"
249
+ appointment.create_xml(message, invite_id)
250
+ end
251
+
252
+ def cancel(message, appointment_id, invite_id)
253
+ message.set_attr 'id', "#{appointment_id}-#{invite_id}"
254
+ message.set_attr 'comp', 0
255
+ end
256
+ end
257
+ end
258
+
259
+ class Parser
260
+ class << self
261
+ def get_search_response(response)
262
+ (response/"//n2:appt").collect do |node|
263
+ Zimbra::Hash.from_xml(node.to_xml)
264
+ end
265
+ end
266
+
267
+ def appointment_response(node)
268
+ # It's much easier to deal with this as a hash
269
+ Zimbra::Hash.from_xml(node.to_xml)
270
+ end
271
+ end
272
+ end
273
+ end
274
+ end
@@ -0,0 +1,101 @@
1
+ module Zimbra
2
+ class Appointment
3
+ class Alarm
4
+ class << self
5
+ def new_from_zimbra_attributes(zimbra_attributes)
6
+ new(parse_zimbra_attributes(zimbra_attributes))
7
+ end
8
+
9
+ # <alarm action="DISPLAY">
10
+ # <trigger>
11
+ # <rel neg="1" m="5" related="START"/>
12
+ # </trigger>
13
+ # <desc/>
14
+ # </alarm>
15
+ def parse_zimbra_attributes(zimbra_attributes)
16
+ attrs = { appointment_invite: zimbra_attributes[:appointment_invite] }
17
+ zimbra_attributes = Zimbra::Hash.symbolize_keys(zimbra_attributes.dup, true)
18
+ zimbra_attributes = zimbra_attributes[:trigger][:rel][:attributes]
19
+
20
+ duration_negative = (zimbra_attributes[:neg] && zimbra_attributes[:neg] == 1) ? true : false
21
+
22
+ attrs.merge({
23
+ duration_negative: duration_negative,
24
+ weeks: zimbra_attributes[:w],
25
+ days: zimbra_attributes[:d],
26
+ hours: zimbra_attributes[:h],
27
+ minutes: zimbra_attributes[:m],
28
+ seconds: zimbra_attributes[:s],
29
+ when: zimbra_attributes[:related] == "START" ? :start : :end,
30
+ repeat_count: zimbra_attributes[:count],
31
+ })
32
+ end
33
+ end
34
+
35
+ ATTRS = [:appointment_invite, :duration_negative, :weeks, :days, :hours, :minutes, :seconds, :when, :repeat_count] unless const_defined?(:ATTRS)
36
+
37
+ attr_accessor *ATTRS
38
+
39
+ def initialize(args = {})
40
+ @duration_negative = true
41
+ @when = :start
42
+ self.attributes = args
43
+ end
44
+
45
+ # take attributes by the xml name or our more descriptive name
46
+ def attributes=(args = {})
47
+ ATTRS.each do |attr_name|
48
+ if args.has_key?(attr_name)
49
+ self.send(:"#{attr_name}=", args[attr_name])
50
+ elsif args.has_key?(attr_name.to_s)
51
+ self.send(:"#{attr_name}=", args[attr_name.to_s])
52
+ end
53
+ end
54
+ end
55
+
56
+ def to_hash(options = {})
57
+ hash = ATTRS.inject({}) do |attr_hash, attr_name|
58
+ attr_hash[attr_name] = self.send(:"#{attr_name}")
59
+ attr_hash
60
+ end
61
+ hash.reject! { |key, value| options[:except].include?(key.to_sym) || options[:except].include?(key.to_s) } if options[:except]
62
+ hash.reject! { |key, value| !options[:only].include?(key.to_sym) && !options[:only].include?(key.to_s) } if options[:only]
63
+ hash
64
+ end
65
+
66
+ def create_xml(document)
67
+ document.add "trigger" do |trigger_element|
68
+ trigger_element.add "rel" do |rel_element|
69
+ rel_element.set_attr "neg", duration_negative ? 1 : 0
70
+ rel_element.set_attr "w", weeks if weeks && weeks > 0
71
+ rel_element.set_attr "d", days if days && days > 0
72
+ rel_element.set_attr "h", hours if hours && hours > 0
73
+ rel_element.set_attr "m", minutes if minutes && minutes > 0
74
+ rel_element.set_attr "s", seconds if seconds && seconds > 0
75
+ rel_element.set_attr "related", self.when.to_s.upcase
76
+ rel_element.set_attr "count", repeat_count if repeat_count && repeat_count > 0
77
+ end
78
+ end
79
+ end
80
+
81
+ def date_time_of_alarm
82
+ return nil if appointment_invite.nil?
83
+
84
+ date_to_calc_from = if self.when == :start
85
+ appointment_invite.start_date_time
86
+ else
87
+ appointment_invite.end_date_time
88
+ end
89
+
90
+ total_seconds = seconds || 0
91
+ total_seconds += minutes * 60 if minutes
92
+ total_seconds += hours * 3600 if hours
93
+ total_seconds += days * 86400 if days
94
+ total_seconds += weeks * 86400 * 7 if weeks
95
+ total_seconds *= -1 if duration_negative
96
+
97
+ date_to_calc_from + total_seconds
98
+ end
99
+ end
100
+ end
101
+ end