/** * * 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 org.qi4j.api.configuration.Configuration; import org.qi4j.api.injection.scope.Service; import org.qi4j.api.injection.scope.This; import org.qi4j.api.injection.scope.Uses; import org.qi4j.api.mixin.Mixins; import org.qi4j.api.service.Activatable; import org.qi4j.api.service.ServiceComposite; import org.qi4j.spi.service.ServiceDescriptor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import se.streamsource.infrastructure.circuitbreaker.CircuitBreaker; import se.streamsource.infrastructure.circuitbreaker.service.ServiceCircuitBreaker; import se.streamsource.streamflow.infrastructure.event.application.ApplicationEvent; 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.util.Visitor; import se.streamsource.streamflow.web.application.defaults.SystemDefaultsService; import se.streamsource.streamflow.web.domain.structure.attachment.AttachedFileValue; import se.streamsource.streamflow.web.infrastructure.attachment.AttachmentStore; import javax.activation.DataHandler; import javax.activation.DataSource; import javax.mail.Authenticator; import javax.mail.MessagingException; import javax.mail.Multipart; import javax.mail.Part; import javax.mail.PasswordAuthentication; import javax.mail.Session; import javax.mail.Transport; import javax.mail.internet.InternetAddress; import javax.mail.internet.MimeBodyPart; import javax.mail.internet.MimeMessage; import javax.mail.internet.MimeMultipart; import javax.mail.internet.MimeUtility; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.Iterator; import java.util.Map; import java.util.Properties; import static se.streamsource.infrastructure.circuitbreaker.CircuitBreakers.*; /** * Send emails. This service * listens for application events, and on "sentEmail" it will send * the provided EmailValue. */ @Mixins(SendMailService.Mixin.class) public interface SendMailService extends Configuration, ServiceCircuitBreaker, Activatable, ServiceComposite { abstract class Mixin implements ServiceCircuitBreaker, Activatable { @org.qi4j.api.injection.scope.Service ApplicationEventSource source; @org.qi4j.api.injection.scope.Service ApplicationEventStream stream; @org.qi4j.api.injection.scope.Service ApplicationEventPlayer player; @org.qi4j.api.injection.scope.Service AttachmentStore attachmentStore; @This Configuration<SendMailConfiguration> config; @Uses ServiceDescriptor descriptor; @Service SystemDefaultsService systemDefaults; public Logger logger; Properties props; Authenticator authenticator; ApplicationTransactionTracker<ApplicationEventReplayException> tracker; private CircuitBreaker circuitBreaker; public void activate() throws Exception { logger = LoggerFactory.getLogger( SendMailService.class ); circuitBreaker = descriptor.metaInfo( CircuitBreaker.class ); tracker = new ApplicationTransactionTracker<ApplicationEventReplayException>( stream, source, config, withBreaker( circuitBreaker, ApplicationEvents.playEvents( player, new SendMails() ) )); if (config.configuration().enabled().get()) { // Authenticator authenticator = new Authenticator() { protected PasswordAuthentication getPasswordAuthentication() { return new PasswordAuthentication( config.configuration().user().get(), config.configuration().password().get() ); } }; // Setup mail server props = new Properties(); props.put( "mail.smtp.host", config.configuration().host().get() ); props.put( "mail.transport.protocol", "smtp" ); props.put( "mail.debug", config.configuration().debug().get() ); props.put( "mail.smtp.port", config.configuration().port().get() ); props.put( "mail.smtp.auth", config.configuration().authentication().get() ); if (config.configuration().useSSL().get()) { props.put( "mail.smtp.auth", "true" ); props.put( "mail.smtp.socketFactory.port", config.configuration().port().get() ); props.put( "mail.smtp.socketFactory.class", "javax.net.ssl.SSLSocketFactory" ); props.put( "mail.smtp.socketFactory.fallback", "false" ); props.setProperty( "mail.smtp.quitwait", "false" ); props.setProperty( "mail.transport.protocol", "smtps" ); } else if (config.configuration().useTLS().get()) { props.put( "mail.smtp.startTLS", "true" ); props.put( "mail.smtp.auth", "true" ); } tracker.start(); } } public void passivate() throws Exception { tracker.stop(); } public CircuitBreaker getCircuitBreaker() { return circuitBreaker; } public class SendMails implements MailSender { public void sentEmail( ApplicationEvent event, EmailValue email ) { try { //TODO needs a better solution but right now we don't try to send any email with no TO address if( Strings.empty( email.to().get() ) ) { logger.error( "Cannot send mail without valid TO address! Subject: " + email.subject().get() ); return; } // Make sure mail.jar and activation.jar are loaded by the same class loader. // http://stackoverflow.com/questions/1969667/send-a-mail-from-java5-and-java6 Thread.currentThread().setContextClassLoader( getClass().getClassLoader() ); Session session = Session.getInstance( props, authenticator ); session.setDebug( config.configuration().debug().get() ); SendMimeMessage msg = new SendMimeMessage( session, email ); if (email.fromName().get() == null) msg.setFrom( new InternetAddress( config.configuration().from().get(), config.configuration().fromName().get(), "ISO-8859-1" ) ); else msg.setFrom( new InternetAddress( config.configuration().from().get(), email.fromName().get(), "ISO-8859-1" ) ); msg.setRecipient( javax.mail.Message.RecipientType.TO, new InternetAddress( email.to().get() ) ); msg.setSubject( email.subject().get(), "UTF-8" ); for (Map.Entry<String, String> header : email.headers().get().entrySet()) { msg.setHeader( header.getKey(), header.getValue() ); } // set Reply-To header if(!Strings.empty( config.configuration().replyTo().get() )) msg.setHeader( "Reply-To", config.configuration().replyTo().get() ); // MimeBodyPart wrap MimeBodyPart wrap = new MimeBodyPart( ); // alternative text/html content Multipart bodyText = new MimeMultipart("alternative"); //PLAIN Content MimeBodyPart plainMimeBodyPart = new MimeBodyPart(); plainMimeBodyPart.setContent( Translator.htmlToText( email.content().get() ), "text/plain; charset=UTF-8" ); bodyText.addBodyPart( plainMimeBodyPart ); // HTML Content MimeBodyPart htmlMimeBodyPart = new MimeBodyPart(); htmlMimeBodyPart.setContent( email.content().get(), "text/html; charset=UTF-8" ); bodyText.addBodyPart( htmlMimeBodyPart ); wrap.setContent( bodyText ); Multipart content = new MimeMultipart(); content.addBodyPart( wrap ); // Add attachments Iterator<AttachedFileValue> attachments = email.attachments().get().iterator(); if (attachments.hasNext()) { AttachedFileValue attachedFileValue = attachments.next(); attachmentStore.attachment(attachedFileValue.uri().get(), new AttachmentVisitor(attachedFileValue, attachments, content, msg)); } else { // No attachments msg.setContent( content ); Transport.send( msg ); } // Removed since it would not be possible to resend mail events if the // generated case pdf is already deleted! // TODO: Consider to generate a html multipart for the contents of the case instead - so we would not need to generate and store a pdf file. // Delete attachments /*for (AttachedFileValue attachedFileValue : email.attachments().get()) { attachmentStore.deleteAttachment(attachedFileValue.uri().get()); }*/ logger.debug( "Sent mail to " + email.to().get() ); } catch (Throwable e) { logger.warn("Caught exception when sending mail, attempting to create error case", e); systemDefaults.createCaseOnSendMailFailure(email, e); } } private class AttachmentVisitor implements Visitor<InputStream, IOException> { private final AttachedFileValue attachedFileValue; private Iterator<AttachedFileValue> attachments; private Multipart multipart; private SendMimeMessage msg; public AttachmentVisitor(AttachedFileValue attachedFileValue, Iterator<AttachedFileValue> attachments, Multipart multipart, SendMimeMessage msg) { this.attachedFileValue = attachedFileValue; this.attachments = attachments; this.multipart = multipart; this.msg = msg; } public boolean visit(final InputStream visited) throws IOException { try { MimeBodyPart attachmentPart = new MimeBodyPart(); attachmentPart.setFileName( MimeUtility.encodeText(attachedFileValue.name().get(), "UTF-8", "Q" )); attachmentPart.setDisposition(Part.ATTACHMENT); attachmentPart.setDataHandler(new DataHandler(new DataSource() { public InputStream getInputStream() throws IOException { return visited; } public OutputStream getOutputStream() throws IOException { return null; } public String getContentType() { return attachedFileValue.mimeType().get(); } public String getName() { return attachedFileValue.name().get(); } })); attachmentPart.setHeader("Content-Transfer-Encoding", "base64"); multipart.addBodyPart(attachmentPart); if (attachments.hasNext()) { AttachedFileValue attachedFileValue = attachments.next(); attachmentStore.attachment(attachedFileValue.uri().get(), new AttachmentVisitor(attachedFileValue, attachments, multipart, msg)); } else { msg.setContent(multipart); Transport.send(msg); } } catch (MessagingException e) { throw new IOException(e); } return true; } } } public static class SendMimeMessage extends MimeMessage { private final EmailValue email; public SendMimeMessage( Session session, EmailValue email ) { super( session ); this.email = email; } @Override protected void updateMessageID() throws MessagingException { String messageId = email.messageId().get(); if (messageId != null) setHeader( "Message-ID", messageId ); else super.updateMessageID(); } @Override protected void updateHeaders() throws MessagingException { super.updateHeaders(); } } } }