zimbra 0.0.4 → 0.0.5

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