linagora/openpaas-esn

View on GitHub
modules/linagora.esn.contact/backend/lib/client/addressbook.js

Summary

Maintainability
D
2 days
Test Coverage
const Q = require('q');
const { parseString } = require('xml2js');
const ICAL = require('@linagora/ical.js');
const davClient = require('../dav-client').rawClient;
const {
  DEFAULT_ADDRESSBOOK_NAME,
  ADDRESSBOOK_ROOT_PATH,
  HEADER_VCARD_JSON,
  HEADER_JSON
} = require('./constants');

module.exports = (dependencies, options = {}) => {
  const logger = dependencies('logger');

  const {
    ESNToken,
    user,
    davServerUrl,
    addressbookHome
  } = options;

  const { getDavEndpoint, checkResponse } = require('./utils')(dependencies, { davServerUrl });

  return (addressbookName = DEFAULT_ADDRESSBOOK_NAME) => {
    const vCardModule = require('./vCard')(dependencies, {
      ESNToken,
      addressbookHome,
      addressbookName,
      user
    });

    return {
      create,
      list,
      get,
      getMultipleContactsFromPaths,
      remove,
      update,
      vcard
    };

    /**
     * The vcard API
     *
     * @param cardId
     */
    function vcard(cardId) {
      return {
        ...vCardModule(cardId),
        removeMultiple: removeMultipleContacts
      };
    }

    /**
     * Create new addressbook
     * @param  {Object} addressbook The addressbook json to be created
     *
     * @return {Promise}
     */
    function create(addressbook) {
      const headers = {
        ESNToken,
        accept: HEADER_VCARD_JSON
      };
      const method = 'POST';

      return _getParentUrl()
        .then(url => {
          const deferred = Q.defer();

          davClient({
            method,
            headers,
            url,
            json: true,
            body: addressbook
          }, checkResponse(deferred, method, 'Error while creating addressbook in DAV'));

          return deferred.promise;
        });
    }

    /**
     * Remove an addressbook
     *
     * @return {Promise}
     */
    function remove() {
      const method = 'DELETE';
      const headers = {
        ESNToken,
        accept: HEADER_VCARD_JSON
      };

      return _getUrl()
        .then(url => {
          const deferred = Q.defer();

          davClient({
            method,
            headers,
            url
          }, checkResponse(deferred, method, 'Error while removing addressbook in DAV'));

          return deferred.promise;
        });
    }

    /**
     * Update an addressbook
     *
     * @return {Promise}
     */
    function update(modified) {
      const method = 'PROPPATCH';
      const headers = {
        ESNToken,
        accept: HEADER_VCARD_JSON
      };

      return _getUrl()
        .then(url => {
          const deferred = Q.defer();

          davClient({
            method,
            headers,
            url,
            json: true,
            body: modified
          }, checkResponse(deferred, method, 'Error while updating addressbook in DAV'));

          return deferred.promise;
        });
    }

    /**
     * Get all addressbooks of current user
     * @param  {Object} options Options for listing address books
     *
     * @return {Promise}
     */
    function list(options = {}) {
      const method = 'GET';
      const headers = {
        ESNToken,
        accept: HEADER_VCARD_JSON
      };

      return _getParentUrl()
        .then(url => {
          const deferred = Q.defer();
          const clientOptions = {
            method,
            headers,
            url,
            json: true
          };

          if (options.query) { clientOptions.query = options.query; }

          davClient(clientOptions, checkResponse(deferred, method, 'Error while getting addressbook list in DAV'));

          return deferred.promise;
        });
    }

    /**
     * Get an addressbook
     * @return {Promise}
     */
    function get() {
      const method = 'PROPFIND';
      const headers = {
        ESNToken,
        accept: HEADER_JSON
      };

      const properties = {
        '{DAV:}displayname': 'dav:name',
        '{urn:ietf:params:xml:ns:carddav}addressbook-description': 'carddav:description',
        '{DAV:}acl': 'dav:acl',
        '{DAV:}invite': 'dav:invite',
        '{DAV:}share-access': 'dav:share-access',
        '{DAV:}group': 'dav:group',
        '{http://open-paas.org/contacts}subscription-type': 'openpaas:subscription-type',
        '{http://open-paas.org/contacts}source': 'openpaas:source',
        '{http://open-paas.org/contacts}type': 'type',
        '{http://open-paas.org/contacts}state': 'state',
        '{http://open-paas.org/contacts}numberOfContacts': 'numberOfContacts',
        acl: 'acl'
      };

      return _getUrl()
        .then(url => {
          const deferred = Q.defer();

          davClient({
            method,
            headers,
            url,
            json: true,
            body: {
              properties: Object.keys(properties)
            }
          }, (err, response, body) => {
            let newBody = body;

            if (!err && response.statusCode === 200) {
              newBody = {
                _links: {
                  self: { href: url }
                }
              };

              Object.keys(properties).forEach(key => {
                newBody[properties[key]] = body[key];
              });
            }

            checkResponse(deferred, method, 'Error while getting an addressbook from DAV')(err, response, newBody);
          });

          return deferred.promise;
        });
    }

    /**
     * Remove multiple contacts from DAV
     * @param  {Object} options Contains:
     *                               + modifiedBefore: timestamp in seconds
     * @return {Promise} Resolve an array of removed contacts object
     *                           informations contains:
     *                               + cardId: the contact ID,
     *                               + data: object contain response and body if success
     *                               + error: error if failure
     */
    function removeMultipleContacts(options = {}) {
      if (!options.hasOwnProperty('modifiedBefore')) {
        return Promise.reject(new Error('options.modifiedBefore is required'));
      }
      const query = {
        modifiedBefore: options.modifiedBefore
      };

      return vCardModule().list(query)
        .then(({ body }) => {
          if (!body || !body._embedded || !body._embedded['dav:item']) {
            return Promise.reject(new Error('Error while deleting multiple contacts'));
          }

          logger.debug('Removing %s contacts from DAV', body._embedded['dav:item'].length);

          return Promise.all(body._embedded['dav:item'].map(davItem => {
              const cardId = (new ICAL.Component(davItem.data)).getFirstPropertyValue('uid');

              return vCardModule(cardId).remove()
                .then(
                  data => ({ cardId: cardId, data: data }),
                  err => {
                    logger.error('Failed to delete contact', cardId, err);

                    return { cardId: cardId, error: err };
                  }
                );
              }));
        });
    }

    /**
     * Get multiple contacts from DAV
     * @param  {Array}  paths An array of contact paths
     * @return {Promise}
     */
    function getMultipleContactsFromPaths(paths) {
      if (!paths.length) {
        return Promise.resolve([]);
      }

      let hrefs = '';

      paths.forEach(path => {
        hrefs += `<D:href>${path}</D:href>`;
      });

      const method = 'REPORT';
      const headers = {
        ESNToken,
        'Content-Type': 'application/xml',
        Accept: 'application/xml'
      };

      return getDavEndpoint()
        .then(davEndpoint => {
          const deferred = Q.defer();

          davClient({
            method,
            headers,
            url: `${davEndpoint}/${ADDRESSBOOK_ROOT_PATH}`,
            body: `<?xml version="1.0" encoding="utf-8" ?>
                    <C:addressbook-multiget xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:carddav">
                      <D:prop>
                        <D:getetag/>
                        <C:address-data/>
                      </D:prop>
                      ${hrefs}
                    </C:addressbook-multiget>`
          }, checkResponse(deferred, method, 'Error while getting multiple contacts from DAV'));

          return deferred.promise;
        })
        .then(({ body }) => Q.nfcall(parseString, body))
        .then(parsedData => parsedData['d:multistatus']['d:response'].map(item => {

          const propstat = item['d:propstat'][0];
          const path = item['d:href'][0];

          if (propstat['d:status'][0] !== 'HTTP/1.1 200 OK') {
            return logger.error('Cannot fetch the contact', path, propstat['d:status'][0]);
          }

          return {
            etag: propstat['d:prop'][0]['d:getetag'][0],
            vcard: ICAL.Component.fromString(propstat['d:prop'][0]['card:address-data'][0]).jCal,
            path
          };
        }).filter(Boolean));
    }

    function _getUrl() {
      return getDavEndpoint(user)
        .then(davEndpoint => `${davEndpoint}/${ADDRESSBOOK_ROOT_PATH}/${addressbookHome}/${addressbookName}.json`);
    }

    function _getParentUrl() {
      return getDavEndpoint(user)
        .then(davEndpoint => `${davEndpoint}/${ADDRESSBOOK_ROOT_PATH}/${addressbookHome}.json`);
    }
  };
};