|
@@ -20,14 +20,12 @@
|
|
|
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
|
|
*/
|
|
|
|
|
|
-package password.pwm.util.queue;
|
|
|
+package password.pwm.svc.email;
|
|
|
|
|
|
import password.pwm.AppProperty;
|
|
|
import password.pwm.PwmApplication;
|
|
|
import password.pwm.PwmApplicationMode;
|
|
|
-import password.pwm.PwmConstants;
|
|
|
import password.pwm.bean.EmailItemBean;
|
|
|
-import password.pwm.config.Configuration;
|
|
|
import password.pwm.config.PwmSetting;
|
|
|
import password.pwm.config.option.DataStorageMethod;
|
|
|
import password.pwm.error.ErrorInformation;
|
|
@@ -37,12 +35,11 @@ import password.pwm.error.PwmOperationalException;
|
|
|
import password.pwm.error.PwmUnrecoverableException;
|
|
|
import password.pwm.health.HealthMessage;
|
|
|
import password.pwm.health.HealthRecord;
|
|
|
-import password.pwm.http.HttpContentType;
|
|
|
import password.pwm.ldap.UserInfo;
|
|
|
import password.pwm.svc.PwmService;
|
|
|
import password.pwm.svc.stats.Statistic;
|
|
|
import password.pwm.svc.stats.StatisticsManager;
|
|
|
-import password.pwm.util.PasswordData;
|
|
|
+import password.pwm.util.java.AtomicLoopIntIncrementer;
|
|
|
import password.pwm.util.java.JavaHelper;
|
|
|
import password.pwm.util.java.StringUtil;
|
|
|
import password.pwm.util.java.TimeDuration;
|
|
@@ -55,52 +52,54 @@ import password.pwm.util.macro.MacroMachine;
|
|
|
import javax.mail.Message;
|
|
|
import javax.mail.MessagingException;
|
|
|
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 java.io.IOException;
|
|
|
-import java.io.UnsupportedEncodingException;
|
|
|
import java.time.Instant;
|
|
|
import java.util.ArrayList;
|
|
|
import java.util.Collections;
|
|
|
-import java.util.Date;
|
|
|
import java.util.HashMap;
|
|
|
import java.util.LinkedHashMap;
|
|
|
import java.util.List;
|
|
|
import java.util.Map;
|
|
|
-import java.util.Properties;
|
|
|
+import java.util.Optional;
|
|
|
+import java.util.concurrent.ConcurrentHashMap;
|
|
|
import java.util.concurrent.TimeUnit;
|
|
|
import java.util.concurrent.atomic.AtomicInteger;
|
|
|
|
|
|
/**
|
|
|
* @author Jason D. Rivard
|
|
|
*/
|
|
|
-public class EmailQueueManager implements PwmService
|
|
|
+public class EmailService implements PwmService
|
|
|
{
|
|
|
|
|
|
- private static final PwmLogger LOGGER = PwmLogger.forClass( EmailQueueManager.class );
|
|
|
+ private static final PwmLogger LOGGER = PwmLogger.forClass( EmailService.class );
|
|
|
|
|
|
private PwmApplication pwmApplication;
|
|
|
- private Properties javaMailProps = new Properties();
|
|
|
+ private final Map<EmailServer, Optional<ErrorInformation>> serverErrors = new ConcurrentHashMap<>( );
|
|
|
+ private final List<EmailServer> servers = new ArrayList<>( );
|
|
|
private WorkQueueProcessor<EmailItemBean> workQueueProcessor;
|
|
|
+ private AtomicLoopIntIncrementer serverIncrementer;
|
|
|
|
|
|
private PwmService.STATUS status = STATUS.NEW;
|
|
|
- private ErrorInformation lastError;
|
|
|
|
|
|
- private final ThreadLocal<Transport> threadLocalTransport = new ThreadLocal<>();
|
|
|
+ private final ThreadLocal<EmailConnection> threadLocalTransport = new ThreadLocal<>();
|
|
|
|
|
|
public void init( final PwmApplication pwmApplication )
|
|
|
throws PwmException
|
|
|
{
|
|
|
status = STATUS.OPENING;
|
|
|
this.pwmApplication = pwmApplication;
|
|
|
- javaMailProps = makeJavaMailProps( pwmApplication.getConfig() );
|
|
|
+
|
|
|
+ servers.addAll( EmailServerUtil.makeEmailServersMap( pwmApplication.getConfig() ) );
|
|
|
+
|
|
|
+ for ( final EmailServer emailServer : servers )
|
|
|
+ {
|
|
|
+ serverErrors.put( emailServer, Optional.empty() );
|
|
|
+ }
|
|
|
+
|
|
|
+ serverIncrementer = new AtomicLoopIntIncrementer( servers.size() - 1 );
|
|
|
|
|
|
if ( pwmApplication.getLocalDB() == null || pwmApplication.getLocalDB().status() != LocalDB.Status.OPEN )
|
|
|
{
|
|
|
- LOGGER.warn( "localdb is not open, EmailQueueManager will remain closed" );
|
|
|
+ LOGGER.warn( "localdb is not open, EmailService will remain closed" );
|
|
|
status = STATUS.CLOSED;
|
|
|
return;
|
|
|
}
|
|
@@ -144,12 +143,17 @@ public class EmailQueueManager implements PwmService
|
|
|
return Collections.singletonList( HealthRecord.forMessage( HealthMessage.ServiceClosed_AppReadOnly, this.getClass().getSimpleName() ) );
|
|
|
}
|
|
|
|
|
|
- if ( lastError != null )
|
|
|
+ final List<HealthRecord> records = new ArrayList<>( );
|
|
|
+ for ( final Map.Entry<EmailServer, Optional<ErrorInformation>> entry : serverErrors.entrySet() )
|
|
|
{
|
|
|
- return Collections.singletonList( HealthRecord.forMessage( HealthMessage.Email_SendFailure, lastError.toDebugStr() ) );
|
|
|
+ if ( entry.getValue().isPresent() )
|
|
|
+ {
|
|
|
+ final ErrorInformation errorInformation = entry.getValue().get();
|
|
|
+ records.add( HealthRecord.forMessage( HealthMessage.Email_SendFailure, errorInformation.toDebugStr() ) );
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
- return Collections.emptyList();
|
|
|
+ return records;
|
|
|
}
|
|
|
|
|
|
@Override
|
|
@@ -200,9 +204,8 @@ public class EmailQueueManager implements PwmService
|
|
|
|
|
|
private boolean determineIfItemCanBeDelivered( final EmailItemBean emailItem )
|
|
|
{
|
|
|
- final String serverAddress = pwmApplication.getConfig().readSettingAsString( PwmSetting.EMAIL_SERVER_ADDRESS );
|
|
|
|
|
|
- if ( serverAddress == null || serverAddress.length() < 1 )
|
|
|
+ if ( servers.isEmpty() )
|
|
|
{
|
|
|
LOGGER.debug( "discarding email send event (no SMTP server address configured) " + emailItem.toDebugString() );
|
|
|
return false;
|
|
@@ -274,12 +277,12 @@ public class EmailQueueManager implements PwmService
|
|
|
if ( ( emailItem.getTo() == null || emailItem.getTo().isEmpty() ) && userInfo != null )
|
|
|
{
|
|
|
final String toAddress = userInfo.getUserEmailAddress();
|
|
|
- workingItemBean = newEmailToAddress( workingItemBean, toAddress );
|
|
|
+ workingItemBean = EmailServerUtil.newEmailToAddress( workingItemBean, toAddress );
|
|
|
}
|
|
|
|
|
|
if ( macroMachine != null )
|
|
|
{
|
|
|
- workingItemBean = applyMacrosToEmail( workingItemBean, macroMachine );
|
|
|
+ workingItemBean = EmailServerUtil.applyMacrosToEmail( workingItemBean, macroMachine );
|
|
|
}
|
|
|
|
|
|
if ( workingItemBean.getTo() == null || workingItemBean.getTo().length() < 1 )
|
|
@@ -328,14 +331,16 @@ public class EmailQueueManager implements PwmService
|
|
|
|
|
|
private WorkQueueProcessor.ProcessResult sendItem( final EmailItemBean emailItemBean )
|
|
|
{
|
|
|
+ EmailConnection serverTransport = null;
|
|
|
|
|
|
// create a new MimeMessage object (using the Session created above)
|
|
|
try
|
|
|
{
|
|
|
if ( threadLocalTransport.get() == null )
|
|
|
{
|
|
|
+
|
|
|
LOGGER.trace( "initializing new threadLocal transport, stats: " + stats() );
|
|
|
- threadLocalTransport.set( getSmtpTransport() );
|
|
|
+ threadLocalTransport.set( getSmtpTransport( ) );
|
|
|
newThreadLocalTransport.getAndIncrement();
|
|
|
}
|
|
|
else
|
|
@@ -343,11 +348,14 @@ public class EmailQueueManager implements PwmService
|
|
|
LOGGER.trace( "using existing threadLocal transport, stats: " + stats() );
|
|
|
useExistingTransport.getAndIncrement();
|
|
|
}
|
|
|
- final Transport transport = threadLocalTransport.get();
|
|
|
- if ( !transport.isConnected() )
|
|
|
+
|
|
|
+ serverTransport = threadLocalTransport.get();
|
|
|
+
|
|
|
+ if ( !serverTransport.getTransport().isConnected() )
|
|
|
{
|
|
|
LOGGER.trace( "connecting threadLocal transport, stats: " + stats() );
|
|
|
- transport.connect();
|
|
|
+ threadLocalTransport.set( getSmtpTransport( ) );
|
|
|
+ serverTransport = threadLocalTransport.get();
|
|
|
newConnectionCounter.getAndIncrement();
|
|
|
}
|
|
|
else
|
|
@@ -356,15 +364,19 @@ public class EmailQueueManager implements PwmService
|
|
|
useExistingConnection.getAndIncrement();
|
|
|
}
|
|
|
|
|
|
- final List<Message> messages = convertEmailItemToMessages( emailItemBean, this.pwmApplication.getConfig() );
|
|
|
+ final List<Message> messages = EmailServerUtil.convertEmailItemToMessages(
|
|
|
+ emailItemBean,
|
|
|
+ this.pwmApplication.getConfig(),
|
|
|
+ serverTransport.getEmailServer()
|
|
|
+ );
|
|
|
|
|
|
for ( final Message message : messages )
|
|
|
{
|
|
|
message.saveChanges();
|
|
|
- transport.sendMessage( message, message.getAllRecipients() );
|
|
|
+ serverTransport.getTransport().sendMessage( message, message.getAllRecipients() );
|
|
|
}
|
|
|
|
|
|
- lastError = null;
|
|
|
+ serverErrors.put( serverTransport.getEmailServer(), Optional.empty() );
|
|
|
|
|
|
LOGGER.debug( "sent email: " + emailItemBean.toDebugString() );
|
|
|
StatisticsManager.incrementStat( pwmApplication, Statistic.EMAIL_SEND_SUCCESSES );
|
|
@@ -391,10 +403,13 @@ public class EmailQueueManager implements PwmService
|
|
|
);
|
|
|
}
|
|
|
|
|
|
- lastError = errorInformation;
|
|
|
+ if ( serverTransport != null )
|
|
|
+ {
|
|
|
+ serverErrors.put( serverTransport.getEmailServer(), Optional.of( errorInformation ) );
|
|
|
+ }
|
|
|
LOGGER.error( errorInformation );
|
|
|
|
|
|
- if ( sendIsRetryable( e ) )
|
|
|
+ if ( EmailServerUtil.sendIsRetryable( e ) )
|
|
|
{
|
|
|
LOGGER.error( "error sending email (" + e.getMessage() + ") " + emailItemBean.toDebugString() + ", will retry" );
|
|
|
StatisticsManager.incrementStat( pwmApplication, Statistic.EMAIL_SEND_FAILURES );
|
|
@@ -402,189 +417,45 @@ public class EmailQueueManager implements PwmService
|
|
|
}
|
|
|
else
|
|
|
{
|
|
|
- LOGGER.error(
|
|
|
- "error sending email (" + e.getMessage() + ") " + emailItemBean.toDebugString() + ", permanent failure, discarding message" );
|
|
|
+ LOGGER.error( "error sending email (" + e.getMessage() + ") " + emailItemBean.toDebugString() + ", permanent failure, discarding message" );
|
|
|
StatisticsManager.incrementStat( pwmApplication, Statistic.EMAIL_SEND_DISCARDS );
|
|
|
return WorkQueueProcessor.ProcessResult.FAILED;
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- private Transport getSmtpTransport( )
|
|
|
- throws MessagingException, PwmUnrecoverableException
|
|
|
- {
|
|
|
- final String mailUser = this.pwmApplication.getConfig().readSettingAsString( PwmSetting.EMAIL_USERNAME );
|
|
|
- final PasswordData mailPassword = this.pwmApplication.getConfig().readSettingAsPassword( PwmSetting.EMAIL_PASSWORD );
|
|
|
- final String mailhost = this.pwmApplication.getConfig().readSettingAsString( PwmSetting.EMAIL_SERVER_ADDRESS );
|
|
|
- final int mailport = ( int ) this.pwmApplication.getConfig().readSettingAsLong( PwmSetting.EMAIL_SERVER_PORT );
|
|
|
-
|
|
|
- // Login to SMTP server first if both username and password is given
|
|
|
- final javax.mail.Session session = javax.mail.Session.getInstance( javaMailProps, null );
|
|
|
- final Transport tr = session.getTransport( "smtp" );
|
|
|
-
|
|
|
- final boolean authenticated = !( mailUser == null || mailUser.length() < 1 || mailPassword == null );
|
|
|
-
|
|
|
- if ( authenticated )
|
|
|
- {
|
|
|
- // create a new Session object for the message
|
|
|
- tr.connect( mailhost, mailport, mailUser, mailPassword.getStringValue() );
|
|
|
- }
|
|
|
- else
|
|
|
- {
|
|
|
- tr.connect();
|
|
|
- }
|
|
|
-
|
|
|
- LOGGER.debug( "connected to " + mailhost + ":" + mailport + " " + ( authenticated ? "(secure)" : "(plaintext)" ) );
|
|
|
- return tr;
|
|
|
- }
|
|
|
-
|
|
|
- public List<Message> convertEmailItemToMessages( final EmailItemBean emailItemBean, final Configuration config )
|
|
|
- throws MessagingException
|
|
|
- {
|
|
|
- final List<Message> messages = new ArrayList<>();
|
|
|
- final boolean hasPlainText = emailItemBean.getBodyPlain() != null && emailItemBean.getBodyPlain().length() > 0;
|
|
|
- final boolean hasHtml = emailItemBean.getBodyHtml() != null && emailItemBean.getBodyHtml().length() > 0;
|
|
|
- final String subjectEncodingCharset = config.readAppProperty( AppProperty.SMTP_SUBJECT_ENCODING_CHARSET );
|
|
|
-
|
|
|
- // create a new Session object for the messagejavamail
|
|
|
- final javax.mail.Session session = javax.mail.Session.getInstance( javaMailProps, null );
|
|
|
-
|
|
|
- final String emailTo = emailItemBean.getTo();
|
|
|
- if ( emailTo != null )
|
|
|
- {
|
|
|
- final InternetAddress[] recipients = InternetAddress.parse( emailTo );
|
|
|
- for ( final InternetAddress recipient : recipients )
|
|
|
- {
|
|
|
- final MimeMessage message = new MimeMessage( session );
|
|
|
- message.setFrom( makeInternetAddress( emailItemBean.getFrom() ) );
|
|
|
- message.setRecipient( Message.RecipientType.TO, recipient );
|
|
|
- {
|
|
|
- if ( subjectEncodingCharset != null && !subjectEncodingCharset.isEmpty() )
|
|
|
- {
|
|
|
- message.setSubject( emailItemBean.getSubject(), subjectEncodingCharset );
|
|
|
- }
|
|
|
- else
|
|
|
- {
|
|
|
- message.setSubject( emailItemBean.getSubject() );
|
|
|
- }
|
|
|
- }
|
|
|
- message.setSentDate( new Date() );
|
|
|
-
|
|
|
- if ( hasPlainText && hasHtml )
|
|
|
- {
|
|
|
- final MimeMultipart content = new MimeMultipart( "alternative" );
|
|
|
- final MimeBodyPart text = new MimeBodyPart();
|
|
|
- final MimeBodyPart html = new MimeBodyPart();
|
|
|
- text.setContent( emailItemBean.getBodyPlain(), HttpContentType.plain.getHeaderValue() );
|
|
|
- html.setContent( emailItemBean.getBodyHtml(), HttpContentType.html.getHeaderValue() );
|
|
|
- content.addBodyPart( text );
|
|
|
- content.addBodyPart( html );
|
|
|
- message.setContent( content );
|
|
|
- }
|
|
|
- else if ( hasPlainText )
|
|
|
- {
|
|
|
- message.setContent( emailItemBean.getBodyPlain(), HttpContentType.plain.getHeaderValue() );
|
|
|
- }
|
|
|
- else if ( hasHtml )
|
|
|
- {
|
|
|
- message.setContent( emailItemBean.getBodyHtml(), HttpContentType.html.getHeaderValue() );
|
|
|
- }
|
|
|
-
|
|
|
- messages.add( message );
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- return messages;
|
|
|
- }
|
|
|
-
|
|
|
- private static Properties makeJavaMailProps( final Configuration config )
|
|
|
+ private EmailConnection getSmtpTransport( )
|
|
|
+ throws PwmUnrecoverableException
|
|
|
{
|
|
|
- //Create a properties item to start setting up the mail
|
|
|
- final Properties props = new Properties();
|
|
|
-
|
|
|
- //Specify the desired SMTP server
|
|
|
- props.put( "mail.smtp.host", config.readSettingAsString( PwmSetting.EMAIL_SERVER_ADDRESS ) );
|
|
|
|
|
|
- //Specify SMTP server port
|
|
|
- props.put( "mail.smtp.port", ( int ) config.readSettingAsLong( PwmSetting.EMAIL_SERVER_PORT ) );
|
|
|
-
|
|
|
- //Specify configured advanced settings.
|
|
|
- final Map<String, String> advancedSettingValues = StringUtil.convertStringListToNameValuePair( config.readSettingAsStringArray( PwmSetting.EMAIL_ADVANCED_SETTINGS ), "=" );
|
|
|
- props.putAll( advancedSettingValues );
|
|
|
-
|
|
|
- return props;
|
|
|
- }
|
|
|
-
|
|
|
- private static InternetAddress makeInternetAddress( final String input )
|
|
|
- throws AddressException
|
|
|
- {
|
|
|
- if ( input == null )
|
|
|
- {
|
|
|
- return null;
|
|
|
- }
|
|
|
+ // the global server incrementer rotates the server list by 1 offset each attempt to get an smtp transport.
|
|
|
+ int nextSlot = serverIncrementer.next();
|
|
|
|
|
|
- if ( input.matches( "^.*<.*>$" ) )
|
|
|
+ for ( int i = 0; i < servers.size(); i++ )
|
|
|
{
|
|
|
- // check for format like: John Doe <jdoe@example.com>
|
|
|
- final String[] splitString = input.split( "<|>" );
|
|
|
- if ( splitString.length < 2 )
|
|
|
- {
|
|
|
- return new InternetAddress( input );
|
|
|
- }
|
|
|
+ nextSlot = nextSlot >= ( servers.size() - 1 )
|
|
|
+ ? 0
|
|
|
+ : nextSlot + 1;
|
|
|
|
|
|
- final InternetAddress address = new InternetAddress();
|
|
|
- address.setAddress( splitString[ 1 ].trim() );
|
|
|
+ final EmailServer server = servers.get( nextSlot );
|
|
|
try
|
|
|
{
|
|
|
- address.setPersonal( splitString[ 0 ].trim(), PwmConstants.DEFAULT_CHARSET.toString() );
|
|
|
+ final Transport transport = EmailServerUtil.makeSmtpTransport( server );
|
|
|
+
|
|
|
+ serverErrors.put( server, Optional.empty() );
|
|
|
+ return new EmailConnection( server, transport );
|
|
|
}
|
|
|
- catch ( UnsupportedEncodingException e )
|
|
|
+ catch ( MessagingException e )
|
|
|
{
|
|
|
- LOGGER.error( "unsupported encoding error while parsing internet address '" + input + "', error: " + e.getMessage() );
|
|
|
+ final String msg = "unable to connect to email server '" + server.toDebugString() + "', error: " + e.getMessage();
|
|
|
+ final ErrorInformation errorInformation = new ErrorInformation( PwmError.ERROR_SERVICE_UNREACHABLE, msg );
|
|
|
+ serverErrors.put( server, Optional.of( errorInformation ) );
|
|
|
+ LOGGER.warn( errorInformation.toDebugStr() );
|
|
|
}
|
|
|
- return address;
|
|
|
}
|
|
|
- return new InternetAddress( input );
|
|
|
- }
|
|
|
-
|
|
|
- private static EmailItemBean applyMacrosToEmail( final EmailItemBean emailItem, final MacroMachine macroMachine )
|
|
|
- {
|
|
|
- final EmailItemBean expandedEmailItem;
|
|
|
- expandedEmailItem = new EmailItemBean(
|
|
|
- macroMachine.expandMacros( emailItem.getTo() ),
|
|
|
- macroMachine.expandMacros( emailItem.getFrom() ),
|
|
|
- macroMachine.expandMacros( emailItem.getSubject() ),
|
|
|
- macroMachine.expandMacros( emailItem.getBodyPlain() ),
|
|
|
- macroMachine.expandMacros( emailItem.getBodyHtml() )
|
|
|
- );
|
|
|
- return expandedEmailItem;
|
|
|
- }
|
|
|
|
|
|
- private static EmailItemBean newEmailToAddress( final EmailItemBean emailItem, final String toAddress )
|
|
|
- {
|
|
|
- final EmailItemBean expandedEmailItem;
|
|
|
- expandedEmailItem = new EmailItemBean(
|
|
|
- toAddress,
|
|
|
- emailItem.getFrom(),
|
|
|
- emailItem.getSubject(),
|
|
|
- emailItem.getBodyPlain(),
|
|
|
- emailItem.getBodyHtml()
|
|
|
- );
|
|
|
- return expandedEmailItem;
|
|
|
+ throw PwmUnrecoverableException.newException( PwmError.ERROR_SERVICE_UNREACHABLE, "unable to reach any configured email server" );
|
|
|
}
|
|
|
|
|
|
- private static boolean sendIsRetryable( final Exception e )
|
|
|
- {
|
|
|
- if ( e != null )
|
|
|
- {
|
|
|
- final Throwable cause = e.getCause();
|
|
|
- if ( cause instanceof IOException )
|
|
|
- {
|
|
|
- LOGGER.trace( "message send failure cause is due to an IOException: " + e.getMessage() );
|
|
|
- return true;
|
|
|
- }
|
|
|
- }
|
|
|
- return false;
|
|
|
- }
|
|
|
}
|
|
|
|