/**
*
* 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.application.mail;
import info.ineighborhood.cardme.engine.VCardEngine;
import info.ineighborhood.cardme.vcard.VCard;
import info.ineighborhood.cardme.vcard.features.AddressFeature;
import org.jsoup.Jsoup;
import org.jsoup.safety.Whitelist;
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.Input;
import org.qi4j.api.io.Output;
import org.qi4j.api.io.Outputs;
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 se.streamsource.dci.api.RoleMap;
import se.streamsource.streamflow.api.workspace.cases.caselog.CaseLogEntryTypes;
import se.streamsource.streamflow.api.workspace.cases.contact.ContactBuilder;
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.Translator;
import se.streamsource.streamflow.web.application.defaults.SystemDefaultsService;
import se.streamsource.streamflow.web.domain.entity.caze.CaseEntity;
import se.streamsource.streamflow.web.domain.entity.gtd.Drafts;
import se.streamsource.streamflow.web.domain.entity.organization.OrganizationsEntity;
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.conversation.Conversation;
import se.streamsource.streamflow.web.domain.structure.conversation.ConversationParticipant;
import se.streamsource.streamflow.web.domain.structure.conversation.Message;
import se.streamsource.streamflow.web.domain.structure.created.Creator;
import se.streamsource.streamflow.web.domain.structure.organization.EmailAccessPoint;
import se.streamsource.streamflow.web.domain.structure.organization.Organization;
import se.streamsource.streamflow.web.domain.structure.organization.Organizations;
import se.streamsource.streamflow.web.domain.structure.user.Contactable;
import se.streamsource.streamflow.web.infrastructure.attachment.AttachmentStore;
import javax.mail.MessagingException;
import javax.mail.internet.MimeUtility;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
/**
* Receive emails and create cases through Access Points
*/
@Mixins(CreateCaseFromEmailService.Mixin.class)
public interface CreateCaseFromEmailService
extends Configuration, Activatable, ServiceComposite
{
class Mixin
implements Activatable
{
@Service
ApplicationEventSource eventSource;
@Service
ApplicationEventStream stream;
@Service
AttachmentStore attachments;
@Service
SystemDefaultsService systemDefaults;
@Structure
Module module;
@This
Configuration<CreateCaseFromEmailConfiguration> config;
private ApplicationTransactionTracker<ApplicationEventReplayException> tracker;
@Service
ApplicationEventPlayer player;
private ReceiveEmails receiveEmails = new ReceiveEmails();
private VCardEngine vcardEngine;
public void activate() throws Exception
{
vcardEngine = new VCardEngine();
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( "Create case from email" ) );
try
{
String references = email.headers().get().get( "References" );
// This is not in response to something that we sent out - create new case from it
if (!hasStreamflowReference( references ))
{
Organizations.Data organizations = uow.get( Organizations.Data.class, OrganizationsEntity.ORGANIZATIONS_ID );
Organization organization = organizations.organization().get();
EmailAccessPoint ap = null;
try
{
ap = organization.getEmailAccessPoint( email.to().get() );
} catch (IllegalArgumentException e)
{
// No AP for this email address - create support case.
ValueBuilder<EmailValue> builder = module.valueBuilderFactory().newValueBuilder( EmailValue.class ).withPrototype( email );
String subj = "Unknown accesspoint: " + builder.prototype().to().get() + " - " + builder.prototype().subject().get();
builder.prototype().subject().set( subj.length() > 50 ? subj.substring( 0, 50 ) : subj );
systemDefaults.createCaseOnEmailFailure( builder.newInstance() );
uow.discard();
return;
}
if( ap != null && hasAutoReplyHeader( email.headers().get() ) )
{
// Possible mail loop - auto reply header present but no References - create support case.
ValueBuilder<EmailValue> builder = module.valueBuilderFactory().newValueBuilder( EmailValue.class ).withPrototype( email );
String subj = "Possible Mail Loop: " + builder.prototype().to().get() + " - " + builder.prototype().subject().get();
builder.prototype().subject().set( subj.length() > 50 ? subj.substring( 0, 50 ) : subj );
systemDefaults.createCaseOnEmailFailure( builder.newInstance() );
uow.discard();
return;
}
Drafts user = systemDefaults.getUser( email );
ConversationParticipant participant = (ConversationParticipant) user;
RoleMap.newCurrentRoleMap();
RoleMap.current().set( organization );
RoleMap.current().set( ap );
RoleMap.current().set( user );
CaseEntity caze = ap.createCase( user );
RoleMap.current().set( caze );
caze.caselog().get().addTypedEntry( "{accesspoint,description=" + ap.getDescription() + "}", CaseLogEntryTypes.system );
// STREAMFLOW-714
String subject = email.subject().get();
caze.changeDescription( subject.length() > 50 ? subject.substring( 0, 50 ) : subject );
if (Translator.HTML.equalsIgnoreCase( email.contentType().get() ))
{
caze.addNote( Jsoup.clean(email.content().get(), Whitelist.basic()), Translator.HTML );
//caze.addNote( Translator.cleanHtml( email.content().get() ), Translator.HTML );
} else
{
caze.addNote( email.content().get(), Translator.PLAIN );
}
// Create conversation
Conversation conversation = caze.createConversation( email.subject().get(), (Creator) user );
Message message = null;
if (Translator.HTML.equalsIgnoreCase( email.contentType().get() ))
{
message = conversation.createMessage( Translator.cleanHtml( email.content().get() ), MessageType.HTML, participant );
} else
{
message = conversation.createMessage( email.content().get(), MessageType.PLAIN, participant );
}
// Create attachments
for (AttachedFileValue attachedFileValue : email.attachments().get())
{
if (attachedFileValue.mimeType().get().contains( "text/x-vcard" )
|| attachedFileValue.mimeType().get().contains( "text/directory" ))
{
addVCardAsContact( (Contactable.Data) user, attachedFileValue );
} else
{
Attachment attachment = conversation.createAttachment( attachedFileValue.uri().get() );
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() );
message.addAttachment( attachment );
// remove attachment from conversation attachments data so AttachmentEntity does not get
// removed for real - we just moved it to message attachments where it actually belongs after
// message creation.
((Attachments.Data)conversation).attachments().remove( attachment );
}
}
// Add contact info
caze.updateContact( 0, ((Contactable.Data) user).contact().get() );
// Open the case
ap.sendTo( caze );
}
System.out.println( "CreateCaseFromEmailService before uow complete");
uow.complete();
System.out.println( "CreateCaseFromEmailService after uow complete");
} catch (Exception ex)
{
ValueBuilder<EmailValue> builder = module.valueBuilderFactory().newValueBuilder( EmailValue.class ).withPrototype( email );
String subj = "General error: " + builder.prototype().to().get() + " - " + builder.prototype().subject().get();
builder.prototype().subject().set( subj.length() > 50 ? subj.substring( 0, 50 ) : subj );
systemDefaults.createCaseOnEmailFailure( builder.newInstance() );
uow.discard();
throw new ApplicationEventReplayException( event, ex );
} finally
{
RoleMap.clearCurrentRoleMap();
}
}
private boolean hasAutoReplyHeader(Map<String,String> headers)
{
List autoReplyHeaders = new ArrayList(
Arrays.asList( "auto-submitted", "x-autoreply", "x-autorespond")
);
List precedenceHeaders = new ArrayList(
Arrays.asList("precedence", "x-precedence")
);
for( String key : headers.keySet() )
{
if( autoReplyHeaders.contains( key.toLowerCase()))
{
return true;
}
if( precedenceHeaders.contains( key.toLowerCase()) )
{
if( !"list".equalsIgnoreCase(headers.get(key)))
{
return true;
}
}
}
return false;
}
private void addVCardAsContact(Contactable.Data user, AttachedFileValue attachedFileValue) throws IOException
{
// Add VCard info to contact and then remove it as attachment
String[] mimeTypeParts = attachedFileValue.mimeType().get().split(";");
String charSet = "UTF-8";
for (String mimeTypePart : mimeTypeParts)
{
if (mimeTypePart.trim().startsWith("charset"))
{
charSet = mimeTypePart.split("=")[1].trim();
}
}
Input<ByteBuffer, IOException> input = attachments.attachment(attachedFileValue.uri().get().substring("store:".length()));
input.transferTo(Outputs.systemOut());
ByteArrayOutputStream baos = new ByteArrayOutputStream();
input.transferTo(Outputs.<Object>byteBuffer(baos));
InputStream inputStream = null;
try
{
inputStream = MimeUtility.decode(new ByteArrayInputStream(baos.toByteArray()), "quoted-printable");
} catch (MessagingException e)
{
throw new IOException("Could not decode VCard", e);
}
String vcardString = "";
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, Charset.forName(charSet)));
String line;
while ((line = reader.readLine()) != null)
vcardString += line +"\n";
VCard vcard = vcardEngine.parse(vcardString);
ContactBuilder contactBuilder = new ContactBuilder(user.contact().get(), module.valueBuilderFactory());
boolean modified = false;
// Check company
if (vcard.getOrganizations().hasOrganizations())
{
contactBuilder.company(vcard.getOrganizations().getOrganizations().next());
modified = true;
}
// Check phone numbers
if (vcard.getTelephoneNumbers().hasNext())
{
contactBuilder.phoneNumber(vcard.getTelephoneNumbers().next().getTelephone());
modified = true;
}
// Check address
if (vcard.getAddresses().hasNext())
{
AddressFeature address = vcard.getAddresses().next();
String addressString = address.getStreetAddress();
if (address.getPostalCode() != null)
addressString+=", "+address.getPostalCode();
if (address.getLocality() != null)
addressString+=", "+address.getLocality();
if (address.getCountryName() != null)
addressString+= ", "+address.getCountryName();
contactBuilder.address(addressString);
modified = true;
}
// Check note
if (vcard.getNotes().hasNext())
{
contactBuilder.note(vcard.getNotes().next().getNote());
modified = true;
}
// Update contact info if necessary
if (modified)
{
((Contactable)user).updateContact(contactBuilder.newInstance());
}
attachments.deleteAttachment(attachedFileValue.uri().get().substring("store:".length()));
}
}
}
}