public void toCSV(
      String format,
      String locale,
      Character separator,
      Iterator<? extends MailItem> contacts,
      StringBuilder sb)
      throws ParseException {
    if (knownFormats == null) {
      return;
    }

    CsvFormat fmt = getFormat(format, locale);
    if (separator != null) {
      fieldSeparator = separator;
    } else {
      String delimKey = fmt.key();
      Character formatDefaultDelim = delimiterInfo.get(delimKey);
      if (formatDefaultDelim != null) {
        LOG.debug("toCSV choosing %c from <delimiter> matching %s", formatDefaultDelim, delimKey);
        fieldSeparator = formatDefaultDelim;
      }
    }
    LOG.debug(
        "toCSV Requested=[format=\"%s\" locale=\"%s\" delim=\"%c\"] Actual=[%s delim=\"%c\"]",
        format, locale, separator, fmt, fieldSeparator);
    if (fmt == null) {
      return;
    }
    if (fmt.allFields()) {
      ArrayList<Map<String, String>> allContacts = new ArrayList<Map<String, String>>();
      HashSet<String> fields = new HashSet<String>();
      while (contacts.hasNext()) {
        Object obj = contacts.next();
        if (obj instanceof Contact) {
          Contact c = (Contact) obj;
          allContacts.add(c.getFields());
          fields.addAll(c.getFields().keySet());
        }
      }
      ArrayList<String> allFields = new ArrayList<String>();
      allFields.addAll(fields);
      Collections.sort(allFields);
      addFieldDef(allFields, sb);
      for (Map<String, String> contactMap : allContacts) {
        toCSVContact(allFields, contactMap, sb);
      }
      return;
    }

    if (!fmt.hasNoHeaders()) {
      addFieldDef(fmt, sb);
    }
    while (contacts.hasNext()) {
      Object c = contacts.next();
      if (c instanceof Contact) {
        toCSVContact(fmt, (Contact) c, sb);
      }
    }
  }
  /**
   * @param csv is the list of fields in a record from a CSV file
   * @param formats is the list of CsvFormats to be considered applicable
   * @return a map from field to value
   * @throws ParseException
   */
  private Map<String, String> toContact(List<String> csv, CsvFormat format) throws ParseException {
    ContactMap contactMap = new ContactMap();

    // NOTE: If there isn't a mapping for a field name defined in "format"
    // NOTE: a user defined attribute with that field name will be used.
    if (csv == null) {
      return contactMap.getContacts();
    }
    if (format.allFields()) {
      int end = csv.size();
      end = (end > fieldNames.size()) ? fieldNames.size() : end;
      for (int i = 0; i < end; i++) {
        contactMap.put(fieldNames.get(i), csv.get(i));
      }
    } else if (format.hasNoHeaders()) {
      int end = csv.size();
      end = (end > format.columns.size()) ? format.columns.size() : end;
      for (int i = 0; i < end; i++) {
        contactMap.put(format.columns.get(i).field, csv.get(i));
      }
    } else {
      /* Many CSV formats are output in a specific order and sometimes
       * contain duplicate field names with mappings to different
       * Zimbra contact fields.
       */
      Map<CsvColumn, Map<String, String>> pendMV = new HashMap<CsvColumn, Map<String, String>>();
      List<CsvColumn> unseenColumns = new ArrayList<CsvColumn>();
      unseenColumns.addAll(format.columns);
      for (int ndx = 0; ndx < fieldNames.size(); ndx++) {
        String csvFieldName = fieldNames.get(ndx);
        String fieldValue = (ndx >= csv.size()) ? null : csv.get(ndx);
        CsvColumn matchingCol = null;
        String matchingFieldLc = null;
        for (CsvColumn unseenC : unseenColumns) {
          matchingFieldLc = unseenC.matchingLcCsvFieldName(csvFieldName);
          if (matchingFieldLc == null) {
            continue;
          }
          if (unseenC.colType == ColType.MULTIVALUE) {
            Map<String, String> currMV = pendMV.get(matchingCol);
            if ((currMV != null) && currMV.get(matchingFieldLc) != null) {
              // already have field with this name that matches this column
              continue;
            }
          }
          matchingCol = unseenC;
          break;
        }
        if (matchingCol == null) {
          // unknown field - setup for adding as a user defined attribute
          LOG.debug(
              "Adding CSV contact attribute [%s=%s] - assuming is user defined.",
              csvFieldName, fieldValue);
          contactMap.put(csvFieldName, fieldValue);
          continue;
        }
        switch (matchingCol.colType) {
          case NAME:
            addNameField(fieldValue, matchingCol.field, contactMap);
            unseenColumns.remove(matchingCol);
            break;
          case DATE:
            addDateField(fieldValue, matchingCol.field, format.key(), contactMap);
            unseenColumns.remove(matchingCol);
            break;
          case TAG:
            contactMap.put(TAG, fieldValue);
            break;
          case MULTIVALUE:
            for (String cname : matchingCol.names) {
              if (cname.toLowerCase().equals(matchingFieldLc)) {
                Map<String, String> currMV = pendMV.get(matchingCol);
                if (currMV == null) {
                  currMV = new HashMap<String, String>();
                  pendMV.put(matchingCol, currMV);
                }
                currMV.put(matchingFieldLc, fieldValue);
                if (currMV.size() >= matchingCol.names.size()) {
                  addMultiValueField(matchingCol, currMV, contactMap);
                  pendMV.remove(currMV);
                  unseenColumns.remove(matchingCol);
                }
              }
            }
            break;
          default:
            contactMap.put(matchingCol.field, fieldValue);
            unseenColumns.remove(matchingCol);
        }
      }
      // Process multi-value fields where only some constituent fields were present
      for (Map.Entry<CsvColumn, Map<String, String>> entry : pendMV.entrySet()) {
        addMultiValueField(entry.getKey(), entry.getValue(), contactMap);
      }
    }

    Map<String, String> contact = contactMap.getContacts();

    // Bug 50069 - Lines with single blank in them got imported as a blank contact
    // Initial fix idea was for parseField to return the trimmed version of the string
    // However, this from rfc4180 - Common Format and MIME Type for Comma-Separated
    // Values (CSV) Files :
    //     "Spaces are considered part of a field and should not be ignored."
    // suggests that might be an invalid thing to do, so now just reject the contact
    // if the whole line would collapse to an empty string with trim.
    if (contact.size() == 1) {
      boolean onlyBlank = true;
      for (String val : contact.values()) {
        if (!val.trim().equals("")) {
          onlyBlank = false;
          break;
        }
      }
      if (onlyBlank) contact = new HashMap<String, String>();
    }
    return contact;
  }