]> git.deb.at Git - pkg/abook.git/blobdiff - filter.c
add-email: fixed a spurious "Valid sender address not found" in interactive-mode
[pkg/abook.git] / filter.c
index de787f74104a5147f8e31d0f45f66be886ea4343..dca51b804e79495580c3af03dedfcf477e7a0cf4 100644 (file)
--- a/filter.c
+++ b/filter.c
@@ -35,6 +35,8 @@
 
 extern abook_field_list *fields_list;
 extern int fields_count;
+// see also enum field_types @database.h
+extern abook_field standard_fields[];
 
 /*
  * function declarations
@@ -503,49 +505,114 @@ export_file(char filtname[FILTNAME_LEN], char *filename)
 
 #include "ldif.h"
 
-static void    ldif_fix_string(char *str);
+/* During LDIF import we need more fields than the
+   ITEM_FIELDS of a *list_item. Eg: "objectclass"
+   to test valid records, ...
+   Here we extends the existing field_types enum
+   to define new fields indexes usable during processing.
+   Newly created LDIF attr names could be associated to
+   them using ldif_conv_table[]. */
+typedef enum {
+       LDIF_OBJECTCLASS = ITEM_FIELDS + 1
+} ldif_field_types;
 
-#define        LDIF_ITEM_FIELDS        16
+#define        LDIF_ITEM_FIELDS        LDIF_OBJECTCLASS
 
 typedef char *ldif_item[LDIF_ITEM_FIELDS];
 
+/* LDIF field's names *must* respect the ordering
+   defined by the field_types enum from database.h
+   This is only used to define (for export only)
+   abook standard field to LDIF attr name mappings */
 static ldif_item ldif_field_names = {
-       "cn",
-       "mail",
-       "streetaddress",
-       "streetaddress2",
-        "locality",
-       "st",
-       "postalcode",
-       "countryname",
-       "homephone",
-       "description",
-       "homeurl",
-       "facsimiletelephonenumber",
-       "cellphone",
-       "xmozillaanyphone",
-       "xmozillanickname",
-       "objectclass", /* this must be the last entry */
+       "cn",                   // NAME
+       "mail",                 // EMAIL
+       "streetaddress",        // ADDRESS
+       "streetaddress2",       // ADDRESS2
+       "locality",             // CITY
+       "st",                   // STATE
+       "postalcode",           // ZIP
+       "countryname",          // COUNTRY
+       "homephone",            // PHONE
+       "telephonenumber",      // WORKPHONE
+       "facsimiletelephonenumber",     // FAX
+       "cellphone",            // MOBILEPHONE
+       "xmozillanickname",     // NICK
+       "homeurl",              // URL
+       "description",          // NOTES
+       "anniversary",          // ANNIVERSARY
+       "ou",                   // GROUPS
 };
 
-static int ldif_conv_table[LDIF_ITEM_FIELDS] = {
-       NAME,           /* "cn" */
-       EMAIL,          /* "mail" */
-       ADDRESS,        /* "streetaddress" */
-       ADDRESS2,       /* "streetaddress2" */
-        CITY,          /* "locality" */
-       STATE,          /* "st" */
-       ZIP,            /* "postalcode" */
-       COUNTRY,        /* "countryname" */
-       PHONE,          /* "homephone" */
-       NOTES,          /* "description" */
-       URL,            /* "homeurl" */
-       FAX,            /* "facsimiletelephonenumber" */
-       MOBILEPHONE,    /* "cellphone" */
-       WORKPHONE,      /* "xmozillaanyphone" */
-       NICK,           /* "xmozillanickname" */
-       -1,             /* "objectclass" */ /* this must be the last entry */
+/* Used to map LDIF attr names from input to
+   the abook restricted set of standard fields. */
+typedef struct {
+       char *key;
+       int  index;
+} ldif_available_items;
+
+static ldif_available_items ldif_conv_table[] = {
+       /* initial field names respect the field_types enum
+          from database.h but this is only for readability.
+          This ldif_item struct allow use to define multiple
+          LDIF field names ("attribute names") for one abook field */
+
+       {"cn",                  NAME},          // 0
+       {"mail",                EMAIL},
+       {"streetaddress",       ADDRESS},
+       {"streetaddress2",      ADDRESS2},
+       {"locality",            CITY},
+       {"st",                  STATE},
+       {"postalcode",          ZIP},
+       {"countryname",         COUNTRY},
+       {"homephone",           PHONE},
+       {"telephonenumber",     WORKPHONE},     // workphone, according to Mozilla
+       {"facsimiletelephonenumber",    FAX},
+       {"cellphone",           MOBILEPHONE},
+       {"mozillanickname",     NICK},
+       {"homeurl",             URL},
+       {"description",         NOTES},
+       {"anniversary",         ANNIVERSARY},
+       {"ou",                  GROUPS},        // 16
+
+       // here comes a couple of aliases
+       {"mozillasecondemail",  EMAIL},
+       {"homecity",            CITY},
+       {"zip",                 ZIP},
+       {"tel",                 PHONE},
+       {"xmozillaanyphone",    WORKPHONE},     // ever used ?
+       {"workphone",           WORKPHONE},
+       {"fax",                 FAX},
+       {"telexnumber",         FAX},
+       {"mobilephone",         MOBILEPHONE},
+       {"mobile",              MOBILEPHONE},
+       {"xmozillanickname",    NICK},
+       {"labeledURI",          URL},
+       {"notes",               NOTES},
+       {"birthday",            ANNIVERSARY},
+       {"category",            GROUPS},
+
+       /* TODO:
+          "sn": append to lastname ?
+          "surname": append to lastname ?
+          "givenname": prepend to firstname ? */
+
+       /* here starts dummy fields:
+
+          As long as additional indexes are created
+          (using the above ldif_field_types),
+          any other LDIF attr name can be added and
+          used during ldif entry processing.
+          But obviously fields > ITEM_FIELDS (database.h) won't be
+          copied into the final *list_item. */
+
+       /* - to avoid mistake, don't use the special ITEM_FIELDS value.
+          - see also: http://mxr.mozilla.org/comm-central/source/mailnews/addrbook/src/nsAbLDIFService.cpp */
+
+       // used to check valid LDIF records:
+       {"objectclass",         LDIF_OBJECTCLASS}
 };
+const int LDIF_IMPORTABLE_ITEM_FIELDS = (int)sizeof(ldif_conv_table)/sizeof(*ldif_conv_table);
 
 /*
   Handles multi-line strings.
@@ -607,35 +674,90 @@ ldif_add_item(ldif_item li)
        list_item item;
        int i;
 
-       item = item_create();
+       /* if there's no value for "objectclass"
+          it's probably a buggy record */
+       if(!li[LDIF_OBJECTCLASS])
+               goto bail_out;
 
-       for(i=0; i < LDIF_ITEM_FIELDS; i++) {
-               if(ldif_conv_table[i] >= 0 && li[i] && *li[i])
-                       item_fput(item,ldif_conv_table[i],xstrdup(li[i]));
+       /* just copy from our extended ldif_item to a regular
+          list_item,
+          TODO: API could be changed so db_fput_byid() is usable */
+       item = item_create();
+       for(i=0; i < ITEM_FIELDS; i++) {
+               if(li[i] && *li[i])
+                       item[i] = xstrdup(li[i]);
        }
 
        add_item2database(item);
+       item_free(&item);
 
+bail_out:
        for(i=0; i < LDIF_ITEM_FIELDS; i++)
                xfree(li[i]);
-       item_free(&item);
-
 }
 
 static void
 ldif_convert(ldif_item item, char *type, char *value)
 {
-       int i;
-
+       /* this is the first (mandatory) attribute to expected
+          from a new valid LDIF record.
+          The previous record must be added to the database before
+          we can go further with the new one */
        if(!strcmp(type, "dn")) {
                ldif_add_item(item);
                return;
        }
 
-       for(i=0; i < LDIF_ITEM_FIELDS - 1; i++) {
-                if(!strcasecmp(ldif_field_names[i], type) && *value) {
-                       item_fput(item, i, xstrdup(value));
-                       break;
+       int i, index;
+
+       for( i=0; i < LDIF_IMPORTABLE_ITEM_FIELDS; i++ ) {
+
+               if( *value &&                                           // there's a value for the attr provided
+                   ldif_conv_table[i].key &&                           // there exists an ldif attr name...
+                   !strcasecmp(ldif_conv_table[i].key, type)) {        // ...matching that provided at input
+
+                       assert((i >= 0) && (i < LDIF_ITEM_FIELDS));
+                       // which abook field this attribute's name maps to ?
+                       index = ldif_conv_table[i].index;
+                       assert((index >= 0) && (index < LDIF_ITEM_FIELDS));
+
+                       /* TODO: here must be handled multi-valued cases
+                          (first or latest win, append or prepend values, ...)
+                          Currently: emails are appended, for other fields the
+                          first attribute found wins.
+                          Eg: the value of "mobile" will be taken into
+                          account if such a line comes before "cellphone". */
+
+                       /* Remember: LDIF_ITEM_FIELDS > ITEM_FIELDS,
+                          lower (common) indexes of *ldif_item map well to *list_item.
+                          We can use item_fput()... */
+                       if(index < ITEM_FIELDS) {
+                               // multiple email support, but two only will stay
+                               // in the final *list_item
+                               if(index == EMAIL && item_fget(item, EMAIL)) {
+                                       item_fput(item,
+                                                 EMAIL,
+                                                 strconcat(item_fget(item, EMAIL), ",", value, 0));
+                               }
+                               else {
+                                       /* Don't override already initialized fields:
+                                          This is the rule of the "first win" */
+                                       if(! item_fget(item, index))
+                                               item_fput(item, index, xstrdup(value));
+                               }
+                       }
+
+                       /* ... but if the ldif field's name index is higher
+                          than what standards abook fields struct can hold,
+                          these extra indexes must be managed manually.
+                          This is the case of LDIF_OBJECTCLASS ("objectclass" attr) */
+                       else {
+                               item[index] = xstrdup(value);
+                       }
+
+                       // matching attr found and field filled:
+                       // no further attr search is needed for `type`
+                       break;
                }
        }
 }
@@ -647,6 +769,11 @@ ldif_parse_file(FILE *handle)
        char *next_line = NULL;
        char *type, *value;
        int vlen;
+
+       /* This is our extended fields holder to put the values from
+          successfully parsed LDIF attributes.
+          ldif_item item is temporary. When the end of an entry is reached,
+          values are copied into a regular *list_item struct, see ldif_add_item() */
        ldif_item item;
 
        memset(item, 0, sizeof(item));
@@ -662,31 +789,17 @@ ldif_parse_file(FILE *handle)
                        continue; /* just skip the errors */
                }
 
-               ldif_fix_string(value);
-
                ldif_convert(item, type, value);
 
                xfree(line);
        } while ( !feof(handle) );
 
+       // force registration (= ldif_add_item()) of the last LDIF entry
        ldif_convert(item, "dn", "");
 
        return 0;
 }
 
-static void
-ldif_fix_string(char *str)
-{
-       int i, j;
-
-       for(i = 0, j = 0; j < (int)strlen(str); i++, j++)
-               str[i] = ( str[j] == (char)0xc3 ?
-                               (char) str[++j] + (char) 0x40 :
-                               str[j] );
-
-       str[i] = 0;
-}
-
 /*
  * end of ldif import
  */
@@ -870,6 +983,7 @@ static int
 ldif_export_database(FILE *out, struct db_enumerator e)
 {
        char email[MAX_EMAILSTR_LEN];
+       abook_list *emails, *em;
 
        fprintf(out, "version: 1\n");
 
@@ -880,25 +994,30 @@ ldif_export_database(FILE *out, struct db_enumerator e)
 
                if(*email)
                        tmp = strdup_printf("cn=%s,mail=%s",db_name_get(e.item),email);
+               /* TODO: this may not be enough for a trully "Distinguished" name
+                  needed by LDAP. Appending a random uuid could do the trick */
                else
                        tmp = strdup_printf("cn=%s",db_name_get(e.item));
 
                ldif_fput_type_and_value(out, "dn", tmp);
                free(tmp);
 
-               for(j = 0; j < LDIF_ITEM_FIELDS; j++) {
-                       if(ldif_conv_table[j] >= 0) {
-                               if(ldif_conv_table[j] == EMAIL) {
-                                       if(*email) // don't dump en empty email field
+               for(j = 0; j < ITEM_FIELDS; j++) {
+                       if(j == EMAIL) {
+                               if(*email) {
+                                       tmp = db_email_get(e.item);
+                                       emails = csv_to_abook_list(tmp);
+                                       free(tmp);
+                                       for(em = emails; em; em = em->next)
                                                ldif_fput_type_and_value(out,
-                                                                        ldif_field_names[j],
-                                                                        email);
+                                                                        ldif_field_names[EMAIL],
+                                                                        em->data);
                                }
-                               else if(db_fget(e.item,ldif_conv_table[j]))
-                                       ldif_fput_type_and_value(out,
-                                               ldif_field_names[j],
-                                               db_fget(e.item,
-                                                       ldif_conv_table[j]));
+                       }
+                       else if(db_fget(e.item,j)) {
+                               ldif_fput_type_and_value(out,
+                                                        ldif_field_names[j],
+                                                        db_fget(e.item, j));
                        }
                }
 
@@ -1168,6 +1287,11 @@ pine_export_database(FILE *out, struct db_enumerator e)
  * csv import filter
  */
 
+/* This is used by both allcsv_export_database() and csv_export_common()
+   to distinguish between standard and defined fields.
+   To avoid confusions this should stay > ITEM_FIELDS  */
+#define CUSTOM_FIELD_START_INDEX       (ITEM_FIELDS + 10)
+
 /* FIXME
  * these files should be parsed according to a certain
  * lay out, or the default if layout is not given, at
@@ -1421,7 +1545,7 @@ static char *vcard_fields[] = {
        "FN",                   /* FORMATTED NAME */
        "EMAIL",                /* EMAIL */
        "ADR",                  /* ADDRESS */
-       "ADR",                  /* ADDRESS2 - not used */
+       "ADR",                  /* ADDRESS2 */
        "ADR",                  /* CITY */
        "ADR",                  /* STATE */
        "ADR",                  /* ZIP */
@@ -1433,22 +1557,9 @@ static char *vcard_fields[] = {
        "NICKNAME",             /* NICK */
        "URL",                  /* URL */
        "NOTE",                 /* NOTES */
+       "BDAY",                 /* ANNIVERSARY */
        "N",                    /* NAME: special case/mapping in vcard_parse_line() */
-       NULL                    /* not implemented: ANNIVERSARY, ITEM_FIELDS */
-};
-
-/*
- * mappings between vCard ADR field and abook's ADDRESS
- * see rfc2426 section 3.2.1
- */
-static int vcard_address_fields[] = {
-       -1,                     /* vCard(post office box) - not used */
-       -1,                     /* vCard(the extended address) - not used */
-       2,                      /* vCard(the street address) - ADDRESS */
-       4,                      /* vCard(the locality) - CITY */
-       5,                      /* vCard(the region) - STATE */
-       6,                      /* vCard(the postal code) - ZIP */
-       7                       /* vCard(the country name) - COUNTRY */
+       NULL                    /* ITEM_FIELDS */
 };
 
 enum {
@@ -1534,33 +1645,49 @@ vcard_parse_email(list_item item, char *line)
        }
 }
 
+
+/*
+ * mappings between vCard ADR field and abook's ADDRESS
+ * see rfc2426 section 3.2.1
+ */
 static void
 vcard_parse_address(list_item item, char *line)
 {
-       int i;
-       int k;
        char *value;
-       char *address_field;
 
        value = vcard_get_line_element(line, VCARD_VALUE);
        if(!value)
                return;
 
-       address_field = value;
-       for(i=k=0; value[i]; i++) {
-               if(value[i] == ';') {
-                       value[i] = '\0';
-                       if(vcard_address_fields[k] >= 0) {
-                               item[vcard_address_fields[k]] = xstrdup(address_field);
-                       }
-                       address_field = &value[i+1];
-                       k++;
-                       if((k+1)==(sizeof(vcard_address_fields)/sizeof(*vcard_address_fields)))
-                               break;
-               }
-       }
-       item[vcard_address_fields[k]] = xstrdup(address_field);
-       xfree(value);
+       // vCard(post office box) - not used
+       strsep(&value, ";");
+       if(!value) return;
+
+       // vCard(the extended address)
+       item_fput(item, ADDRESS2, xstrdup(strsep(&value, ";")));
+       if(!value) return;
+
+       // vCard(the street address)
+       item_fput(item, ADDRESS, xstrdup(strsep(&value, ";")));
+       if(!value) return;
+
+       // vCard(the locality)
+       item_fput(item, CITY, xstrdup(strsep(&value, ";")));
+       if(!value) return;
+
+       // vCard(the region)
+       item_fput(item, STATE, xstrdup(strsep(&value, ";")));
+       if(!value) return;
+
+       // vCard(the postal code)
+       item_fput(item, ZIP, xstrdup(strsep(&value, ";")));
+       if(!value) return;
+
+       // vCard(the country name)
+       item_fput(item, COUNTRY, xstrdup(strsep(&value, ";")));
+
+       // support of optional trailing ";" to the ADR field
+       if(value && *value) xfree(value);
 }
 
 static void
@@ -1707,7 +1834,12 @@ csv_export_common(FILE *out, struct db_enumerator e,
                        else if(CSV_IS_SPECIAL(fields[i])) {
                                if(special_func)
                                        (*special_func)(out, e.item, fields[i]);
-                       } else
+                       }
+                       else if(fields[i] >= CUSTOM_FIELD_START_INDEX) {
+                               fprintf(out, "\"%s\"",
+                                       safe_str(db_fget_byid(e.item, fields[i] - CUSTOM_FIELD_START_INDEX)));
+                       }
+                       else
                                /*fprintf(out,(
                        strchr(safe_str(database[e.item][field_idx(fields[i])]), ',') ||
                        strchr(safe_str(database[e.item][field_idx(fields[i])]), '\"')) ?
@@ -1750,7 +1882,7 @@ allcsv_export_database(FILE *out, struct db_enumerator e)
         * TODO: Should get these atomatically from abook_fileds
         *  - JH
         */
-       int allcsv_export_fields[] = {
+       int allcsv_export_fields[ITEM_FIELDS + 6] = { // only the 5 custom fields are allowed so far
                NAME,
                EMAIL,
                ADDRESS,
@@ -1762,7 +1894,7 @@ allcsv_export_database(FILE *out, struct db_enumerator e)
                PHONE,
                WORKPHONE,
                FAX,
-               MOBILEPHONE,
+               MOBILEPHONE, // spelt "mobile" in standard_fields
                NICK,
                URL,
                NOTES,
@@ -1772,23 +1904,47 @@ allcsv_export_database(FILE *out, struct db_enumerator e)
        };
 
        fprintf(out, "#");
-       fprintf(out, "\"NAME\",");
-       fprintf(out, "\"EMAIL\",");
-       fprintf(out, "\"ADDRESS\",");
-       fprintf(out, "\"ADDRESS2\",");
-       fprintf(out, "\"CITY\",");
-       fprintf(out, "\"STATE\",");
-       fprintf(out, "\"ZIP\",");
-       fprintf(out, "\"COUNTRY\",");
-       fprintf(out, "\"PHONE\",");
-       fprintf(out, "\"WORKPHONE\",");
-       fprintf(out, "\"FAX\",");
-       fprintf(out, "\"MOBILEPHONE\",");
-       fprintf(out, "\"NICK\",");
-       fprintf(out, "\"URL\",");
-       fprintf(out, "\"NOTES\",");
-       fprintf(out, "\"ANNIVERSARY\",");
-       fprintf(out, "\"GROUPS\"\n");
+       int i = 0;
+       while(allcsv_export_fields[i+1] != CSV_LAST) {
+               fprintf(out, "\"%s\",", standard_fields[i++].key);
+       }
+       fprintf(out, "\"%s\"", standard_fields[i].key);
+
+       /*
+         Custom fields handling:
+         This loop appends custom fields' id at the end of allcsv_export_fields and shift
+         the CSV_LAST sentinel value each time one is found.
+         CUSTOM_FIELD_START_INDEX is added to these index values so csv_export_common()
+         can later recognize them and call db_fget_byid() instead of the traditional db_fget()
+
+         It only search for defined the [legacy?] "custom" fields.
+       */
+
+       // pointer to the end of the field list
+       int append_field = ITEM_FIELDS;
+       // custom field's trailing number (between 1 and 5)
+       int j;
+       // full custom field name, eg "custom4"
+       char custom_field_key[8];
+       // index used by custom_field_key
+       int field_no;
+       // name of the defined field <field_no> as chosen by the user
+       char *custom_field_name;
+
+       for (j = 1; j <= 5; j++) {
+               snprintf(custom_field_key, 8, "custom%d", j++);
+               if(find_declared_field(custom_field_key)) {
+                       find_field_number(custom_field_key, &field_no);
+                       get_field_info(field_no, NULL, &custom_field_name, NULL);
+                       // append the field to the list
+                       allcsv_export_fields[append_field] = field_no + CUSTOM_FIELD_START_INDEX;
+                       allcsv_export_fields[++append_field] = CSV_LAST;
+                       // print column name
+                       fprintf(out, ",\"%s\"", custom_field_name);
+               }
+       }
+       free(custom_field_name);
+       fprintf(out, "\n");
 
        csv_export_common(out, e, allcsv_export_fields, NULL);
 
@@ -1891,7 +2047,7 @@ vcard_export_database(FILE *out, struct db_enumerator e)
 void
 vcard_export_item(FILE *out, int item)
 {
-       int j;
+       int j, email_no;
        char *name, *tmp;
        abook_list *emails, *em;
        fprintf(out, "BEGIN:VCARD\r\nFN:%s\r\n",
@@ -1910,15 +2066,25 @@ vcard_export_item(FILE *out, int item)
 
        free(name);
 
-       if(db_fget(item, ADDRESS))
-         fprintf(out, "ADR:;;%s;%s;%s;%s;%s;%s\r\n",
-                 safe_str(db_fget(item, ADDRESS)),
-                 safe_str(db_fget(item, ADDRESS2)),
-                 safe_str(db_fget(item, CITY)),
-                 safe_str(db_fget(item, STATE)),
-                 safe_str(db_fget(item, ZIP)),
-                 safe_str(db_fget(item, COUNTRY))
-                 );
+       if(db_fget(item, NICK))
+         fprintf(out, "NICKNAME:%s\r\n",
+                 safe_str(db_fget(item, NICK)));
+       if(db_fget(item, ANNIVERSARY))
+         fprintf(out, "BDAY:%s\r\n",
+                 safe_str(db_fget(item, ANNIVERSARY)));
+
+       // see rfc6350 section 6.3.1
+       if(db_fget(item, ADDRESS)) {
+               fprintf(out, "ADR:;%s;%s;%s;%s;%s;%s\r\n",
+                       // pobox (unsupported)
+                       safe_str(db_fget(item, ADDRESS2)), // ext (n°, ...)
+                       safe_str(db_fget(item, ADDRESS)), // street
+                       safe_str(db_fget(item, CITY)), // locality
+                       safe_str(db_fget(item, STATE)), // region
+                       safe_str(db_fget(item, ZIP)), // code (postal)
+                       safe_str(db_fget(item, COUNTRY)) // country
+                       );
+       }
 
        if(db_fget(item, PHONE))
          fprintf(out, "TEL;HOME:%s\r\n",
@@ -1936,9 +2102,10 @@ vcard_export_item(FILE *out, int item)
        tmp = db_email_get(item);
        if(*tmp) {
          emails = csv_to_abook_list(tmp);
-
-         for(em = emails; em; em = em->next)
-           fprintf(out, "EMAIL;INTERNET:%s\r\n", em->data);
+         fprintf(out, "EMAIL;PREF;INTERNET:%s\r\n", emails->data);
+         email_no = 1;
+         for(em = emails->next; em; em = em->next, email_no++ )
+                 fprintf(out, "EMAIL;%d;INTERNET:%s\r\n", email_no, em->data);
 
          abook_list_free(&emails);
        }
@@ -2368,37 +2535,13 @@ bsdcal_export_database(FILE *out, struct db_enumerator e)
        return 0;
 }
 
-// see enum field_types @database.h
-static char *conv_table[] = {
-  "name",
-  "email",
-  "address",
-  "address2",
-  "city",
-  "state",
-  "zip",
-  "country",
-  "phone",
-  "workphone",
-  "fax",
-  "mobile",
-  "nick",
-  "url",
-  "notes",
-  "anniversary",
-  0 /* ITEM_FIELDS */
-};
-
 static int
 find_field_enum(char *s) {
-  int i = 0;
-  while (conv_table[i]) {
-    if(!safe_strcmp(conv_table[i], s))
-      return i;
-    i++;
-  }
-  // failed
-  return -1;
+       int i = -1;
+       while(standard_fields[++i].key)
+               if(!strcmp(standard_fields[i].key, s))
+                       return i;
+       return -1;
 }
 
 /* Convert a string with named placeholders to
@@ -2412,17 +2555,17 @@ parse_custom_format(char *s, char *fmt_string, enum field_types *ft)
          exit(EXIT_FAILURE);
        }
 
+       char tmp[1] = { 0 };
        char *p, *start, *field_name = NULL;
        p = start = s;
 
        while(*p) {
                if(*p == '{') {
                  start = ++p;
+
+                 if(! *start) goto cannotparse;
                  p = strchr(start, '}');
-                 if(! *p) {
-                   fprintf(stderr, _("parse_custom_format: invalid format\n"));
-                   exit(EXIT_FAILURE);
-                 }
+                 if(! p) goto cannotparse;
                  strcat(fmt_string, "%s");
                  field_name = strndup(start, (size_t)(p-start));
                  *ft = find_field_enum(field_name);
@@ -2432,23 +2575,53 @@ parse_custom_format(char *s, char *fmt_string, enum field_types *ft)
                  }
 
                  ft++;
-                 p++;
-                 start = p;
-               } else {
+                 start = ++p;
+               }
+
+               else if(*p == '\\') {
+                       ++p;
+                       if(! *p) tmp[0] = '\\'; // last char is a '\' ?
+                       else if(*p == 'n') *tmp = '\n';
+                       else if(*p == 't') *tmp = '\t';
+                       else if(*p == 'r') *tmp = '\r';
+                       else if(*p == 'v') *tmp = '\v';
+                       else if(*p == 'b') *tmp = '\b';
+                       else if(*p == 'a') *tmp = '\a';
+                       else *tmp = *p;
+                       strncat(fmt_string, tmp, 1);
+                       start = ++p;
+               }
+
+               // if no '\' following: quick mode using strchr/strncat
+               else if(! strchr(start, '\\')) {
                  p = strchr(start, '{');
-                 if(p && *p) {
+                 if(p) { // copy until the next placeholder
                    strncat(fmt_string, start, (size_t)(p-start));
                    start = p;
                  }
-                 else {
+                 else { // copy till the end
                    strncat( fmt_string,
                             start,
                             FORMAT_STRING_LEN - strlen(fmt_string) - 1 );
                    break;
                  }
                }
+
+               // otherwise character by character
+               else {
+                       strncat(fmt_string, p, 1);
+                       start = ++p;
+               }
        }
-       *ft = 66;
+
+       *ft = ITEM_FIELDS;
+       return;
+
+ cannotparse:
+       fprintf(stderr, _("%s: invalid format, index %ld\n"), __FUNCTION__, (start - s));
+       free(fmt_string);
+       while(*ft) free(ft--);
+       exit(EXIT_FAILURE);
 }
 
 static int
@@ -2481,7 +2654,7 @@ custom_export_item(FILE *out, int item, char *fmt, enum field_types *ft)
   // we first check that all fields exist before continuing
   if(*fmt == '!') {
     enum field_types *ftp = ft;
-    while(*ft != 66) {
+    while(*ft != ITEM_FIELDS) {
       if(! db_fget(item, *ft) )
        return 1;
       ft++;
@@ -2495,7 +2668,7 @@ custom_export_item(FILE *out, int item, char *fmt, enum field_types *ft)
       fprintf(out, "%s", safe_str(db_fget(item, *ft)));
       ft++;
       fmt+=2;
-    } else if (*ft == 66) {
+    } else if (*ft == ITEM_FIELDS) {
       fprintf(out, "%s", fmt);
       return 0;
     } else {