/*
* Copyright (C) 2000 - 2008 TagServlet Ltd
*
* This file is part of Open BlueDragon (OpenBD) CFML Server Engine.
*
* OpenBD is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* Free Software Foundation,version 3.
*
* OpenBD is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with OpenBD. If not, see http://www.gnu.org/licenses/
*
* Additional permission under GNU GPL version 3 section 7
*
* If you modify this Program, or any covered work, by linking or combining
* it with any of the JARS listed in the README.txt (or a modified version of
* (that library), containing parts covered by the terms of that JAR, the
* licensors of this Program grant you additional permission to convey the
* resulting work.
* README.txt @ http://www.openbluedragon.org/license/README.txt
*
* http://www.openbluedragon.org/
*/
package com.naryx.tagfusion.cfm.mail;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable;
import java.io.UnsupportedEncodingException;
import java.util.Date;
import java.util.Enumeration;
import java.util.List;
import java.util.Properties;
import javax.mail.Address;
import javax.mail.FetchProfile;
import javax.mail.Flags;
import javax.mail.Folder;
import javax.mail.Header;
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.UIDFolder;
import javax.mail.internet.MimeUtility;
import org.apache.commons.io.IOUtils;
import com.nary.io.FileUtils;
import com.nary.util.UTF7Converter;
import com.nary.util.string;
import com.naryx.tagfusion.cfm.engine.cfDateData;
import com.naryx.tagfusion.cfm.engine.cfEngine;
import com.naryx.tagfusion.cfm.engine.cfNumberData;
import com.naryx.tagfusion.cfm.engine.cfQueryResultData;
import com.naryx.tagfusion.cfm.engine.cfSession;
import com.naryx.tagfusion.cfm.engine.cfStringData;
import com.naryx.tagfusion.cfm.engine.cfmBadFileException;
import com.naryx.tagfusion.cfm.engine.cfmRunTimeException;
import com.naryx.tagfusion.cfm.engine.dataNotSupportedException;
import com.naryx.tagfusion.cfm.tag.cfTag;
import com.naryx.tagfusion.cfm.tag.cfTagReturnType;
public class cfPOP3 extends cfTag implements Serializable{
static final long serialVersionUID = 1;
private static String[] header={ "date", "from", "messagenumber", "replyto", "subject", "cc", "to", "messageid", "uid", "header" };
private static String[] headerAndBody={ "date", "from", "messagenumber", "replyto", "subject", "cc", "to", "messageid", "uid", "header", "body", "attachments", "attachmentfiles", "htmlbody", "textbody" };
public void defaultParameters( String _tag ) throws cfmBadFileException {
defaultAttribute("USERNAME", "anonymous");
defaultAttribute("PASSWORD", "anonymous");
defaultAttribute("ACTION", "GetHeaderOnly");
defaultAttribute("TIMEOUT", 60);
defaultAttribute("STARTROW", 1);
defaultAttribute("GENERATEUNIQUEFILENAMES", "NO");
defaultAttribute("URIDIRECTORY", "NO");
defaultAttribute("MESSAGENUMBER", "");
parseTagHeader( _tag );
// ensure server has been specified
if ( !containsAttribute( "SERVER") )
throw newBadFileException( "Missing Attribute", "You need to provide a SERVER" );
}
public cfTagReturnType render( cfSession _Session ) throws cfmRunTimeException {
String action = getDynamic( _Session, "ACTION" ).getString().toLowerCase();
if ( action.equals("getheaderonly") )
getMessagesFromServer( _Session, false );
else if ( action.equals("getall") )
getMessagesFromServer( _Session, true );
else if ( action.equals("delete") )
deleteMessagesFromServer( _Session );
else
throw newRunTimeException( "Invalid ACTION attribute:" + action );
return cfTagReturnType.NORMAL;
}
//----------------------------------------------------------------
private void getMessagesFromServer( cfSession _Session, boolean GetAll ) throws cfmRunTimeException {
if ( !containsAttribute( "NAME" ) ){
throw newRunTimeException( "Missing NAME attribute. You need to name this transaction" );
}
//--[ See if we are pulling attachments down
File attachmentDir = null;
if ( containsAttribute("ATTACHMENTPATH") ){
String attachment = getDynamic( _Session, "ATTACHMENTPATH" ).getString();
if ( getDynamic( _Session, "URIDIRECTORY" ).getBoolean() )
attachmentDir = new File( FileUtils.getRealPath( _Session.REQ, attachment ) );
else
attachmentDir = new File( attachment );
// Fixes issue #304 where attachment path should create directories if they do not exist
if (!attachmentDir.exists())
attachmentDir.mkdirs();
}
int startRow = getDynamic( _Session, "STARTROW" ).getInt();
int maxRows = -1;
if ( containsAttribute( "MAXROWS" ) ){
maxRows = getDynamic( _Session, "MAXROWS" ).getInt();
if ( maxRows < 1 ){
throw newRunTimeException( "The value of MAXROWS must be 1 or greater." );
}
}
if ( startRow < 1 ){
throw newRunTimeException( "The value of STARTROW must be 1 or greater." );
}
//--[ Get Message Store
Store popStore = null;
Folder popFolder = null;
try
{
popStore = openConnection( _Session );
//--[ Setup the query data
cfQueryResultData popData;
if ( GetAll )
popData = new cfQueryResultData( headerAndBody, "CFPOP" );
else
popData = new cfQueryResultData( header, "CFPOP" );
//--[ Open up the Folder:INBOX and retrieve the headers
popFolder = openFolder( _Session, popStore );
//--[ Run through the Messages and populate the query
readMessages( _Session, popFolder, popData, startRow, maxRows, GetAll, attachmentDir );
//--[ Add the query to the session
_Session.setData( getDynamic(_Session,"NAME").getString(), popData );
}
finally
{
closeFolder( popFolder );
closeConnection( popStore );
}
}
private void deleteMessagesFromServer( cfSession _Session ) throws cfmRunTimeException {
//--[ Get Message Store
Store popStore = openConnection( _Session );
//--[ Open up the Folder:INBOX and retrieve the headers
Folder popFolder = openFolder( _Session, popStore );
try{
Message[] listOfMessages = popFolder.getMessages();
FetchProfile fProfile = new FetchProfile();
fProfile.add( FetchProfile.Item.ENVELOPE );
if ( containsAttribute( "UID" ) ){
String[] messageUIDList = getMessageUIDList( getDynamic(_Session,"UID").getString() );
fProfile.add(UIDFolder.FetchProfileItem.UID);
popFolder.fetch( listOfMessages, fProfile );
for ( int x=0; x < listOfMessages.length; x++ ){
if ( messageUIDValid( messageUIDList, getMessageUID( popFolder, listOfMessages[x] ) ) ){
listOfMessages[x].setFlag( Flags.Flag.DELETED, true );
}
}
}else if ( containsAttribute( "MESSAGENUMBER" ) ){
int[] messageList = getMessageList( getDynamic(_Session,"MESSAGENUMBER").getString() );
popFolder.fetch( listOfMessages, fProfile );
for ( int x=0; x < listOfMessages.length; x++ ){
if ( messageIDValid(messageList, listOfMessages[x].getMessageNumber() ) ){
listOfMessages[x].setFlag( Flags.Flag.DELETED, true );
}
}
}else{
throw newRunTimeException( "Either MESSAGENUMBER or UID attribute must be specified when ACTION=DELETE" );
}
}catch(Exception ignore){}
//--[ Close off the folder
closeFolder( popFolder );
closeConnection( popStore );
}
//----------------------------------------------------------------
private int getPort( cfSession _Session, boolean _secure ) throws dataNotSupportedException, cfmRunTimeException{
int port;
if ( containsAttribute( "PORT" ) ){
port = getDynamic( _Session, "PORT" ).getInt();
}else if ( _secure ){
port = 995;
}else{
port = 110;
}
return port;
}
private boolean getSecure( cfSession _Session ) throws dataNotSupportedException, cfmRunTimeException{
if ( containsAttribute( "SECURE" ) ){
return getDynamic( _Session, "SECURE" ).getBoolean();
}else{
return false;
}
}
//----------------------------------------------------------------
private Store openConnection( cfSession _Session ) throws cfmRunTimeException {
String server = getDynamic( _Session, "SERVER" ).getString();
boolean secure = getSecure( _Session );
int port = getPort( _Session, secure );
String user = getDynamic( _Session, "USERNAME" ).getString();
String pass = getDynamic( _Session, "PASSWORD" ).getString();
Properties props = new Properties();
String protocol = "pop3";
if ( secure ){
protocol = "pop3s";
}
props.put( "mail.transport.protocol", protocol );
props.put( "mail." + protocol + ".port", String.valueOf( port ) );
// This is the fix for bug NA#3156
props.put("mail.mime.address.strict", "false");
// With WebLogic Server 8.1sp4 and an IMAIL server, we're seeing that sometimes the first
// attempt to connect after messages have been deleted fails with either a MessagingException
// of "Connection reset" or an AuthenticationFailedException of "EOF on socket". To work
// around this let's try 3 attempts to connect before giving up.
for ( int numAttempts = 1; numAttempts < 4; numAttempts++ )
{
try{
Session session = Session.getInstance( props );
Store mailStore = session.getStore( protocol );
mailStore.connect( server, user, pass );
return mailStore;
}catch(Exception E){
if ( numAttempts == 3 )
throw newRunTimeException( E.getMessage() );
}
}
// The code in the for loop should either return or throw an exception
// so this code should never get hit.
return null;
}
private static void closeConnection( Store store ) {
try{
if ( store != null ) store.close();
}catch(Exception ignoreE){}
}
//----------------------------------------------------------------
private Folder openFolder( cfSession _Session, Store popStore ) throws cfmRunTimeException {
try{
Folder folder = popStore.getDefaultFolder();
Folder popFolder = folder.getFolder("INBOX");
popFolder.open( Folder.READ_WRITE );
return popFolder;
}catch(Exception E){
throw newRunTimeException( E.getMessage() );
}
}
private static void closeFolder( Folder popFolder ) {
try{
if ( popFolder != null ) popFolder.close( true );
}catch(Exception ignoreE){}
}
//----------------------------------------------------------------
private void readMessages( cfSession _Session, Folder popFolder, cfQueryResultData popData, int _start, int _max, boolean GetAll, File attachmentDir ) throws cfmRunTimeException {
try{
int maxRows = _max;
int startRow = _start;
String messageNumber = getDynamic(_Session,"MESSAGENUMBER").getString();
boolean containsUID = containsAttribute( "UID" );
boolean usingMessageNumber = messageNumber.length() > 0;
int msgCount = popFolder.getMessageCount();
// if MAXROWS is not specified, or UID or MESSAGENUMBER is, then we want to get all the messages
if ( _max == -1 || containsUID || usingMessageNumber ){
maxRows = msgCount;
}
if ( containsUID || usingMessageNumber ){
startRow = 1;
}
if ( msgCount != 0 && startRow > msgCount ){
throw newRunTimeException( "The value of STARTROW must not be greater than the total number of messages in the folder, " + popFolder.getMessageCount() + "." );
}
Message[] listOfMessages;
if ( !usingMessageNumber ){
listOfMessages = popFolder.getMessages();
}else{
listOfMessages = popFolder.getMessages( getMessageList( messageNumber ) );
}
FetchProfile fProfile = new FetchProfile();
fProfile.add( FetchProfile.Item.ENVELOPE );
fProfile.add(UIDFolder.FetchProfileItem.UID);
popFolder.fetch( listOfMessages, fProfile );
if ( containsUID ){
String[] messageUIDList = getMessageUIDList( getDynamic(_Session,"UID").getString() );
for ( int x=0; x < listOfMessages.length; x++ ){
if ( messageUIDList.length == 0 || messageUIDValid( messageUIDList, getMessageUID( popFolder, listOfMessages[x] ) ) ){
populateMessage( _Session, listOfMessages[x], popData, GetAll, attachmentDir, popFolder );
}
}
}else{
popFolder.fetch( listOfMessages, fProfile );
int end = startRow -1 + maxRows;
if ( end > listOfMessages.length ){
end = listOfMessages.length;
}
for ( int x=startRow-1; x < end; x++ ){
populateMessage( _Session, listOfMessages[x], popData, GetAll, attachmentDir, popFolder );
}
}
}catch(Exception E){
if ( E.getMessage() != null )
throw newRunTimeException( E.getMessage() );
else
throw newRunTimeException( E.toString() );
}
}
private void populateMessage( cfSession _Session, Message thisMessage, cfQueryResultData popData, boolean GetAll, File attachmentDir, Folder _parent ) throws Exception {
popData.addRow( 1 );
int Row = popData.getNoRows();
Date date = thisMessage.getSentDate();
if ( date != null ){
cfDateData cfdate = new cfDateData( date );
cfdate.setPOPDate();
popData.setCell( Row, 1, cfdate );
}else{
popData.setCell( Row, 1, new cfStringData("") );
}
popData.setCell( Row, 2, new cfStringData( formatAddress( thisMessage.getFrom() ) ) );
popData.setCell( Row, 3, new cfNumberData( thisMessage.getMessageNumber() ) );
popData.setCell( Row, 4, new cfStringData( formatAddress( thisMessage.getReplyTo() ) ) );
popData.setCell( Row, 5, new cfStringData( thisMessage.getSubject() ) );
popData.setCell( Row, 6, new cfStringData( formatAddress( thisMessage.getRecipients(Message.RecipientType.CC) ) ) );
popData.setCell( Row, 7, new cfStringData( formatAddress( thisMessage.getRecipients(Message.RecipientType.TO) ) ) );
String [] msgid = thisMessage.getHeader( "Message-ID" );
popData.setCell( Row, 8, new cfStringData( msgid != null ? msgid[0] : "" ) );
popData.setCell( Row, 9, new cfStringData( getMessageUID( _parent, thisMessage ) ) );
popData.setCell( Row, 10, new cfStringData( formatHeader( thisMessage ) ) );
if ( GetAll ){
retrieveBody( _Session, thisMessage, popData, Row, attachmentDir );
}
}
private static String formatHeader( Message thisMessage ) throws Exception {
Enumeration<Header> E = thisMessage.getAllHeaders();
StringBuilder tmp = new StringBuilder(128);
while (E.hasMoreElements()){
Header hdr = E.nextElement();
tmp.append( hdr.getName() );
tmp.append( ": " );
tmp.append( hdr.getValue() );
tmp.append( "\r\n" );
}
return tmp.toString();
}
private static String formatAddress( Address[] addList ){
if ( addList == null || addList.length == 0 )
return "";
try {
return MimeUtility.decodeText( javax.mail.internet.InternetAddress.toString( addList ) ).trim();
} catch (UnsupportedEncodingException e) {
return javax.mail.internet.InternetAddress.toString( addList );
}
}
private void retrieveBody( cfSession _Session, Part Mess, cfQueryResultData popData, int Row, File attachmentDir ) throws Exception {
if ( Mess.isMimeType("multipart/*") ){
Multipart mp = (Multipart)Mess.getContent();
int count = mp.getCount();
for (int i = 0; i < count; i++)
retrieveBody( _Session, mp.getBodyPart(i), popData, Row, attachmentDir );
}else{
String filename = cfMailMessageData.getFilename( Mess );
String dispos = Mess.getDisposition();
// note: text/enriched shouldn't be treated as a text part of the email (see bug #2227)
if ( ( dispos == null || dispos.equalsIgnoreCase( Part.INLINE ) ) && Mess.isMimeType("text/*") && !Mess.isMimeType( "text/enriched" ) ) {
String content;
String contentType = Mess.getContentType().toLowerCase();
// support aliases of UTF-7 - UTF7, UNICODE-1-1-UTF-7, csUnicode11UTF7, UNICODE-2-0-UTF-7
if ( contentType.indexOf( "utf-7") != -1 || contentType.indexOf( "utf7") != -1){
content = new String( UTF7Converter.convert( readInputStream( Mess ) ) );
}else{
try{
content = (String) Mess.getContent();
}catch( UnsupportedEncodingException e ){
content = "Unable to retrieve message body due to UnsupportedEncodingException:" + e.getMessage();
}catch( ClassCastException e ){
// shouldn't happen but handle it gracefully
content = new String( readInputStream( Mess ) );
}
}
if ( Mess.isMimeType( "text/html" ) ){
popData.setCell( Row, 14, new cfStringData( content ) );
}else if ( Mess.isMimeType( "text/plain" ) ){
popData.setCell( Row, 15, new cfStringData( content ) );
}
popData.setCell( Row, 11, new cfStringData( content ) );
} else if ( attachmentDir != null ){
File outFile;
if ( filename == null )
filename = "unknownfile";
outFile = getAttachedFilename( attachmentDir, filename, getDynamic( _Session, "GENERATEUNIQUEFILENAMES" ).getBoolean() );
try{
BufferedInputStream in = new BufferedInputStream( Mess.getInputStream() );
BufferedOutputStream out = new BufferedOutputStream( cfEngine.thisPlatform.getFileIO().getFileOutputStream( outFile ) );
IOUtils.copy(in,out);
out.flush();
out.close();
in.close();
//--[ Update the fields
cfStringData cell = (cfStringData)popData.getCell( Row, 12 );
if ( cell.getString().length() == 0 )
cell = new cfStringData( filename );
else
cell = new cfStringData( cell.getString() + "," + filename );
popData.setCell( Row, 12, cell );
cell = (cfStringData)popData.getCell( Row, 13 );
if ( cell.getString().length() == 0 )
cell = new cfStringData( outFile.toString() );
else
cell = new cfStringData( cell.getString() + "," + outFile.toString() );
popData.setCell( Row, 13, cell );
}catch(Exception ignoreException){}
}
}
}
private static byte[] readInputStream( Part _part ) throws IOException, MessagingException{
InputStream ins = null;
ByteArrayOutputStream bos = null;
try{
ins = _part.getInputStream();
bos = new ByteArrayOutputStream();
byte [] buffer = new byte[2048];
int read;
while ( (read = ins.read(buffer)) != -1 ){
bos.write( buffer, 0, read );
}
return bos.toByteArray();
}finally{
if ( ins != null ) try{ ins.close(); }catch( IOException ignored ){}
if ( bos != null ) try{ bos.close(); }catch( IOException ignored ){}
}
}
private static File getAttachedFilename( File attachDIR, String filename, boolean unique ){
filename = filename.replace(' ', '_').replace('/','_');
File fileN = new File( attachDIR, filename );
if ( unique ){
int x = 1;
while ( fileN.exists() )
fileN = new File( attachDIR, (x++) + "_" + filename );
}
return fileN;
}
private static int[] getMessageList( String line ){
if ( line == null ) return new int[0];
List<String> tokens = string.split( line, "," );
int [] list = new int[ tokens.size() ];
int indx = 0;
for ( int i = 0; i < tokens.size(); i++ )
list[ indx++ ] = com.nary.util.string.convertToInteger( tokens.get(i).toString().trim(), 0 );
return list;
}
private static String[] getMessageUIDList( String line ){
if ( line == null ) return new String[0];
List<String> tokens = string.split( line, "," );
String [] list = new String[ tokens.size() ];
int indx = 0;
for ( int i = 0; i < tokens.size(); i++ )
list[ indx++ ] = tokens.get(i).toString().trim();
return list;
}
private static boolean messageIDValid( int[] list, int id ){
for ( int x=0; x < list.length; x++ )
if ( id == list[x] ) return true;
return false;
}
private static boolean messageUIDValid( String[] list, String uid ){
if ( uid.length() != 0 ){
for ( int x=0; x < list.length; x++ )
if ( uid.equals(list[x]) ) return true;
}
return false;
}
private static String getMessageUID( Folder _parent, Message _msg ) throws MessagingException{
String uid = null;
if ( _parent instanceof com.sun.mail.pop3.POP3Folder ) {
uid = ( (com.sun.mail.pop3.POP3Folder) _parent ).getUID( _msg );
}
if ( uid == null ){
uid = "";
}
return uid;
}
}