/*
 * @(#)MimeMessage.java
 *
 * Copyright (c) 2001-2002 JScape
 * 1147 S. 53rd Pl., Mesa, Arizona, 85206, U.S.A.
 * All rights reserved.
 *
 * This software is the confidential and proprietary information of
 * JScape. ("Confidential Information").  You shall not disclose such
 * Confidential Information and shall use it only in accordance with
 * the terms of the license agreement you entered into with JScape.
 */
package com.jscape.inet.mime;

import com.jscape.inet.util.Codec;
import com.jscape.inet.util.CodecFactory;
import com.jscape.inet.util.Uuencode;
import com.jscape.util.ByteBuffer;
import com.jscape.util.ByteBufferTokenizer;

import java.io.*;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Vector;

import static com.jscape.inet.mime.MimeConstants.*;

/**
 * Provides methods for building MIME message content and parsing existing MIME
 * message content into its parts.
 */
@SuppressWarnings("unchecked")
public class MimeMessage
        implements Serializable {
    /**
     * The message body as defined using the setBody method.
     */
    protected byte[] definedBody = null;

    /**
     * The message body as parsed in the MimeMessage constructor.
     */
    protected byte[] messageBody = null;

    /**
     * The message contents as provided in the MimeMessage constructor.
     */
    protected byte[] message = null;

    /**
     * The content type used in this MimeMessage.
     */
    protected String contentType = null;

    /**
     * The character encoding used in this MimeMessage.
     */
    protected String characterEncoding = null;

    /**
     * The set of MimeHeader for this MimeMessage.
     */
    protected Vector headers = new Vector();

    /**
     * The MIME parts for this MimeMessage.
     */
    protected Vector parts = new Vector();

    /**
     * Tracks all MIME headers.
     */
    protected Hashtable headerIndex = new Hashtable();

    /**
     * Tracks whether this MimeMessage has changed.
     */
    protected boolean changed = false;

    /**
     * Constructs a new MimeMessage.
     */
    public MimeMessage() {
        
        headers = new Vector();
        parts = new Vector();
    }

    /**
     * Constructs a <code>MimeMessage</code> from existing content.
     *
     * @param message the contents of mime message
     *
     * @throws MimeException a Mime parsing error occurs
     */
    public MimeMessage(byte[] message)
            throws MimeException {
        this.message = message;

        try {
            com.jscape.util.ByteBuffer buffer = new ByteBuffer(message);
            ByteBuffer body = null;
            ByteBuffer headers = null;

            headers = new ByteBuffer();
            body = new ByteBuffer();

            int endHeaders = buffer.indexOf("\r\n\r\n".getBytes(StandardCharsets.US_ASCII));

            // attempt to build by splitting 
            if (endHeaders != -1) {
                headers.append(buffer.getBuffer(0, endHeaders));
                body.append(buffer.getBuffer(endHeaders + 4));
            } else {
                // attempt to build reading one line at a time
                ByteBufferTokenizer tz = new ByteBufferTokenizer(buffer,
                                                                 CRLF.getBytes(StandardCharsets.US_ASCII), true, false);

                boolean headersFound = false;

                // flag that checks if first header has been encountered
                int count = 0;

                // read line at a time from message
                while (tz.hasMoreElements()) {
                    ByteBuffer tmp = (ByteBuffer) tz.nextElement();

                    // continuation of a wrapping header
                    if (!headersFound && ((tmp.indexOf(SPACE) == 0) || (tmp.indexOf(TAB) == 0))) {
                        headers.append(tmp);
                        headers.append(CRLF.getBytes(StandardCharsets.US_ASCII));
                    } else if (!headersFound && (tmp.indexOf(COLON) != -1)) {
                        // start of a header
                        headers.append(tmp);
                        headers.append(CRLF.getBytes(StandardCharsets.US_ASCII));
                    } else {
                        headersFound = true;
                        body.append(tmp);
                        body.append(CRLF.getBytes(StandardCharsets.US_ASCII));
                    }

                    count++;
                }
            }

            // parse the headers found
            parseHeaders(headers);

            // parse the body
            parseBody(body);
        } catch (Exception e) {
            // todo: ignore?
        } finally {
            // reset changed flag to false
            changed = false;
        }
    }

    /**
     * Gets contents of this <code>MimeMessage</code>. Contents include all
     * MIME message headers, body and any parts.
     *
     * @return message contents
     *
     * @throws MimeException if a MIME related parsing error occurs
     */
    public byte[] getMessage()
            throws MimeException {
        // if MIME message was constructed from existing data and not changed
        if ((message != null) && !changed) {
            return message;
        } else {
            // build message from data elements
            ByteBuffer msg = new ByteBuffer();

            // build headers
            for (int i = 0; i < headers.size(); i++) {
                MimeHeader header = (MimeHeader) headers.elementAt(i);
                msg.append(header.toString(), StandardCharsets.US_ASCII);
                msg.append(CRLF, StandardCharsets.US_ASCII);
            }

            // write body data
            if ((definedBody != null) && (definedBody.length > 0)) {
                msg.append(CRLF, StandardCharsets.US_ASCII);

                String encoding = getContentTransferEncoding();
                Codec codec = CodecFactory.getCodec(encoding);

                // encode the user defined data
                if (codec != null) {
                    char[] encodedData = codec.encode(definedBody);

                    // special handling for uuencoded data
                    if (codec instanceof Uuencode) {
                        for (int i = 0; i < encodedData.length; i++) {
                            if (((i % 64) == 0) && (i > 0)) {
                                msg.append('\n');
                            }

                            msg.append(encodedData[i]);
                        }
                    } else {
                        // append encoded data
                        msg.append(new String(encodedData), StandardCharsets.US_ASCII);
                    }
                } else {
                    // append data in native format
                    msg.append(definedBody);
                }

                // write message parts
                if (parts.size() > 0) {
                    msg.append(CRLF, StandardCharsets.US_ASCII);
                }

                // write boundaries
                for (int i = 0; i < parts.size(); i++) {
                    if (i == 0) {
                        if (definedBody != null) {
                            msg.append(CRLF, StandardCharsets.US_ASCII);
                        }
                    } else {
                        msg.append(CRLF, StandardCharsets.US_ASCII);
                    }

                    msg.append("--", StandardCharsets.US_ASCII);
                    msg.append(getBoundary(), StandardCharsets.US_ASCII);
                    msg.append(CRLF, StandardCharsets.US_ASCII);

                    MimeMessage m = (MimeMessage) parts.elementAt(i);
                    msg.append(m.getMessage());
                }

                // close boundaries
                if (parts.size() > 0) {
                    msg.append(CRLF, StandardCharsets.US_ASCII);
                    msg.append("--", StandardCharsets.US_ASCII);
                    msg.append(getBoundary(), StandardCharsets.US_ASCII);
                    msg.append("--", StandardCharsets.US_ASCII);
                    msg.append(CRLF, StandardCharsets.US_ASCII);
                }
            } else { // no defined body
                // add separator between message headers and boundaries if no body
                msg.append(CRLF, StandardCharsets.US_ASCII);

                if ((messageBody != null) && (messageBody.length > 0)) {
                    msg.append(messageBody);

                    // fix for incorrect wrapping of boundary
                    if (parts.size() > 0) {
                        msg.append(CRLF, StandardCharsets.US_ASCII);
                    }
                }

                // write boundaries
                for (int i = 0; i < parts.size(); i++) {
                    if (i != 0) {
                        msg.append(CRLF, StandardCharsets.US_ASCII);
                    }

                    msg.append("--", StandardCharsets.US_ASCII);
                    msg.append(getBoundary(), StandardCharsets.US_ASCII);
                    msg.append(CRLF, StandardCharsets.US_ASCII);

                    MimeMessage m = (MimeMessage) parts.elementAt(i);
                    msg.append(m.getMessage());
                }

                // close boundaries
                if (parts.size() > 0) {
                    msg.append(CRLF, StandardCharsets.US_ASCII);
                    msg.append("--", StandardCharsets.US_ASCII);
                    msg.append(getBoundary(), StandardCharsets.US_ASCII);
                    msg.append("--", StandardCharsets.US_ASCII);
                    msg.append(CRLF, StandardCharsets.US_ASCII);
                }
            }

            return msg.toByteArray();
        }
    }

    /**
     * Parses body portion of this <code>MimeMessage</code>.
     *
     * @param body the body portion of this <code>MimeMessage</code>
     *
     * @throws UnsupportedEncodingException if a character encoding related error occurs
     * @throws MimeException                if a MIME related error occurs
     */
    private void parseBody(ByteBuffer body)
            throws UnsupportedEncodingException, MimeException {
        MimeHeader contentTypeHeader = getHeader("Content-Type");

        if (contentTypeHeader == null) {
            messageBody = body.toByteArray();
        } else {
            if (contentTypeHeader.getValue().toLowerCase().startsWith("multipart") && (contentTypeHeader.getAttribute("boundary") != null)) {
                MimeHeaderAttr boundaryAttr = contentTypeHeader.getAttribute("boundary");

                String boundary = "\r\n--" + removeChar(boundaryAttr.value.trim(), '\"') + "\r\n";
                String endBoundary = "\r\n--" + removeChar(boundaryAttr.value.trim(), '\"') + "--";
                int endBoundaryIndex = body.indexOf(endBoundary, StandardCharsets.US_ASCII);

                // check for improperly set end boundaries
                if (endBoundaryIndex == -1) {
                    endBoundary = "--" + removeChar(boundaryAttr.value.trim(), '\"') + "--";
                    endBoundaryIndex = body.indexOf(endBoundary, StandardCharsets.US_ASCII);
                }

                // no end boundary found .. read until end of message
                if (endBoundaryIndex == -1) {
                    body = body.getBuffer(0);
                } else {
                    body = body.getBuffer(0, endBoundaryIndex);
                }

                // fix improperly encoded body .. missing leading CRLF
                if (body.startsWith("--", StandardCharsets.US_ASCII)) {
                    ByteBuffer tmp = new ByteBuffer();
                    tmp.append(CRLF, StandardCharsets.US_ASCII);
                    tmp.append(body);
                    body = tmp;
                }

                ByteBufferTokenizer boundaryTokenizer = new ByteBufferTokenizer(body, boundary.getBytes(StandardCharsets.US_ASCII), true, false);
                ByteBuffer line = null;

                // get body
                if (boundaryTokenizer.hasMoreElements()) {
                    line = boundaryTokenizer.nextToken();
                    messageBody = line.toByteArray();
                }

                // add all parts
                while (boundaryTokenizer.hasMoreElements()) {
                    line = boundaryTokenizer.nextToken();

                    MimeMessage part = new MimeMessage(line.toByteArray());
                    addPart(part);
                }
            } else {
                messageBody = body.toByteArray();
            }
        }
    }

    /**
     * Parses headers portion of this <code>MimeMessage</code>.
     *
     * @param headers the headers portion of this <code>MimeMessage</code>
     *
     * @throws UnsupportedEncodingException if a character encoding related error occurs
     * @throws MimeException                if a MIME related error occurs
     */
    private void parseHeaders(ByteBuffer headers)
            throws UnsupportedEncodingException, MimeException {
        // parse the headers
        byte[] headerDelim = { CR, LF };
        ByteBufferTokenizer headerTokenizer = new ByteBufferTokenizer(headers,
                                                                      headerDelim, true);
        ByteBuffer line = headerTokenizer.nextToken();
        ByteBuffer header = new ByteBuffer();

        // only 1 header in message
        if (!headerTokenizer.hasMoreElements()) {
            MimeHeader head = new MimeHeader();
            head.parse(headers);

            if (head.isValid()) {
                addHeader(head);
            }
        }

        while (headerTokenizer.hasMoreElements()) {
            // tracks if current header is multi-line
            boolean multiline = false;

            // append line to header buffer
            header.append(line);

            // get next line of header data
            line = headerTokenizer.nextToken();

            // handle multi-line message header
            while (line.startsWith(" ", StandardCharsets.US_ASCII) || line.startsWith("\t", StandardCharsets.US_ASCII)) {
                header.append(line.getBuffer(1));

                // check if more data available
                if (headerTokenizer.hasMoreElements()) {
                    line = headerTokenizer.nextToken();
                } else {
                    // this must be last header so no need to add again at end
                    multiline = true;

                    break;
                }
            }

            // check and parse header
            if (header.toString().trim().length() > 0) {
                MimeHeader head = new MimeHeader();
                head.parse(header);
                addHeader(head);
            }

            // re-init header buffer
            header = new ByteBuffer();

            // if last header and not multiline then add to headers
            if (!headerTokenizer.hasMoreElements() && !multiline) {
                header.append(line);

                MimeHeader last = new MimeHeader();
                last.parse(header);
                addHeader(last);
            }
        }
    }

    /**
     * Removes any instance of specified character in String.
     *
     * @param a  the string to operate on
     * @param ch the character to remove from string <code>a</code>
     *
     * @return the String stripped of specified character
     */
    protected static String removeChar(String a, char ch) {
        int len = a.length();
        StringBuffer buf = new StringBuffer(len);

        for (int i = 0; i < len; i++) {
            char c = a.charAt(i);

            if (c != ch) {
                buf.append(c);
            }
        }

        return buf.toString();
    }

    /**
     * Sets the content-type of this <code>MimeMessage</code>.
     *
     * @param contentType the content-type
     *
     * @throws MimeException if a MIME related parsing error occurs
     */
    public void setContentType(String contentType)
            throws MimeException {
        synchronized (this) {
            this.contentType = contentType;
        }

        MimeHeader h = new MimeHeader("Content-Type", contentType);
        addReplaceHeader(h);
    }

    /**
     * Gets the content-type of this <code>MimeMessage</code>.
     *
     * @return content-type
     */
    public String getContentType() {
        return contentType;
    }

    /**
     * Gets the value of the Content-Transfer-Encoding header for this
     * <code>MimeMessage</code>. The Content-Transfer-Encoding header
     * indicates the encoding method used for the message body. Typical values
     * are BASE64, UUENCODE, 7BIT, 8BIT and QUOTED-PRINTABLE
     *
     * @return content transfer encoding
     */
    public String getContentTransferEncoding() {
        String contentTransferEncoding = null;
        MimeHeader header = getHeader("Content-Transfer-Encoding");

        if (header != null) {
            contentTransferEncoding = header.getValue();
        }

        return contentTransferEncoding;
    }

    /**
     * Sets the body of the MimeMessage as a File object but does not attempt to read it.
     * This method should be used together with HttpFileUpload class to delay writing of the
     * file body and thus avoid reading in the entire file data to computer RAM. This method
     * was made to upload very large files using the HttpFileUpload class.
     *
     * @param body
     */
    private File bodyAsFile = null;

    public void setBodyAsFile(File body) {
        changed = true;
        this.bodyAsFile = body;
        definedBody = com.jscape.inet.http.HttpFileUpload.PLACEHOLDER.getBytes(StandardCharsets.US_ASCII);
        messageBody = null;
    }

    /**
     * Get the file body. Intended to be used together with HttpFileUpload class.
     *
     * @return body
     */
    public File getBodyAsFile() {
        return this.bodyAsFile;
    }

    /**
     * Sets body of this <code>MimeMessage</code>. Default system character
     * encoding is used to convert the body to bytes.
     *
     * @param body the body contents
     */
    public void setBody(String body) {
        synchronized (this) {
            setBody(body.getBytes(StandardCharsets.US_ASCII));
        }
    }

    /**
     * Sets body of this <code>MimeMessage</code> and sets the charset
     * attribute of Content-Type header to provided character set. If
     * Content-Type header is not found then a new Content-Type header will be
     * created with a value of text/plain.
     *
     * @param body         the body contents
     * @param characterSet the character set the body it to be set with.
     *
     * @throws MimeException if a MIME related parsing error occurs
     */
    public void setBody(String body, String characterSet)
            throws MimeException {
        synchronized (this) {
            // flag that message has changed
            changed = true;

            definedBody = body.getBytes(Charset.forName(characterSet));
            messageBody = null;

            MimeHeader tmpType = getHeader("Content-Type");

            //new MimeHeader("Content-Type", contentType);
            if (tmpType == null) {
                tmpType = new MimeHeader("Content-Type", "text/plain");
            }

            if (tmpType.getValue().toLowerCase().startsWith("text")) {
                if (characterSet == null) {
                    tmpType.addAttribute(new MimeHeaderAttr("charset", StandardCharsets.US_ASCII.name()));
                } else {
                    tmpType.addAttribute(new MimeHeaderAttr("charset", characterSet));
                }
            }

            addReplaceHeader(tmpType);
        }
    }

    /**
     * Sets body of this <code>MimeMessage</code>.
     *
     * @param contents the body contents
     */
    public void setBody(byte[] contents) {
        synchronized (this) {
            // flag that message has changed
            changed = true;

            definedBody = contents;
            messageBody = null;
        }
    }

    /**
     * Sets body of this <code>MimeMessage</code>.
     *
     * @param file the body contents
     *
     * @throws IOException if an I/O error occurs reading file
     */
    public void setBody(File file)
            throws IOException {
        synchronized (this) {
            try (InputStream fin = Files.newInputStream(file.toPath())) {
                ByteArrayOutputStream bout = new ByteArrayOutputStream();
                byte[] buffer = new byte[1024];
                int b = 0;

                while ((b = fin.read(buffer)) != -1) {
                    bout.write(buffer, 0, b);
                }

                setBody(bout.toByteArray());
            }
        }
    }

    /**
     * Gets value for header specified by <code>name</code>
     * If more than one header with specified <code>name</code> exists for this <code>MimeMessage</code> then
     * the value of the last header added to this <code>MimeMessage</code> with specified <code>name</code>
     * will be returned. To get all header see the getHeaders method.
     *
     * @param name the name of the header to retrive
     *
     * @return the value of the header or null if header is not found
     *
     * @see #getHeaders
     */
    public String getHeaderValue(String name) {
        String value = null;
        MimeHeader h = getHeader(name);

        if (h != null) {
            value = h.getValue();
        }

        return value;
    }

    /**
     * Returns named header specified by <code>name</code>.
     * If more than one header with specified <code>name</code> exists for this <code>MimeMessage</code> then
     * the last header added to this <code>MimeMessage</code> with specified <code>name</code> will be returned.
     * To get all header see the getHeaders method.
     *
     * @param name the name of the header to retrieve
     *
     * @return a <code>MimeHeader</code> or <code>null</code> if header is not
     *         present
     *
     * @see #getHeaders
     */
    public MimeHeader getHeader(String name) {
        MimeHeader header = (MimeHeader) headerIndex.get(name.toLowerCase());

        if (header == null) {
            Enumeration e = headerIndex.keys();

            while (e.hasMoreElements()) {
                String tmp = (String) e.nextElement();

                if (tmp.equalsIgnoreCase(name)) {
                    header = (MimeHeader) headerIndex.get(tmp);

                    break;
                }
            }
        }

        return header;
    }

    /**
     * Gets decoded message body using character encoding specified in charset
     * attribute of Content-Type message header. If no character encoding is
     * defined then US-ASCII character encoding is used.
     *
     * @return the body of the MIME message
     *
     * @throws MimeException if a MIME related parsing error occurs
     */
    public String getBody()
            throws MimeException {
        String body = null;
        byte[] d = null;

        try {
            Charset charset = null;
            MimeHeader contentTypeHeader = getHeader("Content-Type");

            // attempt to get character set encoding
            if (contentTypeHeader != null) {
                MimeHeaderAttr charSetAttr = contentTypeHeader.getAttribute(
                        "charset");

                if (charSetAttr != null) {
                    charset = Charset.forName(charSetAttr.getValue());
                }
            }

            // if character set not found then assume ISO-8859-1
            if (charset == null) {
                charset = StandardCharsets.ISO_8859_1;
            }

            // get message body using character set
            d = getBodyData();

            if (d != null) {
                body = new String(d, charset);
            }
        } catch (Exception e) {
            // todo: ignore?
        }

        if (body == null) {
            body = "";
        }

        return body;
    }

    /**
     * Gets decoded message body as bytes. If message body is encoded using
     * Base64, Uuencode or Quoted-Printable codecs then  message body will be
     * decoded.  If message body is in 7bit or 8bit form then body will be
     * returned as is.
     *
     * @return body data
     *
     * @throws MimeException if a MIME related parsing error occurs
     */
    public byte[] getBodyData()
            throws MimeException {
        byte[] body = null;

        try {
            if (messageBody != null) {
                Codec codec = null;
                MimeHeader codecHeader = getHeader("Content-Transfer-Encoding");

                if (codecHeader != null) {
                    codec = CodecFactory.getCodec(codecHeader.getValue());
                }

                // decode the message body
                if (codec != null) {
                    char[] encoded = new String(messageBody, StandardCharsets.US_ASCII).toCharArray();
                    body = codec.decode(encoded);
                } else {
                    body = messageBody;
                }
            } else {
                body = definedBody;
            }
        } catch (UnsupportedEncodingException use) {
            use.printStackTrace();
        } catch (Exception e) {
            e.printStackTrace();
        }

        return body;
    }

    /**
     * Gets message body as bytes. the body will be returned as is.
     *
     * @return body data
     */
    public byte[] getRawBodyData() {
        byte[] body = null;

        if (messageBody != null) {
            body = messageBody;
        } else {
            body = definedBody;
        }

        return body;
    }

    /**
     * Gets boundary used in this <code>MimeMessage</code>.
     *
     * @return boundary
     *
     * @throws MimeException if a MIME related parsing error occurs
     */
    public String getBoundary()
            throws MimeException {
        String boundary = null;
        MimeHeader contentTypeHeader = getHeader("Content-Type");

        if (contentTypeHeader != null) {
            MimeHeaderAttr boundaryAttr = contentTypeHeader.getAttribute(
                    "boundary");

            if (boundaryAttr != null) {
                boundary = boundaryAttr.getValue().trim();
            }
        }

        return boundary;
    }

    /**
     * Gets random String used in building boundary.
     *
     * @return random String
     */
    private String getRandomString() {
        double rand = Math.random();

        return Double.toString(rand * 1000);
    }

    /**
     * Adds part to multipart <code>MimeMessage</code>. If not previously set
     * boundary string is generated. This method also sets Content-Type field
     * for this <code>MimeMessage</code> to multipart/mixed to reflect
     * multipart nature of the message.
     *
     * @param message the <code>MimeMessage</code> to add to this
     *                <code>MimeMessage</code>
     *
     * @throws MimeException if a MIME related parsing error occurs
     */
    public void addPart(MimeMessage message)
            throws MimeException {
        synchronized (this) {
            // flag that message has changed
            changed = true;

            parts.addElement(message);

            if (getBoundary() == null) {
                // generate new boundary string    
                String boundary = "___BoUnDaRy_" + Long.toString(System.currentTimeMillis()) + "." + getRandomString();

                // get existing multipart type
                MimeHeader current = getHeader("Content-Type");
                MimeHeader head = new MimeHeader("Content-Type",
                                                 "multipart/mixed");

                if ((current != null) && current.getValue().startsWith("multipart")) {
                    head = current;
                }

                // add boundary
                MimeHeaderAttr attr = new MimeHeaderAttr("boundary", boundary);

                head.addAttribute(attr);
                addReplaceHeader(head);
                contentType = new String("multipart/mixed");
            }

            // remove Content-Transfer-Encoding header ... according to RFC 2045 not needed in multipart message
            removeHeader("Content-Transfer-Encoding");
        }
    }

    /**
     * Gets parts of this <code>MimeMessage</code>. Returns an Enumeration of
     * <code>MimeMessage</code>.
     *
     * @return a Enumeration
     */
    public Enumeration getParts() {
        return parts.elements();
    }

    /**
     * Removes specified <code>index</code> from parts array.
     *
     * @param index the index of part to remove
     */
    public void removePart(int index) {
        synchronized (this) {
            changed = true;
            parts.removeElementAt(index);
        }
    }

    /**
     * Removes all parts from this message.
     */
    public void removeParts() {
        synchronized (this) {
            parts.clear();
        }
    }

    /**
     * Adds MIME header to this <code>MimeMessage</code>.
     *
     * @param header a <code>MimeHeader</code>
     */
    public void addHeader(MimeHeader header) {
        synchronized (this) {
            // flag that message has changed
            changed = true;

            if (header.name.toLowerCase().equals("content-type")) {
                this.contentType = header.value;
            }

            headers.addElement(header);
            headerIndex.put(header.getName().toLowerCase(), header);
        }
    }

    /**
     * Adds MIME header to this <code>MimeMessage</code>, replacing existing header if found.
     *
     * @param header a <code>MimeHeader</code>
     */
    public void addReplaceHeader(MimeHeader header) {
        synchronized (this) {
            changed = true;

            int index = getHeaderIndex(header.name);

            if (index == -1) {
                addHeader(header);
            } else {
                headers.set(index, header);
            }

            headerIndex.put(header.name.toLowerCase(), header);
        }
    }

    /**
     * Adds MIME header to this <code>MimeMessage</code>, replacing existing header if found.
     *
     * @param name  the header name
     * @param value the header value
     *
     * @throws MimeException if a MIME related parsing error occurs
     */
    public void addReplaceHeader(String name, String value)
            throws MimeException {
        synchronized (this) {
            MimeHeader header = new MimeHeader(name, value);
            addReplaceHeader(header);
        }
    }

    /**
     * Gets location in headers Vector of named header.
     *
     * @param name the named header to retrieve
     *
     * @return the index of header if found, -1 otherwise
     */
    private int getHeaderIndex(String name) {
        int index = -1;

        for (int i = 0; i < headers.size(); ++i) {
            MimeHeader h = (MimeHeader) headers.elementAt(i);

            if (h.name.toLowerCase().equals(name.toLowerCase())) {
                index = i;

                break;
            }
        }

        return index;
    }

    /**
     * Adds MIME header to this <code>MimeMessage</code>.
     *
     * @param name  the header name
     * @param value the header value
     *
     * @throws MimeException if a MIME related parsing error occurs
     */
    public void addHeader(String name, String value)
            throws MimeException {
        addHeader(new MimeHeader(name, value));
    }

    /**
     * Removes specified header from this <code>MimeMessage</code>.
     *
     * @param name the name of the header to remove
     */
    public void removeHeader(String name) {
        if (headerIndex.containsKey(name.toLowerCase())) {
            changed = true;

            Vector toRemove = new Vector();

            for (int i = 0; i < headers.size(); ++i) {
                MimeHeader h = (MimeHeader) headers.elementAt(i);

                if (h.getName().equalsIgnoreCase(name)) {
                    // candidate for removal
                    toRemove.add(h);
                }
            }

            // remove found headers
            for (int i = 0; i < toRemove.size(); ++i) {
                headers.remove(toRemove.elementAt(i));
            }
        }
    }

    /**
     * Gets all mime headers for this <code>MimeMessage</code>. Returns an
     * Enumeration of <code>MimeHeader</code>.
     *
     * @return a Enumeration
     */
    public Enumeration getHeaders() {
        return headers.elements();
    }

    /**
     * Gets all MIME headers for this <code>MimeMessage</code> with specified name. Returns an
     * Enumeration of <code>MimeHeader</code>.
     *
     * @param name the header name
     *
     * @return a Enumeration
     */
    public Enumeration getHeaders(String name) {
        Vector tmp = new Vector();

        for (int i = 0; i < headers.size(); ++i) {
            MimeHeader h = (MimeHeader) headers.elementAt(i);

            if (h.getName().equalsIgnoreCase(name)) {
                tmp.addElement(h);
            }
        }

        return tmp.elements();
    }

    /**
     * Gets specified part from this <code>MimeMessage</code>.
     *
     * @param index the index of part
     *
     * @return a <code>MimeMessage</code> or <code>null</code> if not present
     */
    public MimeMessage getPart(int index) {
        return (MimeMessage) parts.elementAt(index);
    }
}
