/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*-
 *
 * The contents of this file are subject to the Mozilla Public License
 * Version 1.0 (the "License"); you may not use this file except in
 * compliance with the License.  You may obtain a copy of the License at
 * http://www.mozilla.org/MPL/
 *
 * Software distributed under the License is distributed on an "AS IS"
 * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied.  See
 * the License for the specific language governing rights and limitations
 * under the License.
 *
 * The Original Code is the Grendel mail/news client.
 *
 * The Initial Developer of the Original Code is Netscape Communications
 * Corporation.  Portions created by Netscape are Copyright (C) 1997
 * Netscape Communications Corporation.  All Rights Reserved.
 *
 * Created: Eric Bina <ebina@netscape.com>, 30 Oct 1997.
 */

package grendel.storage.addressparser;

import java.io.*;
import java.util.*;


/*
 * Parse a comma separated list of RFC822 addresses.
 * The important thing here is all the ERROR cases.
 * If you aren't rigidly RFC822 compliant, you will hit
 * one of them.
 */
class RFC822AddressParser
{
  static final int NO_ERROR = 0;
  static final int LIST_OF_ADDRESSES = -1;
  static final int NO_DOMAIN = -2;
  static final int NO_VECTOR = -3;
  static final int COMMENTS_IN_VECTOR = -4;
  static final int EXTRA_TOKENS_AT_START = -5;
  static final int NO_ADDR_SPEC = -6;
  static final int NO_ROUTE_ADDR = -7;
  static final int BAD_ROUTE = -8;
  static final int BAD_ROUTE_DOMAIN = -9;
  static final int EXTRA_TOKENS_IN_ROUTE_ADDR = -10;
  static final int NO_PHRASE = -11;
  static final int EXTRA_TOKENS_IN_MAILBOX_LIST = -12;
  static final int NO_LOCAL_PART = -13;
  static final int BAD_GROUP = -14;
  static final int ILLEGAL_SPECIAL = -15;

  private Vector address_list;
  private Vector tokens;
  private Vector all_tokens;
  private int error_code;

  /*
   * This is the complete vector of tokens
   * straight from the tokenizer.
   */
  public RFC822AddressParser(Vector tokens) throws RFC822ParserException
  {
    AddressToken addr_token = null;
    int num;
    int start, end;

    this.error_code = NO_ERROR;
    this.all_tokens = tokens;
    this.address_list = new Vector();
    this.tokens = stripComments(tokens);

    num = this.tokens.size();

    if (num == 0)
    {
      // ERROR: zero length input vector.
      this.error_code = NO_VECTOR;
      return;
    }

    start = 0;
    end = num - 1;
    /*
     * Arbitrary commas can start and end this list
     */
    start = skipForwardCommas(start, end);
    end = skipCommas(end);
    if (start > end)
    {
      // ERROR: zero length input vector.
      this.error_code = NO_VECTOR;
      return;
    }

    addr_token = collectAddress(start, end);
    if (addr_token == null)
    {
      return;
    }
    addMailAddress(addr_token);
    end = addr_token.getTokenStart() - 1;
    while (end >= start)
    {
      RFC822Token token;

      token = (RFC822Token)this.tokens.elementAt(end);

      /*
       * Skip the comma separator if found, but
       * don't require it.
       */
      if (token.isSpecialChar(',') == true)
      {
        end = skipCommas(end);
      }

      addr_token = collectAddress(start, end);
      if (addr_token == null)
      {
        return;
      }
      addMailAddress(addr_token);
      end = addr_token.getTokenStart() - 1;
    }
    if (addr_token.getTokenStart() != start)
    {
      // ERROR: All tokens in input vector not part
      //       of a legal address list.
      this.error_code = EXTRA_TOKENS_AT_START;
      return;
    }
  }


  public Vector getTokenList()
  {
    return(this.tokens);
  }


  public boolean isError()
  {
    if (this.error_code != NO_ERROR)
    {
      return(true);
    }
    return(false);
  }


  public String getErrorString()
  {
    String err;

    switch(this.error_code)
    {
      case NO_ERROR:
        err = new String("No error.");
        break;
      case LIST_OF_ADDRESSES:
        err = new String("Cannot handle non-comma separated list of addresses!");
        break;
      case NO_DOMAIN:
        err = new String("No valid domain.");
        break;
      case NO_VECTOR:
        err = new String("No input vector.");
        break;
      case COMMENTS_IN_VECTOR:
        err = new String("Comment tokens in input vector.");
        break;
      case EXTRA_TOKENS_AT_START:
        err = new String("Extra tokens at the start of the input vector after parsing address list.");
        break;
      case NO_ADDR_SPEC:
        err = new String("No valid address specification.");
        break;
      case NO_ROUTE_ADDR:
        err = new String("No valid route address.");
        break;
      case BAD_ROUTE:
        err = new String("Bad route format.");
        break;
      case BAD_ROUTE_DOMAIN:
        err = new String("Bad domain in a route list.");
        break;
      case EXTRA_TOKENS_IN_ROUTE_ADDR:
        err = new String("Extra tokens within the bounds of the route address.");
        break;
      case NO_PHRASE:
        err = new String("No valid phrase.");
        break;
      case EXTRA_TOKENS_IN_MAILBOX_LIST:
        err = new String("Extra tokens within the bounds of the mailbox list.");
        break;
      case NO_LOCAL_PART:
        err = new String("No valid local part.");
        break;
      case BAD_GROUP:
        err = new String("Bad group format.");
        break;
      case ILLEGAL_SPECIAL:
        err = new String("Found a special character where another token type was expected.");
        break;
      default:
        err = new String("");
    }
    return(err);
  }


  public Vector getAddressList()
  {
    return(this.address_list);
  }


  /***********************
   ***********************
   **  PRIVATE METHODS  **
   ***********************
   ***********************/


  /*
   * create a new vector from the passed vector with all
   * the comments stripped from it.
   */
  private Vector stripComments(Vector tokens)
  {
    Vector new_vec = new Vector();
    int num = tokens.size();

    for (int i=0; i < num; i++)
    {
      RFC822Token token = (RFC822Token)tokens.elementAt(i);
      if (token.getType() != RFC822Token.COMMENT)
      {
        new_vec.addElement(token);
      }
    }
    return(new_vec);
  }


  /*
   * Add an address (either group or mailbox) to the
   * beginning of an address list vector.
   */
  private void addMailAddress(AddressToken addr_token)
  {
    MailAddressToken address;

    if (addr_token.getType() == AddressToken.GROUP)
    {
      address = new MailAddressToken((GroupToken)addr_token);
    }
    else
    {
      address = new MailAddressToken((MailboxToken)addr_token);
    }
    this.address_list.insertElementAt((Object)address, 0);
  }


  /*
   * Collect an address.  Starts from a rightmost
   * token and works left.
   * In all cases where we return null we set an error code
   */
  private AddressToken collectAddress(int start, int end) throws
                                      RFC822ParserException
  {
    MailboxToken mailbox = null;
    AddressToken addr_token = null;
    RFC822Token token;
    int type;

    token = (RFC822Token)this.tokens.elementAt(end);
    type = token.getType();
    if (type == RFC822Token.SPECIAL_CHAR)
    {
      /*
       * A '>' must be the right edge of a
       * route-addr which must be a mailbox
       * made up of a phrase route-addr pair.
       */
      if (token.isSpecialChar('>') == true)
      {
        mailbox = collectMailbox(end);
        if (mailbox == null)
        {
          return(null);
        }
        return((AddressToken)mailbox);
      }
      /*
       * A ';' must be the right edge of a
       * group which must have a ':' separating
       * a phrase from zero or more mailboxes
       * in a comma separated list.
       */
      else if (token.isSpecialChar(';') == true)
      {
        GroupToken group;

        group = collectGroup(end);
        if (group == null)
        {
          return(null);
        }
        return((AddressToken)group);
      }
      /*
       * No other special character can terminate a
       * legal address.
       */
      else
      {
        this.error_code = ILLEGAL_SPECIAL;
        return(null);
      }
    }
    else if (type == RFC822Token.COMMENT)
    {
      // ERROR: No comments in address vector.
      this.error_code = COMMENTS_IN_VECTOR;
      return(null);
    }
    else
    {
      addr_token = collectAddrSpec(end);
      // if returns null error code already set.
      if (addr_token == null)
      {
        return(null);
      }
      mailbox = new MailboxToken(addr_token);
      return((AddressToken)mailbox);
    }
  }


  /*
   * Collect a group having been passed the rightmost ';'.
   * In all cases where we return null we set an error code.
   */
  private GroupToken collectGroup(int indx2) throws RFC822ParserException
  {
    GroupToken group = null;
    Vector mailboxes = new Vector();
    AddressToken phrase;
    RFC822Token token;
    int colon_at;
    int indx;

    /*
     * A ';' must be the right edge of a group which
     * must have a ':' separating a phrase from zero or
     * more mailboxes in a comma separated list.
     */
    token = (RFC822Token)this.tokens.elementAt(indx2);

    if (token.isSpecialChar(';') == false)
    {
      // ERROR: group must be terminated
      //        by a ';'.
      this.error_code = BAD_GROUP;
      return(null);
    }

    /*
     * Stupid RFC allows extra commas here at the end
     * of the list.
     */
    indx = skipCommas(indx2 - 1);

    token = (RFC822Token)this.tokens.elementAt(indx);

    /*
     * We have one or more mailboxes in a comma
     * separated list.
     */
    if (token.isSpecialChar(':') == false)
    {
      MailboxToken tmp_mbox;
      RFC822Token comma;

      tmp_mbox = collectMailbox(indx);
      // if returns null error code already set.
      if (tmp_mbox == null)
      {
        return(null);
      }
      mailboxes.insertElementAt((Object)tmp_mbox, 0);

      indx = tmp_mbox.getTokenStart();
      comma = (RFC822Token)this.tokens.elementAt(indx - 1);
      while (comma.isSpecialChar(',') == true)
      {
        /*
         * Skip the extra commas the RFC allows here
         */
        indx = skipCommas(indx - 1);

        /*
         * List could end here with commas
         */
        comma = (RFC822Token)this.tokens.elementAt(indx);
        if (comma.isSpecialChar(':') == true)
        {
          indx++;
          break;
        }

        tmp_mbox = collectMailbox(indx);
        // if returns null error code already set.
        if (tmp_mbox == null)
        {
          return(null);
        }
        mailboxes.insertElementAt((Object)tmp_mbox, 0);

        indx = tmp_mbox.getTokenStart();
        comma = (RFC822Token)this.tokens.elementAt(indx - 1);
      }
      if (comma.isSpecialChar(':') == false)
      {
        // ERROR: group contained more than
        //        a comma separated list of
        //        mailboxes with the
        //        ':' to ';' pair.
        this.error_code = EXTRA_TOKENS_IN_MAILBOX_LIST;
        return(null);
      }
      colon_at = indx - 1;
    }
    /*
     * This is the case of an empty mailbox list
     * in the group.
     */
    else
    {
      colon_at = indx;
    }

    phrase = collectPhrase(colon_at);
    // if returns null error code already set.
    if (phrase == null)
    {
      return(null);
    }
    group = new GroupToken(phrase, mailboxes, indx2);
    return(group);
  }


  /*
   * Function to skip arbitrary number of commas.
   * Needed because of the RFC's stupid comma separated list
   * rules.
   * Passed the index of possibly the first comma,
   * returns the index of the first non-comma.
   */
  private int skipCommas(int indx)
  {
    RFC822Token comma;

    if (indx >= this.tokens.size())
    {
      return(indx);
    }

    comma = (RFC822Token)this.tokens.elementAt(indx);
    while(comma.isSpecialChar(',') == true)
    {
      indx--;
      if (indx < 0)
      {
        break;
      }
      comma = (RFC822Token)this.tokens.elementAt(indx);
    }
    return(indx);
  }


  /*
   * Like skipCommas above but skipping forward
   * instead of backwards.
   */
  private int skipForwardCommas(int indx, int end)
  {
    RFC822Token comma;

    comma = (RFC822Token)this.tokens.elementAt(indx);
    while(comma.isSpecialChar(',') == true)
    {
      indx++;
      if (indx > end)
      {
        break;
      }
      comma = (RFC822Token)this.tokens.elementAt(indx);
    }
    return(indx);
  }


  /*
   * Passing in the final '>' token of a route-addr, work backwards
   * through the tokens to the matching opening '<' token.
   */
  private int findBeginRouteAddr(int end)
  {
    int indx = end - 1;

    while (indx >= 0)
    {
      RFC822Token token;

      token = (RFC822Token)this.tokens.elementAt(indx);
      if (token.isSpecialChar('<') == true)
      {
        break;
      }
      indx--;
    }

    if (indx >= 0)
    {
      return(indx);
    }

    // ERROR:  No matching '<' to '>' pair.
    return(NO_ROUTE_ADDR);
  }


  /*
   * Starting at the suspected terminal sub-domain token
   * parse up to the beginning of the domain.
   * All legal domains begin with an '@' symbol.
   * To support the illegal but very common case of a local-part
   * with no domain, need to return a special error for
   * a missing domain.
   */
  private int findBeginDomain(int end)
  {
    int indx = end;
    boolean need_dot = false;

    while (indx >= 0)
    {
      RFC822Token token;

      token = (RFC822Token)this.tokens.elementAt(indx);
      if (token.isSpecialChar('@') == true)
      {
        break;
      }
      /*
       * Must be a '.' separated list of sub-domains.
       */
      else if ((token.isSpecialChar('.') == true)&&
               (need_dot == true))
      {
        need_dot = false;
      }
      /*
       * Between '.' symbols, only atoms or domain-literals
       * make legal sub-domains.
       */
      else if (((token.getType() == RFC822Token.ATOM)||
                (token.getType() == RFC822Token.DOMAIN_LITERAL))&&
               (need_dot == false))
      {
        need_dot = true;
      }
      else
      {
        // ERROR:  Encountered some token out of
        //         position to make a legal domain.
        return(NO_DOMAIN);
      }
      indx--;
    }

    /*
     * Must contain at least one sub-domain after the '@'
     * symbol to be a legal domain.
     */
    if ((indx >= 0)&&(indx < end))
    {
      return(indx);
    }

    // ERROR:  No legal domain.
    return(NO_DOMAIN);
  }


  /*
   * A local-part looks a lot like a domain.  The difference
   * being it can have quoted-strings as well as atoms between the '.'
   * symbols, while the domain can have domain-literals as well
   * as atoms between the '.' symbols.
   * So for a list of just '.' separated atoms, you depend
   * on the '@' symbol to know which you are.
   *
   * It is illegal but common for the domain to be ommitted.
   * We will treat this as legal.
   */
  private int findBeginLocalPart(int end)
  {
    int indx = end;
    boolean need_dot = false;

    while (indx >= 0)
    {
      RFC822Token token;

      token = (RFC822Token)this.tokens.elementAt(indx);
      if ((token.isSpecialChar('.') == true)&&
          (need_dot == true))
      {
        need_dot = false;
      }
      else if (((token.getType() == RFC822Token.QUOTED_STRING)||
                (token.getType() == RFC822Token.ATOM))&&
               (need_dot == false))
      {
        need_dot = true;
      }
      /*
       * Break the local part on the first non-matching
       * token you find.
       */
      else
      {
        break;
      }
      indx--;
    }

    /*
     * Since we broke on the terminating token, increment
     * to get to the start of the local-part.
     */
    indx++;

    /*
     * Must contain at least one word.
     * Cannot start with a '.' symbol.
     */
    if ((need_dot == true)&&(indx >= 0)&&(indx <= end))
    {
      return(indx);
    }

    // ERROR: Not a legal local-part.
    return(NO_LOCAL_PART);
  }


  /*
   * A legal phrase is always followed by a special symbol
   * (either ':' or '<').  The passed index is assumed to be that
   * symbol, and we search back for the start of the phrase.
   */
  private int findBeginPhrase(int end)
  {
    int indx = end - 1;

    while (indx >= 0)
    {
      RFC822Token token;

      token = (RFC822Token)this.tokens.elementAt(indx);
      if ((token.getType() == RFC822Token.QUOTED_STRING)||
          (token.getType() == RFC822Token.ATOM))
      {
        // OK
      }
      else
      {
        break;
      }
      indx--;
    }

    /*
     * Since we broke on the terminating token, increment
     * to get to the start of the phrase.
     */
    indx++;

    /*
     * A legal index must have at least one word in
     * the phrase.
     */
    if ((indx >= 0)&&(indx < end))
    {
      return(indx);
    }

    // ERROR:  Illegal phrase.
    return(NO_PHRASE);
  }


  /*
   * Having been passed the rightmost token, collect the
   * phrase and pass it back as an address token.
   * On an error return null and set an error code.
   */
  private AddressToken collectPhrase(int indx2)
  {
    int indx1 = findBeginPhrase(indx2);
    AddressToken addr_token;

    if (indx1 == NO_PHRASE)
    {
      this.error_code = NO_PHRASE;
      return(null);
    }

    addr_token = new AddressToken(indx1, indx2 - 1, AddressToken.PHRASE);
    return(addr_token);
  }


  /*
   * Having been passed the rightmost token, collect the
   * route-addr and pass it back as an address token.
   * On an error return null and set an error code.
   */
  private AddressToken collectRouteAddr(int indx2)
  {
    AddressToken addr_token;
    RFC822Token token;
    int indx;
    int indx1 = findBeginRouteAddr(indx2);

    if ((indx1 == NO_ROUTE_ADDR)||(indx1 >= (indx2 - 1)))
    {
      // ERROR: Illegal to have a route-addr with
      //        an empty body.
      this.error_code = NO_ROUTE_ADDR;
      return(null);
    }
    addr_token = collectAddrSpec(indx2 - 1);
    // if returns null error code already set.
    if (addr_token == null)
    {
      return(null);
    }
    indx = addr_token.getTokenStart();

    /*
     * The route is optional, look for the closing ':'
     * symbol to see if we have one.
     */
    token = (RFC822Token)this.tokens.elementAt(indx - 1);
    if (token.isSpecialChar(':') == true)
    {
      AddressToken route = collectRoute(indx - 1);

      // if returns null error code already set.
      if (route == null)
      {
        return(null);
      }
      indx = route.getTokenStart();
    }

    token = (RFC822Token)this.tokens.elementAt(indx - 1);
    if (token.isSpecialChar('<') == false)
    {
      // ERROR:  More in the route-addr than a legal
      //         addr-spec and an optional route.
      this.error_code = EXTRA_TOKENS_IN_ROUTE_ADDR;
      return(null);
    }
    addr_token = new AddressToken(indx - 1, indx2, AddressToken.ROUTE_ADDR);
    return(addr_token);
  }


  /*
   * Having been passed the rightmost token, collect the
   * addr-spec and pass it back as an address token.
   * On an error return null and set an error code.
   */
  private AddressToken collectAddrSpec(int indx2)
  {
    int at_sign = findBeginDomain(indx2);
    int indx1;
    AddressToken addr_token;

    if (at_sign == NO_DOMAIN)
    {
      indx1 = findBeginLocalPart(indx2);
    }
    else
    {
      indx1 = findBeginLocalPart(at_sign - 1);
    }

    if (indx1 == NO_LOCAL_PART)
    {
      this.error_code = NO_ADDR_SPEC;
      return(null);
    }

    addr_token = new AddressToken(indx1, indx2, AddressToken.ADDR_SPEC);

    return(addr_token);
  }

  /*
   * Having been passed the rightmost token, collect the
   * route and pass it back as an address token.
   * On an error return null and set an error code.
   */
  private AddressToken collectRoute(int indx2)
  {
    AddressToken addr_token;
    RFC822Token token;
    int at_sign;
    int indx;

    token = (RFC822Token)this.tokens.elementAt(indx2);
    if (token.isSpecialChar(':') == false)
    {
      // ERROR:  Not a legal route, not ':' terminated.
      this.error_code = BAD_ROUTE;
      return(null);
    }

    /*
     * Stupid RFC allows many commas at end of list.
     */
    indx = skipCommas(indx2 - 1);

    /*
     * Must be at least one domain, look for the '@' symbol
     * that starts it.
     */
    at_sign = findBeginDomain(indx);
    if (at_sign < 1)
    {
      // ERROR:  Must always be room to move back from
      //         the start of a route because it is
      //         always enclosed in a route-addr.
      //         So it is always ok to look for the ','
      //         In the optional comma separated list.
      this.error_code = BAD_ROUTE_DOMAIN;
      return(null);
    }

    /*
     * If there is a comma, there are one or more domains to
     * skip over.
     */
    token = (RFC822Token)this.tokens.elementAt(at_sign - 1);
    while (token.isSpecialChar(',') == true)
    {
      /*
       * Again RFC allows many commas
       */
      indx = skipCommas(at_sign - 1);
      /*
       * These commas may have been the end of the list
       */
      token = (RFC822Token)this.tokens.elementAt(indx);
      if (token.isSpecialChar('<') == true)
      {
        at_sign = indx + 1;
        break;
      }

      at_sign = findBeginDomain(indx);
      if (at_sign < 1)
      {
        // ERROR:  Must always be room to move back from
        //         the start of a route because it is
        //         always enclosed in a route-addr.
        //         So it is always ok to look for the','
        //         In the optional comma separated list.
        this.error_code = BAD_ROUTE_DOMAIN;
        return(null);
      }
      token = (RFC822Token)this.tokens.elementAt(at_sign - 1);
    }

    addr_token = new AddressToken(at_sign, indx2, AddressToken.ROUTE);

    return(addr_token);
  }


  /*
   * Having been passed the rightmost token, collect the
   * mailbox and pass it back as a mailbox token.
   * On an error return null and set an error code.
   */
  private MailboxToken collectMailbox(int indx2) throws RFC822ParserException
  {
    MailboxToken mailbox = null;
    AddressToken addr_token = null;
    AddressToken phrase;
    RFC822Token token;
    int indx;

    /*
     * A legal mailbox either ends in a '>'
     * symbol and is a phrase route-addr pair,
     * or it is a simple addr-spec.
     */
    token = (RFC822Token)this.tokens.elementAt(indx2);
    /*
     * Get the phrase route-addr pair.
     */
    if (token.isSpecialChar('>') == true)
    {
      addr_token = collectRouteAddr(indx2);
      // if returns null error code already set.
      if (addr_token == null)
      {
        return(null);
      }
      indx = addr_token.getTokenStart();
      phrase = collectPhrase(indx);
      if (this.error_code == NO_PHRASE)
      {
        throw new RouteAddrNoPhraseException("No valid phrase with this route address.", addr_token, this.tokens);
      }
      // if returns null error code already set.
      if (phrase == null)
      {
        return(null);
      }
      indx = phrase.getTokenStart();
      mailbox = new MailboxToken(phrase, addr_token);
    }
    /*
     * Get the addr-spec.
     */
    else
    {
      addr_token = collectAddrSpec(indx2);
      // if returns null error code already set.
      if (addr_token == null)
      {
        return(null);
      }
      indx = addr_token.getTokenStart();
      mailbox = new MailboxToken(addr_token);
    }

    return(mailbox);
  }
}

