December 2009
November 2009
October 2009
September 2009
June 2009
April 2009
March 2009
February 2009
January 2009
December 2008
November 2008
October 2008
July 2008
June 2008
October 2007
September 2007
Doing anything with SOAP is a pain without a WSDL, which is the case with Zimbra. All of the Howtos I found about SOAP and ruby either required a WSDL or making several classes in a special, undocumented way to trick a SOAP::RPC::Driver instance into working. Both were unacceptable. After much hardship, I found an easier to read way to do SOAP without an WSDL in ruby, by building SOAP::Elements myself. Here is the code, documented to be easy to read, use, and extend.
# Incomplete library for interacting with Zimbra # # require 'zimbra' # # host = 'zimbra.tylerlesmann.com' # user = 'root' # passwd = 'hard_password' # creds = Zimbra.authenticate(host, user, passwd) # usercreds = Zimbra.masquerade(host, creds.authToken, 'tlesmann') # Zimbra.createappointment(host, usercreds.authToken, # Time.local(2009, 6, 26), 'Make a blog post', 'Maybe some Java', [ # '/home/tlesmann/Documents/java.png', # '/home/tlesmann/Documents/tutorial.pdf', # ]) require 'net/http' require 'net/https' require 'soap/element' require 'soap/rpc/driver' require 'soap/processor' require 'soap/streamHandler' require 'soap/property' require 'zimbra/multipart' module Zimbra # Builds and sends AuthRequest to a provided Zimbra host. # # Returns a SOAP::Mapping instance, with an authToken attribute def self.authenticate(host, name, password) header = SOAP::SOAPHeader.new body = SOAP::SOAPBody.new(element('AuthRequest', nil, { 'xmlns' => 'urn:zimbraAdmin', }, [ element('name', name), element('password', password), ] )) envelope = SOAP::SOAPEnvelope.new(header, body) return send_soap(envelope, host) end # Builds and sends CreateAppointmentRequest to a provided Zimbra host. The # attachments argument expects a list of filename strings. # # Returns a SOAP::Mapping instance def self.createappointment(host, authToken, start, subject, description='', attachments=[]) header = SOAP::SOAPHeader.new context = element('context', nil, {'xmlns' => 'urn:zimbra'}, [ element('authToken', authToken) ]) header.add('context', context) aids = [] for attachment in attachments aids << upload_attachment(host, authToken, attachment) end if aids.empty? attach = nil else attach = element('attach', nil, { 'aid' => aids.join(",") }) end body = SOAP::SOAPBody.new(element('CreateAppointmentRequest', nil, { 'xmlns' => 'urn:zimbraMail' }, [ element('m', nil, {}, [ element('inv', nil, {}, [ element('comp', nil, { 'status' => 'CONF', 'allDay' => 1, 'fb' => 'F', 'name' => subject, 'noBlob' => 1, }, [ datetime('s', start), datetime('e', start), element('descHtml', description), element('alarm', nil, { 'action' => 'DISPLAY' }, [ element('trigger', nil, {}, [ element('rel', nil, { 'm' => 1 }) ]), element('desc', subject), ] ), ] ), ]), attach ]), ] )) envelope = SOAP::SOAPEnvelope.new(header, body) send_soap(envelope, host) end # builds SOAP::SOAPElement with tag name with a *d* attribute of the # provided ruby Time def self.datetime(name, time) return element(name, nil, {'d' => time.strftime("%Y%m%d")}) end # builds SOAP::SOAPElements the way SOAP::SOAPElement constructor _should_ # # element('AuthRequest', nil, # { # 'xmlns' => 'urn:zimbraAdmin', # }, # [ # element('name', 'whoa'), # element('password', 'man'), # ] # ) # # The returned SOAP::SOAPElement converted to XML would be: # # <AuthRequest xmlns="urn:zimbraAdmin"> # <name>whoa</name> # <password>man</password> # </AuthRequest> def self.element(name, value=nil, attrs={}, children=[]) element = SOAP::SOAPElement.new(name, value) element.extraattr.update(attrs) for child in children if child element.add(child) end end return element end # Builds and sends DelegateAuth Request to a provided Zimbra host. The # authToken must be that of an admin! The account arg is nothing fancy, just # the username of the user to spoof. # # Returns a SOAP::Mapping instance, with an authToken attribute def self.masquerade(host, authToken, account) header = SOAP::SOAPHeader.new context = element('context', nil, {'xmlns' => 'urn:zimbra'}, [ element('authToken', authToken) ]) header.add('context', context) body = SOAP::SOAPBody.new(element('DelegateAuthRequest', nil, { 'xmlns' => 'urn:zimbraAdmin' }, [ element('account', account, { 'by' => 'name', }) ] )) envelope = SOAP::SOAPEnvelope.new(header, body) return send_soap(envelope, host) end # Marshals SOAP::Envelopes and sends them to a given Zimbra host # # Returns response as a SOAP::Mapping instance def self.send_soap(envelope, host) url = 'https://' + host + ':7071/service/admin/soap/' stream = SOAP::HTTPStreamHandler.new(SOAP::Property.new) request_string = SOAP::Processor.marshal(envelope) puts request_string if $DEBUG request = SOAP::StreamHandler::ConnectionData.new(request_string) response_string = stream.send(url, request).receive_string puts response_string if $DEBUG env = SOAP::Processor.unmarshal(response_string) return SOAP::Mapping.soap2obj(env.body.root_node) end # Uploads file to given Zimbra host # # Returns a string containing the Zimbra attachment id. These attachments are # only accessible to the user that uploaded them. def self.upload_attachment(host, authToken, filename) params = Hash.new file = File.open(filename, "rb") params["attachment"] = file mp = Multipart::MultipartPost.new query, headers = mp.prepare_query(params) file.close headers['Cookie'] = 'ZM_AUTH_TOKEN=' + authToken url = URI.parse('https://' + host + '/service/upload') client = Net::HTTP.new(url.host, url.port) client.use_ssl = true response = client.post(url.path + '?fmt=raw', query, headers) return response.body.split(',')[2].strip.slice(1..-2) end end
Note: I would have done this in python, if it were not needed for an existing rails application. ;)
With the announcement of the Android Scripting Engine, I had to check it out, what with the ability to code python on the G1. It has one large inconvenience at the moment. There is no built in way to use scripts on the SD card. I have a workaround for the time being. With the following script, which runs with ASE, you will be able to import scripts from the ase folder of your SD card into ASE's normal script directory.
import android import os import shutil src = '/sdcard/ase' dst = '/data/data/com.google.ase/scripts' droid = android.Android() for file in os.listdir(src): shutil.copy(os.path.join(src, file), dst) # Interesting permissions on Android os.chmod(os.path.join(dst, file), 0666) droid.makeToast('Import Complete')
Note that you will be typing this into your Android device through ASE. The last script you will have to do that way. You will have to close and reopen ASE before the scripts appear.
