Zimbra Webservices (SOAP y REST API)

Siguiendo con el tema de los webservices (ver post anterior) y metiendome más con especificidades de Zimbra, decidí escribir sobre la API SOAP y REST de Zimbra.

La primera pregunta que viene es cómo se compara la API SOAP con la API REST de Zimbra o, mejor aún, cuál tengo que usar. Si vemos la documentación de ambas, encontraremos que la API SOAP de Zimbra soporta mucho más funciones que la API REST. La API REST es para acceder a los datos de la cuenta de un usuario (exportar todos sus mails o importar nuevos mails, por ejemplo), mientras que la API SOAP sirve para ejecutar funcionalidad en el servidor, que eventualmente podría ser acceder a los datos de la cuenta del usuario, pero también realizar tareas administrativas (ej. cambiar el password de una cuenta, crear un nuevo dominio, configurar una clase de servicio, etc.).

Para todos los casos se usó Apache HttpComponents para invocar los webservices.

1. API SOAP

Zimbra 8 divide los comandos SOAP en 7 servicios: zimbraAccount, zimbraAdmin, zimbraAdminExt, zimbraMail, zimbraRepl, zimbraSync y zimbraVoice.

Los ejemplo que se muestran acá son de zimbraAccount (acceso a datos de una cuenta en particular) y zimbraAdmin (acceso a funciones de administración).

Para la organización del código se propone la creación de una clase Java por comando SOAP que se utilice, y cada clase heredaría de una clase padre llamada “SoapRequest”. De este modo reutilizaremos líneas de código que deberíamos repetir en otro caso, como el “envelope” clásico de SOAP. La clase SoapRequest sería:

public class SoapRequest {

  static final String CONTENT_TYPE = "application/soap+xml;charset=UTF-8";

  static final String SOAP_MSG_TOP =
      "<soap:Envelope xmlns:soap=\"http://www.w3.org/2003/05/soap-envelope\">" +
      "<soap:Header>" +
      "<context xmlns=\"urn:zimbra\">" +
      "<nosession/>" +
      "</context>" +
      "</soap:Header>" +
      "<soap:Body>";

  static final String SOAP_MSG_BOTTOM =
      "</soap:Body>" +
      "</soap:Envelope>";

  private HttpClient httpClient;
  private String url;
  private String soapBody;
  private byte[] response = null;

  public SoapRequest(HttpClient httpClient, String url) {
    this.httpClient = httpClient;
    this.url = url;
  }

  public void setSoapBody(String soapBody) {
    this.soapBody = soapBody;
  }

  public byte[] getResponse() {
    return response;
  }

  public int execute() throws Exception {

    // se crea el cuerpo de la request
    StringEntity stringEntity = new StringEntity(
        SOAP_MSG_TOP + this.soapBody + SOAP_MSG_BOTTOM,
        ContentType.create("text/xml", "UTF-8"));

    // se crea el post
    HttpPost httpPost = new HttpPost(url);
    httpPost.setHeader("Content-Type", CONTENT_TYPE);
    httpPost.setEntity(stringEntity);

    // se ejecuta
    HttpResponse httpResponse = httpClient.execute(httpPost);

    // se obtiene el resultado
    if (httpResponse.getEntity().getContent() == null)
      throw new RuntimeException("httpResponse.getEntity().getContent() == null");

    InputStream is = httpResponse.getEntity().getContent();
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    IOUtils.copy(is, baos);
    baos.close();
    is.close();

    this.response = baos.toByteArray();

    return httpResponse.getStatusLine().getStatusCode();

  }

}

1. Auth (zimbraAdmin)

Para ejecutar cualquier webservice Zimbra necesitamos estar autenticados. La forma de autenticarnos es a través del comando SOAP “Auth”. Nuestra implementación de la clase para este comando sería:

public class AuthSoapRequest extends SoapRequest {

  public AuthSoapRequest(HttpClient httpClient, String url,
      String name, String password) {

    super(httpClient, url);

    StringBuffer strbuf = new StringBuffer();

    strbuf.append("<AuthRequest xmlns=\"urn:zimbraAdmin\">");
    strbuf.append("<name>");
    strbuf.append(name);
    strbuf.append("</name>");
    strbuf.append("<password>");
    strbuf.append(password);
    strbuf.append("</password>");
    strbuf.append("</AuthRequest>");

    this.setSoapBody(strbuf.toString());

  }

}

Y el código para invocar esta clase:

DefaultHttpClient httpClient = new DefaultHttpClient();

httpClient.params.setIntParameter("http.socket.timeout", new Integer(1000));
httpClient.params.setIntParameter("http.connection.timeout", new Integer(1000));

AuthSoapRequest authSoapRequest =
    new AuthSoapRequest(httpClient,
    "https://zimbra.midominio.com:7071/service/admin/soap",
    "admin-user", "password");

int statusCode = authSoapRequest.execute();

if (statusCode != 200)
  System.err.println(new String(authSoapRequest.getResponse()));

La invoción de este comando setea la cookie “ZM_AUTH_TOKEN” y además devuelve el “auth token” en el xml de la respuesta. Ya sea utilizando explicitamente este parámetro o, para el caso de Apache HttpComponents, dejando que la clase HttpClient se encargue de la gestión de cookies, estamos autenticados para invocar todos los comandos SOAP que necesitemos.

Personalmente opto por dejar que HttpClient se encargue de memorizar la cookie de autenticación y olvidarme del asunto, así que lo único que hay que hacer es recordar de utilizar siempre la misma instancia de HttpClient que utilizamos para invocar el comando SOAP “Auth”.

2. CreateAccount (zimbraAdmin)

Como ejemplo de ejecución de un comando cualquiera luego de la autenticación, vamos a ejecutar “CreateAccount” que crea una cuenta. Esta es la implementación de la clase que creamos para este comando:

public class CreateAccountSoapRequest extends SoapRequest {

  public CreateAccountSoapRequest(HttpClient httpClient, String url,
      String email, String nombre, String apellido) {

    super(httpClient, url);

    StringBuffer strbuf = new StringBuffer();

    strbuf.append("<CreateAccountRequest xmlns=\"urn:zimbraAdmin\">");
    strbuf.append("<name>");
    strbuf.append(email);
    strbuf.append("</name>");
    strbuf.append("<a n=\"cn\">");
    strbuf.append(nombre + " " + apellido);
    strbuf.append("</a>");
    strbuf.append("<a n=\"displayName\">");
    strbuf.append(nombre + " " + apellido);
    strbuf.append("</a>");
    strbuf.append("<a n=\"sn\">");
    strbuf.append(apellido);
    strbuf.append("</a>");
    strbuf.append("<a n=\"givenName\">");
    strbuf.append(nombre);
    strbuf.append("</a>");
    strbuf.append("</CreateAccountRequest>");

    this.setSoapBody(strbuf.toString());

  }

}

Y este es el código que la ejecuta:

CreateAccountSoapRequest createAccountSoapRequest =
    new CreateAccountSoapRequest(httpClient,
    "https://zimbra.midominio.com:7071/service/admin/soap",
    "hhernandez@midominio.com",
    "Tito",
    "Hernandez");

System.out.println(createAccountSoapRequest.execute());
System.out.println(new String(createAccountSoapRequest.getResponse()));

3. zimbraAccount

 Los ejemplos anteriores pertenecían al grupo de servicios “zimbraAdmin”, lo que requería que nos autenticáramos con un usuario administrador y ejecutáramos los webservices en la url “https://zimbra.midominio.com:7071/service/admin/soap”.

 Ahora ejecutaremos comandos del grupo de servicios “zimbraAccount”, que no requieren ser administrador.

 Las diferencias son que la url de invocación es “https://zimbra.midominio.com/service/soap” (o “http” a secas, si no estamos utilizando “https”) y que el espacio xml de los comandos SOAP en “zimbraAccount” es “urn:zimbraAccount” en lugar de “urn:zimbraAdmin”.

 Lo primero que debemos hacer, al igual que en el ejemplo anterior, es autenticarnos. Eso se hace a través del comando “Auth” de “zimbraAccount”, que si bien es similar en funcionamiento a “Auth” de “zimbraAdmin”, tiene algunas diferencias.

 La clase “SoapRequest” es la misma que usamos antes, por lo tanto pasemos a la implementación de la clase para este comando:

public class AuthSoapRequest extends SoapRequest {

  public AuthSoapRequest(HttpClient httpClient, String url,
      String name, String password) {

    super(httpClient, url);

    StringBuffer strbuf = new StringBuffer();

    strbuf.append("<AuthRequest xmlns=\"urn:zimbraAccount\">");
    strbuf.append("<account by=\"name\">");
    strbuf.append(name);
    strbuf.append("</account>");
    strbuf.append("<password>");
    strbuf.append(password);
    strbuf.append("</password>");
    strbuf.append("</AuthRequest>");

    this.setSoapBody(strbuf.toString());

  }

}

Ahora que estamos autenticados ejecutemos algún comando, por ejemplo “GetAccount”. Esta es la implementación de la clase:

public class GetAccountSoapRequest extends SoapRequest {

  public GetAccountSoapRequest(HttpClient httpClient, String url, String email) {

    super(httpClient, url);

    StringBuffer strbuf = new StringBuffer();

    strbuf.append("<GetAccountRequest xmlns=\"urn:zimbraAccount\">");
    strbuf.append("<account by=\"name\">");
    strbuf.append(email);
    strbuf.append("</account>");
    strbuf.append("</GetAccountRequest>");

    this.setSoapBody(strbuf.toString());

  }

}

Este es el código que ejecuta todo en conjunto:

AuthSoapRequest authSoapRequest = new AuthSoapRequest(httpClient,
    "https://zimbra.midominio.com/service/soap",
    "hhernandez@midominio.com", "mipassword");

if (authSoapRequest.execute() != 200) {
  // hubo algun error en la autenticacion...
}

GetInfoSoapRequest getInfoSoapRequest = new GetInfoSoapRequest(httpClient,
    "https://zimbra.midominio.com/service/soap");

System.out.println(getInfoSoapRequest.execute());
System.out.println(new String(getInfoSoapRequest.getResponse()))

2. API REST

La API REST la he utilizado para migrar mails de otro sistema webmail hecho a medida hasta Zimbra. Para ello ejecuté un POST de los mails zippeados a la casilla a cada una de las cuentas.

En este ejemplo se descargan los mails de una cuenta a un archivo zip ejecutando un GET y luego se importan a otra cuenta, ejecutando un POST.

Observese que en los ejemplo de REST se utilizó “Basic access authentication” como método de autenticación.

1. Obteniendo los mails con un GET

Aquí se obtienen todos los mails de la cuenta en un único archivo llamado “mails.zip”.

La conexión es al puerto administrativo de Zimbra 7071 porque nos conectamos con usuario administrador para obtener los mails de una casilla de otro usuario. Para el caso de un usuario obteniendo sus propios mails no hace falta utilizar este puerto especial.

HttpGet httpGet = new HttpGet(
    "https://zimbra.midominio.com:7071/home/tito@midominio.com?fmt=zip");

httpGet.setHeader("Authorization", "Basic " +
    Base64.encodeBase64String("admin@midominio.com:mipassword".getBytes()));

HttpResponse httpResponse = httpClient.execute(httpGet);

System.out.println("Status Code: " +
    httpResponse.getStatusLine().getStatusCode());

FileOutputStream fos = new FileOutputStream("mails.zip");
IOUtils.copy(httpResponse.getEntity().getContent(), fos);
fos.close();

httpResponse.getEntity().getContent().close();

2. Importando mails con un POST

Aquí se importan los mails que se encuentran en “mails.zip”.

// el parametro “timestamp=0” hace que se tome en cuenta la fecha del mail
// para la importacion
HttpPost httpPost = new HttpPost(
    "https://zimbra.midominio.com:7071/home/tito@midominio.com?fmt=zip&timestamp=0");

httpPost.setHeader("Authorization", "Basic " +
    Base64.encodeBase64String("admin@midominio.com:mipassword".getBytes()));

MultipartEntity multipartEntity = new MultipartEntity();

multipartEntity.addPart("zippedMails",
    new FileBody(new File("mails.zip")));

httpPost.setEntity(multipartEntity);

HttpResponse httpResponse = httpClient.execute(httpPost);

System.out.println("Status Code " +
    httpResponse.getStatusLine().getStatusCode());

System.out.println(EntityUtils.toString(httpResponse.getEntity()));