/*! ******************************************************************************
 *
 * Pentaho Data Integration
 *
 * Copyright (C) 2002-2018 by Hitachi Vantara : http://www.pentaho.com
 *
 *******************************************************************************
 *
 * Licensed 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.pentaho.di.job.entries.getpop;

import com.google.common.annotations.VisibleForTesting;
import com.sun.mail.imap.IMAPSSLStore;
import com.sun.mail.pop3.POP3SSLStore;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.io.IOUtils;
import org.pentaho.di.core.Const;
import org.pentaho.di.core.exception.KettleException;
import org.pentaho.di.core.logging.LogChannelInterface;
import org.pentaho.di.core.util.Utils;
import org.pentaho.di.core.vfs.KettleVFS;
import org.pentaho.di.i18n.BaseMessages;

import javax.mail.Flags;
import javax.mail.Flags.Flag;
import javax.mail.Folder;
import javax.mail.Message;
import javax.mail.MessagingException;
import javax.mail.Multipart;
import javax.mail.Part;
import javax.mail.Session;
import javax.mail.Store;
import javax.mail.URLName;
import javax.mail.internet.MimeUtility;
import javax.mail.search.AndTerm;
import javax.mail.search.BodyTerm;
import javax.mail.search.ComparisonTerm;
import javax.mail.search.FlagTerm;
import javax.mail.search.FromStringTerm;
import javax.mail.search.NotTerm;
import javax.mail.search.ReceivedDateTerm;
import javax.mail.search.RecipientStringTerm;
import javax.mail.search.SearchTerm;
import javax.mail.search.SubjectTerm;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Date;
import java.util.HashSet;
import java.util.Properties;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * MailConnection handles the process of connecting to, reading from POP3/IMAP.
 *
 * @author Samatar
 * @since 01-04-2009
 *
 */

public class MailConnection {
  private static Class<?> PKG = JobEntryGetPOP.class; // for i18n purposes, needed by Translator2!!

  /**
   * Target mail server.
   */
  private String server;

  private int port;
  private String username;
  private String password;
  private boolean usessl;
  private boolean write;
  private boolean useproxy;
  private String proxyusername;
  /**
   * Protocol used. Should be PROTOCOL_POP3 (0) for POP3 and PROTOCOL_IMAP (1) to IMAP
   */
  private int protocol;

  private Properties prop;
  private Session session = null;
  private Store store = null;
  private Folder folder = null;
  /**
   * Contains the list of retrieved messages
   */
  private Message[] messages;
  /**
   * Contains the current message
   */
  private Message message;
  private SearchTerm searchTerm = null;

  /**
   * Counts the number of message fetched
   */
  private int messagenr;

  /**
   * Counts the number of message saved in a file
   */
  private int nrSavedMessages;

  /**
   * Counts the number of message move to a folder
   */
  private int nrMovedMessages;

  /**
   * Counts the number of message deleted
   */
  private int nrDeletedMessages;

  /**
   * Counts the number of attached files saved in a file
   */
  private int nrSavedAttachedFiles;

  /**
   * IMAP folder if user want to move some messages
   */
  private Folder destinationIMAPFolder = null;

  private LogChannelInterface log;

  /**
   * Construct a new Database MailConnection
   *
   * @param protocol
   *          the protocol used : MailConnection.PROTOCOL_POP3 or MailConnection.PROTOCOL_IMAP.
   * @param server
   *          the target server (ip ou name)
   * @param port
   *          port number on the server
   * @param password
   * @param usessl
   *          specify if the connection is established via SSL
   * @param useproxy
   *          specify if we use proxy authentication
   * @param proxyusername
   *          proxy authorised user
   */
  public MailConnection( LogChannelInterface log, int protocol, String server, int port, String username,
    String password, boolean usessl, boolean useproxy, String proxyusername ) throws KettleException {

    this.log = log;

    // Get system properties
    try {
      this.prop = System.getProperties();
    } catch ( SecurityException s ) {
      this.prop = new Properties();
    }

    this.port = port;
    this.server = server;
    this.username = username;
    this.password = password;
    this.usessl = usessl;
    this.protocol = protocol;
    this.nrSavedMessages = 0;
    this.nrDeletedMessages = 0;
    this.nrMovedMessages = 0;
    this.nrSavedAttachedFiles = 0;
    this.messagenr = -1;
    this.useproxy = useproxy;
    this.proxyusername = proxyusername;

    try {

      if ( useproxy ) {
        // Need here to pass a proxy
        // use SASL authentication
        this.prop.put( "mail.imap.sasl.enable", "true" );
        this.prop.put( "mail.imap.sasl.authorizationid", proxyusername );
      }

      if ( protocol == MailConnectionMeta.PROTOCOL_POP3 ) {
        this.prop.setProperty( "mail.pop3s.rsetbeforequit", "true" );
        this.prop.setProperty( "mail.pop3.rsetbeforequit", "true" );
      } else if ( protocol == MailConnectionMeta.PROTOCOL_MBOX ) {
        this.prop.setProperty( "mstor.mbox.metadataStrategy", "none" ); // mstor.mbox.metadataStrategy={none|xml|yaml}
        this.prop.setProperty( "mstor.cache.disabled", "true" ); // prevent diskstore fail
      }

      String protocolString =
        ( protocol == MailConnectionMeta.PROTOCOL_POP3 ) ? "pop3" : protocol == MailConnectionMeta.PROTOCOL_MBOX
          ? "mstor" : "imap";
      if ( usessl && protocol != MailConnectionMeta.PROTOCOL_MBOX ) {
        // Supports IMAP/POP3 connection with SSL, the connection is established via SSL.
        this.prop
          .setProperty( "mail." + protocolString + ".socketFactory.class", "javax.net.ssl.SSLSocketFactory" );
        this.prop.setProperty( "mail." + protocolString + ".socketFactory.fallback", "false" );
        this.prop.setProperty( "mail." + protocolString + ".port", "" + port );
        this.prop.setProperty( "mail." + protocolString + ".socketFactory.port", "" + port );

        // Create session object
        this.session = Session.getInstance( this.prop, null );
        this.session.setDebug( log.isDebug() );
        if ( this.port == -1 ) {
          this.port =
            ( ( protocol == MailConnectionMeta.PROTOCOL_POP3 )
              ? MailConnectionMeta.DEFAULT_SSL_POP3_PORT : MailConnectionMeta.DEFAULT_SSL_IMAP_PORT );
        }
        URLName url = new URLName( protocolString, server, port, "", username, password );
        this.store =
          ( protocol == MailConnectionMeta.PROTOCOL_POP3 )
            ? new POP3SSLStore( this.session, url ) : new IMAPSSLStore( this.session, url );
        url = null;
      } else {
        this.session = Session.getInstance( this.prop, null );
        this.session.setDebug( log.isDebug() );
        if ( protocol == MailConnectionMeta.PROTOCOL_MBOX ) {
          this.store = this.session.getStore( new URLName( protocolString + ":" + server ) );
        } else {
          this.store = this.session.getStore( protocolString );
        }
      }

      if ( log.isDetailed() ) {
        log.logDetailed( BaseMessages.getString( PKG, "JobGetMailsFromPOP.NewConnectionDefined" ) );
      }
    } catch ( Exception e ) {
      throw new KettleException( BaseMessages.getString( PKG, "JobGetMailsFromPOP.Error.NewConnection", Const.NVL(
        this.server, "" ) ), e );
    }
  }

  /**
   * @return Returns the connection status. true if the connection is still opened
   */
  public boolean isConnected() {
    return ( this.store != null && this.store.isConnected() );
  }

  /**
   * @return Returns the use of SSL. true if the connection use SSL
   */
  public boolean isUseSSL() {
    return this.usessl;
  }

  /**
   * @return Returns the use of proxy. true if the connection use proxy
   */
  public boolean isUseProxy() {
    return this.useproxy;
  }

  /**
   * @return Returns the proxy username.
   */
  public String getProxyUsername() {
    return this.proxyusername;
  }

  /**
   * @return Returns the store
   *
   */
  public Store getStore() {
    return this.store;
  }

  /**
   * @return Returns the folder
   *
   */
  public Folder getFolder() {
    return this.folder;
  }

  /**
   * Open the connection.
   *
   * @throws KettleException
   *           if something went wrong.
   */
  public void connect() throws KettleException {
    if ( log.isDetailed() ) {
      log.logDetailed( BaseMessages.getString(
        PKG, "JobGetMailsFromPOP.Connecting", this.server, this.username, "" + this.port ) );
    }
    try {
      if ( this.usessl || this.protocol == MailConnectionMeta.PROTOCOL_MBOX ) {
        // Supports IMAP/POP3 connection with SSL,
        // the connection is established via SSL.
        this.store.connect();
      } else {
        if ( this.port > -1 ) {
          this.store.connect( this.server, this.port, this.username, this.password );
        } else {
          this.store.connect( this.server, this.username, this.password );
        }
      }
      if ( log.isDetailed() ) {
        log.logDetailed( BaseMessages.getString(
          PKG, "JobGetMailsFromPOP.Connected", this.server, this.username, "" + this.port ) );
      }
    } catch ( Exception e ) {
      throw new KettleException(
        BaseMessages.getString( PKG, "JobGetMailsFromPOP.Error.Connecting", this.server, this.username, Const
          .NVL( "" + this.port, "" ) ), e );
    }
  }

  /**
   * Open the default folder (INBOX)
   *
   * @param write
   *          open the folder in write mode
   * @throws KettleException
   *           if something went wrong.
   */
  public void openFolder( boolean write ) throws KettleException {
    openFolder( null, true, write );
  }

  /**
   * Open the folder.
   *
   * @param foldername
   *          the name of the folder to open
   * @param write
   *          open the folder in write mode
   * @throws KettleException
   *           if something went wrong.
   */
  public void openFolder( String foldername, boolean write ) throws KettleException {
    openFolder( foldername, false, write );
  }

  /**
   * Open the folder.
   *
   * @param foldername
   *          the name of the folder to open
   * @param defaultFolder
   *          true to open the default folder (INBOX)
   * @param write
   *          open the folder in write mode
   * @throws KettleException
   *           if something went wrong.
   */
  public void openFolder( String foldername, boolean defaultFolder, boolean write ) throws KettleException {
    this.write = write;
    try {
      if ( getFolder() != null ) {
        // A folder is already opened
        // before make sure to close it
        closeFolder( true );
      }

      if ( defaultFolder ) {
        if ( protocol == MailConnectionMeta.PROTOCOL_MBOX ) {
          this.folder = this.store.getDefaultFolder();
        } else {
          // get the default folder
          this.folder = getRecursiveFolder( MailConnectionMeta.INBOX_FOLDER );
        }

        if ( this.folder == null ) {
          throw new KettleException( BaseMessages.getString( PKG, "JobGetMailsFromPOP.InvalidDefaultFolder.Label" ) );
        }

        if ( ( folder.getType() & Folder.HOLDS_MESSAGES ) == 0 ) {
          throw new KettleException( BaseMessages.getString( PKG, "MailConnection.DefaultFolderCanNotHoldMessage" ) );
        }
      } else {
        // Open specified Folder (for IMAP/MBOX)
        if ( this.protocol == MailConnectionMeta.PROTOCOL_IMAP
          || this.protocol == MailConnectionMeta.PROTOCOL_MBOX ) {
          this.folder = getRecursiveFolder( foldername );
        }
        if ( this.folder == null || !this.folder.exists() ) {
          throw new KettleException( BaseMessages.getString( PKG, "JobGetMailsFromPOP.InvalidFolder.Label" ) );
        }
      }
      if ( this.write ) {
        if ( log.isDebug() ) {
          log.logDebug( BaseMessages.getString(
            PKG, "MailConnection.OpeningFolderInWriteMode.Label", getFolderName() ) );
        }
        this.folder.open( Folder.READ_WRITE );
      } else {
        if ( log.isDebug() ) {
          log.logDebug( BaseMessages.getString(
            PKG, "MailConnection.OpeningFolderInReadMode.Label", getFolderName() ) );
        }
        this.folder.open( Folder.READ_ONLY );
      }

      if ( log.isDetailed() ) {
        log.logDetailed( BaseMessages.getString( PKG, "JobGetMailsFromPOP.FolderOpened.Label", getFolderName() ) );
      }
      if ( log.isDebug() ) {
        // display some infos on folder
        //CHECKSTYLE:LineLength:OFF
        log.logDebug( BaseMessages.getString( PKG, "JobGetMailsFromPOP.FolderOpened.Name", getFolderName() ) );
        log.logDebug( BaseMessages.getString( PKG, "JobGetMailsFromPOP.FolderOpened.FullName", this.folder.getFullName() ) );
        log.logDebug( BaseMessages.getString( PKG, "JobGetMailsFromPOP.FolderOpened.Url", this.folder.getURLName().toString() ) );
        log.logDebug( BaseMessages.getString( PKG, "JobGetMailsFromPOP.FolderOpened.Subscribed", "" + this.folder.isSubscribed() ) );
      }

    } catch ( Exception e ) {
      throw new KettleException( defaultFolder
        ? BaseMessages.getString( PKG, "JobGetMailsFromPOP.Error.OpeningDefaultFolder" )
        : BaseMessages.getString( PKG, "JobGetMailsFromPOP.Error.OpeningFolder", foldername ), e );
    }
  }

  private Folder getRecursiveFolder( String foldername ) throws MessagingException {
    Folder dfolder;
    String[] folderparts = foldername.split( "/" );
    dfolder = this.getStore().getDefaultFolder();
    // Open destination folder
    for ( int i = 0; i < folderparts.length; i++ ) {
      dfolder = dfolder.getFolder( folderparts[i] );
    }
    return dfolder;
  }

  /**
   * Clear search terms.
   */
  public void clearFilters() {
    this.nrSavedMessages = 0;
    this.nrDeletedMessages = 0;
    this.nrMovedMessages = 0;
    this.nrSavedAttachedFiles = 0;
    if ( this.searchTerm != null ) {
      this.searchTerm = null;
    }
  }

  /**
   * Disconnect from the server and close folder, connection.
   *
   * @throws KettleException
   */
  public void disconnect() throws KettleException {
    disconnect( true );
  }

  /**
   * Close folder.
   *
   * @param expunge
   *          expunge folder
   * @throws KettleException
   */
  public void closeFolder( boolean expunge ) throws KettleException {
    try {
      if ( this.folder != null && this.folder.isOpen() ) {
        if ( log.isDebug() ) {
          log.logDebug( BaseMessages.getString( PKG, "MailConnection.ClosingFolder", getFolderName() ) );
        }
        this.folder.close( expunge );
        this.folder = null;
        this.messages = null;
        this.message = null;
        this.messagenr = -1;
        if ( log.isDebug() ) {
          log.logDebug( BaseMessages.getString( PKG, "MailConnection.FolderClosed", getFolderName() ) );
        }
      }
    } catch ( Exception e ) {
      throw new KettleException( BaseMessages.getString(
        PKG, "JobGetMailsFromPOP.Error.ClosingFolder", getFolderName() ), e );
    }
  }

  /**
   * Add search term.
   *
   * @param term
   *          search term to add
   */
  private void addSearchTerm( SearchTerm term ) {
    if ( this.searchTerm != null ) {
      this.searchTerm = new AndTerm( this.searchTerm, term );
    } else {
      this.searchTerm = term;
    }
  }

  public SearchTerm getSearchTerm() {
    return searchTerm;
  }

  /**
   * Set filter on subject.
   *
   * @param subject
   *          messages will be filtered on subject
   * @param notTerm
   *          negate condition
   */
  public void setSubjectTerm( String subject, boolean notTerm ) {
    if ( !Utils.isEmpty( subject ) ) {
      if ( notTerm ) {
        addSearchTerm( new NotTerm( new SubjectTerm( subject ) ) );
      } else {
        addSearchTerm( new SubjectTerm( subject ) );
      }
    }
  }

  /**
   * Search all messages with body containing the word bodyfilter
   *
   * @param bodyfilter
   * @param notTerm
   *          negate condition
   */
  public void setBodyTerm( String bodyfilter, boolean notTerm ) {
    if ( !Utils.isEmpty( bodyfilter ) ) {
      if ( notTerm ) {
        addSearchTerm( new NotTerm( new BodyTerm( bodyfilter ) ) );
      } else {
        addSearchTerm( new BodyTerm( bodyfilter ) );
      }
    }
  }

  /**
   * Set filter on message sender.
   *
   * @param sender
   *          messages will be filtered on sender
   * @param notTerm
   *          negate condition
   */
  public void setSenderTerm( String sender, boolean notTerm ) {
    if ( !Utils.isEmpty( sender ) ) {
      if ( notTerm ) {
        addSearchTerm( new NotTerm( new FromStringTerm( sender ) ) );
      } else {
        addSearchTerm( new FromStringTerm( sender ) );
      }
    }
  }

  /**
   * Set filter on receipient.
   *
   * @param receipient
   *          messages will be filtered on receipient
   */
  public void setReceipientTerm( String receipient ) {
    if ( !Utils.isEmpty( receipient ) ) {
      addSearchTerm( new RecipientStringTerm( Message.RecipientType.TO, receipient ) );
    }
  }

  /**
   * Set filter on message received date.
   *
   * @param receiveddate
   *          messages will be filtered on receiveddate
   */
  public void setReceivedDateTermEQ( Date receiveddate ) {
    if ( this.protocol == MailConnectionMeta.PROTOCOL_POP3 ) {
      log.logError( BaseMessages.getString( PKG, "MailConnection.Error.ReceivedDatePOP3Unsupported" ) );
    } else {
      addSearchTerm( new ReceivedDateTerm( ComparisonTerm.EQ, receiveddate ) );
    }
  }

  /**
   * Set filter on message received date.
   *
   * @param futureDate
   *          messages will be filtered on futureDate
   */
  public void setReceivedDateTermLT( Date futureDate ) {
    if ( this.protocol == MailConnectionMeta.PROTOCOL_POP3 ) {
      log.logError( BaseMessages.getString( PKG, "MailConnection.Error.ReceivedDatePOP3Unsupported" ) );
    } else {
      addSearchTerm( new ReceivedDateTerm( ComparisonTerm.LT, futureDate ) );
    }
  }

  /**
   * Set filter on message received date.
   *
   * @param pastDate
   *          messages will be filtered on pastDate
   */
  public void setReceivedDateTermGT( Date pastDate ) {
    if ( this.protocol == MailConnectionMeta.PROTOCOL_POP3 ) {
      log.logError( BaseMessages.getString( PKG, "MailConnection.Error.ReceivedDatePOP3Unsupported" ) );
    } else {
      addSearchTerm( new ReceivedDateTerm( ComparisonTerm.GT, pastDate ) );
    }
  }

  public void setReceivedDateTermBetween( Date beginDate, Date endDate ) {
    if ( this.protocol == MailConnectionMeta.PROTOCOL_POP3 ) {
      log.logError( BaseMessages.getString( PKG, "MailConnection.Error.ReceivedDatePOP3Unsupported" ) );
    } else {
      addSearchTerm( new AndTerm( new ReceivedDateTerm( ComparisonTerm.LT, endDate ), new ReceivedDateTerm(
        ComparisonTerm.GT, beginDate ) ) );
    }
  }

  public void setFlagTermNew() {
    addSearchTerm( new FlagTerm( new Flags( Flags.Flag.RECENT ), true ) );
  }

  public void setFlagTermOld() {
    addSearchTerm( new FlagTerm( new Flags( Flags.Flag.RECENT ), false ) );
  }

  public void setFlagTermRead() {
    addSearchTerm( new FlagTerm( new Flags( Flags.Flag.SEEN ), true ) );
  }

  public void setFlagTermUnread() {
    addSearchTerm( new FlagTerm( new Flags( Flags.Flag.SEEN ), false ) );
  }

  public void setFlagTermFlagged() {
    addSearchTerm( new FlagTerm( new Flags( Flags.Flag.FLAGGED ), true ) );
  }

  public void setFlagTermNotFlagged() {
    addSearchTerm( new FlagTerm( new Flags( Flags.Flag.FLAGGED ), false ) );
  }

  public void setFlagTermDraft() {
    addSearchTerm( new FlagTerm( new Flags( Flags.Flag.DRAFT ), true ) );
  }

  public void setFlagTermNotDraft() {
    addSearchTerm( new FlagTerm( new Flags( Flags.Flag.DRAFT ), false ) );
  }

  /**
   * Retrieve all messages from server
   *
   * @throws KettleException
   */
  public void retrieveMessages() throws KettleException {
    try {
      // search term?
      if ( this.searchTerm != null ) {
        this.messages = this.folder.search( this.searchTerm );
      } else {
        this.messages = this.folder.getMessages();
      }
    } catch ( Exception e ) {
      this.messages = null;
      throw new KettleException( BaseMessages.getString(
        PKG, "MailConnection.Error.RetrieveMessages", getFolderName() ), e );
    }
  }

  /*
   * public void retrieveUnreadMessages() throws Exception { if(this.protocol==PROTOCOL_POP3) throw new
   * KettleException("Cette fonction est uniquement accessible pour le protocol POP3!"); try { Message msgsAll[]; //
   * search term? if(this.searchTerm!=null) { msgsAll = this.folder.search(this.searchTerm); }else { msgsAll =
   * this.folder.getMessages(); } int unreadMsgs = this.folder.getUnreadMessageCount(); int msgCount = msgsAll.length;
   *
   * this.messages = this.folder.getMessages(msgCount - unreadMsgs + 1, msgCount); } catch (Exception e) {
   * this.messages= null; } }
   */

  /**
   * Disconnect from the server and close folder, connection.
   *
   * @param expunge
   *          expunge folder
   * @throws KettleException
   */
  public void disconnect( boolean expunge ) throws KettleException {
    if ( log.isDebug() ) {
      log.logDebug( BaseMessages.getString( PKG, "MailConnection.ClosingConnection" ) );
    }
    try {
      // close the folder, passing in a true value to expunge the deleted message
      closeFolder( expunge );
      clearFilters();
      if ( this.store != null ) {
        this.store.close();
        this.store = null;
      }
      if ( this.session != null ) {
        this.session = null;
      }
      if ( this.destinationIMAPFolder != null ) {
        this.destinationIMAPFolder.close( expunge );
      }

      if ( log.isDebug() ) {
        log.logDebug( BaseMessages.getString( PKG, "MailConnection.ConnectionClosed" ) );
      }
    } catch ( Exception e ) {
      throw new KettleException( BaseMessages.getString( PKG, "JobGetMailsFromPOP.Error.ClosingConnection" ), e );
    }
  }

  /**
   * Export message content to a filename.
   *
   * @param filename
   *          the target filename
   * @param foldername
   *          the parent folder of filename
   * @throws KettleException
   */

  public void saveMessageContentToFile( String filename, String foldername ) throws KettleException {
    OutputStream os = null;
    try {
      os = KettleVFS.getOutputStream( foldername + ( foldername.endsWith( "/" ) ? "" : "/" ) + filename, false );
      getMessage().writeTo( os );
      updateSavedMessagesCounter();
    } catch ( Exception e ) {
      throw new KettleException( BaseMessages.getString( PKG, "MailConnection.Error.SavingMessageContent", ""
        + this.message.getMessageNumber(), filename, foldername ), e );
    } finally {
      if ( os != null ) {
        IOUtils.closeQuietly( os );
      }
    }
  }

  /**
   * Save attached files to a folder.
   *
   * @param foldername
   *          the target foldername
   * @throws KettleException
   */
  public void saveAttachedFiles( String foldername ) throws KettleException {
    saveAttachedFiles( foldername, null );
  }

  /**
   * Save attached files to a folder.
   *
   * @param foldername
   *          the target foldername
   * @param pattern
   *          regular expression to filter on files
   * @throws KettleException
   */
  public void saveAttachedFiles( String foldername, Pattern pattern ) throws KettleException {
    Object content = null;
    try {
      content = getMessage().getContent();
      if ( content instanceof Multipart ) {
        handleMultipart( foldername, (Multipart) content, pattern );
      }
    } catch ( Exception e ) {
      throw new KettleException( BaseMessages.getString( PKG, "MailConnection.Error.SavingAttachedFiles", ""
        + this.message.getMessageNumber(), foldername ), e );
    } finally {
      if ( content != null ) {
        content = null;
      }
    }
  }

  private void handleMultipart( String foldername, Multipart multipart, Pattern pattern ) throws KettleException {
    try {
      for ( int i = 0, n = multipart.getCount(); i < n; i++ ) {
        handlePart( foldername, multipart.getBodyPart( i ), pattern );
      }
    } catch ( Exception e ) {
      throw new KettleException( e );
    }
  }

  private void handlePart( String foldername, Part part, Pattern pattern ) throws KettleException {
    try {
      String disposition = part.getDisposition();

      // The RFC2183 doesn't REQUIRE Content-Disposition header field so we'll create one to
      // fake out the code below.
      if ( disposition == null || disposition.length() < 1 ) {
        disposition = Part.ATTACHMENT;
      }

      if ( disposition.equalsIgnoreCase( Part.ATTACHMENT ) || disposition.equalsIgnoreCase( Part.INLINE ) ) {
        String MimeText = null;
        try {
          MimeText = MimeUtility.decodeText( part.getFileName() );
        } catch ( Exception e ) {
          // Ignore errors
        }
        if ( MimeText != null ) {
          String filename = MimeUtility.decodeText( part.getFileName() );
          if ( isWildcardMatch( filename, pattern ) ) {
            // Save file
            saveFile( foldername, filename, part.getInputStream() );
            updateSavedAttachedFilesCounter();
            if ( log.isDetailed() ) {
              log.logDetailed( BaseMessages.getString( PKG, "JobGetMailsFromPOP.AttachedFileSaved", filename, ""
                + getMessage().getMessageNumber(), foldername ) );
            }
          }
        }
      }
    } catch ( Exception e ) {
      throw new KettleException( e );
    }
  }

  @VisibleForTesting
  static String findValidTarget( String folderName, final String fileName ) throws KettleException {
    if ( fileName == null || folderName == null ) {
      throw new IllegalArgumentException( "Cannot have null arguments to findValidTarget" );
    }
    String fileNameRoot = FilenameUtils.getBaseName( fileName ), ext = "." + FilenameUtils.getExtension( fileName );
    if ( ( ext.length() == 1 ) ) { // only a "."
      ext = "";
    }
    String rtn = "", base = FilenameUtils.concat( folderName, fileNameRoot );
    int baseSz = base.length();
    StringBuilder build = new StringBuilder( baseSz ).append( base );
    int i = -1;
    do {
      i++;
      build.setLength( baseSz ); // bring string back to size
      build.append( i > 0 ? Integer.toString( i ) : "" ).append( ext );
      rtn = build.toString();
    } while ( KettleVFS.fileExists( rtn ) );

    return rtn;
  }

  private static void saveFile( String foldername, String filename, InputStream input ) throws KettleException {
    OutputStream fos = null;
    BufferedOutputStream bos = null;
    BufferedInputStream bis = null;
    try {
      // Do no overwrite existing file
      String targetFileName;
      if ( filename == null ) {
        File f = File.createTempFile( "xx", ".out" );
        f.deleteOnExit(); // Clean up file
        filename = f.getName();
        targetFileName = foldername + "/" + filename; // Note - createTempFile Used - so will be unique
      } else {
        targetFileName = findValidTarget( foldername, filename );
      }
      fos = KettleVFS.getOutputStream( targetFileName, false );
      bos = new BufferedOutputStream( fos );
      bis = new BufferedInputStream( input );
      IOUtils.copy( bis, bos );
      bos.flush();
    } catch ( Exception e ) {
      throw new KettleException( e );
    } finally {
      if ( bis != null ) {
        IOUtils.closeQuietly( bis );
        bis = null; // Help the GC
      }
      if ( bos != null ) {
        IOUtils.closeQuietly( bos );
        bos = null; // Help the GC
        // Note - closing the BufferedOuputStream closes the underlying output stream according to the Javadoc
      }
    }
  }

  private boolean isWildcardMatch( String filename, Pattern pattern ) {
    boolean retval = true;
    if ( pattern != null ) {
      Matcher matcher = pattern.matcher( filename );
      retval = ( matcher.matches() );
    }
    return retval;
  }

  /**
   * Delete current fetched message
   *
   * @throws KettleException
   */
  public void deleteMessage() throws KettleException {
    try {
      this.message.setFlag( Flags.Flag.DELETED, true );
      updateDeletedMessagesCounter();
    } catch ( Exception e ) {
      throw new KettleException( BaseMessages.getString( PKG, "MailConnection.Error.DeletingMessage", ""
        + getMessage().getMessageNumber() ), e );
    }
  }

  /**
   * Set destination folder
   *
   * @param foldername
   *          destination foldername
   * @param createFolder
   *          flag create folder if needed
   * @throws KettleException
   */
  public void setDestinationFolder( String foldername, boolean createFolder ) throws KettleException {
    try {
      String[] folderparts = foldername.split( "/" );
      Folder f = this.getStore().getDefaultFolder();
      // Open destination folder
      for ( int i = 0; i < folderparts.length; i++ ) {
        f = f.getFolder( folderparts[i] );
        if ( !f.exists() ) {
          if ( createFolder ) {
            // Create folder
            f.create( Folder.HOLDS_MESSAGES );
          } else {
            throw new KettleException( BaseMessages.getString( PKG, "MailConnection.Error.FolderNotFound", foldername ) );
          }
        }
      }
      this.destinationIMAPFolder = f;
    } catch ( Exception e ) {
      throw new KettleException( e );
    }
  }

  /**
   * Move current message to a target folder. (IMAP) You must call setDestinationFolder before calling this method
   *
   * @throws KettleException
   */
  public void moveMessage() throws KettleException {
    try {
      // move all messages
      this.folder.copyMessages( new Message[] { this.message }, this.destinationIMAPFolder );
      updatedMovedMessagesCounter();
      // Make sure to delete messages
      deleteMessage();
    } catch ( Exception e ) {
      throw new KettleException( BaseMessages.getString( PKG, "MailConnection.Error.MovingMessage", ""
        + getMessage().getMessageNumber(), this.destinationIMAPFolder.getName() ), e );

    }
  }

  /**
   * Returns the foldername.
   *
   * @return foldername
   */
  public String getFolderName() {
    if ( this.folder == null ) {
      return "";
    }
    return this.folder.getName();
  }

  /**
   * Returns the server name/Ip.
   *
   * @return server
   */
  public String getServer() {
    return server;
  }

  /**
   * Returns the protocol.
   *
   * @return protocol
   */
  public int getProtocol() {
    return protocol;
  }

  /**
   * Returns all messages.
   *
   * @return all messages
   */
  public Message[] getMessages() {
    return messages;
  }

  private void updateMessageNr() {
    this.messagenr++;
  }

  private int getMessageNr() {
    return this.messagenr;
  }

  /**
   * Get next message.
   *
   * @throws KettleException
   */
  public void fetchNext() throws KettleException {
    updateMessageNr();
    try {
      this.message = this.messages[getMessageNr()];
    } catch ( Exception e ) {
      throw new KettleException( BaseMessages.getString( PKG, "MailConnection.Error.FetchingMessages" ), e );
    }
  }

  /**
   * Returns the current message.
   *
   * @return current message
   */
  public Message getMessage() {
    return this.message;
  }

  /**
   * Returns the number of messages.
   *
   * @return messages count
   */
  public int getMessagesCount() {
    return this.messages.length;
  }

  public void updateSavedMessagesCounter() {
    this.nrSavedMessages++;
  }

  public int getSavedMessagesCounter() {
    return this.nrSavedMessages;
  }

  public int getSavedAttachedFilesCounter() {
    return this.nrSavedAttachedFiles;
  }

  public void updateSavedAttachedFilesCounter() {
    this.nrSavedAttachedFiles++;
  }

  public int getDeletedMessagesCounter() {
    return this.nrDeletedMessages;
  }

  private void updateDeletedMessagesCounter() {
    this.nrDeletedMessages++;
  }

  private void setDeletedMessagesCounter() {
    this.nrDeletedMessages = getMessagesCount();
  }

  /**
   * Returns count of moved messages.
   *
   * @return count of moved messages
   */
  public int getMovedMessagesCounter() {
    return this.nrMovedMessages;
  }

  /**
   * Update count of moved messages.
   */
  private void updatedMovedMessagesCounter() {
    this.nrMovedMessages++;
  }

  /**
   * Set count of moved messages.
   */
  private void setMovedMessagesCounter() {
    this.nrMovedMessages = getMessagesCount();
  }

  /**
   * Delete messages.
   *
   * @throws KettleException
   */
  public void deleteMessages( boolean setCounter ) throws KettleException {
    try {
      this.folder.setFlags( this.messages, new Flags( Flags.Flag.DELETED ), true );
      if ( setCounter ) {
        setDeletedMessagesCounter();
      }
    } catch ( Exception e ) {
      throw new KettleException( BaseMessages.getString( PKG, "MailConnection.Error.DeletingMessage" ), e );
    }
  }

  /**
   * Move messages to a folder. You must call setDestinationFolder before calling this method
   *
   * @throws KettleException
   */
  public void moveMessages() throws KettleException {
    try {
      this.folder.copyMessages( this.messages, this.destinationIMAPFolder );
      deleteMessages( false );
      setMovedMessagesCounter();
    } catch ( Exception e ) {
      throw new KettleException( BaseMessages.getString(
        PKG, "MailConnection.Error.MovingMessages", this.destinationIMAPFolder.getName() ), e );
    }
  }

  /**
   * Check if a folder exists on server (only IMAP).
   *
   * @param foldername
   *          the name of the folder
   * @return true is folder exists
   */
  public boolean folderExists( String foldername ) {
    boolean retval = false;
    Folder dfolder = null;
    try {
      // Open destination folder
      dfolder = getRecursiveFolder( foldername );
      if ( dfolder.exists() ) {
        retval = true;
      }
    } catch ( Exception e ) {
      // Ignore errors
    } finally {
      try {
        if ( dfolder != null ) {
          dfolder.close( false );
        }
      } catch ( Exception e ) { /* Ignore */
      }
    }
    return retval;
  }

  private HashSet<String> returnSubfolders( Folder folder ) throws KettleException {
    HashSet<String> list = new HashSet<String>();
    try {
      if ( ( folder.getType() & Folder.HOLDS_FOLDERS ) != 0 ) {
        Folder[] f = folder.list();
        for ( int i = 0; i < f.length; i++ ) {
          // Search for sub folders
          if ( ( f[i].getType() & Folder.HOLDS_FOLDERS ) != 0 ) {
            list.add( f[i].getFullName() );
            list.addAll( returnSubfolders( f[i] ) );
          }
        }
      }
    } catch ( MessagingException m ) {
      throw new KettleException( m );
    }
    return list;
  }

  /**
   * Returns all subfolders of the specified folder
   *
   * @param folder
   *          parent folder
   * @return sub folders
   */
  public String[] returnAllFolders( Folder folder ) throws KettleException {
    HashSet<String> list = new HashSet<String>();
    list = returnSubfolders( folder );
    return list.toArray( new String[list.size()] );
  }

  /**
   * Returns all subfolders of the current folder
   *
   * @return sub folders
   */
  public String[] returnAllFolders() throws KettleException {
    return returnAllFolders( getFolder() );
  }

  /**
   * Returns all subfolders of the folder folder
   *
   * @param folder
   *          target folder
   * @return sub folders
   */
  public String[] returnAllFolders( String folder ) throws KettleException {

    Folder dfolder = null;
    String[] retval = null;
    try {
      if ( Utils.isEmpty( folder ) ) {
        // Default folder
        dfolder = getStore().getDefaultFolder();
      } else {
        dfolder = getStore().getFolder( folder );
      }
      retval = returnAllFolders( dfolder );
    } catch ( Exception e ) {
      // Ignore errors
    } finally {
      try {
        if ( dfolder != null ) {
          dfolder.close( false );
        }
      } catch ( Exception e ) { /* Ignore */
      }
    }
    return retval;
  }

  public String getMessageBody() throws Exception {
    return getMessageBody( getMessage() );
  }

  /**
   * Return the primary text content of the message.
   */
  public String getMessageBody( Message m ) throws MessagingException, IOException {
    return getMessageBodyOrContentType( m, false );
  }

  public String getMessageBodyContentType( Message m ) throws MessagingException, IOException {
    return getMessageBodyOrContentType( m, true );
  }

  private String getMessageBodyOrContentType( Part p, final boolean returnContentType ) throws MessagingException,
    IOException {
    if ( p.isMimeType( "text/*" ) ) {
      String s = (String) p.getContent();
      return returnContentType ? p.getContentType() : s;
    }

    if ( p.isMimeType( "multipart/alternative" ) ) {
      // prefer html text over plain text
      Multipart mp = (Multipart) p.getContent();
      String text = null;
      for ( int i = 0; i < mp.getCount(); i++ ) {
        Part bp = mp.getBodyPart( i );
        if ( bp.isMimeType( "text/plain" ) ) {
          if ( text == null ) {
            text = getMessageBodyOrContentType( bp, returnContentType );
          }
        }
      }
      return text;
    } else if ( p.isMimeType( "multipart/*" ) ) {
      Multipart mp = (Multipart) p.getContent();
      for ( int i = 0; i < mp.getCount(); i++ ) {
        String s = getMessageBodyOrContentType( mp.getBodyPart( i ), returnContentType );
        if ( s != null ) {
          return s;
        }
      }
    }

    return null;
  }

  /**
   * Returns if message is new
   *
   * @return true if new message
   */
  public boolean isMessageNew() {
    return isMessageNew( getMessage() );
  }

  public boolean isMessageNew( Message msg ) {
    try {
      return msg.isSet( Flag.RECENT );
    } catch ( MessagingException e ) {
      return false;
    }
  }

  /**
   * Returns if message is read
   *
   * @return true if message is read
   */
  public boolean isMessageRead() {
    return isMessageRead( getMessage() );
  }

  public boolean isMessageRead( Message msg ) {
    try {
      return msg.isSet( Flag.SEEN );
    } catch ( MessagingException e ) {
      return false;
    }
  }

  /**
   * Returns if message is read
   *
   * @return true if message is flagged
   */
  public boolean isMessageFlagged() {
    return isMessageFlagged( getMessage() );
  }

  public boolean isMessageFlagged( Message msg ) {
    try {
      return msg.isSet( Flag.FLAGGED );
    } catch ( MessagingException e ) {
      return false;
    }
  }

  /**
   * Returns if message is deleted
   *
   * @return true if message is deleted
   */
  public boolean isMessageDeleted() {
    return isMessageDeleted( getMessage() );
  }

  public boolean isMessageDeleted( Message msg ) {
    try {
      return msg.isSet( Flag.DELETED );
    } catch ( MessagingException e ) {
      return false;
    }
  }

  /**
   * Returns if message is Draft
   *
   * @return true if message is Draft
   */
  public boolean isMessageDraft() {
    return isMessageDraft( getMessage() );
  }

  public boolean isMessageDraft( Message msg ) {
    try {
      return msg.isSet( Flag.DRAFT );
    } catch ( MessagingException e ) {
      return false;
    }
  }

  public String toString() {
    if ( getServer() != null ) {
      return getServer();
    } else {
      return "-";
    }
  }

  /**
   * Returns attached files count for the current message
   *
   * @return true if message is Draft
   * @param pattern
   *          (optional)
   */
  public int getAttachedFilesCount( Pattern pattern ) throws KettleException {
    return getAttachedFilesCount( getMessage(), pattern );
  }

  public int getAttachedFilesCount( Message message, Pattern pattern ) throws KettleException {
    Object content = null;
    int retval = 0;
    try {
      content = message.getContent();
      if ( content instanceof Multipart ) {
        Multipart multipart = (Multipart) content;
        for ( int i = 0, n = multipart.getCount(); i < n; i++ ) {
          Part part = multipart.getBodyPart( i );
          String disposition = part.getDisposition();

          if ( ( disposition != null )
            && ( disposition.equalsIgnoreCase( Part.ATTACHMENT ) || disposition.equalsIgnoreCase( Part.INLINE ) ) ) {
            String MimeText = null;
            try {
              MimeText = MimeUtility.decodeText( part.getFileName() );
            } catch ( Exception e ) {
              // Ignore errors
            }
            if ( MimeText != null ) {
              String filename = MimeUtility.decodeText( part.getFileName() );
              if ( isWildcardMatch( filename, pattern ) ) {
                retval++;
              }
            }
          }
        }
      }
    } catch ( Exception e ) {
      throw new KettleException( BaseMessages.getString( PKG, "MailConnection.Error.CountingAttachedFiles", ""
        + this.message.getMessageNumber() ), e );
    } finally {
      if ( content != null ) {
        content = null;
      }
    }
    return retval;
  }
}
