/** * * 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.injection.scope.Service; import org.qi4j.api.injection.scope.Structure; import org.qi4j.api.injection.scope.This; import org.qi4j.api.io.Output; import org.qi4j.api.mixin.Mixins; import org.qi4j.api.service.Activatable; import org.qi4j.api.service.ServiceComposite; import org.qi4j.api.specification.Specification; import org.qi4j.api.structure.Module; import org.qi4j.api.unitofwork.UnitOfWork; import org.qi4j.api.usecase.UsecaseBuilder; import org.qi4j.api.util.Iterables; import org.qi4j.api.value.ValueBuilder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import se.streamsource.dci.api.RoleMap; import se.streamsource.streamflow.api.workspace.cases.CaseStates; import se.streamsource.streamflow.api.workspace.cases.conversation.MessageType; import se.streamsource.streamflow.infrastructure.event.application.ApplicationEvent; import se.streamsource.streamflow.infrastructure.event.application.TransactionApplicationEvents; import se.streamsource.streamflow.infrastructure.event.application.replay.ApplicationEventPlayer; import se.streamsource.streamflow.infrastructure.event.application.replay.ApplicationEventReplayException; import se.streamsource.streamflow.infrastructure.event.application.source.ApplicationEventSource; import se.streamsource.streamflow.infrastructure.event.application.source.ApplicationEventStream; import se.streamsource.streamflow.infrastructure.event.application.source.helper.ApplicationEvents; import se.streamsource.streamflow.infrastructure.event.application.source.helper.ApplicationTransactionTracker; import se.streamsource.streamflow.util.Strings; import se.streamsource.streamflow.util.Translator; import se.streamsource.streamflow.web.application.defaults.SystemDefaultsService; import se.streamsource.streamflow.web.application.mail.EmailValue; import se.streamsource.streamflow.web.application.mail.MailReceiver; import se.streamsource.streamflow.web.context.workspace.cases.CaseCommandsContext; import se.streamsource.streamflow.web.domain.entity.caze.CaseEntity; import se.streamsource.streamflow.web.domain.entity.user.UserEntity; 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.caselog.CaseLoggable; import se.streamsource.streamflow.web.domain.structure.caze.Case; import se.streamsource.streamflow.web.domain.structure.conversation.Conversation; import se.streamsource.streamflow.web.domain.structure.conversation.ConversationParticipant; import se.streamsource.streamflow.web.domain.structure.conversation.Message; import java.net.URLDecoder; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * Receive emails and create responses in conversations */ @Mixins(ConversationResponseService.Mixin.class) public interface ConversationResponseService extends Configuration, Activatable, ServiceComposite { class Mixin implements Activatable { @Service ApplicationEventSource eventSource; @Service ApplicationEventStream stream; @Service SystemDefaultsService systemDefaults; @Structure Module module; @This Configuration<ConversationResponseConfiguration> config; private ApplicationTransactionTracker<ApplicationEventReplayException> tracker; private Logger logger = LoggerFactory.getLogger(ConversationResponseService.class); @Service ApplicationEventPlayer player; private ReceiveEmails receiveEmails = new ReceiveEmails(); public void activate() throws Exception { Output<TransactionApplicationEvents, ApplicationEventReplayException> playerOutput = ApplicationEvents.playEvents( player, receiveEmails ); tracker = new ApplicationTransactionTracker<ApplicationEventReplayException>( stream, eventSource, config, playerOutput ); tracker.start(); } public void passivate() throws Exception { tracker.stop(); } public class ReceiveEmails extends MailReceiver.Mixin { public void receivedEmail( ApplicationEvent event, EmailValue email ) { UnitOfWork uow = module.unitOfWorkFactory().newUnitOfWork(UsecaseBuilder.newUsecase("Receive email in conversation")); try { String references = email.headers().get().get( "References" ); if ( hasStreamflowReference( references ) ) { // This is a response - handle it! List<String> refs = (List<String>) Iterables.addAll((Collection<String>) new ArrayList<String>(), Iterables.iterable(references.split("[ \r\n\t]"))); // Hotmail handles refs a bit differently... String hotmailRefs = Iterables.first( Iterables.filter(new Specification<String>() { public boolean satisfiedBy(String item) { return item.contains( "," ) && item.endsWith("@Streamflow>"); } }, refs)); String lastRef = null; if (!Strings.empty( hotmailRefs )) { lastRef = hotmailRefs.split( "," )[1]; } else { Collections.reverse( refs ); Iterable<String> filter = Iterables.filter( new Specification<String>() { public boolean satisfiedBy(String item) { return item.endsWith( "@Streamflow>" ); } }, refs ); lastRef = Iterables.first( filter ); } if (lastRef == null) { ValueBuilder<EmailValue> builder = module.valueBuilderFactory().newValueBuilder( EmailValue.class ).withPrototype( email ); String subj = "Msg Ref missing: " + builder.prototype().subject().get(); builder.prototype().subject().set( subj.length() > 50 ? subj.substring( 0, 50 ) : subj ); systemDefaults.createCaseOnEmailFailure( builder.newInstance() ); logger.error("Could not find message reference in email header:"+lastRef); uow.discard(); return; } Matcher matcher = Pattern.compile("<([^/]*)/([^@]*)@[^>]*>").matcher(lastRef); if (matcher.find()) { String conversationId = matcher.group(1); String participantId = URLDecoder.decode(matcher.group(2), "UTF-8"); if (!"".equals( conversationId ) && !"".equals( participantId )) { ConversationParticipant from = uow.get( ConversationParticipant.class, participantId ); Conversation conversation = uow.get( Conversation.class, conversationId ); CaseEntity caze = (CaseEntity) conversation.conversationOwner().get(); String content = email.content().get(); // If we have an assignee, ensure it is a member of the conversation first if (caze.isAssigned()) { if (!conversation.isParticipant((ConversationParticipant) caze.assignedTo().get())) conversation.addParticipant((ConversationParticipant) caze.assignedTo().get()); } // Create a new role map and fill it with relevant objects if( RoleMap.current() == null ) RoleMap.newCurrentRoleMap(); RoleMap.current().set( from, ConversationParticipant.class ); RoleMap.current().set( caze, CaseLoggable.Data.class ); RoleMap.current().set( caze, Case.class ); Message message = null; if( Translator.HTML.equalsIgnoreCase( email.contentType().get() )) { message = conversation.createMessage( email.contentHtml().get(), MessageType.HTML, from ); } else { message = conversation.createMessage( email.content().get(), MessageType.PLAIN, from ); } // Create attachments for (AttachedFileValue attachedFileValue : email.attachments().get()) { if (! (attachedFileValue.mimeType().get().contains("text/x-vcard") || attachedFileValue.mimeType().get().contains("text/directory")) ) { Attachment attachment = message.createAttachment(attachedFileValue.uri().get()); attachment.changeDescription( "New Attachment" ); attachment.changeName(attachedFileValue.name().get()); attachment.changeMimeType(attachedFileValue.mimeType().get()); attachment.changeModificationDate(attachedFileValue.modificationDate().get()); attachment.changeSize(attachedFileValue.size().get()); attachment.changeUri(attachedFileValue.uri().get()); } } try { if( caze.isStatus( CaseStates.CLOSED )) { RoleMap.newCurrentRoleMap(); RoleMap.current().set( caze ); if( caze.assignedTo().get() != null ) { RoleMap.current().set( caze.assignedTo().get() ); } else { RoleMap.current().set( uow.get( UserEntity.class, UserEntity.ADMINISTRATOR_USERNAME ) ); } CaseCommandsContext caseCommands = module.transientBuilderFactory().newTransient( CaseCommandsContext.class ); caseCommands.reopen(); caseCommands.unassign(); RoleMap.clearCurrentRoleMap(); } } catch(Throwable e ) { ValueBuilder<EmailValue> builder = module.valueBuilderFactory().newValueBuilder( EmailValue.class ).withPrototype( email ); String subj = "Create Case failed: " + builder.prototype().subject().get(); builder.prototype().subject().set( subj.length() > 50 ? subj.substring( 0, 50 ) : subj ); systemDefaults.createCaseOnEmailFailure( builder.newInstance() ); //throw new IllegalStateException("Could not open case through new message.", e); } } } } uow.complete(); } catch (Exception ex) { ValueBuilder<EmailValue> builder = module.valueBuilderFactory().newValueBuilder( EmailValue.class ).withPrototype( email ); String subj = "Conversation Response Error: " + builder.prototype().subject().get(); builder.prototype().subject().set( subj.length() > 50 ? subj.substring( 0, 50 ) : subj ); StringBuilder content = new StringBuilder(); content.append( "Error Message: " + ex.getMessage() ); content.append( "\n\rStackTrace:\n\r" ); for (StackTraceElement trace : Arrays.asList( ex.getStackTrace() )) { content.append( trace.toString() + "\n\r" ); } builder.prototype().content().set( content.toString() ); // since we create the content of the message our self it's ok to set content type always to text/plain builder.prototype().contentType().set( "text/plain" ); // Make sure to address has some value before vi create a case!! if( builder.prototype().to().get() == null ) { builder.prototype().to().set( "n/a" ); } systemDefaults.createCaseOnEmailFailure( builder.newInstance() ); uow.discard(); //throw new ApplicationEventReplayException(event, ex); } } } } }