/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0 * * 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 org.apache.nifi.processors.email.smtp; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.InetSocketAddress; import java.net.SocketAddress; import java.security.cert.Certificate; import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import org.apache.commons.io.IOUtils; import org.apache.nifi.flowfile.FlowFile; import org.apache.nifi.flowfile.attributes.CoreAttributes; import org.apache.nifi.logging.ComponentLog; import org.apache.nifi.processor.ProcessSession; import org.apache.nifi.processor.ProcessSessionFactory; import org.apache.nifi.processor.exception.FlowFileAccessException; import org.apache.nifi.processors.email.ListenSMTP; import org.apache.nifi.stream.io.LimitingInputStream; import org.apache.nifi.util.StopWatch; import org.subethamail.smtp.MessageContext; import org.subethamail.smtp.MessageHandler; import org.subethamail.smtp.RejectException; import org.subethamail.smtp.TooMuchDataException; import org.subethamail.smtp.server.SMTPServer; /** * A simple consumer that provides a bridge between 'push' message distribution * provided by {@link SMTPServer} and NiFi polling scheduler mechanism. */ public class SmtpConsumer implements MessageHandler { private String from = null; private final List<String> recipientList = new ArrayList<>(); private final MessageContext context; private final ProcessSessionFactory sessionFactory; private final int port; private final int maxMessageSize; private final ComponentLog log; private final String host; public SmtpConsumer( final MessageContext context, final ProcessSessionFactory sessionFactory, final int port, final String host, final ComponentLog log, final int maxMessageSize ) { this.context = context; this.sessionFactory = sessionFactory; this.port = port; if (host == null || host.trim().isEmpty()) { this.host = context.getSMTPServer().getHostName(); } else { this.host = host; } this.log = log; this.maxMessageSize = maxMessageSize; } String getFrom() { return from; } List<String> getRecipients() { return Collections.unmodifiableList(recipientList); } @Override public void data(final InputStream data) throws RejectException, TooMuchDataException, IOException { final ProcessSession processSession = sessionFactory.createSession(); final StopWatch watch = new StopWatch(); watch.start(); try { FlowFile flowFile = processSession.create(); final AtomicBoolean limitExceeded = new AtomicBoolean(false); flowFile = processSession.write(flowFile, (OutputStream out) -> { final LimitingInputStream lis = new LimitingInputStream(data, maxMessageSize); IOUtils.copy(lis, out); if (lis.hasReachedLimit()) { limitExceeded.set(true); } }); if (limitExceeded.get()) { throw new TooMuchDataException("Maximum message size limit reached - client must send smaller messages"); } flowFile = processSession.putAllAttributes(flowFile, extractMessageAttributes()); watch.stop(); processSession.getProvenanceReporter().receive(flowFile, "smtp://" + host + ":" + port + "/", watch.getDuration(TimeUnit.MILLISECONDS)); processSession.transfer(flowFile, ListenSMTP.REL_SUCCESS); processSession.commit(); } catch (FlowFileAccessException | IllegalStateException | RejectException | IOException ex) { log.error("Unable to fully process input due to " + ex.getMessage(), ex); throw ex; } finally { processSession.rollback(); //make sure this happens no matter what - is safe } } @Override public void from(final String from) throws RejectException { this.from = from; } @Override public void recipient(final String recipient) throws RejectException { if (recipient != null && recipient.length() < 100 && recipientList.size() < 100) { recipientList.add(recipient); } } @Override public void done() { } private Map<String, String> extractMessageAttributes() { final Map<String, String> attributes = new HashMap<>(); final Certificate[] tlsPeerCertificates = context.getTlsPeerCertificates(); if (tlsPeerCertificates != null) { for (int i = 0; i < tlsPeerCertificates.length; i++) { if (tlsPeerCertificates[i] instanceof X509Certificate) { X509Certificate x509Cert = (X509Certificate) tlsPeerCertificates[i]; attributes.put("smtp.certificate." + i + ".serial", x509Cert.getSerialNumber().toString()); attributes.put("smtp.certificate." + i + ".subjectName", x509Cert.getSubjectDN().getName()); } } } SocketAddress address = context.getRemoteAddress(); if (address != null) { // will extract and format source address if available String strAddress = address instanceof InetSocketAddress ? ((InetSocketAddress) address).getHostString() + ":" + ((InetSocketAddress) address).getPort() : context.getRemoteAddress().toString(); attributes.put("smtp.src", strAddress); } attributes.put("smtp.helo", context.getHelo()); attributes.put("smtp.from", from); for (int i = 0; i < recipientList.size(); i++) { attributes.put("smtp.recipient." + i, recipientList.get(i)); } attributes.put(CoreAttributes.MIME_TYPE.key(), "message/rfc822"); return attributes; } }