package com.constellio.model.services.emails;
import static com.constellio.model.services.search.query.logical.LogicalSearchQueryOperators.from;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import javax.mail.Message;
import javax.mail.MessagingException;
import javax.mail.Session;
import org.apache.solr.common.params.ModifiableSolrParams;
import org.joda.time.Duration;
import org.joda.time.LocalDateTime;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.constellio.data.dao.dto.records.RecordsFlushing;
import com.constellio.data.dao.dto.records.TransactionDTO;
import com.constellio.data.dao.managers.StatefulService;
import com.constellio.data.dao.services.bigVault.RecordDaoException.OptimisticLocking;
import com.constellio.data.threads.BackgroundThreadConfiguration;
import com.constellio.data.threads.BackgroundThreadExceptionHandling;
import com.constellio.data.threads.BackgroundThreadsManager;
import com.constellio.data.utils.TimeProvider;
import com.constellio.model.conf.email.EmailConfigurationsManager;
import com.constellio.model.conf.email.EmailServerConfiguration;
import com.constellio.model.entities.records.Record;
import com.constellio.model.entities.records.wrappers.EmailToSend;
import com.constellio.model.entities.schemas.Metadata;
import com.constellio.model.entities.schemas.MetadataSchema;
import com.constellio.model.services.collections.CollectionsListManager;
import com.constellio.model.services.factories.ModelLayerFactory;
import com.constellio.model.services.records.RecordServices;
import com.constellio.model.services.records.RecordServicesException;
import com.constellio.model.services.records.SchemasRecordsServices;
import com.constellio.model.services.schemas.MetadataSchemasManager;
import com.constellio.model.services.search.SearchServices;
import com.constellio.model.services.search.query.logical.LogicalSearchQuery;
import com.constellio.model.services.search.query.logical.condition.LogicalSearchCondition;
public class EmailQueueManager implements StatefulService {
public static final int MAXIMUM_FAILURES_BEFORE_DISABLING_SMTP_SERVER = 20;
private static final Logger LOGGER = LoggerFactory.getLogger(EmailQueueManager.class);
private static final long TWENTY_SECONDS = 20 * 1000l;
public static final int MAX_TRY_SEND = 3;
static int SEND_EMAIL_BATCH = 20;
private static final Duration DURATION_BETWEEN_EXECUTION = new Duration(TWENTY_SECONDS);
private EmailConfigurationsManager emailConfigurationManager;
private SearchServices searchServices;
private CollectionsListManager collectionManager;
private RecordServices recordServices;
private EmailServices emailServices;
BackgroundThreadsManager backgroundThreadsManager;
MetadataSchemasManager metadataSchemasManager;
private ModelLayerFactory modelLayerFactory;
private int subsequentFailuresCount;
public EmailQueueManager(ModelLayerFactory modelLayerFactory, EmailServices emailServices) {
this.modelLayerFactory = modelLayerFactory;
this.emailConfigurationManager = modelLayerFactory.getEmailConfigurationsManager();
this.backgroundThreadsManager = modelLayerFactory.getDataLayerFactory().getBackgroundThreadsManager();
this.searchServices = modelLayerFactory.newSearchServices();
this.collectionManager = modelLayerFactory.getCollectionsListManager();
this.recordServices = modelLayerFactory.newRecordServices();
this.metadataSchemasManager = modelLayerFactory.getMetadataSchemasManager();
this.emailServices = emailServices;
}
@Override
public void initialize() {
configureBackgroundThread();
}
void configureBackgroundThread() {
Runnable sendEmailsAction = new Runnable() {
@Override
public void run() {
try {
sendEmails();
} catch (Throwable e) {
LOGGER.error("Exception when sending emails ", e);
}
}
};
backgroundThreadsManager.configure(BackgroundThreadConfiguration
.repeatingAction("EmailQueueManager", sendEmailsAction)
.handlingExceptionWith(BackgroundThreadExceptionHandling.CONTINUE)
.executedEvery(DURATION_BETWEEN_EXECUTION));
}
public void sendEmails() {
List<LogicalSearchQuery> searchConditions = prepareSearchQueries(TimeProvider.getLocalDateTime());
while (!searchConditions.isEmpty()) {
//LOGGER.info("Remaining " + searchConditions.size() + " collections");
for (Iterator<LogicalSearchQuery> iterator = searchConditions.iterator(); iterator.hasNext(); ) {
LogicalSearchQuery query = iterator.next();
// LOGGER.info("Remaining " + searchServices.getResultsCount(query) + " emails in collection " + query.getCondition()
// .getCollection());
query.setNumberOfRows(SEND_EMAIL_BATCH);
List<Record> records = searchServices.search(query);
if (records.isEmpty()) {
iterator.remove();
} else {
try {
trySendCollectionEmails(query, records);
} catch (EmailServicesException.EmailServerException e) {
LOGGER.warn("error when trying send collection emails ", e);
LocalDateTime tomorrow = TimeProvider.getLocalDateTime().plusDays(1);
postponeRemainingEmailToDateTime(query, tomorrow, "server exception");
iterator.remove();
}
}
}
}
}
private void trySendCollectionEmails(LogicalSearchQuery query, List<Record> records)
throws EmailServicesException.EmailServerException {
LocalDateTime tomorrow = TimeProvider.getLocalDateTime().plusDays(1);
EmailBuilder emailBuilder = new EmailBuilder(modelLayerFactory.getEmailTemplatesManager(),
modelLayerFactory.getSystemConfigurationsManager());
String collection = query.getCondition().getCollection();
SchemasRecordsServices schemas = new SchemasRecordsServices(collection, modelLayerFactory);
Metadata numberOfTryMetadata = getNumberOfTryMetadata(collection);
Metadata errorMetadata = getEmailToSendErrorMetadata(collection);
Metadata sendDateMetadata = getSendDateMetadata(collection);
EmailServerConfiguration emailConfiguration = getEmailConfiguration(collection);
boolean enabled = emailConfiguration.isEnabled();
if (enabled) {
Session session = emailServices.openSession(emailConfiguration);
String defaultEmail = emailConfiguration.getDefaultSenderEmail();
List<Record> recordsToUpdate = new ArrayList<>();
for (Record record : records) {
if (emailConfiguration.isEnabled()) {
try {
Message email = buildEmail(session, emailBuilder, schemas.wrapEmailToSend(record), defaultEmail);
emailServices.sendEmail(email);
deleteEmail(record, "email sent correctly");
subsequentFailuresCount = 0;
} catch (EmailServicesException.EmailTempException e) {
subsequentFailuresCount++;
LOGGER.warn("Email not sent correctly", e);
Record updatedRecord = postponeEmailToDateTime(record, tomorrow, e.getMessage(), numberOfTryMetadata,
errorMetadata, sendDateMetadata);
if (updatedRecord != null) {
recordsToUpdate.add(updatedRecord);
}
} catch (EmailServicesException.EmailPermanentException e) {
subsequentFailuresCount++;
LOGGER.warn("Email not sent correctly", e);
deleteEmail(record, e.getMessage());
} catch (Exception e) {
subsequentFailuresCount++;
LOGGER.warn("Email not sent correctly", e);
Record updatedRecord = postponeEmailToDateTime(record, tomorrow, e.getMessage(), numberOfTryMetadata,
errorMetadata, sendDateMetadata);
if (updatedRecord != null) {
recordsToUpdate.add(updatedRecord);
}
}
if (subsequentFailuresCount >= MAXIMUM_FAILURES_BEFORE_DISABLING_SMTP_SERVER) {
enabled = false;
emailConfigurationManager
.updateEmailServerConfiguration(emailConfiguration.whichIsDisabled(), collection, true);
}
}
}
emailServices.closeSession(session);
if (!recordsToUpdate.isEmpty()) {
updateRecords(recordsToUpdate);
}
} else {
for (Record record : records) {
deleteEmail(record, "email is not sent since smtp server is disabled");
}
}
}
private Message buildEmail(Session session, EmailBuilder emailBuilder, EmailToSend emailToSend, String defaultEmail)
throws Exception {
try {
return emailBuilder.build(emailToSend, session, defaultEmail);
} catch (MessagingException e) {
Exception exception = new EmailServices().throwAppropriateException(e);
throw exception;
} catch (EmailBuilder.InvalidBlankEmail invalidBlankEmail) {
throw new EmailServicesException.EmailPermanentException(invalidBlankEmail);
}
}
private void updateRecords(List<Record> recordsToUpdate) {
try {
LOGGER.warn("Update records start");
recordServices.update(recordsToUpdate, null);
LOGGER.warn("Update records succeeded");
} catch (RecordServicesException e) {
LOGGER.warn("Update records failed", e);
}
}
EmailServerConfiguration getEmailConfiguration(String collection) {
return emailConfigurationManager.getEmailConfiguration(collection, true);
}
private List<LogicalSearchQuery> prepareSearchQueries(LocalDateTime dateTime) {
List<LogicalSearchQuery> returnList = new ArrayList<>();
List<String> collections = this.collectionManager.getCollections();
for (String collection : collections) {
if (modelLayerFactory.getEmailConfigurationsManager().getEmailConfiguration(collection, false) != null) {
returnList.add(prepareRemainingEmailsToSend(collection, dateTime));
}
}
return returnList;
}
private LogicalSearchQuery prepareRemainingEmailsToSend(String collection, LocalDateTime dateTime) {
Metadata sendDateMetadata = getSendDateMetadata(collection);
MetadataSchema schema = getEmailToSendSchema(collection);
LogicalSearchCondition condition = from(schema).where(sendDateMetadata).isLessOrEqualThan(dateTime);
return new LogicalSearchQuery(condition).sortAsc(sendDateMetadata);
}
private void postponeRemainingEmailToDateTime(LogicalSearchQuery query, LocalDateTime dateTime, String error) {
do {
List<Record> records = searchServices.search(query);
if (records.isEmpty()) {
break;
}
List<Record> recordsToUpdate = new ArrayList<>();
String collection = query.getCondition().getCollection();
Metadata numberOfTryMetadata = getNumberOfTryMetadata(collection);
Metadata errorMetadata = getEmailToSendErrorMetadata(collection);
Metadata sendDateMetadata = getSendDateMetadata(collection);
for (Record record : records) {
Record updatedRecord = postponeEmailToDateTime(record, dateTime, error, numberOfTryMetadata, errorMetadata,
sendDateMetadata);
if (updatedRecord != null) {
recordsToUpdate.add(updatedRecord);
}
}
updateRecords(recordsToUpdate);
} while (true);
}
private Record postponeEmailToDateTime(Record record, LocalDateTime dateTime, String error, Metadata numberOfTryMetadata,
Metadata errorMetadata, Metadata sendDateMetadata) {
if (record == null) {
LOGGER.error("Cannot postposne null record!!");
return null;
}
Object numberOfTryObject = record.get(numberOfTryMetadata);
int numberOfTry = 0;
if (numberOfTryObject != null) {
numberOfTry = ((Double) numberOfTryObject).intValue();
}
if (numberOfTry >= MAX_TRY_SEND) {
deleteEmail(record, "Exceeded number of try send " + numberOfTry);
return null;
} else {
record.set(numberOfTryMetadata, numberOfTry + 1);
record.set(errorMetadata, error);
record.set(sendDateMetadata, dateTime);
LOGGER.warn("Record (" + record.getId() + ") postponed because of :" + error);
return record;
}
}
private void deleteEmail(Record record, String error) {
LOGGER.warn("Record (" + record.getId() + ") deleted because of: " + error);
recordServices.logicallyDelete(record, null);
recordServices.physicallyDelete(record, null);
}
private Metadata getSendDateMetadata(String collection) {
return getEmailToSendSchema(collection).getMetadata(EmailToSend.SEND_ON);
}
private Metadata getEmailToSendErrorMetadata(String collection) {
return getEmailToSendSchema(collection).getMetadata(EmailToSend.ERROR);
}
private Metadata getNumberOfTryMetadata(String collection) {
return getEmailToSendSchema(collection).getMetadata(EmailToSend.TRYING_COUNT);
}
private MetadataSchema getEmailToSendSchema(String collection) {
SchemasRecordsServices schemasRecords = new SchemasRecordsServices(collection, modelLayerFactory);
return schemasRecords.schema(EmailToSend.DEFAULT_SCHEMA);
}
@Override
public void close() {
}
public void clearQueue() {
ModifiableSolrParams params = new ModifiableSolrParams();
params.set("q", "schema_s:" + EmailToSend.DEFAULT_SCHEMA);
try {
modelLayerFactory.getDataLayerFactory().newRecordDao().execute(new TransactionDTO(RecordsFlushing.NOW())
.withDeletedByQueries(params));
} catch (OptimisticLocking optimisticLocking) {
throw new RuntimeException(optimisticLocking);
}
}
}