package org.bouncycastle.mail.smime;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.cert.CertificateParsingException;
import java.security.cert.X509Certificate;
import java.util.Enumeration;

import javax.mail.BodyPart;
import javax.mail.MessagingException;
import javax.mail.Multipart;
import javax.mail.Part;
import javax.mail.internet.ContentType;
import javax.mail.internet.MimeBodyPart;
import javax.mail.internet.MimeMultipart;

import org.bouncycastle.asn1.cms.IssuerAndSerialNumber;
import org.bouncycastle.cert.jcajce.JcaX509CertificateHolder;
import org.bouncycastle.cms.CMSTypedStream;
import org.bouncycastle.mail.smime.util.CRLFOutputStream;
import org.bouncycastle.mail.smime.util.FileBackedMimeBodyPart;
import org.bouncycastle.util.Strings;

public class SMIMEUtil
{
    private static final String MULTIPART = "multipart";
    private static final int BUF_SIZE = 32760;

    public static boolean isMultipartContent(Part part)
        throws MessagingException
    {
        String          partType = Strings.toLowerCase(part.getContentType());

        return partType.startsWith(MULTIPART);
    }

    static boolean isCanonicalisationRequired(
        MimeBodyPart   bodyPart,
        String defaultContentTransferEncoding) 
        throws MessagingException
    {
        String[]        cte = bodyPart.getHeader("Content-Transfer-Encoding");
        String          contentTransferEncoding;

        if (cte == null)
        {
            contentTransferEncoding = defaultContentTransferEncoding;
        }
        else
        {
            contentTransferEncoding = cte[0];
        }

        return !contentTransferEncoding.equalsIgnoreCase("binary");
    }

    static class LineOutputStream extends FilterOutputStream
    {
        private static byte newline[];

        public LineOutputStream(OutputStream outputstream)
        {
            super(outputstream);
        }

        public void writeln(String s)
            throws MessagingException
        {
            try
            {
                byte abyte0[] = getBytes(s);
                super.out.write(abyte0);
                super.out.write(newline);
            }
            catch(Exception exception)
            {
                throw new MessagingException("IOException", exception);
            }
        }

        public void writeln()
            throws MessagingException
        {
            try
            {
                super.out.write(newline);
            }
            catch(Exception exception)
            {
                throw new MessagingException("IOException", exception);
            }
        }

        static 
        {
            newline = new byte[2];
            newline[0] = 13;
            newline[1] = 10;
        }
        
        private static byte[] getBytes(String s)
        {
            char ac[] = s.toCharArray();
            int i = ac.length;
            byte abyte0[] = new byte[i];
            int j = 0;

            while (j < i)
            {
                abyte0[j] = (byte)ac[j++];
            }

            return abyte0;
        }
    }

    /**
     * internal preamble is generally included in signatures, while this is technically wrong,
     * if we find internal preamble we include it by default.
     */
    static void outputPreamble(LineOutputStream lOut, MimeBodyPart part, String boundary)
        throws MessagingException, IOException
    {
        InputStream in;

        try
        {
            in = part.getRawInputStream();
        }
        catch (MessagingException e)
        {
            return;   // no underlying content rely on default generation
        }

        String line;

        while ((line = readLine(in)) != null)
        {
            if (line.equals(boundary))
            {
                break;
            }

            lOut.writeln(line);
        }

        in.close();

        if (line == null)
        {
            throw new MessagingException("no boundary found");
        }
    }

    /**
     * internal postamble is generally included in signatures, while this is technically wrong,
     * if we find internal postamble we include it by default.
     */
    static void outputPostamble(LineOutputStream lOut, MimeBodyPart part, int count, String boundary)
        throws MessagingException, IOException
    {
        InputStream in;

        try
        {
            in = part.getRawInputStream();
        }
        catch (MessagingException e)
        {
            return;   // no underlying content rely on default generation
        }

        String line;
        int boundaries = count + 1;

        while ((line = readLine(in)) != null)
        {
            if (line.startsWith(boundary))
            {
                boundaries--;
  
                if (boundaries == 0)
                {
                    break;
                }
            }
        }

        while ((line = readLine(in)) != null)
        {
            lOut.writeln(line);
        }
        
        in.close();

        if (boundaries != 0)
        {
            throw new MessagingException("all boundaries not found for: " + boundary);
        }
    }

    static void outputPostamble(LineOutputStream lOut, BodyPart parent, String parentBoundary, BodyPart part)
        throws MessagingException, IOException
    {
        InputStream in;

        try
        {
            in = ((MimeBodyPart)parent).getRawInputStream();
        }
        catch (MessagingException e)
        {
            return;   // no underlying content rely on default generation
        }


        MimeMultipart multipart = (MimeMultipart)part.getContent();
        ContentType contentType = new ContentType(multipart.getContentType());
        String boundary = "--" + contentType.getParameter("boundary");
        int count = multipart.getCount() + 1;
        String line;
        while (count != 0 && (line = readLine(in)) != null)
        {
            if (line.startsWith(boundary))
            {
                count--;
            }
        }

        while ((line = readLine(in)) != null)
        {
            if (line.startsWith(parentBoundary))
            {
                break;
            }
            lOut.writeln(line);
        }

        in.close();
    }

    /*
     * read a line of input stripping of the tailing \r\n
     */
    private static String readLine(InputStream in)
        throws IOException
    {
        StringBuffer b = new StringBuffer();

        int ch;
        while ((ch = in.read()) >= 0 && ch != '\n')
        {
            if (ch != '\r')
            {
                b.append((char)ch);
            }
        }

        if (ch < 0 && b.length() == 0)
        {
            return null;
        }
        
        return b.toString();
    }

    static void outputBodyPart(
        OutputStream out,
        boolean      topLevel,
        BodyPart     bodyPart,
        String       defaultContentTransferEncoding) 
        throws MessagingException, IOException
    {
        if (bodyPart instanceof MimeBodyPart)
        {
            MimeBodyPart    mimePart = (MimeBodyPart)bodyPart;
            String[]        cte = mimePart.getHeader("Content-Transfer-Encoding");
            String          contentTransferEncoding;

            if (isMultipartContent(mimePart))
            {
                Object content = bodyPart.getContent();
                Multipart mp;
                if (content instanceof Multipart)
                {
                    mp = (Multipart)content;
                }
                else
                {
                    mp = new MimeMultipart(bodyPart.getDataHandler().getDataSource());
                }

                ContentType contentType = new ContentType(mp.getContentType());
                String boundary = "--" + contentType.getParameter("boundary");

                SMIMEUtil.LineOutputStream lOut = new SMIMEUtil.LineOutputStream(out);

                Enumeration headers = mimePart.getAllHeaderLines();
                while (headers.hasMoreElements())
                {
                    String header = (String)headers.nextElement();
                    lOut.writeln(header);
                }

                lOut.writeln();      // CRLF separator

                outputPreamble(lOut, mimePart, boundary);

                for (int i = 0; i < mp.getCount(); i++)
                {
                    lOut.writeln(boundary);
                    BodyPart part = mp.getBodyPart(i);
                    outputBodyPart(out, false, part, defaultContentTransferEncoding);
                    if (!isMultipartContent(part))
                    {
                        lOut.writeln();       // CRLF terminator needed
                    }
                    else
                    {                        // output nested preamble
                        outputPostamble(lOut, mimePart, boundary, part);
                    }
                }

                lOut.writeln(boundary + "--");

                if (topLevel)
                {
                    outputPostamble(lOut, mimePart, mp.getCount(), boundary);
                }

                return;
            }

            if (cte == null)
            {
                contentTransferEncoding = defaultContentTransferEncoding;
            }
            else
            {
                contentTransferEncoding = cte[0];
            }

            if (!contentTransferEncoding.equalsIgnoreCase("base64")
                   && !contentTransferEncoding.equalsIgnoreCase("quoted-printable"))
            {
                if (!contentTransferEncoding.equalsIgnoreCase("binary"))
                {
                    out = new CRLFOutputStream(out);
                }
                bodyPart.writeTo(out);
                out.flush();
                return;
            }

            boolean base64 = contentTransferEncoding.equalsIgnoreCase("base64");

            //
            // Write raw content, performing canonicalization
            //
            InputStream inRaw;

            try
            {
                inRaw = mimePart.getRawInputStream();
            }
            catch (MessagingException e)
            {
                // this is less than ideal, but if the raw output stream is unavailable it's the
                // best option we've got.
                out = new CRLFOutputStream(out);
                bodyPart.writeTo(out);
                out.flush();
                return;
            }

            //
            // Write headers
            //
            LineOutputStream outLine = new LineOutputStream(out);
            for (Enumeration e = mimePart.getAllHeaderLines(); e.hasMoreElements();) 
            {
                String header = (String)e.nextElement();
  
                outLine.writeln(header);
            }

            outLine.writeln();
            outLine.flush();


            OutputStream outCRLF;
              
            if (base64)
            {
                outCRLF = new Base64CRLFOutputStream(out);
            }
            else
            {
                outCRLF = new CRLFOutputStream(out);
            }

            byte[]      buf = new byte[BUF_SIZE];

            int len;
            while ((len = inRaw.read(buf, 0, buf.length)) > 0)
            {

                outCRLF.write(buf, 0, len);
            }

            inRaw.close();

            outCRLF.flush();
        }
        else
        {
            if (!defaultContentTransferEncoding.equalsIgnoreCase("binary"))
            {
                out = new CRLFOutputStream(out);
            }

            bodyPart.writeTo(out);

            out.flush();
        }
    }

    /**
     * return the MimeBodyPart described in the raw bytes provided in content
     */
    public static MimeBodyPart toMimeBodyPart(
        byte[]    content)
        throws SMIMEException
    {
        return toMimeBodyPart(new ByteArrayInputStream(content));
    }
    
    /**
     * return the MimeBodyPart described in the input stream content
     */
    public static MimeBodyPart toMimeBodyPart(
        InputStream    content)
        throws SMIMEException
    {
        try
        {
            return new MimeBodyPart(content);
        }
        catch (MessagingException e)
        {
            throw new SMIMEException("exception creating body part.", e);
        }
    }

    static FileBackedMimeBodyPart toWriteOnceBodyPart(
        CMSTypedStream    content)
        throws SMIMEException
    {
        try
        {
            return new WriteOnceFileBackedMimeBodyPart(content.getContentStream(), File.createTempFile("bcMail", ".mime"));
        }
        catch (IOException e)
        {
            throw new SMIMEException("IOException creating tmp file:" + e.getMessage(), e);
        }
        catch (MessagingException e)
        {
            throw new SMIMEException("can't create part: " + e, e);
        }
    }

    /**
     * return a file backed MimeBodyPart described in {@link CMSTypedStream} content.
     */
    public static FileBackedMimeBodyPart toMimeBodyPart(
        CMSTypedStream    content)
        throws SMIMEException
    {
        try
        {
            return toMimeBodyPart(content, File.createTempFile("bcMail", ".mime"));
        }
        catch (IOException e)
        {
            throw new SMIMEException("IOException creating tmp file:" + e.getMessage(), e);
        }
    }
    
    /**
     * Return a file based MimeBodyPart represented by content and backed
     * by the file represented by file.
     * 
     * @param content content stream containing body part.
     * @param file file to store the decoded body part in.
     * @return the decoded body part.
     * @throws SMIMEException
     */
    public static FileBackedMimeBodyPart toMimeBodyPart(
        CMSTypedStream    content,
        File              file)
        throws SMIMEException
    {
        try
        {
            return new FileBackedMimeBodyPart(content.getContentStream(), file);
        }
        catch (IOException e)
        {
            throw new SMIMEException("can't save content to file: " + e, e);
        }
        catch (MessagingException e)
        {
            throw new SMIMEException("can't create part: " + e, e);
        }
    }
    
    /**
     * Return a CMS IssuerAndSerialNumber structure for the passed in X.509 certificate.
     * 
     * @param cert the X.509 certificate to get the issuer and serial number for.
     * @return an IssuerAndSerialNumber structure representing the certificate.
     */
    public static IssuerAndSerialNumber createIssuerAndSerialNumberFor(
        X509Certificate cert)
        throws CertificateParsingException
    {
        try
        {
            return new IssuerAndSerialNumber(new JcaX509CertificateHolder(cert).getIssuer(), cert.getSerialNumber());
        }
        catch (Exception e)
        {
            throw new CertificateParsingException("exception extracting issuer and serial number: " + e);
        }
    }

    private static class WriteOnceFileBackedMimeBodyPart
        extends FileBackedMimeBodyPart
    {
        public WriteOnceFileBackedMimeBodyPart(InputStream content, File file)
            throws MessagingException, IOException
        {
            super(content, file);
        }

        public void writeTo(OutputStream out)
            throws MessagingException, IOException
        {
            super.writeTo(out);

            this.dispose();
        }
    }

    static class Base64CRLFOutputStream extends FilterOutputStream
    {
        protected int lastb;
        protected static byte newline[];
        private boolean isCrlfStream;

        public Base64CRLFOutputStream(OutputStream outputstream)
        {
            super(outputstream);
            lastb = -1;
        }

        public void write(int i)
            throws IOException
        {
            if (i == '\r')
            {
                out.write(newline);
            }
            else if (i == '\n')
            {
                if (lastb != '\r')
                {                                 // imagine my joy...
                    if (!(isCrlfStream && lastb == '\n'))
                    {
                        out.write(newline);
                    }
                }
                else
                {
                    isCrlfStream = true;
                }
            }
            else
            {
                out.write(i);
            }

            lastb = i;
        }

        public void write(byte[] buf)
            throws IOException
        {
            this.write(buf, 0, buf.length);
        }

        public void write(byte buf[], int off, int len)
            throws IOException
        {
            for (int i = off; i != off + len; i++)
            {
                this.write(buf[i]);
            }
        }

        public void writeln()
            throws IOException
        {
            super.out.write(newline);
        }

        static
        {
            newline = new byte[2];
            newline[0] = '\r';
            newline[1] = '\n';
        }
    }
}
