Archive
Tags
android (3)
ant (2)
beautifulsoup (1)
debian (1)
decorators (1)
django (9)
dovecot (1)
encryption (1)
fix (4)
gotcha (2)
hobo (1)
htmlparser (1)
imaplib (2)
java (1)
json (2)
kerberos (2)
linux (7)
lxml (5)
markdown (4)
mechanize (6)
multiprocessing (1)
mysql (2)
nagios (2)
new_features (3)
open_source (5)
optparse (2)
parsing (1)
perl (2)
postgres (1)
preseed (1)
pxe (4)
pyqt4 (1)
python (41)
raid (1)
rails (1)
red_hat (1)
reportlab (4)
request_tracker (2)
rt (2)
ruby (1)
scala (1)
screen_scraping (7)
shell_scripting (8)
soap (1)
solaris (3)
sql (2)
sqlalchemy (2)
tips_and_tricks (1)
twitter (2)
ubuntu (1)
vmware (2)
windows (1)
zimbra (2)

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. ;)

Posted by Tyler Lesmann on June 24, 2009 at 16:14 and commented on 9 times
Tagged as: mechanize ruby soap zimbra

Zimbra has a way of developing unusable share permissions and the only way to fix this is to strip all of the permissions are start fresh. In earlier releases, you could use this script, but the parsing is broken in version 5.0.x. I have written a script to handle this. I could have written it in bash, but shell scripts have a hard time with many characters, like backslashes and quotes. This python script requires simplejson and should be ran as the zimbra user, just like zmmailbox. Usage is as such:

Usage: fixgrants.py -u user [-t] folder ...

-u user = account to which the folders belong
-f = strip flags, this will fix inheritance problems.
-t = test_mode, just show commands, nothing will be executed

And here is the script:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
#!/usr/bin/env python

"""
fixgrants is a utility script for maintaining Zimbra's mailbox sharing
permissions.  Zimbra has a tendency to develop unusable permission sets and this
script will strip all permissions from specified folders of a specified account.
It also will strip the folder flags, which will fix permission inheritance
issues.
"""

import getopt
import os
import simplejson
import sys

def get_flagged_list(folder):
    """ Parse getFolder JSON to get a dictionary of folders with flags """
    if 'flags' in folder:
        # Double escape backslashes
        folders = {folder['path'].replace('\\', '\\\\'): folder['flags'],}
    else:
        folders = {}
    if folder['children']:
        for child in folder['children']:
            folders.update(get_flagged_list(child))
    return folders

def get_grants_list(folder):
    """ Parse getFolder JSON to get a dictionary of folders with grants """
    if folder['grants']:
        # Double escape backslashes
        folders = {folder['path'].replace('\\', '\\\\'): folder['grants'],}
    else:
        folders = {}
    if folder['children']:
        for child in folder['children']:
            folders.update(get_grants_list(child))
    return folders

def rm_folder_grants(user, folder, test_mode=False):
    """
    Remove grants recursively

    rm_folder_grants(user, folder, test_mode=False)

    user is a zimbra account.
    folder is a getFolder JSON parsed by simplejson.
    test_mode determine if any commands will actually be executed.
    """
    print "Removing Grants"
    for folder, grants in get_grants_list(folder).iteritems():
        print "Processing", folder
        for grant in grants:
            grant['account'] = user
            grant['folder'] = folder
            cmd = "zmmailbox -z -m %(account)s mfg \"%(folder)s\" account %(name)s none" % grant
            print cmd
            if not test_mode is True:
                os.popen(cmd)

def rm_folder_flags(user, folder, test_mode=False):
    """
    Remove folder flags recursively

    rm_folder_flags(user, folder, test_mode=False)

    user is a zimbra account.
    folder is a getFolder JSON parsed by simplejson.
    test_mode determine if any commands will actually be executed.
    """
    print "Removing Flags"
    for folder in get_flagged_list(folder):
        print "Processing", folder
        args = {'account': user, 'folder': folder}
        cmd = "zmmailbox -z -m %(account)s mff \"%(folder)s\" ''" % args
        print cmd
        if not test_mode is True:
            os.popen(cmd)

def print_usage():
    sys.stderr.write("""
    Usage: %s -u user [-t] folder ...

    -u user = account to which the folders belong
    -f = strip flags, this will fix inheritance problems.
    -t = test_mode, just show commands, nothing will be executed
""" % sys.argv[0])


if __name__ == '__main__':
    optlist, args = getopt.getopt(sys.argv[1:], 'ftu:')
    if not args:
        sys.stderr.write("You must specify folders to alter\n")
        print_usage()
        raise SystemExit, 1
    optd = dict(optlist)
    if '-u' not in optd:
        sys.stderr.write("You must specify a user\n")
        print_usage()
        raise SystemExit, 1
    test_mode = False
    if '-t' in optd:
        test_mode = True
    strip_flags = False
    if '-f' in optd:
        strip_flags = True

    user = optd['-u']
    for folder in args:
        output = os.popen("zmmailbox -z -m \"%s\" gf \"%s\"" % (user, folder))
        folders = simplejson.load(output)
        rm_folder_grants(user, folders, test_mode=test_mode)
        if strip_flags is True:
            rm_folder_flags(user, folders, test_mode=test_mode)
Posted by Tyler Lesmann on April 3, 2009 at 14:21 and commented on 1 time
Tagged as: fix python zimbra