/* $Cambridge: hermes/src/prayer/session/abook.c,v 1.3 2008/09/16 09:59:58 dpc22 Exp $ */
/************************************************
 *    Prayer - a Webmail Interface              *
 ************************************************/

/* Copyright (c) University of Cambridge 2000 - 2008 */
/* See the file NOTICE for conditions of use and distribution. */

#include "prayer_session.h"

/* abook_create() *******************************************************
 *
 * Create a default abook structure.
 * Retuens: new addressbook structure
 ***********************************************************************/

struct abook *abook_create()
{
    struct pool *pool = pool_create(ABOOK_POOL_SIZE);
    struct abook *abook;

    abook = pool_alloc(pool, sizeof(struct abook));
    abook->pool = pool;
    abook->list = list_create(pool, NIL);
    abook->hash = assoc_create(pool, 64, NIL);
    abook->sort = NULL;
    abook->sort_mode    = ABOOK_SORT_ORDER;
    abook->sort_reverse = NIL;
    abook->unnamed = 0;
    abook->current = 0;
    return (abook);
}

void abook_free(struct abook *abook)
{
    pool_free(abook->pool);
}

/* ====================================================================== */

/* abook_parse_line() ***************************************************
 *
 * Convert a line (from the preferences file) into an addressbook entry
 *   abook: Addressbook to add to
 *   line:  (URL encoded) line to decode
 * session: For logging purposes only
 ***********************************************************************/

void
abook_parse_line(struct abook *abook, char *line, struct session *session)
{
    char *alias, *name, *fcc, *comment, *email;

    if (!strncmp(line, "__UNNAMED__ ", strlen("__UNNAMED__ "))) {
        abook->unnamed = atoi(line + strlen("__UNNAMED__ "));
        return;
    }

    if ((alias = options_decode(string_get_token(&line))) &&
        (name = options_decode(string_get_token(&line))) &&
        (fcc = options_decode(string_get_token(&line))) &&
        (comment = options_decode(string_get_token(&line))) &&
        (email = options_decode(string_get_token(&line))))
        abook_add(abook, alias, name, fcc, comment, email);
}

/* abook_print_options() ************************************************
 *
 * Convert addressbook into printable representation for prefs file.
 *   Addressbook to dump
 *   Target buffer
 ***********************************************************************/

void abook_print_options(struct abook *abook, struct buffer *b)
{
    struct list_item *li;

    if (abook->unnamed > 0)
        bprintf(b, "__UNNAMED__ %lu" CRLF, abook->unnamed);

    for (li = abook->list->head; li; li = li->next) {
        struct abook_entry *abe = (struct abook_entry *) li;
        unsigned long offset = 0L;

        options_print_token(b, abe->alias, &offset);
        options_print_token(b, abe->name, &offset);
        options_print_token(b, abe->fcc, &offset);
        options_print_token(b, abe->comment, &offset);
        options_print_token(b, abe->email, &offset);
        bputs(b, "" CRLF);
    }
}

/* ====================================================================== */

/* Local support routines */

static char *maybe_strdup(struct pool *p, char *s)
{
    return ((s) ? pool_strdup(p, s) : "");
}

/* abook_add() **********************************************************
 *
 * Add entry to addressbook.
 *   abook:   Addressbook
 *   alias:   Alias for new abook entry
 *   name:    Personal name for addressbook entry
 *   fcc:     Fcc field (not yet used, however is recorded)
 *   comment: Addressbook comment field
 *   email:   Email address (can be comment separated list)
 ***********************************************************************/

void
abook_add(struct abook *abook, char *alias, char *name,
          char *fcc, char *comment, char *email)
{
    struct pool *p;
    struct abook_entry *abe;

    p = abook->pool;
    abe = pool_alloc(p, sizeof(struct abook_entry));

    abe->next = NIL;
    abe->used = NIL;

    if (alias && alias[0])
        abe->alias = maybe_strdup(p, alias);
    else
        abe->alias = pool_printf(p, "unnamed_%d", ++abook->unnamed);

    abe->name = maybe_strdup(p, name);
    abe->fcc = maybe_strdup(p, fcc);
    abe->comment = maybe_strdup(p, comment);
    abe->email = maybe_strdup(p, email);
    abe->position = 0;

    list_push(abook->list, (struct list_item *) abe, abe->alias);
    assoc_update(abook->hash, pool_strdup(p, alias), abe, NIL);

    abook_clear_sort(abook);
}

/* abook_replace() ******************************************************
 *
 * Replace existing entry to addressbook. (Not found => abook_add)
 *   abook:   Addressbook
 *   alias:   Alias for new abook entry
 *   name:    Personal name for addressbook entry
 *   fcc:     Fcc field (not yet used, however is recorded)
 *   comment: Addressbook comment field
 *   email:   Email address (can be comment separated list)
 ***********************************************************************/

void
abook_replace(struct abook *abook, char *alias, char *name,
              char *fcc, char *comment, char *email)
{
    struct pool *p = abook->pool;
    struct list_item *li;

    if (alias && alias[0]) {
        for (li = abook->list->head; li; li = li->next) {
            struct abook_entry *abe = (struct abook_entry *) li;

            if (!strcasecmp(abe->alias, alias)) {
                abe->alias = maybe_strdup(p, alias);
                abe->name = maybe_strdup(p, name);
                abe->fcc = maybe_strdup(p, fcc);
                abe->comment = maybe_strdup(p, comment);
                abe->email = maybe_strdup(p, email);
                return;
            }
        }
    }
    abook_add(abook, alias, name, fcc, comment, email);

    abook_clear_sort(abook);
}

/* abook_remove() *******************************************************
 *
 * Remove entry from addressbook
 *   abook:   Addressbook
 *   alias:   Alias to be removed
 ***********************************************************************/

void abook_remove(struct abook *abook, char *alias)
{
    assoc_delete(abook->hash, alias);
    list_remove_byname(abook->list, alias);

    abook_clear_sort(abook);
}

/* abook_lookup() *******************************************************
 *
 * Lookup addressbook entry.
 *   abook:   Addressbook
 *   alias:   Alias to look up
 ***********************************************************************/

struct abook_entry *abook_lookup(struct abook *abook, char *alias)
{
    return ((struct abook_entry *) assoc_lookup(abook->hash, alias));
}

/* abook_lookup_byoffset() **********************************************
 *
 * Lookup addressbook entry.
 *   abook:   Addressbook
 *   offset:  Offset into list in the range 0..max-1
 ***********************************************************************/

struct abook_entry *abook_lookup_byoffset(struct abook *abook,
                                          unsigned long offset)
{
    return ((struct abook_entry *)
            list_lookup_byoffset(abook->list, offset));
}

/* abook_lookup_sorted_byoffset() ***************************************
 *
 * Lookup addressbook entry.
 *   abook:   Addressbook
 *   offset:  Offset into list in the range 0..max-1
 ***********************************************************************/

struct abook_entry *abook_lookup_sorted_byoffset(struct abook *abook,
                                                 unsigned long offset)
{
    if (abook->sort) {
        unsigned long count = list_length(abook->list);

        if (offset >= count)
            return(NIL);

        if (abook->sort_reverse)
            return(abook->sort[count-1-offset]);
        else
            return(abook->sort[offset]);
    } else {
        return ((struct abook_entry *)
                list_lookup_byoffset(abook->list, offset));
    }
}

/* abook_find_email() ***************************************************
 *
 * Look for existing email entry which matches email address.
 *   abook:   Addressbook
 *   email:   Email address to find.
 ***********************************************************************/

struct abook_entry *abook_find_email(struct abook *abook, char *email)
{
    struct abook_entry *abe;

    if (!email)
        return (NIL);

    for (abe = (struct abook_entry *) abook->list->head; abe;
         abe = abe->next) {
        if (abe->email && !strcmp(abe->email, email))
            return (abe);
    }

    return (NIL);
}

/* ====================================================================== */

/* abook_match() *******************************************************
 *
 * Find out if string value from addressbook matches given criteria.
 * Used as primitive operation by the search routines below.
 * Used by
 *   test: String to be tested
 *   type: Test of test (e.g: string starts with "value")
 *  value: Value to match against test string.
 ***********************************************************************/

static BOOL
abook_match(char *test, abook_search_type_type type, char *value)
{
    BOOL match = NIL;
    char *s, c;

    switch (type) {
    case ABOOK_SEARCH_TYPE_NONE:       /* Treat undefined as match */
        match = T;
        break;
    case ABOOK_SEARCH_TYPE_IS:
        if (!strcasecmp(test, value))
            match = T;
        break;
    case ABOOK_SEARCH_TYPE_BEGINS:
        if (!strncasecmp(test, value, strlen(value)))
            match = T;
        break;
    case ABOOK_SEARCH_TYPE_ENDS:
        if (!strncasecmp
            (test + strlen(test) - strlen(value), value, strlen(value)))
            match = T;
        break;
    case ABOOK_SEARCH_TYPE_CONTAINS:
        c = value[0];
        for (s = test; *s; s++) {
            if (!strncasecmp(s, value, strlen(value))) {
                match = T;
                break;
            }
        }
        break;
    }
    return (match);
}

/* ====================================================================== */

/* abook_search_list_create() *******************************************
 *
 * Create a addressbook search list structure with its own private pool.
 * match_all:  TRUE  => All criteria in list must match
 *             FALSE => Any criteria in list must match
 *
 * Returns: new addressbook search list
 ***********************************************************************/

struct abook_search_list *abook_search_list_create(BOOL match_all)
{
    struct pool *p = pool_create(PREFERRED_ASL_POOL_SIZE);
    struct abook_search_list *asl
        = pool_alloc(p, sizeof(struct abook_search_list));

    asl->pool = p;
    asl->match_all = match_all;
    asl->first = NIL;
    asl->last = NIL;

    return (asl);
}

/* abook_search_list_free() *********************************************
 *
 * Free addressbook search list, including any nodes.
 ***********************************************************************/

void abook_search_list_free(struct abook_search_list *asl)
{
    pool_free(asl->pool);
}

/* abook_search_list_add() **********************************************
 *
 * Add new entry to existing addressbook search list
 *   asl:  Addressbook search list
 *  field: Field to match
 *   type: Type of match
 *  value: Value to match against given field using given type of search
 ***********************************************************************/

void
abook_search_list_add(struct abook_search_list *asl,
                      abook_search_field_type field,
                      abook_search_type_type type, char *value)
{
    struct abook_search_elt *ase
        = pool_alloc(asl->pool, sizeof(struct abook_search_elt));

    ase->next = NIL;
    ase->field = field;
    ase->type = type;
    ase->value = pool_strdup(asl->pool, value);

    if (asl->first) {
        asl->last->next = ase;  /* Add to end of list */
        asl->last = ase;
    } else {
        asl->first = ase;       /* Start new list */
        asl->last = ase;
    }
}

/* ====================================================================== */

/* abook_search_init() *************************************************
 *
 * Initialise addressbook for new search
 *  abook: Addressbook to search
 ***********************************************************************/

void abook_search_init(struct abook *abook)
{
    abook->search = (struct abook_entry *) abook->list->head;
}

/* abook_search_find_next() *********************************************
 *
 * Find next addressbook entry which matches the search list criteria
 *  abook: Addressbook to search (includes info about where to start search)
 *  asl:   Addressbook search list
 *
 * Returns: Next addressbook entry to match criteria, NIL if none.
 ***********************************************************************/

struct abook_entry *abook_search_find_next(struct abook *abook,
                                           struct abook_search_list *asl)
{
    BOOL match = NIL;
    struct abook_entry *abe;
    struct abook_search_elt *ase;

    /* Search from last recorded location */
    for (abe = abook->search; abe; abe = abe->next) {
        for (ase = asl->first; ase; ase = ase->next) {
            switch (ase->field) {
            case ABOOK_SEARCH_FIELD_NONE:      /* Ignore undefined fields */
                continue;
            case ABOOK_SEARCH_FIELD_ALIAS:
                match = abook_match(abe->alias, ase->type, ase->value);
                break;
            case ABOOK_SEARCH_FIELD_NAME:
                match = abook_match(abe->name, ase->type, ase->value);
                break;
            case ABOOK_SEARCH_FIELD_COMMENT:
                match = abook_match(abe->comment, ase->type, ase->value);
                break;
            case ABOOK_SEARCH_FIELD_EMAIL:
                match = abook_match(abe->email, ase->type, ase->value);
                break;
            }
            if (match && !asl->match_all)       /* Only need single match */
                break;

            if (!match && asl->match_all)       /* Only need single failure */
                break;
        }
        if (match)              /* Final match in asl suceeded */
            break;
    }

    /* Set up search engine for the next caller */
    abook->search = (abe) ? abe->next : NIL;

    /* abe points to abook_entry matching all criteria, NIL if none */
    return (abe);
}

/* ====================================================================== */

/* abook_text_to_string() ***********************************************
 *
 * Convert addressbook values to string value in the given pool
 *  pool:  Addressbook to use
 *  name:  Personal name
 *  email: Email address (list)
 *
 * Returns: String form
 ***********************************************************************/

char *abook_text_to_string(struct pool *pool, char *name, char *email)
{
    char *result;

    if (email == NIL)
        return ("");

    /* Just return email string if multiple addresses */
    if (strchr(email, ','))
        return (pool_strdup(pool, email));

    /* Single address */

    if (strchr(email, '<')) {   /* email contains comment */
        result = pool_strdup(pool, email);
    } else if (name[0]) {
        if (string_atom_has_special(name))
            result = pool_printf(pool, "\"%s\" <%s>",
                                 string_atom_quote(pool, name), email);
        else
            result = pool_printf(pool, "%s <%s>", name, email);
    } else
        result = pool_strdup(pool, email);

    return (result);
}

/* abook_entry_to_string() **********************************************
 *
 * Convert addressbook entry to string value in the given pool
 *  pool:  Addressbook to use
 *   abe:  Addressbook entry to convert
 *
 * Returns: String form
 ***********************************************************************/

char *abook_entry_to_string(struct pool *pool, struct abook_entry *abe)
{
    return (abook_text_to_string(pool, abe->name, abe->email));
}

/* ====================================================================== */

/* abook_substitute_recurse() *******************************************
 *
 * Recursive addressbook lookup routine (internal)
 *  session:
 *    abook:
 *        b: Target buffer for string expansion.
 *   string: String to expand
 *     root: Root address for this expansion (for error messages)
 * toplevel: T => top level of lookup: clear reference counts
 *
 * Returns: NIL on error (details sent to session_message).
 ***********************************************************************/

static BOOL
abook_substitute_recurse(struct session *session,
                         struct buffer *b,
                         struct abook *abook,
                         char *string, char *root, BOOL toplevel)
{
    struct config *config = session->config;
    struct options *options = session->options;
    struct prefs *prefs = options->prefs;
    char *default_domain = prefs->default_domain;
    struct request *request = session->request;
    struct list_item *li;
    struct abook_entry *abe;
    ADDRESS *a, *addr = NIL;

    if (toplevel) {
        for (li = abook->list->head; li; li = li->next) {
            abe = (struct abook_entry *) li;
            abe->used = NIL;
        }
    }

    if (!(addr=addr_parse(request->pool, string, ""))) {
        session_message(session, "%s", ml_errmsg());
        return (NIL);
    }

    for (a = addr; a; a = a->next) {
        if (!(a->mailbox && a->mailbox[0]))     /* Impossible? */
            continue;

        if (a->host && a->host[0]) {    /* Component already qualified */
            if (a->personal && a->personal[0]) {
                if (string_atom_has_special(a->personal))
                    bprintf(b, "\"%s\" <%s@%s>",
                            string_atom_quote(request->pool, a->personal),
                            a->mailbox, a->host);
                else
                    bprintf(b, "%s <%s@%s>", a->personal, a->mailbox,
                            a->host);
            } else
                bprintf(b, "%s@%s", a->mailbox, a->host);
        } else if ((abe = abook_lookup(abook, a->mailbox))) {
            if (abe->used) {
                session_message(session,
                                "Addressbook loop involving:\"%s\"", root);
                mail_free_address(&addr);
                return (NIL);
            }
            abe->used = T;
            string = abook_entry_to_string(request->pool, abe);
            if (!abook_substitute_recurse
                (session, b, abook, string, root, NIL)) {
                mail_free_address(&addr);
                return (NIL);
            }
        } else {                /* Nothing in personal addressbook */
            if (a->personal && a->personal[0]) {
                if (string_atom_has_special(a->personal))
                    bprintf(b, "\"%s\" <%s@%s>",
                            string_atom_quote(request->pool, a->personal),
                            a->mailbox, default_domain);
                else
                    bprintf(b, "%s <%s@%s>", a->personal, a->mailbox,
                            default_domain);
            } else if (config->local_domain_list) {
                struct list_item *li;
                char *value, *v2;

                for (li = config->local_domain_list->head; li;
                     li = li->next) {
                    struct config_local_domain *cld =
                        (struct config_local_domain *) li;

                    if (cld->cdb_map && !strcmp(default_domain, cld->name)) {
                        if (cdb_find
                            (cld->cdb_map, a->mailbox, strlen(a->mailbox),
                             &v2)) {
                            if (v2 && v2[0]
                                && (value = string_trim_whitespace(v2))) {
                                if (string_atom_has_special(value))
                                    bprintf(b, "\"%s\" <%s@%s>",
                                            string_atom_quote(request->
                                                              pool, value),
                                            a->mailbox, default_domain);
                                else
                                    bprintf(b, "%s <%s@%s>", value,
                                            a->mailbox, default_domain);
                                free(v2);
                            } else
                                bprintf(b, "%s@%s", a->mailbox,
                                        default_domain);
                            break;
                        } else {
                            mail_free_address(&addr);
                            session_message(session,
                                            "Address \"%s\" invalid",
                                            root);
                            return (NIL);
                        }
                    }
                }
                if (li == NIL)  /* Not found */
                    bprintf(b, "%s@%s", a->mailbox, default_domain);
            } else
                bprintf(b, "%s@%s", a->mailbox, default_domain);
        }

        if (a->next)
            bputs(b, ", ");
    }
    mail_free_address(&addr);
    return (T);
}

/* ====================================================================== */

/* abook_substitute() ***************************************************
 *
 * Expand email list through addressbook lookup
 *  session:
 *     pool: Target pool for expansion nominated by client
 *    abook:
 *   string: String to expand
 *
 * Returns: String form
 ***********************************************************************/

char *abook_substitute(struct session *session,
                       struct pool *pool, struct abook *abook,
                       char *string)
{
    struct config *config = session->config;
    struct request *request = session->request;
    struct buffer *b = buffer_create(pool, 4096);
    ADDRESS *a, *addr = NIL;

    if (!(addr=addr_parse(request->pool, string, ""))) {
        session_message(session, "%s", ml_errmsg());
        return (NIL);
    }

    /* Treat each address as new toplevel entity */
    for (a = addr; a; a = a->next) {
        if (!(a->mailbox && a->mailbox[0]))     /* Impossible? */
            continue;

        if (a->host && a->host[0]) {
            /* Name already qualified: don't try addressbook lookup */
            if (a->personal && a->personal[0]) {
                /* Make sure that personal name is properly quoted */
                if (string_atom_has_special(a->personal))
                    bprintf(b, "\"%s\" <%s@%s>",
                            string_atom_quote(request->pool, a->personal),
                            a->mailbox, a->host);
                else
                    bprintf(b, "%s <%s@%s>", a->personal, a->mailbox,
                            a->host);
            } else if (config->local_domain_list) {
                /* No personal name provided: try and find one */
                struct list_item *li;
                char *value, *v2;

                for (li = config->local_domain_list->head; li;
                     li = li->next) {
                    struct config_local_domain *cld =
                        (struct config_local_domain *) li;

                    if (cld->cdb_map && !strcmp(a->host, cld->name) &&
                        cdb_find(cld->cdb_map, a->mailbox,
                                 strlen(a->mailbox), &v2) && v2 && v2[0]
                        && (value = string_trim_whitespace(v2))) {
                        if (string_atom_has_special(value))
                            bprintf(b, "\"%s\" <%s@%s>",
                                    string_atom_quote(request->pool,
                                                      value), a->mailbox,
                                    a->host);
                        else
                            bprintf(b, "%s <%s@%s>", value, a->mailbox,
                                    a->host);
                        free(v2);
                        break;
                    }
                }
                if (li == NIL)  /* Not found */
                    bprintf(b, "%s@%s", a->mailbox, a->host);
            } else
                bprintf(b, "%s@%s", a->mailbox, a->host);
        } else if (!abook_substitute_recurse(session, b, abook,
                                             a->mailbox, a->mailbox, T)) {
            mail_free_address(&addr);
            return (NIL);
        }
        if (a->next)
            bputs(b, ", ");
    }

    mail_free_address(&addr);
    return (buffer_fetch(b, 0, buffer_size(b), NIL));
}

/* ====================================================================== */

/* abook_check_loop() ***************************************************
 *
 * Check for loops in addressbook (cmd_abook_entry and cmd_abook_update)
 *     pool: Target (scratch) pool for expansion nominated by client
 *    abook:
 *   string: String to expand
 * toplevel: Toplevel of recursion.
 *
 * Returns: String form
 ***********************************************************************/

BOOL
abook_check_loop(struct pool * pool,
                 struct abook * abook, char *string, BOOL toplevel)
{
    struct list_item *li;
    struct abook_entry *abe;
    ADDRESS *a, *addr = NIL;

    if (toplevel) {
        for (li = abook->list->head; li; li = li->next) {
            abe = (struct abook_entry *) li;
            abe->used = NIL;
        }
    }

    if (!(addr=addr_parse(pool, string, "")))
        return (NIL);

    for (a = addr; a; a = a->next) {
        if (!(a->mailbox && a->mailbox[0]))     /* Impossible? */
            continue;

        if (a->host && a->host[0])
            continue;

        if (!(abe = abook_lookup(abook, a->mailbox)))
            continue;

        /* If we get this far then "a->mailbox" is live addressbook alias */

        if (abe->used) {
            mail_free_address(&addr);
            return (NIL);
        }
        abe->used = T;

        if (!(abook_check_loop(pool, abook, abe->email, NIL))) {
            mail_free_address(&addr);
            return (NIL);
        }
    }
    mail_free_address(&addr);
    return (T);
}

/* ====================================================================== */

/* abook_import_pine_get_entry() ****************************************
 *
 * Isolate a single addressbook entry from a string. Almost but not the
 * same action as string_get_lws_line():
 *   1) Lines are folded using one or more SP characters at the start of
 *      a line.
 *   2) Entry terminates if next line starts with character != SP,
 *      including a tab character.
 *
 * Returns: Ptr to this line
 ***********************************************************************/

static char *abook_import_pine_get_entry(char **sp)
{
    char *s, *t, *result;
    BOOL quoted = NIL;

    s = *sp;

    if (!(s && s[0]))
        return (NIL);

    /* Skip leading whitespace */
    while ((*s == ' '))
        s++;

    /* Empty string before data proper starts? */
    if ((s[0] == '\0')) {
        *sp = s;
        return (s);
    }

    /* CR, LF or CRLF before data proper starts? */
    if ((s[0] == '\015') || (s[0] == '\012')) {
        result = s;
        if ((s[0] == '\015') && (s[1] == '\012')) {     /* CRLF */
            *s = '\0';
            s += 2;
        } else
            *s++ = '\0';        /* CR or LF */

        *sp = s;
        return (result);
    }

    result = t = s;             /* Record position of non-LWS */

    while (*s) {
        if ((*s == '\015') || (*s == '\012')) { /* CR, LF or CRLF */
            s += ((s[0] == '\015') && (s[1] == '\012')) ? 2 : 1;

            if ((*s != ' '))
                break;
            /* Is a continuation line */
            quoted = NIL;       /* Is "LWS" allowed? */
            if ((t > result) && (t[-1] != ' ')) {       /* Replace LWS with SP */
                *t++ = ' ';
            }
        } else {
            if (*s == '"')
                quoted = (quoted) ? NIL : T;    /* Toggle quoted bit */

            if (t < s)          /* Copy faithfully */
                *t++ = *s++;
            else {
                t++;
                s++;            /* Data hasn't moved yet */
            }
        }
    }

    *t = '\0';                  /* Tie off result string */
    *sp = s;                    /* Set up for next caller */

    return (result);
}

/* abook_import_pine_get_token() *****************************************
 *
 * Extract single token from Pine addressbok entry
 ************************************************************************/

static char *abook_import_pine_get_token(char **sp)
{
    char *s = *sp, *result;

    while ((*s == ' '))
        s++;

    /* Record position of this token */
    result = s;

    /* Find tab character or end of string */
    while ((*s) && (*s != '\t'))
        s++;

    /* Tie off the string unless \0 already reached */
    if (*s) {
        *s++ = '\0';
        while ((*s == ' '))
            s++;
    }

    /* Record position of first non-whitespace character for next caller */
    *sp = s;

    return (result);
}

/* abook_import_pine_valid() *********************************************
 *
 * Test for valid Pine addressbook to best of our ability. Looking for
 * >= 2 TAB characters in first LWS block + lack of binary data.
 *
 ************************************************************************/

BOOL abook_import_pine_valid(char *t)
{
    unsigned long tab_count = 0;
    unsigned char *s = (unsigned char *) t;
    unsigned char c;

    if (!strncmp
        (t, "# Prayer addressbook", strlen("# Prayer addressbook")))
        return (T);

    while ((c = *s++)) {
        if ((c == '\015') || (c == '\012')) {   /* CR or LF */
            if ((c == '\015') && (*s == '\012'))        /* CRLF */
                s++;
            if (*s != ' ')      /* Check for continuation line */
                break;
        } else if ((c == '\t'))
            tab_count++;
        else if (c > 0x80)
            return (NIL);
    }

    return ((tab_count >= 2) ? T : NIL);
}

/* ====================================================================== */

/* abook_import_pine() ***************************************************
 *
 * Import PINE format addressbook
 *    abook: Addressbook to import to
 *    text:  PINE format addressbook to import
 *
 * Returns: Number of abook entries imported
 ************************************************************************/

/* Import Pine format addressbook */

unsigned long abook_import_pine(struct abook *abook, char *text)
{
    struct pool *pool = pool_create(1024);
    char *line;
    char *alias;
    char *email;
    char *fcc;
    char *comment;
    char *name;
    char *s;
    unsigned long count = 0;

    while ((line = abook_import_pine_get_entry(&text))) {
        if (line[0] == '#')
            continue;

        if (line[0] == '\0')
            continue;

        alias = abook_import_pine_get_token(&line);
        name = abook_import_pine_get_token(&line);
        email = abook_import_pine_get_token(&line);
        fcc = abook_import_pine_get_token(&line);
        comment = abook_import_pine_get_token(&line);

        if ((s = strchr(name, ','))) {
            char *first, *last;
            /* "Last, First" */
            *s++ = '\0';
            last = name;
            first = s;
            name = pool_printf(pool, "%s %s", first, last);
        }

        if (email && (email[0] == '(')) {
            unsigned long len = strlen(email);

            if ((len >= 2) && (email[len - 1] == ')')) {
                /* Prune away leading '(' and trailing ')' */
                email[len - 1] = '\0';
                email++;
            }
        }

        alias = (alias) ? string_trim_whitespace(alias) : "";
        name = (name) ? string_trim_whitespace(name) : "";
        fcc = (fcc) ? string_trim_whitespace(fcc) : "";
        comment = (comment) ? string_trim_whitespace(comment) : "";
        email = (email) ? string_trim_whitespace(email) : "";

        abook_replace(abook, alias, name, fcc, comment, email);
        count++;
    }

    return (count);
}

/* ====================================================================== */

/* abook_import_csv_valid() **********************************************
 *
 * Test for valid CSV addressbook to best of our ability. First line
 * should include some of: Name,Nickname,E-mail Address.
 *
 ************************************************************************/

BOOL
abook_import_csv_valid(char *s)
{
    char *t;

    if (!(t=strchr(s, '\n')))
        return(NIL);

    *t = '\0';

    if (strstr(s, "E-mail Address") && strstr(s, "Name")) {
        *t = '\n';
        return(T);
    }

    *t = '\n';
    return(NIL);
}

/* abook_import_csv() ****************************************************
 *
 * Import CSV format addressbook
 *    abook: Addressbook to import to
 *    text:  CSV format addressbook to import
 *
 * Returns: Number of abook entries imported
 ************************************************************************/

/* Small utility function to break CSV lines into "tokens". Possible
 * formats: a,b,c, a,"d, fgg,",c */

static char *csv_item(char **sp)
{
    char *s = *sp;
    char *t;

    if ((sp == NIL) || ((s=*sp) == NIL))
        return(NIL);

    if (*s == '"') {
        s++;
        if ((t=strstr(s, "\","))) {
            *t++ = '\0'; t++;
            *sp = t;
        } else if ((t=strchr(s, '"'))) {
            *t++ = '\0';
            *sp = t;
        } else
            *sp = NIL;

        return(string_trim_whitespace(s));
    }

    if ((t=strchr(s, ','))) {
        *t++ = '\0';
        *sp = t;
        return(string_trim_whitespace(s));
    } else
        *sp = NIL;

    return(string_trim_whitespace(s));
}

unsigned long
abook_import_csv(struct abook *abook, char *text)
{
    char *line, *item;
    unsigned long count = 0;
    int name_offset  = (-1);
    int nick_offset  = (-1);
    int email_offset = (-1);
    int offset = 0;

    if (!(line=string_get_line(&text)))
        return(0);

    offset = 0;
    while ((item=csv_item(&line))) {
        if (!strcmp(item, "Name"))
            name_offset = offset;
        else if (!strcmp(item, "Nickname"))
            nick_offset = offset;
        else if (!strcmp(item, "E-mail Address"))
            email_offset = offset;
        offset++;
    }

    if ((name_offset < 0) || (email_offset < 0))  /* Not a valid format */
        return(0);

    while ((line = string_get_line(&text))) {
        char *alias = "";
        char *email = "";
        char *name = "";

        if (line[0] == '\0')
            continue;
    
        offset = 0;
        while ((item=csv_item(&line))) {
            if (offset == name_offset)
                name = item;
            else if (offset == nick_offset)
                alias = item;
            else if (offset == email_offset)
                email = item;
            offset++;
        }

        /* abook_add() will make up alias if not provided */
        if (email[0]) {
            abook_replace(abook, alias, name, "", "", email);
            count++;
        }
    }
    return(count);
}


/* ====================================================================== */

/* Support functions for abook_export_buffer and abook_export_text */

static void bputc_tab(struct buffer *b, unsigned long *offsetp)
{
    bputc(b, '\t');
    *offsetp = 8 * ((*offsetp + 8) % 8);        /* Locate next tab stop position */
}

static void bputs_offset(struct buffer *b, unsigned long *offsetp, char *s)
{
    unsigned long len;

    if (!s)
        s = "";

    len = strlen(s);

    if ((*offsetp + len) > 76) {
        /* Fold this line */
        bputs(b, "\n   ");
        bputs(b, s);
        *offsetp = len + 3;
    } else {
        bputs(b, s);
        *offsetp += len;
    }
}

static void
bputs_offset_email_list(struct buffer *b, unsigned long *offsetp, char *s)
{
    char *t;

    if (!s)
        s = "";

    while (*s == ' ')
        s++;

    if ((*offsetp + strlen(s)) < 76) {
        /* Will fit on this line without any messing around */
        bputs_offset(b, offsetp, s);
        return;
    }

    /* Print email address one component at a time
     * Hopefully means that line wrap will occur in some sensible place!
     */
    while ((t = strchr(s, ','))) {
        while (*s == ' ')
            s++;

        /* Print this address */
        *t = '\0';
        bputs_offset(b, offsetp, s);
        bputs_offset(b, offsetp, ",");
        *t = ',';

        /* Next address */
        s = t + 1;
    }

    while (*s == ' ')
        s++;

    /* Print remaining string if any */
    if (s)
        bputs_offset(b, offsetp, s);
}

/* abook_export_pine() ***************************************************
 *
 * Export PINE format addressbook to buffer
 *   abook: Addressbook to export
 *    pool: Pool to allocate from
 ************************************************************************/

void abook_export_pine(struct abook *abook, struct buffer *b)
{
    struct list_item *li;
    struct pool *pool = b->pool;

    for (li = abook->list->head; li; li = li->next) {
        struct abook_entry *abe = (struct abook_entry *) li;
        unsigned long offset = 0;
        char *name;

        bputs_offset(b, &offset, abe->alias);
        bputc_tab(b, &offset);

        name = (abe->name && abe->name[0]) ? abe->name : "";

        bputs_offset(b, &offset, name);
        bputc_tab(b, &offset);

        if (strchr(abe->email, ',')) {
            name = pool_strcat3(pool, "(", abe->email, ")");
            bputs_offset_email_list(b, &offset, name);
            bputs_offset(b, &offset, "\t");
        } else {
            bputs_offset(b, &offset, abe->email);
            bputc_tab(b, &offset);
        }

        bputs_offset(b, &offset, abe->fcc);
        bputc_tab(b, &offset);

        bputs_offset(b, &offset, abe->comment);
        bputc(b, '\n');
    }
}

/* abook_export_csv() ****************************************************
 *
 * Export CSV (Outlook format) addressbook to buffer
 *   abook: Addressbook to export
 *    pool: Pool to allocate from
 ************************************************************************/

void abook_export_csv(struct abook *abook, struct buffer *b)
{
    struct list_item *li;

    bputs(b, "Name,Nickname,E-mail Address"CRLF);

    for (li = abook->list->head; li; li = li->next) {
        struct abook_entry *abe = (struct abook_entry *) li;
        char *name;

        name = (abe->name && abe->name[0]) ? abe->name : "";

        if (strchr(abe->email, ','))
            bprintf(b, "%s,%s,\"%s\""CRLF, name, abe->alias, abe->email);
        else
            bprintf(b, "%s,%s,%s"CRLF, name, abe->alias, abe->email);
    }
}

/* abook_export_text() ***************************************************
 *
 * Export addressbook to string
 *   abook: Addressbook to export
 *    pool: Pool to allocate from
 ************************************************************************/

char *abook_export_text(struct abook *abook, struct pool *pool)
{
    struct buffer *b = buffer_create(pool, 4096);

    abook_export_pine(abook, b);
    return (buffer_fetch(b, 0, buffer_size(b), NIL));
}

/* abook_clear_sort() ****************************************************
 *
 *
 * Sort addressbook on given field:
 *    abook: Addressbook to sort
 *
 ************************************************************************/

void abook_clear_sort(struct abook *abook)
{
    if (abook->sort)
        free(abook->sort);

    abook->sort = NULL;
}

/* abook_set_sort() ******************************************************
 *
 *
 * Define addressbook sort sort
 *     abook: Addressbook to sort
 * sort_mode: Field to sort on
 *   reverse: Reverse sort
 *
 ************************************************************************/

void
abook_set_sort(struct abook *abook, abook_sort_mode sort_mode, BOOL reverse)
{
    abook->sort_mode    = sort_mode;
    abook->sort_reverse = reverse;

    abook_clear_sort(abook);
}
                    
/* Sort support functions */

static int
abook_sort_compare_alias(const void *aptr, const void *bptr)
{
    struct abook_entry *a = *((struct abook_entry **)aptr);
    struct abook_entry *b = *((struct abook_entry **)bptr);

    return(strcasecmp(a->alias, b->alias));
}

static int
abook_sort_compare_comment(const void *aptr, const void *bptr)
{
    struct abook_entry *a = *((struct abook_entry **)aptr);
    struct abook_entry *b = *((struct abook_entry **)bptr);

    return(strcasecmp(a->comment, b->comment));
}

static int
abook_sort_compare_address(const void *aptr, const void *bptr)
{
    struct abook_entry *a = *((struct abook_entry **)aptr);
    struct abook_entry *b = *((struct abook_entry **)bptr);

    return(strcasecmp(a->email, b->email));
}

static int
abook_sort_compare_name(const void *aptr, const void *bptr)
{
    struct abook_entry *a = *((struct abook_entry **)aptr);
    struct abook_entry *b = *((struct abook_entry **)bptr);
    char *s;
    char *firstname1, *lastname1;
    char *firstname2, *lastname2;
    int   comp;

    /* Attempt to extract firstname and lastname from plain text field:
     * "Forename(s) Lastname", "Lastname, Forename(s), or "single name" */

    if ((s=strchr(a->name, ','))) {
        s++;
        while (*s == ' ')
            s++;
        firstname1 = s;
        lastname1 = a->name;
    } else if ((s=strrchr(a->name, ' '))) {
        firstname1 = a->name;
        lastname1  = s+1;
    } else {
        firstname1 = "";
        lastname1  = a->name;
    }

    if ((s=strchr(b->name, ','))) {
        s++;
        while (*s == ' ')
            s++;
        firstname2 = s;
        lastname2 = b->name;
    } else if ((s=strrchr(b->name, ' '))) {
        firstname2 = b->name;
        lastname2  = s+1;
    } else {
        firstname2 = "";
        lastname2  = b->name;
    }

    comp=strcasecmp(lastname1, lastname2);
    if (comp != 0)
        return(comp);
        
    return(strcasecmp(firstname1, firstname2));
}

/* abook_sort() **********************************************************
 *
 *
 * Sort addressbook on given field:
 *    abook: Addressbook to sort
 *
 ************************************************************************/

void abook_sort(struct abook *abook)
{
    unsigned long i, count;
    struct abook_entry *current;

    if (abook->sort)     /* Cache remains valid until cleared */
        return;

    count = list_length(abook->list);

    abook->sort = pool_alloc(NIL, (count+1) * sizeof(struct abook_entry *));

    for (i = 0, current = (struct abook_entry *)abook->list->head;
         current && (i<count);
         current = current->next, i++) {
        abook->sort[i]  = current;
        current->position = i + 1;
    }

    if (i != count)
        log_fatal(("abort_sort(): recorded addressbook length"
                   " does not match actual length"));
    
    abook->sort[count] = NULL;

    if (abook->sort_mode == ABOOK_SORT_ALIAS)
        qsort(abook->sort, count, sizeof(struct abook_entry *),
              abook_sort_compare_alias);
    else if (abook->sort_mode == ABOOK_SORT_NAME)
        qsort(abook->sort, count, sizeof(struct abook_entry *),
              abook_sort_compare_name);
    else if (abook->sort_mode == ABOOK_SORT_COMMENT)
        qsort(abook->sort, count, sizeof(struct abook_entry *),
              abook_sort_compare_comment);
    else if (abook->sort_mode == ABOOK_SORT_EMAIL)
        qsort(abook->sort, count, sizeof(struct abook_entry *),
              abook_sort_compare_address);
}
