All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.subethamail.smtp.server.SMTPCodecDecoder Maven / Gradle / Ivy

/*
 *  Licensed to the Apache Software Foundation (ASF) under one
 *  or more contributor license agreements.  See the NOTICE file
 *  distributed with this work for additional information
 *  regarding copyright ownership.  The ASF licenses this file
 *  to you under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing,
 *  software distributed under the License is distributed on an
 *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 *  KIND, either express or implied.  See the License for the
 *  specific language governing permissions and limitations
 *  under the License.
 *
 */
package org.subethamail.smtp.server;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;

import javax.mail.util.SharedByteArrayInputStream;

import org.apache.mina.common.BufferDataException;
import org.apache.mina.common.ByteBuffer;
import org.apache.mina.common.IoSession;
import org.apache.mina.filter.codec.ProtocolDecoder;
import org.apache.mina.filter.codec.ProtocolDecoderOutput;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.subethamail.smtp.server.io.SharedTmpFileInputStream;

/**
 * A {@link ProtocolDecoder} which decodes incoming SMTP data based on session context.
 * 
 * @author De Oliveira Edouard <[email protected]>
 */
public class SMTPCodecDecoder implements ProtocolDecoder 
{
	private final static Logger log = LoggerFactory.getLogger(SMTPCodecDecoder.class);

    private final static String CONTEXT = SMTPCodecDecoder.class.getName()+ ".context";

	private final static String TMPFILE_PREFIX = "subetha";
	private final static String TMPFILE_SUFFIX = ".eml";

    private final static byte[] SMTP_CMD_DELIMITER = new byte[] {'\r','\n'};
    private final static byte[] SMTP_DATA_DELIMITER = new byte[] {'\r','\n', '.', '\r','\n'};

    private final Charset charset;

    /**
     * RFC 2822
     */
    private int maxLineLength = 998;
    
    /** When to trigger */
    private int threshold;    

    /**
	 * Creates a new instance with the specified charset and the
	 * specified thresholdBytes deferring size.
	 */
    public SMTPCodecDecoder(Charset charset, int thresholdBytes) 
    {
        if (charset == null) 
        {
            throw new NullPointerException("charset");
        }

        this.charset = charset;
        this.threshold = thresholdBytes;       
    }

    /** */
	public void setDataDeferredSize(int dataDeferredSize) 
	{
		this.threshold = dataDeferredSize;
	}
	
	/** */
    public static byte[] asArray(ByteBuffer b) 
    {
    	int l = b.remaining();
    	byte[] array = new byte[l];
    	b.get(array, 0, l);
	    
	    return array;
    }
    
    /**
	 * Returns the allowed maximum size of the line to be decoded. If the size
	 * of the line to be decoded exceeds this value, the decoder will throw a
	 * {@link BufferDataException}. The default value is 1024 (1KB).
	 */
    public int getMaxLineLength() 
    {
        return maxLineLength;
    }

    /**
	 * Sets the allowed maximum size of the line to be decoded. If the size of
	 * the line to be decoded exceeds this value, the decoder will throw a
	 * {@link BufferDataException}. The default value is 1024 (1KB).
	 */
    public void setMaxLineLength(int maxLineLength) 
    {
        if (maxLineLength <= 0) 
        {
            throw new IllegalArgumentException("maxLineLength: "+ maxLineLength);
        }

        this.maxLineLength = maxLineLength;
    }

    /** */
    private Context getContext(IoSession session) 
    {
        Context ctx = (Context) session.getAttribute(CONTEXT);
        if (ctx == null) 
        {
            ctx = new Context();
            session.setAttribute(CONTEXT, ctx);
        }
        return ctx;
    }

    /** */
    public void finishDecode(IoSession session, ProtocolDecoderOutput out)
            throws Exception 
    {
    }

    /** */
    public void dispose(IoSession session) 
    	throws Exception 
    {
        Context ctx = (Context) session.getAttribute(CONTEXT);
        if (ctx != null) 
        {
            ctx.getBuffer().release();
            ctx.closeOutputStream();
            session.removeAttribute(CONTEXT);
        }
    }

    /** */
    public void decode(IoSession session, ByteBuffer in, ProtocolDecoderOutput out)
            throws Exception 
    {
    	Context ctx = getContext(session);
        int matchCount = ctx.getMatchCount();
        
        ConnectionHandler.Context minaCtx = (ConnectionHandler.Context) 
        	session.getAttribute(ConnectionHandler.CONTEXT_ATTRIBUTE);

        boolean dataMode = minaCtx.getSession().isDataMode();
        byte[] delimBuf;
        
		if (dataMode) 
		{
			delimBuf = SMTP_DATA_DELIMITER;
		}
		else 
		{
            delimBuf = SMTP_CMD_DELIMITER;
        }

        // Try to find a match
        int oldPos = in.position();
        int oldLimit = in.limit();
        
        if (matchCount == delimBuf.length)
        	matchCount = 0;
        
        while (in.remaining() > 0) 
        {
            byte b = in.get();
            if (delimBuf[matchCount] == b) 
            {
                matchCount++;
                if (matchCount == delimBuf.length) 
                {
                    // Found a match.
                    int pos = in.position();
                    in.limit(pos);
                    in.position(oldPos);

                   	ctx.write(dataMode, in);
                    
                    in.limit(oldLimit);
                    in.position(pos);

                    if (ctx.getOverflowPosition() == 0) 
                    {
                    	ByteBuffer buf = ctx.getBuffer();
                		buf.flip();
                		
                		if (!dataMode)
                		{
                			buf.limit(buf.limit() - matchCount);
                		}
                		
                        try 
                        {
                        	if (dataMode)
                        	{
                        		delimBuf = SMTP_CMD_DELIMITER;
                        		out.write(ctx.getInputStream());
                        		ctx.compactBuffer();
                        	}
                        	else
                        	{
                        		out.write(buf.getString(ctx.getDecoder()));
                        	}
                        }
                        catch (IOException ioex) 
                        {
                        	throw new CharacterCodingException();
                        } 
                        finally 
                        {                       	
                            buf.clear();
                        }
                    } 
                    else 
                    {
                        int overflowPosition = ctx.getOverflowPosition();
                        ctx.reset();
                        throw new BufferDataException("Line is too long: " + overflowPosition);
                    }

                    oldPos = pos;
                    matchCount = 0;
                }
            } 
            else 
            {
				// fix for DIRMINA-506
				in.position(Math.max(0, in.position() - matchCount));
                matchCount = 0;
            }
        }

        // Put remainder to buf.
        in.position(oldPos);
        ctx.write(dataMode, in);

        ctx.setMatchCount(matchCount);
    }

    private class Context 
    {
        private final CharsetDecoder decoder;
        private ByteBuffer buf;
        private int matchCount = 0;
        private int overflowPosition = 0;
    	private boolean thresholdReached = false;        

    	/** If we switch to file output, this is the file. */
    	File outFile;

    	/** If we switch to file output, this is the stream to write to the file. */ 
    	FileOutputStream stream;
    	
        private Context() 
        {
            decoder = charset.newDecoder();
            buf = ByteBuffer.allocate(80).setAutoExpand(true);
        }

        /** */
        public CharsetDecoder getDecoder() 
        {
            return decoder;
        }

        /** */
        public ByteBuffer getBuffer() 
        {
            return buf;
        }
        
        /** */
        public void compactBuffer() 
        {
        	buf.clear();
        	if (buf.capacity() > getMaxLineLength()) 
        	{
        		buf.release();
        		buf = ByteBuffer.allocate(80).setAutoExpand(true);
        	}        	
        }
        
        /** */
        public int getOverflowPosition() 
        {
            return overflowPosition;
        }
        
        /** */
        public int getMatchCount() 
        {
            return matchCount;
        }

        /** */
        public void setMatchCount(int matchCount) 
        {
            this.matchCount = matchCount;
        }
        
        /** */
        public void reset() 
        {
            overflowPosition = 0;
            matchCount = 0;
            decoder.reset();
        }
        
        /** */
        public void write(boolean dataMode, ByteBuffer b) 
        	throws IOException
        {
    		if (dataMode)
    		{
    			write(asArray(b));
    		}
    		else
    		{
    			append(b);
    		}
        }
        
        /** */
    	public void write(byte[] src) 
    		throws IOException
    	{
    		int predicted = this.thresholdReached ? 0 : this.buf.position() + src.length;
    		
    		// Checks whether reading count bytes would cross the limit.
    		if (this.thresholdReached || predicted > threshold)
    		{
    			// If previously hit, then use the stream.
    			if (!this.thresholdReached)
    			{
    				thresholdReached(this.buf.position(), predicted);
    				this.thresholdReached = true;
    				compactBuffer();
    			}
    			
				this.stream.write(src);
    		}
    		else
    		{
    			this.buf.put(src);
    		}
    	}
    	
    	/**
		 * Called when the threshold is about to be exceeded. Once called, it
		 * won't be called again.
		 * 
		 * @param current
		 *            is the current number of bytes that have been written
		 * @param predicted
		 *            is the total number after the write completes
		 */
    	private void thresholdReached(int current, int predicted) 
    		throws IOException
    	{
    		this.outFile = File.createTempFile(TMPFILE_PREFIX, TMPFILE_SUFFIX);
    		if (log.isDebugEnabled())
    		{
    			log.debug("Writing message to file : " + outFile.getAbsolutePath());
    		}
    		
    		this.stream = new FileOutputStream(this.outFile);
    		this.buf.flip();
    		this.stream.write(asArray(this.buf));
    		log.debug("ByteBuffer written to stream");
    	}
    	
    	/** */
    	public void closeOutputStream() throws IOException
    	{
    		if (this.stream != null)
    		{
    			this.stream.flush();
    			this.stream.close();
    			log.debug("Temp file writing achieved");
    		}
    	}
    	
    	/** */
    	public InputStream getInputStream() throws IOException
    	{		
    		if (this.thresholdReached)
    		{
    			return new SharedTmpFileInputStream(this.outFile);
    		}
    		else
    		{
    			return new SharedByteArrayInputStream(asArray(this.buf));
    		}
    	}
    	
    	/** */
        public void append(ByteBuffer in) throws CharacterCodingException 
        {
            if (overflowPosition != 0) 
            {
                discard(in);
            }
            else 
            {
            	int pos = buf.position();
            	if ( pos > maxLineLength - in.remaining()) 
            	{
                    overflowPosition = pos;
                    buf.clear();
                    discard(in);
            	} 
            	else 
            	{
            		this.buf.put(in);
            	}
            }
        }

        /** */
        private void discard(ByteBuffer in) 
        {
            if (Integer.MAX_VALUE - in.remaining() < overflowPosition) 
            {
                overflowPosition = Integer.MAX_VALUE;
            } 
            else 
            {
                overflowPosition += in.remaining();
            }
            in.position(in.limit());
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy