summaryrefslogblamecommitdiffstats
path: root/converter.js
blob: f90fe59c7c1ca86b5078873364099e759783b8e3 (plain) (tree)

































                                                                      

                                     




















































































                                                                    







                                                                     









































                                                                      
                                                  





                                             
                                                  














                                               
                                                  



























































































                                                                              
                                                  
































                                                                  
/*jshint forin:true, noarg:true, noempty:true, eqeqeq:true,
  bitwise:true, strict:true, undef:true, curly:true, browser:true,
  devel:true, indent:2, maxerr:50, moz:true, newcap:false, moz:true */

/* global mozContact: false */

/**
 * Translate objects created from LDIF files to Contacts
 *
 * @param Array of LDIF objects
 * @return Contact with all fields filled
 * @throws ValueError for unknown field in the LDIF object
 *
 * ContactsAPI:
 * @see https://wiki.mozilla.org/WebAPI/ContactsAPI
 * @spec http://www.w3.org/TR/contacts-manager-api/
 *
 * Thunderbid LDAP Schema:
 * @see https://developer.mozilla.org/docs/Thunderbird/LDAP_Support
 * @see https://tools.ietf.org/html/rfc4519
 */
function translateObjectToContact(inRec) {
  "use strict";

  var contact = {},
      year, month, day, curRec = null;

  function ValueError(message) {
    this.name = "ValueError";
    this.message = message || "Unknown Value";
  }
  ValueError.prototype = new Error();
  ValueError.prototype.constructor = ValueError;

  const cleanPhoneNoRE = /[^0-9+]*/g;

  /**
   * Find the proper record (or create new one) in the multi-value
   * attribute
   *
   * @param idx name of the attribute to which the record belongs
   * @param subClass Function constructor of the record if there is
   * none
   * @param subType String with the type of the record
   * @return record of the proper class
   *
   * Uses global variable contact.
   */
  function findSubElement(idx, subType) {
    var cAddr = null;

    function createNewContact(subType) {
      var cont = {};
      if (subType) {
        cont.type = [subType];
      }
      return cont;
    }

    // No contact.adr at all
    if (!contact.hasOwnProperty(idx)) {
      cAddr = createNewContact(subType);
      contact[idx] = cAddr;
    }
    // Single-element property
    else if (!Array.isArray(contact[idx])) {
      if (contact[idx].type.indexOf(subType) === -1) {
        contact[idx] = [contact[idx]];
        cAddr = createNewContact(subType);
        contact[idx].push(cAddr);
      }
      else {
        cAddr = contact[idx];
      }
    }
    // Array
    else {
      cAddr = contact[idx].filter(function (addr) {
        return addr.type.indexOf(subType) !== -1;
      });
      if (cAddr.length === 0) {
        cAddr = createNewContact(subType);
        contact[idx].push(cAddr);
      }
      else {
        cAddr = cAddr[0];
      }
    }

    return cAddr;
  }

  /**
   * Manages squeezing two-line address into one value field
   *
   * @param type String ['home', 'work']
   * @param target String ContactsAddress attribute to be set (e.g.,
   *   streetAddress)
   * @param first String first line index in inRec
   * @param second String second line index in inRec
   * @return None
   *   sets variable inRec local to outer function
   *   deletes both fields from inRec so as to avoid duplication
   */
  function secondLineInAddress(type, target, first, second) {
    var curRec = findSubElement("adr", type);
    if (second in inRec) {
      curRec[target] = inRec[first] + "\n" + inRec[second];
    }
    else if (first in inRec) {
      curRec[target] = inRec[first];
    }

    if (first in inRec) {
      delete inRec[first];
    }
    if (second in inRec) {
      delete inRec[second];
    }
  }

  /**
   * Firefox OS doesn’t recognize well formatted phone numbers, and
   * works best with the plain number-only ones.
   */
  function cleanPhoneNumber(inNo) {
    return  inNo.replace(cleanPhoneNoRE, "");
  }

  for (var key in inRec) {
    if (["birthyear", "birthmonth", "birthday"].indexOf(key) !== -1) {
      // We have alternatively either whole date in birthyear field,
      // e.g. 19940221, or we have all three properties set.
      year = inRec.birthyear || "1970"; // lowest year in Unix time
      month = inRec.birthmonth || null;
      day = inRec.birthday || null;

      if (year.length === 8) {
        contact.bday = new Date(parseInt(year.slice(0,4), 10),
            parseInt(year.slice(4,6), 10) - 1,
            parseInt(year.slice(6,8), 10));
        if (inRec.birthday) {
          delete inRec.birthday;
        }
        if (inRec.birthmonth) {
          delete inRec.birthmonth;
        }
      }
      else if (month && day) {
        contact.bday = new Date(parseInt(year, 10),
          parseInt(month, 10) - 1, parseInt(day, 10));
        delete inRec.birthday;
        delete inRec.birthmonth;
        delete inRec.birthyear;
      }
      else {
        throw new ValueError("Wrong value of birthday!");
      }
    }
    else if (key === "c") {
      curRec = findSubElement("adr", "work");
      curRec.countryName = inRec[key];
    }
    else if (key === "cn") {
      contact.name = [inRec[key]];
    }
    else if (key === "description") {
      contact.note = [inRec[key]];
    }
    else if (key === "facsimiletelephonenumber") {
      curRec = findSubElement("tel", "fax");
      curRec.value = cleanPhoneNumber(inRec[key]);
    }
    else if (key === "givenName") {
      contact.givenName = [inRec[key]];
    }
    else if (key === "homePhone") {
      curRec = findSubElement("tel", "home");
      curRec.value = cleanPhoneNumber(inRec[key]);
    }
    else if (key === "l") {
      curRec = findSubElement("adr", "work");
      curRec.locality = inRec[key];
    }
    else if (key === "mail") {
      curRec = findSubElement("email", "PREF");
      curRec.value = inRec[key];
    }
    else if (key === "mozillaSecondEmail") {
      curRec = findSubElement("email");
      curRec.value = inRec[key];
    }
    else if (key === "mobile") {
      curRec = findSubElement("tel", "mobile");
      curRec.value = cleanPhoneNumber(inRec[key]);
    }
    else if (key === "mozillaHomeCountryName") {
      curRec = findSubElement("adr", "home");
      curRec.countryName = inRec[key];
    }
    else if (key === "mozillaHomeLocalityName") {
      curRec = findSubElement("adr", "home");
      curRec.locality = inRec[key];
    }
    else if (key === "mozillaHomePostalCode") {
      curRec = findSubElement("adr", "home");
      curRec.postalCode = inRec[key];
    }
    else if (key === "mozillaHomeState") {
      curRec = findSubElement("adr", "home");
      curRec.region = inRec[key];
    }
    else if (["mozillaHomeStreet", "mozillaHomeStreet2"].
        indexOf(key) !== -1) {
      secondLineInAddress("home", "streetAddress",
          "mozillaHomeStreet", "mozillaHomeStreet2");
    }
    else if (key === "mozillaWorkUrl") {
      curRec = findSubElement("url", "work");
      curRec.value = inRec[key];
    }
    else if (key === "mozillaHomeUrl") {
      curRec = findSubElement("url", "home");
      curRec.value = inRec[key];
    }
    else if (key === "mozillaNickname") {
      contact.nickname = [inRec[key]];
    }
    // Per W3C Contacts API
    // http://www.w3.org/TR/contacts-manager-api/#widl-ContactProperties-org
    // org of type array of DOMString
    //     A string or set thereof representing the organization(s)
    //     the contact belongs to. It maps to vCard's ORG attribute
    //
    // Per RFC 6350:
    // =============
    // 6.6.4. ORG
    //
    //    Purpose:  To specify the organizational name and units associated
    //       with the vCard.
    //
    //    Value type:  A single structured text value consisting of components
    //       separated by the SEMICOLON character (U+003B).
    //
    //    Special notes:  The property is based on the X.520 Organization Name
    //       and Organization Unit attributes [CCITT.X520.1988].  The property
    //       value is a structured type consisting of the organization name,
    //       followed by zero or more levels of organizational unit names.
    //
    //    Example: A property value consisting of an organizational name,
    //    organizational unit #1 name, and organizational unit #2 name.
    //
    //            ORG:ABC\, Inc.;North American Division;Marketing
    else if (["o", "ou"].indexOf(key) !== -1) {
      if ('ou' in inRec) {
        if ('o' in inRec) {
          contact.org = inRec.o + ";" + inRec.ou;
        }
        else {
          throw new ValueError(
              "Organizational unit without an organization!");
        }
        delete inRec.ou;
      }
      else {
        contact.org = inRec.o;
      }
      delete inRec.o;
    }
    else if (key === "postalCode") {
      curRec = findSubElement("adr", "work");
      curRec.postalCode = inRec[key];
    }
    else if (key === "sn") {
      contact.familyName = [inRec[key]];
    }
    else if (key === "st") {
      curRec = findSubElement("adr", "work");
      curRec.region = inRec[key];
    }
    else if (["street", "mozillaWorkStreet2"].
        indexOf(key) !== -1) {
      secondLineInAddress("work", "streetAddress",
          "street", "mozillaWorkStreet2");
    }
    else if (key === "telephoneNumber") {
      curRec = findSubElement("tel", "work");
      curRec.value = cleanPhoneNumber(inRec[key]);
    }
    else if (key === "title") {
      contact.jobTitle = [inRec[key]];
    }
    // Unknown attribute
    else {
      throw new ValueError("Unknown attribute " + key +
          " with value:\n" + inRec[key]);
    }
  }

  // Per RFC 4519 section 3.12 cn and sn attributes are always
  // required
  if (! contact.hasOwnProperty('name')) {
    if (contact.hasOwnProperty('familyName') &&
        contact.hasOwnProperty('givenName')) {
      contact.name = contact.givenName + " " + contact.familyName;
    }
    else if (contact.hasOwnProperty("org")) {
      contact.name = contact.org;
    }
    else if (contact.hasOwnProperty("jobTitle")) {
      contact.name = contact.jobTitle;
    }
  }

  if (Object.keys(contact).length > 0) {
    var outObj = new mozContact();
    outObj.init(contact);
    return contact;
  }
  return null;
}