MailUtil.java
/*
* Copyright (c) 2002-2022, City of Paris
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* 1. Redistributions of source code must retain the above copyright notice
* and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice
* and the following disclaimer in the documentation and/or other materials
* provided with the distribution.
*
* 3. Neither the name of 'Mairie de Paris' nor 'Lutece' nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*
* License 1.0
*/
package fr.paris.lutece.portal.service.mail;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Properties;
import java.util.StringTokenizer;
import javax.activation.CommandInfo;
import javax.activation.CommandMap;
import javax.activation.DataHandler;
import javax.activation.FileTypeMap;
import javax.activation.MailcapCommandMap;
import javax.activation.MimetypesFileTypeMap;
import javax.mail.Authenticator;
import javax.mail.BodyPart;
import javax.mail.Message;
import javax.mail.MessagingException;
import javax.mail.NoSuchProviderException;
import javax.mail.PasswordAuthentication;
import javax.mail.SendFailedException;
import javax.mail.Session;
import javax.mail.Transport;
import javax.mail.internet.AddressException;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeBodyPart;
import javax.mail.internet.MimeMessage;
import javax.mail.internet.MimeMultipart;
import javax.mail.internet.MimeUtility;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import fr.paris.lutece.portal.service.util.AppException;
import fr.paris.lutece.portal.service.util.AppLogService;
import fr.paris.lutece.portal.service.util.AppPropertiesService;
import fr.paris.lutece.util.mail.ByteArrayDataSource;
import fr.paris.lutece.util.mail.FileAttachment;
import fr.paris.lutece.util.mail.HtmlDocument;
import fr.paris.lutece.util.mail.UrlAttachment;
/**
* This class provides mail utils.
*/
final class MailUtil
{
// Properties
private static final String PROPERTY_CHARSET = "mail.charset";
private static final String PROPERTY_MAIL_LIST_SEPARATOR = "mail.list.separator";
private static final String PROPERTY_MAIL_TYPE_HTML = "mail.type.html";
private static final String PROPERTY_MAIL_TYPE_PLAIN = "mail.type.plain";
private static final String PROPERTY_MAIL_TYPE_CALENDAR = "mail.type.calendar";
private static final String PROPERTY_MAIL_SESSION_DEBUG = "mail.session.debug";
private static final String PROPERTY_CALENDAR_SEPARATOR = "mail.type.calendar.separator";
private static final String PROPERTY_CALENDAR_METHOD_CREATE = "mail.type.calendar.create";
private static final String PROPERTY_CALENDAR_METHOD_CANCEL = "mail.type.calendar.cancel";
// Javax.mail properties
private static final String SMTP = "smtp";
private static final String MAIL = "mail.";
private static final String MAIL_HOST = "mail.host";
private static final String MAIL_TRANSPORT_PROTOCOL = "mail.transport.protocol";
private static final String MAIL_SMTP_AUTH = "mail.smtp.auth";
private static final String MAIL_PROPTOCOL_HOST = MAIL + SMTP + ".host";
private static final String MAIL_PROPTOCOL_PORT = MAIL + SMTP + ".port";
// Constants
private static final String TRUE = "true";
private static final String ENCODING = "Q";
private static final String HEADER_NAME = "Content-Transfer-Encoding";
private static final String HEADER_VALUE = "quoted-printable";
private static final String HEADER_CONTENT_LOCATION = "Content-Location";
private static final String CONTENT_HANDLER = "content-handler";
private static final String MULTIPART_RELATED = "related";
private static final String MSG_ATTACHMENT_NOT_FOUND = " not found, document ignored.";
private static final int CONSTANTE_FILE_ATTACHMET_BUFFER = 4096;
private static final String MIME_TYPE_TEXT_PLAIN = "text/plain";
private static final String MIME_TYPE_TEXT_CALENDAR = "text/calendar";
private static final String CONSTANT_REGISTER_MIME_TYPE_HANDLER = ";; x-java-content-handler=";
private static final String DEFAULT_PLAIN_TEXT_HANDLER = "com.sun.mail.handlers.text_plain";
private static final String CONSTANT_DISPOSITION_ATTACHMENT = "attachment";
private static final String CONSTANT_BASE64 = "base64";
static
{
// We create the mime text/calendar mime type
MimetypesFileTypeMap mimetypes = (MimetypesFileTypeMap) FileTypeMap.getDefaultFileTypeMap( );
mimetypes.addMimeTypes( MIME_TYPE_TEXT_CALENDAR );
// We register the handler for the text/calendar mime type
MailcapCommandMap mailcap = (MailcapCommandMap) CommandMap.getDefaultCommandMap( );
// We try to get the default handler for plain text
CommandInfo [ ] commandInfos = mailcap.getAllCommands( MIME_TYPE_TEXT_PLAIN );
CommandInfo commandInfoText = null;
if ( ( commandInfos != null ) && ( commandInfos.length > 0 ) )
{
for ( CommandInfo commandInfo : commandInfos )
{
if ( StringUtils.equals( commandInfo.getCommandName( ), CONTENT_HANDLER ) )
{
commandInfoText = commandInfo;
break;
}
}
if ( commandInfoText == null )
{
commandInfoText = commandInfos [0];
}
}
// If the default handler for plain text was not found, we just use the default
// one
String strHandler = ( commandInfoText != null ) ? commandInfoText.getCommandClass( ) : DEFAULT_PLAIN_TEXT_HANDLER;
mailcap.addMailcap( MIME_TYPE_TEXT_CALENDAR + CONSTANT_REGISTER_MIME_TYPE_HANDLER + strHandler + "\n" );
}
/**
* Creates a new MailUtil object
*/
private MailUtil( )
{
}
/**
* Send a text message.
*
* @param mail
* The mail to send
* @param transport
* the smtp transport object
* @param session
* the smtp session object
* @throws AddressException
* If invalid address
* @throws SendFailedException
* If an error occured during sending
* @throws MessagingException
* If a messaging error occured
*/
protected static void sendMessageText( MailItem mail, Transport transport, Session session ) throws MessagingException
{
Message msg = prepareMessage( mail, session );
msg.setDataHandler( new DataHandler( new ByteArrayDataSource( mail.getMessage( ),
AppPropertiesService.getProperty( PROPERTY_MAIL_TYPE_PLAIN ) + AppPropertiesService.getProperty( PROPERTY_CHARSET ) ) ) );
sendMessage( msg, transport );
}
/**
* Send a HTML formated message.
*
* @param mail
* The mail to send
* @param transport
* the smtp transport object
* @param session
* the smtp session object
* @throws AddressException
* If invalid address
* @throws SendFailedException
* If an error occured during sending
* @throws MessagingException
* If a messaging error occured
*/
protected static void sendMessageHtml( MailItem mail, Transport transport, Session session ) throws MessagingException
{
Message msg = prepareMessage( mail, session );
msg.setHeader( HEADER_NAME, HEADER_VALUE );
// Message body formated in HTML
msg.setDataHandler( new DataHandler( new ByteArrayDataSource( mail.getMessage( ),
AppPropertiesService.getProperty( PROPERTY_MAIL_TYPE_HTML ) + AppPropertiesService.getProperty( PROPERTY_CHARSET ) ) ) );
sendMessage( msg, transport );
}
/**
* Send a Multipart HTML message with the attachements associated to the message and attached files.
*
* @param mail
* The mail to send
* @param transport
* the smtp transport object
* @param session
* the smtp session object
* @throws AddressException
* If invalid address
* @throws SendFailedException
* If an error occured during sending
* @throws MessagingException
* If a messaging error occurred
*/
protected static void sendMultipartMessageHtml( MailItem mail, Transport transport, Session session ) throws MessagingException
{
Message msg = prepareMessage( mail, session );
msg.setHeader( HEADER_NAME, HEADER_VALUE );
// Creation of the root part containing all the elements of the message
MimeMultipart multipart = CollectionUtils.isEmpty( mail.getFilesAttachement( ) ) ? new MimeMultipart( MULTIPART_RELATED ) : new MimeMultipart( );
// Creation of the html part, the "core" of the message
BodyPart msgBodyPart = new MimeBodyPart( );
msgBodyPart.setDataHandler( new DataHandler( new ByteArrayDataSource( mail.getMessage( ),
AppPropertiesService.getProperty( PROPERTY_MAIL_TYPE_HTML ) + AppPropertiesService.getProperty( PROPERTY_CHARSET ) ) ) );
multipart.addBodyPart( msgBodyPart );
if ( mail.getUrlsAttachement( ) != null )
{
ByteArrayDataSource urlByteArrayDataSource;
for ( UrlAttachment urlAttachement : mail.getUrlsAttachement( ) )
{
urlByteArrayDataSource = convertUrlAttachmentDataSourceToByteArrayDataSource( urlAttachement );
if ( urlByteArrayDataSource != null )
{
msgBodyPart = new MimeBodyPart( );
// Fill this part, then add it to the root part with the
// good headers
msgBodyPart.setDataHandler( new DataHandler( urlByteArrayDataSource ) );
msgBodyPart.setHeader( HEADER_CONTENT_LOCATION, urlAttachement.getContentLocation( ) );
multipart.addBodyPart( msgBodyPart );
}
}
}
// add File Attachement
if ( mail.getFilesAttachement( ) != null )
{
for ( FileAttachment fileAttachement : mail.getFilesAttachement( ) )
{
String strFileName = fileAttachement.getFileName( );
byte [ ] bContentFile = fileAttachement.getData( );
String strContentType = fileAttachement.getType( );
ByteArrayDataSource dataSource = new ByteArrayDataSource( bContentFile, strContentType );
msgBodyPart = new MimeBodyPart( );
msgBodyPart.setDataHandler( new DataHandler( dataSource ) );
msgBodyPart.setFileName( strFileName );
msgBodyPart.setDisposition( CONSTANT_DISPOSITION_ATTACHMENT );
multipart.addBodyPart( msgBodyPart );
}
}
msg.setContent( multipart );
sendMessage( msg, transport );
}
/**
* Send a Multipart text message with attached files.
*
* @param mail
* The mail to send
* @param transport
* the smtp transport object
* @param session
* the smtp session object
* @throws AddressException
* If invalid address
* @throws SendFailedException
* If an error occured during sending
* @throws MessagingException
* If a messaging error occured
*/
protected static void sendMultipartMessageText( MailItem mail, Transport transport, Session session ) throws MessagingException
{
Message msg = prepareMessage( mail, session );
msg.setHeader( HEADER_NAME, HEADER_VALUE );
// Creation of the root part containing all the elements of the message
MimeMultipart multipart = new MimeMultipart( );
// Creation of the html part, the "core" of the message
BodyPart msgBodyPart = new MimeBodyPart( );
msgBodyPart.setDataHandler( new DataHandler( new ByteArrayDataSource( mail.getMessage( ),
AppPropertiesService.getProperty( PROPERTY_MAIL_TYPE_PLAIN ) + AppPropertiesService.getProperty( PROPERTY_CHARSET ) ) ) );
multipart.addBodyPart( msgBodyPart );
// add File Attachement
if ( mail.getFilesAttachement( ) != null )
{
for ( FileAttachment fileAttachement : mail.getFilesAttachement( ) )
{
String strFileName = fileAttachement.getFileName( );
byte [ ] bContentFile = fileAttachement.getData( );
String strContentType = fileAttachement.getType( );
ByteArrayDataSource dataSource = new ByteArrayDataSource( bContentFile, strContentType );
msgBodyPart = new MimeBodyPart( );
msgBodyPart.setDataHandler( new DataHandler( dataSource ) );
msgBodyPart.setFileName( strFileName );
msgBodyPart.setDisposition( CONSTANT_DISPOSITION_ATTACHMENT );
multipart.addBodyPart( msgBodyPart );
}
}
msg.setContent( multipart );
sendMessage( msg, transport );
}
/**
* Send a calendar message.
*
* @param mail
* The mail to send
* @param transport
* the smtp transport object
* @param session
* the smtp session object
* @throws AddressException
* If invalid address
* @throws SendFailedException
* If an error occurred during sending
* @throws MessagingException
* If a messaging error occurred
*/
protected static void sendMessageCalendar( MailItem mail, Transport transport, Session session ) throws MessagingException
{
Message msg = prepareMessage( mail, session );
msg.setHeader( HEADER_NAME, HEADER_VALUE );
MimeMultipart multipart = new MimeMultipart( );
BodyPart msgBodyPart = new MimeBodyPart( );
msgBodyPart.setDataHandler( new DataHandler( new ByteArrayDataSource( mail.getMessage( ),
AppPropertiesService.getProperty( PROPERTY_MAIL_TYPE_HTML ) + AppPropertiesService.getProperty( PROPERTY_CHARSET ) ) ) );
multipart.addBodyPart( msgBodyPart );
BodyPart calendarBodyPart = new MimeBodyPart( );
calendarBodyPart.setContent( mail.getCalendarMessage( ),
AppPropertiesService.getProperty( PROPERTY_MAIL_TYPE_CALENDAR ) + AppPropertiesService.getProperty( PROPERTY_CHARSET )
+ AppPropertiesService.getProperty( PROPERTY_CALENDAR_SEPARATOR )
+ AppPropertiesService.getProperty( mail.getCreateEvent( ) ? PROPERTY_CALENDAR_METHOD_CREATE : PROPERTY_CALENDAR_METHOD_CANCEL ) );
calendarBodyPart.addHeader( HEADER_NAME, CONSTANT_BASE64 );
multipart.addBodyPart( calendarBodyPart );
msg.setContent( multipart );
sendMessage( msg, transport );
}
/**
* Send the message
*
* @param msg
* the message to send
* @param transport
* the transport used to send the message
* @throws MessagingException
* If a messaging error occured
* @throws AddressException
* If invalid address
*/
private static void sendMessage( Message msg, Transport transport ) throws MessagingException
{
if ( msg.getAllRecipients( ) != null )
{
// Send the message
transport.sendMessage( msg, msg.getAllRecipients( ) );
}
else
{
throw new AddressException( "Mail adress is null" );
}
}
/**
* Extract a collection of elements to be attached to a mail from an HTML string. The collection contains the Url used for created DataHandler for each url
* associated with an HTML tag img, script or link. Those urls must start with the url strBaseUrl.
*
* @param strHtml
* The HTML code.
* @param strBaseUrl
* The base url, can be null in order to extract all urls.
* @param useAbsoluteUrl
* Determine if we use absolute or relative url for attachement content-location
* @return a collection of UrlAttachment Object for created DataHandler associated with attachment urls.
*/
protected static List<UrlAttachment> getUrlAttachmentList( String strHtml, String strBaseUrl, boolean useAbsoluteUrl )
{
List<UrlAttachment> listUrlAttachement = new ArrayList<>( );
HtmlDocument doc = new HtmlDocument( strHtml, strBaseUrl, useAbsoluteUrl );
listUrlAttachement.addAll( doc.getAllUrlsAttachement( HtmlDocument.ELEMENT_IMG ) );
listUrlAttachement.addAll( doc.getAllUrlsAttachement( HtmlDocument.ELEMENT_CSS ) );
listUrlAttachement.addAll( doc.getAllUrlsAttachement( HtmlDocument.ELEMENT_JAVASCRIPT ) );
return listUrlAttachement;
}
/**
* Common part for sending message process :
* <ul>
* <li>initializes a mail session with the SMTP server</li>
* <li>activates debugging</li>
* <li>instantiates and initializes a mime message</li>
* <li>sets the sent date, the from field, the subject field</li>
* <li>sets the recipients</li>
* </ul>
*
*
* @return the message object initialized with the common settings
* @param mail
* The mail to send
* @param session
* The SMTP session object
* @throws AddressException
* If invalid address
* @throws MessagingException
* If a messaging error occurred
*/
protected static Message prepareMessage( MailItem mail, Session session ) throws MessagingException
{
// Instantiate and initialize a mime message
Message msg = new MimeMessage( session );
msg.setSentDate( new Date( ) );
try
{
msg.setFrom( new InternetAddress( mail.getSenderEmail( ), mail.getSenderName( ), AppPropertiesService.getProperty( PROPERTY_CHARSET ) ) );
msg.setSubject( MimeUtility.encodeText( mail.getSubject( ), AppPropertiesService.getProperty( PROPERTY_CHARSET ), ENCODING ) );
}
catch( UnsupportedEncodingException e )
{
throw new AppException( e.toString( ) );
}
// Instantiation of the list of address
if ( mail.getRecipientsTo( ) != null )
{
msg.setRecipients( Message.RecipientType.TO, getAllAdressOfRecipients( mail.getRecipientsTo( ) ) );
}
if ( mail.getRecipientsCc( ) != null )
{
msg.setRecipients( Message.RecipientType.CC, getAllAdressOfRecipients( mail.getRecipientsCc( ) ) );
}
if ( mail.getRecipientsBcc( ) != null )
{
msg.setRecipients( Message.RecipientType.BCC, getAllAdressOfRecipients( mail.getRecipientsBcc( ) ) );
}
return msg;
}
/**
* Open mail session with the SMTP server using the given credentials. Will use no authentication if strUsername is null or blank.
*
* @param strHost
* The SMTP name or IP address.
* @param nPort
* The port to use
* @param strUsername
* the username
* @param strPassword
* the password
* @return the mails session object
*/
protected static Session getMailSession( String strHost, int nPort, final String strUsername, final String strPassword )
{
String strDebug = AppPropertiesService.getProperty( PROPERTY_MAIL_SESSION_DEBUG, Boolean.FALSE.toString( ) );
boolean bSessionDebug = Boolean.parseBoolean( strDebug );
// Initializes a mail session with the SMTP server
Properties props = System.getProperties( );
props.put( MAIL_HOST, strHost );
props.put( MAIL_TRANSPORT_PROTOCOL, SMTP );
props.put( MAIL_PROPTOCOL_HOST, strHost );
props.put( MAIL_PROPTOCOL_PORT, nPort );
Authenticator auth;
if ( StringUtils.isNotBlank( strUsername ) )
{
props.put( MAIL_SMTP_AUTH, TRUE );
// using authenticator class that return a PasswordAuthentication
auth = new Authenticator( )
{
@Override
protected PasswordAuthentication getPasswordAuthentication( )
{
return new PasswordAuthentication( strUsername, strPassword );
}
};
}
else
{
// no authentication data provided, no authenticator
auth = null;
}
Session mailSession = Session.getDefaultInstance( props, auth );
// Activate debugging
mailSession.setDebug( bSessionDebug );
return mailSession;
}
/**
* return the transport object of the SMTP session
*
* @return the transport object of the SMTP session
* @param session
* the SMTP session
* @throws NoSuchProviderException
* If the provider is not found
*/
protected static Transport getTransport( Session session ) throws NoSuchProviderException
{
return session.getTransport( SMTP );
}
/**
* extract The list of Internet Address content in the string strRecipients
*
* @return The list of Internet Address content in the string strRecipients
* @param strRecipients
* The list of recipient separated by the mail separator defined in config.properties
* @throws AddressException
* If invalid address
*/
private static InternetAddress [ ] getAllAdressOfRecipients( String strRecipients ) throws AddressException
{
List<String> listRecipients = getAllStringAdressOfRecipients( strRecipients );
InternetAddress [ ] address = new InternetAddress [ listRecipients.size( )];
// Initialization of the address array
for ( int i = 0; i < listRecipients.size( ); i++ )
{
address [i] = new InternetAddress( listRecipients.get( i ) );
}
return address;
}
/**
* Extract The list of String Address content in the string strRecipients
*
* @return The list of String Address content in the string strRecipients
* @param strRecipients
* The list of recipient separated by the mail separator defined in config.properties
*
*/
public static List<String> getAllStringAdressOfRecipients( String strRecipients )
{
StringTokenizer st = new StringTokenizer( strRecipients, AppPropertiesService.getProperty( PROPERTY_MAIL_LIST_SEPARATOR, ";" ) );
List<String> listRecipients = new ArrayList<>( );
while ( st.hasMoreTokens( ) )
{
listRecipients.add( st.nextToken( ) );
}
return listRecipients;
}
/**
* Return a String that contains a list of recipients separated with mail separator
*
* @param listRecipients
* a list of string recipients
* @return A String that contains a list of recipients separated with mail separator
*/
protected static String getStrRecipients( List<String> listRecipients )
{
String strMailListSeparator = AppPropertiesService.getProperty( PROPERTY_MAIL_LIST_SEPARATOR, ";" );
StringBuilder strRecipients = new StringBuilder( );
int ncpt = 0;
if ( listRecipients != null )
{
for ( String strRecipient : listRecipients )
{
strRecipients.append( strRecipient );
if ( ++ncpt < listRecipients.size( ) )
{
strRecipients.append( strMailListSeparator );
}
}
}
return strRecipients.toString( );
}
/**
* This Method convert a UrlAttachmentDataSource to a ByteArrayDataSource and used MailAttachmentCacheService for caching resource.
*
* @param urlAttachement
* {@link UrlAttachment}
* @return a {@link ByteArrayDataSource}
*/
private static ByteArrayDataSource convertUrlAttachmentDataSourceToByteArrayDataSource( UrlAttachment urlAttachement )
{
String strKey = MailAttachmentCacheService.getInstance( ).getKey( urlAttachement.getUrlData( ).toString( ) );
ByteArrayDataSource urlAttachmentDataSource = null;
if ( MailAttachmentCacheService.getInstance( ).isCacheEnable( ) && MailAttachmentCacheService.getInstance( ).getFromCache( strKey ) != null )
{
return (ByteArrayDataSource) MailAttachmentCacheService.getInstance( ).getFromCache( strKey );
}
DataHandler handler = new DataHandler( urlAttachement.getUrlData( ) );
ByteArrayOutputStream bo = null;
InputStream input = null;
String strType = handler.getContentType( );
try
{
Object o = handler.getContent( );
if ( o instanceof InputStream )
{
input = (InputStream) o;
bo = new ByteArrayOutputStream( );
int read;
byte [ ] tab = new byte [ CONSTANTE_FILE_ATTACHMET_BUFFER];
do
{
read = input.read( tab );
if ( read > 0 )
{
bo.write( tab, 0, read );
}
}
while ( read > 0 );
}
}
catch( IOException e )
{
// Document is ignored
AppLogService.info( "{} {} ", urlAttachement.getContentLocation( ), MSG_ATTACHMENT_NOT_FOUND );
}
finally
{
// closed inputstream and outputstream
try
{
if ( input != null )
{
input.close( );
}
if ( bo != null )
{
bo.close( );
urlAttachmentDataSource = new ByteArrayDataSource( bo.toByteArray( ), strType );
}
}
catch( IOException e )
{
AppLogService.error( e.getMessage( ), e );
}
}
if ( MailAttachmentCacheService.getInstance( ).isCacheEnable( ) )
{
// add resource in cache
MailAttachmentCacheService.getInstance( ).putInCache( strKey, urlAttachmentDataSource );
}
return urlAttachmentDataSource;
}
}