/** * * Copyright * 2009-2015 Jayway Products AB * 2016-2017 Föreningen Sambruk * * Licensed under AGPL, Version 3.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.gnu.org/licenses/agpl.txt * * 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 se.streamsource.streamflow.web.rest.service.conversation; import org.qi4j.api.configuration.Configuration; import org.qi4j.api.entity.EntityReference; import org.qi4j.api.entity.association.ManyAssociation; import org.qi4j.api.injection.scope.Service; import org.qi4j.api.injection.scope.Structure; import org.qi4j.api.injection.scope.This; import org.qi4j.api.mixin.Mixins; import org.qi4j.api.service.Activatable; import org.qi4j.api.service.ServiceComposite; import org.qi4j.api.structure.Module; import org.qi4j.api.unitofwork.UnitOfWork; import org.qi4j.api.usecase.UsecaseBuilder; import org.qi4j.api.value.ValueBuilder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import se.streamsource.streamflow.api.workspace.cases.contact.ContactEmailDTO; import se.streamsource.streamflow.api.workspace.cases.conversation.MessageType; import se.streamsource.streamflow.infrastructure.event.domain.DomainEvent; import se.streamsource.streamflow.infrastructure.event.domain.replay.DomainEventPlayer; import se.streamsource.streamflow.infrastructure.event.domain.source.EventSource; import se.streamsource.streamflow.infrastructure.event.domain.source.EventStream; import se.streamsource.streamflow.infrastructure.event.domain.source.helper.EventRouter; import se.streamsource.streamflow.infrastructure.event.domain.source.helper.Events; import se.streamsource.streamflow.infrastructure.event.domain.source.helper.TransactionTracker; import se.streamsource.streamflow.util.MessageTemplate; import se.streamsource.streamflow.util.Translator; import se.streamsource.streamflow.web.application.mail.EmailValue; import se.streamsource.streamflow.web.application.mail.HtmlMailGenerator; import se.streamsource.streamflow.web.application.mail.MailSender; import se.streamsource.streamflow.web.domain.entity.user.UserEntity; import se.streamsource.streamflow.web.domain.interaction.gtd.CaseId; import se.streamsource.streamflow.web.domain.interaction.profile.MailFooter; import se.streamsource.streamflow.web.domain.interaction.profile.MessageRecipient; import se.streamsource.streamflow.web.domain.interaction.security.CaseAccessRestriction; import se.streamsource.streamflow.web.domain.structure.attachment.AttachedFile; import se.streamsource.streamflow.web.domain.structure.attachment.AttachedFileValue; import se.streamsource.streamflow.web.domain.structure.attachment.Attachment; import se.streamsource.streamflow.web.domain.structure.attachment.Attachments; import se.streamsource.streamflow.web.domain.structure.caze.Origin; import se.streamsource.streamflow.web.domain.structure.conversation.Conversation; import se.streamsource.streamflow.web.domain.structure.conversation.ConversationOwner; import se.streamsource.streamflow.web.domain.structure.conversation.Message; import se.streamsource.streamflow.web.domain.structure.conversation.MessageReceiver; import se.streamsource.streamflow.web.domain.structure.conversation.Messages; import se.streamsource.streamflow.web.domain.structure.organization.EmailAccessPoint; import se.streamsource.streamflow.web.domain.structure.organization.EmailAccessPoints; import se.streamsource.streamflow.web.domain.structure.organization.EmailTemplates; import se.streamsource.streamflow.web.domain.structure.user.Contactable; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.ResourceBundle; import java.util.Scanner; /** * Send and receive notifications. This service * listens for domain events, and on "receivedMessage" it will send * a notification to the provided recipient. */ @Mixins(NotificationService.Mixin.class) public interface NotificationService extends Configuration, Activatable, MailSender, ServiceComposite { class Mixin implements Activatable { final Logger logger = LoggerFactory.getLogger( NotificationService.class.getName() ); @Service private EventSource eventSource; @Service private EventStream stream; @Structure private Module module; @This private Configuration<NotificationConfiguration> config; @This private MailSender mailSender; private TransactionTracker tracker; @Service DomainEventPlayer player; private SendEmails sendEmails = new SendEmails(); Map<String, String> templateDefaults = new HashMap<String, String>(); private EmailAccessPoints eap; public void activate() throws Exception { // Get defaults for emails ResourceBundle bundle = ResourceBundle.getBundle(EmailTemplates.class.getName()); for (String key : bundle.keySet()) { templateDefaults.put(key, bundle.getString(key)); } // Store reference to EAP UnitOfWork uow = module.unitOfWorkFactory().newUnitOfWork(UsecaseBuilder.newUsecase("Update templates")); eap = module.queryBuilderFactory().newQueryBuilder(EmailAccessPoints.class).newQuery(uow).find(); uow.discard(); EventRouter router = new EventRouter(); router.route( Events.withNames( SendEmails.class ), Events.playEvents( player, sendEmails, module.unitOfWorkFactory(), UsecaseBuilder.newUsecase("Send email to participant" )) ); tracker = new TransactionTracker( stream, eventSource, config, Events.adapter( router ) ); tracker.start(); } public void passivate() throws Exception { tracker.stop(); } public class SendEmails implements MessageReceiver.Data { public void receivedMessage( DomainEvent event, Message message ) { UnitOfWork uow = module.unitOfWorkFactory().currentUnitOfWork(); try { MessageReceiver recipient = uow.get( MessageReceiver.class, event.entity().get() ); if (shouldSendFullMail(message, recipient)) { EmailValue emailValue = buildFullMail(message, recipient); mailSender.sentEmail( null, emailValue ); } else if (shouldSendNotificationMail(message, recipient)) { EmailValue emailValue = buildNotificationMail(message, recipient); mailSender.sentEmail( null, emailValue ); } } catch (Throwable e) { logger.error("Could not send notification to user entity = " + event.entity().get() , e); } } private EmailValue buildFullMail(Message message, MessageReceiver recipient) throws UnsupportedEncodingException { Message.Data messageData = (Message.Data) message; Conversation conversation = messageData.conversation().get(); ValueBuilder<EmailValue> builder = module.valueBuilderFactory().newValueBuilder(EmailValue.class); builder.prototype().fromName().set( determineFromName(message) ); builder.prototype().to().set( determineRecipientEmailAddress(recipient) ); builder.prototype().subject().set( determineFullSubject(message) ); builder.prototype().content().set( createFullHTMLMailContent(message) ); builder.prototype().contentType().set( Translator.HTML ); addAttachmentsToBuilder(message, builder); addEmailAccessPointHeadersToBuilder(message, builder); addThreadingHeadersToBuilder(conversation, recipient, builder); EmailValue emailValue = builder.newInstance(); return emailValue; } private EmailValue buildNotificationMail(Message message, MessageReceiver recipient) throws UnsupportedEncodingException { Message.Data messageData = (Message.Data) message; Conversation conversation = messageData.conversation().get(); ValueBuilder<EmailValue> builder = module.valueBuilderFactory().newValueBuilder(EmailValue.class); builder.prototype().fromName().set( determineFromName(message) ); builder.prototype().to().set( determineRecipientEmailAddress(recipient) ); builder.prototype().subject().set( determineNotificationSubject(message) ); builder.prototype().content().set( createNotificationHTMLMailContent(message) ); builder.prototype().contentType().set( Translator.HTML ); addEmailAccessPointHeadersToBuilder(message, builder); addThreadingHeadersToBuilder(conversation, recipient, builder); EmailValue emailValue = builder.newInstance(); return emailValue; } private String determineFromName(Message message) { // check if sender is administrator and in that case dont set fromName - this will be picked up by // MailSender and replaced with the configurated default fromName Message.Data messageData = (Message.Data) message; String fromName = null; if( ! EntityReference.getEntityReference( messageData.sender().get() ).identity().equals( UserEntity.ADMINISTRATOR_USERNAME )) { fromName = ((Contactable.Data) messageData.sender().get()).contact().get().name().get(); } return fromName; } private String determineFooter(Message message) { Message.Data messageData = (Message.Data) message; String footer =""; if( messageData.sender().get() instanceof MailFooter) { footer = ((MailFooter.Data)messageData.sender().get()).footer().get(); } return footer; } private String determineCaseId(ConversationOwner owner) { String caseId = "n/a"; if (owner != null) caseId = ((CaseId.Data) owner).caseId().get() != null ? ((CaseId.Data) owner).caseId().get() : "n/a"; return caseId; } private String determineRecipientEmailAddress(MessageReceiver recipient) { ContactEmailDTO recipientEmail = ((Contactable.Data)recipient).contact().get().defaultEmail(); if (recipientEmail == null) { return null; } else { return recipientEmail.emailAddress().get(); } } private String createFullHTMLMailContent(Message message) { Message.Data messageData = (Message.Data) message; Conversation conversation = messageData.conversation().get(); ConversationOwner conversationOwner = conversation.conversationOwner().get(); Origin origin = (Origin) conversationOwner; EmailAccessPoint emailAccessPoint = origin.accesspoint().get(); String formattedMsg; { if (emailAccessPoint != null) { formattedMsg = message.translateBody(emailAccessPoint.emailTemplates().get()); } else { formattedMsg = message.translateBody(templateDefaults); } } if( messageData.messageType().get().equals( MessageType.PLAIN ) || messageData.messageType().get().equals( MessageType.SYSTEM )) { StringBuffer buf = new StringBuffer( ); Scanner scanner = new Scanner( formattedMsg ); while( scanner.hasNextLine() ) { buf.append( scanner.nextLine() + "<BR>" + System.getProperty( "line.separator" ) ); } scanner.close(); formattedMsg = buf.toString(); } HtmlMailGenerator htmlGenerator = module.objectBuilderFactory().newObject( HtmlMailGenerator.class ); String mailContent = htmlGenerator.createMailContent( formattedMsg, determineFooter(message) ); return mailContent; } private String createNotificationHTMLMailContent(Message message) { Message.Data messageData = (Message.Data) message; Conversation conversation = messageData.conversation().get(); ConversationOwner conversationOwner = conversation.conversationOwner().get(); String caseId = determineCaseId(conversationOwner); String mailBody = config.configuration().notificationOnlyMailBody().get(); String formattedMsg = String.format(mailBody, caseId); HtmlMailGenerator htmlGenerator = module.objectBuilderFactory().newObject( HtmlMailGenerator.class ); String mailContent = htmlGenerator.createMailContent( formattedMsg, determineFooter(message) ); return mailContent; } private String determineFullSubject(Message message) { Message.Data messageData = (Message.Data) message; Conversation conversation = messageData.conversation().get(); ConversationOwner conversationOwner = conversation.conversationOwner().get(); Origin origin = (Origin) conversationOwner; EmailAccessPoint emailAccessPoint = origin.accesspoint().get(); String subject; { String caseId = determineCaseId(conversationOwner); if (emailAccessPoint != null) { subject = MessageTemplate.text(emailAccessPoint.subject().get()).bind("caseid", caseId).bind("subject", conversation.getDescription()).eval(); } else { subject = "[" + caseId + "] " + conversation.getDescription(); // Default subject format } } return subject; } private String determineNotificationSubject(Message message) { Message.Data messageData = (Message.Data) message; Conversation conversation = messageData.conversation().get(); ConversationOwner conversationOwner = conversation.conversationOwner().get(); String caseId = determineCaseId(conversationOwner); String subjectText = config.configuration().notificationOnlyMailSubject().get(); return "[" + caseId + "] " + subjectText; } private void addEmailAccessPointHeadersToBuilder(Message message, ValueBuilder<EmailValue> builder) { Message.Data messageData = (Message.Data) message; Conversation conversation = messageData.conversation().get(); ConversationOwner conversationOwner = conversation.conversationOwner().get(); Origin origin = (Origin) conversationOwner; EmailAccessPoint emailAccessPoint = origin.accesspoint().get(); if (emailAccessPoint != null) { builder.prototype().from().set(emailAccessPoint.getDescription() ); builder.prototype().headers().get().put( "Auto-Submitted", "auto-replied" ); builder.prototype().headers().get().put( "X-Auto-Response-Suppress", "OOF, DR, RN, NRN" ); builder.prototype().headers().get().put( "X-Autoreply", "yes" ); builder.prototype().headers().get().put( "X-Autorespond", "yes" ); builder.prototype().headers().get().put( "Precedence", "auto_reply" ); builder.prototype().headers().get().put( "X-Precedence", "auto_reply" ); } } private void addAttachmentsToBuilder(Message message, ValueBuilder<EmailValue> builder) { // add message attachments if any if ( message.hasAttachments()) { List<AttachedFileValue> attachments = builder.prototype().attachments().get(); ValueBuilder<AttachedFileValue> attachment = module.valueBuilderFactory().newValueBuilder(AttachedFileValue.class); for (Attachment caseAttachment : ((Attachments.Data)message).attachments()) { AttachedFile.Data attachedFile = (AttachedFile.Data) caseAttachment; attachment.prototype().mimeType().set(attachedFile.mimeType().get()); attachment.prototype().uri().set(attachedFile.uri().get()); attachment.prototype().modificationDate().set(attachedFile.modificationDate().get()); attachment.prototype().name().set(attachedFile.name().get()); attachment.prototype().size().set(attachedFile.size().get()); attachments.add( attachment.newInstance() ); } } } private void addThreadingHeadersToBuilder(Conversation conversation, MessageReceiver recipient, ValueBuilder<EmailValue> builder) throws UnsupportedEncodingException { // Threading headers builder.prototype().messageId().set( "<"+conversation.toString()+"/"+ URLEncoder.encode(recipient.toString(), "UTF-8")+"@Streamflow>" ); ManyAssociation<Message> messages = ((Messages.Data)conversation).messages(); StringBuilder references = new StringBuilder(); String inReplyTo = null; for (Message previousMessage : messages) { if (references.length() > 0) references.append( " " ); inReplyTo = "<"+previousMessage.toString()+"/"+URLEncoder.encode(recipient.toString(), "UTF-8")+"@Streamflow>"; references.append( inReplyTo ); } builder.prototype().headers().get().put( "References", references.toString() ); if (inReplyTo != null) builder.prototype().headers().get().put( "In-Reply-To", inReplyTo ); } private boolean shouldSendFullMail(Message message, MessageReceiver recipient) { MessageRecipient.Data recipientSettings = (MessageRecipient.Data) recipient; Message.Data messageData = (Message.Data) message; Conversation conversation = messageData.conversation().get(); ConversationOwner conversationOwner = conversation.conversationOwner().get(); boolean isRestricted = ((CaseAccessRestriction.Data) conversationOwner).restricted().get(); return ( recipientSettings.delivery().get().equals( MessageRecipient.MessageDeliveryTypes.email ) && (!messageData.body().get().trim().isEmpty() || message.hasAttachments()) && determineRecipientEmailAddress(recipient) != null && !isRestricted ); } private boolean shouldSendNotificationMail(Message message, MessageReceiver recipient) { MessageRecipient.Data recipientSettings = (MessageRecipient.Data) recipient; Message.Data messageData = (Message.Data) message; Conversation conversation = messageData.conversation().get(); ConversationOwner conversationOwner = conversation.conversationOwner().get(); boolean isRestricted = ((CaseAccessRestriction.Data) conversationOwner).restricted().get(); boolean isRecipientRegularUser = recipient instanceof UserEntity; return ( recipientSettings.delivery().get().equals( MessageRecipient.MessageDeliveryTypes.email ) && (!messageData.body().get().trim().isEmpty() || message.hasAttachments()) && determineRecipientEmailAddress(recipient) != null && (isRestricted && isRecipientRegularUser) ); } } } }